better-review 0.1.0 → 0.3.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/README.md +22 -14
- package/dist/cli.js +255 -203
- package/dist/web/assets/index-DuH6feCh.js +110 -0
- package/dist/web/assets/{index-CJw1unPf.css → index-rNKORPVq.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-bwna7cfM.js +0 -109
package/README.md
CHANGED
|
@@ -50,8 +50,8 @@ Puis « Repos suivis » : ajouter des repos (`owner/repo`) ou des orgs / comptes
|
|
|
50
50
|
|
|
51
51
|
## Raccourcis (vue PR)
|
|
52
52
|
|
|
53
|
-
`j`/`k` fichier · `n`/`p` hunk · `]`/`[` thread non résolu · `v` fichier vu (Focus :
|
|
54
|
-
suivant) · `h` hunk vu · `r` répondre · `x` résoudre · `m` continu/focus · `u` côte à côte/unifié ·
|
|
53
|
+
`j`/`k` fichier · `n`/`p` hunk · `]`/`[` thread non résolu · `v` fichier vu puis fichier suivant (Focus : fichier non
|
|
54
|
+
vu suivant), comme le bouton « Vu » d'un fichier · `h` hunk vu · `r` répondre · `x` résoudre · `m` continu/focus · `u` côte à côte/unifié ·
|
|
55
55
|
`t` conversations · `d` description · `f` panneau fichiers · `Maj+S` review · `i` inbox · `Ctrl+K` aller à une PR ·
|
|
56
56
|
`?` aide.
|
|
57
57
|
|
|
@@ -61,24 +61,27 @@ pour le fichier entier. Chaque commentaire part seul ou dans la review en attent
|
|
|
61
61
|
|
|
62
62
|
## Naviguer dans les fichiers
|
|
63
63
|
|
|
64
|
-
Le panneau de gauche (
|
|
64
|
+
Le panneau de gauche (360 px, masquable avec `f`) propose trois vues. **La vue choisie fixe l'ordre des fichiers** :
|
|
65
|
+
le diff, `j`/`k`, le mode Focus et « Vu → fichier suivant » suivent l'ordre affiché (les filtres masquent des lignes
|
|
66
|
+
sans changer l'ordre).
|
|
65
67
|
|
|
66
|
-
| Vue | Ce qu'elle montre
|
|
67
|
-
| ----- |
|
|
68
|
-
| Liste | Fichiers par dossier ou
|
|
69
|
-
| Arbre | Dossiers repliables
|
|
70
|
-
|
|
|
71
|
-
| Liens | Voisinage du fichier courant (ce qu'il utilise → fichier → ce qui l'utilise), liste en ordre de lecture, graphe plein écran |
|
|
68
|
+
| Vue | Ce qu'elle montre (et l'ordre qu'elle impose) |
|
|
69
|
+
| ----- | -------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| Liste | Fichiers par chemin, groupés par dossier (A→Z), ou du plus gros au plus petit (Taille) ; barre +/− par fichier |
|
|
71
|
+
| Arbre | Dossiers repliables (sous-dossiers avant fichiers, A→Z ou Taille) avec changements, fichiers vus, threads |
|
|
72
|
+
| Liens | Fichiers en ordre de lecture « base d'abord », nombre d'imports / d'utilisateurs, graphe plein écran |
|
|
72
73
|
|
|
73
74
|
Les liens sont calculés côté serveur sur le contenu complet des fichiers au head de la PR : AST TypeScript pour
|
|
74
75
|
TS/JS (y compris `.js` → `.ts`, `require`, `import()`), règles dédiées pour Python, CSS/SCSS/Less, Go, Java/Kotlin,
|
|
75
76
|
C/C++, Rust, Ruby, Vue/Svelte. Les mentions (nom de fichier en CamelCase, ou nom avec extension, ou chemin) couvrent
|
|
76
77
|
la doc et les autres langages. Les alias `tsconfig` `paths` autres que `@/`, `~/`, `#` ne sont pas résolus.
|
|
77
78
|
|
|
78
|
-
**Ordre «
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
**Ordre « base d'abord »** (vue Liens) : ordre topologique, chaque fichier après ceux qu'il importe ou mentionne, les
|
|
80
|
+
plus utilisés en premier, les cycles départagés de la même façon et les fichiers sans lien à la fin. On relit la PR de
|
|
81
|
+
ses fondations vers ce qui les assemble.
|
|
82
|
+
|
|
83
|
+
**Voisinage** (onglet du panneau de droite, 520 px, quelle que soit la vue de gauche) : graphe du fichier courant, ce
|
|
84
|
+
qu'il utilise → fichier → ce qui l'utilise, avec tous ses voisins ; clic sur un voisin pour l'ouvrir.
|
|
82
85
|
|
|
83
86
|
**Monorepos** : chaque dossier contenant un manifeste (`package.json`, `pyproject.toml`, `setup.py`, `go.mod`,
|
|
84
87
|
`Cargo.toml`, `pom.xml`, `build.gradle`, `composer.json`) est un package ; à partir de deux, l'ordre devient
|
|
@@ -91,7 +94,7 @@ monorepo et les dépendances entre packages (option « Autres packages du monore
|
|
|
91
94
|
packages qui contiennent des fichiers modifiés (et les liens entre eux), le reste en gris. Le graphe de voisinage
|
|
92
95
|
mélange fichiers et packages : à gauche les packages que le fichier importe par leur nom, à droite les packages qui
|
|
93
96
|
dépendent de son package (quand aucun de leurs fichiers n'y figure déjà), mêmes couleurs ; un voisin d'un autre package
|
|
94
|
-
porte le nom de son package, et un clic sur un package modifié ouvre son premier fichier dans l'ordre
|
|
97
|
+
porte le nom de son package, et un clic sur un package modifié ouvre son premier fichier dans l'ordre courant.
|
|
95
98
|
|
|
96
99
|
La description de la PR s'ouvre dans l'onglet « Description » du panneau de droite.
|
|
97
100
|
|
|
@@ -105,6 +108,11 @@ packages/shared schémas zod, parseur de diff unifié (empreinte de hunk partag
|
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
- Les diffs sont demandés entre deux SHA (immuables) et mis en cache définitivement.
|
|
111
|
+
- La coloration syntaxique porte sur le fichier entier (blobs désignés par les lignes `index` du diff, mis en cache
|
|
112
|
+
définitivement) : un commentaire ou une chaîne ouvert hors d'un hunk est coloré correctement. Fichier trop gros ou
|
|
113
|
+
indisponible : seules les lignes du diff sont colorées.
|
|
114
|
+
- Un commentaire, une réponse ou un nouveau thread s'affiche dès la réponse de GitHub ; la PR est rechargée ensuite en
|
|
115
|
+
arrière-plan.
|
|
108
116
|
- L'inbox fait une recherche « ids seulement » par section puis charge chaque PR une seule fois
|
|
109
117
|
(≈ 1 point de quota GraphQL par PR).
|
|
110
118
|
- Une PR ouverte est surveillée toutes les 30 s, l'inbox toutes les 2 min ; un changement côté GitHub
|
package/dist/cli.js
CHANGED
|
@@ -5852,6 +5852,8 @@ function parseUnifiedDiff(text) {
|
|
|
5852
5852
|
status: "modified",
|
|
5853
5853
|
binary: false,
|
|
5854
5854
|
truncated: false,
|
|
5855
|
+
oldBlob: null,
|
|
5856
|
+
newBlob: null,
|
|
5855
5857
|
additions: 0,
|
|
5856
5858
|
deletions: 0,
|
|
5857
5859
|
hunks: []
|
|
@@ -5887,7 +5889,12 @@ function parseUnifiedDiff(text) {
|
|
|
5887
5889
|
f.path = unquote(line.slice("rename to ".length));
|
|
5888
5890
|
f.status = "renamed";
|
|
5889
5891
|
} else if (line.startsWith("Binary files ") || line === "GIT binary patch") f.binary = true;
|
|
5890
|
-
else if (line.startsWith("
|
|
5892
|
+
else if (line.startsWith("index ")) {
|
|
5893
|
+
const ids = /^index ([0-9a-f]+)\.\.([0-9a-f]+)/.exec(line);
|
|
5894
|
+
const blob = (id) => id === void 0 || /^0+$/.test(id) ? null : id;
|
|
5895
|
+
f.oldBlob = blob(ids?.[1]);
|
|
5896
|
+
f.newBlob = blob(ids?.[2]);
|
|
5897
|
+
} else if (line.startsWith("--- ")) {
|
|
5891
5898
|
const p = line.slice(4);
|
|
5892
5899
|
if (p !== "/dev/null") f.oldPath = stripPrefix(p);
|
|
5893
5900
|
} else if (line.startsWith("+++ ")) {
|
|
@@ -25774,153 +25781,6 @@ var GitHub = class {
|
|
|
25774
25781
|
}
|
|
25775
25782
|
};
|
|
25776
25783
|
|
|
25777
|
-
// ../server/src/actions.ts
|
|
25778
|
-
async function setFileViewed(gh, prId, path, viewed) {
|
|
25779
|
-
const mutation = viewed ? "markFileAsViewed" : "unmarkFileAsViewed";
|
|
25780
|
-
await gh.graphql(
|
|
25781
|
-
`mutation($id: ID!, $path: String!) { ${mutation}(input: { pullRequestId: $id, path: $path }) { clientMutationId } }`,
|
|
25782
|
-
{ id: prId, path }
|
|
25783
|
-
);
|
|
25784
|
-
}
|
|
25785
|
-
async function replyToThread(gh, threadId, body, pendingReviewId) {
|
|
25786
|
-
const data = await gh.graphql(
|
|
25787
|
-
`mutation($thread: ID!, $body: String!, $review: ID) {
|
|
25788
|
-
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $thread, body: $body, pullRequestReviewId: $review }) {
|
|
25789
|
-
comment { id }
|
|
25790
|
-
}
|
|
25791
|
-
}`,
|
|
25792
|
-
{ thread: threadId, body, review: pendingReviewId }
|
|
25793
|
-
);
|
|
25794
|
-
return data.addPullRequestReviewThreadReply.comment.id;
|
|
25795
|
-
}
|
|
25796
|
-
async function setThreadResolved(gh, threadId, resolved) {
|
|
25797
|
-
const mutation = resolved ? "resolveReviewThread" : "unresolveReviewThread";
|
|
25798
|
-
await gh.graphql(`mutation($id: ID!) { ${mutation}(input: { threadId: $id }) { thread { id } } }`, {
|
|
25799
|
-
id: threadId
|
|
25800
|
-
});
|
|
25801
|
-
}
|
|
25802
|
-
function threadInput(t2) {
|
|
25803
|
-
if (t2.subjectType === "FILE") return { path: t2.path, body: t2.body, subjectType: "FILE" };
|
|
25804
|
-
return {
|
|
25805
|
-
path: t2.path,
|
|
25806
|
-
body: t2.body,
|
|
25807
|
-
subjectType: "LINE",
|
|
25808
|
-
line: t2.line,
|
|
25809
|
-
side: t2.side,
|
|
25810
|
-
...t2.startLine !== null && t2.startLine !== t2.line ? { startLine: t2.startLine, startSide: t2.startSide ?? t2.side } : {}
|
|
25811
|
-
};
|
|
25812
|
-
}
|
|
25813
|
-
async function addThread(gh, prId, pendingReviewId, thread, mode) {
|
|
25814
|
-
if (mode === "single" && pendingReviewId) {
|
|
25815
|
-
throw new GitHubError(
|
|
25816
|
-
"Une review est en attente : ajoutez le commentaire \xE0 la review ou soumettez-la d\u2019abord.",
|
|
25817
|
-
409
|
|
25818
|
-
);
|
|
25819
|
-
}
|
|
25820
|
-
let reviewId = pendingReviewId;
|
|
25821
|
-
if (!reviewId) {
|
|
25822
|
-
const created = await gh.graphql(
|
|
25823
|
-
`mutation($id: ID!) { addPullRequestReview(input: { pullRequestId: $id }) { pullRequestReview { id } } }`,
|
|
25824
|
-
{ id: prId }
|
|
25825
|
-
);
|
|
25826
|
-
reviewId = created.addPullRequestReview.pullRequestReview.id;
|
|
25827
|
-
}
|
|
25828
|
-
try {
|
|
25829
|
-
await gh.graphql(
|
|
25830
|
-
`mutation($input: AddPullRequestReviewThreadInput!) { addPullRequestReviewThread(input: $input) { thread { id } } }`,
|
|
25831
|
-
{ input: { pullRequestReviewId: reviewId, ...threadInput(thread) } }
|
|
25832
|
-
);
|
|
25833
|
-
} catch (error62) {
|
|
25834
|
-
if (!pendingReviewId) await discardReview(gh, reviewId).catch(() => void 0);
|
|
25835
|
-
throw error62;
|
|
25836
|
-
}
|
|
25837
|
-
if (mode === "single") await submitReview(gh, prId, reviewId, "COMMENT", "");
|
|
25838
|
-
}
|
|
25839
|
-
async function submitReview(gh, prId, pendingReviewId, event, body) {
|
|
25840
|
-
if (pendingReviewId) {
|
|
25841
|
-
await gh.graphql(
|
|
25842
|
-
`mutation($id: ID!, $event: PullRequestReviewEvent!, $body: String) {
|
|
25843
|
-
submitPullRequestReview(input: { pullRequestReviewId: $id, event: $event, body: $body }) { pullRequestReview { id } }
|
|
25844
|
-
}`,
|
|
25845
|
-
{ id: pendingReviewId, event, body: body || null }
|
|
25846
|
-
);
|
|
25847
|
-
return;
|
|
25848
|
-
}
|
|
25849
|
-
await gh.graphql(
|
|
25850
|
-
`mutation($input: AddPullRequestReviewInput!) { addPullRequestReview(input: $input) { pullRequestReview { id } } }`,
|
|
25851
|
-
{ input: { pullRequestId: prId, event, ...body ? { body } : {} } }
|
|
25852
|
-
);
|
|
25853
|
-
}
|
|
25854
|
-
async function discardReview(gh, reviewId) {
|
|
25855
|
-
await gh.graphql(
|
|
25856
|
-
`mutation($id: ID!) { deletePullRequestReview(input: { pullRequestReviewId: $id }) { pullRequestReview { id } } }`,
|
|
25857
|
-
{ id: reviewId }
|
|
25858
|
-
);
|
|
25859
|
-
}
|
|
25860
|
-
async function addIssueComment(gh, subjectId, body) {
|
|
25861
|
-
await gh.graphql(
|
|
25862
|
-
`mutation($id: ID!, $body: String!) { addComment(input: { subjectId: $id, body: $body }) { clientMutationId } }`,
|
|
25863
|
-
{
|
|
25864
|
-
id: subjectId,
|
|
25865
|
-
body
|
|
25866
|
-
}
|
|
25867
|
-
);
|
|
25868
|
-
}
|
|
25869
|
-
async function deleteReviewComment(gh, commentId) {
|
|
25870
|
-
await gh.graphql(
|
|
25871
|
-
`mutation($id: ID!) { deletePullRequestReviewComment(input: { id: $id }) { clientMutationId } }`,
|
|
25872
|
-
{ id: commentId }
|
|
25873
|
-
);
|
|
25874
|
-
}
|
|
25875
|
-
async function updateReviewComment(gh, commentId, body) {
|
|
25876
|
-
await gh.graphql(
|
|
25877
|
-
`mutation($id: ID!, $body: String!) {
|
|
25878
|
-
updatePullRequestReviewComment(input: { pullRequestReviewCommentId: $id, body: $body }) { clientMutationId }
|
|
25879
|
-
}`,
|
|
25880
|
-
{ id: commentId, body }
|
|
25881
|
-
);
|
|
25882
|
-
}
|
|
25883
|
-
async function applySuggestion(gh, ref, input2) {
|
|
25884
|
-
const branch = await gh.rest(
|
|
25885
|
-
`/repos/${ref.owner}/${ref.repo}/branches/${encodeURIComponent(input2.branch)}`
|
|
25886
|
-
);
|
|
25887
|
-
if (branch.commit.sha !== input2.expectedHeadOid) {
|
|
25888
|
-
throw new GitHubError(
|
|
25889
|
-
"La branche a boug\xE9 depuis le chargement de la PR : rechargez avant d\u2019appliquer.",
|
|
25890
|
-
409
|
|
25891
|
-
);
|
|
25892
|
-
}
|
|
25893
|
-
const encodedPath = input2.path.split("/").map(encodeURIComponent).join("/");
|
|
25894
|
-
const file2 = await gh.rest(
|
|
25895
|
-
`/repos/${ref.owner}/${ref.repo}/contents/${encodedPath}?ref=${input2.expectedHeadOid}`
|
|
25896
|
-
);
|
|
25897
|
-
const text = Buffer.from(file2.content, "base64").toString("utf8");
|
|
25898
|
-
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
25899
|
-
const lines = text.split(eol);
|
|
25900
|
-
if (input2.endLine > lines.length || input2.startLine < 1 || input2.startLine > input2.endLine) {
|
|
25901
|
-
throw new GitHubError("Lignes de la suggestion hors du fichier.", 422);
|
|
25902
|
-
}
|
|
25903
|
-
const replacement = input2.replacement.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
25904
|
-
lines.splice(
|
|
25905
|
-
input2.startLine - 1,
|
|
25906
|
-
input2.endLine - input2.startLine + 1,
|
|
25907
|
-
...input2.replacement === "" ? [] : replacement
|
|
25908
|
-
);
|
|
25909
|
-
const res = await gh.rest(
|
|
25910
|
-
`/repos/${ref.owner}/${ref.repo}/contents/${encodedPath}`,
|
|
25911
|
-
{
|
|
25912
|
-
method: "PUT",
|
|
25913
|
-
body: {
|
|
25914
|
-
message: input2.message,
|
|
25915
|
-
content: Buffer.from(lines.join(eol), "utf8").toString("base64"),
|
|
25916
|
-
sha: file2.sha,
|
|
25917
|
-
branch: input2.branch
|
|
25918
|
-
}
|
|
25919
|
-
}
|
|
25920
|
-
);
|
|
25921
|
-
return res.commit.sha;
|
|
25922
|
-
}
|
|
25923
|
-
|
|
25924
25784
|
// ../server/src/github/queries.ts
|
|
25925
25785
|
var PR_SUMMARY = (
|
|
25926
25786
|
/* GraphQL */
|
|
@@ -26069,6 +25929,34 @@ var INBOX_FINGERPRINT_QUERY = (
|
|
|
26069
25929
|
}
|
|
26070
25930
|
`
|
|
26071
25931
|
);
|
|
25932
|
+
var REVIEW_COMMENT_FIELDS = (
|
|
25933
|
+
/* GraphQL */
|
|
25934
|
+
`
|
|
25935
|
+
fragment ReviewCommentFields on PullRequestReviewComment {
|
|
25936
|
+
id
|
|
25937
|
+
databaseId
|
|
25938
|
+
url
|
|
25939
|
+
body
|
|
25940
|
+
bodyHTML
|
|
25941
|
+
createdAt
|
|
25942
|
+
state
|
|
25943
|
+
diffHunk
|
|
25944
|
+
author {
|
|
25945
|
+
login
|
|
25946
|
+
avatarUrl
|
|
25947
|
+
}
|
|
25948
|
+
originalCommit {
|
|
25949
|
+
oid
|
|
25950
|
+
}
|
|
25951
|
+
pullRequestReview {
|
|
25952
|
+
id
|
|
25953
|
+
state
|
|
25954
|
+
}
|
|
25955
|
+
viewerCanUpdate
|
|
25956
|
+
viewerCanDelete
|
|
25957
|
+
}
|
|
25958
|
+
`
|
|
25959
|
+
);
|
|
26072
25960
|
var THREAD_FIELDS = (
|
|
26073
25961
|
/* GraphQL */
|
|
26074
25962
|
`
|
|
@@ -26092,30 +25980,11 @@ var THREAD_FIELDS = (
|
|
|
26092
25980
|
viewerCanReply
|
|
26093
25981
|
comments(first: 100) {
|
|
26094
25982
|
nodes {
|
|
26095
|
-
|
|
26096
|
-
databaseId
|
|
26097
|
-
url
|
|
26098
|
-
body
|
|
26099
|
-
bodyHTML
|
|
26100
|
-
createdAt
|
|
26101
|
-
state
|
|
26102
|
-
diffHunk
|
|
26103
|
-
author {
|
|
26104
|
-
login
|
|
26105
|
-
avatarUrl
|
|
26106
|
-
}
|
|
26107
|
-
originalCommit {
|
|
26108
|
-
oid
|
|
26109
|
-
}
|
|
26110
|
-
pullRequestReview {
|
|
26111
|
-
id
|
|
26112
|
-
state
|
|
26113
|
-
}
|
|
26114
|
-
viewerCanUpdate
|
|
26115
|
-
viewerCanDelete
|
|
25983
|
+
...ReviewCommentFields
|
|
26116
25984
|
}
|
|
26117
25985
|
}
|
|
26118
25986
|
}
|
|
25987
|
+
${REVIEW_COMMENT_FIELDS}
|
|
26119
25988
|
`
|
|
26120
25989
|
);
|
|
26121
25990
|
var PR_QUERY = (
|
|
@@ -26380,6 +26249,28 @@ var PR_FINGERPRINT_QUERY = (
|
|
|
26380
26249
|
`
|
|
26381
26250
|
);
|
|
26382
26251
|
|
|
26252
|
+
// ../server/src/github/blobs.ts
|
|
26253
|
+
var BATCH = 50;
|
|
26254
|
+
async function fetchTexts(gh, ref, expressions) {
|
|
26255
|
+
const texts = /* @__PURE__ */ new Map();
|
|
26256
|
+
for (let i = 0; i < expressions.length; i += BATCH) {
|
|
26257
|
+
const batch = expressions.slice(i, i + BATCH);
|
|
26258
|
+
const vars = { owner: ref.owner, repo: ref.repo };
|
|
26259
|
+
batch.forEach((e, k) => vars[`e${k}`] = e);
|
|
26260
|
+
const query = `query(${["$owner: String!", "$repo: String!", ...batch.map((_, k) => `$e${k}: String!`)].join(", ")}) {
|
|
26261
|
+
repository(owner: $owner, name: $repo) {
|
|
26262
|
+
${batch.map((_, k) => `f${k}: object(expression: $e${k}) { ... on Blob { text isBinary isTruncated } }`).join("\n")}
|
|
26263
|
+
}
|
|
26264
|
+
}`;
|
|
26265
|
+
const data = await gh.graphql(query, vars);
|
|
26266
|
+
batch.forEach((e, k) => {
|
|
26267
|
+
const blob = data.repository[`f${k}`];
|
|
26268
|
+
texts.set(e, blob && !blob.isBinary && !blob.isTruncated ? blob.text : null);
|
|
26269
|
+
});
|
|
26270
|
+
}
|
|
26271
|
+
return texts;
|
|
26272
|
+
}
|
|
26273
|
+
|
|
26383
26274
|
// ../server/src/inbox.ts
|
|
26384
26275
|
var SCOPE_CHUNK = 20;
|
|
26385
26276
|
function scopeChunks(config2) {
|
|
@@ -26695,8 +26586,8 @@ async function loadPr(gh, store, ref) {
|
|
|
26695
26586
|
seenHunks: store.seenHunks(key)
|
|
26696
26587
|
};
|
|
26697
26588
|
}
|
|
26698
|
-
function
|
|
26699
|
-
|
|
26589
|
+
function toComment(c) {
|
|
26590
|
+
return {
|
|
26700
26591
|
id: c.id,
|
|
26701
26592
|
databaseId: c.databaseId,
|
|
26702
26593
|
url: c.url,
|
|
@@ -26708,7 +26599,10 @@ function toThread(t2, me, lastReadId) {
|
|
|
26708
26599
|
originalCommitOid: c.originalCommit?.oid ?? null,
|
|
26709
26600
|
canUpdate: c.viewerCanUpdate,
|
|
26710
26601
|
canDelete: c.viewerCanDelete
|
|
26711
|
-
}
|
|
26602
|
+
};
|
|
26603
|
+
}
|
|
26604
|
+
function toThread(t2, me, lastReadId) {
|
|
26605
|
+
const comments = t2.comments.nodes.map(toComment);
|
|
26712
26606
|
const readIdx = lastReadId ? comments.findIndex((c) => c.id === lastReadId) : -1;
|
|
26713
26607
|
const unread = comments.slice(readIdx + 1).filter((c) => c.author?.login !== me).length;
|
|
26714
26608
|
return {
|
|
@@ -26748,6 +26642,7 @@ function restFilesToDiff(files) {
|
|
|
26748
26642
|
if (f.status === "added") header.push("new file mode 100644");
|
|
26749
26643
|
if (f.status === "removed") header.push("deleted file mode 100644");
|
|
26750
26644
|
if (f.status === "renamed") header.push(`rename from ${oldPath}`, `rename to ${f.filename}`);
|
|
26645
|
+
if (f.sha && f.status !== "removed") header.push(`index 0000000..${f.sha}`);
|
|
26751
26646
|
if (f.patch !== void 0) {
|
|
26752
26647
|
header.push(
|
|
26753
26648
|
f.status === "added" ? "--- /dev/null" : `--- a/${oldPath}`,
|
|
@@ -26797,6 +26692,186 @@ async function loadDiff(gh, store, ref, base, head, isFullPr) {
|
|
|
26797
26692
|
const files = compare.files ?? [];
|
|
26798
26693
|
return { base, head, files: restFilesToDiff(files), missingFiles: files.length >= 300 ? 1 : 0 };
|
|
26799
26694
|
}
|
|
26695
|
+
async function loadBlobs(gh, store, ref, oids) {
|
|
26696
|
+
const key = (oid) => `blob:${ref.owner}/${ref.repo}:${oid}`.toLowerCase();
|
|
26697
|
+
const out = {};
|
|
26698
|
+
const missing = [];
|
|
26699
|
+
for (const oid of oids) {
|
|
26700
|
+
const cached2 = store.getDiff(key(oid));
|
|
26701
|
+
if (cached2 === void 0) missing.push(oid);
|
|
26702
|
+
else out[oid] = JSON.parse(cached2);
|
|
26703
|
+
}
|
|
26704
|
+
if (missing.length > 0) {
|
|
26705
|
+
const texts = await fetchTexts(gh, ref, missing);
|
|
26706
|
+
for (const oid of missing) {
|
|
26707
|
+
const text = texts.get(oid) ?? null;
|
|
26708
|
+
store.putDiff(key(oid), JSON.stringify(text));
|
|
26709
|
+
out[oid] = text;
|
|
26710
|
+
}
|
|
26711
|
+
}
|
|
26712
|
+
return out;
|
|
26713
|
+
}
|
|
26714
|
+
|
|
26715
|
+
// ../server/src/actions.ts
|
|
26716
|
+
async function setFileViewed(gh, prId, path, viewed) {
|
|
26717
|
+
const mutation = viewed ? "markFileAsViewed" : "unmarkFileAsViewed";
|
|
26718
|
+
await gh.graphql(
|
|
26719
|
+
`mutation($id: ID!, $path: String!) { ${mutation}(input: { pullRequestId: $id, path: $path }) { clientMutationId } }`,
|
|
26720
|
+
{ id: prId, path }
|
|
26721
|
+
);
|
|
26722
|
+
}
|
|
26723
|
+
async function replyToThread(gh, threadId, body, pendingReviewId) {
|
|
26724
|
+
const data = await gh.graphql(
|
|
26725
|
+
`mutation($thread: ID!, $body: String!, $review: ID) {
|
|
26726
|
+
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $thread, body: $body, pullRequestReviewId: $review }) {
|
|
26727
|
+
comment { ...ReviewCommentFields }
|
|
26728
|
+
}
|
|
26729
|
+
}
|
|
26730
|
+
${REVIEW_COMMENT_FIELDS}`,
|
|
26731
|
+
{ thread: threadId, body, review: pendingReviewId }
|
|
26732
|
+
);
|
|
26733
|
+
return toComment(data.addPullRequestReviewThreadReply.comment);
|
|
26734
|
+
}
|
|
26735
|
+
async function setThreadResolved(gh, threadId, resolved) {
|
|
26736
|
+
const mutation = resolved ? "resolveReviewThread" : "unresolveReviewThread";
|
|
26737
|
+
await gh.graphql(`mutation($id: ID!) { ${mutation}(input: { threadId: $id }) { thread { id } } }`, {
|
|
26738
|
+
id: threadId
|
|
26739
|
+
});
|
|
26740
|
+
}
|
|
26741
|
+
function threadInput(t2) {
|
|
26742
|
+
if (t2.subjectType === "FILE") return { path: t2.path, body: t2.body, subjectType: "FILE" };
|
|
26743
|
+
return {
|
|
26744
|
+
path: t2.path,
|
|
26745
|
+
body: t2.body,
|
|
26746
|
+
subjectType: "LINE",
|
|
26747
|
+
line: t2.line,
|
|
26748
|
+
side: t2.side,
|
|
26749
|
+
...t2.startLine !== null && t2.startLine !== t2.line ? { startLine: t2.startLine, startSide: t2.startSide ?? t2.side } : {}
|
|
26750
|
+
};
|
|
26751
|
+
}
|
|
26752
|
+
async function addThread(gh, prId, pendingReviewId, thread, mode) {
|
|
26753
|
+
if (mode === "single" && pendingReviewId) {
|
|
26754
|
+
throw new GitHubError(
|
|
26755
|
+
"Une review est en attente : ajoutez le commentaire \xE0 la review ou soumettez-la d\u2019abord.",
|
|
26756
|
+
409
|
|
26757
|
+
);
|
|
26758
|
+
}
|
|
26759
|
+
let reviewId = pendingReviewId;
|
|
26760
|
+
if (!reviewId) {
|
|
26761
|
+
const created2 = await gh.graphql(
|
|
26762
|
+
`mutation($id: ID!) { addPullRequestReview(input: { pullRequestId: $id }) { pullRequestReview { id } } }`,
|
|
26763
|
+
{ id: prId }
|
|
26764
|
+
);
|
|
26765
|
+
reviewId = created2.addPullRequestReview.pullRequestReview.id;
|
|
26766
|
+
}
|
|
26767
|
+
let node2;
|
|
26768
|
+
try {
|
|
26769
|
+
const data = await gh.graphql(
|
|
26770
|
+
`mutation($input: AddPullRequestReviewThreadInput!) {
|
|
26771
|
+
addPullRequestReviewThread(input: $input) { thread { ...ThreadFields } }
|
|
26772
|
+
}
|
|
26773
|
+
${THREAD_FIELDS}`,
|
|
26774
|
+
{ input: { pullRequestReviewId: reviewId, ...threadInput(thread) } }
|
|
26775
|
+
);
|
|
26776
|
+
node2 = data.addPullRequestReviewThread.thread;
|
|
26777
|
+
} catch (error62) {
|
|
26778
|
+
if (!pendingReviewId) await discardReview(gh, reviewId).catch(() => void 0);
|
|
26779
|
+
throw error62;
|
|
26780
|
+
}
|
|
26781
|
+
const created = toThread(node2, "", node2.comments.nodes.at(-1)?.id);
|
|
26782
|
+
if (mode === "review") return { thread: created, pendingReviewId: reviewId };
|
|
26783
|
+
await submitReview(gh, prId, reviewId, "COMMENT", "");
|
|
26784
|
+
return {
|
|
26785
|
+
thread: { ...created, pending: false, comments: created.comments.map((c) => ({ ...c, pending: false })) },
|
|
26786
|
+
pendingReviewId: null
|
|
26787
|
+
};
|
|
26788
|
+
}
|
|
26789
|
+
async function submitReview(gh, prId, pendingReviewId, event, body) {
|
|
26790
|
+
if (pendingReviewId) {
|
|
26791
|
+
await gh.graphql(
|
|
26792
|
+
`mutation($id: ID!, $event: PullRequestReviewEvent!, $body: String) {
|
|
26793
|
+
submitPullRequestReview(input: { pullRequestReviewId: $id, event: $event, body: $body }) { pullRequestReview { id } }
|
|
26794
|
+
}`,
|
|
26795
|
+
{ id: pendingReviewId, event, body: body || null }
|
|
26796
|
+
);
|
|
26797
|
+
return;
|
|
26798
|
+
}
|
|
26799
|
+
await gh.graphql(
|
|
26800
|
+
`mutation($input: AddPullRequestReviewInput!) { addPullRequestReview(input: $input) { pullRequestReview { id } } }`,
|
|
26801
|
+
{ input: { pullRequestId: prId, event, ...body ? { body } : {} } }
|
|
26802
|
+
);
|
|
26803
|
+
}
|
|
26804
|
+
async function discardReview(gh, reviewId) {
|
|
26805
|
+
await gh.graphql(
|
|
26806
|
+
`mutation($id: ID!) { deletePullRequestReview(input: { pullRequestReviewId: $id }) { pullRequestReview { id } } }`,
|
|
26807
|
+
{ id: reviewId }
|
|
26808
|
+
);
|
|
26809
|
+
}
|
|
26810
|
+
async function addIssueComment(gh, subjectId, body) {
|
|
26811
|
+
const data = await gh.graphql(
|
|
26812
|
+
`mutation($id: ID!, $body: String!) {
|
|
26813
|
+
addComment(input: { subjectId: $id, body: $body }) {
|
|
26814
|
+
commentEdge { node { id url body bodyHTML createdAt author { login avatarUrl } } }
|
|
26815
|
+
}
|
|
26816
|
+
}`,
|
|
26817
|
+
{ id: subjectId, body }
|
|
26818
|
+
);
|
|
26819
|
+
return { kind: "comment", ...data.addComment.commentEdge.node, unread: false };
|
|
26820
|
+
}
|
|
26821
|
+
async function deleteReviewComment(gh, commentId) {
|
|
26822
|
+
await gh.graphql(
|
|
26823
|
+
`mutation($id: ID!) { deletePullRequestReviewComment(input: { id: $id }) { clientMutationId } }`,
|
|
26824
|
+
{ id: commentId }
|
|
26825
|
+
);
|
|
26826
|
+
}
|
|
26827
|
+
async function updateReviewComment(gh, commentId, body) {
|
|
26828
|
+
await gh.graphql(
|
|
26829
|
+
`mutation($id: ID!, $body: String!) {
|
|
26830
|
+
updatePullRequestReviewComment(input: { pullRequestReviewCommentId: $id, body: $body }) { clientMutationId }
|
|
26831
|
+
}`,
|
|
26832
|
+
{ id: commentId, body }
|
|
26833
|
+
);
|
|
26834
|
+
}
|
|
26835
|
+
async function applySuggestion(gh, ref, input2) {
|
|
26836
|
+
const branch = await gh.rest(
|
|
26837
|
+
`/repos/${ref.owner}/${ref.repo}/branches/${encodeURIComponent(input2.branch)}`
|
|
26838
|
+
);
|
|
26839
|
+
if (branch.commit.sha !== input2.expectedHeadOid) {
|
|
26840
|
+
throw new GitHubError(
|
|
26841
|
+
"La branche a boug\xE9 depuis le chargement de la PR : rechargez avant d\u2019appliquer.",
|
|
26842
|
+
409
|
|
26843
|
+
);
|
|
26844
|
+
}
|
|
26845
|
+
const encodedPath = input2.path.split("/").map(encodeURIComponent).join("/");
|
|
26846
|
+
const file2 = await gh.rest(
|
|
26847
|
+
`/repos/${ref.owner}/${ref.repo}/contents/${encodedPath}?ref=${input2.expectedHeadOid}`
|
|
26848
|
+
);
|
|
26849
|
+
const text = Buffer.from(file2.content, "base64").toString("utf8");
|
|
26850
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
26851
|
+
const lines = text.split(eol);
|
|
26852
|
+
if (input2.endLine > lines.length || input2.startLine < 1 || input2.startLine > input2.endLine) {
|
|
26853
|
+
throw new GitHubError("Lignes de la suggestion hors du fichier.", 422);
|
|
26854
|
+
}
|
|
26855
|
+
const replacement = input2.replacement.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
26856
|
+
lines.splice(
|
|
26857
|
+
input2.startLine - 1,
|
|
26858
|
+
input2.endLine - input2.startLine + 1,
|
|
26859
|
+
...input2.replacement === "" ? [] : replacement
|
|
26860
|
+
);
|
|
26861
|
+
const res = await gh.rest(
|
|
26862
|
+
`/repos/${ref.owner}/${ref.repo}/contents/${encodedPath}`,
|
|
26863
|
+
{
|
|
26864
|
+
method: "PUT",
|
|
26865
|
+
body: {
|
|
26866
|
+
message: input2.message,
|
|
26867
|
+
content: Buffer.from(lines.join(eol), "utf8").toString("base64"),
|
|
26868
|
+
sha: file2.sha,
|
|
26869
|
+
branch: input2.branch
|
|
26870
|
+
}
|
|
26871
|
+
}
|
|
26872
|
+
);
|
|
26873
|
+
return res.commit.sha;
|
|
26874
|
+
}
|
|
26800
26875
|
|
|
26801
26876
|
// ../server/src/links/extract.ts
|
|
26802
26877
|
import ts from "typescript";
|
|
@@ -28195,7 +28270,6 @@ function resolveGradleProject(projectPath, from, gradleRoots) {
|
|
|
28195
28270
|
|
|
28196
28271
|
// ../server/src/prLinks.ts
|
|
28197
28272
|
var LINKS_VERSION = 6;
|
|
28198
|
-
var BATCH = 50;
|
|
28199
28273
|
var languageSchema = external_exports.enum([
|
|
28200
28274
|
"typescript",
|
|
28201
28275
|
"javascript",
|
|
@@ -28306,13 +28380,14 @@ async function loadWorkspace(gh, store, ref, head) {
|
|
|
28306
28380
|
const texts = await fetchTexts(
|
|
28307
28381
|
gh,
|
|
28308
28382
|
ref,
|
|
28309
|
-
manifests.map((path) =>
|
|
28383
|
+
manifests.map((path) => `${head}:${path}`)
|
|
28310
28384
|
);
|
|
28311
|
-
const
|
|
28385
|
+
const textOf = (path) => texts.get(`${head}:${path}`) ?? null;
|
|
28386
|
+
const packages = parseWorkspace(manifests.map((path) => ({ path, text: textOf(path) })));
|
|
28312
28387
|
const refs = packages.map((p) => ({
|
|
28313
28388
|
name: p.name,
|
|
28314
28389
|
root: p.root,
|
|
28315
|
-
entries: entryCandidates(p,
|
|
28390
|
+
entries: entryCandidates(p, textOf(p.manifest))
|
|
28316
28391
|
}));
|
|
28317
28392
|
return { packages, refs };
|
|
28318
28393
|
}
|
|
@@ -28363,25 +28438,6 @@ ${to}`;
|
|
|
28363
28438
|
packageEdges
|
|
28364
28439
|
};
|
|
28365
28440
|
}
|
|
28366
|
-
async function fetchTexts(gh, ref, wanted) {
|
|
28367
|
-
const texts = /* @__PURE__ */ new Map();
|
|
28368
|
-
for (let i = 0; i < wanted.length; i += BATCH) {
|
|
28369
|
-
const batch = wanted.slice(i, i + BATCH);
|
|
28370
|
-
const vars = { owner: ref.owner, repo: ref.repo };
|
|
28371
|
-
batch.forEach((w, k) => vars[`e${k}`] = `${w.oid}:${w.path}`);
|
|
28372
|
-
const query = `query(${["$owner: String!", "$repo: String!", ...batch.map((_, k) => `$e${k}: String!`)].join(", ")}) {
|
|
28373
|
-
repository(owner: $owner, name: $repo) {
|
|
28374
|
-
${batch.map((_, k) => `f${k}: object(expression: $e${k}) { ... on Blob { text isBinary isTruncated } }`).join("\n")}
|
|
28375
|
-
}
|
|
28376
|
-
}`;
|
|
28377
|
-
const data = await gh.graphql(query, vars);
|
|
28378
|
-
batch.forEach((w, k) => {
|
|
28379
|
-
const blob = data.repository[`f${k}`];
|
|
28380
|
-
texts.set(w.path, blob && !blob.isBinary && !blob.isTruncated ? blob.text : null);
|
|
28381
|
-
});
|
|
28382
|
-
}
|
|
28383
|
-
return texts;
|
|
28384
|
-
}
|
|
28385
28441
|
async function loadLinks(gh, store, ref, base, head, fullPr) {
|
|
28386
28442
|
const cacheKey2 = `links:v${LINKS_VERSION}:${ref.owner}/${ref.repo}:${base}...${head}`.toLowerCase();
|
|
28387
28443
|
const cached2 = store.getDiff(cacheKey2);
|
|
@@ -28391,16 +28447,11 @@ async function loadLinks(gh, store, ref, base, head, fullPr) {
|
|
|
28391
28447
|
}
|
|
28392
28448
|
const diff = await loadDiff(gh, store, ref, base, head, fullPr);
|
|
28393
28449
|
const readable = diff.files.filter((f) => !f.binary);
|
|
28394
|
-
const
|
|
28395
|
-
|
|
28396
|
-
ref,
|
|
28397
|
-
readable.map(
|
|
28398
|
-
(f) => f.status === "deleted" ? { path: f.oldPath, oid: base } : { path: f.path, oid: head }
|
|
28399
|
-
)
|
|
28400
|
-
);
|
|
28450
|
+
const expression = (f) => f.status === "deleted" ? `${base}:${f.oldPath}` : `${head}:${f.path}`;
|
|
28451
|
+
const texts = await fetchTexts(gh, ref, readable.map(expression));
|
|
28401
28452
|
const sources = diff.files.map((f) => ({
|
|
28402
28453
|
path: f.path,
|
|
28403
|
-
text: f.binary ? null : texts.get(f
|
|
28454
|
+
text: f.binary ? null : texts.get(expression(f)) ?? null
|
|
28404
28455
|
}));
|
|
28405
28456
|
const workspace = await loadWorkspace(gh, store, ref, head);
|
|
28406
28457
|
const graph = buildLinkGraph(sources, workspace ? { packages: workspace.refs } : void 0);
|
|
@@ -28836,6 +28887,7 @@ var prRouter = router({
|
|
|
28836
28887
|
get: procedure.input(prRefSchema).query(({ ctx, input: input2 }) => loadPr(ctx.gh, ctx.store, input2)),
|
|
28837
28888
|
diff: procedure.input(external_exports.object({ ref: prRefSchema, base: shaSchema, head: shaSchema, fullPr: external_exports.boolean() })).query(({ ctx, input: input2 }) => loadDiff(ctx.gh, ctx.store, input2.ref, input2.base, input2.head, input2.fullPr)),
|
|
28838
28889
|
links: procedure.input(external_exports.object({ ref: prRefSchema, base: shaSchema, head: shaSchema, fullPr: external_exports.boolean() })).query(({ ctx, input: input2 }) => loadLinks(ctx.gh, ctx.store, input2.ref, input2.base, input2.head, input2.fullPr)),
|
|
28890
|
+
blobs: procedure.input(external_exports.object({ ref: prRefSchema, oids: external_exports.array(shaSchema).min(1).max(2) })).query(({ ctx, input: input2 }) => loadBlobs(ctx.gh, ctx.store, input2.ref, input2.oids)),
|
|
28839
28891
|
onChange: procedure.input(prRefSchema).subscription(async function* ({ ctx, input: input2, signal }) {
|
|
28840
28892
|
yield* pollChanges(PR_POLL_MS, signal, () => prFingerprint(ctx.gh, input2, signal));
|
|
28841
28893
|
}),
|