AI가 책 목록에 제목 검색을 추가했다고 합니다. git diff에는 import와 몇 줄만 바뀌었고 검색 구현은 보이지 않습니다. 제안된 코드는 카탈로그에 있는 책도 찾지 못합니다. 정확성을 판단하기 전에 Git이 무엇을 비교했는지 확인하세요.

Git diff 예제에는 책 여섯 권을 다루는 앱과 이 튜토리얼을 위해 작성한 패치 및 테스트가 있습니다. Node.js 22 이상과 Git이 필요하며 의존성 설치나 네트워크 연결은 필요하지 않습니다.

줄을 읽기 전에 두 상태 선택

Git에는 작업 트리, 스테이징 스냅샷인 인덱스 및 커밋된 스냅샷이 있습니다.

명령 비교 범위
git diff 인덱스 → 작업 트리. 추적 파일의 스테이징하지 않은 변경
git diff --staged HEAD → 인덱스. 다음 커밋에 포함할 내용
git diff HEAD HEAD → 작업 트리. 스테이징 여부와 관계없는 추적 파일의 순변경
git diff base HEAD base와 HEAD라는 두 커밋 스냅샷
git diff base...HEAD 공통 조상 → HEAD. 분기 이후 대상 브랜치 변경

일반 미추적 파일은 처음 세 비교에서 빠집니다. 파일 하나에 스테이징한 편집과 하지 않은 편집이 동시에 있을 수도 있습니다. Git diff 문서를 참고하세요.

Diff는 작성자가 아닌 상태를 설명합니다. 이전 수동 편집과 AI 편집이 섞이면 패치만으로 누가 쓴 줄인지 알 수 없습니다. 필요하면 작업 전 상태나 커밋 ID를 보관하세요. 브랜치 비교도 기준을 바꾸면 질문이 바뀝니다.

일반 diff에서 빠진 파일 찾기

압축을 풀고 git-diff-reading-lab에서 임시 저장소를 만드세요.

node scripts/create-workspace.mjs review-workspace
cd review-workspace
git status --short --untracked-files=all

설정 스크립트는 기존 대상을 거부합니다. 원래 목록 코드를 커밋하여 base 태그를 붙이고 제안된 변경은 커밋하지 않습니다. 기존 저장소를 건드리지 않고 재현할 수 있습니다.

 M src/list.mjs
?? src/search.mjs
?? tests/search.test.mjs

충돌 없는 일반 상태에서 첫 열은 인덱스, 둘째는 작업 트리입니다. M의 앞 공백은 중요합니다. list.mjs가 수정됐지만 스테이징되지 않았습니다. ??는 미추적 파일입니다. Git status 짧은 형식에 정의되어 있습니다.

이제 실행하세요.

git diff --name-status
git diff --stat
git diff -- src/list.mjs

src/list.mjs만 보입니다. src/search.mjs와 tests/search.test.mjs는 편집기에서 따로 읽으세요. macOS나 Linux에서는 git diff --no-index -- /dev/null src/search.mjs로 새 소스를 추가로 표시할 수도 있습니다. 종료 코드 1은 파일이 다르다는 뜻입니다. 다운로드의 patches/review.patch에는 세 파일이 모두 있지만 이 상태의 일반 diff에는 없습니다.

아무것도 스테이징하지 않아 git diff --staged는 비어 있습니다. 제안이 커밋되지 않았고 base가 여전히 HEAD여서 git diff base...HEAD도 비어 있습니다. 어느 명령도 작업 중인 제안을 검토하지 않았습니다.

완전한 변경 구간 읽기

제공된 패치의 list.mjs 부분입니다.

diff --git a/src/list.mjs b/src/list.mjs
index f9ed85e..b551297 100644
--- a/src/list.mjs
+++ b/src/list.mjs
@@ -1,6 +1,8 @@
 import { pageBooks } from "./page.mjs";
+import { searchBooks } from "./search.mjs";
 // Keep the caller's array unchanged.
-export function listBooks(books, { page = 1, pageSize = 2 } = {}) {
+export function listBooks(books, { page = 1, pageSize = 2, query = "" } = {}) {
   const sorted = [...books].sort((a, b) => a.title.localeCompare(b.title, "en"));
-  return pageBooks(sorted, { page, pageSize });
+  const currentPage = pageBooks(sorted, { page, pageSize });
+  return searchBooks(currentPage, query);
 }

함수 전에 헤더를 읽으세요.

  • a/와 b/는 이전과 이후 측면이며 브랜치 이름이 아닙니다.
  • index f9ed85e..b551297은 축약된 파일 내용 객체 ID입니다. 커밋 ID가 아닙니다. 100644는 실행 권한 없는 일반 파일 모드가 유지됨을 뜻합니다.
  • ---는 이전 파일, +++는 새 파일입니다. review.patch의 다음 파일처럼 새 파일의 이전 측면은 /dev/null입니다.
  • @@ -1,6 +1,8 @@은 이전 1행부터 6줄, 새 1행부터 8줄을 뜻합니다. 삭제·추가 수가 아니라 범위입니다.
  • 구간 안에서 공백은 유지된 문맥, -는 삭제, +는 추가입니다. 이전 범위는 문맥 4줄과 삭제 2줄, 새 범위는 문맥 4줄과 추가 4줄입니다. 유지된 주석도 문맥입니다.

Git 패치 형식 문서를 참고하세요. 다운로드의 6줄 기준 파일과 8줄 제안 파일로 계산을 확인할 수 있습니다. 이는 일반 양측 비교이며 결합 병합 Diff는 형식이 다릅니다.

새 호출의 입력 따라가기

요구 사항은 전체 카탈로그에서 대소문자 구분 없이 제목을 검색한 다음 결과를 페이지로 나누는 것입니다. 기존 제목 순서와 입력은 유지해야 합니다.

새 도우미는 받은 목록을 필터링합니다.

export function searchBooks(books, query) {
  const needle = query.toLowerCase();
  return books.filter((book) => book.title.toLowerCase().includes(needle));
}

도우미가 올바르게 작동해도 호출자가 틀린 목록을 전달할 수 있습니다. 바뀌지 않은 src/page.mjs는 정렬된 책을 한 페이지로 자릅니다. 제안된 구간에서 currentPage는 이미 잘린 결과로 searchBooks에 전달됩니다.

페이지 크기 2에서 원래 첫 페이지는 Amber Atlas와 Moon Garden입니다. Star Notes는 뒤에 있습니다. 첫 페이지에서 sTaR를 검색하면 []지만, 요구한 첫 검색 결과에는 b5가 있어야 합니다. patches/fix.patch는 sorted를 먼저 검색하고 결과를 pageBooks에 넘깁니다.

검증할 동작은 이것입니다. 사용처가 많은 변경은 코드 변경 영향 분석으로 이어가세요. 변경 파일 목록만으로 사용처를 모두 알 수 없습니다.

통과한 테스트가 실제로 호출한 것 확인

review-workspace에서 예제 최상위로 돌아와 명령을 각각 실행하세요.

cd ..
node scripts/run-tests.mjs proposed author
node scripts/run-tests.mjs proposed requirement
node scripts/run-tests.mjs fixed requirement
node scripts/example.mjs

제안 작성자의 테스트는 통과합니다. 전체 책 두 권을 searchBooks에 전달하여 대소문자가 섞인 검색어를 확인하지만 listBooks를 호출하지 않아 검색 전 페이지 나누기를 검사하지 않습니다. 기존 단언문이 삭제되거나 약해지지는 않았습니다.

같은 공개 목록 요구 세 개를 각 버전에 실행합니다. 기존의 정렬, 페이지 나누기 및 입력 불변 검사 두 개도 있습니다.

버전 기존 회귀 검사 새 공개 목록 요구 요구 검사 종료 코드
base: 검색 없음 2개 통과 0개 통과, 3개 실패 1
proposed: 페이지 나눈 뒤 검색 2개 통과 1개 통과, 2개 실패 1
fixed: 검색 후 페이지 나누기 2개 통과 3개 통과 0

node scripts/run-tests.mjs base requirement에서 버전과 테스트 이름을 바꾸면 다른 행도 재현할 수 있습니다. 기준 실패는 새 기능이 없다는 뜻이지 기존 목록의 회귀가 아닙니다. 제안은 전체 결과 페이지와 원래 첫 페이지 밖의 일치 항목에서 실패합니다. 실패 테스트는 실제로 1로 종료하므로 &&로 연결하면 뒤 명령이 멈춥니다.

scripts/example.mjs는 버전마다 JSON 하나를 출력합니다. results의 sTaR 값은 기준에서 ["b1","b2"], 제안에서 [], 수정에서 ["b5"]입니다. 검색 순서에 대한 좁은 검사이며 운영용 검색 전체를 보증하지 않습니다. 단언문도 바뀌었다면 AI 생성 테스트 검토를 참고하세요.

빈 diff에도 검토할 패치가 남는 이유

세 파일을 읽은 뒤 임시 예제 저장소에서만 스테이징하세요.

cd review-workspace
git add -- src/list.mjs src/search.mjs tests/search.test.mjs
git diff
git diff --staged --stat
git diff --staged

작업 파일과 인덱스가 같아 일반 diff는 비어 있습니다. 스테이징된 diff에는 새 도우미와 테스트를 포함한 세 파일이 모두 있습니다. 예제의 인덱스만 변경하며 커밋하지 않습니다. 실제 검토에서는 diff에 보이게 하려고 무관한 파일을 스테이징하지 마세요.

커밋된 두 버전으로 제안 비교

Mac용 Yenpo Git Diff 뷰어에서 쓰려면 두 버전이 커밋된 별도 연습 저장소를 만드세요. review-workspace에 있다면 cd ..로 git-diff-reading-lab으로 돌아와 실행하세요.

node scripts/create-workspace.mjs committed-workspace --committed
cd committed-workspace
git status --short --untracked-files=all
git rev-parse base
git rev-parse proposal
git diff base proposal --name-status
git diff base proposal

새 모드는 원래 목록을 base로 커밋하고 제안된 세 변경을 복사·스테이징하여 proposal 태그의 두 번째 커밋을 만듭니다. 실제 ID를 출력합니다. proposal은 HEAD, base는 부모입니다. 앞의 review-workspace는 그대로 둡니다.

상태 명령은 아무것도 출력하지 않아야 합니다. 둘 다 커밋되어 일반 git diff와 git diff --staged도 비어 있지만 명시적인 base → proposal 비교에는 수정된 src/list.mjs, 추가된 src/search.mjs와 tests/search.test.mjs가 있습니다. 바뀌지 않은 src/page.mjs도 여세요. 목록 함수는 여전히 페이지로 자른 일부를 검색에 전달합니다.

이 커밋들은 versions/base와 versions/proposed와 같은 소스입니다. 앞의 sTaR 결과는 제안 전 ["b1","b2"], 이후 []이며 요구 결과는 ["b5"]입니다. 커밋해도 검색 순서 문제는 남습니다. 별도의 fixed 스냅샷과 통과 검사는 예제에 남지만 이 모드는 fixed 커밋을 만들지 않습니다.

설정은 새 하위 디렉터리만 허용하고 기존 파일, 폴더 및 심볼릭 링크는 거부합니다. 가상의 학습용 작성자와 고정 날짜를 사용하고 상속된 Git 작성자·설정을 무시하며 새 연습 저장소 안에만 설정을 씁니다. 전역 작성자나 다른 저장소는 바꾸지 않습니다.

Yenpo에서 committed-workspace를 여세요. 출력된 두 ID를 Git 비교 가이드와 대조하세요. 이전은 base, 이후는 proposal입니다. 이 비교는 커밋된 스냅샷만 다루며 스테이징 여부와 관계없이 커밋하지 않은 편집은 포함하지 않고 AI 대화 한 턴의 변경도 식별하지 않습니다.