diff --git a/package-lock.json b/package-lock.json index 518fdee..2cada43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "git-graph-plus", - "version": "0.3.12", + "version": "0.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "git-graph-plus", - "version": "0.3.12", + "version": "0.7.3", "license": "Apache-2.0", "dependencies": { "@vscode/codicons": "^0.0.45" @@ -1417,7 +1417,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -1567,7 +1566,6 @@ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1985,7 +1983,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3439,7 +3436,6 @@ "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -5581,7 +5577,6 @@ "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -5994,7 +5989,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6244,7 +6238,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6361,7 +6354,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6395,7 +6387,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/src/git/__tests__/integration/basic.integration.test.ts b/src/git/__tests__/integration/basic.integration.test.ts index 5c594ac..591f9b6 100644 --- a/src/git/__tests__/integration/basic.integration.test.ts +++ b/src/git/__tests__/integration/basic.integration.test.ts @@ -32,6 +32,18 @@ describe('GitService integration — basic queries', () => { expect(commits.length).toBeLessThanOrEqual(2); }); + it('includeReflog surfaces commits only reachable via the reflog', async () => { + const c1 = commit(repo.path, 'first'); + const c2 = commit(repo.path, 'second'); + runGit(repo.path, ['reset', '--hard', c1]); + + const without = await svc.log(); + expect(without.map(c => c.hash)).not.toContain(c2); + + const withReflog = await svc.log({ includeReflog: true }); + expect(withReflog.map(c => c.hash)).toContain(c2); + }); + it('does not prepend the UNCOMMITTED row on paginated (skip>0) pages', async () => { for (let i = 0; i < 5; i++) commit(repo.path, `c${i}`, { 'a.txt': `${i}\n` }); // Dirty working tree so the first page would carry an UNCOMMITTED row. diff --git a/src/git/__tests__/integration/user-config.integration.test.ts b/src/git/__tests__/integration/user-config.integration.test.ts new file mode 100644 index 0000000..e129442 --- /dev/null +++ b/src/git/__tests__/integration/user-config.integration.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { GitService } from '../../git-service'; +import { createTempRepo, runGit, type TempRepo } from './helpers'; + +describe('GitService integration — user config', () => { + let repo: TempRepo; + let svc: GitService; + + beforeEach(() => { + repo = createTempRepo(); + svc = new GitService(repo.path); + // Isolate the global/system scopes so global reads are deterministic + // (otherwise they'd reflect the developer's own ~/.gitconfig). + svc.setExtraEnv({ GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' }); + }); + afterEach(() => repo.cleanup()); + + it('reads local user.name/user.email', async () => { + const details = await svc.getUserDetails(); + expect(details.name.local).toBe('Test User'); + expect(details.email.local).toBe('test@example.com'); + expect(details.name.global).toBeNull(); + expect(details.email.global).toBeNull(); + }); + + it('returns null for keys that are not set', async () => { + runGit(repo.path, ['config', '--local', '--unset-all', 'user.name']); + const details = await svc.getUserDetails(); + expect(details.name.local).toBeNull(); + expect(details.email.local).toBe('test@example.com'); + }); + + it('setUserConfig writes to the local scope', async () => { + await svc.setUserConfig('user.name', 'Jane Doe', 'local'); + await svc.setUserConfig('user.email', 'jane@example.com', 'local'); + const details = await svc.getUserDetails(); + expect(details.name.local).toBe('Jane Doe'); + expect(details.email.local).toBe('jane@example.com'); + }); + + it('unsetUserConfig removes the key', async () => { + await svc.unsetUserConfig('user.name', 'local'); + const details = await svc.getUserDetails(); + expect(details.name.local).toBeNull(); + expect(details.email.local).toBe('test@example.com'); + }); + + it('setUserConfig rejects empty and flag-like values', async () => { + await expect(svc.setUserConfig('user.name', '', 'local')).rejects.toThrow(); + await expect(svc.setUserConfig('user.name', ' ', 'local')).rejects.toThrow(); + await expect(svc.setUserConfig('user.name', '-x', 'local')).rejects.toThrow(); + }); +}); diff --git a/src/git/git-service.ts b/src/git/git-service.ts index 71e6576..487e73c 100644 --- a/src/git/git-service.ts +++ b/src/git/git-service.ts @@ -15,7 +15,7 @@ import { resolveGitDirs } from '../services/file-watcher-helpers'; const DEFAULT_MAX_BUFFER_BYTES = 256 * 1024 * 1024; import { parseLog, parseBranches, parseTags, parseRemotes, parseStashList, parseDiff, parseWorktreeList, parseLfsFiles, parseLfsLocks, mapSignatureStatus } from './git-parser'; import { buildReversePatch } from './patch-builder'; -import type { Commit, BranchInfo, TagInfo, RemoteInfo, StashEntry, LogOptions, DiffData, WorktreeInfo, CommitSignature } from './types'; +import type { Commit, BranchInfo, TagInfo, RemoteInfo, StashEntry, LogOptions, DiffData, WorktreeInfo, CommitSignature, UserDetails } from './types'; export class GitError extends Error { constructor( @@ -551,6 +551,20 @@ export class GitService { } } + // `--reflog` pulls in commits that are reachable only via the reflog + // (abandoned by a rebase/reset/branch delete). Same scope restriction as the + // stash base hashes above: only the unfiltered first page, so the extra + // commits don't leak into branch/remote-filtered or paginated views. + if ( + options?.includeReflog && + !options?.skip && + !options?.branch && + (!options?.branches || options.branches.length === 0) && + (!options?.remoteFilter || options.remoteFilter.length === 0) + ) { + args.push('--reflog'); + } + const [raw, remoteNames] = await Promise.all([ this.exec(args), this.getRemoteNames(), @@ -1526,6 +1540,58 @@ export class GitService { this.cachedRemoteNames = null; } + /** Reads the `user.name` / `user.email` git config from both the local repo + * and the global user scope. Missing keys resolve to `null` (git `--get` + * exits non-zero when a key is absent, which we swallow here). */ + async getUserDetails(): Promise { + const get = async (key: 'user.name' | 'user.email', location: 'local' | 'global'): Promise => { + try { + const raw = await this.exec(['config', '--' + location, '--get', key], { silent: true }); + const value = raw.replace(/\r?\n$/, '').trim(); + return value.length > 0 ? value : null; + } catch { + return null; + } + }; + const [nameLocal, nameGlobal, emailLocal, emailGlobal] = await Promise.all([ + get('user.name', 'local'), + get('user.name', 'global'), + get('user.email', 'local'), + get('user.email', 'global'), + ]); + return { + name: { local: nameLocal, global: nameGlobal }, + email: { local: emailLocal, global: emailGlobal }, + }; + } + + /** Sets a `user.name` / `user.email` value in the local or global scope. */ + async setUserConfig(key: 'user.name' | 'user.email', value: string, location: 'local' | 'global'): Promise { + this.assertSafeConfigValue(value); + await this.exec(['config', '--' + location, key, value]); + } + + /** Removes all `user.name` / `user.email` values from the local or global scope. */ + async unsetUserConfig(key: 'user.name' | 'user.email', location: 'local' | 'global'): Promise { + await this.exec(['config', '--' + location, '--unset-all', key]); + } + + /** Reject config values git could misinterpret (flag-like, control chars) or + * that make no sense as an identity. Args are passed via spawn argv (no + * shell), but a leading `-` would still be parsed by git as an option. */ + private assertSafeConfigValue(value: string): void { + if (typeof value !== 'string' || value.trim().length === 0) { + throw new GitError('Invalid config value', null, []); + } + if (value.startsWith('-')) { + throw new GitError(`Config value must not start with '-': ${value}`, null, []); + } + // eslint-disable-next-line no-control-regex + if (/[\x00-\x1f\x7f]/.test(value)) { + throw new GitError('Config value contains control characters', null, []); + } + } + async setUpstream(localBranch: string, remote: string, remoteBranch: string, options?: { createRemote?: boolean }): Promise { this.assertSafeRef(localBranch, 'setUpstream'); this.assertSafeRef(remote, 'setUpstream'); diff --git a/src/git/types.ts b/src/git/types.ts index feed7ab..156579e 100644 --- a/src/git/types.ts +++ b/src/git/types.ts @@ -35,6 +35,13 @@ export interface Ref { remote?: string; } +/** The author/committer identity git uses, resolved per scope (`local` repo + * config vs `global` user config). Either scope may be unset (`null`). */ +export interface UserDetails { + name: { local: string | null; global: string | null }; + email: { local: string | null; global: string | null }; +} + export interface GraphNode { commit: string; column: number; @@ -171,4 +178,8 @@ export interface LogOptions { * signatureStatus. Off by default — it forces GPG verification of every * commit in the log, which is slow on large repos. */ includeSignature?: boolean; + /** When true, add `--reflog` so commits reachable only via the reflog + * (abandoned by rebase/reset/branch delete) appear in the graph. Only + * applies on the unfiltered first page, mirroring the stash base hashes. */ + includeReflog?: boolean; } diff --git a/src/panels/MainPanel.ts b/src/panels/MainPanel.ts index 5869905..ad16265 100644 --- a/src/panels/MainPanel.ts +++ b/src/panels/MainPanel.ts @@ -46,6 +46,7 @@ export class MainPanel { private currentLimit = 1000; private currentRemoteFilter: string[] | undefined = undefined; private currentBranchFilter: string[] | undefined = undefined; + private currentIncludeReflog = false; private isFirstGetLog = true; private logSequence = 0; private searchSequence = 0; @@ -416,7 +417,8 @@ export class MainPanel { this.isFirstGetLog = false; this.currentRemoteFilter = effectiveFilter; this.currentBranchFilter = effectiveBranchFilter; - const logPayload = { ...message.payload, remoteFilter: effectiveFilter, branches: effectiveBranchFilter, limit: requestedLimit + 1, sortOrder, includeSignature }; + this.currentIncludeReflog = message.payload.includeReflog ?? false; + const logPayload = { ...message.payload, remoteFilter: effectiveFilter, branches: effectiveBranchFilter, includeReflog: this.currentIncludeReflog, limit: requestedLimit + 1, sortOrder, includeSignature }; const seq = ++this.logSequence; const [allFetched, logBranches] = await Promise.all([ this.gitService.log(logPayload), @@ -775,6 +777,32 @@ export class MainPanel { await vscode.commands.executeCommand('workbench.action.openSettings', '@ext:the0807.git-graph-plus'); break; } + case 'getUserDetails': { + this.post({ type: 'userDetailsData', payload: await this.gitService.getUserDetails() }); + break; + } + case 'editUserDetails': { + const { name, email, location, deleteLocalName, deleteLocalEmail } = message.payload; + await this.gitService.setUserConfig('user.name', name, location); + await this.gitService.setUserConfig('user.email', email, location); + // When switching to the global scope, clear any local override so the + // global values actually take effect for this repo. + if (deleteLocalName) { + await this.gitService.unsetUserConfig('user.name', 'local'); + } + if (deleteLocalEmail) { + await this.gitService.unsetUserConfig('user.email', 'local'); + } + this.post({ type: 'operationComplete', payload: { operation: 'editUserDetails', success: true } }); + break; + } + case 'deleteUserDetails': { + const { name, email, location } = message.payload; + if (name) await this.gitService.unsetUserConfig('user.name', location); + if (email) await this.gitService.unsetUserConfig('user.email', location); + this.post({ type: 'operationComplete', payload: { operation: 'deleteUserDetails', success: true } }); + break; + } case 'amendCommit': { await this.gitService.amendCommit(message.payload); // Optional follow-up: amend rewrites HEAD, so the push force-pushes @@ -1790,7 +1818,7 @@ export class MainPanel { // repo-unrelated "demo"-looking graph. const remoteFilter = this.isFirstGetLog ? MainPanel.savedRemoteFilter : this.currentRemoteFilter; const branchFilter = this.isFirstGetLog ? MainPanel.savedBranchFilter : this.currentBranchFilter; - const logArgs = { limit: refreshLimit + 1, sortOrder, remoteFilter, branches: branchFilter, includeSignature }; + const logArgs = { limit: refreshLimit + 1, sortOrder, remoteFilter, branches: branchFilter, includeReflog: this.currentIncludeReflog, includeSignature }; const buildLogData = (allFetched: Awaited>, branches: Awaited>) => { const hasMore = allFetched.length > refreshLimit; diff --git a/src/utils/message-bus.ts b/src/utils/message-bus.ts index deec2ab..ee0de82 100644 --- a/src/utils/message-bus.ts +++ b/src/utils/message-bus.ts @@ -1,4 +1,4 @@ -import type { CommitGraphData, BranchData, DiffData, Commit, WorktreeInfo, CommitSignature } from '../git/types'; +import type { CommitGraphData, BranchData, DiffData, Commit, WorktreeInfo, CommitSignature, UserDetails } from '../git/types'; export interface LinkRule { pattern: string; @@ -27,7 +27,7 @@ export interface ModalDefaults { // Messages from Webview → Extension export type WebviewMessage = - | { type: 'getLog'; payload: { branch?: string; branches?: string[]; limit?: number; skip?: number; remoteFilter?: string[] } } + | { type: 'getLog'; payload: { branch?: string; branches?: string[]; limit?: number; skip?: number; remoteFilter?: string[]; includeReflog?: boolean } } | { type: 'getBranches' } | { type: 'getRepoList' } | { type: 'checkDirty'; payload?: { requestId?: string } } @@ -127,7 +127,10 @@ export type WebviewMessage = | { type: 'getMultiCommitSections'; payload: { hashes: string[] } } | { type: 'getAvatar'; payload: { email: string; size: number } } | { type: 'openExternalUrl'; payload: { url: string } } - | { type: 'openExtensionSettings' }; + | { type: 'openExtensionSettings' } + | { type: 'getUserDetails' } + | { type: 'editUserDetails'; payload: { name: string; email: string; location: 'local' | 'global'; deleteLocalName?: boolean; deleteLocalEmail?: boolean } } + | { type: 'deleteUserDetails'; payload: { name: boolean; email: boolean; location: 'local' | 'global' } }; // Messages from Extension → Webview export type ExtensionMessage = @@ -169,6 +172,7 @@ export type ExtensionMessage = | { type: 'avatarData'; payload: { email: string; size: number; dataUri: string | null } } | { type: 'conflictData'; payload: { operation: string; files: Array<{ path: string; resolved: boolean }> } } | { type: 'flowStatus'; payload: { installed: boolean; initialized: boolean; config: { productionBranch: string; developBranch: string; featurePrefix: string; releasePrefix: string; hotfixPrefix: string; versionTagPrefix: string } | null } } + | { type: 'userDetailsData'; payload: UserDetails } | { type: 'flowBranches'; payload: { features: string[]; releases: string[]; hotfixes: string[] } } | { type: 'defaultBranch'; payload: { name: string | null } } | { type: 'showModal'; payload: diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 6050171..13fd799 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -985,7 +985,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -1335,7 +1334,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1718,7 +1716,6 @@ "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -2037,7 +2034,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2300,7 +2296,6 @@ "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2424,7 +2419,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2542,7 +2536,6 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2661,7 +2654,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/webview-ui/src/App.svelte b/webview-ui/src/App.svelte index c649320..4d7cc32 100644 --- a/webview-ui/src/App.svelte +++ b/webview-ui/src/App.svelte @@ -60,6 +60,7 @@ import AmendModal from './components/modals/AmendModal.svelte'; let headJumpNonce = $state(0); let remoteFilter = $state([]); let branchFilter = $state([]); + let includeReflog = $state(false); let resizing = $state(false); let conflict = $state<{ operation: string; files: Array<{ path: string; resolved: boolean }> } | null>(null); let rebasePaused = $state(false); @@ -262,6 +263,7 @@ import AmendModal from './components/modals/AmendModal.svelte'; limit: commitStore.currentLimit || undefined, branches: branchFilter.length > 0 ? [...branchFilter] : undefined, remoteFilter: remoteFilter.length > 0 ? [...remoteFilter] : undefined, + includeReflog, }}); vscode.postMessage({ type: 'getBranches' }); } @@ -317,6 +319,7 @@ import AmendModal from './components/modals/AmendModal.svelte'; limit: commitStore.currentLimit || undefined, branches: branchFilter.length > 0 ? [...branchFilter] : undefined, remoteFilter: filter.length > 0 ? [...filter] : undefined, + includeReflog, }, }); } @@ -330,6 +333,21 @@ import AmendModal from './components/modals/AmendModal.svelte'; limit: commitStore.currentLimit || undefined, branches: branches.length > 0 ? [...branches] : undefined, remoteFilter: remoteFilter.length > 0 ? [...remoteFilter] : undefined, + includeReflog, + }, + }); + } + + function handleIncludeReflogChange(value: boolean) { + includeReflog = value; + commitStore.setLoading(true); + vscode.postMessage({ + type: 'getLog', + payload: { + limit: commitStore.currentLimit || undefined, + branches: branchFilter.length > 0 ? [...branchFilter] : undefined, + remoteFilter: remoteFilter.length > 0 ? [...remoteFilter] : undefined, + includeReflog: value, }, }); } @@ -370,6 +388,7 @@ import AmendModal from './components/modals/AmendModal.svelte'; limit: commitStore.currentLimit || undefined, branches: branchFilter.length > 0 ? [...branchFilter] : undefined, remoteFilter: remoteFilter.length > 0 ? [...remoteFilter] : undefined, + includeReflog, }}); vscode.postMessage({ type: 'getBranches' }); vscode.postMessage({ type: 'getRepoList' }); @@ -472,6 +491,8 @@ import AmendModal from './components/modals/AmendModal.svelte'; onBranchFilterChange={handleBranchFilterChange} {headOffscreen} onJumpToHead={handleJumpToHead} + {includeReflog} + onIncludeReflogChange={handleIncludeReflogChange} /> {/if} {#if bisectMessage} diff --git a/webview-ui/src/components/common/SearchBar.svelte b/webview-ui/src/components/common/SearchBar.svelte index eae13f3..7f05a31 100644 --- a/webview-ui/src/components/common/SearchBar.svelte +++ b/webview-ui/src/components/common/SearchBar.svelte @@ -16,6 +16,8 @@ onBranchFilterChange?: (filter: string[]) => void; headOffscreen?: boolean; onJumpToHead?: () => void; + includeReflog?: boolean; + onIncludeReflogChange?: (value: boolean) => void; } let { @@ -29,6 +31,8 @@ onBranchFilterChange = () => {}, headOffscreen = false, onJumpToHead = () => {}, + includeReflog = false, + onIncludeReflogChange = () => {}, }: Props = $props(); let query = $state(''); @@ -362,6 +366,16 @@ {/if} + + diff --git a/webview-ui/src/lib/i18n/en.ts b/webview-ui/src/lib/i18n/en.ts index 716013a..00b583c 100644 --- a/webview-ui/src/lib/i18n/en.ts +++ b/webview-ui/src/lib/i18n/en.ts @@ -15,6 +15,7 @@ export const en: Record = { 'toolbar.settings': 'Open Extension Settings', 'toolbar.noRemotes': 'No remotes configured. Add a remote first.', 'toolbar.addRemote': 'Add Remote', + 'toolbar.userDetails': 'User Details (name & email)', 'toolbar.detachedHead': '(Detached HEAD)', // Push modal @@ -294,6 +295,7 @@ export const en: Record = { 'search.branchFilterTooltip': 'Filter by specific branch', 'search.allBranches': 'All', 'search.filterBranches': 'Search', + 'search.includeReflogTooltip': 'Include commits only mentioned by reflogs', // Activity log 'activityLog.title': 'Activity Log', @@ -676,4 +678,18 @@ export const en: Record = { 'setUpstream.willCreate.post': ' will create it.', 'setUpstream.typeManually': 'Type manually', 'setUpstream.selectFromList': 'Select from list', + + // User details modal + 'userDetails.title': 'User Details', + 'userDetails.desc': 'Set the user name and email used by Git to record the Author and Committer of commits.', + 'userDetails.loading': 'Loading…', + 'userDetails.currentScope': 'Currently stored in', + 'userDetails.local': 'Local', + 'userDetails.global': 'Global', + 'userDetails.name': 'User Name', + 'userDetails.email': 'User Email', + 'userDetails.useGlobally': 'Use globally', + 'userDetails.useGloballyInfo': 'Apply to all repositories. Can be overridden per repository.', + 'userDetails.save': 'Save', + 'userDetails.remove': 'Remove', }; diff --git a/webview-ui/src/lib/i18n/ko.ts b/webview-ui/src/lib/i18n/ko.ts index 1137893..6c93763 100644 --- a/webview-ui/src/lib/i18n/ko.ts +++ b/webview-ui/src/lib/i18n/ko.ts @@ -15,6 +15,7 @@ export const ko: Record = { 'toolbar.settings': '확장 프로그램 설정 열기', 'toolbar.noRemotes': '설정된 리모트가 없습니다. 먼저 리모트를 추가하세요.', 'toolbar.addRemote': '리모트 추가', + 'toolbar.userDetails': '사용자 정보 (이름 및 이메일)', 'toolbar.detachedHead': '(Detached HEAD)', // Push modal @@ -294,6 +295,7 @@ export const ko: Record = { 'search.branchFilterTooltip': '특정 브랜치로 필터', 'search.allBranches': 'All', 'search.filterBranches': 'Search', + 'search.includeReflogTooltip': 'reflog에만 언급된 커밋 포함', // Activity log 'activityLog.title': '활동 로그', @@ -674,4 +676,18 @@ export const ko: Record = { 'setUpstream.willCreate.post': '를 실행하여 새로 생성합니다.', 'setUpstream.typeManually': '직접 입력', 'setUpstream.selectFromList': '목록에서 선택', + + // User details modal + 'userDetails.title': '사용자 정보', + 'userDetails.desc': '커밋의 작성자와 커미터를 기록할 때 Git이 사용할 이름과 이메일을 설정합니다.', + 'userDetails.loading': '불러오는 중…', + 'userDetails.currentScope': '현재 저장 위치', + 'userDetails.local': '로컬', + 'userDetails.global': '전역', + 'userDetails.name': '이름', + 'userDetails.email': '이메일', + 'userDetails.useGlobally': '전역으로 사용', + 'userDetails.useGloballyInfo': '모든 저장소에 적용됩니다. 저장소별로 재정의할 수 있습니다.', + 'userDetails.save': '저장', + 'userDetails.remove': '제거', }; diff --git a/webview-ui/src/lib/i18n/zh.ts b/webview-ui/src/lib/i18n/zh.ts index 8d15a66..da81095 100644 --- a/webview-ui/src/lib/i18n/zh.ts +++ b/webview-ui/src/lib/i18n/zh.ts @@ -15,6 +15,7 @@ export const zh: Record = { 'toolbar.settings': '打开扩展设置', 'toolbar.noRemotes': '未配置远程仓库。请先添加一个远程仓库。', 'toolbar.addRemote': '添加远程仓库', + 'toolbar.userDetails': '用户信息(姓名和邮箱)', 'toolbar.detachedHead': '(分离头指针)', // Push modal @@ -294,6 +295,7 @@ export const zh: Record = { 'search.branchFilterTooltip': '按特定分支筛选', 'search.allBranches': 'All', 'search.filterBranches': 'Search', + 'search.includeReflogTooltip': '包含仅被 reflog 提及的提交', // Activity log 'activityLog.title': '活动日志', @@ -674,4 +676,18 @@ export const zh: Record = { 'setUpstream.willCreate.post': ' 进行创建。', 'setUpstream.typeManually': '手动输入', 'setUpstream.selectFromList': '从列表选择', + + // User details modal + 'userDetails.title': '用户信息', + 'userDetails.desc': '设置 Git 用于记录提交作者和提交者的姓名与邮箱。', + 'userDetails.loading': '加载中…', + 'userDetails.currentScope': '当前存储于', + 'userDetails.local': '本地', + 'userDetails.global': '全局', + 'userDetails.name': '姓名', + 'userDetails.email': '邮箱', + 'userDetails.useGlobally': '全局使用', + 'userDetails.useGloballyInfo': '应用到所有仓库。可被单个仓库覆盖。', + 'userDetails.save': '保存', + 'userDetails.remove': '移除', }; \ No newline at end of file diff --git a/webview-ui/src/lib/types.ts b/webview-ui/src/lib/types.ts index 7830c01..0a89e2e 100644 --- a/webview-ui/src/lib/types.ts +++ b/webview-ui/src/lib/types.ts @@ -156,6 +156,12 @@ export interface WorktreeInfo { isMain: boolean; } +/** The author/committer identity git uses, per scope (`local` vs `global`). */ +export interface UserDetails { + name: { local: string | null; global: string | null }; + email: { local: string | null; global: string | null }; +} + export interface FlowConfig { productionBranch: string; developBranch: string;