@tequila99/githuman 0.3.0 → 0.4.1
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 +8 -1
- package/dist/cli/config.js +4 -2
- package/dist/cli/server-runtime.js +1 -0
- package/dist/server/app.js +10 -0
- package/dist/server/config/openapi.js +29 -0
- package/dist/server/repositories/review.repo.js +31 -5
- package/dist/server/routes/agent.js +145 -18
- package/dist/server/routes/app-info.js +8 -1
- package/dist/server/routes/comments.js +73 -27
- package/dist/server/routes/diff.js +76 -15
- package/dist/server/routes/events.js +14 -1
- package/dist/server/routes/export.js +22 -5
- package/dist/server/routes/git.js +52 -21
- package/dist/server/routes/health.js +8 -1
- package/dist/server/routes/openapi.js +4 -0
- package/dist/server/routes/reviews.js +59 -33
- package/dist/server/services/agent/agent-session.js +5 -3
- package/dist/server/services/diff.service.js +119 -1
- package/dist/server/services/git.service.js +104 -3
- package/dist/shared/agents/schemas.js +120 -31
- package/dist/shared/app/schemas.js +7 -0
- package/dist/shared/comments/schemas.js +48 -0
- package/dist/shared/diff/schemas.js +77 -0
- package/dist/shared/diff/types.js +0 -3
- package/dist/shared/events/schemas.js +4 -0
- package/dist/shared/git/schemas.js +59 -0
- package/dist/shared/http/schemas.js +23 -0
- package/dist/shared/reviews/schemas.js +96 -0
- package/dist/shared/reviews/summary.js +5 -0
- package/dist/shared/utils/schemas.js +9 -0
- package/dist/web/assets/AppPage-BSvF7ik1.js +1 -0
- package/dist/web/assets/CreateReviewFabButton-M0od1QEZ.js +1 -0
- package/dist/web/assets/{ErrorNotFound-B5uYICL4.js → ErrorNotFound-BAP3n0aC.js} +1 -1
- package/dist/web/assets/IndexPage-CztuM7ha.css +1 -0
- package/dist/web/assets/IndexPage-Do-EZOBf.js +1 -0
- package/dist/web/assets/MainLayout-daEN-VtX.js +7 -0
- package/dist/web/assets/QBtn-BDbJU6_B.js +1 -0
- package/dist/web/assets/{QMenu-BZ1eK_rS.js → QMenu-DXgFwoTJ.js} +1 -1
- package/dist/web/assets/QSelect-DzYPZHrW.js +1 -0
- package/dist/web/assets/ReviewDetailPage-CP6YqXPT.css +1 -0
- package/dist/web/assets/ReviewDetailPage-DEtNg2UN.js +1 -0
- package/dist/web/assets/ReviewsPage-Dg-iCxsQ.js +1 -0
- package/dist/web/assets/StatusSwitcher-BpebV47C.js +4 -0
- package/dist/web/assets/{StatusSwitcher-CZYtI0ID.css → StatusSwitcher-Ca5k9Tjx.css} +1 -1
- package/dist/web/assets/{abnfDiagram-O67JEVCF-DkOrqZLn.js → abnfDiagram-O67JEVCF-BUX560Qb.js} +1 -1
- package/dist/web/assets/agent-icon-BJbyp2DP.js +17 -0
- package/dist/web/assets/agent-store-B9E-aiz3.js +1 -0
- package/dist/web/assets/{app-info-store-CIqmSeWK.js → app-info-store-xZiQfdgO.js} +1 -1
- package/dist/web/assets/{arc-BALAYY3f.js → arc-dElW-85p.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-bgxww3gY.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-C-xtrMea.js → architectureDiagram-NJMV4G6O-CUUKpxro.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-DYo9Os-m.js → blockDiagram-BEXU5L5S-D4r5wIie.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-BpSFjUAX.js → c4Diagram-YGBWAQC7-ww04LJhe.js} +1 -1
- package/dist/web/assets/channel-CdThk9yc.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-DtUqbu5K.js → chunk-3FUC2YCW-9i3K5FXP.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-DeYTIM8k.js → chunk-5DYCD2WN-BmtGl0Xn.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-8BfKAsu6.js → chunk-742MDFTN-5p0wX5Yc.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-PXK092EZ.js → chunk-7INBJB4K-CWFVY0jp.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-Db9reT-V.js → chunk-7M6MHVWA-DUgZIYj4.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-CNFpjNaH.js → chunk-7PRAP22T-BrhA6Tm7.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-C5gqOp1W.js → chunk-GTNCS2PH-B83nsnuR.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-BquHLG8L.js → chunk-GWA4HPMP-cewRmL7f.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-DyOEFgCh.js → chunk-MBY4JIJT-D18SeqyB.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-Bd3ij3-b.js → chunk-NETBCI7D-5SjiRygT.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-DQvIvDe6.js → chunk-O7XYJQB3-H6mLYErS.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-BDIHwgCZ.js → chunk-UA2S7LBM-C5VQVDlC.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-DwQKTsK-.js → chunk-WEXAMYUT-CrOcvEcH.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-DTdiON6C.js → chunk-XXDRQBXY-BuVPxQhc.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-CIvnaGnU.js → chunk-Z7XXMR3K-BVP3f1-g.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-BdT_T5CV.js → chunk-ZIGJFQKS-CF-OzGXH.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-BUoclHlO.js → classDiagram-v2-NBCMYWYE-Bd3VDNaV.js} +1 -1
- package/dist/web/assets/{core-B8kUiZoM.js → core-DiiZIGCg.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-C16eW6pc.js → cose-bilkent-JH36ORCC-ifMLrEkd.js} +1 -1
- package/dist/web/assets/create-BkZH2Q0U.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-1mS7ZeoM.js → cynefin-OW5HDTMX-6l9c0yRf.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-D1PC4YT4.js → cynefinDiagram-VND7K2PF-u9Gy0Odz.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-amODOP74.js → dagre-6A5THRUB-D1jsEBl4.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-wUvcsN_B.js → diagram-22UHCM2B-DpyH5n1y.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-CLiOVs2l.js → diagram-3UASUU5V-3X1RJV9-.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-Cs5cFGgz.js → diagram-ATOU4E4O-B_bvvAua.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-C8Jb-YeE.js → diagram-CDSNMT55-DheqAuOA.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-DA6C9fVB.js → diagram-MLGK6HIB-CYwe9zYy.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-BOaFKuUi.js → diagram-MPIPVDR6-CVd9i23e.js} +1 -1
- package/dist/web/assets/{dist-DuFVAX6Y.js → dist-BzuUT3GV.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-e5jvn-EE.js → ebnfDiagram-ZINNZB2B-B5vtXULf.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-D7Gcu17D.js → elk-276RUBZZ-BmMM0tV5.js} +1 -1
- package/dist/web/assets/{engine-oniguruma-HbxpIAu4.js → engine-oniguruma-D6DCTEub.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-Cn1bBUF8.js → erDiagram-OPXOYQCR-1ORPVLQR.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-BH97sTDs.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-CRwhE8ka.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-C3U8jeJB.js → ganttDiagram-FUAMR5RP-CAWNdy1S.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-C165DC0H.js → gitGraph-4MIJSDKK-CXCCnZ1j.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-CVHdT1OC.js → gitGraphDiagram-X574FWY7-DPDrqSt6.js} +1 -1
- package/dist/web/assets/i18n-D5wmXwHr.js +1 -0
- package/dist/web/assets/{index-C6ArNnKs.css → index-CgeU6Wuu.css} +1 -1
- package/dist/web/assets/index-DWi4DKrl.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-cbWtF3FJ.js → info-A6RAGUB7-CGg3Xa4X.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-C_kWn3Ai.js → infoDiagram-VRGFBTTK-vx9EUrdV.js} +1 -1
- package/dist/web/assets/init-app-CgDkvlRb.js +1 -0
- package/dist/web/assets/is-BB7rJzdU.js +1 -0
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-Xb9cDRr7.js → ishikawaDiagram-OU5B5YK6-DL7T0J63.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-6eZcefap.js → journeyDiagram-ZHPQQLJL-D45T1Pj5.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-D_z_AsKR.js → kanban-definition-PNTS6WVX-C9ZnTE1l.js} +1 -1
- package/dist/web/assets/{line-v7JtZ-Za.js → line-CLBEcmiH.js} +1 -1
- package/dist/web/assets/{linear-hp1KWmiC.js → linear-B5bBEu0i.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Cwwk5NqO.js → mermaid-parser.core-Dj-n65Ou.js} +3 -3
- package/dist/web/assets/{mermaid.core-BjPJhwW0.js → mermaid.core-CmPsESsw.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-lQohhDXq.js → mindmap-definition-NLK3R4M7-ZX638ce_.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-DxiGe6nt.js → packet-AYTQ26CC-D-XZHUq_.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-XZRpQTLx.js → pegDiagram-GJSIUBJH-sE-EOKNL.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-fxN0O90k.js → pie-WAS4IAKB-DMkVggJA.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-M_bBKE-n.js → pieDiagram-5QR66LMP-DSdIEQyS.js} +1 -1
- package/dist/web/assets/pinia-BMN9Drn3.js +1 -0
- package/dist/web/assets/pinia-DmTi2T44.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-BNxVPrq3.js → quadrantDiagram-O4NWA36T-BReXSVss.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-Cd-5aokk.js → radar-RG4KPBEZ-CJzPwlj6.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-BvSKL8Ze.js → railroad-74A4TZTK-CkhRuR3N.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CcS8fVgm.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BlpYA103.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-C2EznudM.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-D664lPSL.js → railroadDiagram-XR7U4H2S-DCoF4AEN.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-B_1Dl0-3.js → requirementDiagram-PLB6GJNP-B15aXZXI.js} +1 -1
- package/dist/web/assets/runtime-core.esm-bundler-CUpq88z7.js +1 -0
- package/dist/web/assets/{safe-storage-CWaD6Dq3.js → safe-storage-ak36tVRO.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-BvFmMC_F.js → sankeyDiagram-IPEJSGJF-DcWsnYYP.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-DMK1ey4g.js → sequenceDiagram-PO4LG4MO-CXWUNmSl.js} +1 -1
- package/dist/web/assets/{src-Cy0hT8a0.js → src-CB-shhJB.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-VVElU-_E.js → stateDiagram-v2-GCMORJYK-M9PkKdix.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-DyfYJvHL.js → swimlanes-2SLR337P-B13LpAqh.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BmCQPnJC.js +8 -0
- package/dist/web/assets/theme-BMaTNJdz.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-DBoRb7uC.js → timeline-definition-EJHVYXUP-Dli0jMaX.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-D8AoZg1Y.js → treeView-Q6P3EWNA-BwBFpWIC.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DhcVtZsu.js → treemap-WGGIJYW6-DYGjPuTv.js} +1 -1
- package/dist/web/assets/use-app-theme-C9HTPdVu.js +1 -0
- package/dist/web/assets/use-panel-width-DGbjs_T8.js +1 -0
- package/dist/web/assets/use-virtual-scroll-BVHTKnoC.js +1 -0
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-BCfvWOH4.js → usecaseDiagram-POWQR4AR-Chs4O-o-.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-BIk6-FLo.js → vennDiagram-UO4OBE2U-D3GVZcIr.js} +1 -1
- package/dist/web/assets/{vue-i18n.runtime-BQwnBdmC.js → vue-i18n.runtime-D0pkhc7g.js} +2 -2
- package/dist/web/assets/{wardley-WFR3VGLG-BMHH70A3.js → wardley-WFR3VGLG-B5YUEVo5.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DB-cxlSq.js → wardleyDiagram-VNRHLVJA-liqH-4lm.js} +1 -1
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-BejE-3gi.js → xychartDiagram-PMCCYNJV-C63N6ZvA.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +2 -2
- package/dist/web/assets/AppPage-CSb8txs5.js +0 -1
- package/dist/web/assets/CreateReviewFabButton-B18DLuH6.js +0 -1
- package/dist/web/assets/IndexPage-B-hkrnvt.css +0 -1
- package/dist/web/assets/IndexPage-Cm_ssBeC.js +0 -1
- package/dist/web/assets/MainLayout-ClBPF545.js +0 -7
- package/dist/web/assets/QBtn-E32fUOKx.js +0 -1
- package/dist/web/assets/QItemLabel-Co6oCEUr.js +0 -1
- package/dist/web/assets/QSelect-D1V4tbDr.js +0 -1
- package/dist/web/assets/ReviewDetailPage-BWa3EEkv.css +0 -1
- package/dist/web/assets/ReviewDetailPage-Ct0hXwZL.js +0 -1
- package/dist/web/assets/ReviewsPage-CbLAU6OM.js +0 -1
- package/dist/web/assets/StatusSwitcher-C0k4efwK.js +0 -4
- package/dist/web/assets/agent-icon-BKoq2a6T.js +0 -17
- package/dist/web/assets/agent-store-CEQ6MmTC.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-CTkghkmX.js +0 -1
- package/dist/web/assets/channel-f9cE6NV9.js +0 -1
- package/dist/web/assets/create-B9ZqyTxV.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-BjGpd42n.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-a3N8MVB8.js +0 -1
- package/dist/web/assets/i18n-CyUPETur.js +0 -1
- package/dist/web/assets/index-C_B6Z2lc.js +0 -2
- package/dist/web/assets/init-app-DnfYD-0k.js +0 -1
- package/dist/web/assets/is-ChHvR1Z-.js +0 -1
- package/dist/web/assets/pinia-0B8sIOgD.js +0 -1
- package/dist/web/assets/pinia-DbmXdZbT.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CB32K25L.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-CqQR3zRl.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DRSdB-oL.js +0 -1
- package/dist/web/assets/runtime-core.esm-bundler-Carm8LUR.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BQ0mK2BI.js +0 -8
- package/dist/web/assets/theme-BFceB3b9.js +0 -1
- package/dist/web/assets/use-app-theme-DxZW-eOR.js +0 -1
- package/dist/web/assets/use-panel-width-CY75Fomr.js +0 -1
|
@@ -211,6 +211,107 @@ function statusCodeToStatus(code) {
|
|
|
211
211
|
return 'modified';
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Removes the extra record of a merge conflict. During a conflict, `git diff` prints an
|
|
216
|
+
* `U` record with zero blob ids and also a normal record for the same path. The normal
|
|
217
|
+
* record has the real blob ids and counts, so it stays.
|
|
218
|
+
*/
|
|
219
|
+
function dedupeUnmerged(records) {
|
|
220
|
+
const normalPaths = new Set(records
|
|
221
|
+
.filter(record => record.code !== 'U')
|
|
222
|
+
.map(record => record.item.newPath));
|
|
223
|
+
return records
|
|
224
|
+
.filter(record => record.code !== 'U' || !normalPaths.has(record.item.newPath))
|
|
225
|
+
.map(record => record.item);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parses the output of `git diff --raw --numstat -z`. Git prints all raw records first:
|
|
229
|
+
* a `:<modes> <old oid> <new oid> <status>` token, then one path, or two for a rename or copy.
|
|
230
|
+
* The numstat records follow: `<added>\t<removed>\t<path>` in one token. For a rename, the
|
|
231
|
+
* path part is empty, and two path tokens follow. Paths are read by position, so a path
|
|
232
|
+
* with `:`, a TAB or a line feed does not break the parse.
|
|
233
|
+
*/
|
|
234
|
+
export function parseRawNumstat(stdout) {
|
|
235
|
+
const tokens = stdout.split('\0');
|
|
236
|
+
// With -z, the output ends with NUL, so the last token is empty.
|
|
237
|
+
if (tokens[tokens.length - 1] === '')
|
|
238
|
+
tokens.pop();
|
|
239
|
+
const raw = [];
|
|
240
|
+
const numstat = [];
|
|
241
|
+
let i = 0;
|
|
242
|
+
while (i < tokens.length && tokens[i].startsWith(':')) {
|
|
243
|
+
const fields = tokens[i++].split(' ');
|
|
244
|
+
const code = fields[4] ?? 'M';
|
|
245
|
+
const oldOid = fields[2] ?? '';
|
|
246
|
+
const newOid = fields[3] ?? '';
|
|
247
|
+
const status = statusCodeToStatus(code);
|
|
248
|
+
if (code[0] === 'R' || code[0] === 'C') {
|
|
249
|
+
const oldPath = tokens[i++];
|
|
250
|
+
const newPath = tokens[i++];
|
|
251
|
+
raw.push({ code, oldPath, newPath, status, oldOid, newOid });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const path = tokens[i++];
|
|
255
|
+
raw.push({ code, oldPath: path, newPath: path, status, oldOid, newOid });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
while (i < tokens.length) {
|
|
259
|
+
const token = tokens[i++];
|
|
260
|
+
// Only the first two TABs separate fields: a path can hold a TAB too.
|
|
261
|
+
const firstTab = token.indexOf('\t');
|
|
262
|
+
const secondTab = token.indexOf('\t', firstTab + 1);
|
|
263
|
+
const added = token.slice(0, firstTab);
|
|
264
|
+
const removed = token.slice(firstTab + 1, secondTab);
|
|
265
|
+
let path = token.slice(secondTab + 1);
|
|
266
|
+
if (path === '') {
|
|
267
|
+
i++; // the old path of the rename
|
|
268
|
+
path = tokens[i++];
|
|
269
|
+
}
|
|
270
|
+
const isBinary = added === '-';
|
|
271
|
+
numstat.push({
|
|
272
|
+
path,
|
|
273
|
+
additions: isBinary ? 0 : Number(added),
|
|
274
|
+
deletions: isBinary ? 0 : Number(removed),
|
|
275
|
+
isBinary
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return { raw, numstat };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Joins raw and numstat records. They come from one git run in the same order, so they
|
|
282
|
+
* line up by position. A path mismatch means the parse went wrong: then the join goes by path.
|
|
283
|
+
*/
|
|
284
|
+
function joinRawNumstat(raw, numstat) {
|
|
285
|
+
const aligned = raw.length === numstat.length &&
|
|
286
|
+
raw.every((record, i) => record.newPath === numstat[i].path);
|
|
287
|
+
if (!aligned) {
|
|
288
|
+
process.emitWarning(`git diff --raw --numstat: ${raw.length} raw and ${numstat.length} numstat records do not line up; joining by path`);
|
|
289
|
+
}
|
|
290
|
+
const byPath = aligned
|
|
291
|
+
? undefined
|
|
292
|
+
: new Map(numstat.map(record => [record.path, record]));
|
|
293
|
+
return raw.map(({ code, ...record }, i) => {
|
|
294
|
+
const counts = aligned ? numstat[i] : byPath?.get(record.newPath);
|
|
295
|
+
return {
|
|
296
|
+
code,
|
|
297
|
+
item: {
|
|
298
|
+
...record,
|
|
299
|
+
additions: counts?.additions ?? 0,
|
|
300
|
+
deletions: counts?.deletions ?? 0,
|
|
301
|
+
isBinary: counts?.isBinary ?? false
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Lists changed paths with line counts and both blob ids, without reading file contents.
|
|
308
|
+
* One git run prints both formats, so the records describe one state of the repository.
|
|
309
|
+
*/
|
|
310
|
+
export async function listChangedSummaries(repoPath, diffArgs) {
|
|
311
|
+
const { stdout } = await execFileAsync('git', ['diff', '--raw', '--numstat', '--no-abbrev', '-M', '-z', ...diffArgs], { cwd: repoPath, encoding: 'utf-8', maxBuffer: 1024 * 1024 * 100 });
|
|
312
|
+
const { raw, numstat } = parseRawNumstat(stdout);
|
|
313
|
+
return dedupeUnmerged(joinRawNumstat(raw, numstat));
|
|
314
|
+
}
|
|
214
315
|
/**
|
|
215
316
|
* Lists changed paths for a diff, given the git ref arguments that would
|
|
216
317
|
* normally follow `git diff --name-status -M` (e.g. `['--cached']` for
|
|
@@ -233,14 +334,14 @@ export async function listChangedPaths(repoPath, diffArgs) {
|
|
|
233
334
|
if (code[0] === 'R' || code[0] === 'C') {
|
|
234
335
|
const oldPath = tokens[i++];
|
|
235
336
|
const newPath = tokens[i++];
|
|
236
|
-
results.push({ oldPath, newPath, status });
|
|
337
|
+
results.push({ code, item: { oldPath, newPath, status } });
|
|
237
338
|
}
|
|
238
339
|
else {
|
|
239
340
|
const path = tokens[i++];
|
|
240
|
-
results.push({ oldPath: path, newPath: path, status });
|
|
341
|
+
results.push({ code, item: { oldPath: path, newPath: path, status } });
|
|
241
342
|
}
|
|
242
343
|
}
|
|
243
|
-
return results;
|
|
344
|
+
return dedupeUnmerged(results);
|
|
244
345
|
}
|
|
245
346
|
/** Lists paths that git has never tracked (respects .gitignore). */
|
|
246
347
|
export async function listUntrackedPaths(repoPath) {
|
|
@@ -1,51 +1,140 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { MAX_ATTACHMENT_BYTES, MAX_FILE_SEARCH_LIMIT } from "./constants.js";
|
|
3
|
-
|
|
3
|
+
import { DiffSourceNameSchema } from "../diff/schemas.js";
|
|
4
|
+
import { Nullable } from "../utils/schemas.js";
|
|
5
|
+
export const SessionParams = Type.Object({
|
|
6
|
+
id: Type.String({ description: 'Chat session id.' })
|
|
7
|
+
});
|
|
4
8
|
export const PermissionParams = Type.Object({
|
|
5
|
-
id: Type.String(),
|
|
6
|
-
requestId: Type.String()
|
|
9
|
+
id: Type.String({ description: 'Chat session id.' }),
|
|
10
|
+
requestId: Type.String({ description: 'Pending permission request id.' })
|
|
7
11
|
});
|
|
8
12
|
export const CreateSessionBody = Type.Object({
|
|
9
|
-
presetId: Type.String({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
})
|
|
13
|
-
|
|
13
|
+
presetId: Type.String({
|
|
14
|
+
minLength: 1,
|
|
15
|
+
description: 'Preset from GET /api/agent/presets.'
|
|
16
|
+
}),
|
|
17
|
+
name: Type.Optional(Type.String({
|
|
18
|
+
maxLength: 60,
|
|
19
|
+
description: 'Chat name. Omitted: the server makes a unique one.'
|
|
20
|
+
})),
|
|
21
|
+
reviewId: Type.Optional(Type.String({ description: 'Review that the chat belongs to.' }))
|
|
22
|
+
}, { description: 'A new chat session.' });
|
|
23
|
+
export const AutoApproveBody = Type.Object({
|
|
24
|
+
enabled: Type.Boolean({
|
|
25
|
+
description: 'The server answers permission requests itself.'
|
|
26
|
+
})
|
|
27
|
+
}, { description: 'Auto-approve switch of one session.' });
|
|
14
28
|
export const FileSearchQuery = Type.Object({
|
|
15
|
-
q: Type.Optional(Type.String({
|
|
16
|
-
|
|
29
|
+
q: Type.Optional(Type.String({
|
|
30
|
+
maxLength: 200,
|
|
31
|
+
description: 'Fuzzy text to find in paths. Empty lists files.'
|
|
32
|
+
})),
|
|
33
|
+
limit: Type.Optional(Type.Integer({
|
|
34
|
+
minimum: 1,
|
|
35
|
+
maximum: MAX_FILE_SEARCH_LIMIT,
|
|
36
|
+
description: 'Maximum number of paths.'
|
|
37
|
+
}))
|
|
17
38
|
});
|
|
18
39
|
export const ContextItem = Type.Union([
|
|
19
40
|
Type.Object({
|
|
20
|
-
kind: Type.Literal('diff'),
|
|
21
|
-
source:
|
|
22
|
-
path: Type.Optional(Type.String({
|
|
23
|
-
|
|
41
|
+
kind: Type.Literal('diff', { description: 'Live working-tree diff.' }),
|
|
42
|
+
source: DiffSourceNameSchema,
|
|
43
|
+
path: Type.Optional(Type.String({
|
|
44
|
+
minLength: 1,
|
|
45
|
+
description: 'One file of the diff. Omitted: the whole diff.'
|
|
46
|
+
}))
|
|
47
|
+
}, { description: 'The live diff of one side.' }),
|
|
24
48
|
Type.Object({
|
|
25
|
-
kind: Type.Literal('file'),
|
|
26
|
-
path: Type.String({
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
kind: Type.Literal('file', { description: 'A repository file.' }),
|
|
50
|
+
path: Type.String({
|
|
51
|
+
minLength: 1,
|
|
52
|
+
description: 'Repository-relative path. The agent reads the file.'
|
|
53
|
+
})
|
|
54
|
+
}, { description: 'A link to a repository file.' }),
|
|
29
55
|
Type.Object({
|
|
30
|
-
kind: Type.Literal('
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
kind: Type.Literal('review', { description: 'A review.' }),
|
|
57
|
+
reviewId: Type.String({ description: 'Review id.' })
|
|
58
|
+
}, { description: 'A review as markdown with its comments.' }),
|
|
59
|
+
Type.Object({
|
|
60
|
+
kind: Type.Literal('attachment', {
|
|
61
|
+
description: 'A file from the user.'
|
|
62
|
+
}),
|
|
63
|
+
name: Type.String({
|
|
64
|
+
minLength: 1,
|
|
65
|
+
maxLength: 255,
|
|
66
|
+
description: 'File name.'
|
|
67
|
+
}),
|
|
68
|
+
mimeType: Type.String({ maxLength: 255, description: 'MIME type.' }),
|
|
33
69
|
data: Type.String({
|
|
34
|
-
maxLength: Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4
|
|
70
|
+
maxLength: Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4,
|
|
71
|
+
description: 'File content in base64.'
|
|
35
72
|
})
|
|
36
|
-
})
|
|
37
|
-
]);
|
|
73
|
+
}, { description: 'An uploaded file or a pasted image.' })
|
|
74
|
+
], { description: 'Context of a prompt (docs/agent-context-uris.md).' });
|
|
38
75
|
export const PromptBody = Type.Object({
|
|
39
|
-
text: Type.String({ minLength: 1 }),
|
|
40
|
-
context: Type.Optional(Type.Array(ContextItem, {
|
|
41
|
-
|
|
76
|
+
text: Type.String({ minLength: 1, description: 'Message to the agent.' }),
|
|
77
|
+
context: Type.Optional(Type.Array(ContextItem, {
|
|
78
|
+
maxItems: 20,
|
|
79
|
+
description: 'Context items that go with the message.'
|
|
80
|
+
}))
|
|
81
|
+
}, { description: 'A user message.' });
|
|
42
82
|
export const PermissionBody = Type.Object({
|
|
43
|
-
optionId: Type.Optional(Type.String(
|
|
44
|
-
|
|
83
|
+
optionId: Type.Optional(Type.String({
|
|
84
|
+
description: 'Option from the permission request. Omitted: the request is cancelled.'
|
|
85
|
+
}))
|
|
86
|
+
}, { description: 'Answer to a permission request.' });
|
|
45
87
|
export const ConfigBody = Type.Object({
|
|
46
|
-
configId: Type.String({
|
|
88
|
+
configId: Type.String({
|
|
89
|
+
minLength: 1,
|
|
90
|
+
description: 'Setting id from the config event.'
|
|
91
|
+
}),
|
|
47
92
|
// No schema type on purpose: a Union/anyOf makes Fastify's Ajv coerce `true`
|
|
48
93
|
// into the string "true", and a `type` array trips Ajv's strictTypes. The
|
|
49
94
|
// session checks the value against the option the agent actually offered.
|
|
50
|
-
value: Type.Unsafe({
|
|
95
|
+
value: Type.Unsafe({
|
|
96
|
+
description: 'A select value (string) or a boolean.'
|
|
97
|
+
})
|
|
98
|
+
}, { description: 'A new value of an agent setting.' });
|
|
99
|
+
export const AgentPresetInfoSchema = Type.Object({
|
|
100
|
+
id: Type.String({ description: 'Preset id.' }),
|
|
101
|
+
title: Type.String({ description: 'Name in the UI.' }),
|
|
102
|
+
available: Type.Boolean({
|
|
103
|
+
description: 'The launcher binary is in PATH. Says nothing about login or auth.'
|
|
104
|
+
}),
|
|
105
|
+
autoApprovesEdits: Type.Boolean({
|
|
106
|
+
description: 'The agent applies edits without permission requests.'
|
|
107
|
+
})
|
|
108
|
+
}, { description: 'An ACP agent that can start a chat.' });
|
|
109
|
+
export const AgentSessionStatusSchema = Type.Union([
|
|
110
|
+
Type.Literal('starting'),
|
|
111
|
+
Type.Literal('ready'),
|
|
112
|
+
Type.Literal('busy'),
|
|
113
|
+
Type.Literal('closed')
|
|
114
|
+
], { description: 'Session state.' });
|
|
115
|
+
export const AgentSessionInfoSchema = Type.Object({
|
|
116
|
+
id: Type.String({ description: 'Chat session id.' }),
|
|
117
|
+
presetId: Type.String({ description: 'Preset of the agent.' }),
|
|
118
|
+
name: Type.String({ description: 'Chat name in the UI.' }),
|
|
119
|
+
status: AgentSessionStatusSchema,
|
|
120
|
+
reviewId: Nullable(Type.String({ description: 'Review of the chat. Else null.' })),
|
|
121
|
+
autoApprove: Type.Boolean({
|
|
122
|
+
description: 'The server answers permission requests itself.'
|
|
123
|
+
}),
|
|
124
|
+
error: Nullable(Type.String({
|
|
125
|
+
description: 'Why the session could not start. Else null.'
|
|
126
|
+
}))
|
|
127
|
+
}, { description: 'A chat session.' });
|
|
128
|
+
export const AgentFileSearchResponseSchema = Type.Object({
|
|
129
|
+
files: Type.Array(Type.String(), {
|
|
130
|
+
description: 'Repository-relative paths, best match first.'
|
|
131
|
+
})
|
|
132
|
+
}, { description: 'Files for the context picker.' });
|
|
133
|
+
export const AgentEventsHeaders = Type.Object({
|
|
134
|
+
'last-event-id': Type.Optional(Type.String({
|
|
135
|
+
description: 'Last event id that the client got. Events after it come again.'
|
|
136
|
+
}))
|
|
137
|
+
});
|
|
138
|
+
export const AgentStreamSchema = Type.String({
|
|
139
|
+
description: 'Server-sent events: connected, then agent (AgentStreamEnvelope, SSE id = event id), state (AgentSessionState), gap ({ sessionId }) and sessions (reread the list).'
|
|
51
140
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
export const AppInfoSchema = Type.Object({
|
|
3
|
+
version: Type.String({ description: 'githuman version from package.json.' })
|
|
4
|
+
}, { description: 'Facts about the running server.' });
|
|
5
|
+
export const HealthSchema = Type.Object({
|
|
6
|
+
status: Type.Literal('ok', { description: 'Always "ok".' })
|
|
7
|
+
}, { description: 'The server is up.' });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { Nullable } from "../utils/schemas.js";
|
|
3
|
+
import { DiffLineTypeSchema } from "../diff/schemas.js";
|
|
4
|
+
export const CommentSchema = Type.Object({
|
|
5
|
+
id: Type.String({ description: 'Comment id.' }),
|
|
6
|
+
reviewId: Type.String({ description: 'Review that holds the comment.' }),
|
|
7
|
+
filePath: Type.String({ description: 'Repository-relative file path.' }),
|
|
8
|
+
lineNumber: Nullable(Type.Integer({
|
|
9
|
+
description: 'First line of the comment. Null for a file comment.'
|
|
10
|
+
})),
|
|
11
|
+
lineNumberEnd: Nullable(Type.Integer({
|
|
12
|
+
description: 'Last line of a selected range. Equals lineNumber for one line.'
|
|
13
|
+
})),
|
|
14
|
+
lineType: Nullable(DiffLineTypeSchema),
|
|
15
|
+
content: Type.String({ description: 'Comment text in markdown.' }),
|
|
16
|
+
createdAt: Type.String({ description: 'Creation time, ISO 8601.' }),
|
|
17
|
+
updatedAt: Type.String({ description: 'Last change time, ISO 8601.' }),
|
|
18
|
+
resolved: Type.Optional(Type.Boolean({ description: 'The comment is resolved.' })),
|
|
19
|
+
suggestion: Type.Optional(Nullable(Type.String({ description: 'Code that replaces the selected lines.' })))
|
|
20
|
+
}, { description: 'A comment on a file or on lines of a review.' });
|
|
21
|
+
// Null goes first in each union: Fastify's Ajv coerces types, and with
|
|
22
|
+
// Integer (or String) first it turns a JSON null into 0 (or "") before the
|
|
23
|
+
// Null branch is ever tried — a file-level comment would be stored as line 0.
|
|
24
|
+
export const CreateCommentBody = Type.Object({
|
|
25
|
+
filePath: Type.String({
|
|
26
|
+
minLength: 1,
|
|
27
|
+
description: 'Repository-relative file path.'
|
|
28
|
+
}),
|
|
29
|
+
lineNumber: Type.Optional(Type.Union([Type.Null(), Type.Integer()], {
|
|
30
|
+
description: 'First line. Null or omitted for a file comment.'
|
|
31
|
+
})),
|
|
32
|
+
lineNumberEnd: Type.Optional(Type.Union([Type.Null(), Type.Integer()], {
|
|
33
|
+
description: 'Last line of a range. Null or omitted for one line.'
|
|
34
|
+
})),
|
|
35
|
+
lineType: Type.Optional(Type.Union([Type.Null(), ...DiffLineTypeSchema.anyOf], {
|
|
36
|
+
description: 'Line kind of lineNumber. Null for a file comment.'
|
|
37
|
+
})),
|
|
38
|
+
content: Type.String({ description: 'Comment text in markdown.' }),
|
|
39
|
+
suggestion: Type.Optional(Type.Union([Type.Null(), Type.String()], {
|
|
40
|
+
description: 'Code that replaces the selected lines.'
|
|
41
|
+
}))
|
|
42
|
+
}, { description: 'A new comment.' });
|
|
43
|
+
export const UpdateCommentBody = Type.Object({
|
|
44
|
+
content: Type.String({ description: 'New comment text in markdown.' })
|
|
45
|
+
}, { description: 'A comment change.' });
|
|
46
|
+
export const CommentIdParams = Type.Object({
|
|
47
|
+
id: Type.String({ description: 'Comment id.' })
|
|
48
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { Nullable } from "../utils/schemas.js";
|
|
3
|
+
export const DiffSourceNameSchema = Type.Union([Type.Literal('staged'), Type.Literal('unstaged')], { description: 'Side of the working tree: the index or the files on disk.' });
|
|
4
|
+
export const DiffLineTypeSchema = Type.Union([Type.Literal('added'), Type.Literal('removed'), Type.Literal('context')], { description: 'Line kind: added, removed or unchanged context.' });
|
|
5
|
+
export const DiffFileStatusSchema = Type.Union([
|
|
6
|
+
Type.Literal('added'),
|
|
7
|
+
Type.Literal('modified'),
|
|
8
|
+
Type.Literal('deleted'),
|
|
9
|
+
Type.Literal('renamed')
|
|
10
|
+
], { description: 'What the change does to the file.' });
|
|
11
|
+
export const DiffLineSchema = Type.Object({
|
|
12
|
+
type: DiffLineTypeSchema,
|
|
13
|
+
content: Type.String({
|
|
14
|
+
description: 'Line text without the "+", "-" or " " prefix.'
|
|
15
|
+
}),
|
|
16
|
+
oldLineNumber: Nullable(Type.Integer({
|
|
17
|
+
description: 'Line number in the old file. Null for an added line.'
|
|
18
|
+
})),
|
|
19
|
+
newLineNumber: Nullable(Type.Integer({
|
|
20
|
+
description: 'Line number in the new file. Null for a removed line.'
|
|
21
|
+
}))
|
|
22
|
+
}, { description: 'One line of a hunk.' });
|
|
23
|
+
export const DiffHunkSchema = Type.Object({
|
|
24
|
+
oldStart: Type.Integer({ description: 'First line in the old file.' }),
|
|
25
|
+
oldLines: Type.Integer({ description: 'Line count in the old file.' }),
|
|
26
|
+
newStart: Type.Integer({ description: 'First line in the new file.' }),
|
|
27
|
+
newLines: Type.Integer({ description: 'Line count in the new file.' }),
|
|
28
|
+
lines: Type.Array(DiffLineSchema, {
|
|
29
|
+
description: 'Lines of the hunk in file order.'
|
|
30
|
+
})
|
|
31
|
+
}, { description: 'One hunk of a file diff.' });
|
|
32
|
+
const diffFileFields = {
|
|
33
|
+
oldPath: Type.String({
|
|
34
|
+
description: 'Path before the change. Equals newPath unless renamed.'
|
|
35
|
+
}),
|
|
36
|
+
newPath: Type.String({
|
|
37
|
+
description: 'Path after the change. Equals oldPath for a deleted file.'
|
|
38
|
+
}),
|
|
39
|
+
status: DiffFileStatusSchema,
|
|
40
|
+
additions: Type.Integer({ description: 'Added line count.' }),
|
|
41
|
+
deletions: Type.Integer({ description: 'Removed line count.' }),
|
|
42
|
+
isBinary: Type.Boolean({ description: 'Git sees the file as binary.' })
|
|
43
|
+
};
|
|
44
|
+
export const DiffFileSchema = Type.Object({
|
|
45
|
+
...diffFileFields,
|
|
46
|
+
hunks: Type.Array(DiffHunkSchema, {
|
|
47
|
+
description: 'Hunks of the file. Empty for a binary file.'
|
|
48
|
+
})
|
|
49
|
+
}, { description: 'Diff of one file with its hunks.' });
|
|
50
|
+
export const DiffFileSummarySchema = Type.Object({
|
|
51
|
+
...diffFileFields,
|
|
52
|
+
signature: Type.String({
|
|
53
|
+
description: 'Changes when the file content or its diff changes. Equal signatures mean equal hunks.'
|
|
54
|
+
})
|
|
55
|
+
}, { description: 'Diff of one file without hunks.' });
|
|
56
|
+
export const SourceParams = Type.Object({ source: DiffSourceNameSchema });
|
|
57
|
+
export const FileQuery = Type.Object({
|
|
58
|
+
path: Type.String({
|
|
59
|
+
minLength: 1,
|
|
60
|
+
description: 'Repository-relative path after the change.'
|
|
61
|
+
}),
|
|
62
|
+
oldPath: Type.Optional(Type.String({
|
|
63
|
+
minLength: 1,
|
|
64
|
+
description: 'Repository-relative path before a rename.'
|
|
65
|
+
})),
|
|
66
|
+
status: DiffFileStatusSchema
|
|
67
|
+
});
|
|
68
|
+
export const BranchQuery = Type.Object({
|
|
69
|
+
base: Type.String({
|
|
70
|
+
minLength: 1,
|
|
71
|
+
description: 'Ref that the diff compares HEAD with.'
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
export const CommitsQuery = Type.Object({
|
|
75
|
+
from: Type.String({ minLength: 1, description: 'Older ref.' }),
|
|
76
|
+
to: Type.String({ minLength: 1, description: 'Newer ref.' })
|
|
77
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
export const EventStreamSchema = Type.String({
|
|
3
|
+
description: 'Server-sent events. The first event is connected with { instanceId } data. Then come review:created, review:updated, review:deleted, comment:created, comment:updated and comment:deleted with { reviewId } data, and files:changed with {} data.'
|
|
4
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { Nullable } from "../utils/schemas.js";
|
|
3
|
+
export const RepositoryInfoSchema = Type.Object({
|
|
4
|
+
name: Type.String({ description: 'Name of the repository directory.' }),
|
|
5
|
+
branch: Type.String({ description: 'Current branch.' }),
|
|
6
|
+
remote: Nullable(Type.String({
|
|
7
|
+
description: 'Fetch URL of the origin remote. Null if none.'
|
|
8
|
+
})),
|
|
9
|
+
path: Type.String({ description: 'Absolute path of the repository.' })
|
|
10
|
+
}, { description: 'The repository that the server shows.' });
|
|
11
|
+
export const FileTreeResponseSchema = Type.Object({
|
|
12
|
+
ref: Type.String({ description: 'Ref from the request.' }),
|
|
13
|
+
files: Type.Array(Type.String(), {
|
|
14
|
+
description: 'Repository-relative paths of all files at the ref.'
|
|
15
|
+
})
|
|
16
|
+
}, { description: 'Files of the repository at a ref.' });
|
|
17
|
+
export const FileContentResponseSchema = Type.Object({
|
|
18
|
+
path: Type.String({ description: 'Repository-relative path.' }),
|
|
19
|
+
ref: Type.String({ description: 'Ref from the request.' }),
|
|
20
|
+
content: Type.String({
|
|
21
|
+
description: 'Full file text. Empty for a binary file or a file that is not at the ref.'
|
|
22
|
+
}),
|
|
23
|
+
lines: Type.Array(Type.String(), {
|
|
24
|
+
description: 'Lines of the text. Empty for a binary file.'
|
|
25
|
+
}),
|
|
26
|
+
lineCount: Type.Integer({ description: 'Length of lines.' }),
|
|
27
|
+
isBinary: Type.Boolean({ description: 'The file is binary.' })
|
|
28
|
+
}, { description: 'One file at a ref.' });
|
|
29
|
+
export const OkSchema = Type.Object({
|
|
30
|
+
ok: Type.Literal(true, { description: 'Always true.' })
|
|
31
|
+
}, { description: 'The git command succeeded.' });
|
|
32
|
+
export const TreeParams = Type.Object({
|
|
33
|
+
ref: Type.String({
|
|
34
|
+
minLength: 1,
|
|
35
|
+
description: 'Commit, branch or tag. INDEX lists the index, WORKTREE the files on disk.'
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
export const FileParams = Type.Object({
|
|
39
|
+
'*': Type.String({
|
|
40
|
+
description: 'Repository-relative path. Slashes stay literal.'
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
export const FileContentQuery = Type.Object({
|
|
44
|
+
ref: Type.String({
|
|
45
|
+
minLength: 1,
|
|
46
|
+
description: 'Commit, branch or tag. INDEX reads the index, WORKTREE the file on disk.'
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
export const OptionalPathsBody = Type.Object({
|
|
50
|
+
paths: Type.Optional(Type.Array(Type.String(), {
|
|
51
|
+
description: 'Repository-relative paths. Omitted or empty means all.'
|
|
52
|
+
}))
|
|
53
|
+
}, { description: 'Paths for stage or unstage.' });
|
|
54
|
+
export const DiscardBody = Type.Object({
|
|
55
|
+
paths: Type.Array(Type.String(), {
|
|
56
|
+
minItems: 1,
|
|
57
|
+
description: 'Repository-relative paths. Required: there is no "discard all".'
|
|
58
|
+
})
|
|
59
|
+
}, { description: 'Paths to discard.' });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
/** Fields of the error body that Fastify writes for each error. */
|
|
3
|
+
const API_ERROR_FIELDS = {
|
|
4
|
+
statusCode: Type.Integer({ description: 'HTTP status code.' }),
|
|
5
|
+
code: Type.Optional(Type.String({
|
|
6
|
+
description: 'Stable error code, for example GHT_HTTP_NOT_FOUND.'
|
|
7
|
+
})),
|
|
8
|
+
error: Type.String({ description: 'HTTP status text.' }),
|
|
9
|
+
message: Type.String({ description: 'What went wrong.' })
|
|
10
|
+
};
|
|
11
|
+
/** Error answers that every route can give. */
|
|
12
|
+
export const ERROR_RESPONSES = {
|
|
13
|
+
'4xx': Type.Object(API_ERROR_FIELDS, {
|
|
14
|
+
description: 'The request is not valid.'
|
|
15
|
+
}),
|
|
16
|
+
'5xx': Type.Object(API_ERROR_FIELDS, { description: 'The server failed.' })
|
|
17
|
+
};
|
|
18
|
+
export const ApiErrorSchema = Type.Object(API_ERROR_FIELDS, {
|
|
19
|
+
description: 'Error body of every route.'
|
|
20
|
+
});
|
|
21
|
+
export const NoContentSchema = Type.Null({
|
|
22
|
+
description: 'The request succeeded. The body is empty.'
|
|
23
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { Nullable } from "../utils/schemas.js";
|
|
3
|
+
import { CommentSchema } from "../comments/schemas.js";
|
|
4
|
+
export const ReviewStatusSchema = Type.Union([
|
|
5
|
+
Type.Literal('in_progress'),
|
|
6
|
+
Type.Literal('approved'),
|
|
7
|
+
Type.Literal('changes_requested')
|
|
8
|
+
], { description: 'Review state.' });
|
|
9
|
+
/**
|
|
10
|
+
* 'local' snapshots staged + unstaged diffs together — the only source type
|
|
11
|
+
* the "Start review" UI offers, since a review's comments apply to both
|
|
12
|
+
* regardless of which tab they were left on (see ADR 0018). 'staged'/
|
|
13
|
+
* 'unstaged' remain valid API inputs (kept for API callers that want one
|
|
14
|
+
* side only) but have no UI entry point anymore. 'branch'/'commits' are
|
|
15
|
+
* unrelated future-MVP source types (ADR 0017), unaffected by this.
|
|
16
|
+
*/
|
|
17
|
+
export const ReviewSourceTypeSchema = Type.Union([
|
|
18
|
+
Type.Literal('local'),
|
|
19
|
+
Type.Literal('staged'),
|
|
20
|
+
Type.Literal('unstaged'),
|
|
21
|
+
Type.Literal('branch'),
|
|
22
|
+
Type.Literal('commits')
|
|
23
|
+
], {
|
|
24
|
+
description: 'Diff in the snapshot: local (staged and unstaged), one side, a branch or commits.'
|
|
25
|
+
});
|
|
26
|
+
const reviewSummaryFields = {
|
|
27
|
+
id: Type.String({ description: 'Review id.' }),
|
|
28
|
+
repositoryPath: Type.String({
|
|
29
|
+
description: 'Absolute repository path at creation.'
|
|
30
|
+
}),
|
|
31
|
+
baseRef: Nullable(Type.String({
|
|
32
|
+
description: 'Base ref for branch or commits reviews. Else null.'
|
|
33
|
+
})),
|
|
34
|
+
sourceType: ReviewSourceTypeSchema,
|
|
35
|
+
sourceRef: Nullable(Type.String({
|
|
36
|
+
description: 'Branch name for branch, the newer ref ("to") for commits. Else null.'
|
|
37
|
+
})),
|
|
38
|
+
status: ReviewStatusSchema,
|
|
39
|
+
name: Nullable(Type.String({
|
|
40
|
+
description: 'User or generated name ("source + date/time"). Unique within branch.'
|
|
41
|
+
})),
|
|
42
|
+
branch: Nullable(Type.String({
|
|
43
|
+
description: 'Branch at creation. The server does not update it.'
|
|
44
|
+
})),
|
|
45
|
+
createdAt: Type.String({ description: 'Creation time, ISO 8601.' }),
|
|
46
|
+
updatedAt: Type.String({ description: 'Last change time, ISO 8601.' })
|
|
47
|
+
};
|
|
48
|
+
export const ReviewSummarySchema = Type.Object(reviewSummaryFields, {
|
|
49
|
+
description: 'A review without its snapshot.'
|
|
50
|
+
});
|
|
51
|
+
export const ReviewSchema = Type.Object({
|
|
52
|
+
...reviewSummaryFields,
|
|
53
|
+
snapshotData: Type.String({
|
|
54
|
+
description: 'JSON array of DiffFile, frozen at creation.'
|
|
55
|
+
})
|
|
56
|
+
}, { description: 'A review with its diff snapshot.' });
|
|
57
|
+
export const ReviewExportSchema = Type.Object({
|
|
58
|
+
review: ReviewSchema,
|
|
59
|
+
comments: Type.Array(CommentSchema, {
|
|
60
|
+
description: 'All comments of the review.'
|
|
61
|
+
})
|
|
62
|
+
}, { description: 'A review and its comments as JSON.' });
|
|
63
|
+
export const CreateReviewBody = Type.Object({
|
|
64
|
+
sourceType: Type.Optional(ReviewSourceTypeSchema),
|
|
65
|
+
sourceRef: Type.Optional(Type.String({
|
|
66
|
+
description: 'Branch name for branch, or the newer ref ("to") for commits.'
|
|
67
|
+
})),
|
|
68
|
+
baseRef: Type.Optional(Type.String({
|
|
69
|
+
description: 'Required for branch and commits: the base ref or the older ref ("from").'
|
|
70
|
+
})),
|
|
71
|
+
name: Type.Optional(Type.String({
|
|
72
|
+
description: 'Review name. Omitted or blank: the server makes one.'
|
|
73
|
+
}))
|
|
74
|
+
}, { description: 'A new review. sourceType defaults to local.' });
|
|
75
|
+
export const ReviewsQuery = Type.Object({
|
|
76
|
+
branch: Type.Optional(Type.String({ description: 'Branch filter. Default: current branch.' })),
|
|
77
|
+
search: Type.Optional(Type.String({
|
|
78
|
+
description: 'Text to find in review names, case-insensitive.'
|
|
79
|
+
})),
|
|
80
|
+
createdFrom: Type.Optional(Type.String({ description: 'Earliest creation time, ISO 8601.' })),
|
|
81
|
+
createdTo: Type.Optional(Type.String({ description: 'Latest creation time, ISO 8601.' })),
|
|
82
|
+
files: Type.Optional(Type.String({
|
|
83
|
+
description: 'Comma-separated file paths. A review matches if its snapshot has one of them.'
|
|
84
|
+
}))
|
|
85
|
+
});
|
|
86
|
+
export const UpdateReviewBody = Type.Object({
|
|
87
|
+
status: Type.Optional(ReviewStatusSchema)
|
|
88
|
+
}, { description: 'A review change. status is required.' });
|
|
89
|
+
export const ReviewIdParams = Type.Object({
|
|
90
|
+
id: Type.String({ description: 'Review id.' })
|
|
91
|
+
});
|
|
92
|
+
export const ExportQuery = Type.Object({
|
|
93
|
+
format: Type.Union([Type.Literal('json'), Type.Literal('markdown')], {
|
|
94
|
+
description: 'json gives the review and comments, markdown a report.'
|
|
95
|
+
})
|
|
96
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
/**
|
|
3
|
+
* A nullable value for a response schema. Request schemas put `Type.Null()`
|
|
4
|
+
* first instead, because Fastify's Ajv coerces null into the first type.
|
|
5
|
+
*/
|
|
6
|
+
export function Nullable(schema) {
|
|
7
|
+
const { description } = schema;
|
|
8
|
+
return Type.Union([schema, Type.Null()], typeof description === 'string' ? { description } : {});
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{C as e,I as t,N as n,g as r,it as i,l as a,lt as o,nt as s,q as c,s as l,x as u,y as d}from"./runtime-core.esm-bundler-CUpq88z7.js";import{E as f,O as p,_ as m,j as h,t as g}from"./QBtn-BDbJU6_B.js";import{t as _}from"./create-BkZH2Q0U.js";import{n as v}from"./vue-i18n.runtime-D0pkhc7g.js";import{n as y}from"./pinia-DmTi2T44.js";import{c as b,i as x,l as S,o as C,s as w,t as T}from"./single-flight-r8B7stt1.js";function E(e){if(e.length<2||e[0]!==`"`||e[e.length-1]!==`"`)return e;let t=e.slice(1,-1),n=[];for(let e=0;e<t.length;e++){let r=t[e];r===`\\`&&/^[0-7]{3}$/.test(t.slice(e+1,e+4))?(n.push(parseInt(t.slice(e+1,e+4),8)),e+=3):r===`\\`&&t[e+1]===`"`?(n.push(34),e+=1):r===`\\`&&t[e+1]===`\\`?(n.push(92),e+=1):n.push((r??``).charCodeAt(0))}return new TextDecoder(`utf-8`).decode(new Uint8Array(n))}function D(e){return e.newPath||e.oldPath}function O(e,t){return`${e}:${t}`}function k(e){return{...e,oldPath:E(e.oldPath),newPath:E(e.newPath)}}function A(e){let{signature:t,...n}=e;return{...n,hunks:[]}}function j(e,t,n){let r=new Map(e.map(e=>[D(e),e])),i=new Map;return{files:n.map(e=>{let n=D(e);i.set(n,e.signature);let a=r.get(n);return a&&t.get(n)===e.signature?a:A(e)}),signatures:i}}var M=y(`diff`,()=>{let e=s([]),t=s([]),n=new Map,r=new Map,a=s(!1),o=s(!1),c=s(null),u=l(()=>a.value&&!o.value),d=l(()=>[...e.value,...t.value].map(e=>e.newPath||e.oldPath)),f=i(new Map),p=i(new Map),m=new Map;function h(n){return n===`staged`?e.value:t.value}function g(e,t){return f.value.get(O(e,D(t)))?.file}function _(e,t){return f.value.get(O(e,D(t)))?.for===t}function v(e,t){let n=p.value.get(O(e,D(t)));return n?.for===t?n.message:void 0}function y(e,t){let n=O(e,D(t));if(!p.value.has(n))return;let r=new Map(p.value);r.delete(n),p.value=r}function b(e,t){return y(e,t),S(e,t)}async function S(e,t){let n=O(e,D(t));if(_(e,t)||m.get(n)===t)return;m.set(n,t);let r=new URLSearchParams({path:t.newPath,oldPath:t.oldPath,status:t.status});try{let i=k(await w(`/api/diff/${e}/file?${r}`));if(!h(e).includes(t))return;f.value=new Map(f.value).set(n,{file:i,for:t}),y(e,t)}catch(r){if(!h(e).includes(t))return;p.value=new Map(p.value).set(n,{message:x(r),for:t})}finally{m.get(n)===t&&m.delete(n)}}function C(){let n=new Set([...e.value.map(e=>O(`staged`,D(e))),...t.value.map(e=>O(`unstaged`,D(e)))]),r=new Map([...f.value].filter(([e])=>n.has(e)));r.size!==f.value.size&&(f.value=r);let i=new Map([...p.value].filter(([e])=>n.has(e)));i.size!==p.value.size&&(p.value=i)}let E=T(A);async function A(){a.value=!0;try{let[i,a]=await Promise.all([w(`/api/diff/staged/files`),w(`/api/diff/unstaged/files`)]),o=j(e.value,n,i.map(k)),s=j(t.value,r,a.map(k));e.value=o.files,n=o.signatures,t.value=s.files,r=s.signatures,C(),c.value=null}catch(i){o.value||(e.value=[],t.value=[],n=new Map,r=new Map),c.value=x(i)}finally{a.value=!1,o.value=!0}}return{stagedFiles:e,unstagedFiles:t,loading:a,loaded:o,initialLoading:u,error:c,changedPaths:d,hunksOf:g,hunksFresh:_,hunksError:v,clearHunksError:y,ensureHunks:S,retryHunks:b,fetchDiff:E}});function N(e){let{snapshotData:t,...n}=e;return n}var P=y(`active-review`,()=>{let e=s(null),t=s([]),n=s(!1),r=l(()=>{let e=new Map;for(let n of t.value){let t=e.get(n.filePath);t?t.push(n):e.set(n.filePath,[n])}return e});async function i(){n.value=!0;try{let n=(await w(`/api/reviews`)).filter(e=>e.status===`in_progress`).sort((e,t)=>t.createdAt.localeCompare(e.createdAt));e.value=n[0]??null,t.value=e.value?await w(`/api/reviews/${e.value.id}/comments`):[]}finally{n.value=!1}}function a(n){e.value=N(n),t.value=[]}async function o(t){e.value&&(await b(`/api/reviews/${e.value.id}`,{status:t}),await i())}async function c(n){if(!e.value)throw Error(`No active review`);let r=await S(`/api/reviews/${e.value.id}/comments`,n);return t.value=[...t.value,r],r}async function u(e,n){let r=await b(`/api/comments/${e}`,{content:n});t.value=t.value.map(t=>t.id===e?r:t)}async function d(e){await C(`/api/comments/${e}`),t.value=t.value.filter(t=>t.id!==e)}async function f(e){let n=await b(`/api/comments/${e}/resolve`);t.value=t.value.map(t=>t.id===e?n:t)}async function p(e){let n=await b(`/api/comments/${e}/unresolve`);t.value=t.value.map(t=>t.id===e?n:t)}return{activeReview:e,comments:t,loading:n,commentsByFile:r,refresh:i,setActiveReview:a,setStatus:o,createComment:c,editComment:u,deleteComment:d,resolveComment:f,unresolveComment:p}}),F=r({__name:`CancelButton`,setup(t){let{t:r}=v();return(t,i)=>(n(),a(g,e({padding:`2px 8px`},t.$attrs,{dense:``,flat:``,color:`warning`,label:o(r)(`reviews.comments.cancel`)}),null,16,[`label`]))}}),I=r({__name:`CreateButton`,setup(t){let{t:r}=v();return(t,i)=>(n(),a(g,e({padding:`2px 8px`,label:o(r)(`reviews.create.submit`)},t.$attrs,{unelevated:``,dense:``,color:`accent`}),null,16,[`label`]))}}),L=_({name:`QPage`,props:{padding:Boolean,styleFn:Function},setup(e,{slots:t}){let n=f(),r=u(h,p);if(r===p)return console.error(`QPage needs to be a deep child of QLayout`),p;if(u(`_q_pc_`,p)===p)return console.error(`QPage needs to be child of QPageContainer`),p;let i=l(()=>{let t=(r.header.space?r.header.size:0)+(r.footer.space?r.footer.size:0);if(typeof e.styleFn==`function`){let i=r.isContainer.value?r.containerHeight.value:n.screen.height;return e.styleFn(t,i)}return{minHeight:r.isContainer.value?r.containerHeight.value-t+`px`:n.screen.height===0?t===0?`100vh`:`calc(100vh - ${t}px)`:n.screen.height-t+`px`}}),a=l(()=>`q-page${e.padding?` q-layout-padding`:``}`);return()=>d(`main`,{class:a.value,style:i.value},m(t.default))}}),R=r({__name:`AppPage`,setup(e){function r(e,t){return{height:`${t-e}px`}}return(e,i)=>(n(),a(L,{"style-fn":r},{default:c(()=>[t(e.$slots,`default`)]),_:3}))}});export{N as a,D as c,P as i,E as l,I as n,M as o,F as r,O as s,R as t};
|