@reviewflow/sdk 0.0.0-stage → 6.21.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.
Files changed (50) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +86 -2
  3. package/build/files-node.mjs +70 -0
  4. package/build/git-node.mjs +1792 -0
  5. package/build/index-browser.es.js +3338 -0
  6. package/build/index-node.mjs +3338 -0
  7. package/package.json +136 -4
  8. package/src/api/pullRequest.ts +617 -0
  9. package/src/config/insertCheckoutEntry.ts +54 -0
  10. package/src/diff/diffPaths.ts +78 -0
  11. package/src/diff/impacts.ts +47 -0
  12. package/src/diff/readingOrder.ts +428 -0
  13. package/src/diff/themes.ts +105 -0
  14. package/src/diff/types.ts +141 -0
  15. package/src/files/editableFile.ts +163 -0
  16. package/src/files.ts +13 -0
  17. package/src/git/groupDiffPaths.ts +134 -0
  18. package/src/git/manifests.ts +96 -0
  19. package/src/git/moduleBindings.ts +571 -0
  20. package/src/git/pullRequestDiff.ts +159 -0
  21. package/src/git/readBranchDiff.ts +359 -0
  22. package/src/git/readImpacts.ts +797 -0
  23. package/src/git/readingOrder.ts +190 -0
  24. package/src/git/run.ts +213 -0
  25. package/src/git.ts +38 -0
  26. package/src/github/applySuggestions.ts +399 -0
  27. package/src/github/editPullRequest.ts +790 -0
  28. package/src/github/fetchCompareDiff.ts +21 -0
  29. package/src/github/imageProxies.ts +43 -0
  30. package/src/github/lineDiff.ts +145 -0
  31. package/src/github/prQuery.ts +298 -0
  32. package/src/github/pullRequestDetail.ts +1347 -0
  33. package/src/github/pullRequestHead.ts +65 -0
  34. package/src/github/pullRequestRevisions.ts +124 -0
  35. package/src/github/transport.ts +171 -0
  36. package/src/index.ts +35 -0
  37. package/src/reviewflow/board.ts +333 -0
  38. package/src/reviewflow/changedParts.ts +80 -0
  39. package/src/reviewflow/checksFromReviewflow.ts +75 -0
  40. package/src/reviewflow/client.ts +589 -0
  41. package/src/reviewflow/follow.ts +345 -0
  42. package/src/reviewflow/status.ts +17 -0
  43. package/src/reviewflow/stream.ts +199 -0
  44. package/src/types/entities.ts +8 -0
  45. package/src/types/ids.ts +1 -0
  46. package/src/types/reviewflow.ts +32 -0
  47. package/src/utils/pullRequestEdit.ts +108 -0
  48. package/src/utils/pullRequestPart.ts +12 -0
  49. package/src/utils/pullRequestReaction.ts +19 -0
  50. 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;