spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / mods / diff / tests / git / fetch-hunks.test.ts
281 lines · 8.2 KBRaw
1import { describe, expect, test, tier } from 'claude-code/testing'
2
3import Git from '../../hooks/git'
4import Limits from '../../hooks/limits'
5import Fixtures from '../fixtures'
6
7tier('builtin')
8
9describe('fetch-hunks', () => {
10 type Answer = (asked: readonly string[], nth: number) => Git.RunResult
11
12 const rowOf = (path: string, kind: Partial<Git.FileStat> = {}) => ({
13 path,
14 renamedFrom: null,
15 added: 1,
16 removed: 0,
17 isBinary: false,
18 isUntracked: false,
19 isPreSession: false,
20 ...kind,
21 })
22
23 const dataOf = (
24 files: readonly Git.FileStat[],
25 basis: Partial<Git.DiffData> = {},
26 ): Git.DiffData => ({
27 repository: Fixtures.FETCH_REPOSITORY,
28 mode: 'uncommitted',
29 stats: { filesCount: files.length, linesAdded: 0, linesRemoved: 0 },
30 files,
31 source: { kind: 'working-tree', base: 'HEAD' },
32 isUnborn: false,
33 baseRef: 'HEAD',
34 stalePaths: [],
35 isUntrackedWithheld: false,
36 ...basis,
37 })
38
39 function gitOf(answer: Answer = Fixtures.hunksReadOf) {
40 const asks: (readonly string[])[] = []
41 const argvs: (readonly string[])[] = []
42
43 const run: Git.GitRun = argv => {
44 const asked = argv.slice(argv.indexOf('--') + 1)
45
46 argvs.push(argv)
47 asks.push(asked)
48
49 return Promise.resolve(answer(asked, asks.length))
50 }
51
52 return { run, asks, argvs }
53 }
54
55 const linesOf = (body: Git.FileHunks | null | undefined) =>
56 body?.hunks.flatMap(hunk => hunk.lines)
57
58 const FIFTY = Array.from({ length: Limits.MAX_FILES }, (_, at) =>
59 rowOf(`src/file${at + Limits.MAX_FILES}.ts`),
60 )
61
62 const LONG = ['a', 'b', 'c'].map(name =>
63 rowOf(name.repeat(Limits.MAX_PATHSPEC_CHARS / 2)),
64 )
65
66 test('fifty files are read by one child', async () => {
67 const git = gitOf()
68 const read = await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY)
69
70 expect(git.asks).toEqual([FIFTY.map(file => file.path)])
71 expect(read.size).toBe(Limits.MAX_FILES)
72
73 expect(
74 FIFTY.every(
75 file => linesOf(read.get(file.path))?.[1] === `+in ${file.path}`,
76 ),
77 ).toBe(true)
78 })
79
80 test('the child keeps every pin of the read of one file', async () => {
81 const git = gitOf()
82
83 await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY.slice(0, 1))
84
85 expect(git.argvs[0]?.slice(0, -1)).toEqual([
86 '--literal-pathspecs',
87 '--no-optional-locks',
88 '-c',
89 'diff.relative=false',
90 '-c',
91 'core.quotePath=false',
92 'diff',
93 '--no-ext-diff',
94 '--no-textconv',
95 '--ignore-submodules=dirty',
96 '--submodule=short',
97 '--no-renames',
98 '--src-prefix=a/',
99 '--dst-prefix=b/',
100 '--raw',
101 '-z',
102 '-p',
103 'HEAD',
104 '--',
105 ])
106 })
107
108 test('on an unborn HEAD the child reads what is staged', async () => {
109 const git = gitOf()
110 const files = [rowOf('edited-since.ts'), rowOf('staged.ts')]
111
112 const read = await Git.fetchHunks(
113 git.run,
114 dataOf(files, {
115 isUnborn: true,
116 baseRef: '--cached',
117 stalePaths: ['edited-since.ts'],
118 }),
119 files,
120 )
121
122 expect(git.argvs[0]?.slice(-3)).toEqual(['--cached', '--', 'staged.ts'])
123 expect(read.get('edited-since.ts')).toEqual(Git.EMPTY_FILE_HUNKS)
124 expect(linesOf(read.get('staged.ts'))).toEqual([' one', '+in staged.ts'])
125 })
126
127 test('rows with no body are never named to git', async () => {
128 const git = gitOf()
129
130 const files = [
131 rowOf('app.ts'),
132 rowOf('logo.png', { isBinary: true }),
133 rowOf('moved.ts', { renamedFrom: 'old.ts' }),
134 rowOf('new.ts', { isUntracked: true }),
135 ]
136
137 const read = await Git.fetchHunks(git.run, dataOf(files), files)
138
139 expect(git.asks).toEqual([['app.ts']])
140
141 expect(
142 ['logo.png', 'moved.ts', 'new.ts'].map(path => read.get(path)),
143 ).toEqual([
144 Git.EMPTY_FILE_HUNKS,
145 Git.EMPTY_FILE_HUNKS,
146 Git.EMPTY_FILE_HUNKS,
147 ])
148 })
149
150 test('rows with no body alone start no child', async () => {
151 const git = gitOf()
152 const files = [rowOf('new.ts', { isUntracked: true })]
153
154 await Git.fetchHunks(git.run, dataOf(files), files)
155
156 expect(git.asks).toEqual([])
157 })
158
159 test('every file but the last gains the closing line', async () => {
160 const git = gitOf()
161 const files = [rowOf('a.ts'), rowOf('b.ts')]
162 const read = await Git.fetchHunks(git.run, dataOf(files), files)
163
164 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+in a.ts', ' '])
165 expect(linesOf(read.get('b.ts'))).toEqual([' one', '+in b.ts'])
166 })
167
168 test('paths past the budget are asked of a second child', async () => {
169 const git = gitOf()
170 const read = await Git.fetchHunks(git.run, dataOf(LONG), LONG)
171 const paths = LONG.map(file => file.path)
172
173 expect(git.asks).toEqual([paths.slice(0, 2), paths.slice(2)])
174
175 expect(paths.map(path => linesOf(read.get(path))?.[1])).toEqual(
176 paths.map(path => `+in ${path}`),
177 )
178 })
179
180 test('one path past the budget is still asked, alone', async () => {
181 const git = gitOf()
182 const files = [rowOf('d'.repeat(Limits.MAX_PATHSPEC_CHARS + 1)), rowOf('e')]
183
184 await Git.fetchHunks(git.run, dataOf(files), files)
185
186 expect(git.asks.map(asked => asked.length)).toEqual([1, 1])
187 })
188
189 test('past the output cap the rest is asked again', async () => {
190 const files = [rowOf('a.ts'), rowOf('big.ts'), rowOf('c.ts'), rowOf('d.ts')]
191
192 const git = gitOf((asked, nth) => {
193 const isFirst = nth === 1
194
195 return isFirst
196 ? Fixtures.ok(
197 Fixtures.hunksAnswerOf([
198 ['a.ts', Fixtures.oneAddedOf('in a.ts')],
199 ['big.ts', Fixtures.longLineOf(Limits.HOST_OUTPUT_CAP_BYTES)],
200 ['c.ts', Fixtures.oneAddedOf('never arrived')],
201 ['d.ts', Fixtures.oneAddedOf('never arrived')],
202 ]).slice(0, Limits.HOST_OUTPUT_CAP_BYTES),
203 )
204 : Fixtures.hunksReadOf(asked)
205 })
206
207 const read = await Git.fetchHunks(git.run, dataOf(files), files)
208
209 expect(git.asks, 'the second list starts where the first stopped').toEqual([
210 ['a.ts', 'big.ts', 'c.ts', 'd.ts'],
211 ['c.ts', 'd.ts'],
212 ])
213
214 expect(linesOf(read.get('a.ts'))).toEqual([' one', '+in a.ts', ' '])
215 expect(read.get('big.ts')).toEqual(Git.LARGE_FILE_HUNKS)
216 expect(linesOf(read.get('c.ts'))).toEqual([' one', '+in c.ts', ' '])
217 expect(linesOf(read.get('d.ts'))).toEqual([' one', '+in d.ts'])
218 })
219
220 test('a child that fails leaves every file unread', async () => {
221 const git = gitOf(() => Fixtures.GIT_TIMED_OUT)
222 const read = await Git.fetchHunks(git.run, dataOf(FIFTY), FIFTY)
223
224 expect(git.asks).toHaveLength(1)
225 expect([...read.values()].every(body => body === null)).toBe(true)
226 expect(read.size).toBe(Limits.MAX_FILES)
227 })
228
229 test('a second child that fails keeps what the first read', async () => {
230 const git = gitOf((asked, nth) => {
231 const isFirst = nth === 1
232
233 return isFirst ? Fixtures.hunksReadOf(asked) : Fixtures.UNSCRIPTED
234 })
235
236 const read = await Git.fetchHunks(git.run, dataOf(LONG), LONG)
237 const [first, second, third] = LONG.map(file => read.get(file.path))
238
239 expect(git.asks).toHaveLength(2)
240 expect(linesOf(first)?.[0]).toBe(' one')
241 expect(linesOf(second)?.[0]).toBe(' one')
242 expect(third).toBeNull()
243 })
244
245 test('a second child is paired by its own list', async () => {
246 const git = gitOf((asked, nth) => {
247 const isFirst = nth === 1
248
249 return Fixtures.hunksReadOf(isFirst ? asked : asked.toReversed())
250 })
251
252 const files = [...LONG, rowOf('z.ts')]
253 const read = await Git.fetchHunks(git.run, dataOf(files), files)
254 const [, , third] = LONG.map(file => file.path)
255
256 expect(git.asks[1]).toEqual([third, 'z.ts'])
257 expect(linesOf(read.get(third ?? ''))?.[1]).toBe(`+in ${third}`)
258 expect(linesOf(read.get('z.ts'))).toEqual([' one', '+in z.ts'])
259 })
260
261 test('an answer whose names and patches disagree ends the read', async () => {
262 const git = gitOf(() =>
263 Fixtures.ok(
264 Fixtures.hunksAnswerOf([
265 ['a.ts', Fixtures.oneAddedOf('a'), 'diff --git a/o.ts b/o.ts'],
266 ['b.ts', Fixtures.oneAddedOf('b')],
267 ]),
268 ),
269 )
270
271 const files = [rowOf('a.ts'), rowOf('b.ts')]
272 const read = await Git.fetchHunks(git.run, dataOf(files), files)
273
274 expect(git.asks, 'asked once, never again').toHaveLength(1)
275
276 expect([...read]).toEqual([
277 ['a.ts', null],
278 ['b.ts', null],
279 ])
280 })
281})