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.
This commit is contained in:
Reid 2026-04-19 14:24:01 +08:00 committed by GitHub
parent 4bf5bf22de
commit cd8d9dce6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 7 deletions

View file

@ -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<void> {
await git.init(false);
await git.raw(['symbolic-ref', 'HEAD', 'refs/heads/main']);
}

View file

@ -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 () => {

View file

@ -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 });
}

View file

@ -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');

View file

@ -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('.');