@reviewflow/sdk 0.0.0-stage → 6.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +86 -2
- package/build/files-node.mjs +70 -0
- package/build/git-node.mjs +1792 -0
- package/build/index-browser.es.js +3328 -0
- package/build/index-node.mjs +3328 -0
- package/package.json +135 -3
- package/src/api/pullRequest.ts +617 -0
- package/src/config/insertCheckoutEntry.ts +54 -0
- package/src/diff/diffPaths.ts +78 -0
- package/src/diff/impacts.ts +47 -0
- package/src/diff/readingOrder.ts +428 -0
- package/src/diff/themes.ts +105 -0
- package/src/diff/types.ts +141 -0
- package/src/files/editableFile.ts +163 -0
- package/src/files.ts +13 -0
- package/src/git/groupDiffPaths.ts +134 -0
- package/src/git/manifests.ts +96 -0
- package/src/git/moduleBindings.ts +571 -0
- package/src/git/pullRequestDiff.ts +159 -0
- package/src/git/readBranchDiff.ts +359 -0
- package/src/git/readImpacts.ts +797 -0
- package/src/git/readingOrder.ts +190 -0
- package/src/git/run.ts +213 -0
- package/src/git.ts +38 -0
- package/src/github/applySuggestions.ts +399 -0
- package/src/github/editPullRequest.ts +790 -0
- package/src/github/fetchCompareDiff.ts +21 -0
- package/src/github/imageProxies.ts +43 -0
- package/src/github/lineDiff.ts +145 -0
- package/src/github/prQuery.ts +298 -0
- package/src/github/pullRequestDetail.ts +1347 -0
- package/src/github/pullRequestHead.ts +65 -0
- package/src/github/pullRequestRevisions.ts +124 -0
- package/src/github/transport.ts +171 -0
- package/src/index.ts +35 -0
- package/src/reviewflow/board.ts +333 -0
- package/src/reviewflow/changedParts.ts +80 -0
- package/src/reviewflow/checksFromReviewflow.ts +75 -0
- package/src/reviewflow/client.ts +589 -0
- package/src/reviewflow/follow.ts +345 -0
- package/src/reviewflow/status.ts +17 -0
- package/src/reviewflow/stream.ts +199 -0
- package/src/types/entities.ts +8 -0
- package/src/types/ids.ts +1 -0
- package/src/types/reviewflow.ts +32 -0
- package/src/utils/pullRequestEdit.ts +108 -0
- package/src/utils/pullRequestPart.ts +12 -0
- package/src/utils/pullRequestReaction.ts +19 -0
- package/src/utils/pullRequestSuggestion.ts +148 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { PullRequestRef } from "../api/pullRequest.ts";
|
|
2
|
+
import { diffPaths } from "../diff/diffPaths.ts";
|
|
3
|
+
import type {
|
|
4
|
+
DiffStep,
|
|
5
|
+
DiffWhitespace,
|
|
6
|
+
PullRequestDiffResponse,
|
|
7
|
+
} from "../diff/types.ts";
|
|
8
|
+
import { fetchCompareDiff } from "../github/fetchCompareDiff.ts";
|
|
9
|
+
import type { GitHubTransport } from "../github/transport.ts";
|
|
10
|
+
import {
|
|
11
|
+
groupDiffPaths,
|
|
12
|
+
noPackages,
|
|
13
|
+
packagesAtTree,
|
|
14
|
+
} from "./groupDiffPaths.ts";
|
|
15
|
+
import type { IsPackage } from "./groupDiffPaths.ts";
|
|
16
|
+
import { readBranchDiff } from "./readBranchDiff.ts";
|
|
17
|
+
import { git, gitOrNull } from "./run.ts";
|
|
18
|
+
|
|
19
|
+
export const isSha = (value: string): boolean => /^[\da-f]{40}$/i.test(value);
|
|
20
|
+
|
|
21
|
+
const hasCommit = async (checkout: string, sha: string): Promise<boolean> =>
|
|
22
|
+
(await gitOrNull(checkout, ["cat-file", "-e", `${sha}^{commit}`])) !== null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetches the commits the checkout lacks, by sha: GitHub serves a pull
|
|
26
|
+
* request's head that way even when no branch of the user's names it.
|
|
27
|
+
* Objects only — `--no-write-fetch-head` and a refspec with no destination
|
|
28
|
+
* leave every ref and `FETCH_HEAD` as they were, so whatever works in the
|
|
29
|
+
* checkout sees nothing move. Nothing is asked when both are already there,
|
|
30
|
+
* the common case for the user's own pull request.
|
|
31
|
+
*/
|
|
32
|
+
export const ensureCommits = async (
|
|
33
|
+
checkout: string,
|
|
34
|
+
shas: string[],
|
|
35
|
+
onStep: ((step: DiffStep) => void) | undefined,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<void> => {
|
|
38
|
+
const missing: string[] = [];
|
|
39
|
+
for (const sha of shas) {
|
|
40
|
+
if (!(await hasCommit(checkout, sha))) missing.push(sha);
|
|
41
|
+
}
|
|
42
|
+
if (missing.length === 0) return;
|
|
43
|
+
onStep?.("fetching");
|
|
44
|
+
await git(
|
|
45
|
+
checkout,
|
|
46
|
+
["fetch", "--no-write-fetch-head", "--no-tags", "origin", ...missing],
|
|
47
|
+
{ network: true, ...(signal === undefined ? {} : { signal }) },
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface PullRequestDiffRequest {
|
|
52
|
+
/** the registered checkout, resolved by the caller; null when there is none */
|
|
53
|
+
checkout: string | null;
|
|
54
|
+
transport: GitHubTransport;
|
|
55
|
+
ref: PullRequestRef;
|
|
56
|
+
head: string;
|
|
57
|
+
base: string;
|
|
58
|
+
/** `show` unless set */
|
|
59
|
+
whitespace?: DiffWhitespace;
|
|
60
|
+
/** called with "fetching" before a fetch that runs, and "diffing" before the diff */
|
|
61
|
+
onStep?: (step: DiffStep) => void;
|
|
62
|
+
/** the reader left: the fetch is killed and GitHub is not asked instead */
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const readFromGit = async (
|
|
67
|
+
checkout: string,
|
|
68
|
+
{ head, base, whitespace = "show", onStep, signal }: PullRequestDiffRequest,
|
|
69
|
+
): Promise<PullRequestDiffResponse> => {
|
|
70
|
+
await ensureCommits(checkout, [head, base], onStep, signal);
|
|
71
|
+
onStep?.("diffing");
|
|
72
|
+
const mergeBaseOutput = await git(checkout, ["merge-base", base, head]);
|
|
73
|
+
const mergeBase = mergeBaseOutput.trim();
|
|
74
|
+
const { patch, paths, omitted } = await readBranchDiff({
|
|
75
|
+
cwd: checkout,
|
|
76
|
+
mergeBase,
|
|
77
|
+
scope: "committed",
|
|
78
|
+
head,
|
|
79
|
+
whitespace,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
source: "git",
|
|
83
|
+
fallbackReason: null,
|
|
84
|
+
whitespace,
|
|
85
|
+
patch,
|
|
86
|
+
sides: { old: mergeBase, new: head },
|
|
87
|
+
omitted,
|
|
88
|
+
groups: groupDiffPaths(await packagesAtTree(checkout, head), paths),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The compare endpoint rather than the pull request's own diff: it takes any
|
|
94
|
+
* range. Three dots, from the merge base, as the local path does. Grouped by
|
|
95
|
+
* the head's manifests when a checkout holds the head, else by first directory.
|
|
96
|
+
*/
|
|
97
|
+
const readFromGitHub = async (
|
|
98
|
+
{ checkout, transport, ref, head, base }: PullRequestDiffRequest,
|
|
99
|
+
fallbackReason: string,
|
|
100
|
+
): Promise<PullRequestDiffResponse> => {
|
|
101
|
+
const patch = await fetchCompareDiff(transport, {
|
|
102
|
+
ownerRepo: ref.ownerRepo,
|
|
103
|
+
base,
|
|
104
|
+
head,
|
|
105
|
+
});
|
|
106
|
+
let isPackage: IsPackage = noPackages;
|
|
107
|
+
if (checkout !== null) {
|
|
108
|
+
// the head may be absent from the checkout (that is a reason to be here)
|
|
109
|
+
isPackage = await packagesAtTree(checkout, head).catch(() => noPackages);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
source: "github",
|
|
113
|
+
fallbackReason,
|
|
114
|
+
whitespace: "show",
|
|
115
|
+
patch,
|
|
116
|
+
sides: null,
|
|
117
|
+
omitted: [],
|
|
118
|
+
groups: groupDiffPaths(isPackage, diffPaths(patch)),
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Local git first, for its size bounds and no file cap, over any range of two
|
|
124
|
+
* commits; GitHub's compare endpoint only when there is no checkout or git
|
|
125
|
+
* fails — and the reason travels with the answer, so a fallback is never
|
|
126
|
+
* silent: "no checkout" when none was given, git's own message otherwise.
|
|
127
|
+
*/
|
|
128
|
+
export const readPullRequestDiff = async (
|
|
129
|
+
request: PullRequestDiffRequest,
|
|
130
|
+
): Promise<PullRequestDiffResponse> => {
|
|
131
|
+
if (!isSha(request.head) || !isSha(request.base)) {
|
|
132
|
+
throw new Error("head and base must be commit shas");
|
|
133
|
+
}
|
|
134
|
+
if (request.checkout === null) return readFromGitHub(request, "no checkout");
|
|
135
|
+
try {
|
|
136
|
+
return await readFromGit(request.checkout, request);
|
|
137
|
+
} catch (error: unknown) {
|
|
138
|
+
if (request.signal?.aborted) throw error;
|
|
139
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
140
|
+
return readFromGitHub(request, reason);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `cat-file blob` rather than `show`: the bytes as committed, with no textconv
|
|
146
|
+
* or filter, which is what the patch's lines were read from. The commits are
|
|
147
|
+
* already in the checkout, fetched when its diff was read.
|
|
148
|
+
*/
|
|
149
|
+
export const readFileAtSha = async (
|
|
150
|
+
checkout: string,
|
|
151
|
+
sha: string,
|
|
152
|
+
path: string,
|
|
153
|
+
): Promise<string> => {
|
|
154
|
+
if (!isSha(sha)) throw new Error("sha must be a commit sha");
|
|
155
|
+
if (path === "") throw new Error("path is required");
|
|
156
|
+
return git(checkout, ["cat-file", "blob", `${sha}:${path}`], {
|
|
157
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { DiffOmittedFile, DiffWhitespace } from "../diff/types.ts";
|
|
4
|
+
import { git } from "./run.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The patch of a branch since its merge base, in the unified format any diff
|
|
8
|
+
* viewer parses.
|
|
9
|
+
*
|
|
10
|
+
* **The operator's git config is neutralised flag by flag**, since every one of
|
|
11
|
+
* these would break the parse in the viewer: `diff.noprefix` and
|
|
12
|
+
* `diff.mnemonicPrefix` (explicit prefixes), `color.ui=always` (`--no-color`),
|
|
13
|
+
* `diff.external` (`--no-ext-diff`), a textconv driver (`--no-textconv`),
|
|
14
|
+
* `diff.relative` (`--no-relative`) and `core.quotePath` (octal-escaped names).
|
|
15
|
+
*
|
|
16
|
+
* **Bounded twice.** `--numstat` runs first and is cheap: a binary file, a file
|
|
17
|
+
* past `maxLinesPerFile` (a lockfile) and everything past the total budget are
|
|
18
|
+
* listed as omitted and excluded from the patch by pathspec, so one generated
|
|
19
|
+
* file never costs the whole diff. `maxBuffer` is the backstop for what line
|
|
20
|
+
* counts cannot see — a minified file is one line.
|
|
21
|
+
*
|
|
22
|
+
* **Untracked files are diffed against `/dev/null` one by one**, never through
|
|
23
|
+
* `git add -N`: that writes the index, and a read must not change what the user
|
|
24
|
+
* will commit. Whitespace is never hidden there: every line of a new file is an
|
|
25
|
+
* addition.
|
|
26
|
+
*
|
|
27
|
+
* **Hiding whitespace is `-w` on both reads**, so the budget counts the lines
|
|
28
|
+
* shown. `git diff -w` drops a file whose every change is whitespace, so a
|
|
29
|
+
* third read without it names those files, listed as omitted.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export interface DiffLimits {
|
|
33
|
+
maxLinesPerFile: number;
|
|
34
|
+
maxTotalLines: number;
|
|
35
|
+
maxFiles: number;
|
|
36
|
+
/** untracked files read under the `all` scope; the rest are omitted */
|
|
37
|
+
maxUntrackedFiles: number;
|
|
38
|
+
/** an untracked file larger than this is omitted as too large */
|
|
39
|
+
maxUntrackedBytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const defaultDiffLimits: DiffLimits = {
|
|
43
|
+
maxLinesPerFile: 3000,
|
|
44
|
+
maxTotalLines: 30_000,
|
|
45
|
+
maxFiles: 500,
|
|
46
|
+
maxUntrackedFiles: 100,
|
|
47
|
+
maxUntrackedBytes: 512 * 1024,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const maxPatchBytes = 64 * 1024 * 1024;
|
|
51
|
+
|
|
52
|
+
const neutralConfig = ["-c", "core.quotePath=false"];
|
|
53
|
+
|
|
54
|
+
const diffFlags = [
|
|
55
|
+
"--no-color",
|
|
56
|
+
"--no-ext-diff",
|
|
57
|
+
"--no-textconv",
|
|
58
|
+
"--src-prefix=a/",
|
|
59
|
+
"--dst-prefix=b/",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export interface NumstatEntry {
|
|
63
|
+
/** the path after the change, the one a viewer names */
|
|
64
|
+
path: string;
|
|
65
|
+
/** both sides of a rename, which must be excluded together */
|
|
66
|
+
paths: string[];
|
|
67
|
+
/** null for a binary file */
|
|
68
|
+
lines: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `-z` keeps names verbatim. A rename leaves the path field empty and puts
|
|
73
|
+
* both names in the next two records.
|
|
74
|
+
*/
|
|
75
|
+
export const parseNumstat = (output: string): NumstatEntry[] => {
|
|
76
|
+
const records = output.split("\0");
|
|
77
|
+
const entries: NumstatEntry[] = [];
|
|
78
|
+
for (let index = 0; index < records.length; index++) {
|
|
79
|
+
const record = records[index] ?? "";
|
|
80
|
+
if (record === "") continue;
|
|
81
|
+
const [added = "", deleted = "", ...rest] = record.split("\t");
|
|
82
|
+
const lines =
|
|
83
|
+
added === "-" || deleted === "-" ? null : Number(added) + Number(deleted);
|
|
84
|
+
const path = rest.join("\t");
|
|
85
|
+
if (path !== "") {
|
|
86
|
+
entries.push({ path, paths: [path], lines });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const from = records[index + 1] ?? "";
|
|
90
|
+
const to = records[index + 2] ?? "";
|
|
91
|
+
index += 2;
|
|
92
|
+
entries.push({ path: to, paths: [from, to], lines });
|
|
93
|
+
}
|
|
94
|
+
return entries;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
interface Budget {
|
|
98
|
+
files: number;
|
|
99
|
+
lines: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const omissionReason = (
|
|
103
|
+
lines: number | null,
|
|
104
|
+
budget: Budget,
|
|
105
|
+
limits: DiffLimits,
|
|
106
|
+
): DiffOmittedFile["reason"] | null => {
|
|
107
|
+
if (lines === null) return "binary";
|
|
108
|
+
if (lines > limits.maxLinesPerFile) return "tooLarge";
|
|
109
|
+
if (
|
|
110
|
+
budget.files + 1 > limits.maxFiles ||
|
|
111
|
+
budget.lines + lines > limits.maxTotalLines
|
|
112
|
+
) {
|
|
113
|
+
return "budget";
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One pathspec per path makes the command line grow with the diff: naming
|
|
120
|
+
* the shorter side keeps it under the OS argument limit for a codemod touching
|
|
121
|
+
* tens of thousands of files, since what is shown is capped by `maxFiles`.
|
|
122
|
+
*/
|
|
123
|
+
const pathspecs = (included: string[], excluded: string[]): string[] => {
|
|
124
|
+
if (excluded.length === 0) return [];
|
|
125
|
+
if (included.length < excluded.length) {
|
|
126
|
+
return ["--", ...included.map((path) => `:(literal)${path}`)];
|
|
127
|
+
}
|
|
128
|
+
return ["--", ...excluded.map((path) => `:(exclude,literal)${path}`)];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
interface TrackedDiff {
|
|
132
|
+
patch: string;
|
|
133
|
+
/** the files in `patch`, after any rename */
|
|
134
|
+
paths: string[];
|
|
135
|
+
omitted: DiffOmittedFile[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const readNumstat = async (
|
|
139
|
+
cwd: string,
|
|
140
|
+
range: string[],
|
|
141
|
+
whitespaceFlags: string[],
|
|
142
|
+
): Promise<NumstatEntry[]> =>
|
|
143
|
+
parseNumstat(
|
|
144
|
+
await git(cwd, [
|
|
145
|
+
...neutralConfig,
|
|
146
|
+
"diff",
|
|
147
|
+
"--no-relative",
|
|
148
|
+
"-M",
|
|
149
|
+
...whitespaceFlags,
|
|
150
|
+
"--numstat",
|
|
151
|
+
"-z",
|
|
152
|
+
...range,
|
|
153
|
+
]),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const whitespaceOnlyFiles = async (
|
|
157
|
+
cwd: string,
|
|
158
|
+
range: string[],
|
|
159
|
+
shown: NumstatEntry[],
|
|
160
|
+
): Promise<DiffOmittedFile[]> => {
|
|
161
|
+
const shownPaths = new Set(shown.map(({ path }) => path));
|
|
162
|
+
const all = await readNumstat(cwd, range, []);
|
|
163
|
+
return all
|
|
164
|
+
.filter(({ path }) => !shownPaths.has(path))
|
|
165
|
+
.map(({ path }) => ({ path, reason: "whitespace" }));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
interface ReadTrackedDiffParams {
|
|
169
|
+
cwd: string;
|
|
170
|
+
range: string[];
|
|
171
|
+
whitespace: DiffWhitespace;
|
|
172
|
+
limits: DiffLimits;
|
|
173
|
+
budget: Budget;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const readTrackedDiff = async ({
|
|
177
|
+
cwd,
|
|
178
|
+
range,
|
|
179
|
+
whitespace,
|
|
180
|
+
limits,
|
|
181
|
+
budget,
|
|
182
|
+
}: ReadTrackedDiffParams): Promise<TrackedDiff> => {
|
|
183
|
+
const whitespaceFlags = whitespace === "hide" ? ["-w"] : [];
|
|
184
|
+
const entries = await readNumstat(cwd, range, whitespaceFlags);
|
|
185
|
+
const omitted: DiffOmittedFile[] =
|
|
186
|
+
whitespace === "hide" ? await whitespaceOnlyFiles(cwd, range, entries) : [];
|
|
187
|
+
const excluded: string[] = [];
|
|
188
|
+
const included: string[] = [];
|
|
189
|
+
const paths: string[] = [];
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const reason = omissionReason(entry.lines, budget, limits);
|
|
192
|
+
if (reason === null) {
|
|
193
|
+
budget.files += 1;
|
|
194
|
+
budget.lines += entry.lines ?? 0;
|
|
195
|
+
paths.push(entry.path);
|
|
196
|
+
included.push(...entry.paths);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
omitted.push({ path: entry.path, reason });
|
|
200
|
+
excluded.push(...entry.paths);
|
|
201
|
+
}
|
|
202
|
+
if (paths.length === 0) return { patch: "", paths, omitted };
|
|
203
|
+
|
|
204
|
+
const patch = await git(
|
|
205
|
+
cwd,
|
|
206
|
+
[
|
|
207
|
+
...neutralConfig,
|
|
208
|
+
"diff",
|
|
209
|
+
...diffFlags,
|
|
210
|
+
"--no-relative",
|
|
211
|
+
"-M",
|
|
212
|
+
...whitespaceFlags,
|
|
213
|
+
...range,
|
|
214
|
+
...pathspecs(included, excluded),
|
|
215
|
+
],
|
|
216
|
+
{ maxBuffer: maxPatchBytes },
|
|
217
|
+
);
|
|
218
|
+
return { patch, paths, omitted };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const countAddedLines = (patch: string): number => {
|
|
222
|
+
const lines = patch.split("\n");
|
|
223
|
+
const firstHunk = lines.findIndex((line) => line.startsWith("@@"));
|
|
224
|
+
if (firstHunk === -1) return 0;
|
|
225
|
+
return lines.slice(firstHunk).filter((line) => line.startsWith("+")).length;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const isBinaryPatch = (patch: string): boolean => /^Binary files /m.test(patch);
|
|
229
|
+
|
|
230
|
+
interface UntrackedDiff {
|
|
231
|
+
patches: string[];
|
|
232
|
+
/** the files in `patches`, in the same order */
|
|
233
|
+
paths: string[];
|
|
234
|
+
omitted: DiffOmittedFile[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const readUntrackedDiff = async (
|
|
238
|
+
cwd: string,
|
|
239
|
+
limits: DiffLimits,
|
|
240
|
+
budget: Budget,
|
|
241
|
+
): Promise<UntrackedDiff> => {
|
|
242
|
+
const listing = await git(cwd, [
|
|
243
|
+
"ls-files",
|
|
244
|
+
"--others",
|
|
245
|
+
"--exclude-standard",
|
|
246
|
+
"-z",
|
|
247
|
+
]);
|
|
248
|
+
const listed = listing.split("\0").filter((path) => path !== "");
|
|
249
|
+
const patches: string[] = [];
|
|
250
|
+
const paths: string[] = [];
|
|
251
|
+
const omitted: DiffOmittedFile[] = [];
|
|
252
|
+
|
|
253
|
+
for (const [index, path] of listed.entries()) {
|
|
254
|
+
if (index >= limits.maxUntrackedFiles) {
|
|
255
|
+
omitted.push({ path, reason: "budget" });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const entry = await lstat(join(cwd, path)).catch(() => null);
|
|
259
|
+
// gone between the listing and this read, or a nested repository
|
|
260
|
+
if (entry === null || entry.isDirectory()) continue;
|
|
261
|
+
if (entry.size > limits.maxUntrackedBytes) {
|
|
262
|
+
omitted.push({ path, reason: "tooLarge" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const patch = await git(
|
|
266
|
+
cwd,
|
|
267
|
+
[
|
|
268
|
+
...neutralConfig,
|
|
269
|
+
"diff",
|
|
270
|
+
...diffFlags,
|
|
271
|
+
"--no-index",
|
|
272
|
+
"--",
|
|
273
|
+
"/dev/null",
|
|
274
|
+
path,
|
|
275
|
+
],
|
|
276
|
+
{ exitOneIsAnswer: true, maxBuffer: maxPatchBytes },
|
|
277
|
+
);
|
|
278
|
+
const lines = isBinaryPatch(patch) ? null : countAddedLines(patch);
|
|
279
|
+
const reason = omissionReason(lines, budget, limits);
|
|
280
|
+
if (reason !== null) {
|
|
281
|
+
omitted.push({ path, reason });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
budget.files += 1;
|
|
285
|
+
budget.lines += lines ?? 0;
|
|
286
|
+
patches.push(patch);
|
|
287
|
+
paths.push(path);
|
|
288
|
+
}
|
|
289
|
+
return { patches, paths, omitted };
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* `committed`: the merge base to `head`. `all`: the merge base to the working
|
|
294
|
+
* tree, which adds the staged and unstaged changes and the untracked files.
|
|
295
|
+
*/
|
|
296
|
+
export type BranchDiffScope = "all" | "committed";
|
|
297
|
+
|
|
298
|
+
export interface BranchDiff {
|
|
299
|
+
patch: string;
|
|
300
|
+
/** the files in `patch`, in its order */
|
|
301
|
+
paths: string[];
|
|
302
|
+
omitted: DiffOmittedFile[];
|
|
303
|
+
/** the untracked files in `patch`; always 0 under `committed` */
|
|
304
|
+
untrackedCount: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface ReadBranchDiffParams {
|
|
308
|
+
cwd: string;
|
|
309
|
+
mergeBase: string;
|
|
310
|
+
/** `committed` unless set: the pull request path reads no working tree */
|
|
311
|
+
scope?: BranchDiffScope;
|
|
312
|
+
/**
|
|
313
|
+
* The commit the `committed` scope ends at: a pull request's head, which no
|
|
314
|
+
* worktree need be on, or `HEAD` unless set. Ignored under `all`, which is
|
|
315
|
+
* the working tree.
|
|
316
|
+
*/
|
|
317
|
+
head?: string;
|
|
318
|
+
/** `show` unless set */
|
|
319
|
+
whitespace?: DiffWhitespace;
|
|
320
|
+
limits?: DiffLimits;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const readBranchDiff = async ({
|
|
324
|
+
cwd,
|
|
325
|
+
mergeBase,
|
|
326
|
+
scope = "committed",
|
|
327
|
+
head = "HEAD",
|
|
328
|
+
whitespace = "show",
|
|
329
|
+
limits = defaultDiffLimits,
|
|
330
|
+
}: ReadBranchDiffParams): Promise<BranchDiff> => {
|
|
331
|
+
if (mergeBase.startsWith("-") || head.startsWith("-")) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`not a revision: ${mergeBase.startsWith("-") ? mergeBase : head}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const budget: Budget = { files: 0, lines: 0 };
|
|
337
|
+
const tracked = await readTrackedDiff({
|
|
338
|
+
cwd,
|
|
339
|
+
range: scope === "committed" ? [mergeBase, head] : [mergeBase],
|
|
340
|
+
whitespace,
|
|
341
|
+
limits,
|
|
342
|
+
budget,
|
|
343
|
+
});
|
|
344
|
+
if (scope === "committed") {
|
|
345
|
+
return {
|
|
346
|
+
patch: tracked.patch,
|
|
347
|
+
paths: tracked.paths,
|
|
348
|
+
omitted: tracked.omitted,
|
|
349
|
+
untrackedCount: 0,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const untracked = await readUntrackedDiff(cwd, limits, budget);
|
|
353
|
+
return {
|
|
354
|
+
patch: [tracked.patch, ...untracked.patches].join(""),
|
|
355
|
+
paths: [...tracked.paths, ...untracked.paths],
|
|
356
|
+
omitted: [...tracked.omitted, ...untracked.omitted],
|
|
357
|
+
untrackedCount: untracked.patches.length,
|
|
358
|
+
};
|
|
359
|
+
};
|