Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/src/lib/app-state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Account } from '../models/account'
import { CommitIdentity } from '../models/commit-identity'
import { IConfigValueOrigin } from './git/config'
import { IDiff, ImageDiffType } from '../models/diff'
import { Repository, ILocalRepositoryState } from '../models/repository'
import { Branch, IAheadBehind } from '../models/branch'
Expand Down Expand Up @@ -363,6 +364,8 @@ export interface IAppState {
*/
readonly commitSpellcheckEnabled: boolean

readonly showCommitAuthorInfo: boolean

/**
* Record of what logged in users have been checked to see if thank you is in
* order for external contributions in latest release.
Expand Down Expand Up @@ -562,6 +565,9 @@ export interface IRepositoryState {
*/
readonly commitAuthor: CommitIdentity | null

readonly commitAuthorNameOrigin: IConfigValueOrigin | null
readonly commitAuthorEmailOrigin: IConfigValueOrigin | null

readonly branchesState: IBranchesState

readonly worktreesState: IWorktreesState
Expand Down
108 changes: 108 additions & 0 deletions app/src/lib/git/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,111 @@ async function removeConfigValueInPath(

await git(flags, path || __dirname, 'removeConfigValueInPath', options)
}

export interface IConfigValueOrigin {
readonly value: string
readonly scope: string
readonly origin: string
}

/**
* Look up a config value along with its source file and scope.
* Requires Git 2.26+ for --show-scope.
*/
export async function getConfigValueWithOrigin(
repository: Repository,
name: string
): Promise<IConfigValueOrigin | null> {
const result = await git(
['config', '--show-origin', '--show-scope', '-z', name],
repository.path,
'getConfigValueWithOrigin',
// 0 = found, 1 = key not set, 128 = not a git repo or git error
{ successExitCodes: new Set([0, 1, 128]) }
)

if (result.exitCode !== 0) {
return null
}

const parts = result.stdout.split('\0')
if (parts.length >= 3) {
return {
scope: parts[0],
origin: parts[1],
value: parts[2],
}
}

return null
}

/**
* Extract the file path from a config value origin, stripping the `file:` prefix.
* When repositoryPath is provided, relative paths (e.g. `.git/config` for local
* scope) are resolved to absolute paths.
*/
export function getOriginFilePath(
origin: IConfigValueOrigin,
repositoryPath?: string
): string {
const filePath = origin.origin.replace(/^file:/, '')
// Git returns relative paths for local/worktree scope (e.g. `.git/config`)
if (repositoryPath && !/^([a-zA-Z]:|[/\\])/.test(filePath)) {
const base = repositoryPath.replace(/[\\/]+$/, '')
return `${base}/${filePath}`
}
return filePath
}

/**
* Check whether a global-scoped config value comes from a conditionally
* included file (via includeIf directive) rather than a standard location.
*/
export function isConditionalInclude(origin: IConfigValueOrigin): boolean {
if (origin.scope !== 'global') {
return false
}
const filePath = getOriginFilePath(origin)
return (
!/[/\\]\.gitconfig$/i.test(filePath) &&
!/[/\\]\.config[/\\]git[/\\]config$/i.test(filePath)
)
}

/** Format a human-readable scope description for a config value origin. */
export function formatConfigScope(origin: IConfigValueOrigin): string {
if (origin.scope === 'local') {
return 'local'
} else if (origin.scope === 'system') {
return 'system'
} else if (origin.scope === 'worktree') {
return 'worktree'
} else if (origin.scope === 'global') {
return isConditionalInclude(origin) ? 'global, via [includeIf]' : 'global'
}
return origin.scope
}

/**
* Format the file path for a config value origin.
* For local/worktree scope, displays the path with a `<repo>` prefix.
*/
export function formatConfigPath(
origin: IConfigValueOrigin,
repositoryPath: string
): string {
const rawPath = origin.origin.replace(/^file:/, '')
if (origin.scope === 'local' || origin.scope === 'worktree') {
// Git returns relative paths for local scope (e.g. `.git/config`)
if (!/^([a-zA-Z]:|[/\\])/.test(rawPath)) {
return '<repo>/' + rawPath
}
// Absolute path — strip repo prefix
const normalized = repositoryPath.replace(/[\\/]+$/, '')
if (rawPath.toLowerCase().startsWith(normalized.toLowerCase())) {
return '<repo>' + rawPath.slice(normalized.length)
}
}
return rawPath
}
35 changes: 35 additions & 0 deletions app/src/lib/stores/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ import {
installLFSHooks,
isUsingLFS,
} from '../git/lfs'
import { getConfigValueWithOrigin, IConfigValueOrigin } from '../git/config'
import { determineMergeability } from '../git/merge-tree'
import { listWorktrees } from '../git/worktree'
import { reorder } from '../git/reorder'
Expand Down Expand Up @@ -464,6 +465,9 @@ const hideWhitespaceInPullRequestDiffKey =
const commitSpellcheckEnabledDefault = true
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'

const showCommitAuthorInfoDefault = false
const showCommitAuthorInfoKey = 'show-commit-author-info'

export const tabSizeDefault: number = 4
const tabSizeKey: string = 'tab-size'

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

private uncommittedChangesStrategy = defaultUncommittedChangesStrategy
Expand Down Expand Up @@ -1224,6 +1229,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
repositoryIndicatorsEnabled: this.repositoryIndicatorsEnabled,
hideWindowOnQuit: this.hideWindowOnQuit,
commitSpellcheckEnabled: this.commitSpellcheckEnabled,
showCommitAuthorInfo: this.showCommitAuthorInfo,
currentDragElement: this.currentDragElement,
lastThankYou: this.lastThankYou,
useCustomEditor: this.useCustomEditor,
Expand Down Expand Up @@ -2590,6 +2596,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
commitSpellcheckEnabledKey,
commitSpellcheckEnabledDefault
)
this.showCommitAuthorInfo = getBoolean(
showCommitAuthorInfoKey,
showCommitAuthorInfoDefault
)
this.showSideBySideDiff = getShowSideBySideDiff()

this.selectedTheme = getPersistedThemeName()
Expand Down Expand Up @@ -4225,6 +4235,17 @@ export class AppStore extends TypedBaseStore<IAppState> {
this.emitUpdate()
}

public _setShowCommitAuthorInfo(showCommitAuthorInfo: boolean) {
if (this.showCommitAuthorInfo === showCommitAuthorInfo) {
return
}

setBoolean(showCommitAuthorInfoKey, showCommitAuthorInfo)
this.showCommitAuthorInfo = showCommitAuthorInfo

this.emitUpdate()
}

public _setUseWindowsOpenSSH(useWindowsOpenSSH: boolean) {
setBoolean(UseWindowsOpenSSHKey, useWindowsOpenSSH)
this.useWindowsOpenSSH = useWindowsOpenSSH
Expand Down Expand Up @@ -4297,8 +4318,22 @@ export class AppStore extends TypedBaseStore<IAppState> {
getAuthorIdentity(repository)
)) || null

let commitAuthorNameOrigin: IConfigValueOrigin | null = null
let commitAuthorEmailOrigin: IConfigValueOrigin | null = null

try {
;[commitAuthorNameOrigin, commitAuthorEmailOrigin] = await Promise.all([
getConfigValueWithOrigin(repository, 'user.name'),
getConfigValueWithOrigin(repository, 'user.email'),
])
} catch (e) {
log.warn('Failed to get config value origins', e)
}

this.repositoryStateCache.update(repository, () => ({
commitAuthor,
commitAuthorNameOrigin,
commitAuthorEmailOrigin,
}))
this.emitUpdate()
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/lib/stores/repository-state-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,8 @@ function getInitialRepositoryState(): IRepositoryState {
},
pullRequestState: null,
commitAuthor: null,
commitAuthorNameOrigin: null,
commitAuthorEmailOrigin: null,
commitLookup: new Map<string, Commit>(),
localCommitSHAs: [],
localTags: null,
Expand Down
2 changes: 2 additions & 0 deletions app/src/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,7 @@ export class App extends React.Component<IAppProps, IAppState> {
selectedExternalEditor={this.state.selectedExternalEditor}
useWindowsOpenSSH={this.state.useWindowsOpenSSH}
showCommitLengthWarning={this.state.showCommitLengthWarning}
showCommitAuthorInfo={this.state.showCommitAuthorInfo}
notificationsEnabled={this.state.notificationsEnabled}
optOutOfUsageTracking={this.state.optOutOfUsageTracking}
useExternalCredentialHelper={this.state.useExternalCredentialHelper}
Expand Down Expand Up @@ -3735,6 +3736,7 @@ export class App extends React.Component<IAppProps, IAppState> {
aheadBehindStore={this.props.aheadBehindStore}
commitSpellcheckEnabled={this.state.commitSpellcheckEnabled}
showCommitLengthWarning={this.state.showCommitLengthWarning}
showCommitAuthorInfo={this.state.showCommitAuthorInfo}
onCherryPick={this.startCherryPickWithoutBranch}
pullRequestSuggestedNextAction={state.pullRequestSuggestedNextAction}
showChangesFilter={state.showChangesFilter}
Expand Down
Loading
Loading