Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- mods/diff/hooks/entry-kinds-of/entry-kinds-of.ts +17 -0
- mods/diff/hooks/git/argv/diff-leading-args.ts +20 -0
- mods/diff/hooks/git/argv/git-child-env/git-child-env.ts +8 -0
- mods/diff/hooks/git/argv/git-child-env/index.ts +3 -0
- mods/diff/hooks/git/argv/index.ts +6 -0
- mods/diff/hooks/git/argv/no-optional-locks.ts +5 -0
- mods/diff/hooks/git/argv/pinned-lead-of/index.ts +3 -0
- mods/diff/hooks/git/argv/pinned-lead-of/pinned-lead-of.ts +18 -0
- mods/diff/hooks/git/cut-hunks/cut-hunks.ts +30 -0
- mods/diff/hooks/git/cut-hunks/index.ts +3 -0
- mods/diff/hooks/git/display-path-of/display-path-of.ts +17 -0
- mods/diff/hooks/git/display-path-of/index.ts +3 -0
- mods/diff/hooks/git/empty-file-hunks/empty-file-hunks.ts +11 -0
- mods/diff/hooks/git/empty-file-hunks/index.ts +3 -0
- mods/diff/hooks/git/failed-run/failed-run.ts +11 -0
- mods/diff/hooks/git/failed-run/index.ts +3 -0
- mods/diff/hooks/git/fetch-diff/fetch-diff.ts +33 -0
- mods/diff/hooks/git/fetch-diff/index.ts +3 -0
- mods/diff/hooks/git/fetch-file-hunks/fetch-file-hunks.ts +51 -0
- mods/diff/hooks/git/fetch-file-hunks/index.ts +5 -0
- mods/diff/hooks/git/fetch-file-hunks/is-last-of-diff/index.ts +3 -0
- mods/diff/hooks/git/fetch-file-hunks/is-last-of-diff/is-last-of-diff.ts +18 -0
- mods/diff/hooks/git/fetch-file-hunks/with-closing-line/index.ts +3 -0
- mods/diff/hooks/git/fetch-file-hunks/with-closing-line/with-closing-line.ts +27 -0
- mods/diff/hooks/git/git-backend-of.ts +79 -0
- mods/diff/hooks/git/git-base-modes/git-base-modes.ts +10 -0
- mods/diff/hooks/git/git-base-modes/index.ts +3 -0
- mods/diff/hooks/git/git-words/git-words.ts +19 -0
- mods/diff/hooks/git/git-words/index.ts +3 -0
- mods/diff/hooks/git/hunk-after.ts +20 -0
- mods/diff/hooks/git/hunk-of/hunk-of.ts +17 -0
- mods/diff/hooks/git/hunk-of/index.ts +3 -0
- mods/diff/hooks/git/index.ts +20 -0
- mods/diff/hooks/git/large-file-hunks/index.ts +3 -0
- mods/diff/hooks/git/large-file-hunks/large-file-hunks.ts +11 -0
- mods/diff/hooks/git/parse/hunk-header/hunk-header.ts +5 -0
- mods/diff/hooks/git/parse/hunk-header/index.ts +3 -0
- mods/diff/hooks/git/parse/index.ts +11 -0
- mods/diff/hooks/git/parse/is-body-line/index.ts +3 -0
- mods/diff/hooks/git/parse/is-body-line/is-body-line.ts +9 -0
- mods/diff/hooks/git/parse/is-whole-answer/index.ts +3 -0
- mods/diff/hooks/git/parse/is-whole-answer/is-whole-answer.ts +12 -0
- mods/diff/hooks/git/parse/is-whole-listing/index.ts +5 -0
- mods/diff/hooks/git/parse/is-whole-listing/is-under-utf8-ceiling/index.ts +3 -0
- mods/diff/hooks/git/parse/is-whole-listing/is-under-utf8-ceiling/is-under-utf8-ceiling.ts +13 -0
- mods/diff/hooks/git/parse/is-whole-listing/is-whole-listing.ts +18 -0
- mods/diff/hooks/git/parse/is-whole-listing/most-utf8-bytes/index.ts +3 -0
- mods/diff/hooks/git/parse/is-whole-listing/most-utf8-bytes/most-utf8-bytes.ts +5 -0
- mods/diff/hooks/git/parse/parse-file-diff.ts +41 -0
- mods/diff/hooks/git/parse/parse-numstat.ts +66 -0
mods/diff/hooks/entry-kinds-of/entry-kinds-of.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Git from '../git'
|
| 2 |
+
import type { Host } from '../host'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* A directory's entries by name with their own kinds, through the host's
|
| 6 |
+
* listing (no link followed); null when it cannot be listed.
|
| 7 |
+
*
|
| 8 |
+
* @param engine the bound host
|
| 9 |
+
* @returns the probe (GitDeps `entryKindsOf`)
|
| 10 |
+
*/
|
| 11 |
+
export const entryKindsOf =
|
| 12 |
+
(engine: Host) =>
|
| 13 |
+
(dir: string): Promise<ReadonlyMap<string, Git.EntryKind> | null> =>
|
| 14 |
+
engine.listDir(dir).then(
|
| 15 |
+
entries => new Map(entries.map(entry => [entry.name, entry.kind])),
|
| 16 |
+
() => null,
|
| 17 |
+
)
|
mods/diff/hooks/git/argv/diff-leading-args.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* The leading argv of every `git diff` the plugin runs: the config the
|
| 3 |
+
* engine's own diff panel pins, `core.quotePath=false`, and raw-blob flags.
|
| 4 |
+
*
|
| 5 |
+
* Root-relative paths whatever `diff.relative` says, non-ASCII paths as
|
| 6 |
+
* their own bytes, no optional locks, no child `git status` in a dirty
|
| 7 |
+
* submodule, and no external diff or textconv driver even on a stats pass.
|
| 8 |
+
*/
|
| 9 |
+
export const DIFF_LEADING_ARGS = [
|
| 10 |
+
'--no-optional-locks',
|
| 11 |
+
'-c',
|
| 12 |
+
'diff.relative=false',
|
| 13 |
+
'-c',
|
| 14 |
+
'core.quotePath=false',
|
| 15 |
+
'diff',
|
| 16 |
+
'--no-ext-diff',
|
| 17 |
+
'--no-textconv',
|
| 18 |
+
'--ignore-submodules=dirty',
|
| 19 |
+
'--submodule=short',
|
| 20 |
+
] as const
|
mods/diff/hooks/git/argv/git-child-env/git-child-env.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* The environment laid over every git child the plugin spawns: the C
|
| 3 |
+
* locale, so `--shortstat` and git's messages parse the same everywhere.
|
| 4 |
+
*
|
| 5 |
+
* Nothing else is pinned, as for the built-in panel's own diff children:
|
| 6 |
+
* a partial clone lazy-fetches as it would for `git diff` at the prompt.
|
| 7 |
+
*/
|
| 8 |
+
export const GIT_CHILD_ENV = { LC_ALL: 'C', LANGUAGE: '' } as const
|
mods/diff/hooks/git/argv/git-child-env/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './git-child-env.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/argv/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './diff-leading-args.js'
|
| 2 |
+
export * from './git-child-env'
|
| 3 |
+
export * from './no-optional-locks.js'
|
| 4 |
+
export * from './pinned-lead-of'
|
| 5 |
+
|
| 6 |
+
export * as default from '.'
|
mods/diff/hooks/git/argv/no-optional-locks.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* The flag every probe carries so a read never takes the index lock from a
|
| 3 |
+
* git the person is running.
|
| 4 |
+
*/
|
| 5 |
+
export const NO_OPTIONAL_LOCKS = '--no-optional-locks'
|
mods/diff/hooks/git/argv/pinned-lead-of/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './pinned-lead-of.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/argv/pinned-lead-of/pinned-lead-of.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* The argv that leads every git child once the repository is pinned: its
|
| 5 |
+
* git directory and working tree named, as the built-in panel pins them.
|
| 6 |
+
*
|
| 7 |
+
* With `--git-dir` given git discovers nothing from its cwd, so a nested
|
| 8 |
+
* repository the shell later moves into is never the one read.
|
| 9 |
+
*
|
| 10 |
+
* @param repository the session's pinned repository
|
| 11 |
+
* @returns the leading argv
|
| 12 |
+
*/
|
| 13 |
+
export const pinnedLeadOf = (
|
| 14 |
+
repository: Types.Repository,
|
| 15 |
+
): readonly string[] => [
|
| 16 |
+
`--git-dir=${repository.gitDir}`,
|
| 17 |
+
`--work-tree=${repository.toplevel}`,
|
| 18 |
+
]
|
mods/diff/hooks/git/cut-hunks/cut-hunks.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Limits from '../../limits'
|
| 2 |
+
import type Types from '../types'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Hunks with their lines cut at MAX_LINES_PER_FILE in all, in order, and
|
| 6 |
+
* whether any line was dropped.
|
| 7 |
+
*
|
| 8 |
+
* The one bound on every body the pane draws or attaches, whether git
|
| 9 |
+
* printed it or the transcript carried it.
|
| 10 |
+
*
|
| 11 |
+
* @param hunks the hunks as parsed or reported
|
| 12 |
+
* @returns the cut hunks and whether the cut dropped lines
|
| 13 |
+
*/
|
| 14 |
+
export function cutHunks(
|
| 15 |
+
hunks: readonly Types.Hunk[],
|
| 16 |
+
): Pick<Types.FileHunks, 'hunks' | 'isTruncated'> {
|
| 17 |
+
let budget = Limits.MAX_LINES_PER_FILE
|
| 18 |
+
let total = 0
|
| 19 |
+
|
| 20 |
+
return {
|
| 21 |
+
hunks: hunks.map(hunk => {
|
| 22 |
+
const lines = hunk.lines.slice(0, budget)
|
| 23 |
+
budget -= lines.length
|
| 24 |
+
total += hunk.lines.length
|
| 25 |
+
|
| 26 |
+
return { ...hunk, lines }
|
| 27 |
+
}),
|
| 28 |
+
isTruncated: total > Limits.MAX_LINES_PER_FILE,
|
| 29 |
+
}
|
| 30 |
+
}
|
mods/diff/hooks/git/cut-hunks/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './cut-hunks.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/display-path-of/display-path-of.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { renameDisplayOf } from '../rename-display-of'
|
| 2 |
+
import type Types from '../types'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* A row's name as the built-in lists it: the path, or for a rename the one
|
| 6 |
+
* line `git diff --stat` prints for it (renameDisplayOf).
|
| 7 |
+
*
|
| 8 |
+
* @param file the row
|
| 9 |
+
* @returns the name to draw and to sort by
|
| 10 |
+
*/
|
| 11 |
+
export function displayPathOf(
|
| 12 |
+
file: Pick<Types.FileStat, 'path' | 'renamedFrom'>,
|
| 13 |
+
): string {
|
| 14 |
+
const { renamedFrom, path } = file
|
| 15 |
+
|
| 16 |
+
return renamedFrom === null ? path : renameDisplayOf(renamedFrom, path)
|
| 17 |
+
}
|
mods/diff/hooks/git/display-path-of/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './display-path-of.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/empty-file-hunks/empty-file-hunks.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* A body with nothing to draw: an untracked or binary row's, or a staged
|
| 5 |
+
* file's whose index body went stale on an unborn HEAD.
|
| 6 |
+
*/
|
| 7 |
+
export const EMPTY_FILE_HUNKS: Types.FileHunks = Object.freeze({
|
| 8 |
+
hunks: [],
|
| 9 |
+
isTruncated: false,
|
| 10 |
+
isLarge: false,
|
| 11 |
+
})
|
mods/diff/hooks/git/empty-file-hunks/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './empty-file-hunks.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/failed-run/failed-run.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* What a child that never ran, was killed, or timed out reads as: exit code
|
| 5 |
+
* -1 and no output, so a runner never rejects (GitRun).
|
| 6 |
+
*/
|
| 7 |
+
export const FAILED_RUN: Types.RunResult = Object.freeze({
|
| 8 |
+
exitCode: -1,
|
| 9 |
+
stdout: '',
|
| 10 |
+
stderr: '',
|
| 11 |
+
})
|
mods/diff/hooks/git/failed-run/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './failed-run.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/fetch-diff/fetch-diff.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Probes from '../probes'
|
| 2 |
+
import Tiers from '../tiers'
|
| 3 |
+
import type Types from '../types'
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* One fetch of the session's pinned repository, as the built-in panel
|
| 7 |
+
* fetches; every git child names it so paths stay root-relative.
|
| 8 |
+
*
|
| 9 |
+
* Mid-merge, mid-rebase, mid-cherry-pick or mid-revert, or when git cannot
|
| 10 |
+
* answer, it is unavailable and the caller keeps what it had.
|
| 11 |
+
*
|
| 12 |
+
* @param deps the host: pinned git, the repository, the file probes
|
| 13 |
+
* @param mode what to compare against
|
| 14 |
+
* @returns the outcome
|
| 15 |
+
*/
|
| 16 |
+
export async function fetchDiff(
|
| 17 |
+
deps: Types.GitDeps,
|
| 18 |
+
mode: Types.BaseMode,
|
| 19 |
+
): Promise<Types.FetchOutcome> {
|
| 20 |
+
const isBusy = await Probes.isTransient(
|
| 21 |
+
deps.entryKindsOf,
|
| 22 |
+
deps.repository.gitDir,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
if (isBusy) {
|
| 26 |
+
return { kind: 'unavailable' }
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const context = Tiers.fetchContextOf(deps, mode)
|
| 30 |
+
const isBranch = mode === 'branch'
|
| 31 |
+
|
| 32 |
+
return isBranch ? Tiers.branchTier(context) : Tiers.workingTreeTier(context)
|
| 33 |
+
}
|
mods/diff/hooks/git/fetch-diff/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './fetch-diff.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/fetch-file-hunks/fetch-file-hunks.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Argv from '../argv'
|
| 2 |
+
import { EMPTY_FILE_HUNKS } from '../empty-file-hunks'
|
| 3 |
+
import Parse from '../parse'
|
| 4 |
+
import type Types from '../types'
|
| 5 |
+
import { isLastOfDiff } from './is-last-of-diff'
|
| 6 |
+
import { withClosingLine } from './with-closing-line'
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* One file's hunks against the base its row was read against, so body and
|
| 10 |
+
* counts agree, by literal pathspec, as the built-in panel reads hunks.
|
| 11 |
+
*
|
| 12 |
+
* An untracked, binary or renamed row (the built-in shows a rename's
|
| 13 |
+
* counts, never its body), or on an unborn HEAD a file edited after
|
| 14 |
+
* staging, answers no hunks; each file but the last, a closing empty row.
|
| 15 |
+
*
|
| 16 |
+
* @param run runs git against the pinned repository
|
| 17 |
+
* @param data the fetch the row belongs to
|
| 18 |
+
* @param file the row
|
| 19 |
+
* @returns the parsed body, or null when git failed
|
| 20 |
+
*/
|
| 21 |
+
export async function fetchFileHunks(
|
| 22 |
+
run: Types.GitRun,
|
| 23 |
+
data: Types.DiffData,
|
| 24 |
+
file: Types.FileStat,
|
| 25 |
+
): Promise<Types.FileHunks | null> {
|
| 26 |
+
const hasNoBody =
|
| 27 |
+
file.isUntracked ||
|
| 28 |
+
file.isBinary ||
|
| 29 |
+
file.renamedFrom !== null ||
|
| 30 |
+
data.stalePaths.includes(file.path)
|
| 31 |
+
|
| 32 |
+
if (hasNoBody) {
|
| 33 |
+
return EMPTY_FILE_HUNKS
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const { exitCode, stdout } = await run([
|
| 37 |
+
'--literal-pathspecs',
|
| 38 |
+
...Argv.DIFF_LEADING_ARGS,
|
| 39 |
+
data.baseRef,
|
| 40 |
+
'--',
|
| 41 |
+
file.path,
|
| 42 |
+
])
|
| 43 |
+
|
| 44 |
+
if (exitCode !== 0) {
|
| 45 |
+
return null
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const body = Parse.parseFileDiff(stdout)
|
| 49 |
+
|
| 50 |
+
return isLastOfDiff(data, file) ? body : withClosingLine(body)
|
| 51 |
+
}
|
mods/diff/hooks/git/fetch-file-hunks/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './fetch-file-hunks.js'
|
| 2 |
+
export * from './is-last-of-diff'
|
| 3 |
+
export * from './with-closing-line'
|
| 4 |
+
|
| 5 |
+
export * as default from '.'
|
mods/diff/hooks/git/fetch-file-hunks/is-last-of-diff/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './is-last-of-diff.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/fetch-file-hunks/is-last-of-diff/is-last-of-diff.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Whether a row is the last file of the whole diff git printed for its
|
| 5 |
+
* fetch: the last tracked row, the untracked ones being listed apart.
|
| 6 |
+
*
|
| 7 |
+
* The built-in reads that diff in one piece and trims its end, so the last
|
| 8 |
+
* file alone has no closing empty line to draw (withClosingLine).
|
| 9 |
+
*
|
| 10 |
+
* @param data the fetch the row belongs to
|
| 11 |
+
* @param file the row
|
| 12 |
+
* @returns whether git printed this file last
|
| 13 |
+
*/
|
| 14 |
+
export const isLastOfDiff = (
|
| 15 |
+
data: Types.DiffData,
|
| 16 |
+
file: Types.FileStat,
|
| 17 |
+
): boolean =>
|
| 18 |
+
data.files.filter(row => !row.isUntracked).at(-1)?.path === file.path
|
mods/diff/hooks/git/fetch-file-hunks/with-closing-line/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './with-closing-line.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/fetch-file-hunks/with-closing-line/with-closing-line.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* A parsed body with one empty context row after its last hunk's lines, as
|
| 5 |
+
* the built-in panel draws the empty line that closes git's output.
|
| 6 |
+
*
|
| 7 |
+
* The built-in reads every file's diff in one piece: the newline before the
|
| 8 |
+
* next file's header parses as a context line of the file before it. A
|
| 9 |
+
* body with no hunk, or one already cut, gains nothing.
|
| 10 |
+
*
|
| 11 |
+
* @param body the parsed body
|
| 12 |
+
* @returns the body, its last hunk one row longer
|
| 13 |
+
*/
|
| 14 |
+
export function withClosingLine(body: Types.FileHunks): Types.FileHunks {
|
| 15 |
+
const last = body.hunks.at(-1)
|
| 16 |
+
const isClosed = last !== undefined && !body.isTruncated
|
| 17 |
+
|
| 18 |
+
return isClosed
|
| 19 |
+
? {
|
| 20 |
+
...body,
|
| 21 |
+
hunks: [
|
| 22 |
+
...body.hunks.slice(0, -1),
|
| 23 |
+
{ ...last, lines: [...last.lines, ' '] },
|
| 24 |
+
],
|
| 25 |
+
}
|
| 26 |
+
: body
|
| 27 |
+
}
|
mods/diff/hooks/git/git-backend-of.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Backend from '../backend'
|
| 2 |
+
import Limits from '../limits'
|
| 3 |
+
import Argv from './argv'
|
| 4 |
+
import { FAILED_RUN } from './failed-run'
|
| 5 |
+
import { fetchDiff } from './fetch-diff'
|
| 6 |
+
import { fetchFileHunks } from './fetch-file-hunks'
|
| 7 |
+
import { GIT_BASE_MODES } from './git-base-modes'
|
| 8 |
+
import { GIT_WORDS } from './git-words'
|
| 9 |
+
import Probes from './probes'
|
| 10 |
+
import type Types from './types'
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* The git backend over the session's directory: the repository read with
|
| 14 |
+
* one `rev-parse` in that directory, then every child pinned to it.
|
| 15 |
+
*
|
| 16 |
+
* Each child runs with `--git-dir`, `--work-tree` and cwd the top, under
|
| 17 |
+
* the C locale and the fetch's timeout (the built-in's execPinnedGit); the
|
| 18 |
+
* first fetch reads the paths dirty then, once (Probes.dirtyPathsOf).
|
| 19 |
+
*
|
| 20 |
+
* @param host the bound host's runner and probes
|
| 21 |
+
* @returns the backend, or null outside a git working tree
|
| 22 |
+
*/
|
| 23 |
+
export async function gitBackendOf(
|
| 24 |
+
host: Backend.BackendHost,
|
| 25 |
+
): Promise<Backend.Backend | null> {
|
| 26 |
+
const runOf =
|
| 27 |
+
(pinned: Types.Repository | null): Types.GitRun =>
|
| 28 |
+
async argv => {
|
| 29 |
+
try {
|
| 30 |
+
return await host.run(
|
| 31 |
+
['git', ...(pinned ? Argv.pinnedLeadOf(pinned) : []), ...argv],
|
| 32 |
+
{
|
| 33 |
+
timeoutMs: Limits.GIT_TIMEOUT_MS,
|
| 34 |
+
env: Argv.GIT_CHILD_ENV,
|
| 35 |
+
...(pinned && { cwd: pinned.toplevel }),
|
| 36 |
+
},
|
| 37 |
+
)
|
| 38 |
+
} catch {
|
| 39 |
+
return FAILED_RUN
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const repository = await Probes.repositoryOf(runOf(null))
|
| 44 |
+
|
| 45 |
+
if (!repository) {
|
| 46 |
+
return null
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const run = runOf(repository)
|
| 50 |
+
|
| 51 |
+
let baseline: Promise<ReadonlySet<string> | null> | null = null
|
| 52 |
+
|
| 53 |
+
const depsOf = (dirty: ReadonlySet<string> | null): Types.GitDeps => ({
|
| 54 |
+
run,
|
| 55 |
+
repository,
|
| 56 |
+
mtimeOf: host.mtimeOf,
|
| 57 |
+
entryKindsOf: host.entryKindsOf,
|
| 58 |
+
sessionStartMs: host.sessionStartMsOf(),
|
| 59 |
+
baseline: dirty,
|
| 60 |
+
onBranchBase: host.onBranchBase,
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
return {
|
| 64 |
+
repository,
|
| 65 |
+
baseModes: GIT_BASE_MODES,
|
| 66 |
+
words: GIT_WORDS,
|
| 67 |
+
fetchDiff: async mode => {
|
| 68 |
+
baseline ??= Probes.dirtyPathsOf(run)
|
| 69 |
+
|
| 70 |
+
return fetchDiff(depsOf(await baseline), mode)
|
| 71 |
+
},
|
| 72 |
+
fetchFileHunks: (data, file) => fetchFileHunks(run, data, file),
|
| 73 |
+
headKeyOf: () =>
|
| 74 |
+
Probes.headKeyOf(
|
| 75 |
+
{ ...depsOf(null), readFile: host.readFile },
|
| 76 |
+
repository,
|
| 77 |
+
),
|
| 78 |
+
}
|
| 79 |
+
}
|
mods/diff/hooks/git/git-base-modes/git-base-modes.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* The comparisons git offers, in picker order: all three.
|
| 5 |
+
*/
|
| 6 |
+
export const GIT_BASE_MODES: readonly Types.BaseMode[] = Object.freeze([
|
| 7 |
+
'session',
|
| 8 |
+
'uncommitted',
|
| 9 |
+
'branch',
|
| 10 |
+
])
|
mods/diff/hooks/git/git-base-modes/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './git-base-modes.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/git-words/git-words.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Backend from '../../backend'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* The pane's git words, unquoted as DiffDetailView words them.
|
| 5 |
+
*
|
| 6 |
+
* Compared with HEAD, read by `git diff`, untracked files listed by git and
|
| 7 |
+
* staged with `git add :/…`.
|
| 8 |
+
*/
|
| 9 |
+
export const GIT_WORDS: Backend.BackendWords = Object.freeze({
|
| 10 |
+
base: 'HEAD',
|
| 11 |
+
diffCommand: 'git diff',
|
| 12 |
+
lister: 'git',
|
| 13 |
+
untrackedNoteOf: (path: string, isPasteable: boolean) => [
|
| 14 |
+
'New file not yet staged.',
|
| 15 |
+
isPasteable
|
| 16 |
+
? `Run \`git add :/${path}\` to see line counts.`
|
| 17 |
+
: 'Stage it with git add to see line counts.',
|
| 18 |
+
],
|
| 19 |
+
})
|
mods/diff/hooks/git/git-words/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './git-words.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/hunk-after.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { countOf } from '../count-of'
|
| 2 |
+
import type { Hunk } from './types'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* The hunk less its first so many lines, its starts moved past them so
|
| 6 |
+
* the numbers of the lines kept stay right; whole at 0.
|
| 7 |
+
*
|
| 8 |
+
* @param hunk the hunk, its lines body lines only
|
| 9 |
+
* @param count how many leading lines to drop
|
| 10 |
+
* @returns the rest of the hunk
|
| 11 |
+
*/
|
| 12 |
+
export function hunkAfter(hunk: Hunk, count: number): Hunk {
|
| 13 |
+
const dropped = hunk.lines.slice(0, count)
|
| 14 |
+
|
| 15 |
+
return {
|
| 16 |
+
oldStart: hunk.oldStart + countOf(dropped, line => !line.startsWith('+')),
|
| 17 |
+
newStart: hunk.newStart + countOf(dropped, line => !line.startsWith('-')),
|
| 18 |
+
lines: hunk.lines.slice(count),
|
| 19 |
+
}
|
| 20 |
+
}
|
mods/diff/hooks/git/hunk-of/hunk-of.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* One hunk from its header's captured starts and its body lines.
|
| 5 |
+
*
|
| 6 |
+
* @param starts the `old` and `new` groups of HUNK_HEADER, when it matched
|
| 7 |
+
* @param lines the body lines after the header
|
| 8 |
+
* @returns the hunk
|
| 9 |
+
*/
|
| 10 |
+
export const hunkOf = (
|
| 11 |
+
starts: Record<string, string> | undefined,
|
| 12 |
+
lines: readonly string[],
|
| 13 |
+
): Types.Hunk => ({
|
| 14 |
+
oldStart: Number(starts?.old),
|
| 15 |
+
newStart: Number(starts?.new),
|
| 16 |
+
lines,
|
| 17 |
+
})
|
mods/diff/hooks/git/hunk-of/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './hunk-of.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './argv'
|
| 2 |
+
export * from './cut-hunks'
|
| 3 |
+
export * from './display-path-of'
|
| 4 |
+
export * from './empty-file-hunks'
|
| 5 |
+
export * from './failed-run'
|
| 6 |
+
export * from './fetch-diff'
|
| 7 |
+
export * from './fetch-file-hunks'
|
| 8 |
+
export * from './git-backend-of.js'
|
| 9 |
+
export * from './git-base-modes'
|
| 10 |
+
export * from './git-words'
|
| 11 |
+
export * from './hunk-after.js'
|
| 12 |
+
export * from './hunk-of'
|
| 13 |
+
export * from './large-file-hunks'
|
| 14 |
+
export * from './parse'
|
| 15 |
+
export * from './probes'
|
| 16 |
+
export * from './rename-display-of'
|
| 17 |
+
export * from './tiers'
|
| 18 |
+
export * from './types'
|
| 19 |
+
|
| 20 |
+
export * as default from '.'
|
mods/diff/hooks/git/large-file-hunks/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './large-file-hunks.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/large-file-hunks/large-file-hunks.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../types'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* The body of a file whose diff text passed MAX_DIFF_BYTES: no hunks, and
|
| 5 |
+
* the pane says the file is too large.
|
| 6 |
+
*/
|
| 7 |
+
export const LARGE_FILE_HUNKS: Types.FileHunks = Object.freeze({
|
| 8 |
+
hunks: [],
|
| 9 |
+
isTruncated: false,
|
| 10 |
+
isLarge: true,
|
| 11 |
+
})
|
mods/diff/hooks/git/parse/hunk-header/hunk-header.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* A unified diff's hunk header, `@@ -old[,n] +new[,n] @@`, its two starts
|
| 3 |
+
* captured as `old` and `new`.
|
| 4 |
+
*/
|
| 5 |
+
export const HUNK_HEADER = /^@@ -(?<old>\d+)(?:,\d+)? \+(?<new>\d+)(?:,\d+)? @@/
|
mods/diff/hooks/git/parse/hunk-header/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './hunk-header.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './hunk-header'
|
| 2 |
+
export * from './is-body-line'
|
| 3 |
+
export * from './is-whole-answer'
|
| 4 |
+
export * from './is-whole-listing'
|
| 5 |
+
export * from './parse-file-diff.js'
|
| 6 |
+
export * from './parse-numstat.js'
|
| 7 |
+
export * from './parse-shortstat.js'
|
| 8 |
+
export * from './rename-records'
|
| 9 |
+
export * from './shortstat-pattern'
|
| 10 |
+
|
| 11 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/is-body-line/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './is-body-line.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/is-body-line/is-body-line.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Whether a line after a hunk header is a body line (added, removed or
|
| 3 |
+
* context) rather than a note such as ``.
|
| 4 |
+
*
|
| 5 |
+
* @param line one line of `git diff` output
|
| 6 |
+
* @returns whether it starts with `+`, `-` or a space
|
| 7 |
+
*/
|
| 8 |
+
export const isBodyLine = (line: string) =>
|
| 9 |
+
line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')
|
mods/diff/hooks/git/parse/is-whole-answer/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './is-whole-answer.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/is-whole-answer/is-whole-answer.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../../types'
|
| 2 |
+
import { isWholeListing } from '../is-whole-listing'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Whether a `-z` git child both succeeded and answered a whole listing,
|
| 6 |
+
* so its stdout may be parsed (isWholeListing).
|
| 7 |
+
*
|
| 8 |
+
* @param result what the child left
|
| 9 |
+
* @returns whether to parse it
|
| 10 |
+
*/
|
| 11 |
+
export const isWholeAnswer = (result: Types.RunResult) =>
|
| 12 |
+
result.exitCode === 0 && isWholeListing(result.stdout)
|
mods/diff/hooks/git/parse/is-whole-listing/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './is-under-utf8-ceiling'
|
| 2 |
+
export * from './is-whole-listing.js'
|
| 3 |
+
export * from './most-utf8-bytes'
|
| 4 |
+
|
| 5 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/is-whole-listing/is-under-utf8-ceiling/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './is-under-utf8-ceiling.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/is-whole-listing/is-under-utf8-ceiling/is-under-utf8-ceiling.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MOST_UTF8_BYTES } from '../most-utf8-bytes'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Whether the text's UTF-8 encoding stays under a byte count, encoding it
|
| 5 |
+
* only when its length alone cannot already say so.
|
| 6 |
+
*
|
| 7 |
+
* @param text the text
|
| 8 |
+
* @param ceiling the byte count it must stay under
|
| 9 |
+
* @returns whether it is under
|
| 10 |
+
*/
|
| 11 |
+
export const isUnderUtf8Ceiling = (text: string, ceiling: number) =>
|
| 12 |
+
text.length * MOST_UTF8_BYTES < ceiling ||
|
| 13 |
+
new TextEncoder().encode(text).length < ceiling
|
mods/diff/hooks/git/parse/is-whole-listing/is-whole-listing.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Limits from '../../../limits'
|
| 2 |
+
import { isUnderUtf8Ceiling } from './is-under-utf8-ceiling'
|
| 3 |
+
import { MOST_UTF8_BYTES } from './most-utf8-bytes'
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Whether a listing arrived whole, so no record was cut or lost.
|
| 7 |
+
*
|
| 8 |
+
* Every record is terminated (NUL for `-z` listings, a newline for line
|
| 9 |
+
* records) and the text is short of the host's output cap by MOST_UTF8_BYTES,
|
| 10 |
+
* what a cut inside a multi-byte character can cost the decoded text.
|
| 11 |
+
*
|
| 12 |
+
* @param stdout the listing as `$.process.run` returned it
|
| 13 |
+
* @param terminator what ends every record; NUL when absent
|
| 14 |
+
* @returns whether it can be parsed as the whole answer
|
| 15 |
+
*/
|
| 16 |
+
export const isWholeListing = (stdout: string, terminator = '\0') =>
|
| 17 |
+
(stdout === '' || stdout.endsWith(terminator)) &&
|
| 18 |
+
isUnderUtf8Ceiling(stdout, Limits.HOST_OUTPUT_CAP_BYTES - MOST_UTF8_BYTES)
|
mods/diff/hooks/git/parse/is-whole-listing/most-utf8-bytes/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export * from './most-utf8-bytes.js'
|
| 2 |
+
|
| 3 |
+
export * as default from '.'
|
mods/diff/hooks/git/parse/is-whole-listing/most-utf8-bytes/most-utf8-bytes.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* The most bytes one UTF-16 code unit takes in UTF-8, and so both the
|
| 3 |
+
* bound on a text's encoded length and the most a cut character drops.
|
| 4 |
+
*/
|
| 5 |
+
export const MOST_UTF8_BYTES = 3
|
mods/diff/hooks/git/parse/parse-file-diff.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Limits from '../../limits'
|
| 2 |
+
import { cutHunks } from '../cut-hunks'
|
| 3 |
+
import { hunkOf } from '../hunk-of'
|
| 4 |
+
import { LARGE_FILE_HUNKS } from '../large-file-hunks'
|
| 5 |
+
import type Types from '../types'
|
| 6 |
+
import { HUNK_HEADER } from './hunk-header'
|
| 7 |
+
import { isBodyLine } from './is-body-line'
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* One file's unified `git diff` output as hunks, the body cut at
|
| 11 |
+
* MAX_LINES_PER_FILE lines in all, as the built-in panel parses one file.
|
| 12 |
+
*
|
| 13 |
+
* Past MAX_DIFF_BYTES there are no hunks and `isLarge` is set; everything
|
| 14 |
+
* before the first hunk header is the file's header.
|
| 15 |
+
*
|
| 16 |
+
* @param stdout the command's standard output for that one path
|
| 17 |
+
* @returns the parsed body
|
| 18 |
+
*/
|
| 19 |
+
export function parseFileDiff(stdout: string): Types.FileHunks {
|
| 20 |
+
if (stdout.length > Limits.MAX_DIFF_BYTES) {
|
| 21 |
+
return LARGE_FILE_HUNKS
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const rows = stdout.split('\n')
|
| 25 |
+
|
| 26 |
+
const headerRows = rows.flatMap((row, at) =>
|
| 27 |
+
HUNK_HEADER.test(row) ? [at] : [],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
return {
|
| 31 |
+
...cutHunks(
|
| 32 |
+
headerRows.map((at, ordinal) =>
|
| 33 |
+
hunkOf(
|
| 34 |
+
HUNK_HEADER.exec(rows[at] ?? '')?.groups,
|
| 35 |
+
rows.slice(at + 1, headerRows[ordinal + 1]).filter(isBodyLine),
|
| 36 |
+
),
|
| 37 |
+
),
|
| 38 |
+
),
|
| 39 |
+
isLarge: false,
|
| 40 |
+
}
|
| 41 |
+
}
|
mods/diff/hooks/git/parse/parse-numstat.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type Types from '../types'
|
| 2 |
+
import { RENAME_RECORDS } from './rename-records'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* `git diff --numstat -z` output as totals over every row and the first
|
| 6 |
+
* rows up to the cap; a binary file (`-` counts) is a 0/0 row marked binary.
|
| 7 |
+
*
|
| 8 |
+
* A record is `<added>\t<removed>\t<path>` ended by NUL, the path raw; a
|
| 9 |
+
* rename is `<added>\t<removed>\t` then the old and the new path as the
|
| 10 |
+
* next two records, kept as one row under the new path.
|
| 11 |
+
*
|
| 12 |
+
* @param stdout the command's standard output
|
| 13 |
+
* @param maxFiles how many rows to keep
|
| 14 |
+
* @returns the totals and the kept rows
|
| 15 |
+
*/
|
| 16 |
+
export function parseNumstat(
|
| 17 |
+
stdout: string,
|
| 18 |
+
maxFiles: number,
|
| 19 |
+
): Types.NumstatResult {
|
| 20 |
+
const files: Types.FileStat[] = []
|
| 21 |
+
const stats = { filesCount: 0, linesAdded: 0, linesRemoved: 0 }
|
| 22 |
+
const records = stdout.split('\0')
|
| 23 |
+
|
| 24 |
+
let at = 0
|
| 25 |
+
|
| 26 |
+
while (at < records.length) {
|
| 27 |
+
const [addedField, removedField, ...pathFields] = (records[at] ?? '').split(
|
| 28 |
+
'\t',
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
const inline = pathFields.join('\t')
|
| 32 |
+
const isRename = pathFields.length === 1 && inline === ''
|
| 33 |
+
const renamedFrom = isRename ? (records[at + 1] ?? null) : null
|
| 34 |
+
const path = isRename ? (records[at + 2] ?? '') : inline
|
| 35 |
+
at += isRename ? RENAME_RECORDS : 1
|
| 36 |
+
|
| 37 |
+
const isRow =
|
| 38 |
+
addedField !== undefined && removedField !== undefined && path !== ''
|
| 39 |
+
|
| 40 |
+
if (!isRow) {
|
| 41 |
+
continue
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const isBinary = addedField === '-' || removedField === '-'
|
| 45 |
+
const added = isBinary ? 0 : Number(addedField) || 0
|
| 46 |
+
const removed = isBinary ? 0 : Number(removedField) || 0
|
| 47 |
+
|
| 48 |
+
stats.filesCount += 1
|
| 49 |
+
stats.linesAdded += added
|
| 50 |
+
stats.linesRemoved += removed
|
| 51 |
+
|
| 52 |
+
if (files.length < maxFiles) {
|
| 53 |
+
files.push({
|
| 54 |
+
path,
|
| 55 |
+
renamedFrom,
|
| 56 |
+
added,
|
| 57 |
+
removed,
|
| 58 |
+
isBinary,
|
| 59 |
+
isUntracked: false,
|
| 60 |
+
isPreSession: false,
|
| 61 |
+
})
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
return { stats, files }
|
| 66 |
+
}
|