handling files when stages and dirty at once
This commit is contained in:
+67
-19
@@ -76,19 +76,41 @@ async function git(root: string, args: string[]): Promise<string> {
|
||||
return stdout
|
||||
}
|
||||
|
||||
/** Map a porcelain code pair to our display letter + staged flag. */
|
||||
export function classify(index: string, working: string): { letter: GitStatusLetter; staged: boolean } {
|
||||
const staged = index !== ' ' && index !== '?'
|
||||
const code = staged ? index : working
|
||||
let letter: GitStatusLetter
|
||||
/** Map one porcelain status code to our display letter. */
|
||||
function letterFor(code: string): GitStatusLetter {
|
||||
switch (code) {
|
||||
case 'A': case 'C': case '?': letter = 'A'; break
|
||||
case 'D': letter = 'D'; break
|
||||
case 'R': letter = 'R'; break
|
||||
case 'U': letter = 'M'; break
|
||||
case 'M': default: letter = 'M'; break
|
||||
case 'A': case 'C': case '?': return 'A'
|
||||
case 'D': return 'D'
|
||||
case 'R': return 'R'
|
||||
case 'U': return 'M'
|
||||
default: return 'M'
|
||||
}
|
||||
return { letter, staged }
|
||||
}
|
||||
|
||||
/** A merge conflict. Git reports both sides, but neither half can be staged on
|
||||
* its own, so a conflict stays one row. */
|
||||
function isConflict(index: string, working: string): boolean {
|
||||
return index === 'U' || working === 'U'
|
||||
|| (index === 'A' && working === 'A')
|
||||
|| (index === 'D' && working === 'D')
|
||||
}
|
||||
|
||||
export interface GitRowSpec { letter: GitStatusLetter; staged: boolean }
|
||||
|
||||
/**
|
||||
* Split a porcelain code pair into the rows the git panel shows.
|
||||
*
|
||||
* A file can be staged AND changed again on disk. Git reports that as "MM".
|
||||
* That is two rows: one staged (HEAD vs index) and one unstaged (index vs
|
||||
* disk). Folding it into a single row hid the newer edit completely.
|
||||
*/
|
||||
export function classify(index: string, working: string): GitRowSpec[] {
|
||||
if (isConflict(index, working)) return [{ letter: 'M', staged: true }]
|
||||
const rows: GitRowSpec[] = []
|
||||
if (index !== ' ' && index !== '?') rows.push({ letter: letterFor(index), staged: true })
|
||||
if (working !== ' ') rows.push({ letter: letterFor(working), staged: false })
|
||||
// Should not happen (git does not report a clean file), but never drop an entry.
|
||||
return rows.length ? rows : [{ letter: letterFor(index), staged: true }]
|
||||
}
|
||||
|
||||
/** Parse a `## ...` porcelain branch header into a display branch name. */
|
||||
@@ -135,6 +157,15 @@ async function headText(root: string, path: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
/** The staged copy of a file: the blob sitting in the index. */
|
||||
async function indexText(root: string, path: string): Promise<string> {
|
||||
try {
|
||||
return await git(root, ['show', `:${path}`])
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function diskText(root: string, path: string): Promise<string> {
|
||||
try {
|
||||
const buf = await readFile(join(root, path))
|
||||
@@ -214,14 +245,31 @@ async function doLoad(root: string): Promise<GitLoad | null> {
|
||||
// re-reads ALL of them, which is the dominant cost of a reload. Run them with
|
||||
// bounded concurrency instead so the spawns overlap (cap keeps us well under
|
||||
// macOS's low default FD limit). Order is preserved by index.
|
||||
const changes = await mapLimit(files, 12, async (f) => {
|
||||
const { letter, staged } = classify(f.index, f.working)
|
||||
const isNew = f.index === '?' || f.index === 'A'
|
||||
const isDeleted = letter === 'D'
|
||||
const original = isNew ? '' : await headText(root, f.path)
|
||||
const updated = isDeleted ? '' : await diskText(root, f.path)
|
||||
return { path: f.path, status: letter, staged, original, updated } as GitChange
|
||||
})
|
||||
const changes = (await mapLimit(files, 12, async (f) => {
|
||||
const rows = classify(f.index, f.working)
|
||||
const stagedRow = rows.find((r) => r.staged)
|
||||
const workRow = rows.find((r) => !r.staged)
|
||||
// The index blob is only needed when a file sits in BOTH groups. With one
|
||||
// row the index copy equals HEAD (unstaged only) or the disk copy (staged
|
||||
// only), so the common case still costs no extra `git show`.
|
||||
const both = !!stagedRow && !!workRow
|
||||
const idx = both ? await indexText(root, f.path) : ''
|
||||
const out: GitChange[] = []
|
||||
if (stagedRow) {
|
||||
// Staged row: HEAD -> index.
|
||||
const original = stagedRow.letter === 'A' ? '' : await headText(root, f.path)
|
||||
const updated = stagedRow.letter === 'D' ? '' : both ? idx : await diskText(root, f.path)
|
||||
out.push({ path: f.path, status: stagedRow.letter, staged: true, original, updated })
|
||||
}
|
||||
if (workRow) {
|
||||
// Unstaged row: index -> disk. An untracked file has no index copy.
|
||||
const untracked = f.index === '?'
|
||||
const original = untracked ? '' : both ? idx : await headText(root, f.path)
|
||||
const updated = workRow.letter === 'D' ? '' : await diskText(root, f.path)
|
||||
out.push({ path: f.path, status: workRow.letter, staged: false, original, updated })
|
||||
}
|
||||
return out
|
||||
})).flat()
|
||||
|
||||
return { branch, changes }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user