AI 도우미가 작은 줄 읽기 함수에서 검사를 지우자고 제안합니다. “호출자가 이미 유효한 줄 번호를 전달합니다.” 함수는 단순해 보입니다. 변경을 수락하기 전에 Git 이력을 확인했더니 해당 줄이 “Compact comparison spacing”이라는 커밋을 가리킵니다.

다음에 살펴볼 곳을 찾은 것입니다. 중요한 질문은 이 검사가 어떤 입력을 막는지, 그 동작이 언제 생겼는지, 지금의 호출자도 여전히 필요한지입니다. 직접 재구성할 수 있는 작은 이력으로 구체적으로 확인해 보겠습니다.

의심스러운 줄에서 시작하세요

소스 뷰어가 1부터 시작하는 줄 번호를 사용한다고 가정해 보겠습니다. 현재 함수는 다음과 같습니다.

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을 사용하며 macOS API는 사용하지 않습니다. 기록된 실행 환경은 macOS의 Git 2.50.1과 Python 3.9.6입니다. Windows와 Linux는 별도로 테스트하지 않았습니다.

스크립트는 새 로컬 저장소를 만들고 검사를 실행한 뒤 history-check에 결과를 저장합니다. 대상이 이미 있으면 거부하고, 자신이 만드는 커밋에만 가상의 작성자를 사용하며, 전역 Git 설정을 바꾸지 않습니다. 이름, 날짜, 커밋 메시지와 뷰어 상황은 모두 학습용 fixture입니다.

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로 바꾸고 테스트 import 수정
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 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는 문자열 출현 횟수가 바뀐 변경을 선택합니다. 일치하는 부분 주변의 모든 수정을 선택하지는 않습니다. Git의 pickaxe 설명을 참고하세요.

포맷 작업에서 바뀐 문자열로 시도해 보세요.

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의 원래 인덱싱 식까지 도달합니다. 실습은 .gitattributes에 *.py diff=python을 선언해 Git이 Python 함수 헤더 규칙을 사용하게 합니다. 함수 인식은 언어와 diff 설정에 따라 다릅니다. 사용자 지정 hunk 헤더 정의를 참고하세요. 이 파일에서 숫자 범위를 사용하는 대안은 다음과 같습니다.

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

패치는 하한 검사와 0, -1에서 ValueError를 기대하는 테스트를 추가합니다. 실습 검증기는 커밋된 소스와 테스트를 임시 폴더로 추출합니다. 같은 회귀 테스트를 기준 구현에 실행한 뒤 각 커밋의 자체 테스트도 실행합니다. 체크아웃은 전환하지 않습니다.

기록된 실행의 관찰 뒷받침하는 내용
기준 구현은 0에서 omega, -1에서 alpha 반환 이 입력이 Python 음수 인덱싱에 도달함
수정 커밋의 테스트는 기준 구현에서 두 입력 모두 실패 추가 검증문이 기존 동작을 감지함
수정, 이름 변경, 공백 수정 버전은 각각 다섯 테스트 통과 테스트한 사례가 수정 이후 의도대로 동작함
이름 변경은 소스 바이트 유지, 공백 수정은 구문 트리 유지 이후 수정이 이 함수의 구현을 보존함

기준 버전의 원래 테스트 세 개도 통과합니다. 통과하는 테스트 모음이 관심 있는 입력을 빠뜨릴 수 있습니다. 전체 기대 결과에 검사가 설명되어 있으며, 자신의 history-check/transcript.txt에는 예상된 두 실패를 포함한 실제 명령과 출력이 있습니다.

예제 커밋 메시지는 0번 줄을 요청하면 마지막 줄이 반환된다고 설명하고, 테스트는 그 함수 동작을 재현합니다. 자신의 프로젝트에서 검사를 삭제하기 전에 현재 호출자가 0과 음수 줄 번호를 어떻게 거부하는지 확인하세요. 이 예제는 정수 입력을 가정하며 다른 타입은 별도 검증이 필요합니다.

AI에게 확인 가능한 질문을 하세요

리비전 ID, 패치, 테스트 출력이 준비되면 도우미에게 연결해 달라고 요청하세요.

이 하한 검사가 있는 이유를 설명해 주세요. 제공한 수정 커밋과 부모를 사용해 0과 -1의 결과를 보여 주고 회귀 검증문을 찾아 주세요. diff와 실행한 테스트가 보여 주는 사실을 커밋 메시지의 주장과 구분하세요. 삭제를 추천하기 전에 현재 호출자를 살펴보세요. 호출자나 과거 보고서가 없다면 어떤 결론이 미확정인지 밝혀 주세요.

유용한 답변에는 동작 변경, 보고된 동기, 현재 호출자에 관한 미확인 사항이 들어 있습니다. 메시지만 근거로 실제 사고가 있었다고 단정하면 주장을 좁히도록 요청하세요. 검사를 삭제하자고 하면 지원하는 모든 진입 경로가 실패 입력을 어떻게 배제하는지 보여 달라고 하세요. 호출자 조사는 낯선 코드에서 하나의 값 따라가기로 이어갈 수 있습니다. AI가 작성한 패치 검토하기에서는 제안된 설명을 집중된 회귀 테스트로 바꾸는 방법을 보여 줍니다.