@@ -20,12 +20,12 @@ export abstract class ShadowCheckpointService extends EventEmitter {
2020 public readonly checkpointsDir : string
2121 public readonly workspaceDir : string
2222
23+ protected configDir : string
24+ protected readonly log : ( message : string ) => void
25+
2326 protected _checkpoints : string [ ] = [ ]
2427 protected _baseHash ?: string
25-
26- protected readonly dotGitDir : string
2728 protected git ?: SimpleGit
28- protected readonly log : ( message : string ) => void
2929 protected shadowGitConfigWorktree ?: string
3030
3131 public get baseHash ( ) {
@@ -56,8 +56,8 @@ export abstract class ShadowCheckpointService extends EventEmitter {
5656 this . taskId = taskId
5757 this . checkpointsDir = checkpointsDir
5858 this . workspaceDir = workspaceDir
59+ this . configDir = path . join ( this . checkpointsDir , ".git" )
5960
60- this . dotGitDir = path . join ( this . checkpointsDir , ".git" )
6161 this . log = log
6262 }
6363
@@ -66,47 +66,16 @@ export abstract class ShadowCheckpointService extends EventEmitter {
6666 throw new Error ( "Shadow git repo already initialized" )
6767 }
6868
69- await fs . mkdir ( this . checkpointsDir , { recursive : true } )
70- const git = simpleGit ( this . checkpointsDir )
71- const gitVersion = await git . version ( )
72- this . log ( `[${ this . constructor . name } #create] git = ${ gitVersion } ` )
73-
74- let created = false
7569 const startTime = Date . now ( )
76-
77- if ( await fileExistsAtPath ( this . dotGitDir ) ) {
78- this . log ( `[${ this . constructor . name } #initShadowGit] shadow git repo already exists at ${ this . dotGitDir } ` )
79- const worktree = await this . getShadowGitConfigWorktree ( git )
80-
81- if ( worktree !== this . workspaceDir ) {
82- throw new Error (
83- `Checkpoints can only be used in the original workspace: ${ worktree } !== ${ this . workspaceDir } ` ,
84- )
85- }
86-
87- await this . writeExcludeFile ( )
88- this . baseHash = await git . revparse ( [ "HEAD" ] )
89- } else {
90- this . log ( `[${ this . constructor . name } #initShadowGit] creating shadow git repo at ${ this . checkpointsDir } ` )
91- await git . init ( )
92- await git . addConfig ( "core.worktree" , this . workspaceDir ) // Sets the working tree to the current workspace.
93- await git . addConfig ( "commit.gpgSign" , "false" ) // Disable commit signing for shadow repo.
94- await git . addConfig ( "user.name" , "Roo Code" )
95- await git . addConfig ( "user.email" , "[email protected] " ) 96- await this . writeExcludeFile ( )
97- await this . stageAll ( git )
98- const { commit } = await git . commit ( "initial commit" , { "--allow-empty" : null } )
99- this . baseHash = commit
100- created = true
101- }
102-
70+ const { git, baseHash, created } = await this . initializeShadowRepo ( )
10371 const duration = Date . now ( ) - startTime
10472
10573 this . log (
106- `[${ this . constructor . name } #initShadowGit] initialized shadow repo with base commit ${ this . baseHash } in ${ duration } ms` ,
74+ `[${ this . constructor . name } #initShadowGit] initialized shadow repo with base commit ${ baseHash } in ${ duration } ms` ,
10775 )
10876
10977 this . git = git
78+ this . baseHash = baseHash
11079
11180 await onInit ?.( )
11281
@@ -121,22 +90,68 @@ export abstract class ShadowCheckpointService extends EventEmitter {
12190 return { created, duration }
12291 }
12392
93+ protected isShadowRepoAvailable ( ) {
94+ return fileExistsAtPath ( this . configDir )
95+ }
96+
97+ protected async initializeShadowRepo ( ) {
98+ await fs . mkdir ( this . checkpointsDir , { recursive : true } )
99+ const git = simpleGit ( this . checkpointsDir )
100+ const gitVersion = await git . version ( )
101+ this . log ( `[${ this . constructor . name } #initializeShadowRepo] git = ${ gitVersion } ` )
102+ const exists = await this . isShadowRepoAvailable ( )
103+ console . log ( `[${ this . constructor . name } #initializeShadowRepo] exists = ${ exists } [${ this . configDir } ]` )
104+ const baseHash = exists ? await this . checkShadowRepo ( git ) : await this . createShadowRepo ( git )
105+ return { git, baseHash, created : ! exists }
106+ }
107+
108+ protected async checkShadowRepo ( git : SimpleGit ) {
109+ this . log ( `[${ this . constructor . name } #checkShadowRepo] checking existing shadow repo at ${ this . configDir } ` )
110+ const worktree = await this . getShadowGitConfigWorktree ( git )
111+
112+ if ( worktree !== this . workspaceDir ) {
113+ throw new Error (
114+ `Checkpoints can only be used in the original workspace: ${ worktree } !== ${ this . workspaceDir } ` ,
115+ )
116+ }
117+
118+ await this . writeExcludeFile ( )
119+ return await git . revparse ( [ "HEAD" ] )
120+ }
121+
122+ protected async createShadowRepo ( git : SimpleGit ) {
123+ this . log ( `[${ this . constructor . name } #createShadowRepo] creating new shadow repo at ${ this . checkpointsDir } ` )
124+ await git . init ( )
125+ await git . addConfig ( "core.worktree" , this . workspaceDir ) // Sets the working tree to the current workspace.
126+ await git . addConfig ( "commit.gpgSign" , "false" ) // Disable commit signing for shadow repo.
127+ await git . addConfig ( "user.name" , "Roo Code" )
128+ await git . addConfig ( "user.email" , "[email protected] " ) 129+ await this . writeExcludeFile ( )
130+ await this . stageAll ( git )
131+ const result = await git . commit ( "initial commit" , { "--allow-empty" : null } )
132+ return result . commit
133+ }
134+
124135 // Add basic excludes directly in git config, while respecting any
125136 // .gitignore in the workspace.
126137 // .git/info/exclude is local to the shadow git repo, so it's not
127138 // shared with the main repo - and won't conflict with user's
128139 // .gitignore.
129140 protected async writeExcludeFile ( ) {
130- await fs . mkdir ( path . join ( this . dotGitDir , "info" ) , { recursive : true } )
141+ await fs . mkdir ( path . join ( this . configDir , "info" ) , { recursive : true } )
131142 const patterns = await getExcludePatterns ( this . workspaceDir )
132- await fs . writeFile ( path . join ( this . dotGitDir , "info" , "exclude" ) , patterns . join ( "\n" ) )
143+ await fs . writeFile ( path . join ( this . configDir , "info" , "exclude" ) , patterns . join ( "\n" ) )
144+ }
145+
146+ protected stagePath ( git : SimpleGit , path : string ) {
147+ return git . add ( path )
133148 }
134149
135- private async stageAll ( git : SimpleGit ) {
150+ protected async stageAll ( git : SimpleGit ) {
136151 await this . renameNestedGitRepos ( true )
137152
138153 try {
139- await git . add ( "." )
154+ await this . stagePath ( git , "." )
140155 } catch ( error ) {
141156 this . log (
142157 `[${ this . constructor . name } #stageAll] failed to add files to git: ${ error instanceof Error ? error . message : String ( error ) } ` ,
@@ -149,7 +164,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
149164 // Since we use git to track checkpoints, we need to temporarily disable
150165 // nested git repos to work around git's requirement of using submodules for
151166 // nested repos.
152- private async renameNestedGitRepos ( disable : boolean ) {
167+ protected async renameNestedGitRepos ( disable : boolean ) {
153168 // Find all .git directories that are not at the root level.
154169 const gitPaths = await globby ( "**/.git" + ( disable ? "" : GIT_DISABLED_SUFFIX ) , {
155170 cwd : this . workspaceDir ,
@@ -186,7 +201,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
186201 }
187202 }
188203
189- private async getShadowGitConfigWorktree ( git : SimpleGit ) {
204+ protected async getShadowGitConfigWorktree ( git : SimpleGit ) {
190205 if ( ! this . shadowGitConfigWorktree ) {
191206 try {
192207 this . shadowGitConfigWorktree = ( await git . getConfig ( "core.worktree" ) ) . value || undefined
0 commit comments