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

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/git/probes/branch-base-of/branch-base-of.ts +75 -0
  2. mods/diff/hooks/git/probes/branch-base-of/index.ts +3 -0
  3. mods/diff/hooks/git/probes/conventional-default-branches/conventional-default-branches.ts +5 -0
  4. mods/diff/hooks/git/probes/conventional-default-branches/index.ts +3 -0
  5. mods/diff/hooks/git/probes/current-branch-of/current-branch-of.ts +21 -0
  6. mods/diff/hooks/git/probes/current-branch-of/index.ts +3 -0
  7. mods/diff/hooks/git/probes/datings-of/datings-of.ts +38 -0
  8. mods/diff/hooks/git/probes/datings-of/index.ts +3 -0
  9. mods/diff/hooks/git/probes/default-branch-of/default-branch-of.ts +47 -0
  10. mods/diff/hooks/git/probes/default-branch-of/index.ts +3 -0
  11. mods/diff/hooks/git/probes/dirty-paths-of/dirty-paths-of.ts +35 -0
  12. mods/diff/hooks/git/probes/dirty-paths-of/index.ts +4 -0
  13. mods/diff/hooks/git/probes/dirty-paths-of/status-path-offset/index.ts +3 -0
  14. mods/diff/hooks/git/probes/dirty-paths-of/status-path-offset/status-path-offset.ts +5 -0
  15. mods/diff/hooks/git/probes/head-key-of/head-key-of.ts +66 -0
  16. mods/diff/hooks/git/probes/head-key-of/index.ts +3 -0
  17. mods/diff/hooks/git/probes/is-absolute-path/index.ts +3 -0
  18. mods/diff/hooks/git/probes/is-absolute-path/is-absolute-path.ts +9 -0
  19. mods/diff/hooks/git/probes/is-safe-ref-name/index.ts +4 -0
  20. mods/diff/hooks/git/probes/is-safe-ref-name/is-safe-ref-name.ts +15 -0
  21. mods/diff/hooks/git/probes/is-safe-ref-name/safe-ref-pattern/index.ts +3 -0
  22. mods/diff/hooks/git/probes/is-safe-ref-name/safe-ref-pattern/safe-ref-pattern.ts +5 -0
  23. mods/diff/hooks/git/probes/is-sha/index.ts +3 -0
  24. mods/diff/hooks/git/probes/is-sha/is-sha.ts +9 -0
  25. mods/diff/hooks/git/probes/kind-walk-of/index.ts +3 -0
  26. mods/diff/hooks/git/probes/kind-walk-of/kind-walk-of.ts +72 -0
  27. mods/diff/hooks/git/probes/merge-base-candidates-of/index.ts +4 -0
  28. mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates-of.ts +38 -0
  29. mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates/index.ts +3 -0
  30. mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates/merge-base-candidates.ts +8 -0
  31. mods/diff/hooks/git/probes/missing-base-of/index.ts +3 -0
  32. mods/diff/hooks/git/probes/missing-base-of/missing-base-of.ts +39 -0
  33. mods/diff/hooks/git/probes/repository-lines/index.ts +3 -0
  34. mods/diff/hooks/git/probes/repository-lines/repository-lines.ts +5 -0
  35. mods/diff/hooks/git/probes/repository-of/index.ts +3 -0
  36. mods/diff/hooks/git/probes/repository-of/repository-of.ts +38 -0
  37. mods/diff/hooks/git/probes/stamp-if-file/index.ts +3 -0
  38. mods/diff/hooks/git/probes/stamp-if-file/stamp-if-file.ts +29 -0
  39. mods/diff/hooks/git/probes/stamp-probe-of/index.ts +3 -0
  40. mods/diff/hooks/git/probes/stamp-probe-of/stamp-probe-of.ts +21 -0
  41. mods/diff/hooks/git/probes/transient-state-files/index.ts +3 -0
  42. mods/diff/hooks/git/probes/transient-state-files/transient-state-files.ts +10 -0
  43. mods/diff/hooks/git/probes/untracked-files/index.ts +3 -0
  44. mods/diff/hooks/git/probes/untracked-files/untracked-files.ts +40 -0
  45. mods/diff/hooks/git/probes/untracked-place/index.ts +3 -0
  46. mods/diff/hooks/git/probes/untracked-place/untracked-place.ts +10 -0
  47. mods/diff/hooks/git/probes/untracked-rows-of/index.ts +3 -0
  48. mods/diff/hooks/git/probes/untracked-rows-of/untracked-rows-of.ts +59 -0
  49. mods/diff/hooks/git/probes/was-dirty-at-start/index.ts +3 -0
  50. mods/diff/hooks/git/probes/was-dirty-at-start/was-dirty-at-start.ts +16 -0
mods/diff/hooks/git/probes/branch-base-of/branch-base-of.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import type Types from '../../types'
3
+ import { currentBranchOf } from '../current-branch-of'
4
+ import { defaultBranchOf } from '../default-branch-of'
5
+ import { mergeBaseCandidatesOf } from '../merge-base-candidates-of'
6
+ import { missingBaseOf } from '../missing-base-of'
7
+
8
+ /**
9
+ * What branch mode compares against, resolved as the built-in panel does:
10
+ * the merge-base of HEAD and the default branch, HEAD itself, or none.
11
+ *
12
+ * `origin/<default>` wins unless the local ref's base descends from it; a
13
+ * detached HEAD, a `-`-led default name or no common ancestor is `none`;
14
+ * a failure that may pass (a ref still there, a failed rev-parse) `error`.
15
+ *
16
+ * @param run runs git
17
+ * @returns the base
18
+ */
19
+ export async function branchBaseOf(
20
+ run: Types.GitRun,
21
+ ): Promise<Types.BranchBase> {
22
+ const [branch, defaultBranch] = await Promise.all([
23
+ currentBranchOf(run),
24
+ defaultBranchOf(run),
25
+ ])
26
+
27
+ const isUnrelated = branch === 'HEAD' || defaultBranch.startsWith('-')
28
+
29
+ if (isUnrelated) {
30
+ return { kind: 'none' }
31
+ }
32
+
33
+ if (branch === defaultBranch) {
34
+ return { kind: 'head-is-base', baseBranch: defaultBranch }
35
+ }
36
+
37
+ const { candidates, hasNoCommonAncestor } = await mergeBaseCandidatesOf(
38
+ run,
39
+ defaultBranch,
40
+ )
41
+
42
+ const [first, second] = candidates
43
+
44
+ if (first === undefined) {
45
+ return hasNoCommonAncestor
46
+ ? { kind: 'none' }
47
+ : missingBaseOf(run, defaultBranch)
48
+ }
49
+
50
+ const isLocalBaseNewer =
51
+ second !== undefined &&
52
+ second !== first &&
53
+ (
54
+ await run([
55
+ Argv.NO_OPTIONAL_LOCKS,
56
+ 'merge-base',
57
+ '--is-ancestor',
58
+ first,
59
+ second,
60
+ ])
61
+ ).exitCode === 0
62
+
63
+ const mergeBase = isLocalBaseNewer ? second : first
64
+ const head = await run([Argv.NO_OPTIONAL_LOCKS, 'rev-parse', 'HEAD'])
65
+
66
+ if (head.exitCode !== 0) {
67
+ return { kind: 'error', reason: 'head_rev_parse_failed' }
68
+ }
69
+
70
+ const isHeadTheBase = head.stdout.trim() === mergeBase
71
+
72
+ return isHeadTheBase
73
+ ? { kind: 'head-is-base', baseBranch: defaultBranch }
74
+ : { kind: 'merge-base', mergeBase, baseBranch: defaultBranch }
75
+ }
mods/diff/hooks/git/probes/branch-base-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './branch-base-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/conventional-default-branches/conventional-default-branches.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /**
2
+ * Tried in order as the default branch when `refs/remotes/origin/HEAD` names
3
+ * none that resolves; past them the guess is `main`, as the engine's is.
4
+ */
5
+ export const CONVENTIONAL_DEFAULT_BRANCHES = ['main', 'master'] as const
mods/diff/hooks/git/probes/conventional-default-branches/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './conventional-default-branches.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/current-branch-of/current-branch-of.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import type Types from '../../types'
3
+
4
+ /**
5
+ * The checked-out branch's name, or `HEAD` when detached or unreadable.
6
+ *
7
+ * @param run runs git
8
+ * @returns the branch name
9
+ */
10
+ export async function currentBranchOf(run: Types.GitRun): Promise<string> {
11
+ const { exitCode, stdout } = await run([
12
+ Argv.NO_OPTIONAL_LOCKS,
13
+ 'rev-parse',
14
+ '--abbrev-ref',
15
+ 'HEAD',
16
+ ])
17
+
18
+ const name = stdout.trim()
19
+
20
+ return exitCode === 0 && name !== '' ? name : 'HEAD'
21
+ }
mods/diff/hooks/git/probes/current-branch-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './current-branch-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/datings-of/datings-of.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type Types from '../../types'
2
+ import { stampIfFile } from '../stamp-if-file'
3
+
4
+ /**
5
+ * When each path was last modified against the session's start, dating a
6
+ * path only when it is a real file under real directories at every level.
7
+ *
8
+ * Listings report an entry's own kind, so a symbolic link anywhere on the
9
+ * path is never listed, descended or stat'ed: session work (the engine
10
+ * lstats); a path the listing budget never reached is the caller's to place.
11
+ *
12
+ * @param context the fetch's stamp probe over the working tree's top, and
13
+ * when the session began
14
+ * @param paths repository-relative paths, `/`-separated as git prints them
15
+ * @returns each path's Dating
16
+ */
17
+ export async function datingsOf(
18
+ context: Types.DatingContext,
19
+ paths: readonly string[],
20
+ ): Promise<ReadonlyMap<string, Types.Dating>> {
21
+ const { sessionStartMs } = context.deps
22
+
23
+ const stamps: readonly Types.Stamp[] = await Promise.all(
24
+ paths.map(path => stampIfFile(context.stamps, path)),
25
+ )
26
+
27
+ function datingOf(stamp: Types.Stamp): Types.Dating {
28
+ if (stamp === 'over-budget') {
29
+ return 'unlisted'
30
+ }
31
+
32
+ const isEarlier = (stamp ?? Number.POSITIVE_INFINITY) < sessionStartMs
33
+
34
+ return isEarlier ? 'pre-session' : 'session'
35
+ }
36
+
37
+ return new Map(paths.map((path, at) => [path, datingOf(stamps[at] ?? null)]))
38
+ }
mods/diff/hooks/git/probes/datings-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './datings-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/default-branch-of/default-branch-of.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import type Types from '../../types'
3
+ import { CONVENTIONAL_DEFAULT_BRANCHES } from '../conventional-default-branches'
4
+
5
+ /**
6
+ * The remote's default branch: what `origin/HEAD` names while that ref
7
+ * resolves, else the first conventional name with a remote ref, else main.
8
+ *
9
+ * The engine's own default-branch confidence ladder, over `$.process.run`.
10
+ *
11
+ * @param run runs git
12
+ * @returns the branch name, never empty
13
+ */
14
+ export async function defaultBranchOf(run: Types.GitRun): Promise<string> {
15
+ const hasRemoteRef = async (branch: string): Promise<boolean> =>
16
+ (
17
+ await run([
18
+ Argv.NO_OPTIONAL_LOCKS,
19
+ 'show-ref',
20
+ '--verify',
21
+ '--quiet',
22
+ `refs/remotes/origin/${branch}`,
23
+ ])
24
+ ).exitCode === 0
25
+
26
+ const symref = await run([
27
+ Argv.NO_OPTIONAL_LOCKS,
28
+ 'symbolic-ref',
29
+ '--short',
30
+ 'refs/remotes/origin/HEAD',
31
+ ])
32
+
33
+ const named =
34
+ symref.exitCode === 0 ? symref.stdout.trim().replace(/^origin\//, '') : ''
35
+
36
+ if (named !== '' && (await hasRemoteRef(named))) {
37
+ return named
38
+ }
39
+
40
+ for (const candidate of CONVENTIONAL_DEFAULT_BRANCHES) {
41
+ if (await hasRemoteRef(candidate)) {
42
+ return candidate
43
+ }
44
+ }
45
+
46
+ return 'main'
47
+ }
mods/diff/hooks/git/probes/default-branch-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './default-branch-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/dirty-paths-of/dirty-paths-of.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import Parse from '../../parse'
3
+ import type Types from '../../types'
4
+ import { STATUS_PATH_OFFSET } from './status-path-offset'
5
+
6
+ /**
7
+ * Every path dirty in the working tree right now, tracked changes and
8
+ * untracked files alike, as `git status -z` lists them; null unread.
9
+ *
10
+ * Read once, by the backend's first fetch (it walks the whole tree), so a
11
+ * later fetch can tell a path that was dirty then from one that turned up
12
+ * since (GitDeps `baseline`). Renames unpaired: each side is its own path.
13
+ *
14
+ * @param run runs git against the pinned repository
15
+ * @returns the paths, root-relative, or null on a failed or cut listing
16
+ */
17
+ export async function dirtyPathsOf(
18
+ run: Types.GitRun,
19
+ ): Promise<ReadonlySet<string> | null> {
20
+ const listing = await run([
21
+ Argv.NO_OPTIONAL_LOCKS,
22
+ 'status',
23
+ '--porcelain',
24
+ '-z',
25
+ '--untracked-files=all',
26
+ '--no-renames',
27
+ '--ignore-submodules=dirty',
28
+ ])
29
+
30
+ const records = listing.stdout.split('\0').filter(record => record !== '')
31
+
32
+ return Parse.isWholeAnswer(listing)
33
+ ? new Set(records.map(record => record.slice(STATUS_PATH_OFFSET)))
34
+ : null
35
+ }
mods/diff/hooks/git/probes/dirty-paths-of/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export * from './dirty-paths-of.js'
2
+ export * from './status-path-offset'
3
+
4
+ export * as default from '.'
mods/diff/hooks/git/probes/dirty-paths-of/status-path-offset/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './status-path-offset.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/dirty-paths-of/status-path-offset/status-path-offset.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /**
2
+ * Where the path starts in a `status --porcelain -z` record: after the two
3
+ * status letters and a space.
4
+ */
5
+ export const STATUS_PATH_OFFSET = 3
mods/diff/hooks/git/probes/head-key-of/head-key-of.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Limits from '../../../limits'
2
+ import Argv from '../../argv'
3
+ import type Types from '../../types'
4
+ import { isSafeRefName } from '../is-safe-ref-name'
5
+ import { stampIfFile } from '../stamp-if-file'
6
+ import { stampProbeOf } from '../stamp-probe-of'
7
+
8
+ /**
9
+ * A string that changes when HEAD moves: HEAD's text and the timestamps
10
+ * of HEAD, its ref file and packed-refs, each a listed real file.
11
+ *
12
+ * A HEAD that is not a plain file, or a `ref:` outside the safe refname
13
+ * grammar, is read through `git rev-parse` instead and nothing is stat'ed;
14
+ * every directory above a stat'ed file was listed as a real directory.
15
+ *
16
+ * @param deps git, listings, timestamps, a file read
17
+ * @param repository the repository's directories
18
+ * @returns the key for this tick
19
+ */
20
+ export async function headKeyOf(
21
+ deps: Types.HeadKeyDeps,
22
+ repository: Types.Repository,
23
+ ): Promise<string> {
24
+ const own = stampProbeOf(
25
+ deps,
26
+ repository.gitDir,
27
+ Limits.MAX_LISTED_DIRECTORIES,
28
+ )
29
+
30
+ const isShared = repository.commonDir === repository.gitDir
31
+
32
+ const common = isShared
33
+ ? own
34
+ : stampProbeOf(deps, repository.commonDir, Limits.MAX_LISTED_DIRECTORIES)
35
+
36
+ async function viaGit() {
37
+ const parsed = await deps.run([
38
+ Argv.NO_OPTIONAL_LOCKS,
39
+ 'rev-parse',
40
+ '--verify',
41
+ '--quiet',
42
+ 'HEAD',
43
+ ])
44
+
45
+ return `rev:${parsed.exitCode === 0 ? parsed.stdout.trim() : ''}`
46
+ }
47
+
48
+ if ((await own.walk.kindOf('HEAD')) !== 'file') {
49
+ return viaGit()
50
+ }
51
+
52
+ const head = await deps.readFile(`${repository.gitDir}/HEAD`).catch(() => '')
53
+ const ref = head.startsWith('ref: ') ? head.slice('ref: '.length).trim() : ''
54
+ const isSymbolic = ref !== ''
55
+
56
+ const isUnsafeRef = isSymbolic && !isSafeRefName(ref)
57
+
58
+ return isUnsafeRef
59
+ ? viaGit()
60
+ : [
61
+ head,
62
+ await stampIfFile(own, 'HEAD'),
63
+ isSymbolic ? await stampIfFile(common, ref) : null,
64
+ await stampIfFile(common, 'packed-refs'),
65
+ ].join('|')
66
+ }
mods/diff/hooks/git/probes/head-key-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './head-key-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/is-absolute-path/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './is-absolute-path.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/is-absolute-path/is-absolute-path.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Whether a line `rev-parse --path-format=absolute` printed is an absolute
3
+ * path (POSIX or drive-letter), and so neither relative nor an echoed flag.
4
+ *
5
+ * @param line one line of rev-parse output
6
+ * @returns whether it names an absolute path
7
+ */
8
+ export const isAbsolutePath = (line: string) =>
9
+ line.startsWith('/') || /^[A-Za-z]:[\\/]/.test(line)
mods/diff/hooks/git/probes/is-safe-ref-name/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export * from './is-safe-ref-name.js'
2
+ export * from './safe-ref-pattern'
3
+
4
+ export * as default from '.'
mods/diff/hooks/git/probes/is-safe-ref-name/is-safe-ref-name.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SAFE_REF_PATTERN } from './safe-ref-pattern'
2
+
3
+ /**
4
+ * Whether HEAD's `ref:` text names a ref the poll may stat: under `refs/`,
5
+ * plain characters, no `..`, no component ending `.lock`.
6
+ *
7
+ * Git's refname rules, narrowed; anything else is read through git.
8
+ *
9
+ * @param ref the text after `ref: `
10
+ * @returns whether the ref is safe to join under the common directory
11
+ */
12
+ export const isSafeRefName = (ref: string) =>
13
+ SAFE_REF_PATTERN.test(ref) &&
14
+ !ref.includes('..') &&
15
+ !ref.split('/').some(part => part.endsWith('.lock'))
mods/diff/hooks/git/probes/is-safe-ref-name/safe-ref-pattern/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './safe-ref-pattern.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/is-safe-ref-name/safe-ref-pattern/safe-ref-pattern.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /**
2
+ * The refnames the HEAD poll will touch on disk: under `refs/`, of plain
3
+ * name characters only, `/` between components.
4
+ */
5
+ export const SAFE_REF_PATTERN = /^refs(?:\/[A-Za-z0-9_@+-][A-Za-z0-9._@+-]*)+$/
mods/diff/hooks/git/probes/is-sha/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './is-sha.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/is-sha/is-sha.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Whether git printed an object name: 40 or 64 lowercase hex digits, the
3
+ * only shape a merge-base may take before it reaches a `git diff` argv.
4
+ *
5
+ * @param text one trimmed line of git output
6
+ * @returns whether it is a full object name
7
+ */
8
+ export const isSha = (text: string) =>
9
+ /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(text)
mods/diff/hooks/git/probes/kind-walk-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './kind-walk-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/kind-walk-of/kind-walk-of.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type Types from '../../types'
2
+
3
+ /**
4
+ * A KindWalk: each directory is listed once, only after its own entry
5
+ * read as `dir` in its parent, and only while listings remain.
6
+ *
7
+ * Read from the root down, one listing a component: the depth is the
8
+ * budget's, never the stack's. A symbolic link is never listed or descended
9
+ * and what lies under it reads as null; the unlisted reads `over-budget`.
10
+ *
11
+ * @param entryKindsOf the host's directory listing
12
+ * @param root the absolute directory the walk's paths are relative to
13
+ * @param budget how many directories this walk may list
14
+ * @returns the walk
15
+ */
16
+ export function kindWalkOf(
17
+ entryKindsOf: Types.GitDeps['entryKindsOf'],
18
+ root: string,
19
+ budget: number,
20
+ ): Types.KindWalk {
21
+ const listings = new Map<
22
+ string,
23
+ Promise<ReadonlyMap<string, Types.EntryKind> | null>
24
+ >()
25
+
26
+ function listingOf(directory: string) {
27
+ const held = listings.get(directory)
28
+ const isOverBudget = held === undefined && listings.size >= budget
29
+
30
+ if (isOverBudget) {
31
+ return 'over-budget'
32
+ }
33
+
34
+ const listed =
35
+ held ?? entryKindsOf(directory === '' ? root : `${root}/${directory}`)
36
+
37
+ listings.set(directory, listed)
38
+
39
+ return listed
40
+ }
41
+
42
+ async function kindOf(path: string): Promise<Types.WalkKind> {
43
+ const names = path === '' ? [] : path.split('/')
44
+
45
+ let directory = ''
46
+
47
+ for (const [depth, name] of names.entries()) {
48
+ const listing = listingOf(directory)
49
+
50
+ if (listing === 'over-budget') {
51
+ return 'over-budget'
52
+ }
53
+
54
+ const kind = (await listing)?.get(name) ?? null
55
+ const isLeaf = depth === names.length - 1
56
+
57
+ if (isLeaf) {
58
+ return kind
59
+ }
60
+
61
+ if (kind !== 'dir') {
62
+ return null
63
+ }
64
+
65
+ directory = directory === '' ? name : `${directory}/${name}`
66
+ }
67
+
68
+ return 'dir'
69
+ }
70
+
71
+ return { kindOf }
72
+ }
mods/diff/hooks/git/probes/merge-base-candidates-of/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export * from './merge-base-candidates'
2
+ export * from './merge-base-candidates-of.js'
3
+
4
+ export * as default from '.'
mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates-of.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import type Types from '../../types'
3
+ import { isSha } from '../is-sha'
4
+ import type { MergeBaseCandidates } from './merge-base-candidates'
5
+
6
+ /**
7
+ * The merge-bases of HEAD with `origin/<default>` and with `<default>`, in
8
+ * that order, each present only when git printed an object name.
9
+ *
10
+ * Also whether any call ran and exited 1: no common ancestor, a stable
11
+ * state rather than a failure.
12
+ *
13
+ * @param run runs git
14
+ * @param defaultBranch the default branch's name
15
+ * @returns the candidate object names and the no-ancestor flag
16
+ */
17
+ export async function mergeBaseCandidatesOf(
18
+ run: Types.GitRun,
19
+ defaultBranch: string,
20
+ ): Promise<MergeBaseCandidates> {
21
+ const results = [
22
+ await run([
23
+ Argv.NO_OPTIONAL_LOCKS,
24
+ 'merge-base',
25
+ 'HEAD',
26
+ `origin/${defaultBranch}`,
27
+ ]),
28
+ await run([Argv.NO_OPTIONAL_LOCKS, 'merge-base', 'HEAD', defaultBranch]),
29
+ ]
30
+
31
+ return {
32
+ candidates: results
33
+ .filter(result => result.exitCode === 0)
34
+ .map(result => result.stdout.trim())
35
+ .filter(isSha),
36
+ hasNoCommonAncestor: results.some(result => result.exitCode === 1),
37
+ }
38
+ }
mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export type * from './merge-base-candidates.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates/merge-base-candidates.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * What the two merge-base calls yielded: the object names printed, in
3
+ * call order, and whether either ran to a no-common-ancestor exit.
4
+ */
5
+ export type MergeBaseCandidates = {
6
+ candidates: readonly string[]
7
+ hasNoCommonAncestor: boolean
8
+ }
mods/diff/hooks/git/probes/missing-base-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './missing-base-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/missing-base-of/missing-base-of.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import type Types from '../../types'
3
+
4
+ /**
5
+ * The verdict for a merge-base that produced nothing: `none` when neither
6
+ * ref of the default branch exists, else `error`.
7
+ *
8
+ * Only a probe that ran and exited non-zero is an authoritative miss; one
9
+ * that answered 0 or never ran means the merge-base failure may pass.
10
+ *
11
+ * @param run runs git
12
+ * @param defaultBranch the default branch's name
13
+ * @returns `none` or `error`
14
+ */
15
+ export async function missingBaseOf(
16
+ run: Types.GitRun,
17
+ defaultBranch: string,
18
+ ): Promise<Types.BranchBase> {
19
+ for (const ref of [
20
+ `refs/remotes/origin/${defaultBranch}`,
21
+ `refs/heads/${defaultBranch}`,
22
+ ]) {
23
+ const { exitCode } = await run([
24
+ Argv.NO_OPTIONAL_LOCKS,
25
+ 'show-ref',
26
+ '--verify',
27
+ '--quiet',
28
+ ref,
29
+ ])
30
+
31
+ const isPresentOrUnknown = exitCode === 0 || exitCode === -1
32
+
33
+ if (isPresentOrUnknown) {
34
+ return { kind: 'error', reason: 'merge_base_failed' }
35
+ }
36
+ }
37
+
38
+ return { kind: 'none' }
39
+ }
mods/diff/hooks/git/probes/repository-lines/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './repository-lines.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/repository-lines/repository-lines.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /**
2
+ * How many lines the repository probe's `rev-parse` answers when git
3
+ * understood every option: top level, git directory, common directory.
4
+ */
5
+ export const REPOSITORY_LINES = 3
mods/diff/hooks/git/probes/repository-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './repository-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/repository-of/repository-of.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import type Types from '../../types'
3
+ import { isAbsolutePath } from '../is-absolute-path'
4
+ import { REPOSITORY_LINES } from '../repository-lines'
5
+
6
+ /**
7
+ * The repository the session's directory is in, read with one `rev-parse`:
8
+ * working tree top, git directory, common directory, all absolute.
9
+ *
10
+ * Exactly three absolute lines or nothing: a git too old for
11
+ * `--path-format` echoes the flag as a line of its own, and such an answer
12
+ * pins no repository rather than a wrong one.
13
+ *
14
+ * @param run runs git in the session's directory, nothing pinned yet
15
+ * @returns the three absolute paths, or null outside a working tree
16
+ */
17
+ export async function repositoryOf(
18
+ run: Types.GitRun,
19
+ ): Promise<Types.Repository | null> {
20
+ const { exitCode, stdout } = await run([
21
+ Argv.NO_OPTIONAL_LOCKS,
22
+ 'rev-parse',
23
+ '--path-format=absolute',
24
+ '--show-toplevel',
25
+ '--git-dir',
26
+ '--git-common-dir',
27
+ ])
28
+
29
+ const lines = stdout.split('\n').filter(line => line !== '')
30
+ const [toplevel = '', gitDir = '', commonDir = ''] = lines
31
+
32
+ const isResolved =
33
+ exitCode === 0 &&
34
+ lines.length === REPOSITORY_LINES &&
35
+ lines.every(isAbsolutePath)
36
+
37
+ return isResolved ? { toplevel, gitDir, commonDir } : null
38
+ }
mods/diff/hooks/git/probes/stamp-if-file/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './stamp-if-file.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/stamp-if-file/stamp-if-file.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type Types from '../../types'
2
+
3
+ /**
4
+ * A path's modification time when the walk shows it as a real entry of the
5
+ * wanted kind under real directories; otherwise nothing is stat'ed.
6
+ *
7
+ * A path git printed with a trailing `/` (an untracked directory, a nested
8
+ * repository) names a directory and is dated as one.
9
+ *
10
+ * @param probe the walk over the base directory and the timestamp probe
11
+ * @param path the path relative to the walk's root
12
+ * @returns the timestamp, `over-budget`, or null
13
+ */
14
+ export async function stampIfFile(
15
+ probe: Types.StampProbe,
16
+ path: string,
17
+ ): Promise<Types.Stamp> {
18
+ const isDirectory = path.endsWith('/')
19
+ const entry = isDirectory ? path.slice(0, -1) : path
20
+ const kind = await probe.walk.kindOf(entry)
21
+ const isUnlisted = kind === 'over-budget'
22
+ const isWanted = kind === (isDirectory ? 'dir' : 'file')
23
+
24
+ return isUnlisted
25
+ ? 'over-budget'
26
+ : isWanted
27
+ ? probe.mtimeOf(`${probe.base}/${entry}`)
28
+ : null
29
+ }
mods/diff/hooks/git/probes/stamp-probe-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './stamp-probe-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/stamp-probe-of/stamp-probe-of.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type Types from '../../types'
2
+ import { kindWalkOf } from '../kind-walk-of'
3
+
4
+ /**
5
+ * A StampProbe rooted at a directory: a fresh, bounded kind walk over it
6
+ * and the host's timestamp probe.
7
+ *
8
+ * @param deps lists a directory, reads a timestamp
9
+ * @param base the absolute directory the probe's paths are under
10
+ * @param budget how many directories the walk may list
11
+ * @returns the probe
12
+ */
13
+ export const stampProbeOf = (
14
+ deps: Pick<Types.GitDeps, 'entryKindsOf' | 'mtimeOf'>,
15
+ base: string,
16
+ budget: number,
17
+ ): Types.StampProbe => ({
18
+ walk: kindWalkOf(deps.entryKindsOf, base, budget),
19
+ base,
20
+ mtimeOf: deps.mtimeOf,
21
+ })
mods/diff/hooks/git/probes/transient-state-files/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './transient-state-files.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/transient-state-files/transient-state-files.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * The git-directory files that mean a merge, rebase, cherry-pick or revert
3
+ * is under way: the working tree then holds incoming changes, unread.
4
+ */
5
+ export const TRANSIENT_STATE_FILES = [
6
+ 'MERGE_HEAD',
7
+ 'REBASE_HEAD',
8
+ 'CHERRY_PICK_HEAD',
9
+ 'REVERT_HEAD',
10
+ ] as const
mods/diff/hooks/git/probes/untracked-files/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './untracked-files.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/untracked-files/untracked-files.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Argv from '../../argv'
2
+ import Parse from '../../parse'
3
+ import type Types from '../../types'
4
+ import type { UntrackedPlace } from '../untracked-place'
5
+ import { untrackedRowsOf } from '../untracked-rows-of'
6
+
7
+ /**
8
+ * The untracked, unignored files as 0/0 rows (untrackedRowsOf), listed
9
+ * with `ls-files -z`. A listing not given whole: no answer.
10
+ *
11
+ * @param context the fetch: its pinned git, stamp probe and session start
12
+ * @param place the free row slots and the scope
13
+ * @returns the rows to merge after the tracked ones, or null when the
14
+ * `-z` listing failed, timed out or was cut
15
+ */
16
+ export async function untrackedFiles(
17
+ context: Pick<Types.FetchContext, 'run'> & Types.DatingContext,
18
+ place: UntrackedPlace,
19
+ ): Promise<readonly Types.FileStat[] | null> {
20
+ if (place.slots <= 0) {
21
+ return []
22
+ }
23
+
24
+ const listing = await context.run([
25
+ Argv.NO_OPTIONAL_LOCKS,
26
+ 'ls-files',
27
+ '-z',
28
+ '--others',
29
+ '--exclude-standard',
30
+ '--full-name',
31
+ ])
32
+
33
+ return Parse.isWholeAnswer(listing)
34
+ ? untrackedRowsOf(
35
+ context,
36
+ listing.stdout.split('\0').filter(path => path !== ''),
37
+ place,
38
+ )
39
+ : null
40
+ }
mods/diff/hooks/git/probes/untracked-place/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export type * from './untracked-place.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/untracked-place/untracked-place.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type Types from '../../types'
2
+
3
+ /**
4
+ * How much untrackedFiles may keep: the row slots left under MAX_FILES, and
5
+ * which files count.
6
+ */
7
+ export type UntrackedPlace = {
8
+ slots: number
9
+ scope: Types.UntrackedScope
10
+ }
mods/diff/hooks/git/probes/untracked-rows-of/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './untracked-rows-of.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/untracked-rows-of/untracked-rows-of.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Limits from '../../../limits'
2
+ import type Types from '../../types'
3
+ import { datingsOf } from '../datings-of'
4
+ import type { UntrackedPlace } from '../untracked-place'
5
+ import { wasDirtyAtStart } from '../was-dirty-at-start'
6
+
7
+ /**
8
+ * Listed untracked paths as 0/0 rows: the session's own first, then,
9
+ * when the scope asks, the tagged pre-session ones, up to the slots.
10
+ *
11
+ * At most MAX_UNTRACKED_PROBES are dated; a file past that or past the
12
+ * listing budget reads as pre-session, an undatable one or one that turned
13
+ * up since the start (wasDirtyAtStart) as the session's (the built-in's).
14
+ *
15
+ * @param context the fetch's stamp probe and session start
16
+ * @param paths the untracked paths, root-relative, as the lister printed them
17
+ * @param place the free row slots and the scope
18
+ * @returns the rows to merge after the tracked ones
19
+ */
20
+ export async function untrackedRowsOf(
21
+ context: Types.DatingContext,
22
+ paths: readonly string[],
23
+ place: UntrackedPlace,
24
+ ): Promise<readonly Types.FileStat[]> {
25
+ const probedPaths = paths.slice(0, Limits.MAX_UNTRACKED_PROBES)
26
+ const datings = await datingsOf(context, probedPaths)
27
+
28
+ const isPreSessionOf = (path: string, dating: Types.Dating | undefined) =>
29
+ dating === 'unlisted' ||
30
+ (dating === 'pre-session' && wasDirtyAtStart(context, path))
31
+
32
+ const probed = probedPaths.map(path => ({
33
+ path,
34
+ isPreSession: isPreSessionOf(path, datings.get(path)),
35
+ }))
36
+
37
+ const isWithPreSession = place.scope === 'with-pre-session'
38
+
39
+ const earlier = isWithPreSession
40
+ ? [
41
+ ...probed.filter(file => file.isPreSession),
42
+ ...paths
43
+ .slice(Limits.MAX_UNTRACKED_PROBES)
44
+ .map(path => ({ path, isPreSession: true })),
45
+ ]
46
+ : []
47
+
48
+ const ordered = [...probed.filter(file => !file.isPreSession), ...earlier]
49
+
50
+ return ordered.slice(0, place.slots).map(file => ({
51
+ path: file.path,
52
+ renamedFrom: null,
53
+ added: 0,
54
+ removed: 0,
55
+ isBinary: false,
56
+ isUntracked: true,
57
+ isPreSession: file.isPreSession,
58
+ }))
59
+ }
mods/diff/hooks/git/probes/was-dirty-at-start/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export * from './was-dirty-at-start.js'
2
+
3
+ export * as default from '.'
mods/diff/hooks/git/probes/was-dirty-at-start/was-dirty-at-start.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type Types from '../../types'
2
+
3
+ /**
4
+ * Whether a path may predate the session at all: it was dirty when the
5
+ * baseline was read (the first fetch), or nothing says otherwise (none was).
6
+ *
7
+ * A path missing from the baseline turned up since, so its old timestamp
8
+ * (a rename keeps the file's, a move keeps the source's) does not date it,
9
+ * as the built-in's change-time check does not let it.
10
+ *
11
+ * @param context the fetch's dating context
12
+ * @param path the root-relative path
13
+ * @returns false only when a baseline exists and lacks the path
14
+ */
15
+ export const wasDirtyAtStart = (context: Types.DatingContext, path: string) =>
16
+ context.deps.baseline?.has(path) ?? true