@getmarrow/mcp 3.9.102 → 3.9.106

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.
Files changed (152) hide show
  1. package/README.md +71 -24
  2. package/dist/approval-reply.d.ts +2 -0
  3. package/dist/approval-reply.d.ts.map +1 -0
  4. package/dist/approval-reply.js +18 -0
  5. package/dist/approval-reply.js.map +1 -0
  6. package/dist/cli.js +1716 -1644
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codex-plugin-pins.d.ts +19 -0
  9. package/dist/codex-plugin-pins.d.ts.map +1 -0
  10. package/dist/codex-plugin-pins.js +61 -0
  11. package/dist/codex-plugin-pins.js.map +1 -0
  12. package/dist/codex-plugins.d.ts +34 -0
  13. package/dist/codex-plugins.d.ts.map +1 -0
  14. package/dist/codex-plugins.js +240 -0
  15. package/dist/codex-plugins.js.map +1 -0
  16. package/dist/env.d.ts +8 -0
  17. package/dist/env.d.ts.map +1 -1
  18. package/dist/env.js +83 -4
  19. package/dist/env.js.map +1 -1
  20. package/dist/hook-context.d.ts.map +1 -1
  21. package/dist/hook-context.js +25 -0
  22. package/dist/hook-context.js.map +1 -1
  23. package/dist/hook-contract.d.ts +4 -3
  24. package/dist/hook-contract.d.ts.map +1 -1
  25. package/dist/hook-contract.js +43 -5
  26. package/dist/hook-contract.js.map +1 -1
  27. package/dist/hook-pre-action.d.ts +80 -3
  28. package/dist/hook-pre-action.d.ts.map +1 -1
  29. package/dist/hook-pre-action.js +435 -29
  30. package/dist/hook-pre-action.js.map +1 -1
  31. package/dist/hook-tool-policy.d.ts +73 -4
  32. package/dist/hook-tool-policy.d.ts.map +1 -1
  33. package/dist/hook-tool-policy.js +287 -18
  34. package/dist/hook-tool-policy.js.map +1 -1
  35. package/dist/hook.d.ts +1 -1
  36. package/dist/hook.d.ts.map +1 -1
  37. package/dist/hook.js +15 -7
  38. package/dist/hook.js.map +1 -1
  39. package/dist/host-approval-state.d.ts +7 -0
  40. package/dist/host-approval-state.d.ts.map +1 -1
  41. package/dist/host-approval-state.js +15 -0
  42. package/dist/host-approval-state.js.map +1 -1
  43. package/dist/host-approval.d.ts +22 -1
  44. package/dist/host-approval.d.ts.map +1 -1
  45. package/dist/host-approval.js +65 -4
  46. package/dist/host-approval.js.map +1 -1
  47. package/dist/host-session.d.ts +11 -0
  48. package/dist/host-session.d.ts.map +1 -1
  49. package/dist/host-session.js +49 -0
  50. package/dist/host-session.js.map +1 -1
  51. package/dist/index.d.ts +5 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +23 -11
  54. package/dist/index.js.map +1 -1
  55. package/dist/keyless-server.d.ts +53 -0
  56. package/dist/keyless-server.d.ts.map +1 -0
  57. package/dist/keyless-server.js +230 -0
  58. package/dist/keyless-server.js.map +1 -0
  59. package/dist/normalized-action.d.ts +31 -6
  60. package/dist/normalized-action.d.ts.map +1 -1
  61. package/dist/normalized-action.js +550 -105
  62. package/dist/normalized-action.js.map +1 -1
  63. package/dist/own-server-identity.d.ts +142 -0
  64. package/dist/own-server-identity.d.ts.map +1 -0
  65. package/dist/own-server-identity.js +1123 -0
  66. package/dist/own-server-identity.js.map +1 -0
  67. package/dist/own-tool.d.ts +21 -9
  68. package/dist/own-tool.d.ts.map +1 -1
  69. package/dist/own-tool.js +25 -10
  70. package/dist/own-tool.js.map +1 -1
  71. package/dist/redact.d.ts +23 -0
  72. package/dist/redact.d.ts.map +1 -1
  73. package/dist/redact.js +181 -8
  74. package/dist/redact.js.map +1 -1
  75. package/dist/seatbelt/classifier-pin.d.ts +15 -0
  76. package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
  77. package/dist/seatbelt/classifier-pin.js +26 -0
  78. package/dist/seatbelt/classifier-pin.js.map +1 -0
  79. package/dist/seatbelt/env-dump.d.ts +45 -0
  80. package/dist/seatbelt/env-dump.d.ts.map +1 -0
  81. package/dist/seatbelt/env-dump.js +308 -0
  82. package/dist/seatbelt/env-dump.js.map +1 -0
  83. package/dist/seatbelt/fallback-copy.d.ts +16 -0
  84. package/dist/seatbelt/fallback-copy.d.ts.map +1 -0
  85. package/dist/seatbelt/fallback-copy.js +68 -0
  86. package/dist/seatbelt/fallback-copy.js.map +1 -0
  87. package/dist/seatbelt/guards.d.ts +10 -0
  88. package/dist/seatbelt/guards.d.ts.map +1 -0
  89. package/dist/seatbelt/guards.js +509 -0
  90. package/dist/seatbelt/guards.js.map +1 -0
  91. package/dist/seatbelt/hook.d.ts +91 -0
  92. package/dist/seatbelt/hook.d.ts.map +1 -0
  93. package/dist/seatbelt/hook.js +396 -0
  94. package/dist/seatbelt/hook.js.map +1 -0
  95. package/dist/seatbelt/longopts.d.ts +68 -0
  96. package/dist/seatbelt/longopts.d.ts.map +1 -0
  97. package/dist/seatbelt/longopts.js +377 -0
  98. package/dist/seatbelt/longopts.js.map +1 -0
  99. package/dist/seatbelt/mapper.d.ts +83 -0
  100. package/dist/seatbelt/mapper.d.ts.map +1 -0
  101. package/dist/seatbelt/mapper.js +10581 -0
  102. package/dist/seatbelt/mapper.js.map +1 -0
  103. package/dist/seatbelt/record.d.ts +94 -0
  104. package/dist/seatbelt/record.d.ts.map +1 -0
  105. package/dist/seatbelt/record.js +776 -0
  106. package/dist/seatbelt/record.js.map +1 -0
  107. package/dist/seatbelt/tables.d.ts +82 -0
  108. package/dist/seatbelt/tables.d.ts.map +1 -0
  109. package/dist/seatbelt/tables.js +352 -0
  110. package/dist/seatbelt/tables.js.map +1 -0
  111. package/dist/seatbelt-deploy.d.ts +100 -0
  112. package/dist/seatbelt-deploy.d.ts.map +1 -0
  113. package/dist/seatbelt-deploy.js +393 -0
  114. package/dist/seatbelt-deploy.js.map +1 -0
  115. package/dist/seatbelt-fs.d.ts +30 -0
  116. package/dist/seatbelt-fs.d.ts.map +1 -0
  117. package/dist/seatbelt-fs.js +94 -0
  118. package/dist/seatbelt-fs.js.map +1 -0
  119. package/dist/seatbelt-git.d.ts +212 -0
  120. package/dist/seatbelt-git.d.ts.map +1 -0
  121. package/dist/seatbelt-git.js +844 -0
  122. package/dist/seatbelt-git.js.map +1 -0
  123. package/dist/seatbelt-mode.d.ts +77 -0
  124. package/dist/seatbelt-mode.d.ts.map +1 -0
  125. package/dist/seatbelt-mode.js +496 -0
  126. package/dist/seatbelt-mode.js.map +1 -0
  127. package/dist/seatbelt-path.d.ts +123 -0
  128. package/dist/seatbelt-path.d.ts.map +1 -0
  129. package/dist/seatbelt-path.js +482 -0
  130. package/dist/seatbelt-path.js.map +1 -0
  131. package/dist/seatbelt-scripts.d.ts +109 -0
  132. package/dist/seatbelt-scripts.d.ts.map +1 -0
  133. package/dist/seatbelt-scripts.js +1060 -0
  134. package/dist/seatbelt-scripts.js.map +1 -0
  135. package/dist/secret-paths.d.ts +17 -0
  136. package/dist/secret-paths.d.ts.map +1 -0
  137. package/dist/secret-paths.js +32 -0
  138. package/dist/secret-paths.js.map +1 -0
  139. package/dist/session-loop-guard.d.ts.map +1 -1
  140. package/dist/session-loop-guard.js +15 -5
  141. package/dist/session-loop-guard.js.map +1 -1
  142. package/dist/tool-catalog.js +2 -2
  143. package/dist/tool-catalog.js.map +1 -1
  144. package/dist/types.d.ts +2 -1
  145. package/dist/types.d.ts.map +1 -1
  146. package/package.json +3 -2
  147. package/server.json +3 -3
  148. package/vendor/classifier/MANIFEST.json +25 -0
  149. package/vendor/classifier/classifier.js +1356 -0
  150. package/vendor/classifier/policy-baseline.js +187 -0
  151. package/vendor/classifier/redact.js +59 -0
  152. 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