Skip to content

Commit cd8d9dc

Browse files
authored
fix(core): support older Git during repository initialization
Replace git init --initial-branch with git init followed by symbolic-ref HEAD refs/heads/main. This keeps new repositories on main without requiring Git 2.28 or newer. Also ensure checkpoint shadow repository setup uses its dedicated git config during the initial commit.
1 parent 4bf5bf2 commit cd8d9dc

5 files changed

Lines changed: 77 additions & 7 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Qwen Team
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type { SimpleGit } from 'simple-git';
8+
9+
export async function initRepositoryWithMainBranch(
10+
git: SimpleGit,
11+
): Promise<void> {
12+
await git.init(false);
13+
await git.raw(['symbolic-ref', 'HEAD', 'refs/heads/main']);
14+
}

packages/core/src/services/gitService.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,27 @@ describe('GitService', () => {
162162
expect(actualConfigContent).toBe(expectedConfigContent);
163163
});
164164

165+
it('should use the shadow git config during repository setup', async () => {
166+
const service = new GitService(projectRoot, storage);
167+
await service.setupShadowGitRepository();
168+
169+
expect(hoistedMockEnv).toHaveBeenCalledWith({
170+
HOME: repoDir,
171+
XDG_CONFIG_HOME: repoDir,
172+
});
173+
});
174+
165175
it('should initialize git repo in historyDir if not already initialized', async () => {
166176
hoistedMockCheckIsRepo.mockResolvedValue(false);
167177
const service = new GitService(projectRoot, storage);
168178
await service.setupShadowGitRepository();
169179
expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir);
170-
expect(hoistedMockInit).toHaveBeenCalled();
180+
expect(hoistedMockInit).toHaveBeenCalledWith(false);
181+
expect(hoistedMockRaw).toHaveBeenCalledWith([
182+
'symbolic-ref',
183+
'HEAD',
184+
'refs/heads/main',
185+
]);
171186
});
172187

173188
it('should initialize git repo when root repo check throws', async () => {
@@ -184,6 +199,7 @@ describe('GitService', () => {
184199
const service = new GitService(projectRoot, storage);
185200
await service.setupShadowGitRepository();
186201
expect(hoistedMockInit).not.toHaveBeenCalled();
202+
expect(hoistedMockRaw).not.toHaveBeenCalled();
187203
});
188204

189205
it('should copy .gitignore from projectRoot if it exists', async () => {

packages/core/src/services/gitService.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { SimpleGit } from 'simple-git';
1111
import { simpleGit, CheckRepoActions } from 'simple-git';
1212
import type { Storage } from '../config/storage.js';
1313
import { isNodeError } from '../utils/errors.js';
14+
import { initRepositoryWithMainBranch } from './gitInit.js';
1415

1516
export class GitService {
1617
private projectRoot: string;
@@ -57,7 +58,11 @@ export class GitService {
5758
'[user]\n name = Qwen Code\n email = qwen-code@qwen.ai\n[commit]\n gpgsign = false\n';
5859
await fs.writeFile(gitConfigPath, gitConfigContent);
5960

60-
const repo = simpleGit(repoDir);
61+
const repo = simpleGit(repoDir).env({
62+
// Prevent git from using the user's global git config.
63+
HOME: repoDir,
64+
XDG_CONFIG_HOME: repoDir,
65+
});
6166
let isRepoDefined = false;
6267
try {
6368
isRepoDefined = await repo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
@@ -68,10 +73,7 @@ export class GitService {
6873
}
6974

7075
if (!isRepoDefined) {
71-
await repo.init(false, {
72-
'--initial-branch': 'main',
73-
});
74-
76+
await initRepositoryWithMainBranch(repo);
7577
await repo.commit('Initial commit', { '--allow-empty': null });
7678
}
7779

packages/core/src/services/gitWorktreeService.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,43 @@ describe('GitWorktreeService', () => {
135135
expect(hoistedMockCheckIsRepo).toHaveBeenNthCalledWith(2);
136136
});
137137

138+
it('initializeRepository should initialize a new repo on main', async () => {
139+
hoistedMockCheckIsRepo.mockResolvedValue(false);
140+
const service = new GitWorktreeService('/repo');
141+
142+
const result = await service.initializeRepository();
143+
144+
expect(result).toEqual({ initialized: true });
145+
expect(hoistedMockInit).toHaveBeenCalledWith(false);
146+
expect(hoistedMockRaw).toHaveBeenCalledWith([
147+
'symbolic-ref',
148+
'HEAD',
149+
'refs/heads/main',
150+
]);
151+
expect(hoistedMockAdd).toHaveBeenCalledWith('.');
152+
expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', {
153+
'--allow-empty': null,
154+
});
155+
expect(hoistedMockInit.mock.invocationCallOrder[0]!).toBeLessThan(
156+
hoistedMockRaw.mock.invocationCallOrder[0]!,
157+
);
158+
expect(hoistedMockRaw.mock.invocationCallOrder[0]!).toBeLessThan(
159+
hoistedMockCommit.mock.invocationCallOrder[0]!,
160+
);
161+
});
162+
163+
it('initializeRepository should not update HEAD for an existing repo', async () => {
164+
hoistedMockCheckIsRepo.mockResolvedValue(true);
165+
const service = new GitWorktreeService('/repo');
166+
167+
const result = await service.initializeRepository();
168+
169+
expect(result).toEqual({ initialized: false });
170+
expect(hoistedMockInit).not.toHaveBeenCalled();
171+
expect(hoistedMockRaw).not.toHaveBeenCalled();
172+
expect(hoistedMockCommit).not.toHaveBeenCalled();
173+
});
174+
138175
it('createWorktree should create a sanitized branch and worktree path', async () => {
139176
const service = new GitWorktreeService('/repo');
140177

packages/core/src/services/gitWorktreeService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { SimpleGit } from 'simple-git';
1212
import { Storage } from '../config/storage.js';
1313
import { isCommandAvailable } from '../utils/shell-utils.js';
1414
import { isNodeError } from '../utils/errors.js';
15+
import { initRepositoryWithMainBranch } from './gitInit.js';
1516

1617
/**
1718
* Commit message used for the baseline snapshot in worktrees.
@@ -185,7 +186,7 @@ export class GitWorktreeService {
185186
}
186187

187188
try {
188-
await this.git.init(false, { '--initial-branch': 'main' });
189+
await initRepositoryWithMainBranch(this.git);
189190

190191
// Create initial commit so we can create worktrees
191192
await this.git.add('.');

0 commit comments

Comments
 (0)