SaylorTwift HF Staff commited on
Commit
00080b0
·
verified ·
1 Parent(s): bdbd3b2

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. mods/diff/hooks/entry-kinds-of/entry-kinds-of.ts +17 -0
  2. mods/diff/hooks/git/argv/diff-leading-args.ts +20 -0
  3. mods/diff/hooks/git/argv/git-child-env/git-child-env.ts +8 -0
  4. mods/diff/hooks/git/argv/git-child-env/index.ts +3 -0
  5. mods/diff/hooks/git/argv/index.ts +6 -0
  6. mods/diff/hooks/git/argv/no-optional-locks.ts +5 -0
  7. mods/diff/hooks/git/argv/pinned-lead-of/index.ts +3 -0
  8. mods/diff/hooks/git/argv/pinned-lead-of/pinned-lead-of.ts +18 -0
  9. mods/diff/hooks/git/cut-hunks/cut-hunks.ts +30 -0
  10. mods/diff/hooks/git/cut-hunks/index.ts +3 -0
  11. mods/diff/hooks/git/display-path-of/display-path-of.ts +17 -0
  12. mods/diff/hooks/git/display-path-of/index.ts +3 -0
  13. mods/diff/hooks/git/empty-file-hunks/empty-file-hunks.ts +11 -0
  14. mods/diff/hooks/git/empty-file-hunks/index.ts +3 -0
  15. mods/diff/hooks/git/failed-run/failed-run.ts +11 -0
  16. mods/diff/hooks/git/failed-run/index.ts +3 -0
  17. mods/diff/hooks/git/fetch-diff/fetch-diff.ts +33 -0
  18. mods/diff/hooks/git/fetch-diff/index.ts +3 -0
  19. mods/diff/hooks/git/fetch-file-hunks/fetch-file-hunks.ts +51 -0
  20. mods/diff/hooks/git/fetch-file-hunks/index.ts +5 -0
  21. mods/diff/hooks/git/fetch-file-hunks/is-last-of-diff/index.ts +3 -0
  22. mods/diff/hooks/git/fetch-file-hunks/is-last-of-diff/is-last-of-diff.ts +18 -0
  23. mods/diff/hooks/git/fetch-file-hunks/with-closing-line/index.ts +3 -0
  24. mods/diff/hooks/git/fetch-file-hunks/with-closing-line/with-closing-line.ts +27 -0
  25. mods/diff/hooks/git/git-backend-of.ts +79 -0
  26. mods/diff/hooks/git/git-base-modes/git-base-modes.ts +10 -0
  27. mods/diff/hooks/git/git-base-modes/index.ts +3 -0
  28. mods/diff/hooks/git/git-words/git-words.ts +19 -0
  29. mods/diff/hooks/git/git-words/index.ts +3 -0
  30. mods/diff/hooks/git/hunk-after.ts +20 -0
  31. mods/diff/hooks/git/hunk-of/hunk-of.ts +17 -0
  32. mods/diff/hooks/git/hunk-of/index.ts +3 -0
  33. mods/diff/hooks/git/index.ts +20 -0
  34. mods/diff/hooks/git/large-file-hunks/index.ts +3 -0
  35. mods/diff/hooks/git/large-file-hunks/large-file-hunks.ts +11 -0
  36. mods/diff/hooks/git/parse/hunk-header/hunk-header.ts +5 -0
  37. mods/diff/hooks/git/parse/hunk-header/index.ts +3 -0
  38. mods/diff/hooks/git/parse/index.ts +11 -0
  39. mods/diff/hooks/git/parse/is-body-line/index.ts +3 -0
  40. mods/diff/hooks/git/parse/is-body-line/is-body-line.ts +9 -0
  41. mods/diff/hooks/git/parse/is-whole-answer/index.ts +3 -0
  42. mods/diff/hooks/git/parse/is-whole-answer/is-whole-answer.ts +12 -0
  43. mods/diff/hooks/git/parse/is-whole-listing/index.ts +5 -0
  44. mods/diff/hooks/git/parse/is-whole-listing/is-under-utf8-ceiling/index.ts +3 -0
  45. mods/diff/hooks/git/parse/is-whole-listing/is-under-utf8-ceiling/is-under-utf8-ceiling.ts +13 -0
  46. mods/diff/hooks/git/parse/is-whole-listing/is-whole-listing.ts +18 -0
  47. mods/diff/hooks/git/parse/is-whole-listing/most-utf8-bytes/index.ts +3 -0
  48. mods/diff/hooks/git/parse/is-whole-listing/most-utf8-bytes/most-utf8-bytes.ts +5 -0
  49. mods/diff/hooks/git/parse/parse-file-diff.ts +41 -0
  50. 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
+ }