@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,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestEdit,
|
|
3
|
+
PullRequestFilesResponse,
|
|
4
|
+
PullRequestPart,
|
|
5
|
+
} from "../api/pullRequest.ts";
|
|
6
|
+
import { pullRequestParts } from "./pullRequestPart.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The parts of the pull request screen an edit GitHub took makes stale, the
|
|
10
|
+
* ones read again after it rather than the whole screen. An edit GitHub also
|
|
11
|
+
* lists as an event (a label, a reviewer, a rename, a close) stales the
|
|
12
|
+
* timeline with the header. A viewed mark stales nothing: what it changes is
|
|
13
|
+
* known, and applied in place with `withFileViewed` by the daemon's last read
|
|
14
|
+
* and the screen alike — reviewflow's fingerprint does not move on one, so
|
|
15
|
+
* only that keeps the last read from serving the mark it replaced.
|
|
16
|
+
*/
|
|
17
|
+
export const pullRequestPartsStaleAfter = (
|
|
18
|
+
edit: PullRequestEdit,
|
|
19
|
+
pullRequestId: string,
|
|
20
|
+
): readonly PullRequestPart[] => {
|
|
21
|
+
switch (edit.kind) {
|
|
22
|
+
case "applySuggestions":
|
|
23
|
+
case "updateBranch": {
|
|
24
|
+
return pullRequestParts;
|
|
25
|
+
}
|
|
26
|
+
case "body": {
|
|
27
|
+
return ["detail"];
|
|
28
|
+
}
|
|
29
|
+
case "reaction": {
|
|
30
|
+
return edit.subjectId === pullRequestId ? ["detail"] : ["timeline"];
|
|
31
|
+
}
|
|
32
|
+
case "viewed": {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
case "comment":
|
|
36
|
+
case "editComment":
|
|
37
|
+
case "inlineComment":
|
|
38
|
+
case "reply":
|
|
39
|
+
case "resolveThread": {
|
|
40
|
+
return ["timeline"];
|
|
41
|
+
}
|
|
42
|
+
case "review": {
|
|
43
|
+
// the header's verdicts and merge state, and the files' "since my last review"
|
|
44
|
+
return ["detail", "timeline", "files"];
|
|
45
|
+
}
|
|
46
|
+
case "assignees":
|
|
47
|
+
case "draft":
|
|
48
|
+
case "labels":
|
|
49
|
+
case "merge":
|
|
50
|
+
case "reviewers":
|
|
51
|
+
case "state":
|
|
52
|
+
case "title": {
|
|
53
|
+
return ["detail", "timeline"];
|
|
54
|
+
}
|
|
55
|
+
default: {
|
|
56
|
+
return edit satisfies never;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** A path past the first page of files is not held, and is left as it is. */
|
|
62
|
+
export const withFileViewed = (
|
|
63
|
+
files: PullRequestFilesResponse,
|
|
64
|
+
path: string,
|
|
65
|
+
viewed: boolean,
|
|
66
|
+
): PullRequestFilesResponse => ({
|
|
67
|
+
...files,
|
|
68
|
+
files: files.files.map((file) =>
|
|
69
|
+
file.path === path
|
|
70
|
+
? { ...file, viewed: viewed ? "viewed" : "unviewed" }
|
|
71
|
+
: file,
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether an edit changes what a board row holds of the pull request: its
|
|
77
|
+
* state, its review decision, its checks. A comment or a reaction does not
|
|
78
|
+
* cost the board a read.
|
|
79
|
+
*/
|
|
80
|
+
export const pullRequestEditMovesBoard = (edit: PullRequestEdit): boolean => {
|
|
81
|
+
switch (edit.kind) {
|
|
82
|
+
case "applySuggestions":
|
|
83
|
+
case "draft":
|
|
84
|
+
case "merge":
|
|
85
|
+
case "review":
|
|
86
|
+
case "reviewers":
|
|
87
|
+
case "state":
|
|
88
|
+
case "updateBranch": {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
case "assignees":
|
|
92
|
+
case "body":
|
|
93
|
+
case "comment":
|
|
94
|
+
case "editComment":
|
|
95
|
+
case "inlineComment":
|
|
96
|
+
case "labels":
|
|
97
|
+
case "reaction":
|
|
98
|
+
case "reply":
|
|
99
|
+
case "resolveThread":
|
|
100
|
+
case "title":
|
|
101
|
+
case "viewed": {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
return edit satisfies never;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PullRequestPart } from "../api/pullRequest.ts";
|
|
2
|
+
|
|
3
|
+
export const pullRequestParts: readonly PullRequestPart[] = [
|
|
4
|
+
"detail",
|
|
5
|
+
"checks",
|
|
6
|
+
"timeline",
|
|
7
|
+
"commits",
|
|
8
|
+
"files",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const isPullRequestPart = (value: unknown): value is PullRequestPart =>
|
|
12
|
+
pullRequestParts.includes(value as PullRequestPart);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PullRequestReactionContent } from "../api/pullRequest.ts";
|
|
2
|
+
|
|
3
|
+
/** In the order GitHub's picker offers them. */
|
|
4
|
+
export const pullRequestReactionContents: readonly PullRequestReactionContent[] =
|
|
5
|
+
[
|
|
6
|
+
"thumbsUp",
|
|
7
|
+
"thumbsDown",
|
|
8
|
+
"laugh",
|
|
9
|
+
"hooray",
|
|
10
|
+
"confused",
|
|
11
|
+
"heart",
|
|
12
|
+
"rocket",
|
|
13
|
+
"eyes",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const isPullRequestReactionContent = (
|
|
17
|
+
value: unknown,
|
|
18
|
+
): value is PullRequestReactionContent =>
|
|
19
|
+
pullRequestReactionContents.includes(value as PullRequestReactionContent);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PullRequestDiffSide,
|
|
3
|
+
PullRequestThread,
|
|
4
|
+
} from "../api/pullRequest.ts";
|
|
5
|
+
import type { PrState } from "../types/entities.ts";
|
|
6
|
+
|
|
7
|
+
const fenceOpen = /^( {0,3})(`{3,}(?!`)|~{3,}(?!~))(.*)$/;
|
|
8
|
+
|
|
9
|
+
const withoutIndent = (line: string, indent: number): string => {
|
|
10
|
+
let index = 0;
|
|
11
|
+
while (index < indent && line[index] === " ") index += 1;
|
|
12
|
+
return line.slice(index);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The ```` ```suggestion ```` blocks of a comment, each as the lines it puts
|
|
17
|
+
* in place of the commented ones (none: it deletes them). Every fence is
|
|
18
|
+
* walked, so a suggestion quoted inside another code block is not one. Like
|
|
19
|
+
* CommonMark, a fence left open runs to the end of the comment.
|
|
20
|
+
*/
|
|
21
|
+
export const parseSuggestions = (body: string): string[][] => {
|
|
22
|
+
const lines = body.replaceAll("\r\n", "\n").split("\n");
|
|
23
|
+
const suggestions: string[][] = [];
|
|
24
|
+
let index = 0;
|
|
25
|
+
while (index < lines.length) {
|
|
26
|
+
const open = fenceOpen.exec(lines[index] ?? "");
|
|
27
|
+
index += 1;
|
|
28
|
+
if (open === null) continue;
|
|
29
|
+
const [, indent = "", fence = "", info = ""] = open;
|
|
30
|
+
if (fence.startsWith("`") && info.includes("`")) continue;
|
|
31
|
+
const close = new RegExp(
|
|
32
|
+
`^ {0,3}${fence[0] ?? ""}{${String(fence.length)},}[ \\t]*$`,
|
|
33
|
+
);
|
|
34
|
+
const content: string[] = [];
|
|
35
|
+
while (index < lines.length && !close.test(lines[index] ?? "")) {
|
|
36
|
+
content.push(withoutIndent(lines[index] ?? "", indent.length));
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
index += 1;
|
|
40
|
+
if (info.trim().split(/\s/)[0] === "suggestion") suggestions.push(content);
|
|
41
|
+
}
|
|
42
|
+
return suggestions;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type SuggestionThread = Pick<
|
|
46
|
+
PullRequestThread,
|
|
47
|
+
| "diffHunk"
|
|
48
|
+
| "line"
|
|
49
|
+
| "originalLine"
|
|
50
|
+
| "originalStartLine"
|
|
51
|
+
| "side"
|
|
52
|
+
| "startLine"
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
const rangeLength = (
|
|
56
|
+
start: number | null,
|
|
57
|
+
end: number | null,
|
|
58
|
+
): number | null => {
|
|
59
|
+
if (end === null) return null;
|
|
60
|
+
return end - (start ?? end) + 1;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The lines a suggestion of the thread replaces, read from the end of its
|
|
65
|
+
* diff hunk, which GitHub cuts at the commented line. Null when the hunk
|
|
66
|
+
* holds fewer of them than the range spans.
|
|
67
|
+
*/
|
|
68
|
+
export const suggestionOriginalLines = (
|
|
69
|
+
thread: SuggestionThread,
|
|
70
|
+
): string[] | null => {
|
|
71
|
+
const count =
|
|
72
|
+
thread.line === null
|
|
73
|
+
? rangeLength(thread.originalStartLine, thread.originalLine)
|
|
74
|
+
: rangeLength(thread.startLine, thread.line);
|
|
75
|
+
if (count === null || count < 1) return null;
|
|
76
|
+
const dropped = thread.side === "RIGHT" ? "-" : "+";
|
|
77
|
+
const sideLines = thread.diffHunk
|
|
78
|
+
.replaceAll("\r\n", "\n")
|
|
79
|
+
.split("\n")
|
|
80
|
+
.filter(
|
|
81
|
+
(line) =>
|
|
82
|
+
!line.startsWith("@@") &&
|
|
83
|
+
!line.startsWith("\\") &&
|
|
84
|
+
!line.startsWith(dropped),
|
|
85
|
+
);
|
|
86
|
+
if (sideLines.length < count) return null;
|
|
87
|
+
return sideLines.slice(-count).map((line) => line.slice(1));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export interface SuggestionCommitFacts {
|
|
91
|
+
state: PrState;
|
|
92
|
+
viewerCanApplySuggestion: boolean;
|
|
93
|
+
/** `parseSuggestions(body).length` */
|
|
94
|
+
suggestionCount: number;
|
|
95
|
+
outdated: boolean;
|
|
96
|
+
/** the line on the head; null once gone from the diff */
|
|
97
|
+
line: number | null;
|
|
98
|
+
side: PullRequestDiffSide;
|
|
99
|
+
startSide: PullRequestDiffSide | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Why a comment's suggestion cannot be committed, or null. The screen shows
|
|
104
|
+
* it on the disabled button; the daemon refuses with it, from GitHub's
|
|
105
|
+
* current state of the thread.
|
|
106
|
+
*/
|
|
107
|
+
export const suggestionCommitRefusal = ({
|
|
108
|
+
state,
|
|
109
|
+
viewerCanApplySuggestion,
|
|
110
|
+
suggestionCount,
|
|
111
|
+
outdated,
|
|
112
|
+
line,
|
|
113
|
+
side,
|
|
114
|
+
startSide,
|
|
115
|
+
}: SuggestionCommitFacts): string | null => {
|
|
116
|
+
if (state === "closed" || state === "merged") {
|
|
117
|
+
return `The pull request is ${state}`;
|
|
118
|
+
}
|
|
119
|
+
if (!viewerCanApplySuggestion) {
|
|
120
|
+
return "GitHub does not let you commit to this pull request's branch";
|
|
121
|
+
}
|
|
122
|
+
if (suggestionCount === 0) return "The comment holds no suggestion";
|
|
123
|
+
if (suggestionCount > 1) {
|
|
124
|
+
return "The comment holds more than one suggestion: GitHub commits one per comment";
|
|
125
|
+
}
|
|
126
|
+
if (outdated || line === null) {
|
|
127
|
+
return "Outdated: the lines it replaces changed since";
|
|
128
|
+
}
|
|
129
|
+
if (side === "LEFT" || startSide === "LEFT") {
|
|
130
|
+
return "It is on removed lines, which the branch no longer has";
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export interface SuggestionRange {
|
|
136
|
+
path: string;
|
|
137
|
+
startLine: number | null;
|
|
138
|
+
line: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** GitHub takes one suggestion per line in a batch. */
|
|
142
|
+
export const suggestionRangesOverlap = (
|
|
143
|
+
a: SuggestionRange,
|
|
144
|
+
b: SuggestionRange,
|
|
145
|
+
): boolean =>
|
|
146
|
+
a.path === b.path &&
|
|
147
|
+
(a.startLine ?? a.line) <= b.line &&
|
|
148
|
+
(b.startLine ?? b.line) <= a.line;
|