mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
* 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.
113 lines
3.9 KiB
TypeScript
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: [] }
|
|
}
|
|
}
|