ИИ-помощник предлагает удалить проверку из небольшой функции чтения строк. «Вызывающий код уже передаёт допустимые номера строк», — говорит он. Функция действительно выглядит простой. Перед принятием изменения вы смотрите историю 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 и выполненные тесты, и то, что утверждает сообщение коммита. Затем изучи нынешние вызовы, прежде чем рекомендовать удаление. Если вызовы или исторические отчёты недоступны, назови незакрытый вывод.
Полезный ответ включает три части: изменение поведения, описанную мотивацию и неизвестное о нынешних вызовах. Если он утверждает факт инцидента лишь по сообщению коммита, попросите сузить утверждение. Если предлагает удалить проверку, попросите показать, как каждый поддерживаемый путь входа исключает ошибочные значения. Трассировка значения в незнакомом коде поможет изучить вызовы, а проверка патча ИИ — превратить объяснение в целевой регрессионный тест.