diff --git a/packages/core/src/services/gitInit.ts b/packages/core/src/services/gitInit.ts new file mode 100644 index 000000000..4756c0466 --- /dev/null +++ b/packages/core/src/services/gitInit.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { SimpleGit } from 'simple-git'; + +export async function initRepositoryWithMainBranch( + git: SimpleGit, +): Promise { + await git.init(false); + await git.raw(['symbolic-ref', 'HEAD', 'refs/heads/main']); +} diff --git a/packages/core/src/services/gitService.test.ts b/packages/core/src/services/gitService.test.ts index 7ef871274..10cde1067 100644 --- a/packages/core/src/services/gitService.test.ts +++ b/packages/core/src/services/gitService.test.ts @@ -162,12 +162,27 @@ describe('GitService', () => { expect(actualConfigContent).toBe(expectedConfigContent); }); + it('should use the shadow git config during repository setup', async () => { + const service = new GitService(projectRoot, storage); + await service.setupShadowGitRepository(); + + expect(hoistedMockEnv).toHaveBeenCalledWith({ + HOME: repoDir, + XDG_CONFIG_HOME: repoDir, + }); + }); + it('should initialize git repo in historyDir if not already initialized', async () => { hoistedMockCheckIsRepo.mockResolvedValue(false); const service = new GitService(projectRoot, storage); await service.setupShadowGitRepository(); expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir); - expect(hoistedMockInit).toHaveBeenCalled(); + expect(hoistedMockInit).toHaveBeenCalledWith(false); + expect(hoistedMockRaw).toHaveBeenCalledWith([ + 'symbolic-ref', + 'HEAD', + 'refs/heads/main', + ]); }); it('should initialize git repo when root repo check throws', async () => { @@ -184,6 +199,7 @@ describe('GitService', () => { const service = new GitService(projectRoot, storage); await service.setupShadowGitRepository(); expect(hoistedMockInit).not.toHaveBeenCalled(); + expect(hoistedMockRaw).not.toHaveBeenCalled(); }); it('should copy .gitignore from projectRoot if it exists', async () => { diff --git a/packages/core/src/services/gitService.ts b/packages/core/src/services/gitService.ts index 9a220d35a..da29cf014 100644 --- a/packages/core/src/services/gitService.ts +++ b/packages/core/src/services/gitService.ts @@ -11,6 +11,7 @@ import type { SimpleGit } from 'simple-git'; import { simpleGit, CheckRepoActions } from 'simple-git'; import type { Storage } from '../config/storage.js'; import { isNodeError } from '../utils/errors.js'; +import { initRepositoryWithMainBranch } from './gitInit.js'; export class GitService { private projectRoot: string; @@ -57,7 +58,11 @@ export class GitService { '[user]\n name = Qwen Code\n email = qwen-code@qwen.ai\n[commit]\n gpgsign = false\n'; await fs.writeFile(gitConfigPath, gitConfigContent); - const repo = simpleGit(repoDir); + const repo = simpleGit(repoDir).env({ + // Prevent git from using the user's global git config. + HOME: repoDir, + XDG_CONFIG_HOME: repoDir, + }); let isRepoDefined = false; try { isRepoDefined = await repo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT); @@ -68,10 +73,7 @@ export class GitService { } if (!isRepoDefined) { - await repo.init(false, { - '--initial-branch': 'main', - }); - + await initRepositoryWithMainBranch(repo); await repo.commit('Initial commit', { '--allow-empty': null }); } diff --git a/packages/core/src/services/gitWorktreeService.test.ts b/packages/core/src/services/gitWorktreeService.test.ts index f34eb1ca2..acfafc39e 100644 --- a/packages/core/src/services/gitWorktreeService.test.ts +++ b/packages/core/src/services/gitWorktreeService.test.ts @@ -135,6 +135,43 @@ describe('GitWorktreeService', () => { expect(hoistedMockCheckIsRepo).toHaveBeenNthCalledWith(2); }); + it('initializeRepository should initialize a new repo on main', async () => { + hoistedMockCheckIsRepo.mockResolvedValue(false); + const service = new GitWorktreeService('/repo'); + + const result = await service.initializeRepository(); + + expect(result).toEqual({ initialized: true }); + expect(hoistedMockInit).toHaveBeenCalledWith(false); + expect(hoistedMockRaw).toHaveBeenCalledWith([ + 'symbolic-ref', + 'HEAD', + 'refs/heads/main', + ]); + expect(hoistedMockAdd).toHaveBeenCalledWith('.'); + expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', { + '--allow-empty': null, + }); + expect(hoistedMockInit.mock.invocationCallOrder[0]!).toBeLessThan( + hoistedMockRaw.mock.invocationCallOrder[0]!, + ); + expect(hoistedMockRaw.mock.invocationCallOrder[0]!).toBeLessThan( + hoistedMockCommit.mock.invocationCallOrder[0]!, + ); + }); + + it('initializeRepository should not update HEAD for an existing repo', async () => { + hoistedMockCheckIsRepo.mockResolvedValue(true); + const service = new GitWorktreeService('/repo'); + + const result = await service.initializeRepository(); + + expect(result).toEqual({ initialized: false }); + expect(hoistedMockInit).not.toHaveBeenCalled(); + expect(hoistedMockRaw).not.toHaveBeenCalled(); + expect(hoistedMockCommit).not.toHaveBeenCalled(); + }); + it('createWorktree should create a sanitized branch and worktree path', async () => { const service = new GitWorktreeService('/repo'); diff --git a/packages/core/src/services/gitWorktreeService.ts b/packages/core/src/services/gitWorktreeService.ts index 6ceebf11e..bec5ca6ae 100644 --- a/packages/core/src/services/gitWorktreeService.ts +++ b/packages/core/src/services/gitWorktreeService.ts @@ -12,6 +12,7 @@ import type { SimpleGit } from 'simple-git'; import { Storage } from '../config/storage.js'; import { isCommandAvailable } from '../utils/shell-utils.js'; import { isNodeError } from '../utils/errors.js'; +import { initRepositoryWithMainBranch } from './gitInit.js'; /** * Commit message used for the baseline snapshot in worktrees. @@ -185,7 +186,7 @@ export class GitWorktreeService { } try { - await this.git.init(false, { '--initial-branch': 'main' }); + await initRepositoryWithMainBranch(this.git); // Create initial commit so we can create worktrees await this.git.add('.');