Skip to content

Commit 90f899b

Browse files
authored
Merge pull request #112 from voiduin/feature/show-commit-author-config-origin
Add option to display commit author identity with git config source
2 parents 6cf045f + 92e22fb commit 90f899b

17 files changed

Lines changed: 557 additions & 10 deletions

File tree

app/src/lib/app-state.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Account } from '../models/account'
22
import { CommitIdentity } from '../models/commit-identity'
3+
import { IConfigValueOrigin } from './git/config'
34
import { IDiff, ImageDiffType } from '../models/diff'
45
import { Repository, ILocalRepositoryState } from '../models/repository'
56
import { Branch, IAheadBehind } from '../models/branch'
@@ -363,6 +364,8 @@ export interface IAppState {
363364
*/
364365
readonly commitSpellcheckEnabled: boolean
365366

367+
readonly showCommitAuthorInfo: boolean
368+
366369
/**
367370
* Record of what logged in users have been checked to see if thank you is in
368371
* order for external contributions in latest release.
@@ -562,6 +565,9 @@ export interface IRepositoryState {
562565
*/
563566
readonly commitAuthor: CommitIdentity | null
564567

568+
readonly commitAuthorNameOrigin: IConfigValueOrigin | null
569+
readonly commitAuthorEmailOrigin: IConfigValueOrigin | null
570+
565571
readonly branchesState: IBranchesState
566572

567573
readonly worktreesState: IWorktreesState

app/src/lib/git/config.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,111 @@ async function removeConfigValueInPath(
282282

283283
await git(flags, path || __dirname, 'removeConfigValueInPath', options)
284284
}
285+
286+
export interface IConfigValueOrigin {
287+
readonly value: string
288+
readonly scope: string
289+
readonly origin: string
290+
}
291+
292+
/**
293+
* Look up a config value along with its source file and scope.
294+
* Requires Git 2.26+ for --show-scope.
295+
*/
296+
export async function getConfigValueWithOrigin(
297+
repository: Repository,
298+
name: string
299+
): Promise<IConfigValueOrigin | null> {
300+
const result = await git(
301+
['config', '--show-origin', '--show-scope', '-z', name],
302+
repository.path,
303+
'getConfigValueWithOrigin',
304+
// 0 = found, 1 = key not set, 128 = not a git repo or git error
305+
{ successExitCodes: new Set([0, 1, 128]) }
306+
)
307+
308+
if (result.exitCode !== 0) {
309+
return null
310+
}
311+
312+
const parts = result.stdout.split('\0')
313+
if (parts.length >= 3) {
314+
return {
315+
scope: parts[0],
316+
origin: parts[1],
317+
value: parts[2],
318+
}
319+
}
320+
321+
return null
322+
}
323+
324+
/**
325+
* Extract the file path from a config value origin, stripping the `file:` prefix.
326+
* When repositoryPath is provided, relative paths (e.g. `.git/config` for local
327+
* scope) are resolved to absolute paths.
328+
*/
329+
export function getOriginFilePath(
330+
origin: IConfigValueOrigin,
331+
repositoryPath?: string
332+
): string {
333+
const filePath = origin.origin.replace(/^file:/, '')
334+
// Git returns relative paths for local/worktree scope (e.g. `.git/config`)
335+
if (repositoryPath && !/^([a-zA-Z]:|[/\\])/.test(filePath)) {
336+
const base = repositoryPath.replace(/[\\/]+$/, '')
337+
return `${base}/${filePath}`
338+
}
339+
return filePath
340+
}
341+
342+
/**
343+
* Check whether a global-scoped config value comes from a conditionally
344+
* included file (via includeIf directive) rather than a standard location.
345+
*/
346+
export function isConditionalInclude(origin: IConfigValueOrigin): boolean {
347+
if (origin.scope !== 'global') {
348+
return false
349+
}
350+
const filePath = getOriginFilePath(origin)
351+
return (
352+
!/[/\\]\.gitconfig$/i.test(filePath) &&
353+
!/[/\\]\.config[/\\]git[/\\]config$/i.test(filePath)
354+
)
355+
}
356+
357+
/** Format a human-readable scope description for a config value origin. */
358+
export function formatConfigScope(origin: IConfigValueOrigin): string {
359+
if (origin.scope === 'local') {
360+
return 'local'
361+
} else if (origin.scope === 'system') {
362+
return 'system'
363+
} else if (origin.scope === 'worktree') {
364+
return 'worktree'
365+
} else if (origin.scope === 'global') {
366+
return isConditionalInclude(origin) ? 'global, via [includeIf]' : 'global'
367+
}
368+
return origin.scope
369+
}
370+
371+
/**
372+
* Format the file path for a config value origin.
373+
* For local/worktree scope, displays the path with a `<repo>` prefix.
374+
*/
375+
export function formatConfigPath(
376+
origin: IConfigValueOrigin,
377+
repositoryPath: string
378+
): string {
379+
const rawPath = origin.origin.replace(/^file:/, '')
380+
if (origin.scope === 'local' || origin.scope === 'worktree') {
381+
// Git returns relative paths for local scope (e.g. `.git/config`)
382+
if (!/^([a-zA-Z]:|[/\\])/.test(rawPath)) {
383+
return '<repo>/' + rawPath
384+
}
385+
// Absolute path — strip repo prefix
386+
const normalized = repositoryPath.replace(/[\\/]+$/, '')
387+
if (rawPath.toLowerCase().startsWith(normalized.toLowerCase())) {
388+
return '<repo>' + rawPath.slice(normalized.length)
389+
}
390+
}
391+
return rawPath
392+
}

app/src/lib/stores/app-store.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ import {
301301
installLFSHooks,
302302
isUsingLFS,
303303
} from '../git/lfs'
304+
import { getConfigValueWithOrigin, IConfigValueOrigin } from '../git/config'
304305
import { determineMergeability } from '../git/merge-tree'
305306
import { listWorktrees } from '../git/worktree'
306307
import { reorder } from '../git/reorder'
@@ -464,6 +465,9 @@ const hideWhitespaceInPullRequestDiffKey =
464465
const commitSpellcheckEnabledDefault = true
465466
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
466467

468+
const showCommitAuthorInfoDefault = false
469+
const showCommitAuthorInfoKey = 'show-commit-author-info'
470+
467471
export const tabSizeDefault: number = 4
468472
const tabSizeKey: string = 'tab-size'
469473

@@ -610,6 +614,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
610614
hideWhitespaceInPullRequestDiffDefault
611615
/** Whether or not the spellchecker is enabled for commit summary and description */
612616
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
617+
private showCommitAuthorInfo: boolean = showCommitAuthorInfoDefault
613618
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
614619

615620
private uncommittedChangesStrategy = defaultUncommittedChangesStrategy
@@ -1226,6 +1231,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
12261231
repositoryIndicatorsEnabled: this.repositoryIndicatorsEnabled,
12271232
hideWindowOnQuit: this.hideWindowOnQuit,
12281233
commitSpellcheckEnabled: this.commitSpellcheckEnabled,
1234+
showCommitAuthorInfo: this.showCommitAuthorInfo,
12291235
currentDragElement: this.currentDragElement,
12301236
lastThankYou: this.lastThankYou,
12311237
useCustomEditor: this.useCustomEditor,
@@ -2592,6 +2598,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
25922598
commitSpellcheckEnabledKey,
25932599
commitSpellcheckEnabledDefault
25942600
)
2601+
this.showCommitAuthorInfo = getBoolean(
2602+
showCommitAuthorInfoKey,
2603+
showCommitAuthorInfoDefault
2604+
)
25952605
this.showSideBySideDiff = getShowSideBySideDiff()
25962606

25972607
this.selectedTheme = getPersistedThemeName()
@@ -4227,6 +4237,17 @@ export class AppStore extends TypedBaseStore<IAppState> {
42274237
this.emitUpdate()
42284238
}
42294239

4240+
public _setShowCommitAuthorInfo(showCommitAuthorInfo: boolean) {
4241+
if (this.showCommitAuthorInfo === showCommitAuthorInfo) {
4242+
return
4243+
}
4244+
4245+
setBoolean(showCommitAuthorInfoKey, showCommitAuthorInfo)
4246+
this.showCommitAuthorInfo = showCommitAuthorInfo
4247+
4248+
this.emitUpdate()
4249+
}
4250+
42304251
public _setUseWindowsOpenSSH(useWindowsOpenSSH: boolean) {
42314252
setBoolean(UseWindowsOpenSSHKey, useWindowsOpenSSH)
42324253
this.useWindowsOpenSSH = useWindowsOpenSSH
@@ -4299,8 +4320,22 @@ export class AppStore extends TypedBaseStore<IAppState> {
42994320
getAuthorIdentity(repository)
43004321
)) || null
43014322

4323+
let commitAuthorNameOrigin: IConfigValueOrigin | null = null
4324+
let commitAuthorEmailOrigin: IConfigValueOrigin | null = null
4325+
4326+
try {
4327+
;[commitAuthorNameOrigin, commitAuthorEmailOrigin] = await Promise.all([
4328+
getConfigValueWithOrigin(repository, 'user.name'),
4329+
getConfigValueWithOrigin(repository, 'user.email'),
4330+
])
4331+
} catch (e) {
4332+
log.warn('Failed to get config value origins', e)
4333+
}
4334+
43024335
this.repositoryStateCache.update(repository, () => ({
43034336
commitAuthor,
4337+
commitAuthorNameOrigin,
4338+
commitAuthorEmailOrigin,
43044339
}))
43054340
this.emitUpdate()
43064341
}

app/src/lib/stores/repository-state-cache.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ function getInitialRepositoryState(): IRepositoryState {
375375
},
376376
pullRequestState: null,
377377
commitAuthor: null,
378+
commitAuthorNameOrigin: null,
379+
commitAuthorEmailOrigin: null,
378380
commitLookup: new Map<string, Commit>(),
379381
localCommitSHAs: [],
380382
localTags: null,

app/src/ui/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,7 @@ export class App extends React.Component<IAppProps, IAppState> {
16721672
selectedExternalEditor={this.state.selectedExternalEditor}
16731673
useWindowsOpenSSH={this.state.useWindowsOpenSSH}
16741674
showCommitLengthWarning={this.state.showCommitLengthWarning}
1675+
showCommitAuthorInfo={this.state.showCommitAuthorInfo}
16751676
notificationsEnabled={this.state.notificationsEnabled}
16761677
optOutOfUsageTracking={this.state.optOutOfUsageTracking}
16771678
useExternalCredentialHelper={this.state.useExternalCredentialHelper}
@@ -3735,6 +3736,7 @@ export class App extends React.Component<IAppProps, IAppState> {
37353736
aheadBehindStore={this.props.aheadBehindStore}
37363737
commitSpellcheckEnabled={this.state.commitSpellcheckEnabled}
37373738
showCommitLengthWarning={this.state.showCommitLengthWarning}
3739+
showCommitAuthorInfo={this.state.showCommitAuthorInfo}
37383740
onCherryPick={this.startCherryPickWithoutBranch}
37393741
pullRequestSuggestedNextAction={state.pullRequestSuggestedNextAction}
37403742
showChangesFilter={state.showChangesFilter}

0 commit comments

Comments
 (0)