Files
orca/src/shared/git-history-ref-display.ts
T
Trevin ChowandJinjing a325ac467c feat: expandable commits and actions in the git history panel (#5419)
* feat: expandable commits and actions in the git history panel

Expand a commit row in the Commits panel to see its changed files inline; click a file to open that file's commit diff. Author and date surface on expand, so the dense row itself stays subject-only.

Right-click a commit for: open in the in-app browser, copy hash, copy message, and explain changes (spawns the default agent seeded with the commit context).

Open-in-browser resolves the provider commit URL in the main process via a new remoteCommitUrl resolver (GitHub/GitLab/Bitbucket), mirroring the existing remoteFileUrl chain end-to-end (repo, IPC, SSH provider, runtime RPC, preload) so it works for local and SSH/remote workspaces.

Layout: subject-first single-line rows with a tighter graph, refs moved inline, and local/remote ref pills deduped when they point at the same commit.

* fix: address git history review feedback

* fix: address PR review feedback on the git history panel

- Trim commit SHA before building the remote URL so whitespace input returns null instead of an invalid %20 URL.
- Gate commit-row expansion on the file loader (onLoadCommitFiles) so a row can't expand into a perpetual loading state.
- Harden the explain prompt: treat the commit subject and diff as untrusted data and run git show --no-ext-diff.
- Keep ambiguous multi-segment remote refs instead of mis-deduping them against a local branch.
- Use standard 10-char i18n keys for the new commit-history strings and translate them into es/ja/ko/zh.

* refactor: extract commit-history actions into useGitHistoryCommitActions hook

Moves the commit load/open/context-menu action callbacks (and the per-commit compare cache) out of SourceControl.tsx — which already carries a max-lines disable — into a focused hook, addressing the PR review nitpick. Behavior is unchanged.

* Refine git history row rendering, ref deduplication, and OID validation

- Prevent deduplication of remote branch badges in the history view
  when multiple remotes exist or when a ref is explicitly preserved.
- Render GitHistoryRow as an accessible button with dynamic ARIA labels
  for expansion states.
- Add double-click handler on commit files to open them permanently.
- Validate commit SHAs as full 40-character Git object IDs before
  requesting remote commit URLs.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-06-15 19:28:45 -07:00

67 lines
2.3 KiB
TypeScript

import { splitRemoteBranchName } from './git-effective-upstream'
import type { GitHistoryItemRef } from './git-history-types'
type DedupeRemoteTrackingRefsOptions = {
preserveRefIds?: ReadonlySet<string> | readonly string[]
}
// Drops a remote-tracking ref (e.g. origin/feature) when the matching local
// branch (feature) sits on the same commit. The two pills are redundant while
// local and remote point at the same commit; when they diverge they land on
// different commits and both still show.
export function dedupeRemoteTrackingRefs(
refs: readonly GitHistoryItemRef[],
options: DedupeRemoteTrackingRefsOptions = {}
): GitHistoryItemRef[] {
const localBranchNames = new Set(
refs.filter((ref) => ref.category === 'branches').map((ref) => ref.name)
)
if (localBranchNames.size === 0) {
return [...refs]
}
const preserveRefIds = new Set(options.preserveRefIds ?? [])
const matchingRemoteCounts = countUnambiguousMatchingRemoteBranches(refs, localBranchNames)
return refs.filter((ref) => {
if (ref.category !== 'remote branches') {
return true
}
if (preserveRefIds.has(ref.id)) {
return true
}
if (isAmbiguousRemoteTrackingRef(ref.name)) {
return true
}
const split = splitRemoteBranchName(ref.name)
if (!split || !localBranchNames.has(split.branchName)) {
return true
}
// Why: without the repo's configured upstream remote, multiple matching
// remotes (origin/main, upstream/main) are distinct context, not duplicates.
return matchingRemoteCounts.get(split.branchName) !== 1
})
}
function isAmbiguousRemoteTrackingRef(refName: string): boolean {
// Why: without configured remote names, `foo/bar/main` could be remote
// `foo` branch `bar/main` or remote `foo/bar` branch `main`.
return refName.split('/').length > 2
}
function countUnambiguousMatchingRemoteBranches(
refs: readonly GitHistoryItemRef[],
localBranchNames: ReadonlySet<string>
): Map<string, number> {
const counts = new Map<string, number>()
for (const ref of refs) {
if (ref.category !== 'remote branches' || isAmbiguousRemoteTrackingRef(ref.name)) {
continue
}
const split = splitRemoteBranchName(ref.name)
if (!split || !localBranchNames.has(split.branchName)) {
continue
}
counts.set(split.branchName, (counts.get(split.branchName) ?? 0) + 1)
}
return counts
}