mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
GIT_HISTORY_COMMIT_FORMAT asked for decorations with %(decorate:…), which Git 2.43 introduced. Older Git prints the placeholder verbatim and exits zero, so nothing raised and every commit in the Source Control panel silently lost its branch, remote and tag badges. The record now also carries %D (Git 2.10) on its own line, selected by an exact match against the unexpanded placeholder — a ref name can never contain the \x1f that Git expands inside the echoed text. %n emits the %D line on both sides of the boundary, so the message index is fixed and a missed match degrades to no badges rather than a corrupted message. The decoration separator is now bound to the field that produced the text instead of sniffed from it. A lone decoration carries no separator, so the old sniff split `refs/heads/feat,one` into two bogus refs. Verified against real Git 2.38.1 and 2.49.1. Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
166 lines
4.9 KiB
TypeScript
166 lines
4.9 KiB
TypeScript
import type { GitHistoryItem, GitHistoryItemRef } from './git-history-types'
|
|
import { iterateNulDelimitedFields } from './nul-delimited-fields'
|
|
|
|
const GIT_HISTORY_DECORATION_SEPARATOR = '\x1f'
|
|
const GIT_HISTORY_LEGACY_DECORATION_SEPARATOR = ','
|
|
|
|
// Why %D too: %(decorate:…) is Git 2.43+, and older Git echoes it verbatim and exits zero.
|
|
// Callers must pass --decorate=full; both fields emit short names otherwise, which parse to no refs.
|
|
export const GIT_HISTORY_COMMIT_FORMAT =
|
|
'%H%n%aN%n%aE%n%at%n%ct%n%P%n%(decorate:prefix=,suffix=,separator=%x1f)%n%D%n%B'
|
|
|
|
// Why exact-match: no ref name may contain the \x1f an old Git echoes here.
|
|
const UNEXPANDED_DECORATE_PLACEHOLDER = `%(decorate:prefix=,suffix=,separator=${GIT_HISTORY_DECORATION_SEPARATOR})`
|
|
|
|
export function shortGitHash(hash: string): string {
|
|
return hash.slice(0, 7)
|
|
}
|
|
|
|
function commitSubject(message: string): string {
|
|
const firstLine = message.split(/\r?\n/, 1)[0]?.trim()
|
|
return firstLine || '(no commit message)'
|
|
}
|
|
|
|
function parseGitDecorationRefs(
|
|
raw: string,
|
|
revision: string,
|
|
separator: string
|
|
): GitHistoryItemRef[] {
|
|
if (!raw.trim()) {
|
|
return []
|
|
}
|
|
|
|
const refs: GitHistoryItemRef[] = []
|
|
// Why passed in: a lone decoration carries no separator, so sniffing `raw` split `feat,one`.
|
|
const parts = raw.split(separator)
|
|
|
|
for (const part of parts) {
|
|
const ref = part.trim()
|
|
if (!ref || ref === 'HEAD' || /^refs\/remotes\/[^/]+\/HEAD(?:\s|$)/.test(ref)) {
|
|
continue
|
|
}
|
|
|
|
if (ref.startsWith('HEAD -> refs/heads/')) {
|
|
refs.push({
|
|
id: ref.slice('HEAD -> '.length),
|
|
name: ref.slice('HEAD -> refs/heads/'.length),
|
|
revision,
|
|
category: 'branches'
|
|
})
|
|
continue
|
|
}
|
|
|
|
if (ref.startsWith('refs/heads/')) {
|
|
refs.push({
|
|
id: ref,
|
|
name: ref.slice('refs/heads/'.length),
|
|
revision,
|
|
category: 'branches'
|
|
})
|
|
continue
|
|
}
|
|
|
|
if (ref.startsWith('refs/remotes/')) {
|
|
refs.push({
|
|
id: ref,
|
|
name: ref.slice('refs/remotes/'.length),
|
|
revision,
|
|
category: 'remote branches'
|
|
})
|
|
continue
|
|
}
|
|
|
|
if (ref.startsWith('tag: refs/tags/')) {
|
|
refs.push({
|
|
id: ref.slice('tag: '.length),
|
|
name: ref.slice('tag: refs/tags/'.length),
|
|
revision,
|
|
category: 'tags'
|
|
})
|
|
}
|
|
}
|
|
|
|
return refs.sort(compareGitHistoryItemRefsByCategory)
|
|
}
|
|
|
|
export function compareGitHistoryItemRefsByCategory(
|
|
ref1: GitHistoryItemRef,
|
|
ref2: GitHistoryItemRef
|
|
): number {
|
|
const order = (ref: GitHistoryItemRef): number => {
|
|
if (ref.id.startsWith('refs/heads/')) {
|
|
return 1
|
|
}
|
|
if (ref.id.startsWith('refs/remotes/')) {
|
|
return 2
|
|
}
|
|
if (ref.id.startsWith('refs/tags/')) {
|
|
return 3
|
|
}
|
|
return 99
|
|
}
|
|
|
|
const categoryOrder = order(ref1) - order(ref2)
|
|
return categoryOrder || ref1.name.localeCompare(ref2.name)
|
|
}
|
|
|
|
export function parseGitHistoryLog(stdout: string): GitHistoryItem[] {
|
|
const items: GitHistoryItem[] = []
|
|
for (const rawRecord of iterateNulDelimitedFields(stdout)) {
|
|
const record = rawRecord.replace(/^\n+/, '')
|
|
if (!record.trim()) {
|
|
continue
|
|
}
|
|
|
|
const lines = record.split('\n')
|
|
const hash = lines[0]?.trim() ?? ''
|
|
if (!/^[0-9a-fA-F]{40,64}$/.test(hash)) {
|
|
continue
|
|
}
|
|
|
|
const authorName = lines[1] ?? ''
|
|
const authorEmail = lines[2] ?? ''
|
|
const authorDateSeconds = Number.parseInt(lines[3] ?? '', 10)
|
|
const parents = (lines[5] ?? '').trim()
|
|
const decorateField = lines[6] ?? ''
|
|
const isLegacyGit = decorateField === UNEXPANDED_DECORATE_PLACEHOLDER
|
|
const decorations = isLegacyGit ? (lines[7] ?? '') : decorateField
|
|
const message = lines.slice(8).join('\n').replace(/\n$/, '')
|
|
|
|
items.push({
|
|
id: hash,
|
|
parentIds: parents ? parents.split(' ') : [],
|
|
subject: commitSubject(message),
|
|
message,
|
|
author: authorName || undefined,
|
|
authorEmail: authorEmail || undefined,
|
|
displayId: shortGitHash(hash),
|
|
timestamp: Number.isFinite(authorDateSeconds) ? authorDateSeconds * 1000 : undefined,
|
|
references: parseGitDecorationRefs(
|
|
decorations,
|
|
hash,
|
|
isLegacyGit ? GIT_HISTORY_LEGACY_DECORATION_SEPARATOR : GIT_HISTORY_DECORATION_SEPARATOR
|
|
)
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
export function gitHistoryRefFromFullName(
|
|
fullName: string | null,
|
|
fallbackName: string,
|
|
revision: string
|
|
): GitHistoryItemRef {
|
|
const id = fullName || fallbackName
|
|
if (id.startsWith('refs/heads/')) {
|
|
return { id, name: id.slice('refs/heads/'.length), revision, category: 'branches' }
|
|
}
|
|
if (id.startsWith('refs/remotes/')) {
|
|
return { id, name: id.slice('refs/remotes/'.length), revision, category: 'remote branches' }
|
|
}
|
|
if (id.startsWith('refs/tags/')) {
|
|
return { id, name: id.slice('refs/tags/'.length), revision, category: 'tags' }
|
|
}
|
|
return { id, name: fallbackName || shortGitHash(revision), revision, category: 'commits' }
|
|
}
|