@getmarrow/mcp 3.9.102 → 3.9.103
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/README.md +49 -23
- package/dist/cli.js +41 -10
- package/dist/cli.js.map +1 -1
- package/dist/codex-plugin-pins.d.ts +19 -0
- package/dist/codex-plugin-pins.d.ts.map +1 -0
- package/dist/codex-plugin-pins.js +43 -0
- package/dist/codex-plugin-pins.js.map +1 -0
- package/dist/codex-plugins.d.ts +34 -0
- package/dist/codex-plugins.d.ts.map +1 -0
- package/dist/codex-plugins.js +240 -0
- package/dist/codex-plugins.js.map +1 -0
- package/dist/hook-contract.d.ts +2 -2
- package/dist/hook-contract.d.ts.map +1 -1
- package/dist/hook-contract.js +8 -3
- package/dist/hook-contract.js.map +1 -1
- package/dist/hook-pre-action.d.ts +48 -2
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +195 -22
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook.d.ts +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +12 -6
- package/dist/hook.js.map +1 -1
- package/dist/host-session.d.ts +11 -0
- package/dist/host-session.d.ts.map +1 -1
- package/dist/host-session.js +49 -0
- package/dist/host-session.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -10
- package/dist/index.js.map +1 -1
- package/dist/normalized-action.d.ts +29 -6
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +492 -102
- package/dist/normalized-action.js.map +1 -1
- package/dist/own-server-identity.d.ts +123 -0
- package/dist/own-server-identity.d.ts.map +1 -0
- package/dist/own-server-identity.js +1052 -0
- package/dist/own-server-identity.js.map +1 -0
- package/dist/own-tool.d.ts +7 -3
- package/dist/own-tool.d.ts.map +1 -1
- package/dist/own-tool.js +7 -3
- package/dist/own-tool.js.map +1 -1
- package/dist/redact.d.ts +23 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +181 -8
- package/dist/redact.js.map +1 -1
- package/dist/seatbelt/classifier-pin.d.ts +15 -0
- package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
- package/dist/seatbelt/classifier-pin.js +26 -0
- package/dist/seatbelt/classifier-pin.js.map +1 -0
- package/dist/seatbelt/guards.d.ts +10 -0
- package/dist/seatbelt/guards.d.ts.map +1 -0
- package/dist/seatbelt/guards.js +509 -0
- package/dist/seatbelt/guards.js.map +1 -0
- package/dist/seatbelt/hook.d.ts +72 -0
- package/dist/seatbelt/hook.d.ts.map +1 -0
- package/dist/seatbelt/hook.js +362 -0
- package/dist/seatbelt/hook.js.map +1 -0
- package/dist/seatbelt/longopts.d.ts +68 -0
- package/dist/seatbelt/longopts.d.ts.map +1 -0
- package/dist/seatbelt/longopts.js +377 -0
- package/dist/seatbelt/longopts.js.map +1 -0
- package/dist/seatbelt/mapper.d.ts +83 -0
- package/dist/seatbelt/mapper.d.ts.map +1 -0
- package/dist/seatbelt/mapper.js +10248 -0
- package/dist/seatbelt/mapper.js.map +1 -0
- package/dist/seatbelt/record.d.ts +94 -0
- package/dist/seatbelt/record.d.ts.map +1 -0
- package/dist/seatbelt/record.js +776 -0
- package/dist/seatbelt/record.js.map +1 -0
- package/dist/seatbelt/tables.d.ts +82 -0
- package/dist/seatbelt/tables.d.ts.map +1 -0
- package/dist/seatbelt/tables.js +359 -0
- package/dist/seatbelt/tables.js.map +1 -0
- package/dist/seatbelt-deploy.d.ts +100 -0
- package/dist/seatbelt-deploy.d.ts.map +1 -0
- package/dist/seatbelt-deploy.js +393 -0
- package/dist/seatbelt-deploy.js.map +1 -0
- package/dist/seatbelt-fs.d.ts +30 -0
- package/dist/seatbelt-fs.d.ts.map +1 -0
- package/dist/seatbelt-fs.js +94 -0
- package/dist/seatbelt-fs.js.map +1 -0
- package/dist/seatbelt-git.d.ts +212 -0
- package/dist/seatbelt-git.d.ts.map +1 -0
- package/dist/seatbelt-git.js +844 -0
- package/dist/seatbelt-git.js.map +1 -0
- package/dist/seatbelt-mode.d.ts +77 -0
- package/dist/seatbelt-mode.d.ts.map +1 -0
- package/dist/seatbelt-mode.js +496 -0
- package/dist/seatbelt-mode.js.map +1 -0
- package/dist/seatbelt-path.d.ts +123 -0
- package/dist/seatbelt-path.d.ts.map +1 -0
- package/dist/seatbelt-path.js +482 -0
- package/dist/seatbelt-path.js.map +1 -0
- package/dist/seatbelt-scripts.d.ts +109 -0
- package/dist/seatbelt-scripts.d.ts.map +1 -0
- package/dist/seatbelt-scripts.js +1060 -0
- package/dist/seatbelt-scripts.js.map +1 -0
- package/dist/tool-catalog.js +2 -2
- package/dist/tool-catalog.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/server.json +3 -3
- package/vendor/classifier/MANIFEST.json +25 -0
- package/vendor/classifier/classifier.js +1356 -0
- package/vendor/classifier/policy-baseline.js +187 -0
- package/vendor/classifier/redact.js +59 -0
- package/vendor/classifier/shell-parse.js +455 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BASELINE_PROTECTED_BRANCHES = exports.GIT_PUSH_LONG_OPTIONS = exports.GIT_PUSH_RISKY_OPTIONS = exports.GIT_MAYBE_COMMANDS = exports.GIT_CORE_BUILTINS = exports.GIT_EXEC_ENV = exports.GIT_RELOCATING_ENV = exports.MAX_CONFIG_FILES = exports.MAX_ALIAS_STEPS = exports.MAX_ALIAS_DEPTH = exports.MAX_INCLUDE_DEPTH = exports.MAX_GIT_CONFIG_BYTES = void 0;
|
|
4
|
+
exports.canonicalGitKey = canonicalGitKey;
|
|
5
|
+
exports.parseGitConfigText = parseGitConfigText;
|
|
6
|
+
exports.gitExecVectors = gitExecVectors;
|
|
7
|
+
exports.gitRelocatingEnv = gitRelocatingEnv;
|
|
8
|
+
exports.discoverGitDirs = discoverGitDirs;
|
|
9
|
+
exports.loadGitConfig = loadGitConfig;
|
|
10
|
+
exports.withInvocationConfig = withInvocationConfig;
|
|
11
|
+
exports.gitConfigValues = gitConfigValues;
|
|
12
|
+
exports.splitGitCmdline = splitGitCmdline;
|
|
13
|
+
exports.parseGitInvocation = parseGitInvocation;
|
|
14
|
+
exports.expandGitAliases = expandGitAliases;
|
|
15
|
+
exports.gitLongOptionCandidates = gitLongOptionCandidates;
|
|
16
|
+
exports.readGitHead = readGitHead;
|
|
17
|
+
exports.readRemoteDefaultBranch = readRemoteDefaultBranch;
|
|
18
|
+
exports.isProtectedBranch = isProtectedBranch;
|
|
19
|
+
exports.resolvePushRefspec = resolvePushRefspec;
|
|
20
|
+
exports.implicitPushTargets = implicitPushTargets;
|
|
21
|
+
const seatbelt_path_1 = require("./seatbelt-path");
|
|
22
|
+
// Local git resolution for seatbelt rule S1 (spec §1.2): config from every
|
|
23
|
+
// scope and include, aliases (including `!sh` aliases), HEAD, the remote
|
|
24
|
+
// default branch and implicit push targets. Where git would pick one value
|
|
25
|
+
// (last one wins), these resolvers keep every candidate so the mapper can stop
|
|
26
|
+
// if any of them is risky: config can only make an answer stricter.
|
|
27
|
+
exports.MAX_GIT_CONFIG_BYTES = 1024 * 1024;
|
|
28
|
+
exports.MAX_INCLUDE_DEPTH = 10;
|
|
29
|
+
exports.MAX_ALIAS_DEPTH = 10;
|
|
30
|
+
/** Total alias nodes and config files visited: a planted config cannot fan out without bound. */
|
|
31
|
+
exports.MAX_ALIAS_STEPS = 256;
|
|
32
|
+
exports.MAX_CONFIG_FILES = 64;
|
|
33
|
+
/** Canonical config key: git compares section and variable names case-insensitively, subsections exactly. */
|
|
34
|
+
function canonicalGitKey(key) {
|
|
35
|
+
const first = key.indexOf('.');
|
|
36
|
+
const last = key.lastIndexOf('.');
|
|
37
|
+
if (first < 0)
|
|
38
|
+
return key.toLowerCase();
|
|
39
|
+
if (first === last)
|
|
40
|
+
return key.toLowerCase();
|
|
41
|
+
return `${key.slice(0, first).toLowerCase()}.${key.slice(first + 1, last)}.${key.slice(last + 1).toLowerCase()}`;
|
|
42
|
+
}
|
|
43
|
+
function unquoteConfigValue(raw) {
|
|
44
|
+
let out = '';
|
|
45
|
+
let quoted = false;
|
|
46
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
47
|
+
const ch = raw[i];
|
|
48
|
+
if (ch === '"') {
|
|
49
|
+
quoted = !quoted;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!quoted && (ch === '#' || ch === ';'))
|
|
53
|
+
break;
|
|
54
|
+
if (ch === '\\' && i + 1 < raw.length) {
|
|
55
|
+
const next = raw[i + 1];
|
|
56
|
+
out += next === 'n' ? '\n' : next === 't' ? '\t' : next === 'b' ? '\b' : next;
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
out += ch;
|
|
61
|
+
}
|
|
62
|
+
return quoted ? out : out.replace(/\s+$/, '');
|
|
63
|
+
}
|
|
64
|
+
/** Parses git config text into entries (git-config(1) syntax). */
|
|
65
|
+
function parseGitConfigText(text, source) {
|
|
66
|
+
const entries = [];
|
|
67
|
+
const lines = text.replace(/\\\r?\n/g, '').split(/\r?\n/);
|
|
68
|
+
let section = '';
|
|
69
|
+
for (const rawLine of lines) {
|
|
70
|
+
let line = rawLine.trim();
|
|
71
|
+
if (!line || line.startsWith('#') || line.startsWith(';'))
|
|
72
|
+
continue;
|
|
73
|
+
if (line.startsWith('[')) {
|
|
74
|
+
const header = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\]\s*(.*)$/.exec(line);
|
|
75
|
+
if (!header) {
|
|
76
|
+
section = '';
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const name = header[1];
|
|
80
|
+
if (header[2] !== undefined)
|
|
81
|
+
section = `${name.toLowerCase()}.${header[2].replace(/\\(.)/g, '$1')}`;
|
|
82
|
+
else {
|
|
83
|
+
// [section.sub] (deprecated form): subsection is lower-cased by git.
|
|
84
|
+
const dot = name.indexOf('.');
|
|
85
|
+
section = dot < 0 ? name.toLowerCase() : `${name.slice(0, dot).toLowerCase()}.${name.slice(dot + 1).toLowerCase()}`;
|
|
86
|
+
}
|
|
87
|
+
line = header[3].trim();
|
|
88
|
+
if (!line || line.startsWith('#') || line.startsWith(';'))
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!section)
|
|
92
|
+
continue;
|
|
93
|
+
const pair = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=\s*(.*))?$/.exec(line);
|
|
94
|
+
if (!pair)
|
|
95
|
+
continue;
|
|
96
|
+
const value = pair[2] === undefined ? 'true' : unquoteConfigValue(pair[2]);
|
|
97
|
+
entries.push({ key: `${section}.${pair[1].toLowerCase()}`, value, source });
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
function readSmall(fs, path) {
|
|
102
|
+
try {
|
|
103
|
+
const entry = fs.lstat(path);
|
|
104
|
+
if (!entry)
|
|
105
|
+
return { status: 'absent' };
|
|
106
|
+
if (entry.type === 'symlink') {
|
|
107
|
+
const real = (0, seatbelt_path_1.realPath)(fs, path, { followFinal: true });
|
|
108
|
+
if (real.status !== 'resolved' || !real.exists)
|
|
109
|
+
return { status: 'unknown' };
|
|
110
|
+
return readSmall(fs, real.path);
|
|
111
|
+
}
|
|
112
|
+
const text = fs.readFile(path, exports.MAX_GIT_CONFIG_BYTES);
|
|
113
|
+
return text === null ? { status: 'unknown' } : { status: 'ok', text };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { status: 'unknown' };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function joinPath(dir, rel) {
|
|
120
|
+
return rel.startsWith('/') ? rel : `${dir.replace(/\/+$/, '')}/${rel}`;
|
|
121
|
+
}
|
|
122
|
+
function dirname(path) {
|
|
123
|
+
const cut = path.replace(/\/+$/, '').lastIndexOf('/');
|
|
124
|
+
return cut <= 0 ? '/' : path.slice(0, cut);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Env that makes git use a different repository, config or work tree than
|
|
128
|
+
* discovery from the cwd finds. The resolvers do not model these: any of
|
|
129
|
+
* them set (in the hook env or as a prefix assignment of the git command)
|
|
130
|
+
* makes discovery and config 'unknown'/incomplete, never a definite answer.
|
|
131
|
+
*/
|
|
132
|
+
exports.GIT_RELOCATING_ENV = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_CEILING_DIRECTORIES', 'GIT_DISCOVERY_ACROSS_FILESYSTEM', 'GIT_NAMESPACE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_EXEC_PATH', 'GIT_TEMPLATE_DIR'];
|
|
133
|
+
/**
|
|
134
|
+
* Env and config that make git run another program (ssh, pager, editor,
|
|
135
|
+
* diff/merge drivers, credential helpers, hooks, fsmonitor). The mapper (U2)
|
|
136
|
+
* treats any of them present as a shell command it cannot see: stop.
|
|
137
|
+
*/
|
|
138
|
+
exports.GIT_EXEC_ENV = ['GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_PROXY_COMMAND', 'GIT_ASKPASS', 'SSH_ASKPASS', 'GIT_PAGER', 'PAGER', 'GIT_EDITOR', 'GIT_SEQUENCE_EDITOR', 'EDITOR', 'VISUAL', 'GIT_EXTERNAL_DIFF', 'GIT_EXEC_PATH'];
|
|
139
|
+
const GIT_EXEC_CONFIG = /^(?:core\.(?:sshcommand|pager|editor|fsmonitor|hookspath|gitproxy|askpass)|sequence\.editor|diff\.external|diff\..+\.(?:command|textconv)|merge\..+\.driver|filter\..+\.(?:clean|smudge|process)|credential(?:\..+)?\.helper|pager\..+|gpg(?:\..+)?\.program|uploadpack\.packobjectshook|remote\..+\.(?:receivepack|uploadpack|vcs)|url\..+\.insteadof|protocol\..+\.allow|http\..*proxy|alias\..+)$/;
|
|
140
|
+
/**
|
|
141
|
+
* Names of exec-capable env vars and config keys present (alias.* with a
|
|
142
|
+
* leading `!` only). Names only, never values. `env` is what the agent set:
|
|
143
|
+
* the git command's prefix assignments and earlier `export`s in the same
|
|
144
|
+
* command line, not the hook's own environment (the user's EDITOR/PAGER).
|
|
145
|
+
*/
|
|
146
|
+
function gitExecVectors(config, env) {
|
|
147
|
+
const out = new Set();
|
|
148
|
+
for (const name of exports.GIT_EXEC_ENV)
|
|
149
|
+
if (env && env[name] !== undefined)
|
|
150
|
+
out.add(name);
|
|
151
|
+
for (const entry of config.entries) {
|
|
152
|
+
if (!GIT_EXEC_CONFIG.test(entry.key))
|
|
153
|
+
continue;
|
|
154
|
+
if (entry.key.startsWith('alias.') && !entry.value.startsWith('!'))
|
|
155
|
+
continue;
|
|
156
|
+
// Subsections (remote names, URLs that may carry credentials) are never returned.
|
|
157
|
+
const first = entry.key.indexOf('.');
|
|
158
|
+
const last = entry.key.lastIndexOf('.');
|
|
159
|
+
if (entry.key.startsWith('alias.'))
|
|
160
|
+
out.add('alias.*');
|
|
161
|
+
else
|
|
162
|
+
out.add(first === last ? entry.key : `${entry.key.slice(0, first)}.*.${entry.key.slice(last + 1)}`);
|
|
163
|
+
}
|
|
164
|
+
return [...out];
|
|
165
|
+
}
|
|
166
|
+
/** The relocating names present in env (any value, even empty, counts). */
|
|
167
|
+
function gitRelocatingEnv(env) {
|
|
168
|
+
if (!env)
|
|
169
|
+
return [];
|
|
170
|
+
return exports.GIT_RELOCATING_ENV.filter((name) => env[name] !== undefined);
|
|
171
|
+
}
|
|
172
|
+
/** git's is_git_directory: HEAD plus objects/ and refs/ (a bare repo, or cwd inside a .git dir). */
|
|
173
|
+
function isGitDirectory(fs, dir) {
|
|
174
|
+
try {
|
|
175
|
+
const base = dir === '/' ? '' : dir;
|
|
176
|
+
const head = fs.lstat(`${base}/HEAD`);
|
|
177
|
+
if (!head || (head.type !== 'file' && head.type !== 'symlink'))
|
|
178
|
+
return false;
|
|
179
|
+
const objects = fs.lstat(`${base}/objects`);
|
|
180
|
+
const refs = fs.lstat(`${base}/refs`);
|
|
181
|
+
return Boolean(objects && refs && objects.type !== 'file' && refs.type !== 'file');
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return 'unknown';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Finds the git dirs for a working directory the way git's discovery does: at
|
|
189
|
+
* each level a `.git` directory, a `.git` file (`gitdir:`) plus `commondir`,
|
|
190
|
+
* or the level itself as a git directory (bare repo). With relocating env
|
|
191
|
+
* (`env`: hook env merged with the command's prefix assignments) the answer
|
|
192
|
+
* is 'unknown'.
|
|
193
|
+
*/
|
|
194
|
+
function discoverGitDirs(fs, cwd, env) {
|
|
195
|
+
if (gitRelocatingEnv(env).length > 0)
|
|
196
|
+
return 'unknown';
|
|
197
|
+
const real = (0, seatbelt_path_1.realPath)(fs, cwd, { followFinal: true });
|
|
198
|
+
if (real.status !== 'resolved')
|
|
199
|
+
return 'unknown';
|
|
200
|
+
let dir = real.path;
|
|
201
|
+
for (;;) {
|
|
202
|
+
const dotGit = dir === '/' ? '/.git' : `${dir}/.git`;
|
|
203
|
+
let entry;
|
|
204
|
+
try {
|
|
205
|
+
entry = fs.lstat(dotGit);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return 'unknown';
|
|
209
|
+
}
|
|
210
|
+
if (entry?.type === 'dir')
|
|
211
|
+
return { gitDir: dotGit, commonDir: commonDirOf(fs, dotGit) };
|
|
212
|
+
if (entry?.type === 'file') {
|
|
213
|
+
const pointer = readSmall(fs, dotGit);
|
|
214
|
+
if (pointer.status !== 'ok')
|
|
215
|
+
return 'unknown';
|
|
216
|
+
const match = /^gitdir:\s*(.+?)\s*$/m.exec(pointer.text);
|
|
217
|
+
if (!match)
|
|
218
|
+
return 'unknown';
|
|
219
|
+
const gitDir = joinPath(dir, match[1]);
|
|
220
|
+
return { gitDir, commonDir: commonDirOf(fs, gitDir) };
|
|
221
|
+
}
|
|
222
|
+
if (entry)
|
|
223
|
+
return 'unknown';
|
|
224
|
+
const bare = isGitDirectory(fs, dir);
|
|
225
|
+
if (bare === 'unknown')
|
|
226
|
+
return 'unknown';
|
|
227
|
+
if (bare)
|
|
228
|
+
return { gitDir: dir, commonDir: commonDirOf(fs, dir) };
|
|
229
|
+
if (dir === '/')
|
|
230
|
+
return null;
|
|
231
|
+
dir = dirname(dir);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function commonDirOf(fs, gitDir) {
|
|
235
|
+
const pointer = readSmall(fs, `${gitDir}/commondir`);
|
|
236
|
+
return pointer.status === 'ok' && pointer.text.trim() ? joinPath(gitDir, pointer.text.trim()) : gitDir;
|
|
237
|
+
}
|
|
238
|
+
const DEFAULT_SYSTEM_FILES = ['/etc/gitconfig', '/usr/local/etc/gitconfig', '/opt/homebrew/etc/gitconfig', '/Library/Developer/CommandLineTools/usr/share/git-core/gitconfig'];
|
|
239
|
+
/** Parses GIT_CONFIG_PARAMETERS ('key'='value' pairs, as git writes them). Null when malformed. */
|
|
240
|
+
function parseConfigParameters(raw) {
|
|
241
|
+
const out = [];
|
|
242
|
+
const re = /\s*'((?:[^']|'\\'')*)'(?:=(?:'((?:[^']|'\\'')*)'))?/y;
|
|
243
|
+
let index = 0;
|
|
244
|
+
while (index < raw.length) {
|
|
245
|
+
if (/^\s*$/.test(raw.slice(index)))
|
|
246
|
+
break;
|
|
247
|
+
re.lastIndex = index;
|
|
248
|
+
const match = re.exec(raw);
|
|
249
|
+
if (!match)
|
|
250
|
+
return null;
|
|
251
|
+
const unq = (s) => s.replace(/'\\''/g, "'");
|
|
252
|
+
const key = unq(match[1]);
|
|
253
|
+
if (match[2] !== undefined)
|
|
254
|
+
out.push([key, unq(match[2])]);
|
|
255
|
+
else {
|
|
256
|
+
const eq = key.indexOf('=');
|
|
257
|
+
out.push(eq < 0 ? [key, 'true'] : [key.slice(0, eq), key.slice(eq + 1)]);
|
|
258
|
+
}
|
|
259
|
+
index = re.lastIndex;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
const INCLUDE_KEY = /^include(?:if\..*)?\.path$/;
|
|
264
|
+
/**
|
|
265
|
+
* Reads config files and follows `include.path` / `includeIf.<cond>.path` the
|
|
266
|
+
* way git does for config text (conditions are not evaluated: including more
|
|
267
|
+
* can only add candidates). One loader shares one file budget, so a planted
|
|
268
|
+
* include chain cannot fan out without bound.
|
|
269
|
+
*/
|
|
270
|
+
function createConfigLoader(fs, home) {
|
|
271
|
+
const entries = [];
|
|
272
|
+
const state = { incomplete: false, files: 0 };
|
|
273
|
+
const expandIncludePath = (path, from) => {
|
|
274
|
+
if (path.startsWith('~/'))
|
|
275
|
+
return home ? `${home}${path.slice(1)}` : null;
|
|
276
|
+
if (path.startsWith('~'))
|
|
277
|
+
return null;
|
|
278
|
+
return path.startsWith('/') ? path : joinPath(dirname(from), path);
|
|
279
|
+
};
|
|
280
|
+
const addEntries = (list, from, depth) => {
|
|
281
|
+
for (const entry of list) {
|
|
282
|
+
entries.push(entry);
|
|
283
|
+
if (!INCLUDE_KEY.test(entry.key))
|
|
284
|
+
continue;
|
|
285
|
+
if (depth >= exports.MAX_INCLUDE_DEPTH) {
|
|
286
|
+
state.incomplete = true;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Command-line config (from === null) has no including file: git accepts only an absolute or `~/` path there.
|
|
290
|
+
const target = from === null
|
|
291
|
+
? (entry.value.startsWith('/') ? entry.value : entry.value.startsWith('~/') && home ? `${home}${entry.value.slice(1)}` : null)
|
|
292
|
+
: expandIncludePath(entry.value, from);
|
|
293
|
+
if (!target) {
|
|
294
|
+
state.incomplete = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
addFile(target, depth + 1);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const addFile = (path, depth) => {
|
|
301
|
+
if (!path.startsWith('/')) {
|
|
302
|
+
state.incomplete = true;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
state.files += 1;
|
|
306
|
+
if (state.files > exports.MAX_CONFIG_FILES) {
|
|
307
|
+
state.incomplete = true;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const read = readSmall(fs, path);
|
|
311
|
+
if (read.status === 'absent')
|
|
312
|
+
return;
|
|
313
|
+
if (read.status === 'unknown') {
|
|
314
|
+
state.incomplete = true;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
addEntries(parseGitConfigText(read.text, path), path, depth);
|
|
318
|
+
};
|
|
319
|
+
return { entries, state, addEntries, addFile };
|
|
320
|
+
}
|
|
321
|
+
/** Pairs git reads from the environment: GIT_CONFIG_COUNT/KEY_n/VALUE_n and GIT_CONFIG_PARAMETERS. */
|
|
322
|
+
function configPairsFromEnv(env) {
|
|
323
|
+
const pairs = [];
|
|
324
|
+
let incomplete = false;
|
|
325
|
+
const count = env.GIT_CONFIG_COUNT;
|
|
326
|
+
if (count !== undefined) {
|
|
327
|
+
const n = /^\d{1,4}$/.test(count) ? Number(count) : NaN;
|
|
328
|
+
if (!Number.isFinite(n))
|
|
329
|
+
incomplete = true;
|
|
330
|
+
for (let i = 0; i < (Number.isFinite(n) ? n : 0); i += 1) {
|
|
331
|
+
const key = env[`GIT_CONFIG_KEY_${i}`];
|
|
332
|
+
const value = env[`GIT_CONFIG_VALUE_${i}`];
|
|
333
|
+
if (key === undefined || value === undefined)
|
|
334
|
+
incomplete = true;
|
|
335
|
+
else
|
|
336
|
+
pairs.push([key, value]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (env.GIT_CONFIG_PARAMETERS) {
|
|
340
|
+
const parsed = parseConfigParameters(env.GIT_CONFIG_PARAMETERS);
|
|
341
|
+
if (parsed)
|
|
342
|
+
pairs.push(...parsed);
|
|
343
|
+
else
|
|
344
|
+
incomplete = true;
|
|
345
|
+
}
|
|
346
|
+
return { pairs, incomplete };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Loads config from every scope git reads, plus every include and includeIf
|
|
350
|
+
* (conditions are not evaluated: including more can only add candidates).
|
|
351
|
+
*/
|
|
352
|
+
function loadGitConfig(fs, sources) {
|
|
353
|
+
const env = sources.env || {};
|
|
354
|
+
const home = sources.home;
|
|
355
|
+
const loader = createConfigLoader(fs, home);
|
|
356
|
+
const { entries, state, addEntries, addFile } = loader;
|
|
357
|
+
// Env-named config files are added to the defaults, never substituted for
|
|
358
|
+
// them (a superset of candidates). A relative one depends on the command's
|
|
359
|
+
// cwd, which is not modelled.
|
|
360
|
+
const addEnvFile = (value) => {
|
|
361
|
+
if (value === undefined || value === '')
|
|
362
|
+
return;
|
|
363
|
+
if (!value.startsWith('/'))
|
|
364
|
+
state.incomplete = true;
|
|
365
|
+
else
|
|
366
|
+
addFile(value, 0);
|
|
367
|
+
};
|
|
368
|
+
if (gitRelocatingEnv(env).length > 0)
|
|
369
|
+
state.incomplete = true;
|
|
370
|
+
// GIT_CONFIG_NOSYSTEM is ignored: reading the system files anyway can only add candidates.
|
|
371
|
+
for (const file of sources.systemFiles || DEFAULT_SYSTEM_FILES)
|
|
372
|
+
addFile(file, 0);
|
|
373
|
+
addEnvFile(env.GIT_CONFIG_SYSTEM);
|
|
374
|
+
if (home) {
|
|
375
|
+
addFile(`${home}/.config/git/config`, 0);
|
|
376
|
+
addFile(`${home}/.gitconfig`, 0);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
state.incomplete = true;
|
|
380
|
+
}
|
|
381
|
+
// git reads $HOME and $XDG_CONFIG_HOME, not the trusted home: add theirs, and
|
|
382
|
+
// a different HOME also changes `~/` in include paths, so it is incomplete.
|
|
383
|
+
if (env.HOME !== undefined && env.HOME !== home) {
|
|
384
|
+
state.incomplete = true;
|
|
385
|
+
if (env.HOME.startsWith('/')) {
|
|
386
|
+
addFile(`${env.HOME.replace(/\/+$/, '')}/.gitconfig`, 0);
|
|
387
|
+
if (env.XDG_CONFIG_HOME === undefined)
|
|
388
|
+
addFile(`${env.HOME.replace(/\/+$/, '')}/.config/git/config`, 0);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (env.XDG_CONFIG_HOME !== undefined && env.XDG_CONFIG_HOME !== '') {
|
|
392
|
+
if (env.XDG_CONFIG_HOME.startsWith('/'))
|
|
393
|
+
addFile(`${env.XDG_CONFIG_HOME.replace(/\/+$/, '')}/git/config`, 0);
|
|
394
|
+
else
|
|
395
|
+
state.incomplete = true;
|
|
396
|
+
}
|
|
397
|
+
addEnvFile(env.GIT_CONFIG_GLOBAL);
|
|
398
|
+
addEnvFile(env.GIT_CONFIG);
|
|
399
|
+
if (sources.dirs) {
|
|
400
|
+
addFile(`${sources.dirs.commonDir}/config`, 0);
|
|
401
|
+
addFile(`${sources.dirs.gitDir}/config.worktree`, 0);
|
|
402
|
+
}
|
|
403
|
+
const fromEnv = configPairsFromEnv(env);
|
|
404
|
+
if (fromEnv.incomplete)
|
|
405
|
+
state.incomplete = true;
|
|
406
|
+
const command = [...fromEnv.pairs, ...(sources.commandConfig || [])];
|
|
407
|
+
addEntries(command.map(([key, value]) => ({ key: canonicalGitKey(key), value, source: 'command' })), null, 0);
|
|
408
|
+
return { entries, incomplete: state.incomplete };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The config an invocation's own `-c` / `--config-env` pairs (and what they
|
|
412
|
+
* include) add to the loaded config, for resolvers that take a GitConfig
|
|
413
|
+
* (implicitPushTargets): `git -c remote.origin.push=+refs/heads/*:refs/heads/main push`
|
|
414
|
+
* must be judged with that pair, not without it.
|
|
415
|
+
*/
|
|
416
|
+
function withInvocationConfig(config, invocation) {
|
|
417
|
+
const entries = [...config.entries];
|
|
418
|
+
for (const [key, value] of invocation.config)
|
|
419
|
+
entries.push({ key: canonicalGitKey(key), value, source: 'command' });
|
|
420
|
+
return { entries, incomplete: config.incomplete };
|
|
421
|
+
}
|
|
422
|
+
/** Every value of a key across all scopes, in load order (git uses the last). */
|
|
423
|
+
function gitConfigValues(config, key) {
|
|
424
|
+
const want = canonicalGitKey(key);
|
|
425
|
+
return config.entries.filter((entry) => entry.key === want).map((entry) => entry.value);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Built-ins of every supported git (≥2.20): git runs these before it looks at
|
|
429
|
+
* aliases, so an alias with one of these names never applies.
|
|
430
|
+
*/
|
|
431
|
+
exports.GIT_CORE_BUILTINS = new Set('add am annotate apply archive bisect blame branch bundle cat-file check-attr check-ignore check-mailmap check-ref-format checkout checkout-index cherry cherry-pick clean clone column commit commit-graph commit-tree config count-objects credential credential-cache credential-cache--daemon credential-store describe diff diff-files diff-index diff-tree difftool fast-export fast-import fetch fetch-pack fmt-merge-msg for-each-ref format-patch fsck fsck-objects gc get-tar-commit-id grep hash-object help index-pack init init-db interpret-trailers log ls-files ls-remote ls-tree mailinfo mailsplit merge merge-base merge-file merge-index merge-ours merge-recursive merge-recursive-ours merge-recursive-theirs merge-subtree merge-tree mktag mktree multi-pack-index mv name-rev notes pack-objects pack-refs patch-id pickaxe prune prune-packed pull push range-diff read-tree rebase receive-pack reflog remote remote-ext remote-fd repack replace rerere reset rev-list rev-parse revert rm send-pack shortlog show show-branch show-index show-ref stage stash status stripspace symbolic-ref tag unpack-file unpack-objects update-index update-ref update-server-info upload-archive upload-pack var verify-commit verify-pack verify-tag version worktree write-tree'.split(' '));
|
|
432
|
+
/**
|
|
433
|
+
* Commands that shadow an alias only when present: exec-path programs that may
|
|
434
|
+
* not be installed (git-lfs, git-svn ...) and built-ins newer than 2.20. For
|
|
435
|
+
* these the alias is kept as a candidate next to the command itself.
|
|
436
|
+
*/
|
|
437
|
+
exports.GIT_MAYBE_COMMANDS = new Set('diagnose bugreport for-each-repo maintenance hook fsmonitor--daemon sparse-checkout switch restore replay refs checkout--worker submodule--helper upload-archive--writer daemon difftool--helper filter-branch http-backend http-fetch http-push imap-send instaweb merge-octopus merge-one-file merge-resolve mergetool quiltimport whatchanged pack-redundant remote-ftp remote-ftps remote-http remote-https request-pull send-email sh-i18n--envsubst shell submodule subtree web--browse gui citool svn p4 lfs'.split(' '));
|
|
438
|
+
/** git's split_cmdline: whitespace-separated words with single/double quotes and backslash escapes. Null on an unclosed quote. */
|
|
439
|
+
function splitGitCmdline(value) {
|
|
440
|
+
const words = [];
|
|
441
|
+
let word = '';
|
|
442
|
+
let inWord = false;
|
|
443
|
+
let quote = null;
|
|
444
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
445
|
+
const ch = value[i];
|
|
446
|
+
if (quote) {
|
|
447
|
+
if (ch === quote)
|
|
448
|
+
quote = null;
|
|
449
|
+
else if (ch === '\\' && quote === '"' && i + 1 < value.length)
|
|
450
|
+
word += value[++i];
|
|
451
|
+
else
|
|
452
|
+
word += ch;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (/\s/.test(ch)) {
|
|
456
|
+
if (inWord)
|
|
457
|
+
words.push(word);
|
|
458
|
+
word = '';
|
|
459
|
+
inWord = false;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
inWord = true;
|
|
463
|
+
if (ch === '"' || ch === "'")
|
|
464
|
+
quote = ch;
|
|
465
|
+
else if (ch === '\\' && i + 1 < value.length)
|
|
466
|
+
word += value[++i];
|
|
467
|
+
else
|
|
468
|
+
word += ch;
|
|
469
|
+
}
|
|
470
|
+
if (quote)
|
|
471
|
+
return null;
|
|
472
|
+
if (inWord)
|
|
473
|
+
words.push(word);
|
|
474
|
+
return words;
|
|
475
|
+
}
|
|
476
|
+
/** Global flags that relocate git's repository, config or exec path (the env twins are in GIT_RELOCATING_ENV). */
|
|
477
|
+
const RELOCATING_FLAGS = new Set(['--bare', '--namespace', '--exec-path', '--attr-source', '--super-prefix']);
|
|
478
|
+
const GLOBAL_FLAGS = new Set(['-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs', '--no-optional-locks', '--no-advice', '--no-lazy-fetch']);
|
|
479
|
+
/** Splits `git <global options> <subcommand> <args>` (git.c handle_options: exact option names only). */
|
|
480
|
+
function parseGitInvocation(argv) {
|
|
481
|
+
const out = { config: [], directories: [], unknownGlobals: [], relocated: [], configEnv: [], subcommand: null, args: [] };
|
|
482
|
+
const relocate = (name) => {
|
|
483
|
+
if (!out.relocated.includes(name))
|
|
484
|
+
out.relocated.push(name);
|
|
485
|
+
if (!out.unknownGlobals.includes(name))
|
|
486
|
+
out.unknownGlobals.push(name);
|
|
487
|
+
};
|
|
488
|
+
let i = 0;
|
|
489
|
+
while (i < argv.length) {
|
|
490
|
+
const arg = argv[i];
|
|
491
|
+
if (!arg.startsWith('-') || arg === '-')
|
|
492
|
+
break;
|
|
493
|
+
if (arg === '--') {
|
|
494
|
+
i += 1;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
if (arg === '-c' || arg === '--config-env' || arg === '-C' || arg === '--git-dir' || arg === '--work-tree' || arg === '--namespace' || arg === '--exec-path' || arg === '--super-prefix' || arg === '--attr-source') {
|
|
498
|
+
const value = argv[i + 1];
|
|
499
|
+
if (value === undefined) {
|
|
500
|
+
if (RELOCATING_FLAGS.has(arg))
|
|
501
|
+
relocate(arg);
|
|
502
|
+
else
|
|
503
|
+
out.unknownGlobals.push(arg);
|
|
504
|
+
i += 1;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (arg === '-c') {
|
|
508
|
+
const eq = value.indexOf('=');
|
|
509
|
+
out.config.push(eq < 0 ? [value, 'true'] : [value.slice(0, eq), value.slice(eq + 1)]);
|
|
510
|
+
}
|
|
511
|
+
else if (arg === '--config-env') {
|
|
512
|
+
const eq = value.indexOf('=');
|
|
513
|
+
if (eq < 0)
|
|
514
|
+
out.unknownGlobals.push(arg);
|
|
515
|
+
else
|
|
516
|
+
out.configEnv.push([value.slice(0, eq), value.slice(eq + 1)]);
|
|
517
|
+
}
|
|
518
|
+
else if (arg === '-C' || arg === '--git-dir' || arg === '--work-tree')
|
|
519
|
+
out.directories.push(value);
|
|
520
|
+
else
|
|
521
|
+
relocate(arg);
|
|
522
|
+
i += 2;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const eqForm = /^(--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env|--attr-source)=(.*)$/s.exec(arg);
|
|
526
|
+
if (eqForm) {
|
|
527
|
+
if (eqForm[1] === '--git-dir' || eqForm[1] === '--work-tree')
|
|
528
|
+
out.directories.push(eqForm[2]);
|
|
529
|
+
else if (eqForm[1] === '--config-env') {
|
|
530
|
+
const eq = eqForm[2].indexOf('=');
|
|
531
|
+
if (eq < 0)
|
|
532
|
+
out.unknownGlobals.push(arg);
|
|
533
|
+
else
|
|
534
|
+
out.configEnv.push([eqForm[2].slice(0, eq), eqForm[2].slice(eq + 1)]);
|
|
535
|
+
}
|
|
536
|
+
else
|
|
537
|
+
relocate(eqForm[1]);
|
|
538
|
+
i += 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (RELOCATING_FLAGS.has(arg))
|
|
542
|
+
relocate(arg);
|
|
543
|
+
else if (!GLOBAL_FLAGS.has(arg))
|
|
544
|
+
out.unknownGlobals.push(arg);
|
|
545
|
+
i += 1;
|
|
546
|
+
}
|
|
547
|
+
out.subcommand = i < argv.length ? argv[i] : null;
|
|
548
|
+
out.args = argv.slice(i + 1);
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Expands git aliases the way git would, keeping every candidate: each
|
|
553
|
+
* definition of alias.<name> from any scope or include, and `-c alias.*` from
|
|
554
|
+
* the command. Core built-in names are never expanded (git ignores aliases
|
|
555
|
+
* for them); maybe-installed commands keep both answers. `!` aliases become shell commands with the remaining args appended.
|
|
556
|
+
*
|
|
557
|
+
* `config` must be loaded with the same `env`. A per-invocation `-c` or
|
|
558
|
+
* `--config-env` pair that names `include.path` / `includeIf.*.path` is
|
|
559
|
+
* followed like git does when `fs` is given (`home` for `~/`); without `fs`, or
|
|
560
|
+
* when the file cannot be read in full, the answer is `unknown/config_incomplete`.
|
|
561
|
+
* `--bare`, `--exec-path`, `--namespace`, `--attr-source`, `--super-prefix`,
|
|
562
|
+
* `-C`, `--git-dir` and `--work-tree` make it `unknown/relocated`.
|
|
563
|
+
*/
|
|
564
|
+
function expandGitAliases(argv, context) {
|
|
565
|
+
const results = [];
|
|
566
|
+
let steps = 0;
|
|
567
|
+
let loader = null;
|
|
568
|
+
const addUnknown = (reason) => {
|
|
569
|
+
if (!results.some((r) => r.kind === 'unknown' && r.reason === reason))
|
|
570
|
+
results.push({ kind: 'unknown', reason });
|
|
571
|
+
};
|
|
572
|
+
/** The pairs these `pairs` bring in through include.path / includeIf.*.path (command-line config has no including file). */
|
|
573
|
+
const expandIncludes = (pairs) => {
|
|
574
|
+
const included = [];
|
|
575
|
+
let incomplete = false;
|
|
576
|
+
for (const [key, value] of pairs) {
|
|
577
|
+
const canonical = canonicalGitKey(key);
|
|
578
|
+
if (!INCLUDE_KEY.test(canonical))
|
|
579
|
+
continue;
|
|
580
|
+
if (!context.fs) {
|
|
581
|
+
incomplete = true;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
loader = loader || createConfigLoader(context.fs, context.home ?? null);
|
|
585
|
+
const before = loader.entries.length;
|
|
586
|
+
loader.addEntries([{ key: canonical, value, source: 'command' }], null, 0);
|
|
587
|
+
for (const entry of loader.entries.slice(before + 1))
|
|
588
|
+
included.push([entry.key, entry.value]);
|
|
589
|
+
}
|
|
590
|
+
if (loader && loader.state.incomplete)
|
|
591
|
+
incomplete = true;
|
|
592
|
+
return { pairs: included, incomplete };
|
|
593
|
+
};
|
|
594
|
+
const walk = (invocation, inherited, seen, depth) => {
|
|
595
|
+
steps += 1;
|
|
596
|
+
if (steps > exports.MAX_ALIAS_STEPS) {
|
|
597
|
+
addUnknown('too_deep');
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const own = [...invocation.config];
|
|
601
|
+
for (const [key, envName] of invocation.configEnv) {
|
|
602
|
+
const value = context.env?.[envName];
|
|
603
|
+
if (value === undefined) {
|
|
604
|
+
results.push({ kind: 'unknown', reason: 'dynamic_config' });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
own.push([key, value]);
|
|
608
|
+
}
|
|
609
|
+
// include.path / includeIf.*.path given on this invocation: expand like git, or the config is incomplete.
|
|
610
|
+
const expanded = expandIncludes(own);
|
|
611
|
+
if (expanded.incomplete)
|
|
612
|
+
addUnknown('config_incomplete');
|
|
613
|
+
const included = expanded.pairs;
|
|
614
|
+
const extra = [...inherited, ...own, ...included];
|
|
615
|
+
const merged = { ...invocation, config: extra };
|
|
616
|
+
const name = invocation.subcommand;
|
|
617
|
+
// -C / --git-dir / --work-tree / --bare ..., or relocating env: the config
|
|
618
|
+
// this expansion used is not the one git reads, so no answer here is definite.
|
|
619
|
+
if (invocation.directories.length > 0 || invocation.relocated.length > 0 || gitRelocatingEnv(context.env).length > 0)
|
|
620
|
+
addUnknown('relocated');
|
|
621
|
+
if (name === null || exports.GIT_CORE_BUILTINS.has(name)) {
|
|
622
|
+
results.push({ kind: 'git', invocation: merged });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (exports.GIT_MAYBE_COMMANDS.has(name))
|
|
626
|
+
results.push({ kind: 'git', invocation: merged });
|
|
627
|
+
if (depth >= exports.MAX_ALIAS_DEPTH) {
|
|
628
|
+
addUnknown('too_deep');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const lowered = name.toLowerCase();
|
|
632
|
+
if (seen.includes(lowered)) {
|
|
633
|
+
addUnknown('loop');
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const want = `alias.${lowered}`;
|
|
637
|
+
const definitions = [
|
|
638
|
+
...context.config.entries.filter((entry) => entry.key === want).map((entry) => entry.value),
|
|
639
|
+
...extra.filter(([key]) => canonicalGitKey(key) === want).map(([, value]) => value),
|
|
640
|
+
];
|
|
641
|
+
if (definitions.length === 0) {
|
|
642
|
+
if (!exports.GIT_MAYBE_COMMANDS.has(name))
|
|
643
|
+
results.push({ kind: 'unknown', reason: context.config.incomplete ? 'config_incomplete' : 'not_found' });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Config that could not be read in full may hold a later definition (git uses the last).
|
|
647
|
+
if (context.config.incomplete)
|
|
648
|
+
addUnknown('config_incomplete');
|
|
649
|
+
for (const definition of new Set(definitions)) {
|
|
650
|
+
if (definition.startsWith('!')) {
|
|
651
|
+
results.push({ kind: 'shell', command: definition.slice(1), args: invocation.args });
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const words = splitGitCmdline(definition);
|
|
655
|
+
if (!words || words.length === 0) {
|
|
656
|
+
results.push({ kind: 'unknown', reason: 'unparsed' });
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
// An alias may start with global options (`-c x=y push`): re-parse them.
|
|
660
|
+
walk(parseGitInvocation([...words, ...invocation.args]), extra, [...seen, lowered], depth + 1);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
// GIT_CONFIG_COUNT/KEY/VALUE and GIT_CONFIG_PARAMETERS in `env` are command-line level config too: seen here
|
|
664
|
+
// even when `config` was loaded from another env (their includes are followed the same way).
|
|
665
|
+
const fromEnv = configPairsFromEnv(context.env || {});
|
|
666
|
+
if (fromEnv.incomplete)
|
|
667
|
+
addUnknown('config_incomplete');
|
|
668
|
+
const envExpanded = expandIncludes(fromEnv.pairs);
|
|
669
|
+
if (envExpanded.incomplete)
|
|
670
|
+
addUnknown('config_incomplete');
|
|
671
|
+
walk(parseGitInvocation(argv), [...fromEnv.pairs, ...envExpanded.pairs], [], 0);
|
|
672
|
+
return results;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* `git push` long options that can rewrite or delete remote refs (S1): the
|
|
676
|
+
* mapper treats a candidate in this set as risky. `prune` deletes remote
|
|
677
|
+
* branches that have no local counterpart under `--all`/a pattern refspec.
|
|
678
|
+
*/
|
|
679
|
+
exports.GIT_PUSH_RISKY_OPTIONS = ['force', 'force-with-lease', 'force-if-includes', 'mirror', 'delete', 'prune'];
|
|
680
|
+
/** Long options of `git push` (builtin/push.c), for prefix abbreviation. */
|
|
681
|
+
exports.GIT_PUSH_LONG_OPTIONS = ['repo', 'all', 'branches', 'mirror', 'delete', 'tags', 'dry-run', 'porcelain', 'force', 'force-with-lease', 'force-if-includes', 'recurse-submodules', 'thin', 'receive-pack', 'exec', 'set-upstream', 'progress', 'prune', 'no-verify', 'verify', 'follow-tags', 'signed', 'atomic', 'push-option', 'ipv4', 'ipv6', 'quiet', 'verbose'];
|
|
682
|
+
const GIT_PUSH_NEGATABLE = new Set(['all', 'branches', 'mirror', 'delete', 'tags', 'dry-run', 'porcelain', 'force', 'force-with-lease', 'force-if-includes', 'recurse-submodules', 'thin', 'set-upstream', 'progress', 'prune', 'follow-tags', 'signed', 'atomic', 'push-option', 'quiet', 'verbose']);
|
|
683
|
+
/**
|
|
684
|
+
* Every long option a `--name[=value]` argument can mean under git's unique
|
|
685
|
+
* prefix abbreviation. Exact names win; an ambiguous prefix keeps all matches
|
|
686
|
+
* (git would refuse, but the mapper must stop if any match is risky). Negated
|
|
687
|
+
* forms are returned as `no-<name>`.
|
|
688
|
+
*/
|
|
689
|
+
function gitLongOptionCandidates(arg, options = exports.GIT_PUSH_LONG_OPTIONS, negatable = GIT_PUSH_NEGATABLE) {
|
|
690
|
+
if (!arg.startsWith('--') || arg === '--')
|
|
691
|
+
return [];
|
|
692
|
+
const name = arg.slice(2).split('=')[0].toLowerCase();
|
|
693
|
+
if (options.includes(name))
|
|
694
|
+
return [name];
|
|
695
|
+
const negated = name.startsWith('no-') ? name.slice(3) : null;
|
|
696
|
+
if (negated !== null && negatable.has(negated) && options.includes(negated))
|
|
697
|
+
return [`no-${negated}`];
|
|
698
|
+
const matches = options.filter((option) => option.startsWith(name));
|
|
699
|
+
if (negated !== null)
|
|
700
|
+
for (const option of options)
|
|
701
|
+
if (negatable.has(option) && option.startsWith(negated))
|
|
702
|
+
matches.push(`no-${option}`);
|
|
703
|
+
// `--no-` alone, or `--n`, may prefix every negation.
|
|
704
|
+
if ('no-'.startsWith(name) && name.length > 0)
|
|
705
|
+
for (const option of negatable)
|
|
706
|
+
matches.push(`no-${option}`);
|
|
707
|
+
return [...new Set(matches)];
|
|
708
|
+
}
|
|
709
|
+
function readGitHead(fs, dirs) {
|
|
710
|
+
const head = readSmall(fs, `${dirs.gitDir}/HEAD`);
|
|
711
|
+
if (head.status !== 'ok')
|
|
712
|
+
return { kind: 'unknown' };
|
|
713
|
+
const match = /^ref:\s*refs\/heads\/(\S+)\s*$/.exec(head.text.trim());
|
|
714
|
+
if (match)
|
|
715
|
+
return { kind: 'branch', branch: match[1] };
|
|
716
|
+
if (/^[0-9a-f]{40,64}$/.test(head.text.trim()))
|
|
717
|
+
return { kind: 'detached' };
|
|
718
|
+
return { kind: 'unknown' };
|
|
719
|
+
}
|
|
720
|
+
/** The remote's default branch from refs/remotes/<remote>/HEAD; null when absent; 'unknown' when unreadable. */
|
|
721
|
+
function readRemoteDefaultBranch(fs, dirs, remote) {
|
|
722
|
+
if (!/^[A-Za-z0-9._-]+$/.test(remote) || remote.includes('..'))
|
|
723
|
+
return 'unknown';
|
|
724
|
+
const ref = readSmall(fs, `${dirs.commonDir}/refs/remotes/${remote}/HEAD`);
|
|
725
|
+
if (ref.status === 'absent')
|
|
726
|
+
return null;
|
|
727
|
+
if (ref.status !== 'ok')
|
|
728
|
+
return 'unknown';
|
|
729
|
+
const match = new RegExp(`^ref:\\s*refs/remotes/${remote.replace(/[.]/g, '\\.')}/(\\S+)\\s*$`).exec(ref.text.trim());
|
|
730
|
+
return match ? match[1] : 'unknown';
|
|
731
|
+
}
|
|
732
|
+
exports.BASELINE_PROTECTED_BRANCHES = ['main', 'master', 'production', 'prod', 'release', 'release/*', 'releases/*', 'live', 'stable', 'trunk', 'hotfix/*', 'deploy', 'deploy/*', 'gh-pages'];
|
|
733
|
+
/** PB membership (§1.2). Compared case-insensitively; an unresolved target is protected. */
|
|
734
|
+
function isProtectedBranch(branch, extra = []) {
|
|
735
|
+
if (branch === null)
|
|
736
|
+
return true;
|
|
737
|
+
const name = branch.replace(/^refs\/heads\//, '').toLowerCase();
|
|
738
|
+
if (!name)
|
|
739
|
+
return true;
|
|
740
|
+
return [...exports.BASELINE_PROTECTED_BRANCHES, ...extra].some((pattern) => {
|
|
741
|
+
const p = pattern.toLowerCase();
|
|
742
|
+
return p.endsWith('/*') ? name.startsWith(p.slice(0, -1)) && name.length > p.length - 1 : name === p;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
const AMBIGUOUS_REF_PREFIX = /^(?:refs|heads|tags|remotes)(?:\/|$)/i;
|
|
746
|
+
/** Resolves one push refspec's destination against HEAD and the upstream config. */
|
|
747
|
+
function resolvePushRefspec(refspec, head) {
|
|
748
|
+
let spec = refspec;
|
|
749
|
+
const force = spec.startsWith('+');
|
|
750
|
+
if (force)
|
|
751
|
+
spec = spec.slice(1);
|
|
752
|
+
if (spec === ':')
|
|
753
|
+
return { force, delete: false, destination: null, all: true };
|
|
754
|
+
const colon = spec.indexOf(':');
|
|
755
|
+
const src = colon < 0 ? spec : spec.slice(0, colon);
|
|
756
|
+
const dst = colon < 0 ? null : spec.slice(colon + 1);
|
|
757
|
+
const del = colon >= 0 && src === '';
|
|
758
|
+
if (spec.includes('*'))
|
|
759
|
+
return { force, delete: del, destination: null, all: true };
|
|
760
|
+
// git matches a short name with ref_rev_parse_rules (`%s`, `refs/%s`,
|
|
761
|
+
// `refs/tags/%s`, `refs/heads/%s`, `refs/remotes/%s`, `refs/remotes/%s/HEAD`):
|
|
762
|
+
// `heads/main` is refs/heads/main, `tags/x` or `remotes/x` may name a
|
|
763
|
+
// branch too. Any short name whose first part is one of those namespaces
|
|
764
|
+
// (compared case-insensitively: loose refs on a case-insensitive volume)
|
|
765
|
+
// has more than one reading, so it is unresolved, which is protected.
|
|
766
|
+
const name = (ref) => {
|
|
767
|
+
if (ref === 'HEAD' || ref === '@')
|
|
768
|
+
return head.kind === 'branch' ? head.branch : null;
|
|
769
|
+
if (/[$`~^:?*[\\\s{}]|@\{|\.\.|\/\/|^\/|\/$|\.lock(?:\/|$)/.test(ref))
|
|
770
|
+
return null;
|
|
771
|
+
if (ref.startsWith('refs/heads/')) {
|
|
772
|
+
const branch = ref.slice('refs/heads/'.length);
|
|
773
|
+
return branch && !AMBIGUOUS_REF_PREFIX.test(branch) ? branch : null;
|
|
774
|
+
}
|
|
775
|
+
if (/^refs\//i.test(ref))
|
|
776
|
+
return null;
|
|
777
|
+
if (AMBIGUOUS_REF_PREFIX.test(ref) || /^(?:head|fetch_head|orig_head|merge_head)$/i.test(ref))
|
|
778
|
+
return null;
|
|
779
|
+
return ref || null;
|
|
780
|
+
};
|
|
781
|
+
return { force, delete: del, destination: dst !== null ? name(dst) : name(src), all: false };
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Targets of `git push` with no refspec (and optionally no remote), as the
|
|
785
|
+
* union of every config candidate: remote.<r>.push, push.default and the
|
|
786
|
+
* branch's upstream.
|
|
787
|
+
*/
|
|
788
|
+
function implicitPushTargets(config, head, remote) {
|
|
789
|
+
const out = { branches: [], all: false, unknown: config.incomplete, forceFromConfig: false, mirror: false, remotes: [] };
|
|
790
|
+
const current = head.kind === 'branch' ? head.branch : null;
|
|
791
|
+
if (remote)
|
|
792
|
+
out.remotes.push(remote);
|
|
793
|
+
else {
|
|
794
|
+
const candidates = current
|
|
795
|
+
? [...gitConfigValues(config, `branch.${current}.pushremote`), ...gitConfigValues(config, 'remote.pushdefault'), ...gitConfigValues(config, `branch.${current}.remote`)]
|
|
796
|
+
: [...gitConfigValues(config, 'remote.pushdefault')];
|
|
797
|
+
out.remotes.push(...new Set(candidates.length > 0 ? candidates : ['origin']));
|
|
798
|
+
}
|
|
799
|
+
for (const r of out.remotes) {
|
|
800
|
+
if (gitConfigValues(config, `remote.${r}.mirror`).some((v) => !/^(false|no|off|0)$/i.test(v)))
|
|
801
|
+
out.mirror = true;
|
|
802
|
+
const refspecs = gitConfigValues(config, `remote.${r}.push`);
|
|
803
|
+
for (const refspec of refspecs) {
|
|
804
|
+
const resolved = resolvePushRefspec(refspec, head);
|
|
805
|
+
if (resolved.force)
|
|
806
|
+
out.forceFromConfig = true;
|
|
807
|
+
if (resolved.all)
|
|
808
|
+
out.all = true;
|
|
809
|
+
else if (resolved.destination === null)
|
|
810
|
+
out.unknown = true;
|
|
811
|
+
else
|
|
812
|
+
out.branches.push(resolved.destination);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const defaults = gitConfigValues(config, 'push.default');
|
|
816
|
+
for (const mode of defaults.length > 0 ? defaults : ['simple']) {
|
|
817
|
+
const m = mode.toLowerCase();
|
|
818
|
+
if (m === 'nothing')
|
|
819
|
+
continue;
|
|
820
|
+
if (m === 'matching') {
|
|
821
|
+
out.all = true;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (!current) {
|
|
825
|
+
out.unknown = true;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
out.branches.push(current);
|
|
829
|
+
if (m === 'upstream' || m === 'tracking' || m === 'simple') {
|
|
830
|
+
for (const merge of gitConfigValues(config, `branch.${current}.merge`)) {
|
|
831
|
+
const upstream = merge.startsWith('refs/heads/') ? merge.slice('refs/heads/'.length) || null : null;
|
|
832
|
+
if (upstream)
|
|
833
|
+
out.branches.push(upstream);
|
|
834
|
+
else
|
|
835
|
+
out.unknown = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else if (m !== 'current')
|
|
839
|
+
out.unknown = true;
|
|
840
|
+
}
|
|
841
|
+
out.branches = [...new Set(out.branches)];
|
|
842
|
+
return out;
|
|
843
|
+
}
|
|
844
|
+
//# sourceMappingURL=seatbelt-git.js.map
|