diff --git a/src/main/git/git-username.ts b/src/main/git/git-username.ts index 6f32fd59451..e69d46d25f9 100644 --- a/src/main/git/git-username.ts +++ b/src/main/git/git-username.ts @@ -326,7 +326,14 @@ export async function resolveLocalGitUsernameDetailed( } if (await localRepoHasEffectiveGitHubRemote(repoPath)) { const outcome = await getGhLoginOutcome() - return { username: outcome.login, authoritative: !outcome.timedOut } + if (outcome.login || outcome.timedOut) { + return { username: outcome.login, authoritative: !outcome.timedOut } + } + } + // Author names become prefixes only when they already form a safe branch component. + const authorName = (await readGitStdout(repoPath, ['config', '--get', 'user.name'])).trim() + if (isBranchSafeHostedLogin(authorName)) { + return { username: authorName, authoritative: true } } return { username: '', authoritative: true } } diff --git a/src/main/git/repo-username.test.ts b/src/main/git/repo-username.test.ts index 617cf6d219e..417c2974bdb 100644 --- a/src/main/git/repo-username.test.ts +++ b/src/main/git/repo-username.test.ts @@ -148,7 +148,8 @@ describe('resolveLocalGitUsername', () => { expect(gitExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([ ['config', '--get', 'github.user'], ['config', '--get', 'user.username'], - ['remote'] + ['remote'], + ['config', '--get', 'user.name'] ]) expect(ghExecFileAsyncMock).not.toHaveBeenCalled() }) @@ -197,16 +198,48 @@ describe('resolveLocalGitUsername', () => { expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1) }) - it('does not derive GitHub username prefixes from non-GitHub remotes', async () => { + it('does not derive GitHub username prefixes from free-form author names on non-GitHub remotes', async () => { originRemoteUrl = 'https://gitlab.com/stablyai/orca.git' gitConfig['user.email'] = 'demo@example.com' gitConfig['user.name'] = 'Demo User' ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'gh-demo\n', stderr: '' }) + // Spaces make "Demo User" branch-unsafe; free-form author identity stays out of prefixes. await expect(resolveLocalGitUsername('/repo')).resolves.toBe('') expect(ghExecFileAsyncMock).not.toHaveBeenCalled() }) + it('falls back to branch-safe user.name when github.user and gh login are absent', async () => { + // Issue #11590: only `user.name = lorengroves` is configured; branch prefix was empty. + originRemoteUrl = 'https://github.com/stablyai/orca.git' + gitConfig['user.name'] = 'lorengroves' + ghExecFileAsyncMock.mockRejectedValue(makeExecError('gh unavailable')) + + await expect(resolveLocalGitUsername('/repo')).resolves.toBe('lorengroves') + }) + + it('uses branch-safe user.name on non-GitHub remotes when no explicit username keys exist', async () => { + originRemoteUrl = 'https://gitlab.com/stablyai/orca.git' + gitConfig['user.name'] = 'lorengroves' + ghExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'gh-demo\n', stderr: '' }) + + await expect(resolveLocalGitUsername('/repo')).resolves.toBe('lorengroves') + expect(ghExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it.each([ + 'person@example.com', + '42+person', + 'person..name', + 'person.lock', + 'person\\name', + 'person/name' + ])('does not rewrite or accept unsafe author name %s', async (authorName) => { + originRemoteUrl = 'https://gitlab.com/stablyai/orca.git' + gitConfig['user.name'] = authorName + await expect(resolveLocalGitUsername('/repo')).resolves.toBe('') + }) + it('ignores a secondary GitHub mirror when the effective remote is GitLab', async () => { // Why: a GitLab-primary repo with a GitHub mirror must not pick up the // GitHub account name as its branch prefix — only the effective remote