ИИ-помощник предлагает удалить проверку из небольшой функции чтения строк. «Вызывающий код уже передаёт допустимые номера строк», — говорит он. Функция действительно выглядит простой. Перед принятием изменения вы смотрите историю Git, а строка указывает на коммит «Compact comparison spacing».

Это даёт следующую зацепку. Важно выяснить, от какого входа защищает проверка, когда появилось это поведение и нужна ли она нынешним вызывающим функциям. Небольшая воспроизводимая история делает вопросы конкретными.

Начните с подозрительной строки

Предположим, просмотрщик исходников нумерует строки с единицы. Текущая функция выглядит так:

def read_line(lines, line_number):
    """Read a source line using a one-based line number."""
    if line_number<1:
        raise ValueError("line number must be positive")
    if line_number > len(lines):
        raise IndexError("line number is past the end")
    return lines[line_number - 1]

Для двух строк ["alpha", "omega"] строка 1 должна вернуть alpha, а строка 2 — omega. Но в Python индекс -1 читает последний элемент. Без нижней проверки номер строки 0 становится индексом -1 и возвращает omega. Номер -1 становится индексом -2 и возвращает alpha.

Это можно объяснить текущим кодом. История отвечает на другой вопрос: когда отклонение этих входов стало частью функции и что вошло в изменение?

Воссоздайте четыре коммита

Скачайте git-history-lab.py в пустой рабочий каталог. Нужны Git 2.29+ и Python 3.9+; зависимостей пакетов нет. Выполните в своём терминале:

python3 git-history-lab.py create history-lab --record history-check
cd history-lab
git log --reverse --format='%h %s' HEAD

В Windows при необходимости замените python3 своим запуском Python 3. Процесс использует Git и Python без API macOS. Зафиксированный запуск выполнен с Git 2.50.1 и Python 3.9.6 в macOS; Windows и Linux отдельно не проверялись.

Скрипт создаёт новый локальный репозиторий, выполняет проверки и сохраняет вывод в history-check. Он отказывается работать с существующим целевым каталогом, использует вымышленного автора только для своих коммитов и не меняет глобальную конфигурацию Git. Все имена, даты, сообщения коммитов и сценарий просмотрщика — учебные фикстуры.

ce4c0cf Add one-based source line lookup
e87088e Reject nonpositive source line numbers
7d41ebc Rename line reader module
27a0dc5 Compact comparison spacing

У каждого коммита есть читаемый тег:

Тег Изменение
baseline Добавляет line_reader.py; тесты проверяют первую, последнюю строку и вход за концом списка
fix-positive-lines Добавляет нижнюю проверку и тесты для 0, -1 и пустого списка
rename-module Переименовывает файл в source_lines.py и обновляет импорт теста
format-spacing Меняет line_number < 1 на line_number<1

Уплотнённая запись намеренно заметна, чтобы последнюю правку было легко узнать.

Пусть blame укажет следующий diff

Изучите строку 3:

git blame -L 3,3 HEAD -- source_lines.py
git show --format=fuller format-spacing -- source_lines.py

Blame относит строку к 27a0dc57, коммиту изменения пробелов. Его патч содержит:

-    if line_number < 1:
+    if line_number<1:

Теперь попросите Git игнорировать пробелы при отслеживании строки:

git blame -w -L 3,4 HEAD -- source_lines.py

Результат указывает на e87088e0 и старый путь line_reader.py. Git blame автоматически следует за переименованием целого файла; -w игнорирует пробелы при атрибуции. Здесь вводящий в заблуждение результат вызвало не переименование, а последующая правка пробелов. См. официальную документацию git-blame.

Для этой конкретной правки проверяющий скрипт также сравнивает синтаксические деревья Python и запускает тесты до и после. Оба результата совпадают. Игнорирование пробелов помогает исследовать авторство строк, но не проверяет поведение: отступы и пробелы внутри строк в Python могут быть значимы.

После изучения известного форматирующего коммита его можно явно исключить:

git blame --ignore-rev format-spacing -L 3,3 HEAD -- source_lines.py

В этом примере так тоже находится исправление. Не исключайте коммит только из-за слова «format» в заголовке; сначала прочитайте патч. Blame фиксирует принадлежность строк, а не рассуждения разработчика или ответственность за ошибку.

Ищите текст, переживший переработку

Сообщение об ошибке не изменилось при переименовании и форматировании. Найдите его появление:

git log --format='%h %s' -p -S 'line number must be positive' HEAD -- '*.py'

Будет найдено исправление и его патч в line_reader.py. Кавычки вокруг *.py оставляют сопоставление путей Git и включают старое имя файла. В репозитории только один коммит добавляет или удаляет это сообщение.

-S выбирает изменения числа вхождений строки. Он не выбирает каждую правку рядом с совпадением. См. объяснение pickaxe в Git.

Попробуйте строку, которая изменилась при форматировании:

git log --format='%h %s' -p -S 'line_number < 1' HEAD -- '*.py'

Теперь вывод включает и коммит пробелов, удаляющий точное написание, и исправление, добавляющее его. Дополнительное совпадение полезно: прочитайте изменение и при необходимости выберите устойчивый фрагмент. Отсутствие результата тоже нужно объяснять: текст может меняться при неизменном числе вхождений.

Для известного файла в этом примере работает и такой вариант:

git log --follow --format='%h %s' -p -S 'line number must be positive' HEAD -- source_lines.py

--follow обрабатывает историю переименований одного файла. Это не общий поиск по файлам. В реальном исследовании проверяйте старые пути или расширяйте их выбор, если узкий поиск заканчивается слишком рано.

Проследите строки во времени с log -L

Если нужный диапазон известен, запросите его патчи в истории:

git log --format='%h %s' -L 3,4:source_lines.py HEAD

Здесь вывод включает изменение пробелов и первоначальное добавление проверки. Он проходит через чистое переименование файла, но для неизменного переименованного блока отдельного патча нет. Для полной последовательности используйте предыдущий журнал четырёх коммитов.

Для всей функции выполните:

git log --format='%h %s' -L :read_line:source_lines.py HEAD

Так находится и исходное выражение индексации в baseline. Проект задаёт *.py diff=python в .gitattributes, чтобы Git использовал правила заголовков функций Python. Распознавание зависит от языка и конфигурации diff; см. определение пользовательского заголовка блока. Числовой вариант для файла:

git log --format='%h %s' -L 1,7:source_lines.py HEAD

В log -L путь входит в аргумент диапазона; не добавляйте ограничитель -- path. Диапазон должен существовать в начальной ревизии, а обход начинается с одной ревизии. Ограничения указаны в официальной документации git-log.

Реальная история может содержать слияния, скопированный код, переписанные функции или отсутствующие старые коммиты в поверхностном клоне. Трассировка строк — отправная точка для чтения патчей; она не восстановит причины, которые никто не записал.

Проверьте объяснение на обеих версиях

Прочитайте вместе реализацию и тесты найденного исправления:

git show --format=fuller fix-positive-lines -- line_reader.py test_lines.py

Патч добавляет нижнюю проверку и тест, ожидающий ValueError для 0 и -1. Проверяющий скрипт извлекает закоммиченные исходники и тесты во временные каталоги. Он запускает одну регрессионную серию на базовой реализации, затем собственные тесты каждого коммита. Ваша рабочая копия не переключается.

Наблюдение зафиксированного запуска Что оно подтверждает
База возвращает omega для 0 и alpha для -1 Эти входы достигают отрицательной индексации Python
Тесты исправления падают на базе для обоих входов Новые проверки обнаруживают старое поведение
Версии исправления, переименования и форматирования проходят по пять тестов Проверенные случаи после исправления ведут себя ожидаемо
Переименование сохраняет байты исходника, форматирование — синтаксическое дерево Последующие правки сохраняют реализацию функции

Три исходных теста базы тоже проходят. Успешная серия может просто не включать интересующий вход. Полные ожидаемые результаты описывают проверки; ваш history-check/transcript.txt содержит фактические команды и вывод, включая два ожидаемых падения.

В учебном сообщении коммита описано, как нулевая строка возвращает последнюю; тесты воспроизводят это поведение функции. Прежде чем удалять проверку в своём проекте, выясните, как текущие вызовы отклоняют ноль и отрицательные номера. Пример предполагает целые числа; другие типы требуют отдельной проверки.

Задайте ИИ проверяемый вопрос

Получив ревизии, патч и вывод тестов, попросите помощника связать их:

Объясни, зачем существует нижняя проверка. Используй предоставленное исправление и его родительский коммит, покажи результаты для 0 и -1 и укажи регрессионные проверки. Раздели то, что показывают diff и выполненные тесты, и то, что утверждает сообщение коммита. Затем изучи нынешние вызовы, прежде чем рекомендовать удаление. Если вызовы или исторические отчёты недоступны, назови незакрытый вывод.

Полезный ответ включает три части: изменение поведения, описанную мотивацию и неизвестное о нынешних вызовах. Если он утверждает факт инцидента лишь по сообщению коммита, попросите сузить утверждение. Если предлагает удалить проверку, попросите показать, как каждый поддерживаемый путь входа исключает ошибочные значения. Трассировка значения в незнакомом коде поможет изучить вызовы, а проверка патча ИИ — превратить объяснение в целевой регрессионный тест.