Skip to content

Commit c77603d

Browse files
committed
checkpoints for nested git repos
1 parent d890b4b commit c77603d

File tree

2 files changed

+125
-8
lines changed

2 files changed

+125
-8
lines changed

npm-app/src/checkpoints/checkpoint-manager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import { AgentState, ToolResult } from 'common/types/agent-state'
88
import { blue, bold, cyan, gray, red, underline, yellow } from 'picocolors'
99

1010
import { getProjectRoot } from '../project-files'
11+
import { gitCommandIsAvailable } from '../utils/git'
12+
import { logger } from '../utils/logger'
1113
import {
1214
getBareRepoPath,
1315
getLatestCommit,
1416
hasUnsavedChanges,
1517
} from './file-manager'
16-
import { logger } from '../utils/logger'
1718

1819
export class CheckpointsDisabledError extends Error {
1920
constructor(message?: string, options?: ErrorOptions) {
@@ -167,6 +168,11 @@ export class CheckpointManager {
167168
throw new CheckpointsDisabledError(this.disabledReason)
168169
}
169170

171+
if (!gitCommandIsAvailable()) {
172+
this.disabledReason = 'Git required for checkpoints'
173+
throw new CheckpointsDisabledError(this.disabledReason)
174+
}
175+
170176
const id = this.checkpoints.length + 1
171177
const projectDir = getProjectRoot()
172178
if (projectDir === os.homedir()) {

npm-app/src/checkpoints/file-manager.ts

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { execFileSync } from 'child_process'
22
import { createHash } from 'crypto'
33
import fs from 'fs'
44
import os from 'os'
5-
import { join } from 'path'
5+
import path, { join } from 'path'
66

77
import {
88
add,
@@ -15,6 +15,7 @@ import {
1515
statusMatrix,
1616
} from 'isomorphic-git'
1717

18+
import { buildArray } from 'common/util/array'
1819
import { getProjectDataDir } from '../project-files'
1920
import { gitCommandIsAvailable } from '../utils/git'
2021
import { logger } from '../utils/logger'
@@ -61,12 +62,14 @@ export async function hasUnsavedChanges({
6162
],
6263
{ stdio: ['ignore', 'pipe', 'ignore'] }
6364
).toString()
64-
return output.trim().length > 0
65+
return (
66+
buildArray(output.split('\n').filter((line) => !line.startsWith(' M ')))
67+
.length > 0
68+
)
6569
} catch (error) {
6670
logger.error(
6771
{
68-
errorMessage: error instanceof Error ? error.message : String(error),
69-
errorStack: error instanceof Error ? error.stack : undefined,
72+
error,
7073
projectDir,
7174
bareRepoPath,
7275
},
@@ -200,6 +203,8 @@ async function gitAddAll({
200203
projectDir,
201204
'add',
202205
'.',
206+
':!**/*.codebuffbackup',
207+
':!**/*.codebuffbackup/**',
203208
],
204209
{ stdio: 'ignore' }
205210
)
@@ -271,6 +276,113 @@ async function gitAddAll({
271276
}
272277
}
273278

279+
async function gitAddAllIgnoringNestedRepos({
280+
projectDir,
281+
bareRepoPath,
282+
relativeFilepaths,
283+
}: {
284+
projectDir: string
285+
bareRepoPath: string
286+
relativeFilepaths: Array<string>
287+
}): Promise<void> {
288+
const allNestedRepos: string[] = []
289+
try {
290+
while (true) {
291+
let output: string
292+
try {
293+
output = execFileSync(
294+
'git',
295+
[
296+
'--git-dir',
297+
bareRepoPath,
298+
'--work-tree',
299+
projectDir,
300+
'status',
301+
'--porcelain',
302+
],
303+
{ stdio: ['ignore', 'pipe', 'ignore'] }
304+
).toString()
305+
} catch (error) {
306+
logger.error(
307+
{ error, projectDir, bareRepoPath },
308+
'Failed to get git status while finding nested git repos'
309+
)
310+
return
311+
}
312+
313+
if (!output) {
314+
break
315+
}
316+
317+
const nestedRepos = buildArray(output.split('\n'))
318+
.filter((line) => line[1] === 'M')
319+
.map((line) => line.slice(3).trim())
320+
321+
await gitAddAll({ projectDir, bareRepoPath, relativeFilepaths })
322+
323+
if (nestedRepos.length === 0) {
324+
break
325+
}
326+
327+
for (const nestedRepo of nestedRepos) {
328+
try {
329+
fs.renameSync(
330+
path.join(projectDir, nestedRepo, '.git'),
331+
path.join(projectDir, nestedRepo, '.git.codebuffbackup')
332+
)
333+
allNestedRepos.push(nestedRepo)
334+
} catch (error) {
335+
logger.error(
336+
{
337+
error,
338+
nestedRepo,
339+
},
340+
'Failed to backup .git directory for nested repo'
341+
)
342+
}
343+
}
344+
345+
execFileSync('git', [
346+
'--git-dir',
347+
bareRepoPath,
348+
'--work-tree',
349+
projectDir,
350+
'-C',
351+
projectDir,
352+
'rm',
353+
'--cached',
354+
'-rf',
355+
...nestedRepos,
356+
])
357+
}
358+
} finally {
359+
for (const nestedRepo of allNestedRepos) {
360+
const codebuffBackup = path.join(
361+
projectDir,
362+
nestedRepo,
363+
'.git.codebuffbackup'
364+
)
365+
const gitDir = path.join(projectDir, nestedRepo, '.git')
366+
try {
367+
fs.renameSync(codebuffBackup, gitDir)
368+
} catch (error) {
369+
console.error(
370+
`Failed to restore .git directory for nested repo. Please rename ${codebuffBackup} to ${gitDir}\n${{ error }}`
371+
)
372+
logger.error(
373+
{
374+
errorMessage:
375+
error instanceof Error ? error.message : String(error),
376+
errorStack: error instanceof Error ? error.stack : undefined,
377+
nestedRepo,
378+
},
379+
'Failed to restore .git directory for nested repo'
380+
)
381+
}
382+
}
383+
}
384+
}
385+
274386
async function gitCommit({
275387
projectDir,
276388
bareRepoPath,
@@ -337,8 +449,7 @@ async function gitCommit({
337449
} catch (error) {
338450
logger.error(
339451
{
340-
errorMessage: error instanceof Error ? error.message : String(error),
341-
errorStack: error instanceof Error ? error.stack : undefined,
452+
error,
342453
projectDir,
343454
bareRepoPath,
344455
},
@@ -372,7 +483,7 @@ export async function storeFileState({
372483
message: string
373484
relativeFilepaths: Array<string>
374485
}): Promise<string> {
375-
await gitAddAll({
486+
await gitAddAllIgnoringNestedRepos({
376487
projectDir,
377488
bareRepoPath,
378489
relativeFilepaths,

0 commit comments

Comments
 (0)