* Restore a stash identified by its push MESSAGE (not a positional `stash pop`, * which can cross-pop another session's WIP). Finds the matching ref via * `git stash list`, applies it, then drops it. Returns: * 'ok' — applied and dropped cleanly, * 'missing' — no stash with th
(
message: string,
signal?: AbortSignal,
)
| 243 | * 'error' — some other failure. |
| 244 | */ |
| 245 | private async restoreStashByMessage( |
| 246 | message: string, |
| 247 | signal?: AbortSignal, |
| 248 | ): Promise<'ok' | 'missing' | 'conflict' | 'error'> { |
| 249 | const list = await git(['stash', 'list', '--format=%gd %gs'], { cwd: this.cwd, signal }); |
| 250 | if (list.exitCode !== 0) return 'error'; |
| 251 | // Lines look like: "stash@{0} On main: qodex-sandbox-wip-<id>" |
| 252 | const line = list.stdout.split('\n').find((l) => l.includes(message)); |
| 253 | if (!line) return 'missing'; |
| 254 | const ref = line.trim().split(/\s+/)[0]; // e.g. stash@{0} |
| 255 | if (!ref) return 'error'; |
| 256 | const ap = await git(['stash', 'apply', ref], { cwd: this.cwd, signal }); |
| 257 | if (ap.exitCode !== 0) { |
| 258 | const out = `${ap.stdout ?? ''}\n${ap.stderr ?? ''}`; |
| 259 | if (/conflict/i.test(out)) { |
| 260 | logger.warn('GitSandbox: restoring WIP stash hit conflicts — left for manual resolution', { ref, message }); |
| 261 | return 'conflict'; |
| 262 | } |
| 263 | logger.warn('GitSandbox: failed to apply WIP stash', { ref, message, err: (ap.stderr || ap.stdout || '').trim() }); |
| 264 | return 'error'; |
| 265 | } |
| 266 | // Applied cleanly — drop it so it isn't restored twice. |
| 267 | await git(['stash', 'drop', ref], { cwd: this.cwd, signal }).catch(() => {}); |
| 268 | return 'ok'; |
| 269 | } |
| 270 | |
| 271 | /** Commit current progress as a returnable checkpoint. Returns the SHA or null. */ |
| 272 | async checkpoint(label: string, signal?: AbortSignal): Promise<string | null> { |