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/git/probes/branch-base-of/branch-base-of.ts +75 -0
- mods/diff/hooks/git/probes/branch-base-of/index.ts +3 -0
- mods/diff/hooks/git/probes/conventional-default-branches/conventional-default-branches.ts +5 -0
- mods/diff/hooks/git/probes/conventional-default-branches/index.ts +3 -0
- mods/diff/hooks/git/probes/current-branch-of/current-branch-of.ts +21 -0
- mods/diff/hooks/git/probes/current-branch-of/index.ts +3 -0
- mods/diff/hooks/git/probes/datings-of/datings-of.ts +38 -0
- mods/diff/hooks/git/probes/datings-of/index.ts +3 -0
- mods/diff/hooks/git/probes/default-branch-of/default-branch-of.ts +47 -0
- mods/diff/hooks/git/probes/default-branch-of/index.ts +3 -0
- mods/diff/hooks/git/probes/dirty-paths-of/dirty-paths-of.ts +35 -0
- mods/diff/hooks/git/probes/dirty-paths-of/index.ts +4 -0
- mods/diff/hooks/git/probes/dirty-paths-of/status-path-offset/index.ts +3 -0
- mods/diff/hooks/git/probes/dirty-paths-of/status-path-offset/status-path-offset.ts +5 -0
- mods/diff/hooks/git/probes/head-key-of/head-key-of.ts +66 -0
- mods/diff/hooks/git/probes/head-key-of/index.ts +3 -0
- mods/diff/hooks/git/probes/is-absolute-path/index.ts +3 -0
- mods/diff/hooks/git/probes/is-absolute-path/is-absolute-path.ts +9 -0
- mods/diff/hooks/git/probes/is-safe-ref-name/index.ts +4 -0
- mods/diff/hooks/git/probes/is-safe-ref-name/is-safe-ref-name.ts +15 -0
- mods/diff/hooks/git/probes/is-safe-ref-name/safe-ref-pattern/index.ts +3 -0
- mods/diff/hooks/git/probes/is-safe-ref-name/safe-ref-pattern/safe-ref-pattern.ts +5 -0
- mods/diff/hooks/git/probes/is-sha/index.ts +3 -0
- mods/diff/hooks/git/probes/is-sha/is-sha.ts +9 -0
- mods/diff/hooks/git/probes/kind-walk-of/index.ts +3 -0
- mods/diff/hooks/git/probes/kind-walk-of/kind-walk-of.ts +72 -0
- mods/diff/hooks/git/probes/merge-base-candidates-of/index.ts +4 -0
- mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates-of.ts +38 -0
- mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates/index.ts +3 -0
- mods/diff/hooks/git/probes/merge-base-candidates-of/merge-base-candidates/merge-base-candidates.ts +8 -0
- mods/diff/hooks/git/probes/missing-base-of/index.ts +3 -0
- mods/diff/hooks/git/probes/missing-base-of/missing-base-of.ts +39 -0
- mods/diff/hooks/git/probes/repository-lines/index.ts +3 -0
- mods/diff/hooks/git/probes/repository-lines/repository-lines.ts +5 -0
- mods/diff/hooks/git/probes/repository-of/index.ts +3 -0
- mods/diff/hooks/git/probes/repository-of/repository-of.ts +38 -0
- mods/diff/hooks/git/probes/stamp-if-file/index.ts +3 -0
- mods/diff/hooks/git/probes/stamp-if-file/stamp-if-file.ts +29 -0
- mods/diff/hooks/git/probes/stamp-probe-of/index.ts +3 -0
- mods/diff/hooks/git/probes/stamp-probe-of/stamp-probe-of.ts +21 -0
- mods/diff/hooks/git/probes/transient-state-files/index.ts +3 -0
- mods/diff/hooks/git/probes/transient-state-files/transient-state-files.ts +10 -0
- mods/diff/hooks/git/probes/untracked-files/index.ts +3 -0
- mods/diff/hooks/git/probes/untracked-files/untracked-files.ts +40 -0
- mods/diff/hooks/git/probes/untracked-place/index.ts +3 -0
- mods/diff/hooks/git/probes/untracked-place/untracked-place.ts +10 -0
- mods/diff/hooks/git/probes/untracked-rows-of/index.ts +3 -0
- mods/diff/hooks/git/probes/untracked-rows-of/untracked-rows-of.ts +59 -0
- mods/diff/hooks/git/probes/was-dirty-at-start/index.ts +3 -0
- 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
|