Skip to content

Commit ca54d1e

Browse files
committed
Stashless checkpoints
1 parent 145819f commit ca54d1e

File tree

1 file changed

+31
-99
lines changed

1 file changed

+31
-99
lines changed

src/services/checkpoints/CheckpointService.ts

Lines changed: 31 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export type CheckpointServiceOptions = {
5151
export class CheckpointService {
5252
private static readonly USER_NAME = "Roo Code"
5353
private static readonly USER_EMAIL = "[email protected]"
54+
private static readonly CHECKPOINT_BRANCH = "roo-code-checkpoints"
55+
private static readonly STASH_BRANCH = "roo-code-stash"
5456

5557
private _currentCheckpoint?: string
5658

@@ -72,39 +74,6 @@ export class CheckpointService {
7274
private readonly log: (message: string) => void,
7375
) {}
7476

75-
private async pushStash() {
76-
const status = await this.git.status()
77-
78-
if (status.files.length > 0) {
79-
await this.git.stash(["-u"]) // Includes tracked and untracked files.
80-
return true
81-
}
82-
83-
return false
84-
}
85-
86-
private async applyStash() {
87-
const stashList = await this.git.stashList()
88-
89-
if (stashList.all.length > 0) {
90-
await this.git.stash(["apply"]) // Applies the most recent stash only.
91-
return true
92-
}
93-
94-
return false
95-
}
96-
97-
private async popStash() {
98-
const stashList = await this.git.stashList()
99-
100-
if (stashList.all.length > 0) {
101-
await this.git.stash(["pop", "--index"]) // Pops the most recent stash only.
102-
return true
103-
}
104-
105-
return false
106-
}
107-
10877
private async ensureBranch(expectedBranch: string) {
10978
const branch = await this.git.revparse(["--abbrev-ref", "HEAD"])
11079

@@ -156,84 +125,47 @@ export class CheckpointService {
156125
public async saveCheckpoint(message: string) {
157126
await this.ensureBranch(this.mainBranch)
158127

159-
// Attempt to stash pending changes (including untracked files).
160-
const pendingChanges = await this.pushStash()
161-
162-
// Get the latest commit on the hidden branch before we reset it.
163-
const latestHash = await this.git.revparse([this.hiddenBranch])
164-
165-
// Check if there is any diff relative to the latest commit.
166-
if (!pendingChanges) {
167-
const diff = await this.git.diff([latestHash])
168-
169-
if (!diff) {
170-
this.log(`[saveCheckpoint] No changes detected, giving up`)
171-
return undefined
172-
}
173-
}
174-
175-
await this.git.checkout(this.hiddenBranch)
176-
177-
const reset = async () => {
178-
await this.git.reset(["HEAD", "."])
179-
await this.git.clean([CleanOptions.FORCE, CleanOptions.RECURSIVE])
180-
await this.git.reset(["--hard", latestHash])
181-
await this.git.checkout(this.mainBranch)
182-
await this.popStash()
183-
}
128+
// Create temporary branch with all current changes
129+
const tempBranch = `${CheckpointService.STASH_BRANCH}-${Date.now()}`
130+
await this.git.checkout(["-b", tempBranch])
184131

185132
try {
186-
// Reset hidden branch to match main and apply the pending changes.
187-
await this.git.reset(["--hard", this.mainBranch])
188-
189-
if (pendingChanges) {
190-
await this.applyStash()
191-
}
192-
193-
// Using "-A" ensures that deletions are staged as well.
133+
// Stage and commit all changes to temporary branch
194134
await this.git.add(["-A"])
195-
const diff = await this.git.diff([latestHash])
196-
197-
if (!diff) {
198-
this.log(`[saveCheckpoint] No changes detected, resetting and giving up`)
199-
await reset()
200-
return undefined
201-
}
202-
203-
// Otherwise, commit the changes.
204-
const status = await this.git.status()
205-
this.log(`[saveCheckpoint] Changes detected, committing ${JSON.stringify(status)}`)
206-
207-
// Allow empty commits in order to correctly handle deletion of
208-
// untracked files (see unit tests for an example of this).
209-
// Additionally, skip pre-commit hooks so that they don't slow
210-
// things down or tamper with the contents of the commit.
211-
const commit = await this.git.commit(message, undefined, {
135+
await this.git.commit(message, undefined, {
212136
"--allow-empty": null,
213137
"--no-verify": null,
214138
})
215139

216-
await this.git.checkout(this.mainBranch)
140+
// Get the latest commit on the hidden branch before we reset it
141+
const latestHash = await this.git.revparse([this.hiddenBranch])
142+
await this.git.checkout(this.hiddenBranch)
217143

218-
if (pendingChanges) {
219-
await this.popStash()
220-
}
144+
try {
145+
// Reset hidden branch to match main and apply the changes
146+
await this.git.reset(["--hard", this.mainBranch])
221147

222-
this.currentCheckpoint = commit.commit
148+
// Cherry-pick the temporary commit
149+
const commit = await this.git.raw(["cherry-pick", tempBranch])
223150

224-
return commit
225-
} catch (err) {
226-
this.log(`[saveCheckpoint] Failed to save checkpoint: ${err instanceof Error ? err.message : String(err)}`)
151+
// Return to main branch and cleanup
152+
await this.git.checkout(this.mainBranch)
153+
await this.git.branch(["-D", tempBranch])
227154

228-
// If we're not on the main branch then we need to trigger a reset
229-
// to return to the main branch and restore it's previous state.
230-
const currentBranch = await this.git.revparse(["--abbrev-ref", "HEAD"])
231-
232-
if (currentBranch.trim() !== this.mainBranch) {
233-
await reset()
155+
this.currentCheckpoint = commit
156+
return { commit }
157+
} catch (err) {
158+
// If something went wrong after switching to hidden branch
159+
await this.git.reset(["--hard", latestHash])
160+
await this.git.checkout(["-f", this.mainBranch])
161+
await this.git.branch(["-D", tempBranch])
162+
throw err
234163
}
235-
236-
throw err
164+
} catch (err) {
165+
// If something went wrong before switching to hidden branch
166+
await this.git.checkout(["-f", this.mainBranch])
167+
await this.git.branch(["-D", tempBranch])
168+
throw new Error(`Failed to save checkpoint: ${err instanceof Error ? err.message : String(err)}`)
237169
}
238170
}
239171

0 commit comments

Comments
 (0)