Files
orca/src/main/git/source-control/branch-compare.ts
T
Jinjing 933345d347 Clarify upstream divergence stats for rebased branches (#16358)
* Clarify upstream divergence stats for rebased branches

When a branch is rebased, it still tracks the pre-rebase upstream
while comparing against the new base. Move upstream arrows to the
head line to prevent them being confused with compare-base counts.

* Show upstream divergence stats independent of compare base

Measure HEAD against upstream regardless of compare-base state,
so divergence indicators stay visible even when comparison is
missing, loading, or failed. Also use cross-platform temp paths
in tests.

* Show commit counts against compare base, not upstream

Upstream divergence (↑/↓ against tracking branch) was confusing for
rebased branches — the counts appeared beside the base ref but measured
against the upstream branch. Show only the compare base count instead,
on the line that names it.

* Report branch divergence in both directions

Rebased branches are typically ahead AND behind their base; a single count
hides this case. Use symmetric range with --left-right --count to capture
both directions efficiently, then expose commitsBehind in the UI alongside
commitsAhead.

* Use semantic names for i18n keys and template variables

Rename hash-based translation keys to descriptive identifiers and replace generic value0/value1 placeholders with semantic variable names like `count` and `ref`. Improves code maintainability and makes translation strings self-documenting.
2026-08-25 22:19:04 -07:00

113 lines
3.9 KiB
TypeScript

import type {
GitBranchCompareResult,
GitBranchCompareSummary
} from '../../../shared/git-diff-compare-types'
import { readBranchCompareHead } from '../../../shared/git-branch-compare-head'
import { resolveWorktreeAddBaseRef } from '../../../shared/worktree/base-ref'
import type { GitRuntimeOptions } from '../git-runtime-options'
import { resolveWorktreeBaseCommitOid } from '../worktree-base-ref-probe'
import { loadBranchChanges } from './branch-change-entries'
import {
countCompareDivergence,
resolveCompareRef,
resolveMergeBase,
resolveRefOid
} from './compare-ref-oids'
export async function getBranchCompare(
worktreePath: string,
baseRef: string,
options: GitRuntimeOptions = {}
): Promise<GitBranchCompareResult> {
const summary: GitBranchCompareSummary = {
baseRef,
baseOid: null,
compareRef: 'HEAD',
headOid: null,
mergeBase: null,
changedFiles: 0,
status: 'loading'
}
// The base-ref probe peels to a commit. Only branch refs are guaranteed to store
// commits; remote-tracking refs may store annotated tags whose raw oid must be preserved.
const reusableProbedOidByRef = new Map<string, string>()
const { compareRef, headOidResult, baseOidResult } = await readBranchCompareHead({
readCompareRef: () => resolveCompareRef(worktreePath, options),
resolveBaseRef: () =>
// Why: short refs like "origin/main" can collide with a local branch; use the proven remote-tracking ref.
resolveWorktreeAddBaseRef(baseRef, async (qualifiedRef) => {
const oid = await resolveWorktreeBaseCommitOid(worktreePath, qualifiedRef, options)
if (oid !== null && qualifiedRef.startsWith('refs/heads/')) {
reusableProbedOidByRef.set(qualifiedRef, oid)
}
return oid !== null
}),
readHeadOid: () => resolveRefOid(worktreePath, 'HEAD', options),
readBaseOid: (ref) => {
const reusableOid = reusableProbedOidByRef.get(ref)
return reusableOid === undefined
? resolveRefOid(worktreePath, ref, options)
: Promise.resolve(reusableOid)
}
})
summary.compareRef = compareRef
let headOid = ''
let baseOid = ''
if (headOidResult.ok) {
headOid = headOidResult.oid
summary.headOid = headOid
} else {
if (baseOidResult.ok) {
baseOid = baseOidResult.oid
summary.baseOid = baseOid
// Why: an unborn branch (new remote worktree) has no changes yet; a compare error would look broken.
summary.changedFiles = 0
summary.commitsAhead = 0
summary.commitsBehind = 0
summary.status = 'ready'
return { summary, entries: [] }
}
summary.status = 'unborn-head'
summary.errorMessage =
'This branch does not have a committed HEAD yet, so compare-to-base is unavailable.'
return { summary, entries: [] }
}
if (baseOidResult.ok) {
baseOid = baseOidResult.oid
summary.baseOid = baseOid
} else {
summary.status = 'invalid-base'
summary.errorMessage = `Base ref ${baseRef} could not be resolved in this repository.`
return { summary, entries: [] }
}
let mergeBase = ''
try {
mergeBase = await resolveMergeBase(worktreePath, baseOid, headOid, options)
summary.mergeBase = mergeBase
} catch {
summary.status = 'no-merge-base'
summary.errorMessage = `This branch and ${baseRef} do not share a merge base, so compare-to-base is unavailable.`
return { summary, entries: [] }
}
try {
const [entries, divergence] = await Promise.all([
loadBranchChanges(worktreePath, mergeBase, headOid, options),
countCompareDivergence(worktreePath, baseOid, headOid, options)
])
summary.changedFiles = entries.length
summary.commitsAhead = divergence.ahead
summary.commitsBehind = divergence.behind
summary.status = 'ready'
return { summary, entries }
} catch (error) {
summary.status = 'error'
summary.errorMessage = error instanceof Error ? error.message : 'Failed to load branch compare'
return { summary, entries: [] }
}
}