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 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 : puis fichier non vu
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 (520 px, masquable avec `f`) propose quatre vues :
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 triés par taille, barre +/− par fichier |
69
- | Arbre | Dossiers repliables avec le total des changements, les fichiers vus et les threads ouverts |
70
- | Carte | Treemap : surface ∝ lignes modifiées, couleur ajouts → suppressions, fichiers vus estompés ; clic sur un dossier pour zoomer |
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 « Base d'abord »** (barre de la PR, à côté de Continu / Focus) : le diff, `j`/`k` et le mode Focus suivent un
79
- ordre topologique, chaque fichier après ceux qu'il importe ou mentionne, les plus utilisés en premier, les cycles
80
- départagés de la même façon et les fichiers sans lien à la fin. On relit la PR de ses fondations vers ce qui les
81
- assemble.
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 de lecture.
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
- id
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 toThread(t2, me, lastReadId) {
26699
- const comments = t2.comments.nodes.map((c) => ({
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) => ({ path, oid: head }))
28383
+ manifests.map((path) => `${head}:${path}`)
28310
28384
  );
28311
- const packages = parseWorkspace(manifests.map((path) => ({ path, text: texts.get(path) ?? null })));
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, texts.get(p.manifest) ?? null)
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 texts = await fetchTexts(
28395
- gh,
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.status === "deleted" ? f.oldPath : f.path) ?? null
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
  }),