javi-forge 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/git-diff.d.ts +29 -0
- package/dist/lib/git-diff.js +132 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the base ref to diff HEAD against, forge-agnostic. Precedence:
|
|
3
|
+
* 1. `$CI_MERGE_REQUEST_DIFF_BASE_SHA` (GitLab MR) when non-empty.
|
|
4
|
+
* 2. `$CI_COMMIT_BEFORE_SHA` (GitLab push) when non-empty AND not the
|
|
5
|
+
* all-zeros new-branch sentinel.
|
|
6
|
+
* 3. `$GITHUB_BASE_REF` (GitHub Actions PR) → `git merge-base origin/<ref> HEAD`.
|
|
7
|
+
* 4. Local fallback: `git merge-base <candidate> HEAD` over
|
|
8
|
+
* `origin/main`, `origin/master`, `main`, `master` — first that resolves.
|
|
9
|
+
* 5. Nothing resolves → `null` (caller loud-degrades).
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveBaseRef(env: Record<string, string | undefined>, cwd: string): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* The union (deduped) of files changed relative to `base`:
|
|
14
|
+
* - committed: `git diff --name-only --diff-filter=ACMR <base>...HEAD`
|
|
15
|
+
* (three-dot; ACMR keeps Added/Copied/Modified/Renamed, drops deletions)
|
|
16
|
+
* - unstaged: `git diff --name-only`
|
|
17
|
+
* - staged: `git diff --name-only --cached`
|
|
18
|
+
*
|
|
19
|
+
* Invoked as `execFileAsync("git", [...], { cwd })` — an argv array, never a
|
|
20
|
+
* shell string.
|
|
21
|
+
*
|
|
22
|
+
* THROWS if any git invocation fails. A base sha absent from local history
|
|
23
|
+
* (CI shallow clone / bad object) makes the committed diff error; that failure
|
|
24
|
+
* MUST propagate so the caller can skip the scope:changed gate with a named
|
|
25
|
+
* warning. It MUST NOT be swallowed into an empty set — an empty set means
|
|
26
|
+
* "no changed files" and would silently pass a scope:changed gate.
|
|
27
|
+
*/
|
|
28
|
+
export declare function changedFiles(base: string, cwd: string): Promise<string[]>;
|
|
29
|
+
//# sourceMappingURL=git-diff.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execFileAsync } from "./exec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Forge-agnostic changed-file diff engine for `scope: changed` gates.
|
|
4
|
+
*
|
|
5
|
+
* Two injectable functions:
|
|
6
|
+
* - {@link resolveBaseRef} — resolve the base commit to diff HEAD against,
|
|
7
|
+
* following a forge-agnostic precedence chain (GitLab MR / GitLab push /
|
|
8
|
+
* GitHub PR / local merge-base). Returns `null` when nothing resolves so the
|
|
9
|
+
* caller can loud-degrade (skip the scope:changed gate with a named warning).
|
|
10
|
+
* - {@link changedFiles} — the union of committed (Added/Copied/Modified/Renamed,
|
|
11
|
+
* deletions dropped), unstaged, and staged changes. THROWS on a git failure
|
|
12
|
+
* (e.g. a base sha absent from local history under a CI shallow clone) so the
|
|
13
|
+
* caller can skip-with-warning; it MUST NOT swallow the failure into an empty
|
|
14
|
+
* set (that would look like "no changes" and silently pass a scope gate).
|
|
15
|
+
*
|
|
16
|
+
* This module is UNWIRED: nothing in the run path imports it yet. The gate
|
|
17
|
+
* phase consumes it in a later slice.
|
|
18
|
+
*/
|
|
19
|
+
/** The all-zeros sha git emits for a brand-new branch's "before" ref. */
|
|
20
|
+
const NEW_BRANCH_SENTINEL = "0".repeat(40);
|
|
21
|
+
/**
|
|
22
|
+
* Local base-ref candidates, tried in order. The first whose `git merge-base
|
|
23
|
+
* <candidate> HEAD` resolves wins.
|
|
24
|
+
*/
|
|
25
|
+
const LOCAL_BASE_CANDIDATES = [
|
|
26
|
+
"origin/main",
|
|
27
|
+
"origin/master",
|
|
28
|
+
"main",
|
|
29
|
+
"master",
|
|
30
|
+
];
|
|
31
|
+
function isNonEmpty(value) {
|
|
32
|
+
return typeof value === "string" && value.length > 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compute `git merge-base <ref> HEAD` in `cwd`, returning the resolved sha or
|
|
36
|
+
* `null` when the ref does not exist / has no common ancestor.
|
|
37
|
+
*/
|
|
38
|
+
async function tryMergeBase(ref, cwd) {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFileAsync("git", ["merge-base", ref, "HEAD"], {
|
|
41
|
+
cwd,
|
|
42
|
+
});
|
|
43
|
+
const sha = stdout.trim();
|
|
44
|
+
return sha.length > 0 ? sha : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the base ref to diff HEAD against, forge-agnostic. Precedence:
|
|
52
|
+
* 1. `$CI_MERGE_REQUEST_DIFF_BASE_SHA` (GitLab MR) when non-empty.
|
|
53
|
+
* 2. `$CI_COMMIT_BEFORE_SHA` (GitLab push) when non-empty AND not the
|
|
54
|
+
* all-zeros new-branch sentinel.
|
|
55
|
+
* 3. `$GITHUB_BASE_REF` (GitHub Actions PR) → `git merge-base origin/<ref> HEAD`.
|
|
56
|
+
* 4. Local fallback: `git merge-base <candidate> HEAD` over
|
|
57
|
+
* `origin/main`, `origin/master`, `main`, `master` — first that resolves.
|
|
58
|
+
* 5. Nothing resolves → `null` (caller loud-degrades).
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveBaseRef(env, cwd) {
|
|
61
|
+
// 1. GitLab merge request — the base sha is provided directly.
|
|
62
|
+
if (isNonEmpty(env.CI_MERGE_REQUEST_DIFF_BASE_SHA)) {
|
|
63
|
+
return env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
64
|
+
}
|
|
65
|
+
// 2. GitLab push — the previous sha, unless it is the new-branch sentinel.
|
|
66
|
+
if (isNonEmpty(env.CI_COMMIT_BEFORE_SHA) &&
|
|
67
|
+
env.CI_COMMIT_BEFORE_SHA !== NEW_BRANCH_SENTINEL) {
|
|
68
|
+
return env.CI_COMMIT_BEFORE_SHA;
|
|
69
|
+
}
|
|
70
|
+
// 3. GitHub Actions — a PR sets GITHUB_BASE_REF (merge-base against the
|
|
71
|
+
// target branch); a push has no base ref and falls back to GITHUB_SHA.
|
|
72
|
+
if (isNonEmpty(env.GITHUB_BASE_REF)) {
|
|
73
|
+
const base = await tryMergeBase(`origin/${env.GITHUB_BASE_REF}`, cwd);
|
|
74
|
+
if (base !== null)
|
|
75
|
+
return base;
|
|
76
|
+
}
|
|
77
|
+
else if (isNonEmpty(env.GITHUB_SHA)) {
|
|
78
|
+
return env.GITHUB_SHA;
|
|
79
|
+
}
|
|
80
|
+
// 4. Local fallback — first candidate whose merge-base resolves.
|
|
81
|
+
for (const candidate of LOCAL_BASE_CANDIDATES) {
|
|
82
|
+
const base = await tryMergeBase(candidate, cwd);
|
|
83
|
+
if (base !== null)
|
|
84
|
+
return base;
|
|
85
|
+
}
|
|
86
|
+
// 5. Nothing resolved.
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse `git diff --name-only` stdout into a list of repo-root-relative paths,
|
|
91
|
+
* dropping blank lines.
|
|
92
|
+
*/
|
|
93
|
+
function parseNameOnly(stdout) {
|
|
94
|
+
return stdout
|
|
95
|
+
.split("\n")
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
.filter((line) => line.length > 0);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The union (deduped) of files changed relative to `base`:
|
|
101
|
+
* - committed: `git diff --name-only --diff-filter=ACMR <base>...HEAD`
|
|
102
|
+
* (three-dot; ACMR keeps Added/Copied/Modified/Renamed, drops deletions)
|
|
103
|
+
* - unstaged: `git diff --name-only`
|
|
104
|
+
* - staged: `git diff --name-only --cached`
|
|
105
|
+
*
|
|
106
|
+
* Invoked as `execFileAsync("git", [...], { cwd })` — an argv array, never a
|
|
107
|
+
* shell string.
|
|
108
|
+
*
|
|
109
|
+
* THROWS if any git invocation fails. A base sha absent from local history
|
|
110
|
+
* (CI shallow clone / bad object) makes the committed diff error; that failure
|
|
111
|
+
* MUST propagate so the caller can skip the scope:changed gate with a named
|
|
112
|
+
* warning. It MUST NOT be swallowed into an empty set — an empty set means
|
|
113
|
+
* "no changed files" and would silently pass a scope:changed gate.
|
|
114
|
+
*/
|
|
115
|
+
export async function changedFiles(base, cwd) {
|
|
116
|
+
const invocations = [
|
|
117
|
+
["diff", "--name-only", "--diff-filter=ACMR", `${base}...HEAD`],
|
|
118
|
+
["diff", "--name-only"],
|
|
119
|
+
["diff", "--name-only", "--cached"],
|
|
120
|
+
];
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
for (const args of invocations) {
|
|
123
|
+
// Deliberately NOT wrapped in try/catch: a git failure here (shallow
|
|
124
|
+
// clone / missing base object) must surface to the caller.
|
|
125
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
126
|
+
for (const file of parseNameOnly(stdout)) {
|
|
127
|
+
seen.add(file);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...seen];
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=git-diff.js.map
|