@sanurb/ringi 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 +674 -0
- package/README.md +378 -0
- package/dist/chunk-3JLVANJR.js +1782 -0
- package/dist/chunk-3JLVANJR.js.map +1 -0
- package/dist/cli.js +1770 -0
- package/dist/cli.js.map +1 -0
- package/dist/mcp.js +1228 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +124 -0
|
@@ -0,0 +1,1782 @@
|
|
|
1
|
+
import * as HttpApiSchema from '@effect/platform/HttpApiSchema';
|
|
2
|
+
import * as Schema3 from 'effect/Schema';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import * as Effect4 from 'effect/Effect';
|
|
5
|
+
import * as Option from 'effect/Option';
|
|
6
|
+
import { mkdirSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, relative } from 'node:path';
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
import * as Config from 'effect/Config';
|
|
10
|
+
import * as Exit from 'effect/Exit';
|
|
11
|
+
import { execFile } from 'node:child_process';
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import * as Layer from 'effect/Layer';
|
|
14
|
+
import 'effect/ManagedRuntime';
|
|
15
|
+
import { platform } from 'node:os';
|
|
16
|
+
import chokidar from 'chokidar';
|
|
17
|
+
import * as Queue from 'effect/Queue';
|
|
18
|
+
import * as Runtime from 'effect/Runtime';
|
|
19
|
+
import * as Stream from 'effect/Stream';
|
|
20
|
+
|
|
21
|
+
// src/api/schemas/review.ts
|
|
22
|
+
var ReviewId = Schema3.String.pipe(Schema3.brand("ReviewId"));
|
|
23
|
+
var ReviewStatus = Schema3.Literal(
|
|
24
|
+
"in_progress",
|
|
25
|
+
"approved",
|
|
26
|
+
"changes_requested"
|
|
27
|
+
);
|
|
28
|
+
var ReviewSourceType = Schema3.Literal("staged", "branch", "commits");
|
|
29
|
+
Schema3.Struct({
|
|
30
|
+
baseRef: Schema3.NullOr(Schema3.String),
|
|
31
|
+
createdAt: Schema3.String,
|
|
32
|
+
id: ReviewId,
|
|
33
|
+
repositoryPath: Schema3.String,
|
|
34
|
+
snapshotData: Schema3.String,
|
|
35
|
+
sourceRef: Schema3.NullOr(Schema3.String),
|
|
36
|
+
sourceType: ReviewSourceType,
|
|
37
|
+
status: ReviewStatus,
|
|
38
|
+
updatedAt: Schema3.String
|
|
39
|
+
});
|
|
40
|
+
Schema3.Struct({
|
|
41
|
+
sourceRef: Schema3.optionalWith(Schema3.NullOr(Schema3.String), {
|
|
42
|
+
default: () => null
|
|
43
|
+
}),
|
|
44
|
+
sourceType: Schema3.optionalWith(ReviewSourceType, {
|
|
45
|
+
default: () => "staged"
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
Schema3.Struct({
|
|
49
|
+
status: Schema3.optionalWith(ReviewStatus, { as: "Option" })
|
|
50
|
+
});
|
|
51
|
+
var ReviewNotFound = class extends Schema3.TaggedError()(
|
|
52
|
+
"ReviewNotFound",
|
|
53
|
+
{ id: ReviewId },
|
|
54
|
+
HttpApiSchema.annotations({ status: 404 })
|
|
55
|
+
) {
|
|
56
|
+
};
|
|
57
|
+
var TodoId = Schema3.String.pipe(Schema3.brand("TodoId"));
|
|
58
|
+
Schema3.Struct({
|
|
59
|
+
completed: Schema3.Boolean,
|
|
60
|
+
content: Schema3.String,
|
|
61
|
+
createdAt: Schema3.String,
|
|
62
|
+
id: TodoId,
|
|
63
|
+
position: Schema3.Number,
|
|
64
|
+
reviewId: Schema3.NullOr(ReviewId),
|
|
65
|
+
updatedAt: Schema3.String
|
|
66
|
+
});
|
|
67
|
+
Schema3.Struct({
|
|
68
|
+
content: Schema3.String.pipe(Schema3.minLength(1)),
|
|
69
|
+
reviewId: Schema3.optionalWith(Schema3.NullOr(ReviewId), {
|
|
70
|
+
default: () => null
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
Schema3.Struct({
|
|
74
|
+
completed: Schema3.optionalWith(Schema3.Boolean, { as: "Option" }),
|
|
75
|
+
content: Schema3.optionalWith(Schema3.String.pipe(Schema3.minLength(1)), {
|
|
76
|
+
as: "Option"
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
var TodoNotFound = class extends Schema3.TaggedError()(
|
|
80
|
+
"TodoNotFound",
|
|
81
|
+
{ id: TodoId },
|
|
82
|
+
HttpApiSchema.annotations({ status: 404 })
|
|
83
|
+
) {
|
|
84
|
+
};
|
|
85
|
+
var CommentId = Schema3.String.pipe(Schema3.brand("CommentId"));
|
|
86
|
+
var LineType = Schema3.Literal("added", "removed", "context");
|
|
87
|
+
Schema3.Struct({
|
|
88
|
+
content: Schema3.String,
|
|
89
|
+
createdAt: Schema3.String,
|
|
90
|
+
filePath: Schema3.String,
|
|
91
|
+
id: CommentId,
|
|
92
|
+
lineNumber: Schema3.NullOr(Schema3.Number),
|
|
93
|
+
lineType: Schema3.NullOr(LineType),
|
|
94
|
+
resolved: Schema3.Boolean,
|
|
95
|
+
reviewId: ReviewId,
|
|
96
|
+
suggestion: Schema3.NullOr(Schema3.String),
|
|
97
|
+
updatedAt: Schema3.String
|
|
98
|
+
});
|
|
99
|
+
Schema3.Struct({
|
|
100
|
+
content: Schema3.String.pipe(Schema3.minLength(1)),
|
|
101
|
+
filePath: Schema3.String.pipe(Schema3.minLength(1)),
|
|
102
|
+
lineNumber: Schema3.optionalWith(Schema3.NullOr(Schema3.Number), {
|
|
103
|
+
default: () => null
|
|
104
|
+
}),
|
|
105
|
+
lineType: Schema3.optionalWith(Schema3.NullOr(LineType), {
|
|
106
|
+
default: () => null
|
|
107
|
+
}),
|
|
108
|
+
suggestion: Schema3.optionalWith(Schema3.NullOr(Schema3.String), {
|
|
109
|
+
default: () => null
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
Schema3.Struct({
|
|
113
|
+
content: Schema3.optionalWith(Schema3.String.pipe(Schema3.minLength(1)), {
|
|
114
|
+
as: "Option"
|
|
115
|
+
}),
|
|
116
|
+
suggestion: Schema3.optionalWith(Schema3.NullOr(Schema3.String), {
|
|
117
|
+
as: "Option"
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
var CommentNotFound = class extends Schema3.TaggedError()(
|
|
121
|
+
"CommentNotFound",
|
|
122
|
+
{ id: CommentId },
|
|
123
|
+
HttpApiSchema.annotations({ status: 404 })
|
|
124
|
+
) {
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/core/db/migrations.ts
|
|
128
|
+
var migrations = [
|
|
129
|
+
// v1: reviews table
|
|
130
|
+
`CREATE TABLE IF NOT EXISTS reviews (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
repository_path TEXT NOT NULL,
|
|
133
|
+
base_ref TEXT,
|
|
134
|
+
snapshot_data TEXT NOT NULL,
|
|
135
|
+
status TEXT DEFAULT 'in_progress',
|
|
136
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
137
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
138
|
+
) STRICT`,
|
|
139
|
+
// v2: comments table
|
|
140
|
+
`CREATE TABLE IF NOT EXISTS comments (
|
|
141
|
+
id TEXT PRIMARY KEY,
|
|
142
|
+
review_id TEXT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
|
|
143
|
+
file_path TEXT NOT NULL,
|
|
144
|
+
line_number INTEGER,
|
|
145
|
+
line_type TEXT,
|
|
146
|
+
content TEXT NOT NULL,
|
|
147
|
+
suggestion TEXT,
|
|
148
|
+
resolved INTEGER DEFAULT 0,
|
|
149
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
150
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
151
|
+
) STRICT`,
|
|
152
|
+
// v3: add source_type and source_ref to reviews
|
|
153
|
+
`ALTER TABLE reviews ADD COLUMN source_type TEXT DEFAULT 'staged';
|
|
154
|
+
ALTER TABLE reviews ADD COLUMN source_ref TEXT`,
|
|
155
|
+
// v4: todos table
|
|
156
|
+
`CREATE TABLE IF NOT EXISTS todos (
|
|
157
|
+
id TEXT PRIMARY KEY,
|
|
158
|
+
content TEXT NOT NULL,
|
|
159
|
+
completed INTEGER DEFAULT 0,
|
|
160
|
+
review_id TEXT REFERENCES reviews(id) ON DELETE CASCADE,
|
|
161
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
162
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
163
|
+
) STRICT`,
|
|
164
|
+
// v5: add position to todos
|
|
165
|
+
`ALTER TABLE todos ADD COLUMN position INTEGER DEFAULT 0`,
|
|
166
|
+
// v6: review_files table
|
|
167
|
+
`CREATE TABLE IF NOT EXISTS review_files (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
review_id TEXT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
|
|
170
|
+
file_path TEXT NOT NULL,
|
|
171
|
+
old_path TEXT,
|
|
172
|
+
status TEXT NOT NULL,
|
|
173
|
+
additions INTEGER NOT NULL DEFAULT 0,
|
|
174
|
+
deletions INTEGER NOT NULL DEFAULT 0,
|
|
175
|
+
hunks_data TEXT,
|
|
176
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
177
|
+
) STRICT`
|
|
178
|
+
];
|
|
179
|
+
var runMigrations = (db) => {
|
|
180
|
+
const currentVersion = db.prepare("PRAGMA user_version").get().user_version;
|
|
181
|
+
for (let i = currentVersion; i < migrations.length; i++) {
|
|
182
|
+
const statements = migrations[i].split(";").map((s) => s.trim()).filter(Boolean);
|
|
183
|
+
for (const sql of statements) {
|
|
184
|
+
db.exec(sql);
|
|
185
|
+
}
|
|
186
|
+
db.exec(`PRAGMA user_version = ${i + 1}`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/core/db/database.ts
|
|
191
|
+
var withTransaction = (db, body) => Effect4.acquireUseRelease(
|
|
192
|
+
Effect4.sync(() => db.exec("BEGIN")),
|
|
193
|
+
() => body,
|
|
194
|
+
(_, exit) => Effect4.sync(() => {
|
|
195
|
+
if (Exit.isSuccess(exit)) {
|
|
196
|
+
db.exec("COMMIT");
|
|
197
|
+
} else {
|
|
198
|
+
db.exec("ROLLBACK");
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
var SqliteService = class extends Effect4.Service()(
|
|
203
|
+
"@ringi/SqliteService",
|
|
204
|
+
{
|
|
205
|
+
effect: Effect4.gen(function* effect() {
|
|
206
|
+
const dbPath = yield* Config.string("DB_PATH").pipe(
|
|
207
|
+
Config.withDefault(".ringi/reviews.db")
|
|
208
|
+
);
|
|
209
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
210
|
+
const db = new DatabaseSync(dbPath);
|
|
211
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
212
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
213
|
+
runMigrations(db);
|
|
214
|
+
return { db };
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
) {
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// src/core/repos/comment.repo.ts
|
|
221
|
+
var rowToComment = (row) => ({
|
|
222
|
+
content: row.content,
|
|
223
|
+
createdAt: row.created_at,
|
|
224
|
+
filePath: row.file_path,
|
|
225
|
+
id: row.id,
|
|
226
|
+
lineNumber: row.line_number,
|
|
227
|
+
lineType: row.line_type,
|
|
228
|
+
resolved: row.resolved === 1,
|
|
229
|
+
reviewId: row.review_id,
|
|
230
|
+
suggestion: row.suggestion,
|
|
231
|
+
updatedAt: row.updated_at
|
|
232
|
+
});
|
|
233
|
+
var CommentRepo = class extends Effect4.Service()(
|
|
234
|
+
"@ringi/CommentRepo",
|
|
235
|
+
{
|
|
236
|
+
dependencies: [SqliteService.Default],
|
|
237
|
+
effect: Effect4.gen(function* effect() {
|
|
238
|
+
const { db } = yield* SqliteService;
|
|
239
|
+
const stmtFindById = db.prepare("SELECT * FROM comments WHERE id = ?");
|
|
240
|
+
const stmtFindByReview = db.prepare(
|
|
241
|
+
"SELECT * FROM comments WHERE review_id = ? ORDER BY created_at ASC"
|
|
242
|
+
);
|
|
243
|
+
const stmtFindByFile = db.prepare(
|
|
244
|
+
"SELECT * FROM comments WHERE review_id = ? AND file_path = ? ORDER BY line_number ASC, created_at ASC"
|
|
245
|
+
);
|
|
246
|
+
const stmtInsert = db.prepare(
|
|
247
|
+
`INSERT INTO comments (id, review_id, file_path, line_number, line_type, content, suggestion, resolved, created_at, updated_at)
|
|
248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, datetime('now'), datetime('now'))`
|
|
249
|
+
);
|
|
250
|
+
const stmtDelete = db.prepare("DELETE FROM comments WHERE id = ?");
|
|
251
|
+
const stmtDeleteByReview = db.prepare(
|
|
252
|
+
"DELETE FROM comments WHERE review_id = ?"
|
|
253
|
+
);
|
|
254
|
+
const stmtSetResolved = db.prepare(
|
|
255
|
+
"UPDATE comments SET resolved = ?, updated_at = datetime('now') WHERE id = ?"
|
|
256
|
+
);
|
|
257
|
+
const stmtCountByReview = db.prepare(
|
|
258
|
+
`SELECT
|
|
259
|
+
COUNT(*) as total,
|
|
260
|
+
SUM(CASE WHEN resolved = 1 THEN 1 ELSE 0 END) as resolved,
|
|
261
|
+
SUM(CASE WHEN resolved = 0 THEN 1 ELSE 0 END) as unresolved,
|
|
262
|
+
SUM(CASE WHEN suggestion IS NOT NULL THEN 1 ELSE 0 END) as with_suggestions
|
|
263
|
+
FROM comments WHERE review_id = ?`
|
|
264
|
+
);
|
|
265
|
+
const findById = (id) => Effect4.sync(() => {
|
|
266
|
+
const row = stmtFindById.get(id);
|
|
267
|
+
return row ? rowToComment(row) : null;
|
|
268
|
+
});
|
|
269
|
+
const findByReview = (reviewId) => Effect4.sync(() => {
|
|
270
|
+
const rows = stmtFindByReview.all(
|
|
271
|
+
reviewId
|
|
272
|
+
);
|
|
273
|
+
return rows.map(rowToComment);
|
|
274
|
+
});
|
|
275
|
+
const findByFile = (reviewId, filePath) => Effect4.sync(() => {
|
|
276
|
+
const rows = stmtFindByFile.all(
|
|
277
|
+
reviewId,
|
|
278
|
+
filePath
|
|
279
|
+
);
|
|
280
|
+
return rows.map(rowToComment);
|
|
281
|
+
});
|
|
282
|
+
const create = (input) => Effect4.sync(() => {
|
|
283
|
+
stmtInsert.run(
|
|
284
|
+
input.id,
|
|
285
|
+
input.reviewId,
|
|
286
|
+
input.filePath,
|
|
287
|
+
input.lineNumber,
|
|
288
|
+
input.lineType,
|
|
289
|
+
input.content,
|
|
290
|
+
input.suggestion
|
|
291
|
+
);
|
|
292
|
+
return rowToComment(
|
|
293
|
+
stmtFindById.get(input.id)
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
const update = (id, updates) => Effect4.sync(() => {
|
|
297
|
+
const setClauses = [];
|
|
298
|
+
const params = [];
|
|
299
|
+
if (updates.content !== void 0) {
|
|
300
|
+
setClauses.push("content = ?");
|
|
301
|
+
params.push(updates.content);
|
|
302
|
+
}
|
|
303
|
+
if (updates.suggestion !== void 0) {
|
|
304
|
+
setClauses.push("suggestion = ?");
|
|
305
|
+
params.push(updates.suggestion);
|
|
306
|
+
}
|
|
307
|
+
if (setClauses.length === 0) {
|
|
308
|
+
const row2 = stmtFindById.get(id);
|
|
309
|
+
return row2 ? rowToComment(row2) : null;
|
|
310
|
+
}
|
|
311
|
+
setClauses.push("updated_at = datetime('now')");
|
|
312
|
+
params.push(id);
|
|
313
|
+
db.prepare(
|
|
314
|
+
`UPDATE comments SET ${setClauses.join(", ")} WHERE id = ?`
|
|
315
|
+
).run(...params);
|
|
316
|
+
const row = stmtFindById.get(id);
|
|
317
|
+
return row ? rowToComment(row) : null;
|
|
318
|
+
});
|
|
319
|
+
const setResolved = (id, resolved) => Effect4.sync(() => {
|
|
320
|
+
stmtSetResolved.run(resolved ? 1 : 0, id);
|
|
321
|
+
const row = stmtFindById.get(id);
|
|
322
|
+
return row ? rowToComment(row) : null;
|
|
323
|
+
});
|
|
324
|
+
const remove = (id) => Effect4.sync(() => {
|
|
325
|
+
const result = stmtDelete.run(id);
|
|
326
|
+
return Number(result.changes) > 0;
|
|
327
|
+
});
|
|
328
|
+
const removeByReview = (reviewId) => Effect4.sync(() => {
|
|
329
|
+
const result = stmtDeleteByReview.run(reviewId);
|
|
330
|
+
return Number(result.changes);
|
|
331
|
+
});
|
|
332
|
+
const countByReview = (reviewId) => Effect4.sync(() => {
|
|
333
|
+
const row = stmtCountByReview.get(reviewId);
|
|
334
|
+
return {
|
|
335
|
+
resolved: row.resolved,
|
|
336
|
+
total: row.total,
|
|
337
|
+
unresolved: row.unresolved,
|
|
338
|
+
withSuggestions: row.with_suggestions
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
countByReview,
|
|
343
|
+
create,
|
|
344
|
+
findByFile,
|
|
345
|
+
findById,
|
|
346
|
+
findByReview,
|
|
347
|
+
remove,
|
|
348
|
+
removeByReview,
|
|
349
|
+
setResolved,
|
|
350
|
+
update
|
|
351
|
+
};
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
) {
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/core/services/comment.service.ts
|
|
358
|
+
var CommentService = class extends Effect4.Service()(
|
|
359
|
+
"@ringi/CommentService",
|
|
360
|
+
{
|
|
361
|
+
dependencies: [CommentRepo.Default],
|
|
362
|
+
effect: Effect4.sync(() => {
|
|
363
|
+
const create = Effect4.fn("CommentService.create")(function* create2(reviewId, input) {
|
|
364
|
+
const repo = yield* CommentRepo;
|
|
365
|
+
const id = randomUUID();
|
|
366
|
+
return yield* repo.create({
|
|
367
|
+
content: input.content,
|
|
368
|
+
filePath: input.filePath,
|
|
369
|
+
id,
|
|
370
|
+
lineNumber: input.lineNumber,
|
|
371
|
+
lineType: input.lineType,
|
|
372
|
+
reviewId,
|
|
373
|
+
suggestion: input.suggestion
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
const getById = Effect4.fn("CommentService.getById")(function* getById2(id) {
|
|
377
|
+
const repo = yield* CommentRepo;
|
|
378
|
+
const comment = yield* repo.findById(id);
|
|
379
|
+
if (!comment) {
|
|
380
|
+
return yield* new CommentNotFound({ id });
|
|
381
|
+
}
|
|
382
|
+
return comment;
|
|
383
|
+
});
|
|
384
|
+
const getByReview = Effect4.fn("CommentService.getByReview")(
|
|
385
|
+
function* getByReview2(reviewId) {
|
|
386
|
+
const repo = yield* CommentRepo;
|
|
387
|
+
return yield* repo.findByReview(reviewId);
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
const getByFile = Effect4.fn("CommentService.getByFile")(
|
|
391
|
+
function* getByFile2(reviewId, filePath) {
|
|
392
|
+
const repo = yield* CommentRepo;
|
|
393
|
+
return yield* repo.findByFile(reviewId, filePath);
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
const update = Effect4.fn("CommentService.update")(function* update2(id, input) {
|
|
397
|
+
const repo = yield* CommentRepo;
|
|
398
|
+
const updates = {};
|
|
399
|
+
if (Option.isSome(input.content)) {
|
|
400
|
+
updates.content = input.content.value;
|
|
401
|
+
}
|
|
402
|
+
if (Option.isSome(input.suggestion)) {
|
|
403
|
+
updates.suggestion = input.suggestion.value;
|
|
404
|
+
}
|
|
405
|
+
const comment = yield* repo.update(id, updates);
|
|
406
|
+
if (!comment) {
|
|
407
|
+
return yield* new CommentNotFound({ id });
|
|
408
|
+
}
|
|
409
|
+
return comment;
|
|
410
|
+
});
|
|
411
|
+
const resolve = Effect4.fn("CommentService.resolve")(function* resolve2(id) {
|
|
412
|
+
const repo = yield* CommentRepo;
|
|
413
|
+
const comment = yield* repo.setResolved(id, true);
|
|
414
|
+
if (!comment) {
|
|
415
|
+
return yield* new CommentNotFound({ id });
|
|
416
|
+
}
|
|
417
|
+
return comment;
|
|
418
|
+
});
|
|
419
|
+
const unresolve = Effect4.fn("CommentService.unresolve")(
|
|
420
|
+
function* unresolve2(id) {
|
|
421
|
+
const repo = yield* CommentRepo;
|
|
422
|
+
const comment = yield* repo.setResolved(id, false);
|
|
423
|
+
if (!comment) {
|
|
424
|
+
return yield* new CommentNotFound({ id });
|
|
425
|
+
}
|
|
426
|
+
return comment;
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
const remove = Effect4.fn("CommentService.remove")(function* remove2(id) {
|
|
430
|
+
const repo = yield* CommentRepo;
|
|
431
|
+
const existed = yield* repo.remove(id);
|
|
432
|
+
if (!existed) {
|
|
433
|
+
return yield* new CommentNotFound({ id });
|
|
434
|
+
}
|
|
435
|
+
return { success: true };
|
|
436
|
+
});
|
|
437
|
+
const getStats = Effect4.fn("CommentService.getStats")(function* getStats2(reviewId) {
|
|
438
|
+
const repo = yield* CommentRepo;
|
|
439
|
+
return yield* repo.countByReview(reviewId);
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
create,
|
|
443
|
+
getByFile,
|
|
444
|
+
getById,
|
|
445
|
+
getByReview,
|
|
446
|
+
getStats,
|
|
447
|
+
remove,
|
|
448
|
+
resolve,
|
|
449
|
+
unresolve,
|
|
450
|
+
update
|
|
451
|
+
};
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
) {
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/core/services/diff.service.ts
|
|
458
|
+
var HUNK_HEADER = /@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
459
|
+
var splitIntoFiles = (diffText) => {
|
|
460
|
+
const files = [];
|
|
461
|
+
const lines2 = diffText.split("\n");
|
|
462
|
+
let current = [];
|
|
463
|
+
for (const line of lines2) {
|
|
464
|
+
if (line.startsWith("diff --git")) {
|
|
465
|
+
if (current.length > 0) {
|
|
466
|
+
files.push(current.join("\n"));
|
|
467
|
+
}
|
|
468
|
+
current = [line];
|
|
469
|
+
} else {
|
|
470
|
+
current.push(line);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (current.length > 0) {
|
|
474
|
+
files.push(current.join("\n"));
|
|
475
|
+
}
|
|
476
|
+
return files;
|
|
477
|
+
};
|
|
478
|
+
var parseHunks = (lines2) => {
|
|
479
|
+
const hunks = [];
|
|
480
|
+
let currentHunk = null;
|
|
481
|
+
let oldLineNum = 0;
|
|
482
|
+
let newLineNum = 0;
|
|
483
|
+
for (const line of lines2) {
|
|
484
|
+
const match = line.match(HUNK_HEADER);
|
|
485
|
+
if (match) {
|
|
486
|
+
if (currentHunk) {
|
|
487
|
+
hunks.push(currentHunk);
|
|
488
|
+
}
|
|
489
|
+
const oldStart = Number.parseInt(match[1], 10);
|
|
490
|
+
const oldLines = Number.parseInt(match[2] ?? "1", 10);
|
|
491
|
+
const newStart = Number.parseInt(match[3], 10);
|
|
492
|
+
const newLines = Number.parseInt(match[4] ?? "1", 10);
|
|
493
|
+
currentHunk = { lines: [], newLines, newStart, oldLines, oldStart };
|
|
494
|
+
oldLineNum = oldStart;
|
|
495
|
+
newLineNum = newStart;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (!currentHunk) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
502
|
+
currentHunk.lines.push({
|
|
503
|
+
content: line.slice(1),
|
|
504
|
+
newLineNumber: newLineNum++,
|
|
505
|
+
oldLineNumber: null,
|
|
506
|
+
type: "added"
|
|
507
|
+
});
|
|
508
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
509
|
+
currentHunk.lines.push({
|
|
510
|
+
content: line.slice(1),
|
|
511
|
+
newLineNumber: null,
|
|
512
|
+
oldLineNumber: oldLineNum++,
|
|
513
|
+
type: "removed"
|
|
514
|
+
});
|
|
515
|
+
} else if (line.startsWith(" ")) {
|
|
516
|
+
currentHunk.lines.push({
|
|
517
|
+
content: line.slice(1),
|
|
518
|
+
newLineNumber: newLineNum++,
|
|
519
|
+
oldLineNumber: oldLineNum++,
|
|
520
|
+
type: "context"
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (currentHunk) {
|
|
525
|
+
hunks.push(currentHunk);
|
|
526
|
+
}
|
|
527
|
+
return hunks;
|
|
528
|
+
};
|
|
529
|
+
var parseFileDiff = (fileDiff) => {
|
|
530
|
+
const lines2 = fileDiff.split("\n");
|
|
531
|
+
const diffLine = lines2.find((l) => l.startsWith("diff --git"));
|
|
532
|
+
if (!diffLine) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
const pathMatch = diffLine.match(/diff --git a\/(.+) b\/(.+)/);
|
|
536
|
+
if (!pathMatch) {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
const oldPath = pathMatch[1];
|
|
540
|
+
const newPath = pathMatch[2];
|
|
541
|
+
let status = "modified";
|
|
542
|
+
if (lines2.some((l) => l.startsWith("deleted file mode"))) {
|
|
543
|
+
status = "deleted";
|
|
544
|
+
} else if (lines2.some((l) => l.startsWith("new file mode"))) {
|
|
545
|
+
status = "added";
|
|
546
|
+
} else if (lines2.some((l) => l.startsWith("rename from")) || oldPath !== newPath) {
|
|
547
|
+
status = "renamed";
|
|
548
|
+
}
|
|
549
|
+
const hunks = parseHunks(lines2);
|
|
550
|
+
let additions = 0;
|
|
551
|
+
let deletions = 0;
|
|
552
|
+
for (const hunk of hunks) {
|
|
553
|
+
for (const line of hunk.lines) {
|
|
554
|
+
if (line.type === "added") {
|
|
555
|
+
additions++;
|
|
556
|
+
} else if (line.type === "removed") {
|
|
557
|
+
deletions++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { additions, deletions, hunks, newPath, oldPath, status };
|
|
562
|
+
};
|
|
563
|
+
var parseDiff = (diffText) => {
|
|
564
|
+
if (!diffText.trim()) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
const blocks = splitIntoFiles(diffText);
|
|
568
|
+
const files = [];
|
|
569
|
+
for (const block of blocks) {
|
|
570
|
+
const parsed = parseFileDiff(block);
|
|
571
|
+
if (parsed) {
|
|
572
|
+
files.push(parsed);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return files;
|
|
576
|
+
};
|
|
577
|
+
var getDiffSummary = (files) => {
|
|
578
|
+
let totalAdditions = 0;
|
|
579
|
+
let totalDeletions = 0;
|
|
580
|
+
for (const file of files) {
|
|
581
|
+
totalAdditions += file.additions;
|
|
582
|
+
totalDeletions += file.deletions;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
filesAdded: files.filter((f) => f.status === "added").length,
|
|
586
|
+
filesDeleted: files.filter((f) => f.status === "deleted").length,
|
|
587
|
+
filesModified: files.filter((f) => f.status === "modified").length,
|
|
588
|
+
filesRenamed: files.filter((f) => f.status === "renamed").length,
|
|
589
|
+
totalAdditions,
|
|
590
|
+
totalDeletions,
|
|
591
|
+
totalFiles: files.length
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
var GitError = class extends Schema3.TaggedError()(
|
|
595
|
+
"GitError",
|
|
596
|
+
{ message: Schema3.String },
|
|
597
|
+
HttpApiSchema.annotations({ status: 500 })
|
|
598
|
+
) {
|
|
599
|
+
};
|
|
600
|
+
var execGit = (args, repoPath) => Effect4.tryPromise({
|
|
601
|
+
catch: (error) => new GitError({ message: String(error) }),
|
|
602
|
+
try: () => new Promise((resolve, reject) => {
|
|
603
|
+
execFile(
|
|
604
|
+
"git",
|
|
605
|
+
[...args],
|
|
606
|
+
{ cwd: repoPath, maxBuffer: 50 * 1024 * 1024 },
|
|
607
|
+
(err, stdout) => {
|
|
608
|
+
if (err) {
|
|
609
|
+
reject(err);
|
|
610
|
+
} else {
|
|
611
|
+
resolve(stdout);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
})
|
|
616
|
+
});
|
|
617
|
+
var lines = (output) => output.trim().split("\n").filter(Boolean);
|
|
618
|
+
var parseNameStatus = (output) => lines(output).map((line) => {
|
|
619
|
+
const [status, ...rest] = line.split(" ");
|
|
620
|
+
return { path: rest.join(" "), status };
|
|
621
|
+
});
|
|
622
|
+
var GitService = class extends Effect4.Service()(
|
|
623
|
+
"@ringi/GitService",
|
|
624
|
+
{
|
|
625
|
+
effect: Effect4.gen(function* effect() {
|
|
626
|
+
const repoPath = yield* Config.string("REPOSITORY_PATH").pipe(
|
|
627
|
+
Config.withDefault(process.cwd())
|
|
628
|
+
);
|
|
629
|
+
const hasCommits = execGit(["rev-parse", "HEAD"], repoPath).pipe(
|
|
630
|
+
Effect4.as(true),
|
|
631
|
+
Effect4.catchTag("GitError", () => Effect4.succeed(false)),
|
|
632
|
+
Effect4.withSpan("GitService.hasCommits")
|
|
633
|
+
);
|
|
634
|
+
const getRepositoryInfo = Effect4.gen(function* getRepositoryInfo2() {
|
|
635
|
+
const name = yield* execGit(
|
|
636
|
+
["rev-parse", "--show-toplevel"],
|
|
637
|
+
repoPath
|
|
638
|
+
).pipe(Effect4.map((s) => s.trim().split("/").pop() ?? "unknown"));
|
|
639
|
+
const branch = yield* execGit(
|
|
640
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
641
|
+
repoPath
|
|
642
|
+
).pipe(Effect4.map((s) => s.trim()));
|
|
643
|
+
const remote = yield* execGit(
|
|
644
|
+
["config", "--get", "remote.origin.url"],
|
|
645
|
+
repoPath
|
|
646
|
+
).pipe(
|
|
647
|
+
Effect4.map((s) => s.trim() || null),
|
|
648
|
+
Effect4.catchTag("GitError", () => Effect4.succeed(null))
|
|
649
|
+
);
|
|
650
|
+
return { branch, name, path: repoPath, remote };
|
|
651
|
+
}).pipe(Effect4.withSpan("GitService.getRepositoryInfo"));
|
|
652
|
+
const getStagedDiff = execGit(
|
|
653
|
+
["diff", "--cached", "--no-color", "--unified=3"],
|
|
654
|
+
repoPath
|
|
655
|
+
).pipe(Effect4.withSpan("GitService.getStagedDiff"));
|
|
656
|
+
const getUncommittedDiff = hasCommits.pipe(
|
|
657
|
+
Effect4.flatMap(
|
|
658
|
+
(has) => has ? execGit(["diff", "HEAD", "--no-color", "--unified=3"], repoPath) : Effect4.succeed("")
|
|
659
|
+
),
|
|
660
|
+
Effect4.withSpan("GitService.getUncommittedDiff")
|
|
661
|
+
);
|
|
662
|
+
const getUnstagedDiff = execGit(
|
|
663
|
+
["diff", "--no-color", "--unified=3"],
|
|
664
|
+
repoPath
|
|
665
|
+
).pipe(Effect4.withSpan("GitService.getUnstagedDiff"));
|
|
666
|
+
const getLastCommitDiff = hasCommits.pipe(
|
|
667
|
+
Effect4.flatMap(
|
|
668
|
+
(has) => has ? execGit(
|
|
669
|
+
["show", "HEAD", "--format=", "--no-color", "--unified=3"],
|
|
670
|
+
repoPath
|
|
671
|
+
) : Effect4.succeed("")
|
|
672
|
+
),
|
|
673
|
+
Effect4.withSpan("GitService.getLastCommitDiff")
|
|
674
|
+
);
|
|
675
|
+
const getBranchDiff = Effect4.fn("GitService.getBranchDiff")(
|
|
676
|
+
function* getBranchDiff2(branch) {
|
|
677
|
+
return yield* execGit(
|
|
678
|
+
["diff", `${branch}...HEAD`, "--no-color", "--unified=3"],
|
|
679
|
+
repoPath
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
const getCommitDiff = Effect4.fn("GitService.getCommitDiff")(
|
|
684
|
+
function* getCommitDiff2(shas) {
|
|
685
|
+
if (shas.length === 1) {
|
|
686
|
+
return yield* execGit(
|
|
687
|
+
["show", shas[0], "--format=", "--no-color", "--unified=3"],
|
|
688
|
+
repoPath
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const first = shas.at(-1);
|
|
692
|
+
const last = shas[0];
|
|
693
|
+
return yield* execGit(
|
|
694
|
+
["diff", `${first}~1..${last}`, "--no-color", "--unified=3"],
|
|
695
|
+
repoPath
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
const getStagedFiles = execGit(
|
|
700
|
+
["diff", "--cached", "--name-status"],
|
|
701
|
+
repoPath
|
|
702
|
+
).pipe(
|
|
703
|
+
Effect4.map(parseNameStatus),
|
|
704
|
+
Effect4.withSpan("GitService.getStagedFiles")
|
|
705
|
+
);
|
|
706
|
+
const getUncommittedFiles = hasCommits.pipe(
|
|
707
|
+
Effect4.flatMap(
|
|
708
|
+
(has) => has ? execGit(["diff", "HEAD", "--name-status"], repoPath).pipe(
|
|
709
|
+
Effect4.map(parseNameStatus)
|
|
710
|
+
) : Effect4.succeed([])
|
|
711
|
+
),
|
|
712
|
+
Effect4.withSpan("GitService.getUncommittedFiles")
|
|
713
|
+
);
|
|
714
|
+
const getUnstagedFiles = execGit(
|
|
715
|
+
["diff", "--name-status"],
|
|
716
|
+
repoPath
|
|
717
|
+
).pipe(
|
|
718
|
+
Effect4.map(parseNameStatus),
|
|
719
|
+
Effect4.withSpan("GitService.getUnstagedFiles")
|
|
720
|
+
);
|
|
721
|
+
const getLastCommitFiles = hasCommits.pipe(
|
|
722
|
+
Effect4.flatMap(
|
|
723
|
+
(has) => has ? execGit(
|
|
724
|
+
["show", "HEAD", "--format=", "--name-status"],
|
|
725
|
+
repoPath
|
|
726
|
+
).pipe(Effect4.map(parseNameStatus)) : Effect4.succeed([])
|
|
727
|
+
),
|
|
728
|
+
Effect4.withSpan("GitService.getLastCommitFiles")
|
|
729
|
+
);
|
|
730
|
+
const getFileContent = Effect4.fn("GitService.getFileContent")(
|
|
731
|
+
function* getFileContent2(filePath, version) {
|
|
732
|
+
switch (version) {
|
|
733
|
+
case "staged": {
|
|
734
|
+
return yield* execGit(["show", `:${filePath}`], repoPath);
|
|
735
|
+
}
|
|
736
|
+
case "head": {
|
|
737
|
+
return yield* execGit(["show", `HEAD:${filePath}`], repoPath);
|
|
738
|
+
}
|
|
739
|
+
case "working":
|
|
740
|
+
default: {
|
|
741
|
+
return yield* Effect4.tryPromise({
|
|
742
|
+
catch: (error) => new GitError({
|
|
743
|
+
message: `Failed to read ${filePath}: ${String(error)}`
|
|
744
|
+
}),
|
|
745
|
+
try: () => readFile(join(repoPath, filePath), "utf8")
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
const getFileTree = Effect4.fn("GitService.getFileTree")(
|
|
752
|
+
function* getFileTree2(ref) {
|
|
753
|
+
return yield* execGit(
|
|
754
|
+
["ls-tree", "-r", "--name-only", ref],
|
|
755
|
+
repoPath
|
|
756
|
+
).pipe(Effect4.map(lines));
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
const getBranches = execGit(
|
|
760
|
+
["branch", "--format=%(refname:short) %(HEAD)"],
|
|
761
|
+
repoPath
|
|
762
|
+
).pipe(
|
|
763
|
+
Effect4.map(
|
|
764
|
+
(output) => lines(output).map((line) => {
|
|
765
|
+
const [name, head] = line.split(" ");
|
|
766
|
+
return { current: head === "*", name };
|
|
767
|
+
})
|
|
768
|
+
),
|
|
769
|
+
Effect4.withSpan("GitService.getBranches")
|
|
770
|
+
);
|
|
771
|
+
const getCommits = Effect4.fn("GitService.getCommits")(
|
|
772
|
+
function* getCommits2(opts) {
|
|
773
|
+
const limit = (opts.limit ?? 20) + 1;
|
|
774
|
+
const args = [
|
|
775
|
+
"log",
|
|
776
|
+
`--max-count=${limit}`,
|
|
777
|
+
`--skip=${opts.offset ?? 0}`,
|
|
778
|
+
"--format=%H %s %an %aI"
|
|
779
|
+
];
|
|
780
|
+
if (opts.search) {
|
|
781
|
+
args.push(`--grep=${opts.search}`, "-i");
|
|
782
|
+
}
|
|
783
|
+
const output = yield* execGit(args, repoPath);
|
|
784
|
+
const rows = lines(output);
|
|
785
|
+
const hasMore = rows.length === limit;
|
|
786
|
+
const commits = (hasMore ? rows.slice(0, -1) : rows).map((line) => {
|
|
787
|
+
const [hash, message, author, date] = line.split(" ");
|
|
788
|
+
return {
|
|
789
|
+
author,
|
|
790
|
+
date,
|
|
791
|
+
hash,
|
|
792
|
+
message
|
|
793
|
+
};
|
|
794
|
+
});
|
|
795
|
+
return { commits, hasMore };
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
const stageFiles = Effect4.fn("GitService.stageFiles")(
|
|
799
|
+
function* stageFiles2(files) {
|
|
800
|
+
return yield* execGit(["add", "--", ...files], repoPath).pipe(
|
|
801
|
+
Effect4.as(files)
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
const stageAll = execGit(["add", "-A"], repoPath).pipe(
|
|
806
|
+
Effect4.flatMap(() => getStagedFiles),
|
|
807
|
+
Effect4.map((files) => files.map((f) => f.path)),
|
|
808
|
+
Effect4.withSpan("GitService.stageAll")
|
|
809
|
+
);
|
|
810
|
+
const unstageFiles = Effect4.fn("GitService.unstageFiles")(
|
|
811
|
+
function* unstageFiles2(files) {
|
|
812
|
+
return yield* execGit(
|
|
813
|
+
["reset", "HEAD", "--", ...files],
|
|
814
|
+
repoPath
|
|
815
|
+
).pipe(Effect4.as(files));
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
const getRepositoryPath = execGit(
|
|
819
|
+
["rev-parse", "--show-toplevel"],
|
|
820
|
+
repoPath
|
|
821
|
+
).pipe(
|
|
822
|
+
Effect4.map((s) => s.trim()),
|
|
823
|
+
Effect4.withSpan("GitService.getRepositoryPath")
|
|
824
|
+
);
|
|
825
|
+
return {
|
|
826
|
+
getBranchDiff,
|
|
827
|
+
getBranches,
|
|
828
|
+
getCommitDiff,
|
|
829
|
+
getCommits,
|
|
830
|
+
getFileContent,
|
|
831
|
+
getFileTree,
|
|
832
|
+
getLastCommitDiff,
|
|
833
|
+
getLastCommitFiles,
|
|
834
|
+
getRepositoryInfo,
|
|
835
|
+
getRepositoryPath,
|
|
836
|
+
getStagedDiff,
|
|
837
|
+
getStagedFiles,
|
|
838
|
+
getUncommittedDiff,
|
|
839
|
+
getUncommittedFiles,
|
|
840
|
+
getUnstagedDiff,
|
|
841
|
+
getUnstagedFiles,
|
|
842
|
+
hasCommits,
|
|
843
|
+
stageAll,
|
|
844
|
+
stageFiles,
|
|
845
|
+
unstageFiles
|
|
846
|
+
};
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
) {
|
|
850
|
+
};
|
|
851
|
+
var parseHunks2 = (hunksData) => hunksData == null ? Effect4.succeed([]) : Effect4.try(() => JSON.parse(hunksData)).pipe(
|
|
852
|
+
Effect4.orElseSucceed(() => [])
|
|
853
|
+
);
|
|
854
|
+
var serializeHunks = (hunks) => JSON.stringify(hunks);
|
|
855
|
+
var ReviewFileRepo = class extends Effect4.Service()(
|
|
856
|
+
"@ringi/ReviewFileRepo",
|
|
857
|
+
{
|
|
858
|
+
dependencies: [SqliteService.Default],
|
|
859
|
+
effect: Effect4.gen(function* effect() {
|
|
860
|
+
const { db } = yield* SqliteService;
|
|
861
|
+
const stmtFindByReview = db.prepare(
|
|
862
|
+
`SELECT id, review_id, file_path, old_path, status, additions, deletions, created_at
|
|
863
|
+
FROM review_files WHERE review_id = ? ORDER BY file_path`
|
|
864
|
+
);
|
|
865
|
+
const stmtFindByReviewAndPath = db.prepare(
|
|
866
|
+
"SELECT * FROM review_files WHERE review_id = ? AND file_path = ?"
|
|
867
|
+
);
|
|
868
|
+
const stmtInsert = db.prepare(
|
|
869
|
+
`INSERT INTO review_files (id, review_id, file_path, old_path, status, additions, deletions, hunks_data, created_at)
|
|
870
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`
|
|
871
|
+
);
|
|
872
|
+
const stmtDeleteByReview = db.prepare(
|
|
873
|
+
"DELETE FROM review_files WHERE review_id = ?"
|
|
874
|
+
);
|
|
875
|
+
const stmtCountByReview = db.prepare(
|
|
876
|
+
"SELECT COUNT(*) as count FROM review_files WHERE review_id = ?"
|
|
877
|
+
);
|
|
878
|
+
const findByReview = (reviewId) => Effect4.sync(
|
|
879
|
+
() => stmtFindByReview.all(reviewId)
|
|
880
|
+
);
|
|
881
|
+
const findByReviewAndPath = (reviewId, filePath) => Effect4.sync(() => {
|
|
882
|
+
const row = stmtFindByReviewAndPath.get(
|
|
883
|
+
reviewId,
|
|
884
|
+
filePath
|
|
885
|
+
);
|
|
886
|
+
return row ?? null;
|
|
887
|
+
});
|
|
888
|
+
const createBulk = (files) => withTransaction(
|
|
889
|
+
db,
|
|
890
|
+
Effect4.sync(() => {
|
|
891
|
+
for (const f of files) {
|
|
892
|
+
stmtInsert.run(
|
|
893
|
+
randomUUID(),
|
|
894
|
+
f.reviewId,
|
|
895
|
+
f.filePath,
|
|
896
|
+
f.oldPath,
|
|
897
|
+
f.status,
|
|
898
|
+
f.additions,
|
|
899
|
+
f.deletions,
|
|
900
|
+
f.hunksData
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
const deleteByReview = (reviewId) => Effect4.sync(() => {
|
|
906
|
+
const result = stmtDeleteByReview.run(reviewId);
|
|
907
|
+
return Number(result.changes);
|
|
908
|
+
});
|
|
909
|
+
const countByReview = (reviewId) => Effect4.sync(() => {
|
|
910
|
+
const row = stmtCountByReview.get(reviewId);
|
|
911
|
+
return row.count;
|
|
912
|
+
});
|
|
913
|
+
return {
|
|
914
|
+
countByReview,
|
|
915
|
+
createBulk,
|
|
916
|
+
deleteByReview,
|
|
917
|
+
findByReview,
|
|
918
|
+
findByReviewAndPath
|
|
919
|
+
};
|
|
920
|
+
})
|
|
921
|
+
}
|
|
922
|
+
) {
|
|
923
|
+
};
|
|
924
|
+
var rowToReview = (row) => ({
|
|
925
|
+
baseRef: row.base_ref,
|
|
926
|
+
createdAt: row.created_at,
|
|
927
|
+
id: row.id,
|
|
928
|
+
repositoryPath: row.repository_path,
|
|
929
|
+
snapshotData: row.snapshot_data,
|
|
930
|
+
sourceRef: row.source_ref,
|
|
931
|
+
sourceType: row.source_type,
|
|
932
|
+
status: row.status,
|
|
933
|
+
updatedAt: row.updated_at
|
|
934
|
+
});
|
|
935
|
+
var ReviewRepo = class extends Effect4.Service()(
|
|
936
|
+
"@ringi/ReviewRepo",
|
|
937
|
+
{
|
|
938
|
+
dependencies: [SqliteService.Default],
|
|
939
|
+
effect: Effect4.gen(function* effect() {
|
|
940
|
+
const { db } = yield* SqliteService;
|
|
941
|
+
const stmtFindById = db.prepare("SELECT * FROM reviews WHERE id = ?");
|
|
942
|
+
const stmtInsert = db.prepare(
|
|
943
|
+
`INSERT INTO reviews (id, repository_path, base_ref, source_type, source_ref, snapshot_data, status, created_at, updated_at)
|
|
944
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`
|
|
945
|
+
);
|
|
946
|
+
const stmtUpdate = db.prepare(
|
|
947
|
+
`UPDATE reviews SET status = COALESCE(?, status), updated_at = datetime('now') WHERE id = ?`
|
|
948
|
+
);
|
|
949
|
+
const stmtDelete = db.prepare("DELETE FROM reviews WHERE id = ?");
|
|
950
|
+
const stmtCountAll = db.prepare("SELECT COUNT(*) as count FROM reviews");
|
|
951
|
+
const stmtCountByStatus = db.prepare(
|
|
952
|
+
"SELECT COUNT(*) as count FROM reviews WHERE status = ?"
|
|
953
|
+
);
|
|
954
|
+
const findById = (id) => Effect4.sync(() => {
|
|
955
|
+
const row = stmtFindById.get(id);
|
|
956
|
+
return row ? rowToReview(row) : null;
|
|
957
|
+
});
|
|
958
|
+
const findAll = (opts = {}) => Effect4.sync(() => {
|
|
959
|
+
const conditions = [];
|
|
960
|
+
const params = [];
|
|
961
|
+
if (opts.status != null) {
|
|
962
|
+
conditions.push("status = ?");
|
|
963
|
+
params.push(opts.status);
|
|
964
|
+
}
|
|
965
|
+
if (opts.repositoryPath != null) {
|
|
966
|
+
conditions.push("repository_path = ?");
|
|
967
|
+
params.push(opts.repositoryPath);
|
|
968
|
+
}
|
|
969
|
+
if (opts.sourceType != null) {
|
|
970
|
+
conditions.push("source_type = ?");
|
|
971
|
+
params.push(opts.sourceType);
|
|
972
|
+
}
|
|
973
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
974
|
+
const page = opts.page ?? 1;
|
|
975
|
+
const pageSize = opts.pageSize ?? 20;
|
|
976
|
+
const offset = (page - 1) * pageSize;
|
|
977
|
+
const totalRow = db.prepare(`SELECT COUNT(*) as count FROM reviews${where}`).get(
|
|
978
|
+
...params
|
|
979
|
+
);
|
|
980
|
+
const rows = db.prepare(
|
|
981
|
+
`SELECT * FROM reviews${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
982
|
+
).all(
|
|
983
|
+
...params,
|
|
984
|
+
pageSize,
|
|
985
|
+
offset
|
|
986
|
+
);
|
|
987
|
+
return { data: rows.map(rowToReview), total: totalRow.count };
|
|
988
|
+
});
|
|
989
|
+
const create = (input) => Effect4.sync(() => {
|
|
990
|
+
stmtInsert.run(
|
|
991
|
+
input.id,
|
|
992
|
+
input.repositoryPath,
|
|
993
|
+
input.baseRef,
|
|
994
|
+
input.sourceType,
|
|
995
|
+
input.sourceRef,
|
|
996
|
+
input.snapshotData,
|
|
997
|
+
input.status
|
|
998
|
+
);
|
|
999
|
+
return rowToReview(
|
|
1000
|
+
stmtFindById.get(input.id)
|
|
1001
|
+
);
|
|
1002
|
+
});
|
|
1003
|
+
const update = (id, status) => Effect4.sync(() => {
|
|
1004
|
+
stmtUpdate.run(status, id);
|
|
1005
|
+
const row = stmtFindById.get(id);
|
|
1006
|
+
return row ? rowToReview(row) : null;
|
|
1007
|
+
});
|
|
1008
|
+
const remove = (id) => Effect4.sync(() => {
|
|
1009
|
+
const result = stmtDelete.run(id);
|
|
1010
|
+
return Number(result.changes) > 0;
|
|
1011
|
+
});
|
|
1012
|
+
const countAll = () => Effect4.sync(() => {
|
|
1013
|
+
const row = stmtCountAll.get();
|
|
1014
|
+
return row.count;
|
|
1015
|
+
});
|
|
1016
|
+
const countByStatus = (status) => Effect4.sync(() => {
|
|
1017
|
+
const row = stmtCountByStatus.get(status);
|
|
1018
|
+
return row.count;
|
|
1019
|
+
});
|
|
1020
|
+
return {
|
|
1021
|
+
countAll,
|
|
1022
|
+
countByStatus,
|
|
1023
|
+
create,
|
|
1024
|
+
findAll,
|
|
1025
|
+
findById,
|
|
1026
|
+
remove,
|
|
1027
|
+
update
|
|
1028
|
+
};
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
) {
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// src/core/services/review.service.ts
|
|
1035
|
+
var ReviewError = class extends Schema3.TaggedError()(
|
|
1036
|
+
"ReviewError",
|
|
1037
|
+
{ code: Schema3.String, message: Schema3.String },
|
|
1038
|
+
HttpApiSchema.annotations({ status: 400 })
|
|
1039
|
+
) {
|
|
1040
|
+
};
|
|
1041
|
+
var getHeadSha = (repoPath) => Effect4.tryPromise({
|
|
1042
|
+
catch: () => new ReviewError({ code: "GIT_ERROR", message: "Failed to get HEAD" }),
|
|
1043
|
+
try: () => new Promise((resolve, reject) => {
|
|
1044
|
+
execFile(
|
|
1045
|
+
"git",
|
|
1046
|
+
["rev-parse", "HEAD"],
|
|
1047
|
+
{ cwd: repoPath },
|
|
1048
|
+
(err, stdout) => {
|
|
1049
|
+
if (err) {
|
|
1050
|
+
reject(err);
|
|
1051
|
+
} else {
|
|
1052
|
+
resolve(stdout.trim());
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
})
|
|
1057
|
+
});
|
|
1058
|
+
var parseSnapshotData = (s) => Effect4.try(() => JSON.parse(s)).pipe(
|
|
1059
|
+
Effect4.orElseSucceed(() => ({}))
|
|
1060
|
+
);
|
|
1061
|
+
var ReviewService = class extends Effect4.Service()(
|
|
1062
|
+
"@ringi/ReviewService",
|
|
1063
|
+
{
|
|
1064
|
+
dependencies: [
|
|
1065
|
+
ReviewRepo.Default,
|
|
1066
|
+
ReviewFileRepo.Default,
|
|
1067
|
+
GitService.Default
|
|
1068
|
+
],
|
|
1069
|
+
effect: Effect4.sync(() => {
|
|
1070
|
+
const create = Effect4.fn("ReviewService.create")(function* create2(input) {
|
|
1071
|
+
const git = yield* GitService;
|
|
1072
|
+
const repo = yield* ReviewRepo;
|
|
1073
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
1074
|
+
const repoPath = yield* git.getRepositoryPath;
|
|
1075
|
+
const hasCommitsResult = yield* git.hasCommits;
|
|
1076
|
+
if (!hasCommitsResult) {
|
|
1077
|
+
return yield* new ReviewError({
|
|
1078
|
+
code: "NO_COMMITS",
|
|
1079
|
+
message: "Repository has no commits"
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
let diffText;
|
|
1083
|
+
let baseRef = null;
|
|
1084
|
+
const { sourceType, sourceRef } = input;
|
|
1085
|
+
switch (sourceType) {
|
|
1086
|
+
case "staged": {
|
|
1087
|
+
diffText = yield* git.getStagedDiff;
|
|
1088
|
+
if (!diffText.trim()) {
|
|
1089
|
+
return yield* new ReviewError({
|
|
1090
|
+
code: "NO_STAGED_CHANGES",
|
|
1091
|
+
message: "No staged changes"
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
baseRef = yield* getHeadSha(repoPath);
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
case "branch": {
|
|
1098
|
+
if (!sourceRef) {
|
|
1099
|
+
return yield* new ReviewError({
|
|
1100
|
+
code: "INVALID_SOURCE",
|
|
1101
|
+
message: "Branch name required"
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
diffText = yield* git.getBranchDiff(sourceRef);
|
|
1105
|
+
baseRef = sourceRef;
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case "commits": {
|
|
1109
|
+
if (!sourceRef) {
|
|
1110
|
+
return yield* new ReviewError({
|
|
1111
|
+
code: "INVALID_SOURCE",
|
|
1112
|
+
message: "Commit SHAs required"
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const shas = sourceRef.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1116
|
+
if (shas.length === 0) {
|
|
1117
|
+
return yield* new ReviewError({
|
|
1118
|
+
code: "INVALID_SOURCE",
|
|
1119
|
+
message: "No valid commit SHAs"
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
diffText = yield* git.getCommitDiff(shas);
|
|
1123
|
+
baseRef = shas.at(-1) ?? null;
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
default: {
|
|
1127
|
+
return yield* new ReviewError({
|
|
1128
|
+
code: "INVALID_SOURCE",
|
|
1129
|
+
message: "Unsupported review source"
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const files = parseDiff(diffText);
|
|
1134
|
+
if (files.length === 0) {
|
|
1135
|
+
return yield* new ReviewError({
|
|
1136
|
+
code: "NO_CHANGES",
|
|
1137
|
+
message: "No changes found"
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
const repoInfo = yield* git.getRepositoryInfo;
|
|
1141
|
+
const reviewId = crypto.randomUUID();
|
|
1142
|
+
const storeHunks = sourceType === "staged";
|
|
1143
|
+
const fileInputs = files.map((f) => ({
|
|
1144
|
+
additions: f.additions,
|
|
1145
|
+
deletions: f.deletions,
|
|
1146
|
+
filePath: f.newPath,
|
|
1147
|
+
hunksData: storeHunks ? serializeHunks(f.hunks) : null,
|
|
1148
|
+
oldPath: f.oldPath !== f.newPath ? f.oldPath : null,
|
|
1149
|
+
reviewId,
|
|
1150
|
+
status: f.status
|
|
1151
|
+
}));
|
|
1152
|
+
const snapshotData = JSON.stringify({
|
|
1153
|
+
repository: repoInfo,
|
|
1154
|
+
version: 2
|
|
1155
|
+
});
|
|
1156
|
+
const review = yield* repo.create({
|
|
1157
|
+
baseRef,
|
|
1158
|
+
id: reviewId,
|
|
1159
|
+
repositoryPath: repoPath,
|
|
1160
|
+
snapshotData,
|
|
1161
|
+
sourceRef: sourceRef ?? null,
|
|
1162
|
+
sourceType,
|
|
1163
|
+
status: "in_progress"
|
|
1164
|
+
});
|
|
1165
|
+
yield* fileRepo.createBulk(fileInputs);
|
|
1166
|
+
return review;
|
|
1167
|
+
});
|
|
1168
|
+
const list = Effect4.fn("ReviewService.list")(function* list2(opts) {
|
|
1169
|
+
const repo = yield* ReviewRepo;
|
|
1170
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
1171
|
+
const page = opts.page ?? 1;
|
|
1172
|
+
const pageSize = opts.pageSize ?? 20;
|
|
1173
|
+
const result = yield* repo.findAll({
|
|
1174
|
+
page,
|
|
1175
|
+
pageSize,
|
|
1176
|
+
repositoryPath: opts.repositoryPath,
|
|
1177
|
+
sourceType: opts.sourceType,
|
|
1178
|
+
status: opts.status
|
|
1179
|
+
});
|
|
1180
|
+
const reviews = [];
|
|
1181
|
+
for (const review of result.data) {
|
|
1182
|
+
const fileCount = yield* fileRepo.countByReview(review.id);
|
|
1183
|
+
const snapshot = yield* parseSnapshotData(review.snapshotData);
|
|
1184
|
+
reviews.push({
|
|
1185
|
+
...review,
|
|
1186
|
+
fileCount,
|
|
1187
|
+
repository: snapshot.repository ?? null
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
return {
|
|
1191
|
+
hasMore: page * pageSize < result.total,
|
|
1192
|
+
page,
|
|
1193
|
+
pageSize,
|
|
1194
|
+
reviews,
|
|
1195
|
+
total: result.total
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
const getById = Effect4.fn("ReviewService.getById")(function* getById2(id) {
|
|
1199
|
+
const repo = yield* ReviewRepo;
|
|
1200
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
1201
|
+
const review = yield* repo.findById(id);
|
|
1202
|
+
if (!review) {
|
|
1203
|
+
return yield* new ReviewNotFound({ id });
|
|
1204
|
+
}
|
|
1205
|
+
const fileRows = yield* fileRepo.findByReview(id);
|
|
1206
|
+
const files = fileRows.map((r) => ({
|
|
1207
|
+
additions: r.additions,
|
|
1208
|
+
deletions: r.deletions,
|
|
1209
|
+
filePath: r.file_path,
|
|
1210
|
+
id: r.id,
|
|
1211
|
+
oldPath: r.old_path,
|
|
1212
|
+
status: r.status
|
|
1213
|
+
}));
|
|
1214
|
+
const snapshot = yield* parseSnapshotData(review.snapshotData);
|
|
1215
|
+
const summary = getDiffSummary(
|
|
1216
|
+
files.map((f) => ({
|
|
1217
|
+
additions: f.additions,
|
|
1218
|
+
deletions: f.deletions,
|
|
1219
|
+
hunks: [],
|
|
1220
|
+
newPath: f.filePath,
|
|
1221
|
+
oldPath: f.oldPath ?? f.filePath,
|
|
1222
|
+
status: f.status
|
|
1223
|
+
}))
|
|
1224
|
+
);
|
|
1225
|
+
return {
|
|
1226
|
+
...review,
|
|
1227
|
+
files,
|
|
1228
|
+
repository: snapshot.repository ?? null,
|
|
1229
|
+
summary
|
|
1230
|
+
};
|
|
1231
|
+
});
|
|
1232
|
+
const getFileHunks = Effect4.fn("ReviewService.getFileHunks")(
|
|
1233
|
+
function* getFileHunks2(reviewId, filePath) {
|
|
1234
|
+
const repo = yield* ReviewRepo;
|
|
1235
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
1236
|
+
const git = yield* GitService;
|
|
1237
|
+
const review = yield* repo.findById(reviewId);
|
|
1238
|
+
if (!review) {
|
|
1239
|
+
return yield* new ReviewNotFound({ id: reviewId });
|
|
1240
|
+
}
|
|
1241
|
+
const fileRecord = yield* fileRepo.findByReviewAndPath(
|
|
1242
|
+
reviewId,
|
|
1243
|
+
filePath
|
|
1244
|
+
);
|
|
1245
|
+
if (fileRecord?.hunks_data) {
|
|
1246
|
+
return yield* parseHunks2(fileRecord.hunks_data);
|
|
1247
|
+
}
|
|
1248
|
+
if (review.sourceType === "branch" && review.sourceRef) {
|
|
1249
|
+
const diff = yield* git.getBranchDiff(review.sourceRef);
|
|
1250
|
+
const diffFiles = parseDiff(diff);
|
|
1251
|
+
const file = diffFiles.find((f) => f.newPath === filePath);
|
|
1252
|
+
return file?.hunks ?? [];
|
|
1253
|
+
}
|
|
1254
|
+
if (review.sourceType === "commits" && review.sourceRef) {
|
|
1255
|
+
const shas = review.sourceRef.split(",").map((s) => s.trim());
|
|
1256
|
+
const diff = yield* git.getCommitDiff(shas);
|
|
1257
|
+
const diffFiles = parseDiff(diff);
|
|
1258
|
+
const file = diffFiles.find((f) => f.newPath === filePath);
|
|
1259
|
+
return file?.hunks ?? [];
|
|
1260
|
+
}
|
|
1261
|
+
const snapshot = yield* parseSnapshotData(review.snapshotData);
|
|
1262
|
+
if (snapshot.files) {
|
|
1263
|
+
const legacyFile = snapshot.files.find(
|
|
1264
|
+
(f) => f.newPath === filePath
|
|
1265
|
+
);
|
|
1266
|
+
return legacyFile?.hunks ?? [];
|
|
1267
|
+
}
|
|
1268
|
+
return [];
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
const update = Effect4.fn("ReviewService.update")(function* update2(id, input) {
|
|
1272
|
+
const repo = yield* ReviewRepo;
|
|
1273
|
+
const existing = yield* repo.findById(id);
|
|
1274
|
+
if (!existing) {
|
|
1275
|
+
return yield* new ReviewNotFound({ id });
|
|
1276
|
+
}
|
|
1277
|
+
const status = Option.getOrNull(input.status);
|
|
1278
|
+
const review = yield* repo.update(id, status);
|
|
1279
|
+
if (!review) {
|
|
1280
|
+
return yield* new ReviewNotFound({ id });
|
|
1281
|
+
}
|
|
1282
|
+
return review;
|
|
1283
|
+
});
|
|
1284
|
+
const remove = Effect4.fn("ReviewService.remove")(function* remove2(id) {
|
|
1285
|
+
const repo = yield* ReviewRepo;
|
|
1286
|
+
const fileRepo = yield* ReviewFileRepo;
|
|
1287
|
+
const existing = yield* repo.findById(id);
|
|
1288
|
+
if (!existing) {
|
|
1289
|
+
return yield* new ReviewNotFound({ id });
|
|
1290
|
+
}
|
|
1291
|
+
yield* fileRepo.deleteByReview(id);
|
|
1292
|
+
yield* repo.remove(id);
|
|
1293
|
+
return { success: true };
|
|
1294
|
+
});
|
|
1295
|
+
const getStats = Effect4.fn("ReviewService.getStats")(
|
|
1296
|
+
function* getStats2() {
|
|
1297
|
+
const repo = yield* ReviewRepo;
|
|
1298
|
+
const total = yield* repo.countAll();
|
|
1299
|
+
const inProgress = yield* repo.countByStatus("in_progress");
|
|
1300
|
+
const approved = yield* repo.countByStatus("approved");
|
|
1301
|
+
const changesRequested = yield* repo.countByStatus("changes_requested");
|
|
1302
|
+
return { approved, changesRequested, inProgress, total };
|
|
1303
|
+
}
|
|
1304
|
+
);
|
|
1305
|
+
return {
|
|
1306
|
+
create,
|
|
1307
|
+
getById,
|
|
1308
|
+
getFileHunks,
|
|
1309
|
+
getStats,
|
|
1310
|
+
list,
|
|
1311
|
+
remove,
|
|
1312
|
+
update
|
|
1313
|
+
};
|
|
1314
|
+
})
|
|
1315
|
+
}
|
|
1316
|
+
) {
|
|
1317
|
+
};
|
|
1318
|
+
var rowToTodo = (row) => ({
|
|
1319
|
+
completed: row.completed === 1,
|
|
1320
|
+
content: row.content,
|
|
1321
|
+
createdAt: row.created_at,
|
|
1322
|
+
id: row.id,
|
|
1323
|
+
position: row.position,
|
|
1324
|
+
reviewId: row.review_id,
|
|
1325
|
+
updatedAt: row.updated_at
|
|
1326
|
+
});
|
|
1327
|
+
var TodoRepo = class extends Effect4.Service()("@ringi/TodoRepo", {
|
|
1328
|
+
dependencies: [SqliteService.Default],
|
|
1329
|
+
effect: Effect4.gen(function* effect() {
|
|
1330
|
+
const { db } = yield* SqliteService;
|
|
1331
|
+
const stmtFindById = db.prepare("SELECT * FROM todos WHERE id = ?");
|
|
1332
|
+
const stmtInsert = db.prepare(
|
|
1333
|
+
`INSERT INTO todos (id, content, completed, review_id, position, created_at, updated_at)
|
|
1334
|
+
VALUES (?, ?, 0, ?, ?, datetime('now'), datetime('now'))`
|
|
1335
|
+
);
|
|
1336
|
+
const stmtDelete = db.prepare("DELETE FROM todos WHERE id = ?");
|
|
1337
|
+
const stmtDeleteCompleted = db.prepare(
|
|
1338
|
+
"DELETE FROM todos WHERE completed = 1"
|
|
1339
|
+
);
|
|
1340
|
+
const stmtNextPosition = db.prepare(
|
|
1341
|
+
"SELECT COALESCE(MAX(position), -1) + 1 AS next_pos FROM todos"
|
|
1342
|
+
);
|
|
1343
|
+
const stmtCountAll = db.prepare("SELECT COUNT(*) as count FROM todos");
|
|
1344
|
+
const stmtCountCompleted = db.prepare(
|
|
1345
|
+
"SELECT COUNT(*) as count FROM todos WHERE completed = 1"
|
|
1346
|
+
);
|
|
1347
|
+
const stmtCountPending = db.prepare(
|
|
1348
|
+
"SELECT COUNT(*) as count FROM todos WHERE completed = 0"
|
|
1349
|
+
);
|
|
1350
|
+
const findById = (id) => Effect4.sync(() => {
|
|
1351
|
+
const row = stmtFindById.get(id);
|
|
1352
|
+
return row ? rowToTodo(row) : null;
|
|
1353
|
+
});
|
|
1354
|
+
const findAll = (opts = {}) => Effect4.sync(() => {
|
|
1355
|
+
const conditions = [];
|
|
1356
|
+
const params = [];
|
|
1357
|
+
if (opts.reviewId != null) {
|
|
1358
|
+
conditions.push("review_id = ?");
|
|
1359
|
+
params.push(opts.reviewId);
|
|
1360
|
+
}
|
|
1361
|
+
if (opts.completed != null) {
|
|
1362
|
+
conditions.push("completed = ?");
|
|
1363
|
+
params.push(opts.completed ? 1 : 0);
|
|
1364
|
+
}
|
|
1365
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
1366
|
+
const totalRow = db.prepare(`SELECT COUNT(*) as count FROM todos${where}`).get(
|
|
1367
|
+
...params
|
|
1368
|
+
);
|
|
1369
|
+
const limitClause = opts.limit != null ? ` LIMIT ? OFFSET ?` : "";
|
|
1370
|
+
const queryParams = opts.limit != null ? [...params, opts.limit, opts.offset ?? 0] : params;
|
|
1371
|
+
const rows = db.prepare(
|
|
1372
|
+
`SELECT * FROM todos${where} ORDER BY position ASC${limitClause}`
|
|
1373
|
+
).all(
|
|
1374
|
+
...queryParams
|
|
1375
|
+
);
|
|
1376
|
+
return { data: rows.map(rowToTodo), total: totalRow.count };
|
|
1377
|
+
});
|
|
1378
|
+
const create = (input) => Effect4.sync(() => {
|
|
1379
|
+
const { next_pos } = stmtNextPosition.get();
|
|
1380
|
+
stmtInsert.run(input.id, input.content, input.reviewId, next_pos);
|
|
1381
|
+
return rowToTodo(stmtFindById.get(input.id));
|
|
1382
|
+
});
|
|
1383
|
+
const update = (id, updates) => Effect4.sync(() => {
|
|
1384
|
+
const sets = [];
|
|
1385
|
+
const params = [];
|
|
1386
|
+
if (updates.content != null) {
|
|
1387
|
+
sets.push("content = ?");
|
|
1388
|
+
params.push(updates.content);
|
|
1389
|
+
}
|
|
1390
|
+
if (updates.completed != null) {
|
|
1391
|
+
sets.push("completed = ?");
|
|
1392
|
+
params.push(updates.completed ? 1 : 0);
|
|
1393
|
+
}
|
|
1394
|
+
if (sets.length === 0) {
|
|
1395
|
+
const row2 = stmtFindById.get(id);
|
|
1396
|
+
return row2 ? rowToTodo(row2) : null;
|
|
1397
|
+
}
|
|
1398
|
+
sets.push("updated_at = datetime('now')");
|
|
1399
|
+
params.push(id);
|
|
1400
|
+
db.prepare(`UPDATE todos SET ${sets.join(", ")} WHERE id = ?`).run(
|
|
1401
|
+
...params
|
|
1402
|
+
);
|
|
1403
|
+
const row = stmtFindById.get(id);
|
|
1404
|
+
return row ? rowToTodo(row) : null;
|
|
1405
|
+
});
|
|
1406
|
+
const toggle = (id) => Effect4.sync(() => {
|
|
1407
|
+
const row = stmtFindById.get(id);
|
|
1408
|
+
if (!row) {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
const newCompleted = row.completed === 1 ? 0 : 1;
|
|
1412
|
+
db.prepare(
|
|
1413
|
+
"UPDATE todos SET completed = ?, updated_at = datetime('now') WHERE id = ?"
|
|
1414
|
+
).run(newCompleted, id);
|
|
1415
|
+
return rowToTodo(stmtFindById.get(id));
|
|
1416
|
+
});
|
|
1417
|
+
const remove = (id) => Effect4.sync(() => {
|
|
1418
|
+
const result = stmtDelete.run(id);
|
|
1419
|
+
return Number(result.changes) > 0;
|
|
1420
|
+
});
|
|
1421
|
+
const removeCompleted = () => Effect4.sync(() => {
|
|
1422
|
+
const result = stmtDeleteCompleted.run();
|
|
1423
|
+
return Number(result.changes);
|
|
1424
|
+
});
|
|
1425
|
+
const reorder = (orderedIds) => withTransaction(
|
|
1426
|
+
db,
|
|
1427
|
+
Effect4.sync(() => {
|
|
1428
|
+
const stmt = db.prepare(
|
|
1429
|
+
"UPDATE todos SET position = ?, updated_at = datetime('now') WHERE id = ?"
|
|
1430
|
+
);
|
|
1431
|
+
let updated = 0;
|
|
1432
|
+
for (let i = 0; i < orderedIds.length; i++) {
|
|
1433
|
+
const result = stmt.run(i, orderedIds[i]);
|
|
1434
|
+
updated += Number(result.changes);
|
|
1435
|
+
}
|
|
1436
|
+
return updated;
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
const move = (id, newPosition) => Effect4.gen(function* move2() {
|
|
1440
|
+
const row = stmtFindById.get(id);
|
|
1441
|
+
if (!row) {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
const oldPosition = row.position;
|
|
1445
|
+
yield* withTransaction(
|
|
1446
|
+
db,
|
|
1447
|
+
Effect4.sync(() => {
|
|
1448
|
+
if (newPosition < oldPosition) {
|
|
1449
|
+
db.prepare(
|
|
1450
|
+
"UPDATE todos SET position = position + 1, updated_at = datetime('now') WHERE position >= ? AND position < ? AND id != ?"
|
|
1451
|
+
).run(newPosition, oldPosition, id);
|
|
1452
|
+
} else if (newPosition > oldPosition) {
|
|
1453
|
+
db.prepare(
|
|
1454
|
+
"UPDATE todos SET position = position - 1, updated_at = datetime('now') WHERE position > ? AND position <= ? AND id != ?"
|
|
1455
|
+
).run(oldPosition, newPosition, id);
|
|
1456
|
+
}
|
|
1457
|
+
db.prepare(
|
|
1458
|
+
"UPDATE todos SET position = ?, updated_at = datetime('now') WHERE id = ?"
|
|
1459
|
+
).run(newPosition, id);
|
|
1460
|
+
})
|
|
1461
|
+
);
|
|
1462
|
+
return rowToTodo(stmtFindById.get(id));
|
|
1463
|
+
});
|
|
1464
|
+
const countAll = () => Effect4.sync(() => {
|
|
1465
|
+
const row = stmtCountAll.get();
|
|
1466
|
+
return row.count;
|
|
1467
|
+
});
|
|
1468
|
+
const countCompleted = () => Effect4.sync(() => {
|
|
1469
|
+
const row = stmtCountCompleted.get();
|
|
1470
|
+
return row.count;
|
|
1471
|
+
});
|
|
1472
|
+
const countPending = () => Effect4.sync(() => {
|
|
1473
|
+
const row = stmtCountPending.get();
|
|
1474
|
+
return row.count;
|
|
1475
|
+
});
|
|
1476
|
+
return {
|
|
1477
|
+
countAll,
|
|
1478
|
+
countCompleted,
|
|
1479
|
+
countPending,
|
|
1480
|
+
create,
|
|
1481
|
+
findAll,
|
|
1482
|
+
findById,
|
|
1483
|
+
move,
|
|
1484
|
+
remove,
|
|
1485
|
+
removeCompleted,
|
|
1486
|
+
reorder,
|
|
1487
|
+
toggle,
|
|
1488
|
+
update
|
|
1489
|
+
};
|
|
1490
|
+
})
|
|
1491
|
+
}) {
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/core/services/todo.service.ts
|
|
1495
|
+
var TodoService = class extends Effect4.Service()(
|
|
1496
|
+
"@ringi/TodoService",
|
|
1497
|
+
{
|
|
1498
|
+
dependencies: [TodoRepo.Default],
|
|
1499
|
+
effect: Effect4.sync(() => {
|
|
1500
|
+
const create = Effect4.fn("TodoService.create")(function* create2(input) {
|
|
1501
|
+
const repo = yield* TodoRepo;
|
|
1502
|
+
const id = randomUUID();
|
|
1503
|
+
return yield* repo.create({
|
|
1504
|
+
content: input.content,
|
|
1505
|
+
id,
|
|
1506
|
+
reviewId: input.reviewId
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1509
|
+
const getById = Effect4.fn("TodoService.getById")(function* getById2(id) {
|
|
1510
|
+
const repo = yield* TodoRepo;
|
|
1511
|
+
const todo = yield* repo.findById(id);
|
|
1512
|
+
if (!todo) {
|
|
1513
|
+
return yield* new TodoNotFound({ id });
|
|
1514
|
+
}
|
|
1515
|
+
return todo;
|
|
1516
|
+
});
|
|
1517
|
+
const list = Effect4.fn("TodoService.list")(function* list2(opts = {}) {
|
|
1518
|
+
const repo = yield* TodoRepo;
|
|
1519
|
+
const result = yield* repo.findAll(opts);
|
|
1520
|
+
return {
|
|
1521
|
+
data: result.data,
|
|
1522
|
+
limit: opts.limit ?? null,
|
|
1523
|
+
offset: opts.offset ?? 0,
|
|
1524
|
+
total: result.total
|
|
1525
|
+
};
|
|
1526
|
+
});
|
|
1527
|
+
const update = Effect4.fn("TodoService.update")(function* update2(id, input) {
|
|
1528
|
+
const repo = yield* TodoRepo;
|
|
1529
|
+
const existing = yield* repo.findById(id);
|
|
1530
|
+
if (!existing) {
|
|
1531
|
+
return yield* new TodoNotFound({ id });
|
|
1532
|
+
}
|
|
1533
|
+
const updates = {};
|
|
1534
|
+
if (Option.isSome(input.content)) {
|
|
1535
|
+
updates.content = input.content.value;
|
|
1536
|
+
}
|
|
1537
|
+
if (Option.isSome(input.completed)) {
|
|
1538
|
+
updates.completed = input.completed.value;
|
|
1539
|
+
}
|
|
1540
|
+
const todo = yield* repo.update(id, updates);
|
|
1541
|
+
if (!todo) {
|
|
1542
|
+
return yield* new TodoNotFound({ id });
|
|
1543
|
+
}
|
|
1544
|
+
return todo;
|
|
1545
|
+
});
|
|
1546
|
+
const toggle = Effect4.fn("TodoService.toggle")(function* toggle2(id) {
|
|
1547
|
+
const repo = yield* TodoRepo;
|
|
1548
|
+
const todo = yield* repo.toggle(id);
|
|
1549
|
+
if (!todo) {
|
|
1550
|
+
return yield* new TodoNotFound({ id });
|
|
1551
|
+
}
|
|
1552
|
+
return todo;
|
|
1553
|
+
});
|
|
1554
|
+
const remove = Effect4.fn("TodoService.remove")(function* remove2(id) {
|
|
1555
|
+
const repo = yield* TodoRepo;
|
|
1556
|
+
const existing = yield* repo.findById(id);
|
|
1557
|
+
if (!existing) {
|
|
1558
|
+
return yield* new TodoNotFound({ id });
|
|
1559
|
+
}
|
|
1560
|
+
yield* repo.remove(id);
|
|
1561
|
+
return { success: true };
|
|
1562
|
+
});
|
|
1563
|
+
const removeCompleted = Effect4.fn("TodoService.removeCompleted")(
|
|
1564
|
+
function* removeCompleted2() {
|
|
1565
|
+
const repo = yield* TodoRepo;
|
|
1566
|
+
const deleted = yield* repo.removeCompleted();
|
|
1567
|
+
return { deleted };
|
|
1568
|
+
}
|
|
1569
|
+
);
|
|
1570
|
+
const reorder = Effect4.fn("TodoService.reorder")(function* reorder2(orderedIds) {
|
|
1571
|
+
const repo = yield* TodoRepo;
|
|
1572
|
+
const updated = yield* repo.reorder(orderedIds);
|
|
1573
|
+
return { updated };
|
|
1574
|
+
});
|
|
1575
|
+
const move = Effect4.fn("TodoService.move")(function* move2(id, position) {
|
|
1576
|
+
const repo = yield* TodoRepo;
|
|
1577
|
+
const todo = yield* repo.move(id, position);
|
|
1578
|
+
if (!todo) {
|
|
1579
|
+
return yield* new TodoNotFound({ id });
|
|
1580
|
+
}
|
|
1581
|
+
return todo;
|
|
1582
|
+
});
|
|
1583
|
+
const getStats = Effect4.fn("TodoService.getStats")(function* getStats2() {
|
|
1584
|
+
const repo = yield* TodoRepo;
|
|
1585
|
+
const total = yield* repo.countAll();
|
|
1586
|
+
const completed = yield* repo.countCompleted();
|
|
1587
|
+
const pending = yield* repo.countPending();
|
|
1588
|
+
return { completed, pending, total };
|
|
1589
|
+
});
|
|
1590
|
+
return {
|
|
1591
|
+
create,
|
|
1592
|
+
getById,
|
|
1593
|
+
getStats,
|
|
1594
|
+
list,
|
|
1595
|
+
move,
|
|
1596
|
+
remove,
|
|
1597
|
+
removeCompleted,
|
|
1598
|
+
reorder,
|
|
1599
|
+
toggle,
|
|
1600
|
+
update
|
|
1601
|
+
};
|
|
1602
|
+
})
|
|
1603
|
+
}
|
|
1604
|
+
) {
|
|
1605
|
+
};
|
|
1606
|
+
var ExportService = class extends Effect4.Service()(
|
|
1607
|
+
"@ringi/ExportService",
|
|
1608
|
+
{
|
|
1609
|
+
dependencies: [
|
|
1610
|
+
ReviewService.Default,
|
|
1611
|
+
CommentService.Default,
|
|
1612
|
+
TodoService.Default
|
|
1613
|
+
],
|
|
1614
|
+
effect: Effect4.sync(() => {
|
|
1615
|
+
const exportReview = Effect4.fn("ExportService.exportReview")(
|
|
1616
|
+
function* exportReview2(reviewId) {
|
|
1617
|
+
const reviewSvc = yield* ReviewService;
|
|
1618
|
+
const commentSvc = yield* CommentService;
|
|
1619
|
+
const todoSvc = yield* TodoService;
|
|
1620
|
+
const review = yield* reviewSvc.getById(reviewId);
|
|
1621
|
+
const repo = review.repository;
|
|
1622
|
+
const repoName = repo?.name ?? "Unknown";
|
|
1623
|
+
const branch = repo?.branch ?? "unknown";
|
|
1624
|
+
const comments = yield* commentSvc.getByReview(reviewId);
|
|
1625
|
+
const commentStats = yield* commentSvc.getStats(reviewId);
|
|
1626
|
+
const todos = yield* todoSvc.list({ reviewId });
|
|
1627
|
+
const lines2 = [];
|
|
1628
|
+
lines2.push(`# Code Review: ${repoName}`);
|
|
1629
|
+
lines2.push("");
|
|
1630
|
+
lines2.push(`**Status:** ${review.status}`);
|
|
1631
|
+
lines2.push(`**Branch:** ${branch}`);
|
|
1632
|
+
lines2.push(`**Created:** ${review.createdAt}`);
|
|
1633
|
+
if (review.files && review.files.length > 0) {
|
|
1634
|
+
lines2.push("");
|
|
1635
|
+
lines2.push("## Files Changed");
|
|
1636
|
+
lines2.push("");
|
|
1637
|
+
lines2.push("| File | Status | Additions | Deletions |");
|
|
1638
|
+
lines2.push("|------|--------|-----------|-----------|");
|
|
1639
|
+
for (const f of review.files) {
|
|
1640
|
+
const statusLabel = f.status === "modified" ? "M" : f.status === "added" ? "A" : f.status === "deleted" ? "D" : f.status;
|
|
1641
|
+
lines2.push(
|
|
1642
|
+
`| ${f.filePath} | ${statusLabel} | +${f.additions} | -${f.deletions} |`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (comments.length > 0) {
|
|
1647
|
+
lines2.push("");
|
|
1648
|
+
lines2.push(
|
|
1649
|
+
`## Comments (${commentStats.total} total, ${commentStats.resolved} resolved)`
|
|
1650
|
+
);
|
|
1651
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1652
|
+
for (const c of comments) {
|
|
1653
|
+
const key = c.filePath ?? "(general)";
|
|
1654
|
+
const arr = byFile.get(key) ?? [];
|
|
1655
|
+
arr.push(c);
|
|
1656
|
+
byFile.set(key, arr);
|
|
1657
|
+
}
|
|
1658
|
+
for (const [filePath, fileComments] of byFile) {
|
|
1659
|
+
lines2.push("");
|
|
1660
|
+
lines2.push(`### ${filePath}`);
|
|
1661
|
+
for (const c of fileComments) {
|
|
1662
|
+
lines2.push("");
|
|
1663
|
+
lines2.push(
|
|
1664
|
+
`**Line ${c.lineNumber ?? "\u2013"}** (${c.lineType ?? "context"})`
|
|
1665
|
+
);
|
|
1666
|
+
lines2.push(`> ${c.content}`);
|
|
1667
|
+
if (c.suggestion) {
|
|
1668
|
+
lines2.push("");
|
|
1669
|
+
lines2.push("```suggestion");
|
|
1670
|
+
lines2.push(c.suggestion);
|
|
1671
|
+
lines2.push("```");
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (todos.data.length > 0) {
|
|
1677
|
+
const completed = todos.data.filter((t) => t.completed).length;
|
|
1678
|
+
lines2.push("");
|
|
1679
|
+
lines2.push("---");
|
|
1680
|
+
lines2.push("");
|
|
1681
|
+
lines2.push(
|
|
1682
|
+
`## Todos (${todos.total} total, ${completed} completed)`
|
|
1683
|
+
);
|
|
1684
|
+
lines2.push("");
|
|
1685
|
+
for (const t of todos.data) {
|
|
1686
|
+
const check = t.completed ? "x" : " ";
|
|
1687
|
+
lines2.push(`- [${check}] ${t.content}`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
lines2.push("");
|
|
1691
|
+
return lines2.join("\n");
|
|
1692
|
+
}
|
|
1693
|
+
);
|
|
1694
|
+
return { exportReview };
|
|
1695
|
+
})
|
|
1696
|
+
}
|
|
1697
|
+
) {
|
|
1698
|
+
};
|
|
1699
|
+
var EventService = class extends Effect4.Service()(
|
|
1700
|
+
"@ringi/EventService",
|
|
1701
|
+
{
|
|
1702
|
+
effect: Effect4.gen(function* effect() {
|
|
1703
|
+
const rt = yield* Effect4.runtime();
|
|
1704
|
+
const runFork2 = Runtime.runFork(rt);
|
|
1705
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1706
|
+
const broadcast = Effect4.fn("EventService.broadcast")(function* broadcast2(type, data) {
|
|
1707
|
+
const event = { data, timestamp: Date.now(), type };
|
|
1708
|
+
for (const queue of subscribers) {
|
|
1709
|
+
yield* Queue.offer(queue, event);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
const subscribe = Effect4.fn("EventService.subscribe")(
|
|
1713
|
+
function* subscribe2() {
|
|
1714
|
+
const queue = yield* Queue.sliding(100);
|
|
1715
|
+
subscribers.add(queue);
|
|
1716
|
+
const stream = Stream.fromQueue(queue);
|
|
1717
|
+
const unsubscribe = Effect4.sync(() => {
|
|
1718
|
+
subscribers.delete(queue);
|
|
1719
|
+
}).pipe(Effect4.andThen(Queue.shutdown(queue)));
|
|
1720
|
+
return { stream, unsubscribe };
|
|
1721
|
+
}
|
|
1722
|
+
);
|
|
1723
|
+
const startFileWatcher = (repoPath) => Effect4.acquireRelease(
|
|
1724
|
+
Effect4.sync(() => {
|
|
1725
|
+
let debounceTimer = null;
|
|
1726
|
+
const watcher = chokidar.watch(repoPath, {
|
|
1727
|
+
ignoreInitial: true,
|
|
1728
|
+
ignored: [
|
|
1729
|
+
"**/node_modules/**",
|
|
1730
|
+
"**/.git/**",
|
|
1731
|
+
"**/.ringi/**",
|
|
1732
|
+
"**/dist/**"
|
|
1733
|
+
],
|
|
1734
|
+
persistent: true,
|
|
1735
|
+
...platform() === "darwin" ? { interval: 1e3, usePolling: true } : {}
|
|
1736
|
+
});
|
|
1737
|
+
const debouncedBroadcast = (filePath) => {
|
|
1738
|
+
if (debounceTimer) {
|
|
1739
|
+
clearTimeout(debounceTimer);
|
|
1740
|
+
}
|
|
1741
|
+
debounceTimer = setTimeout(() => {
|
|
1742
|
+
const rel = relative(repoPath, filePath);
|
|
1743
|
+
runFork2(broadcast("files", { path: rel }));
|
|
1744
|
+
}, 300);
|
|
1745
|
+
};
|
|
1746
|
+
watcher.on("add", debouncedBroadcast);
|
|
1747
|
+
watcher.on("change", debouncedBroadcast);
|
|
1748
|
+
watcher.on("unlink", debouncedBroadcast);
|
|
1749
|
+
return watcher;
|
|
1750
|
+
}),
|
|
1751
|
+
(watcher) => Effect4.promise(() => watcher.close())
|
|
1752
|
+
);
|
|
1753
|
+
const getClientCount = () => Effect4.sync(() => subscribers.size);
|
|
1754
|
+
return {
|
|
1755
|
+
broadcast,
|
|
1756
|
+
getClientCount,
|
|
1757
|
+
startFileWatcher,
|
|
1758
|
+
subscribe
|
|
1759
|
+
};
|
|
1760
|
+
})
|
|
1761
|
+
}
|
|
1762
|
+
) {
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/core/runtime.ts
|
|
1766
|
+
var CoreLive = Layer.mergeAll(
|
|
1767
|
+
ReviewService.Default,
|
|
1768
|
+
ReviewRepo.Default,
|
|
1769
|
+
ReviewFileRepo.Default,
|
|
1770
|
+
CommentService.Default,
|
|
1771
|
+
CommentRepo.Default,
|
|
1772
|
+
TodoService.Default,
|
|
1773
|
+
TodoRepo.Default,
|
|
1774
|
+
GitService.Default,
|
|
1775
|
+
EventService.Default,
|
|
1776
|
+
ExportService.Default,
|
|
1777
|
+
SqliteService.Default
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
export { CommentService, CoreLive, ExportService, GitService, ReviewId, ReviewNotFound, ReviewService, ReviewSourceType, TodoId, TodoNotFound, TodoService, getDiffSummary, parseDiff };
|
|
1781
|
+
//# sourceMappingURL=chunk-3JLVANJR.js.map
|
|
1782
|
+
//# sourceMappingURL=chunk-3JLVANJR.js.map
|