From 81420c8cb4391b5550038153ebdca64058998e85 Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 13 May 2026 15:16:56 +1000 Subject: [PATCH] fix(workspace): allow git base_commit SHAs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/evaluation/workspace/repo-manager.ts | 9 +++++- .../evaluation/workspace/repo-manager.test.ts | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/core/src/evaluation/workspace/repo-manager.ts b/packages/core/src/evaluation/workspace/repo-manager.ts index 66006f579..1d5970510 100644 --- a/packages/core/src/evaluation/workspace/repo-manager.ts +++ b/packages/core/src/evaluation/workspace/repo-manager.ts @@ -39,6 +39,10 @@ function getSourceUrl(source: RepoSource): string { return source.type === 'git' ? source.url : source.path; } +function isFullCommitSha(ref: string | undefined): boolean { + return typeof ref === 'string' && /^[0-9a-f]{40}$/i.test(ref); +} + async function git(args: string[], opts?: { cwd?: string; timeout?: number }): Promise { const { stdout } = await execFileAsync('git', args, { cwd: opts?.cwd, @@ -169,9 +173,12 @@ export class RepoManager { // Resolve ref const ref = getRepoCheckoutRef(repo.checkout); const resolve = repo.checkout?.resolve ?? 'remote'; + const baseCommit = repo.checkout?.base_commit; + const shouldResolveLocally = + resolve === 'local' || (repo.source.type === 'git' && isFullCommitSha(baseCommit)); let resolvedSha: string; - if (resolve === 'remote' && repo.source.type === 'git') { + if (!shouldResolveLocally && repo.source.type === 'git') { // Resolve via ls-remote for remote refs const url = getSourceUrl(repo.source); try { diff --git a/packages/core/test/evaluation/workspace/repo-manager.test.ts b/packages/core/test/evaluation/workspace/repo-manager.test.ts index 6d5744a47..d55e0b3c7 100644 --- a/packages/core/test/evaluation/workspace/repo-manager.test.ts +++ b/packages/core/test/evaluation/workspace/repo-manager.test.ts @@ -99,6 +99,34 @@ describe('RepoManager', () => { expect(existsSync(path.join(targetDir, 'third.txt'))).toBe(false); }, 30_000); + it('checks out raw base_commit SHAs from git sources without resolve: local', async () => { + const repoDir = path.join(tmpDir, 'source-repo'); + createTestRepo(repoDir); + writeFileSync(path.join(repoDir, 'second.txt'), 'second'); + execSync('git add -A && git commit -m "second"', { cwd: repoDir, ...EXEC_OPTS }); + const secondSha = gitExec('git rev-parse HEAD', repoDir); + writeFileSync(path.join(repoDir, 'third.txt'), 'third'); + execSync('git add -A && git commit -m "third"', { cwd: repoDir, ...EXEC_OPTS }); + + const remoteDir = path.join(tmpDir, 'remote.git'); + execSync(`git clone --bare "${repoDir}" "${remoteDir}"`, { env: cleanGitEnv() }); + + await manager.materialize( + { + path: './my-repo', + source: { type: 'git', url: remoteDir }, + checkout: { base_commit: secondSha }, + }, + workspaceDir, + ); + + const targetDir = path.join(workspaceDir, 'my-repo'); + const headSha = gitExec('git rev-parse HEAD', targetDir); + expect(headSha).toBe(secondSha); + expect(existsSync(path.join(targetDir, 'second.txt'))).toBe(true); + expect(existsSync(path.join(targetDir, 'third.txt'))).toBe(false); + }, 30_000); + it('walks ancestor commits', async () => { const repoDir = path.join(tmpDir, 'source-repo'); const firstSha = createTestRepo(repoDir);