fix(cli): ensure git committer identity during ktx setup (#276)

* fix(cli): ensure git committer identity during ktx setup

ktx setup threw "Failed to initialize git repository" when the project
directory was already a git repo with no commits and the machine had no
configured git identity (e.g. a fresh Mac with no ~/.gitconfig).
GitService only set the identity on the path where it created the repo
itself, so the bootstrap commit had no resolvable committer.

Carry ktx's identity via GIT_AUTHOR/GIT_COMMITTER env on the shared
git client so every commit succeeds regardless of whether ktx created
the repo, without mutating the user's repo config. Also preserve the
underlying git error when rethrowing so the failure is diagnosable in
telemetry and actionable for the user.

* chore: sync uv.lock ktx-daemon and ktx-sl versions to 0.10.0
This commit is contained in:
Andrey Avtomonov 2026-06-09 12:10:02 +02:00 committed by GitHub
parent bd3a375081
commit 6b2f7c3365
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 135 additions and 11 deletions

View file

@ -24,6 +24,21 @@ function sanitizedGitEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEn
return sanitized;
}
export function createSimpleGit(baseDir: string): SimpleGit {
return simpleGit({ baseDir, unsafe: { allowUnsafeAskPass: true } }).env(sanitizedGitEnv());
/**
* Create a simple-git client scoped to `baseDir`. When an identity is provided, ktx's own
* commits carry it through the GIT_AUTHOR and GIT_COMMITTER environment variables instead of
* relying on repo-local or global git config. This keeps commits working when the project
* directory is an existing repo ktx did not create and the machine has no configured git
* identity (e.g. a fresh Mac with no ~/.gitconfig), without mutating the user's repo config.
* Explicit `--author` flags on individual commits still take precedence over GIT_AUTHOR_NAME.
*/
export function createSimpleGit(baseDir: string, identity?: { name: string; email: string }): SimpleGit {
const env = sanitizedGitEnv();
if (identity?.name && identity.email) {
env.GIT_AUTHOR_NAME = identity.name;
env.GIT_AUTHOR_EMAIL = identity.email;
env.GIT_COMMITTER_NAME = identity.name;
env.GIT_COMMITTER_EMAIL = identity.email;
}
return simpleGit({ baseDir, unsafe: { allowUnsafeAskPass: true } }).env(env);
}

View file

@ -85,8 +85,12 @@ export class GitService {
await fs.mkdir(this.configDir, { recursive: true });
this.logger.log(`Config directory ensured at: ${this.configDir}`);
// Initialize simple-git
this.git = createSimpleGit(this.configDir);
// Initialize simple-git. Carry ktx's identity in the environment so commits succeed even
// when this repo already exists and the machine has no configured git identity.
this.git = createSimpleGit(this.configDir, {
name: this.config.git.userName,
email: this.config.git.userEmail,
});
// Initialize git repository
await this.initialize();
@ -99,9 +103,6 @@ export class GitService {
if (!isRepo) {
await this.git.init();
const gitConfig = this.config.git;
await this.git.addConfig('user.name', gitConfig.userName);
await this.git.addConfig('user.email', gitConfig.userEmail);
this.logger.log('Initialized git repository');
}
@ -125,7 +126,11 @@ export class GitService {
}
} catch (error) {
this.logger.error('Failed to initialize git repository', error);
throw new Error('Failed to initialize git repository');
// Preserve the underlying git error: the generic message alone is undiagnosable in
// telemetry and unactionable for the user. The exception reporter walks `cause` and
// redacts secrets before send.
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to initialize git repository: ${detail}`, { cause: error });
}
}
@ -899,7 +904,10 @@ export class GitService {
*/
forWorktree(workdir: string): GitService {
const scoped = new GitService(this.config, this.logger);
scoped.git = createSimpleGit(workdir);
scoped.git = createSimpleGit(workdir, {
name: this.config.git.userName,
email: this.config.git.userEmail,
});
scoped.configDir = workdir;
return scoped;
}