@ridit/forge 0.2.6
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/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/README.md +87 -0
- package/dist/index.mjs +45649 -0
- package/index.ts +1 -0
- package/package.json +40 -0
- package/src/colors.ts +10 -0
- package/src/commands/add.tsx +152 -0
- package/src/commands/branch.tsx +150 -0
- package/src/commands/checkout.tsx +55 -0
- package/src/commands/commit.tsx +77 -0
- package/src/commands/init.tsx +45 -0
- package/src/commands/log.tsx +99 -0
- package/src/commands/status.tsx +77 -0
- package/src/index.tsx +80 -0
- package/src/types/add.ts +4 -0
- package/src/types/branch.ts +6 -0
- package/src/types/checkpoint.ts +7 -0
- package/src/types/commit.ts +16 -0
- package/src/types/files.ts +13 -0
- package/src/types/repo.ts +5 -0
- package/src/utils/add.ts +126 -0
- package/src/utils/branch.ts +590 -0
- package/src/utils/checkout.ts +57 -0
- package/src/utils/checkpoint.ts +52 -0
- package/src/utils/commit.ts +237 -0
- package/src/utils/forgeIgnore.ts +56 -0
- package/src/utils/objects.ts +28 -0
- package/src/utils/repo.ts +64 -0
- package/src/utils/status.ts +46 -0
- package/src/utils/switchEvents.ts +10 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import type { Branch } from "../types/branch";
|
|
4
|
+
import type { Repo } from "../types/repo";
|
|
5
|
+
import type { Commit } from "../types/commit";
|
|
6
|
+
import { matchPaths } from "./forgeIgnore";
|
|
7
|
+
import { createCheckpoint } from "./checkpoint";
|
|
8
|
+
import type { FileBlob } from "../types/files";
|
|
9
|
+
import { determineFileStatus } from "./add";
|
|
10
|
+
import { commitInBranch, getCommit, getLatestCommitId } from "./commit";
|
|
11
|
+
import { updateRepo } from "./repo";
|
|
12
|
+
import type { Checkpoint } from "../types/checkpoint";
|
|
13
|
+
import { switchEmitter } from "./switchEvents";
|
|
14
|
+
import { checkoutCommit } from "./checkout";
|
|
15
|
+
import { readObject, writeObject } from "./objects";
|
|
16
|
+
|
|
17
|
+
function getBranchPaths(repo_path: string, branch_name?: string) {
|
|
18
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
19
|
+
const branchesFolder = path.join(forgeFolder, "branches");
|
|
20
|
+
const branchFolder = branch_name
|
|
21
|
+
? path.join(branchesFolder, branch_name)
|
|
22
|
+
: undefined;
|
|
23
|
+
const branchDataFile = branchFolder
|
|
24
|
+
? path.join(branchFolder, "branch.json")
|
|
25
|
+
: undefined;
|
|
26
|
+
return { forgeFolder, branchesFolder, branchFolder, branchDataFile };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createBranch(
|
|
30
|
+
repo_path: string,
|
|
31
|
+
branch_name: string,
|
|
32
|
+
): { status: "ok" | "error"; error?: string } {
|
|
33
|
+
const { branchDataFile, branchFolder, branchesFolder } = getBranchPaths(
|
|
34
|
+
repo_path,
|
|
35
|
+
branch_name,
|
|
36
|
+
);
|
|
37
|
+
const branchCommitsFolder = path.join(branchFolder!, "commits");
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(branchesFolder))
|
|
40
|
+
fs.mkdirSync(branchesFolder, { recursive: true });
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(branchFolder!, { recursive: true });
|
|
44
|
+
fs.mkdirSync(branchCommitsFolder, { recursive: true });
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
branchDataFile!,
|
|
48
|
+
JSON.stringify({ name: branch_name, latestCommitId: "" } as Branch),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const currentBranch = getCurrentBranch(repo_path);
|
|
52
|
+
if (currentBranch.error || !currentBranch.branch) {
|
|
53
|
+
commitInBranch("Initial commit", repo_path, branch_name);
|
|
54
|
+
} else {
|
|
55
|
+
const branch = currentBranch.branch;
|
|
56
|
+
const commitsFolder = path.join(
|
|
57
|
+
repo_path,
|
|
58
|
+
".forge",
|
|
59
|
+
"branches",
|
|
60
|
+
branch.name,
|
|
61
|
+
"commits",
|
|
62
|
+
);
|
|
63
|
+
const newBranchCommitsFolder = path.join(
|
|
64
|
+
repo_path,
|
|
65
|
+
".forge",
|
|
66
|
+
"branches",
|
|
67
|
+
branch_name,
|
|
68
|
+
"commits",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
fs.cpSync(commitsFolder, newBranchCommitsFolder, { recursive: true });
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
branchDataFile!,
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
name: branch_name,
|
|
77
|
+
latestCommitId: branch.latestCommitId,
|
|
78
|
+
} as Branch),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { status: "error", error: err as string };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { status: "ok" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getBranch(
|
|
89
|
+
repo_path: string,
|
|
90
|
+
branch_name: string,
|
|
91
|
+
): { status: "ok" | "error"; branch?: Branch; error?: string } {
|
|
92
|
+
const { branchDataFile, branchFolder, branchesFolder } = getBranchPaths(
|
|
93
|
+
repo_path,
|
|
94
|
+
branch_name,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(branchesFolder))
|
|
98
|
+
return {
|
|
99
|
+
status: "error",
|
|
100
|
+
error: "branches folder is missing, consider reinitialize repo.",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(branchFolder!))
|
|
104
|
+
return {
|
|
105
|
+
status: "error",
|
|
106
|
+
error: `${branch_name} doesn't exists.`,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(branchDataFile!))
|
|
110
|
+
return {
|
|
111
|
+
status: "error",
|
|
112
|
+
error: `${branch_name} meta data is missing, consider reinitialize the branch.`,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const branchData = JSON.parse(fs.readFileSync(branchDataFile!, "utf-8"));
|
|
116
|
+
|
|
117
|
+
return { status: "ok", branch: branchData };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function updateBranch(
|
|
121
|
+
repo_path: string,
|
|
122
|
+
branch_name: string,
|
|
123
|
+
latest_commit_id: string,
|
|
124
|
+
): { status: "ok" | "error"; error?: string } {
|
|
125
|
+
const { branchDataFile, branchFolder, branchesFolder } = getBranchPaths(
|
|
126
|
+
repo_path,
|
|
127
|
+
branch_name,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!fs.existsSync(branchesFolder))
|
|
131
|
+
return {
|
|
132
|
+
status: "error",
|
|
133
|
+
error: "branches folder is missing, consider reinitialize repo.",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(branchFolder!))
|
|
137
|
+
return {
|
|
138
|
+
status: "error",
|
|
139
|
+
error: `${branch_name} doesn't exists.`,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(branchDataFile!))
|
|
143
|
+
return {
|
|
144
|
+
status: "error",
|
|
145
|
+
error: `${branch_name} meta data is missing, consider reinitialize the branch.`,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const branchData = JSON.parse(
|
|
149
|
+
fs.readFileSync(branchDataFile!, "utf-8"),
|
|
150
|
+
) as Branch;
|
|
151
|
+
const newData = { ...branchData, latestCommitId: latest_commit_id } as Branch;
|
|
152
|
+
|
|
153
|
+
fs.writeFileSync(branchDataFile!, JSON.stringify(newData));
|
|
154
|
+
|
|
155
|
+
return { status: "ok" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getCurrentBranch(repo_path: string): {
|
|
159
|
+
status: "ok" | "error";
|
|
160
|
+
branch?: Branch;
|
|
161
|
+
error?: string;
|
|
162
|
+
} {
|
|
163
|
+
const { branchesFolder } = getBranchPaths(repo_path);
|
|
164
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
165
|
+
const repoDataFile = path.join(forgeFolder, "repo.json");
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(branchesFolder))
|
|
168
|
+
return {
|
|
169
|
+
status: "error",
|
|
170
|
+
error: "branches folder is missing, consider reinitialize repo.",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(repoDataFile))
|
|
174
|
+
return {
|
|
175
|
+
status: "error",
|
|
176
|
+
error: `repo meta data is missing, consider reinitialize the branch.`,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const branchName = (
|
|
180
|
+
JSON.parse(fs.readFileSync(repoDataFile, "utf-8")) as Repo
|
|
181
|
+
).branch;
|
|
182
|
+
|
|
183
|
+
const branchFolder = path.join(branchesFolder, branchName);
|
|
184
|
+
const branchDataFile = path.join(branchFolder, "branch.json");
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(branchFolder))
|
|
187
|
+
return {
|
|
188
|
+
status: "error",
|
|
189
|
+
error: `${branchName} doesn't exists, consider reinitializing the repo.`,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (!fs.existsSync(branchDataFile))
|
|
193
|
+
return {
|
|
194
|
+
status: "error",
|
|
195
|
+
error: `${branchName} meta data is missing, consider reinitialize the branch.`,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const branchData = JSON.parse(
|
|
199
|
+
fs.readFileSync(branchDataFile, "utf-8"),
|
|
200
|
+
) as Branch;
|
|
201
|
+
|
|
202
|
+
return { status: "ok", branch: branchData };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function listBranches(repo_path: string): {
|
|
206
|
+
status: "ok" | "error";
|
|
207
|
+
branches?: Branch[];
|
|
208
|
+
error?: string;
|
|
209
|
+
} {
|
|
210
|
+
const { branchesFolder } = getBranchPaths(repo_path);
|
|
211
|
+
|
|
212
|
+
if (!fs.existsSync(branchesFolder))
|
|
213
|
+
return {
|
|
214
|
+
status: "error",
|
|
215
|
+
error: "branches folder is missing, consider reinitialize repo.",
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const files = fs.readdirSync(branchesFolder);
|
|
219
|
+
const branches = files.map((file) => {
|
|
220
|
+
const branch = JSON.parse(
|
|
221
|
+
fs.readFileSync(
|
|
222
|
+
path.join(branchesFolder, file.toString(), "branch.json"),
|
|
223
|
+
"utf-8",
|
|
224
|
+
),
|
|
225
|
+
) as Branch;
|
|
226
|
+
return branch;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return { status: "ok", branches };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function processFiles(basePath: string, files: (string | Buffer)[]) {
|
|
233
|
+
const normalized = files
|
|
234
|
+
.map((f) => path.join(basePath, f.toString()).replace(/\\/g, "/"))
|
|
235
|
+
.filter((p) => {
|
|
236
|
+
try {
|
|
237
|
+
return fs.statSync(p).isFile();
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const checkedFiles = matchPaths(normalized);
|
|
244
|
+
|
|
245
|
+
return checkedFiles;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function switchBranch(
|
|
249
|
+
repo_path: string,
|
|
250
|
+
branch_name: string,
|
|
251
|
+
): { status: "ok" | "error"; error?: string } {
|
|
252
|
+
const currentBranch = getCurrentBranch(repo_path);
|
|
253
|
+
if (currentBranch.error || !currentBranch.branch)
|
|
254
|
+
return { status: "error", error: "no current branch found" };
|
|
255
|
+
|
|
256
|
+
const list = fs.readdirSync(".", { recursive: true });
|
|
257
|
+
const files = processFiles(".", list);
|
|
258
|
+
|
|
259
|
+
const latestCommitId = getLatestCommitId(
|
|
260
|
+
repo_path,
|
|
261
|
+
currentBranch.branch.name,
|
|
262
|
+
);
|
|
263
|
+
if (latestCommitId.error)
|
|
264
|
+
return { status: "error", error: latestCommitId.error };
|
|
265
|
+
|
|
266
|
+
const fileBlobs = files.files?.map((f) => ({
|
|
267
|
+
name: path.basename(f),
|
|
268
|
+
path: f,
|
|
269
|
+
hash: writeObject(repo_path, fs.readFileSync(f).toString()),
|
|
270
|
+
status: determineFileStatus(repo_path, f).file_status ?? "untracked",
|
|
271
|
+
})) as FileBlob[];
|
|
272
|
+
|
|
273
|
+
const res = createCheckpoint(
|
|
274
|
+
repo_path,
|
|
275
|
+
fileBlobs,
|
|
276
|
+
currentBranch.branch.name,
|
|
277
|
+
latestCommitId.latestCommitId ?? "",
|
|
278
|
+
);
|
|
279
|
+
if (res.error) return { status: "error", error: res.error };
|
|
280
|
+
switchEmitter.emit("event", {
|
|
281
|
+
type: "checkpoint_created",
|
|
282
|
+
branch: currentBranch.branch.name,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
files.files?.forEach((f) => fs.rmSync(f, { recursive: true }));
|
|
286
|
+
switchEmitter.emit("event", {
|
|
287
|
+
type: "files_deleted",
|
|
288
|
+
files: files.files ?? [],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const newBranch = getBranch(repo_path, branch_name);
|
|
292
|
+
if (newBranch.error || !newBranch.branch)
|
|
293
|
+
return {
|
|
294
|
+
status: "error",
|
|
295
|
+
error: newBranch.error || `${branch_name} not found`,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const latestCommit = newBranch.branch.latestCommitId
|
|
299
|
+
? getCommit(repo_path, newBranch.branch.latestCommitId, branch_name)
|
|
300
|
+
: null;
|
|
301
|
+
if (latestCommit?.error)
|
|
302
|
+
return { status: "error", error: latestCommit.error };
|
|
303
|
+
|
|
304
|
+
const checkpointFile = path.join(
|
|
305
|
+
repo_path,
|
|
306
|
+
".forge",
|
|
307
|
+
"branches",
|
|
308
|
+
branch_name,
|
|
309
|
+
"checkpoint.json",
|
|
310
|
+
);
|
|
311
|
+
const hasCheckpoint = fs.existsSync(checkpointFile);
|
|
312
|
+
|
|
313
|
+
if (hasCheckpoint) {
|
|
314
|
+
const checkpointData = JSON.parse(
|
|
315
|
+
fs.readFileSync(checkpointFile, "utf-8"),
|
|
316
|
+
) as Checkpoint;
|
|
317
|
+
const targetLatestCommitId = getLatestCommitId(repo_path, branch_name);
|
|
318
|
+
|
|
319
|
+
const checkpointIsLatest =
|
|
320
|
+
targetLatestCommitId.latestCommitId === checkpointData.commitId ||
|
|
321
|
+
new Date(checkpointData.date) >=
|
|
322
|
+
new Date(latestCommit?.commit?.date ?? 0);
|
|
323
|
+
|
|
324
|
+
if (checkpointIsLatest) {
|
|
325
|
+
checkpointData.files.forEach((file) =>
|
|
326
|
+
fs.writeFileSync(file.path, readObject(repo_path, file.hash)),
|
|
327
|
+
);
|
|
328
|
+
switchEmitter.emit("event", {
|
|
329
|
+
type: "files_restored",
|
|
330
|
+
files: checkpointData.files.map((f) => f.path),
|
|
331
|
+
source: "checkpoint",
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
latestCommit?.commit?.fileBlobs.forEach((file) =>
|
|
335
|
+
fs.writeFileSync(file.path, readObject(repo_path, file.hash)),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
switchEmitter.emit("event", {
|
|
339
|
+
type: "files_restored",
|
|
340
|
+
files: latestCommit?.commit?.fileBlobs.map((f) => f.path) ?? [],
|
|
341
|
+
source: "commit",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
latestCommit?.commit?.fileBlobs.forEach((file) =>
|
|
346
|
+
fs.writeFileSync(file.path, readObject(repo_path, file.hash)),
|
|
347
|
+
);
|
|
348
|
+
switchEmitter.emit("event", {
|
|
349
|
+
type: "files_restored",
|
|
350
|
+
files: latestCommit?.commit?.fileBlobs.map((f) => f.path) ?? [],
|
|
351
|
+
source: "commit",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const res_2 = updateRepo(repo_path, { branch: branch_name });
|
|
356
|
+
if (res_2.error) return { status: "error", error: res_2.error };
|
|
357
|
+
|
|
358
|
+
switchEmitter.emit("event", {
|
|
359
|
+
type: "switched",
|
|
360
|
+
from: currentBranch.branch.name,
|
|
361
|
+
to: branch_name,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return { status: "ok" };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function getCommitHistory(
|
|
368
|
+
repo_path: string,
|
|
369
|
+
branch_name: string,
|
|
370
|
+
): string[] {
|
|
371
|
+
const commitIds: string[] = [];
|
|
372
|
+
const latestCommitId = getLatestCommitId(repo_path, branch_name);
|
|
373
|
+
if (!latestCommitId.latestCommitId) return commitIds;
|
|
374
|
+
|
|
375
|
+
let currentId: string | undefined = latestCommitId.latestCommitId;
|
|
376
|
+
|
|
377
|
+
while (currentId) {
|
|
378
|
+
commitIds.push(currentId);
|
|
379
|
+
const commit = getCommit(repo_path, currentId, branch_name);
|
|
380
|
+
currentId = commit.commit?.parent;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return commitIds;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function mergeBranch(
|
|
387
|
+
current_branch: string,
|
|
388
|
+
target_branch: string,
|
|
389
|
+
repo_path: string,
|
|
390
|
+
): { status: "ok" | "error"; error?: string } {
|
|
391
|
+
const currentBranchCommitHistory = getCommitHistory(
|
|
392
|
+
repo_path,
|
|
393
|
+
current_branch,
|
|
394
|
+
);
|
|
395
|
+
const targetBranchommitHistory = getCommitHistory(repo_path, target_branch);
|
|
396
|
+
|
|
397
|
+
let sharedCommit;
|
|
398
|
+
|
|
399
|
+
for (const cb of currentBranchCommitHistory) {
|
|
400
|
+
if (targetBranchommitHistory.includes(cb)) {
|
|
401
|
+
sharedCommit = cb;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!sharedCommit)
|
|
407
|
+
return { status: "error", error: "no shared commit found." };
|
|
408
|
+
|
|
409
|
+
const latestCommitIdOfCurrentBranch = getLatestCommitId(
|
|
410
|
+
repo_path,
|
|
411
|
+
current_branch,
|
|
412
|
+
);
|
|
413
|
+
if (
|
|
414
|
+
latestCommitIdOfCurrentBranch.error ||
|
|
415
|
+
!latestCommitIdOfCurrentBranch.latestCommitId
|
|
416
|
+
)
|
|
417
|
+
return {
|
|
418
|
+
status: "error",
|
|
419
|
+
error: `no current latest commit in ${current_branch}`,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
let conflict: boolean = false;
|
|
423
|
+
let targetChanged: FileBlob[] = [];
|
|
424
|
+
|
|
425
|
+
if (latestCommitIdOfCurrentBranch.latestCommitId === sharedCommit) {
|
|
426
|
+
conflict = false;
|
|
427
|
+
} else {
|
|
428
|
+
const latestCommitIdOfTargetBranch = getLatestCommitId(
|
|
429
|
+
repo_path,
|
|
430
|
+
target_branch,
|
|
431
|
+
);
|
|
432
|
+
if (
|
|
433
|
+
latestCommitIdOfTargetBranch.error ||
|
|
434
|
+
!latestCommitIdOfTargetBranch.latestCommitId
|
|
435
|
+
)
|
|
436
|
+
return {
|
|
437
|
+
status: "error",
|
|
438
|
+
error: `no current latest commit in ${target_branch}`,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const sharedCommitOfCurrentBranch = getCommit(
|
|
442
|
+
repo_path,
|
|
443
|
+
sharedCommit,
|
|
444
|
+
current_branch,
|
|
445
|
+
);
|
|
446
|
+
const latestCommitOfTargetBranch = getCommit(
|
|
447
|
+
repo_path,
|
|
448
|
+
latestCommitIdOfTargetBranch.latestCommitId,
|
|
449
|
+
target_branch,
|
|
450
|
+
);
|
|
451
|
+
const latestCommitOfCurrentBranch = getCommit(
|
|
452
|
+
repo_path,
|
|
453
|
+
latestCommitIdOfCurrentBranch.latestCommitId,
|
|
454
|
+
current_branch,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
sharedCommitOfCurrentBranch.error ||
|
|
459
|
+
!sharedCommitOfCurrentBranch.commit ||
|
|
460
|
+
latestCommitOfTargetBranch.error ||
|
|
461
|
+
!latestCommitOfTargetBranch.commit ||
|
|
462
|
+
latestCommitOfCurrentBranch.error ||
|
|
463
|
+
!latestCommitOfCurrentBranch.commit
|
|
464
|
+
)
|
|
465
|
+
return { status: "error", error: `` };
|
|
466
|
+
|
|
467
|
+
const currentChanged = latestCommitOfCurrentBranch.commit.fileBlobs.filter(
|
|
468
|
+
(f) =>
|
|
469
|
+
sharedCommitOfCurrentBranch.commit!.fileBlobs.find(
|
|
470
|
+
(s) => s.path === f.path,
|
|
471
|
+
)?.hash !== f.hash,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
targetChanged = latestCommitOfTargetBranch.commit.fileBlobs.filter(
|
|
475
|
+
(f) =>
|
|
476
|
+
sharedCommitOfCurrentBranch.commit!.fileBlobs.find(
|
|
477
|
+
(s) => s.path === f.path,
|
|
478
|
+
)?.hash !== f.hash,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
conflict = currentChanged.some((cf) =>
|
|
482
|
+
targetChanged.find((tf) => tf.path === cf.path),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (conflict) {
|
|
486
|
+
const conflictFiles = currentChanged
|
|
487
|
+
.filter((cf) => targetChanged.find((tf) => tf.path === cf.path))
|
|
488
|
+
.map((f) => f.path);
|
|
489
|
+
switchEmitter.emit("event", {
|
|
490
|
+
type: "merge_conflict_detected",
|
|
491
|
+
files: conflictFiles,
|
|
492
|
+
});
|
|
493
|
+
return { status: "error", error: "conflict found." };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const targetBranchCommitFolder = path.join(
|
|
498
|
+
repo_path,
|
|
499
|
+
".forge",
|
|
500
|
+
"branches",
|
|
501
|
+
target_branch,
|
|
502
|
+
"commits",
|
|
503
|
+
);
|
|
504
|
+
const currentBranchCommitFolder = path.join(
|
|
505
|
+
repo_path,
|
|
506
|
+
".forge",
|
|
507
|
+
"branches",
|
|
508
|
+
current_branch,
|
|
509
|
+
"commits",
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
fs.cpSync(targetBranchCommitFolder, currentBranchCommitFolder, {
|
|
513
|
+
recursive: true,
|
|
514
|
+
force: true,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const latestCommitIdOfTargetBranch = getLatestCommitId(
|
|
518
|
+
repo_path,
|
|
519
|
+
target_branch,
|
|
520
|
+
);
|
|
521
|
+
if (
|
|
522
|
+
latestCommitIdOfTargetBranch.error ||
|
|
523
|
+
!latestCommitIdOfTargetBranch.latestCommitId
|
|
524
|
+
)
|
|
525
|
+
return {
|
|
526
|
+
status: "error",
|
|
527
|
+
error: `no latest commit found in ${target_branch}`,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
updateBranch(
|
|
531
|
+
repo_path,
|
|
532
|
+
current_branch,
|
|
533
|
+
latestCommitIdOfTargetBranch.latestCommitId,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const latestCommitIdOfCurrentBranch2 = getLatestCommitId(
|
|
537
|
+
repo_path,
|
|
538
|
+
current_branch,
|
|
539
|
+
);
|
|
540
|
+
if (
|
|
541
|
+
latestCommitIdOfCurrentBranch2.error ||
|
|
542
|
+
!latestCommitIdOfCurrentBranch2.latestCommitId
|
|
543
|
+
)
|
|
544
|
+
return {
|
|
545
|
+
status: "error",
|
|
546
|
+
error: `no latest commit found in ${current_branch}`,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
checkoutCommit(
|
|
550
|
+
latestCommitIdOfCurrentBranch2.latestCommitId,
|
|
551
|
+
repo_path,
|
|
552
|
+
current_branch,
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
switchEmitter.emit("event", {
|
|
556
|
+
type: "merge_complete",
|
|
557
|
+
from: target_branch,
|
|
558
|
+
to: current_branch,
|
|
559
|
+
filesChanged: targetChanged.length,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
return { status: "ok" };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function deleteBranch(
|
|
566
|
+
repo_path: string,
|
|
567
|
+
branch_name: string,
|
|
568
|
+
): { status: "ok" | "error"; error?: string } {
|
|
569
|
+
const { branchFolder, branchesFolder } = getBranchPaths(
|
|
570
|
+
repo_path,
|
|
571
|
+
branch_name,
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
if (!fs.existsSync(branchesFolder))
|
|
575
|
+
return {
|
|
576
|
+
status: "error",
|
|
577
|
+
error: "branches folder is missing, consider reinitialize repo.",
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
if (!fs.existsSync(branchFolder!))
|
|
581
|
+
return { status: "error", error: `${branch_name} doesn't exists.` };
|
|
582
|
+
|
|
583
|
+
const currentBranch = getCurrentBranch(repo_path);
|
|
584
|
+
if (currentBranch.branch?.name === branch_name)
|
|
585
|
+
return { status: "error", error: `cannot delete current branch.` };
|
|
586
|
+
|
|
587
|
+
fs.rmSync(branchFolder!, { recursive: true, force: true });
|
|
588
|
+
|
|
589
|
+
return { status: "ok" };
|
|
590
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import type { Commit } from "../types/commit";
|
|
4
|
+
import { getCurrentBranch } from "./branch";
|
|
5
|
+
import { readObject } from "./objects";
|
|
6
|
+
|
|
7
|
+
export function checkoutCommit(
|
|
8
|
+
commitId: string,
|
|
9
|
+
repo_path: string,
|
|
10
|
+
branch_name?: string,
|
|
11
|
+
): {
|
|
12
|
+
status: "ok" | "error";
|
|
13
|
+
error?: string;
|
|
14
|
+
} {
|
|
15
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
16
|
+
const currentBranch = getCurrentBranch(repo_path);
|
|
17
|
+
if (currentBranch.error || !currentBranch.branch) {
|
|
18
|
+
return {
|
|
19
|
+
status: "error",
|
|
20
|
+
error: currentBranch.error || "no current branch found.",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const branchName = branch_name ?? currentBranch.branch.name;
|
|
24
|
+
const commitFolder = path.join(
|
|
25
|
+
forgeFolder,
|
|
26
|
+
"branches",
|
|
27
|
+
branchName,
|
|
28
|
+
"commits",
|
|
29
|
+
);
|
|
30
|
+
const commitFile = path.join(commitFolder, `${commitId}.json`);
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(commitFolder))
|
|
33
|
+
return {
|
|
34
|
+
status: "error",
|
|
35
|
+
error: "commits folder is missing, consider reinitialize repo.",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(commitFile))
|
|
39
|
+
return {
|
|
40
|
+
status: "error",
|
|
41
|
+
error: "commit not found.",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const commitData: Commit = JSON.parse(fs.readFileSync(commitFile, "utf-8"));
|
|
45
|
+
const files = commitData.fileBlobs;
|
|
46
|
+
|
|
47
|
+
console.log(
|
|
48
|
+
"restoring files:",
|
|
49
|
+
files.map((f) => f.path),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
files.forEach((file) => {
|
|
53
|
+
fs.writeFileSync(file.path, readObject(repo_path, file.hash));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return { status: "ok" };
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import type { Checkpoint } from "../types/checkpoint";
|
|
4
|
+
import type { FileBlob } from "../types/files";
|
|
5
|
+
|
|
6
|
+
export function createCheckpoint(
|
|
7
|
+
repo_path: string,
|
|
8
|
+
fileBlobs: FileBlob[],
|
|
9
|
+
branch_name: string,
|
|
10
|
+
commitId: string,
|
|
11
|
+
): { status: "ok" | "error"; error?: string } {
|
|
12
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
13
|
+
const checkpointsFolder = path.join(forgeFolder, "branches", branch_name);
|
|
14
|
+
[];
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(checkpointsFolder))
|
|
17
|
+
fs.mkdirSync(checkpointsFolder, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const checkpointData = {
|
|
20
|
+
date: new Date().toISOString(),
|
|
21
|
+
files: fileBlobs,
|
|
22
|
+
} as Checkpoint;
|
|
23
|
+
|
|
24
|
+
const checkpointFile = path.join(checkpointsFolder, "checkpoint.json");
|
|
25
|
+
fs.writeFileSync(checkpointFile, JSON.stringify(checkpointData));
|
|
26
|
+
|
|
27
|
+
return { status: "ok" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getCheckpoint(
|
|
31
|
+
repo_path: string,
|
|
32
|
+
branch_name: string,
|
|
33
|
+
): { status: "ok" | "error"; checkpoint?: Checkpoint; error?: string } {
|
|
34
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
35
|
+
const checkpointsFolder = path.join(forgeFolder, "branches", branch_name);
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(checkpointsFolder))
|
|
38
|
+
return {
|
|
39
|
+
status: "error",
|
|
40
|
+
error: "checkpoints folder is missing, consider reinitializing repo.",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const checkpointsFile = path.join(checkpointsFolder, "checkpoint.json");
|
|
44
|
+
if (!fs.existsSync(checkpointsFile))
|
|
45
|
+
return { status: "error", error: `checkpoint doesn't exists.` };
|
|
46
|
+
|
|
47
|
+
const checkpointData = JSON.parse(
|
|
48
|
+
fs.readFileSync(checkpointsFile, "utf-8"),
|
|
49
|
+
) as Checkpoint;
|
|
50
|
+
|
|
51
|
+
return { status: "ok", checkpoint: checkpointData };
|
|
52
|
+
}
|