@tequila99/githuman 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/cli/commands/export.js +55 -0
- package/dist/cli/commands/list.js +39 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/index.js +50 -0
- package/dist/cli/server-runtime.js +66 -0
- package/dist/cli/startup-message.js +7 -0
- package/dist/server/app.js +62 -0
- package/dist/server/db/index.js +17 -0
- package/dist/server/db/migrations.js +34 -0
- package/dist/server/event-bus.js +14 -0
- package/dist/server/repositories/comment.repo.js +52 -0
- package/dist/server/repositories/review.repo.js +41 -0
- package/dist/server/routes/comments.js +114 -0
- package/dist/server/routes/diff.js +39 -0
- package/dist/server/routes/events.js +16 -0
- package/dist/server/routes/export.js +27 -0
- package/dist/server/routes/git.js +100 -0
- package/dist/server/routes/health.js +5 -0
- package/dist/server/routes/reviews.js +90 -0
- package/dist/server/services/comment.service.js +57 -0
- package/dist/server/services/diff.service.js +144 -0
- package/dist/server/services/export.service.js +71 -0
- package/dist/server/services/file-watcher.service.js +124 -0
- package/dist/server/services/git.service.js +219 -0
- package/dist/server/services/review.service.js +71 -0
- package/dist/shared/types.js +4 -0
- package/dist/web/assets/ErrorNotFound-C0A8n_dh.js +1 -0
- package/dist/web/assets/IndexPage-Ca7Xb-lc.js +3 -0
- package/dist/web/assets/IndexPage-Dn15ye8s.css +1 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
- package/dist/web/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
- package/dist/web/assets/MainLayout-CC5JJvA9.css +1 -0
- package/dist/web/assets/MainLayout-DHuQjMOV.js +1 -0
- package/dist/web/assets/QPage-DLIDrL1Z.js +1 -0
- package/dist/web/assets/ReviewsPage-f8fJEuU9.js +1 -0
- package/dist/web/assets/bash-CLZ0U2zV.js +1 -0
- package/dist/web/assets/client-TNhDHmkt.js +1 -0
- package/dist/web/assets/core-Ruj7F7dx.js +12 -0
- package/dist/web/assets/create-C46qMo0y.js +1 -0
- package/dist/web/assets/css-CdtkT07m.js +1 -0
- package/dist/web/assets/css-LbU1hoFO.js +1 -0
- package/dist/web/assets/engine-oniguruma-Bon2Icja.js +1 -0
- package/dist/web/assets/fira-code-cyrillic-400-normal-UC0NFL4U.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-400-normal-dc1q3VD7.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-500-normal-Bu7VCL72.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-500-normal-CX35h3Mg.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-600-normal-BD56RyRT.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-600-normal-CPRdbepc.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-400-normal-Buh61xzB.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-400-normal-txZ9Fk_1.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-500-normal-CYRspHgm.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-500-normal-CwtLmSFD.woff +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-600-normal-B9wE0zmr.woff2 +0 -0
- package/dist/web/assets/fira-code-cyrillic-ext-600-normal-DldcyWhZ.woff +0 -0
- package/dist/web/assets/fira-code-greek-400-normal-B2Gh_Y8s.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-400-normal-DobhaxvF.woff +0 -0
- package/dist/web/assets/fira-code-greek-500-normal-CVO0IRNt.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-500-normal-DEXIWiey.woff +0 -0
- package/dist/web/assets/fira-code-greek-600-normal-D_h3X2X5.woff +0 -0
- package/dist/web/assets/fira-code-greek-600-normal-Dxp52_By.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-ext-400-normal-DR7mBgIM.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-ext-400-normal-DSPUheWg.woff +0 -0
- package/dist/web/assets/fira-code-greek-ext-500-normal-BO3iq16n.woff2 +0 -0
- package/dist/web/assets/fira-code-greek-ext-500-normal-CMiNLXER.woff +0 -0
- package/dist/web/assets/fira-code-greek-ext-600-normal-C-cQDoOk.woff +0 -0
- package/dist/web/assets/fira-code-greek-ext-600-normal-DtBpFRLc.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-400-normal-C-QZfXAs.woff +0 -0
- package/dist/web/assets/fira-code-latin-400-normal-DGosTW8U.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-500-normal-B6gioGu8.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-500-normal-aMLC2AMm.woff +0 -0
- package/dist/web/assets/fira-code-latin-600-normal-BmSSHshu.woff +0 -0
- package/dist/web/assets/fira-code-latin-600-normal-CvYZpPHg.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-ext-400-normal-DG73JjOf.woff +0 -0
- package/dist/web/assets/fira-code-latin-ext-400-normal-KSMg0QLl.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-ext-500-normal-CaANqXjI.woff +0 -0
- package/dist/web/assets/fira-code-latin-ext-500-normal-Crd9e_oI.woff2 +0 -0
- package/dist/web/assets/fira-code-latin-ext-600-normal-0PFQL1eg.woff +0 -0
- package/dist/web/assets/fira-code-latin-ext-600-normal-Cb7jxoAi.woff2 +0 -0
- package/dist/web/assets/fira-code-symbols2-400-normal-_QEZCEwe.woff +0 -0
- package/dist/web/assets/fira-code-symbols2-500-normal-DwsgbIjQ.woff +0 -0
- package/dist/web/assets/fira-code-symbols2-600-normal-Dvq6IQE5.woff +0 -0
- package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/dist/web/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
- package/dist/web/assets/format-SW_H2PBI.js +1 -0
- package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/web/assets/go-rLFTqkRN.js +1 -0
- package/dist/web/assets/html-BcACZWSP.js +1 -0
- package/dist/web/assets/html-DDNsd9lV.js +1 -0
- package/dist/web/assets/i18n-RyrLuvXU.js +1 -0
- package/dist/web/assets/index-CRwZKCt6.js +2 -0
- package/dist/web/assets/index-Fo431P2N.css +1 -0
- package/dist/web/assets/init-app-DSQzny_G.js +1 -0
- package/dist/web/assets/java-BXcb2rRD.js +1 -0
- package/dist/web/assets/javascript-BzbQS41l.js +1 -0
- package/dist/web/assets/javascript-Yelw-5ZN.js +1 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/dist/web/assets/json-B88Oqtu5.js +1 -0
- package/dist/web/assets/json-CaljABEy.js +1 -0
- package/dist/web/assets/jsx-Bz0zcwM4.js +1 -0
- package/dist/web/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/web/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/web/assets/pinia-BDCzxZ18.js +1 -0
- package/dist/web/assets/pinia-BJouyyXU.js +1 -0
- package/dist/web/assets/python-gzcpVVnB.js +1 -0
- package/dist/web/assets/repository-store-DqnwJIJW.js +1 -0
- package/dist/web/assets/runtime-core.esm-bundler-oS9U9Vp7.js +1 -0
- package/dist/web/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/web/assets/scss-DHtrkMkT.js +1 -0
- package/dist/web/assets/syne-greek-400-normal-B4XGRaFa.woff2 +0 -0
- package/dist/web/assets/syne-greek-400-normal-C1nzy5ms.woff +0 -0
- package/dist/web/assets/syne-greek-500-normal-DUQv5E33.woff2 +0 -0
- package/dist/web/assets/syne-greek-500-normal-L1Ji5iNA.woff +0 -0
- package/dist/web/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
- package/dist/web/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
- package/dist/web/assets/syne-greek-700-normal-C01yjWE_.woff +0 -0
- package/dist/web/assets/syne-greek-700-normal-RbvlO3f8.woff2 +0 -0
- package/dist/web/assets/syne-greek-800-normal-DtzFnRR0.woff +0 -0
- package/dist/web/assets/syne-greek-800-normal-n2LZS_yH.woff2 +0 -0
- package/dist/web/assets/syne-latin-400-normal-CULFKXub.woff +0 -0
- package/dist/web/assets/syne-latin-400-normal-CcHADMBd.woff2 +0 -0
- package/dist/web/assets/syne-latin-500-normal-DR6mUQpY.woff +0 -0
- package/dist/web/assets/syne-latin-500-normal-emxikWQ7.woff2 +0 -0
- package/dist/web/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
- package/dist/web/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
- package/dist/web/assets/syne-latin-700-normal-AF3Rs61n.woff2 +0 -0
- package/dist/web/assets/syne-latin-700-normal-Cd-_iCn5.woff +0 -0
- package/dist/web/assets/syne-latin-800-normal-BuJcbPcX.woff +0 -0
- package/dist/web/assets/syne-latin-800-normal-EehdNYzx.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-400-normal-B5s6XpS5.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-400-normal-DCnt6qEA.woff +0 -0
- package/dist/web/assets/syne-latin-ext-500-normal-C5b4wHnS.woff +0 -0
- package/dist/web/assets/syne-latin-ext-500-normal-DHUXVE3f.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
- package/dist/web/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-700-normal-C-qCH-vM.woff +0 -0
- package/dist/web/assets/syne-latin-ext-700-normal-DuyXglMe.woff2 +0 -0
- package/dist/web/assets/syne-latin-ext-800-normal-B2iBRfEf.woff +0 -0
- package/dist/web/assets/syne-latin-ext-800-normal-CAQAsWuS.woff2 +0 -0
- package/dist/web/assets/theme-DUV6mF_4.js +1 -0
- package/dist/web/assets/tsx-udAQXfEw.js +1 -0
- package/dist/web/assets/typescript-Utq2Cl8c.js +1 -0
- package/dist/web/assets/typescript-j_1H8WHN.js +1 -0
- package/dist/web/assets/use-app-theme-DrR0AvTg.js +1 -0
- package/dist/web/assets/vue-CSBlZ1UP.js +1 -0
- package/dist/web/assets/vue-i18n.runtime-nPdXRlNg.js +3 -0
- package/dist/web/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/web/assets/yaml-rwi0_p6S.js +1 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/icons/favicon-128x128.png +0 -0
- package/dist/web/icons/favicon-16x16.png +0 -0
- package/dist/web/icons/favicon-32x32.png +0 -0
- package/dist/web/icons/favicon-96x96.png +0 -0
- package/dist/web/index.html +5 -0
- package/package.json +89 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function rowToReview(row) {
|
|
2
|
+
return {
|
|
3
|
+
id: row.id,
|
|
4
|
+
repositoryPath: row.repository_path,
|
|
5
|
+
baseRef: row.base_ref,
|
|
6
|
+
sourceType: row.source_type,
|
|
7
|
+
sourceRef: row.source_ref,
|
|
8
|
+
snapshotData: row.snapshot_data,
|
|
9
|
+
status: row.status,
|
|
10
|
+
createdAt: row.created_at,
|
|
11
|
+
updatedAt: row.updated_at
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createReview(db, review) {
|
|
15
|
+
db.prepare(`INSERT INTO reviews (id, repository_path, base_ref, source_type, source_ref, snapshot_data, status, created_at, updated_at)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(review.id, review.repositoryPath, review.baseRef, review.sourceType, review.sourceRef, review.snapshotData, review.status, review.createdAt, review.updatedAt);
|
|
17
|
+
return review;
|
|
18
|
+
}
|
|
19
|
+
export function findReviewById(db, id) {
|
|
20
|
+
const row = db.prepare('SELECT * FROM reviews WHERE id = ?').get(id);
|
|
21
|
+
return row ? rowToReview(row) : null;
|
|
22
|
+
}
|
|
23
|
+
export function listReviews(db) {
|
|
24
|
+
const rows = db
|
|
25
|
+
.prepare('SELECT * FROM reviews ORDER BY created_at DESC')
|
|
26
|
+
.all();
|
|
27
|
+
return rows.map(rowToReview);
|
|
28
|
+
}
|
|
29
|
+
export function deleteReview(db, id) {
|
|
30
|
+
db.prepare('DELETE FROM reviews WHERE id = ?').run(id);
|
|
31
|
+
}
|
|
32
|
+
export function updateReviewStatus(db, id, status) {
|
|
33
|
+
const updatedAt = new Date().toISOString();
|
|
34
|
+
const result = db
|
|
35
|
+
.prepare('UPDATE reviews SET status = ?, updated_at = ? WHERE id = ?')
|
|
36
|
+
.run(status, updatedAt, id);
|
|
37
|
+
if (result.changes === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return findReviewById(db, id);
|
|
41
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { getReview } from "../services/review.service.js";
|
|
3
|
+
import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment, ValidationError } from "../services/comment.service.js";
|
|
4
|
+
const CreateCommentBody = Type.Object({
|
|
5
|
+
filePath: Type.String({ minLength: 1 }),
|
|
6
|
+
lineNumber: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
|
|
7
|
+
lineType: Type.Optional(Type.Union([
|
|
8
|
+
Type.Literal('added'),
|
|
9
|
+
Type.Literal('removed'),
|
|
10
|
+
Type.Literal('context'),
|
|
11
|
+
Type.Null()
|
|
12
|
+
])),
|
|
13
|
+
content: Type.String(),
|
|
14
|
+
suggestion: Type.Optional(Type.Union([Type.String(), Type.Null()]))
|
|
15
|
+
});
|
|
16
|
+
const UpdateCommentBody = Type.Object({
|
|
17
|
+
content: Type.String()
|
|
18
|
+
});
|
|
19
|
+
const ReviewIdParams = Type.Object({ id: Type.String() });
|
|
20
|
+
const CommentIdParams = Type.Object({ id: Type.String() });
|
|
21
|
+
export async function commentRoutes(app, opts) {
|
|
22
|
+
const { db, eventBus } = opts;
|
|
23
|
+
const typedApp = app.withTypeProvider();
|
|
24
|
+
typedApp.post('/api/reviews/:id/comments', { schema: { params: ReviewIdParams, body: CreateCommentBody } }, async (request, reply) => {
|
|
25
|
+
if (!getReview(db, request.params.id)) {
|
|
26
|
+
reply.code(404);
|
|
27
|
+
return {
|
|
28
|
+
error: 'Not Found',
|
|
29
|
+
message: `Review ${request.params.id} not found`,
|
|
30
|
+
statusCode: 404
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const comment = createComment(db, request.params.id, request.body, eventBus);
|
|
35
|
+
reply.code(201);
|
|
36
|
+
return comment;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof ValidationError) {
|
|
40
|
+
reply.code(400);
|
|
41
|
+
return {
|
|
42
|
+
error: 'Bad Request',
|
|
43
|
+
message: error.message,
|
|
44
|
+
statusCode: 400
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request, reply) => {
|
|
51
|
+
if (!getReview(db, request.params.id)) {
|
|
52
|
+
reply.code(404);
|
|
53
|
+
return {
|
|
54
|
+
error: 'Not Found',
|
|
55
|
+
message: `Review ${request.params.id} not found`,
|
|
56
|
+
statusCode: 404
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return getComments(db, request.params.id);
|
|
60
|
+
});
|
|
61
|
+
typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request, reply) => {
|
|
62
|
+
try {
|
|
63
|
+
const updated = editComment(db, request.params.id, request.body.content, eventBus);
|
|
64
|
+
if (!updated) {
|
|
65
|
+
reply.code(404);
|
|
66
|
+
return {
|
|
67
|
+
error: 'Not Found',
|
|
68
|
+
message: `Comment ${request.params.id} not found`,
|
|
69
|
+
statusCode: 404
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return updated;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof ValidationError) {
|
|
76
|
+
reply.code(400);
|
|
77
|
+
return {
|
|
78
|
+
error: 'Bad Request',
|
|
79
|
+
message: error.message,
|
|
80
|
+
statusCode: 400
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
|
|
87
|
+
const updated = resolveComment(db, request.params.id, eventBus);
|
|
88
|
+
if (!updated) {
|
|
89
|
+
reply.code(404);
|
|
90
|
+
return {
|
|
91
|
+
error: 'Not Found',
|
|
92
|
+
message: `Comment ${request.params.id} not found`,
|
|
93
|
+
statusCode: 404
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return updated;
|
|
97
|
+
});
|
|
98
|
+
typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
|
|
99
|
+
const updated = unresolveComment(db, request.params.id, eventBus);
|
|
100
|
+
if (!updated) {
|
|
101
|
+
reply.code(404);
|
|
102
|
+
return {
|
|
103
|
+
error: 'Not Found',
|
|
104
|
+
message: `Comment ${request.params.id} not found`,
|
|
105
|
+
statusCode: 404
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return updated;
|
|
109
|
+
});
|
|
110
|
+
typedApp.delete('/api/comments/:id', { schema: { params: CommentIdParams } }, async (request, reply) => {
|
|
111
|
+
removeComment(db, request.params.id, eventBus);
|
|
112
|
+
reply.code(204);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { getStagedDiff, getUnstagedDiff, getBranchDiff, getCommitsDiff } from "../services/diff.service.js";
|
|
3
|
+
const BranchQuery = Type.Object({
|
|
4
|
+
base: Type.String({ minLength: 1 })
|
|
5
|
+
});
|
|
6
|
+
const CommitsQuery = Type.Object({
|
|
7
|
+
from: Type.String({ minLength: 1 }),
|
|
8
|
+
to: Type.String({ minLength: 1 })
|
|
9
|
+
});
|
|
10
|
+
export async function diffRoutes(app, opts) {
|
|
11
|
+
const { repositoryPath } = opts;
|
|
12
|
+
const typedApp = app.withTypeProvider();
|
|
13
|
+
typedApp.get('/api/diff/staged', async () => {
|
|
14
|
+
return getStagedDiff(repositoryPath);
|
|
15
|
+
});
|
|
16
|
+
typedApp.get('/api/diff/unstaged', async () => {
|
|
17
|
+
return getUnstagedDiff(repositoryPath);
|
|
18
|
+
});
|
|
19
|
+
typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request, reply) => {
|
|
20
|
+
try {
|
|
21
|
+
return await getBranchDiff(repositoryPath, request.query.base);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
reply.code(400);
|
|
25
|
+
const message = err instanceof Error ? err.message : 'Bad ref';
|
|
26
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request, reply) => {
|
|
30
|
+
try {
|
|
31
|
+
return await getCommitsDiff(repositoryPath, request.query.from, request.query.to);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
reply.code(400);
|
|
35
|
+
const message = err instanceof Error ? err.message : 'Bad ref';
|
|
36
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function eventRoutes(app, opts) {
|
|
2
|
+
const { eventBus } = opts;
|
|
3
|
+
app.get('/api/events', { sse: true }, async (request, reply) => {
|
|
4
|
+
reply.sse.keepAlive();
|
|
5
|
+
await reply.sse.send({ event: 'connected', data: {} });
|
|
6
|
+
const unsubscribe = eventBus.subscribe(event => {
|
|
7
|
+
void reply.sse.send({
|
|
8
|
+
event: event.type,
|
|
9
|
+
data: { reviewId: event.reviewId }
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
reply.sse.onClose(() => {
|
|
13
|
+
unsubscribe();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { exportAsJson, exportAsMarkdown, ExportNotFoundError } from "../services/export.service.js";
|
|
3
|
+
const ExportParams = Type.Object({ id: Type.String() });
|
|
4
|
+
const ExportQuery = Type.Object({
|
|
5
|
+
format: Type.Union([Type.Literal('json'), Type.Literal('markdown')])
|
|
6
|
+
});
|
|
7
|
+
export async function exportRoutes(app, opts) {
|
|
8
|
+
const { db } = opts;
|
|
9
|
+
const typedApp = app.withTypeProvider();
|
|
10
|
+
typedApp.get('/api/reviews/:id/export', { schema: { params: ExportParams, querystring: ExportQuery } }, async (request, reply) => {
|
|
11
|
+
try {
|
|
12
|
+
if (request.query.format === 'json') {
|
|
13
|
+
return exportAsJson(db, request.params.id);
|
|
14
|
+
}
|
|
15
|
+
const markdown = exportAsMarkdown(db, request.params.id);
|
|
16
|
+
reply.type('text/markdown');
|
|
17
|
+
return markdown;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof ExportNotFoundError) {
|
|
21
|
+
reply.code(404);
|
|
22
|
+
return { error: 'Not Found', message: error.message, statusCode: 404 };
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { getRepositoryInfo, stagePaths, unstagePaths, discardPaths, getFilesAtRef, getFileAtRef } from "../services/git.service.js";
|
|
3
|
+
const TreeParams = Type.Object({
|
|
4
|
+
ref: Type.String({ minLength: 1 })
|
|
5
|
+
});
|
|
6
|
+
const FileContentQuery = Type.Object({
|
|
7
|
+
ref: Type.String({ minLength: 1 })
|
|
8
|
+
});
|
|
9
|
+
/** Body for `/stage` and `/unstage`: an omitted/empty `paths` means "all". */
|
|
10
|
+
const OptionalPathsBody = Type.Object({
|
|
11
|
+
paths: Type.Optional(Type.Array(Type.String()))
|
|
12
|
+
});
|
|
13
|
+
/** Body for `/discard`: `paths` is required — no "discard everything". */
|
|
14
|
+
const DiscardBody = Type.Object({
|
|
15
|
+
paths: Type.Array(Type.String(), { minItems: 1 })
|
|
16
|
+
});
|
|
17
|
+
export async function gitRoutes(app, opts) {
|
|
18
|
+
const { repositoryPath } = opts;
|
|
19
|
+
const typedApp = app.withTypeProvider();
|
|
20
|
+
app.get('/api/git/info', async () => {
|
|
21
|
+
return getRepositoryInfo(repositoryPath);
|
|
22
|
+
});
|
|
23
|
+
typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
|
|
24
|
+
try {
|
|
25
|
+
await stagePaths(repositoryPath, request.body.paths ?? []);
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
reply.code(400);
|
|
30
|
+
const message = err instanceof Error ? err.message : 'Failed to stage';
|
|
31
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
|
|
35
|
+
try {
|
|
36
|
+
await unstagePaths(repositoryPath, request.body.paths ?? []);
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
reply.code(400);
|
|
41
|
+
const message = err instanceof Error ? err.message : 'Failed to unstage';
|
|
42
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request, reply) => {
|
|
46
|
+
try {
|
|
47
|
+
await discardPaths(repositoryPath, request.body.paths);
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
reply.code(400);
|
|
52
|
+
const message = err instanceof Error ? err.message : 'Failed to discard changes';
|
|
53
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request, reply) => {
|
|
57
|
+
const { ref } = request.params;
|
|
58
|
+
try {
|
|
59
|
+
const files = await getFilesAtRef(repositoryPath, ref);
|
|
60
|
+
return { ref, files };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
reply.code(400);
|
|
64
|
+
const message = err instanceof Error ? err.message : 'Failed to list files';
|
|
65
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request, reply) => {
|
|
69
|
+
const filePath = request.params['*'];
|
|
70
|
+
const { ref } = request.query;
|
|
71
|
+
if (!filePath) {
|
|
72
|
+
reply.code(400);
|
|
73
|
+
return {
|
|
74
|
+
error: 'Bad Request',
|
|
75
|
+
message: 'File path is required',
|
|
76
|
+
statusCode: 400
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let content;
|
|
80
|
+
let isBinary;
|
|
81
|
+
try {
|
|
82
|
+
;
|
|
83
|
+
({ content, isBinary } = await getFileAtRef(repositoryPath, ref, filePath));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
reply.code(400);
|
|
87
|
+
const message = err instanceof Error ? err.message : 'Failed to read file';
|
|
88
|
+
return { error: 'Bad Request', message, statusCode: 400 };
|
|
89
|
+
}
|
|
90
|
+
const lines = isBinary ? [] : content.split('\n');
|
|
91
|
+
return {
|
|
92
|
+
path: filePath,
|
|
93
|
+
ref,
|
|
94
|
+
content,
|
|
95
|
+
lines,
|
|
96
|
+
lineCount: lines.length,
|
|
97
|
+
isBinary
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { createReview, getReview, getReviews, setReviewStatus, removeReview, ValidationError } from "../services/review.service.js";
|
|
3
|
+
const CreateReviewBody = Type.Object({
|
|
4
|
+
sourceType: Type.Optional(Type.Union([
|
|
5
|
+
Type.Literal('staged'),
|
|
6
|
+
Type.Literal('unstaged'),
|
|
7
|
+
Type.Literal('branch'),
|
|
8
|
+
Type.Literal('commits')
|
|
9
|
+
])),
|
|
10
|
+
sourceRef: Type.Optional(Type.String()),
|
|
11
|
+
baseRef: Type.Optional(Type.String())
|
|
12
|
+
});
|
|
13
|
+
const UpdateReviewBody = Type.Object({
|
|
14
|
+
status: Type.Optional(Type.Union([
|
|
15
|
+
Type.Literal('in_progress'),
|
|
16
|
+
Type.Literal('approved'),
|
|
17
|
+
Type.Literal('changes_requested')
|
|
18
|
+
]))
|
|
19
|
+
});
|
|
20
|
+
const ReviewIdParams = Type.Object({ id: Type.String() });
|
|
21
|
+
export async function reviewRoutes(app, opts) {
|
|
22
|
+
const { repositoryPath, db, eventBus } = opts;
|
|
23
|
+
const typedApp = app.withTypeProvider();
|
|
24
|
+
typedApp.post('/api/reviews', { schema: { body: CreateReviewBody } }, async (request, reply) => {
|
|
25
|
+
try {
|
|
26
|
+
const review = await createReview(db, repositoryPath, request.body, eventBus);
|
|
27
|
+
reply.code(201);
|
|
28
|
+
return review;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof ValidationError) {
|
|
32
|
+
reply.code(400);
|
|
33
|
+
return {
|
|
34
|
+
error: 'Bad Request',
|
|
35
|
+
message: error.message,
|
|
36
|
+
statusCode: 400
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
typedApp.get('/api/reviews', async () => {
|
|
43
|
+
return getReviews(db);
|
|
44
|
+
});
|
|
45
|
+
typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
|
|
46
|
+
const review = getReview(db, request.params.id);
|
|
47
|
+
if (!review) {
|
|
48
|
+
reply.code(404);
|
|
49
|
+
return {
|
|
50
|
+
error: 'Not Found',
|
|
51
|
+
message: `Review ${request.params.id} not found`,
|
|
52
|
+
statusCode: 404
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return review;
|
|
56
|
+
});
|
|
57
|
+
typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request, reply) => {
|
|
58
|
+
if (!request.body.status) {
|
|
59
|
+
reply.code(400);
|
|
60
|
+
return {
|
|
61
|
+
error: 'Bad Request',
|
|
62
|
+
message: '"status" is required',
|
|
63
|
+
statusCode: 400
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const updated = setReviewStatus(db, request.params.id, request.body.status, eventBus);
|
|
67
|
+
if (!updated) {
|
|
68
|
+
reply.code(404);
|
|
69
|
+
return {
|
|
70
|
+
error: 'Not Found',
|
|
71
|
+
message: `Review ${request.params.id} not found`,
|
|
72
|
+
statusCode: 404
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return updated;
|
|
76
|
+
});
|
|
77
|
+
typedApp.delete('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
|
|
78
|
+
if (!getReview(db, request.params.id)) {
|
|
79
|
+
reply.code(404);
|
|
80
|
+
return {
|
|
81
|
+
error: 'Not Found',
|
|
82
|
+
message: `Review ${request.params.id} not found`,
|
|
83
|
+
statusCode: 404
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
removeReview(db, request.params.id, eventBus);
|
|
87
|
+
reply.code(204);
|
|
88
|
+
return null;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createComment as insertComment, findCommentById, listCommentsByReview, updateComment as updateCommentRepo, deleteComment as deleteCommentRepo, setCommentResolved } from "../repositories/comment.repo.js";
|
|
2
|
+
export class ValidationError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export function createComment(db, reviewId, input, eventBus) {
|
|
5
|
+
if (!input.content) {
|
|
6
|
+
throw new ValidationError('"content" is required');
|
|
7
|
+
}
|
|
8
|
+
const now = new Date().toISOString();
|
|
9
|
+
const comment = {
|
|
10
|
+
id: crypto.randomUUID(),
|
|
11
|
+
reviewId,
|
|
12
|
+
filePath: input.filePath,
|
|
13
|
+
lineNumber: input.lineNumber ?? null,
|
|
14
|
+
lineType: input.lineType ?? null,
|
|
15
|
+
content: input.content,
|
|
16
|
+
createdAt: now,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
suggestion: input.suggestion ?? null
|
|
19
|
+
};
|
|
20
|
+
const created = insertComment(db, comment);
|
|
21
|
+
eventBus?.publish({ type: 'comment:created', reviewId });
|
|
22
|
+
return created;
|
|
23
|
+
}
|
|
24
|
+
export function getComments(db, reviewId) {
|
|
25
|
+
return listCommentsByReview(db, reviewId);
|
|
26
|
+
}
|
|
27
|
+
export function editComment(db, id, content, eventBus) {
|
|
28
|
+
if (!content) {
|
|
29
|
+
throw new ValidationError('"content" is required');
|
|
30
|
+
}
|
|
31
|
+
const updated = updateCommentRepo(db, id, content);
|
|
32
|
+
if (updated) {
|
|
33
|
+
eventBus?.publish({ type: 'comment:updated', reviewId: updated.reviewId });
|
|
34
|
+
}
|
|
35
|
+
return updated;
|
|
36
|
+
}
|
|
37
|
+
export function resolveComment(db, id, eventBus) {
|
|
38
|
+
const updated = setCommentResolved(db, id, true);
|
|
39
|
+
if (updated) {
|
|
40
|
+
eventBus?.publish({ type: 'comment:updated', reviewId: updated.reviewId });
|
|
41
|
+
}
|
|
42
|
+
return updated;
|
|
43
|
+
}
|
|
44
|
+
export function unresolveComment(db, id, eventBus) {
|
|
45
|
+
const updated = setCommentResolved(db, id, false);
|
|
46
|
+
if (updated) {
|
|
47
|
+
eventBus?.publish({ type: 'comment:updated', reviewId: updated.reviewId });
|
|
48
|
+
}
|
|
49
|
+
return updated;
|
|
50
|
+
}
|
|
51
|
+
export function removeComment(db, id, eventBus) {
|
|
52
|
+
const comment = findCommentById(db, id);
|
|
53
|
+
deleteCommentRepo(db, id);
|
|
54
|
+
if (comment) {
|
|
55
|
+
eventBus?.publish({ type: 'comment:deleted', reviewId: comment.reviewId });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { diffLines } from 'diff';
|
|
2
|
+
import { assertSafeRef, getFileAtRef, listChangedPaths, listUntrackedPaths } from "./git.service.js";
|
|
3
|
+
function splitLines(text) {
|
|
4
|
+
if (text === '')
|
|
5
|
+
return [];
|
|
6
|
+
const lines = text.split('\n');
|
|
7
|
+
if (lines[lines.length - 1] === '')
|
|
8
|
+
lines.pop();
|
|
9
|
+
return lines;
|
|
10
|
+
}
|
|
11
|
+
const CONTEXT_LINES = 3;
|
|
12
|
+
function windowHunks(lines, contextLines = CONTEXT_LINES) {
|
|
13
|
+
if (lines.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const keep = Array.from({ length: lines.length }).fill(false);
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
if (lines[i].type === 'context')
|
|
18
|
+
continue;
|
|
19
|
+
for (let j = Math.max(0, i - contextLines); j <= Math.min(lines.length - 1, i + contextLines); j++) {
|
|
20
|
+
keep[j] = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const hunks = [];
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < lines.length) {
|
|
26
|
+
if (!keep[i]) {
|
|
27
|
+
i += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
let j = i;
|
|
31
|
+
while (j < lines.length && keep[j])
|
|
32
|
+
j += 1;
|
|
33
|
+
const segment = lines.slice(i, j);
|
|
34
|
+
const oldNumbers = segment
|
|
35
|
+
.map(l => l.oldLineNumber)
|
|
36
|
+
.filter((n) => n !== null);
|
|
37
|
+
const newNumbers = segment
|
|
38
|
+
.map(l => l.newLineNumber)
|
|
39
|
+
.filter((n) => n !== null);
|
|
40
|
+
hunks.push({
|
|
41
|
+
oldStart: oldNumbers.length > 0 ? oldNumbers[0] : 0,
|
|
42
|
+
oldLines: oldNumbers.length,
|
|
43
|
+
newStart: newNumbers.length > 0 ? newNumbers[0] : 0,
|
|
44
|
+
newLines: newNumbers.length,
|
|
45
|
+
lines: segment
|
|
46
|
+
});
|
|
47
|
+
i = j;
|
|
48
|
+
}
|
|
49
|
+
return hunks;
|
|
50
|
+
}
|
|
51
|
+
export function computeFileDiff(oldContent, newContent, meta) {
|
|
52
|
+
const base = {
|
|
53
|
+
oldPath: meta.oldPath,
|
|
54
|
+
newPath: meta.newPath,
|
|
55
|
+
status: meta.status,
|
|
56
|
+
isBinary: meta.isBinary
|
|
57
|
+
};
|
|
58
|
+
if (meta.isBinary) {
|
|
59
|
+
return { ...base, additions: 0, deletions: 0, hunks: [] };
|
|
60
|
+
}
|
|
61
|
+
if (oldContent === newContent) {
|
|
62
|
+
return { ...base, additions: 0, deletions: 0, hunks: [] };
|
|
63
|
+
}
|
|
64
|
+
const parts = diffLines(oldContent, newContent);
|
|
65
|
+
let oldLineNumber = 1;
|
|
66
|
+
let newLineNumber = 1;
|
|
67
|
+
let additions = 0;
|
|
68
|
+
let deletions = 0;
|
|
69
|
+
const lines = [];
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
for (const content of splitLines(part.value)) {
|
|
72
|
+
if (part.added) {
|
|
73
|
+
lines.push({
|
|
74
|
+
type: 'added',
|
|
75
|
+
content,
|
|
76
|
+
oldLineNumber: null,
|
|
77
|
+
newLineNumber
|
|
78
|
+
});
|
|
79
|
+
newLineNumber += 1;
|
|
80
|
+
additions += 1;
|
|
81
|
+
}
|
|
82
|
+
else if (part.removed) {
|
|
83
|
+
lines.push({
|
|
84
|
+
type: 'removed',
|
|
85
|
+
content,
|
|
86
|
+
oldLineNumber,
|
|
87
|
+
newLineNumber: null
|
|
88
|
+
});
|
|
89
|
+
oldLineNumber += 1;
|
|
90
|
+
deletions += 1;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
lines.push({ type: 'context', content, oldLineNumber, newLineNumber });
|
|
94
|
+
oldLineNumber += 1;
|
|
95
|
+
newLineNumber += 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const hunks = windowHunks(lines);
|
|
100
|
+
return { ...base, additions, deletions, hunks };
|
|
101
|
+
}
|
|
102
|
+
async function buildDiffFiles(repoPath, diffArgs, oldRef, newRef) {
|
|
103
|
+
const changes = await listChangedPaths(repoPath, diffArgs);
|
|
104
|
+
return Promise.all(changes.map(async (change) => {
|
|
105
|
+
const [oldFile, newFile] = await Promise.all([
|
|
106
|
+
getFileAtRef(repoPath, oldRef, change.oldPath),
|
|
107
|
+
getFileAtRef(repoPath, newRef, change.newPath)
|
|
108
|
+
]);
|
|
109
|
+
return computeFileDiff(oldFile.content, newFile.content, {
|
|
110
|
+
oldPath: change.oldPath,
|
|
111
|
+
newPath: change.newPath,
|
|
112
|
+
status: change.status,
|
|
113
|
+
isBinary: oldFile.isBinary || newFile.isBinary
|
|
114
|
+
});
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
export async function getStagedDiff(repoPath) {
|
|
118
|
+
return buildDiffFiles(repoPath, ['--cached'], 'HEAD', 'INDEX');
|
|
119
|
+
}
|
|
120
|
+
export async function getUnstagedDiff(repoPath) {
|
|
121
|
+
const [trackedFiles, untrackedPaths] = await Promise.all([
|
|
122
|
+
buildDiffFiles(repoPath, [], 'INDEX', 'WORKTREE'),
|
|
123
|
+
listUntrackedPaths(repoPath)
|
|
124
|
+
]);
|
|
125
|
+
const untrackedFiles = await Promise.all(untrackedPaths.map(async (path) => {
|
|
126
|
+
const worktreeFile = await getFileAtRef(repoPath, 'WORKTREE', path);
|
|
127
|
+
return computeFileDiff('', worktreeFile.content, {
|
|
128
|
+
oldPath: path,
|
|
129
|
+
newPath: path,
|
|
130
|
+
status: 'added',
|
|
131
|
+
isBinary: worktreeFile.isBinary
|
|
132
|
+
});
|
|
133
|
+
}));
|
|
134
|
+
return [...trackedFiles, ...untrackedFiles];
|
|
135
|
+
}
|
|
136
|
+
export async function getBranchDiff(repoPath, base) {
|
|
137
|
+
assertSafeRef(base);
|
|
138
|
+
return buildDiffFiles(repoPath, [base, 'HEAD'], base, 'HEAD');
|
|
139
|
+
}
|
|
140
|
+
export async function getCommitsDiff(repoPath, from, to) {
|
|
141
|
+
assertSafeRef(from);
|
|
142
|
+
assertSafeRef(to);
|
|
143
|
+
return buildDiffFiles(repoPath, [from, to], from, to);
|
|
144
|
+
}
|