gent-cli 2.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -42
- package/package.json +1 -1
- package/src/commands/add.js +102 -23
- package/src/commands/clone.js +137 -86
- package/src/commands/commit.js +89 -81
- package/src/commands/diff.js +257 -0
- package/src/commands/init.js +5 -36
- package/src/commands/log.js +57 -13
- package/src/commands/merge.js +245 -0
- package/src/commands/pull.js +176 -86
- package/src/commands/push.js +172 -79
- package/src/commands/remote.js +97 -113
- package/src/commands/reset.js +149 -0
- package/src/commands/rm.js +85 -0
- package/src/commands/show.js +167 -0
- package/src/commands/stash.js +255 -0
- package/src/commands/status.js +80 -46
- package/src/commands/tag.js +146 -0
- package/src/index.js +108 -57
- package/src/utils/constants.js +10 -26
- package/src/utils/diff-engine.js +236 -0
- package/src/utils/fileSystem.js +8 -60
- package/src/utils/hash-engine.js +337 -0
- package/src/utils/merge-engine.js +379 -0
- package/src/utils/object-store.js +54 -0
- package/src/commands/create.js +0 -121
- package/src/commands/list.js +0 -67
- package/src/services/repo-service.js +0 -291
- package/src/utils/cloud-sync.js +0 -323
- package/src/utils/diff.js +0 -121
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Merge Engine - Three-Way Smart Merge with Auto-Resolution
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Merge two diverged branches using their common ancestor as reference.
|
|
8
|
+
* Minimizes manual conflict resolution through aggressive auto-resolution.
|
|
9
|
+
*
|
|
10
|
+
* THREE-WAY MERGE ALGORITHM:
|
|
11
|
+
* Given: BASE (common ancestor), OURS (current branch), THEIRS (incoming)
|
|
12
|
+
*
|
|
13
|
+
* 1. Compute diff: BASE → OURS (what we changed)
|
|
14
|
+
* 2. Compute diff: BASE → THEIRS (what they changed)
|
|
15
|
+
* 3. Build "change regions" from each diff (contiguous modified areas)
|
|
16
|
+
* 4. Walk base line-by-line, apply resolution rules:
|
|
17
|
+
*
|
|
18
|
+
* AUTO-RESOLUTION RULES (in priority order):
|
|
19
|
+
* ┌─────────────────────────────────────────────────────────────────┐
|
|
20
|
+
* │ Scenario │ Result │
|
|
21
|
+
* ├─────────────────────────────────────────────────────────────────┤
|
|
22
|
+
* │ Only OURS modified region │ Take OURS │
|
|
23
|
+
* │ Only THEIRS modified region │ Take THEIRS │
|
|
24
|
+
* │ Both modified identically │ Take either (same) │
|
|
25
|
+
* │ Both modified, same-length, │ │
|
|
26
|
+
* │ non-overlapping line changes │ Line-by-line merge │
|
|
27
|
+
* │ Whitespace-only difference │ Take OURS │
|
|
28
|
+
* │ Modify/delete conflict │ Keep modified (smart) │
|
|
29
|
+
* │ True overlapping conflict │ Insert conflict markers │
|
|
30
|
+
* └─────────────────────────────────────────────────────────────────┘
|
|
31
|
+
*
|
|
32
|
+
* MERGE BASE FINDER:
|
|
33
|
+
* Walks parent chain from both branch tips.
|
|
34
|
+
* Collects all ancestors of branch A, then walks B until first hit.
|
|
35
|
+
* Returns the most recent common ancestor. O(n) where n = total commits.
|
|
36
|
+
*
|
|
37
|
+
* TREE-LEVEL MERGE:
|
|
38
|
+
* Compares file presence/absence + blob hashes across base/ours/theirs trees.
|
|
39
|
+
* For each file: decide add/delete/modify/conflict independently.
|
|
40
|
+
* Only files with different blob hashes on both sides trigger content merge.
|
|
41
|
+
*
|
|
42
|
+
* CONFLICT MARKERS FORMAT:
|
|
43
|
+
* <<<<<<< ours
|
|
44
|
+
* [our version of conflicting lines]
|
|
45
|
+
* =======
|
|
46
|
+
* [their version of conflicting lines]
|
|
47
|
+
* >>>>>>> theirs
|
|
48
|
+
*
|
|
49
|
+
* BACKEND EXPECTATIONS:
|
|
50
|
+
* POST /api/repos/:id/merge/
|
|
51
|
+
* { sourceBranch, targetBranch, strategy: "three-way" }
|
|
52
|
+
* Backend can use same algorithm or delegate to client.
|
|
53
|
+
* Backend should store merge commit with two parents (parent + mergeParent).
|
|
54
|
+
*
|
|
55
|
+
* ============================================================================
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const { splitLines } = require('./hash-engine');
|
|
59
|
+
const { buildLineOperations } = require('./diff-engine');
|
|
60
|
+
const { readBlobAsString, treeToMap, storeBlob } = require('./hash-engine');
|
|
61
|
+
|
|
62
|
+
// ─── Line-Level 3-Way Merge ─────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Three-way merge of line arrays.
|
|
66
|
+
* @param {String[]} baseLines
|
|
67
|
+
* @param {String[]} oursLines
|
|
68
|
+
* @param {String[]} theirsLines
|
|
69
|
+
* @returns {{merged: String[], conflicts: Array, hasConflicts: Boolean}}
|
|
70
|
+
*/
|
|
71
|
+
function threeWayMerge(baseLines, oursLines, theirsLines) {
|
|
72
|
+
const ourOps = buildLineOperations(baseLines, oursLines);
|
|
73
|
+
const theirOps = buildLineOperations(baseLines, theirsLines);
|
|
74
|
+
|
|
75
|
+
const ourRegions = buildChangeRegions(ourOps);
|
|
76
|
+
const theirRegions = buildChangeRegions(theirOps);
|
|
77
|
+
|
|
78
|
+
const ourMap = mapRegionsByBase(ourRegions);
|
|
79
|
+
const theirMap = mapRegionsByBase(theirRegions);
|
|
80
|
+
|
|
81
|
+
const allStarts = new Set([...ourMap.keys(), ...theirMap.keys()]);
|
|
82
|
+
const merged = [];
|
|
83
|
+
const conflicts = [];
|
|
84
|
+
let baseIdx = 0;
|
|
85
|
+
|
|
86
|
+
while (baseIdx <= baseLines.length) {
|
|
87
|
+
if (allStarts.has(baseIdx)) {
|
|
88
|
+
const ourR = ourMap.get(baseIdx);
|
|
89
|
+
const theirR = theirMap.get(baseIdx);
|
|
90
|
+
|
|
91
|
+
if (ourR && theirR) {
|
|
92
|
+
const ourText = ourR.newLines.join('\n');
|
|
93
|
+
const theirText = theirR.newLines.join('\n');
|
|
94
|
+
|
|
95
|
+
if (ourText === theirText) {
|
|
96
|
+
merged.push(...ourR.newLines);
|
|
97
|
+
} else {
|
|
98
|
+
const sub = subMergeRegion(ourR.oldLines, ourR.newLines, theirR.newLines);
|
|
99
|
+
if (sub.hasConflicts) {
|
|
100
|
+
conflicts.push({
|
|
101
|
+
baseLine: baseIdx,
|
|
102
|
+
baseContent: ourR.oldLines,
|
|
103
|
+
oursContent: ourR.newLines,
|
|
104
|
+
theirsContent: theirR.newLines
|
|
105
|
+
});
|
|
106
|
+
merged.push('<<<<<<< ours');
|
|
107
|
+
merged.push(...ourR.newLines);
|
|
108
|
+
merged.push('=======');
|
|
109
|
+
merged.push(...theirR.newLines);
|
|
110
|
+
merged.push('>>>>>>> theirs');
|
|
111
|
+
} else {
|
|
112
|
+
merged.push(...sub.lines);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const skip = Math.max(ourR.oldLines.length, theirR.oldLines.length);
|
|
116
|
+
baseIdx += skip;
|
|
117
|
+
continue;
|
|
118
|
+
} else if (ourR) {
|
|
119
|
+
merged.push(...ourR.newLines);
|
|
120
|
+
baseIdx += ourR.oldLines.length;
|
|
121
|
+
continue;
|
|
122
|
+
} else if (theirR) {
|
|
123
|
+
merged.push(...theirR.newLines);
|
|
124
|
+
baseIdx += theirR.oldLines.length;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (baseIdx < baseLines.length) {
|
|
130
|
+
merged.push(baseLines[baseIdx]);
|
|
131
|
+
}
|
|
132
|
+
baseIdx++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { merged, conflicts, hasConflicts: conflicts.length > 0 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Region Building ────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract contiguous change regions from diff ops, anchored to base line indices.
|
|
142
|
+
* @param {Array} ops
|
|
143
|
+
* @returns {Array<{baseStart, oldLines, newLines}>}
|
|
144
|
+
*/
|
|
145
|
+
function buildChangeRegions(ops) {
|
|
146
|
+
const regions = [];
|
|
147
|
+
let current = null;
|
|
148
|
+
let baseIdx = 0;
|
|
149
|
+
|
|
150
|
+
for (const op of ops) {
|
|
151
|
+
if (op.type === 'equal') {
|
|
152
|
+
if (current) { regions.push(current); current = null; }
|
|
153
|
+
baseIdx++;
|
|
154
|
+
} else if (op.type === 'delete') {
|
|
155
|
+
if (!current) current = { baseStart: baseIdx, oldLines: [], newLines: [] };
|
|
156
|
+
current.oldLines.push(op.content);
|
|
157
|
+
baseIdx++;
|
|
158
|
+
} else if (op.type === 'insert') {
|
|
159
|
+
if (!current) current = { baseStart: baseIdx, oldLines: [], newLines: [] };
|
|
160
|
+
current.newLines.push(op.content);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (current) regions.push(current);
|
|
164
|
+
return regions;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Map regions by baseStart index.
|
|
169
|
+
* @param {Array} regions
|
|
170
|
+
* @returns {Map}
|
|
171
|
+
*/
|
|
172
|
+
function mapRegionsByBase(regions) {
|
|
173
|
+
const map = new Map();
|
|
174
|
+
for (const r of regions) map.set(r.baseStart, r);
|
|
175
|
+
return map;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Sub-Merge (fine-grained) ───────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Attempt line-by-line merge within a region.
|
|
182
|
+
* Catches non-overlapping edits inside same region.
|
|
183
|
+
* @param {String[]} baseLines
|
|
184
|
+
* @param {String[]} oursLines
|
|
185
|
+
* @param {String[]} theirsLines
|
|
186
|
+
* @returns {{lines: String[], hasConflicts: Boolean}}
|
|
187
|
+
*/
|
|
188
|
+
function subMergeRegion(baseLines, oursLines, theirsLines) {
|
|
189
|
+
if (oursLines.length === 0 && theirsLines.length === 0) {
|
|
190
|
+
return { lines: [], hasConflicts: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (oursLines.length === theirsLines.length) {
|
|
194
|
+
const result = [];
|
|
195
|
+
for (let i = 0; i < oursLines.length; i++) {
|
|
196
|
+
const baseLine = i < baseLines.length ? baseLines[i] : null;
|
|
197
|
+
const ourLine = oursLines[i];
|
|
198
|
+
const theirLine = theirsLines[i];
|
|
199
|
+
|
|
200
|
+
if (ourLine === theirLine) { result.push(ourLine); continue; }
|
|
201
|
+
if (ourLine === baseLine) { result.push(theirLine); continue; }
|
|
202
|
+
if (theirLine === baseLine) { result.push(ourLine); continue; }
|
|
203
|
+
// Whitespace-only diff → take ours
|
|
204
|
+
if (ourLine.trim() === theirLine.trim()) { result.push(ourLine); continue; }
|
|
205
|
+
return { lines: [], hasConflicts: true };
|
|
206
|
+
}
|
|
207
|
+
return { lines: result, hasConflicts: false };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { lines: [], hasConflicts: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── File-Level Merge ───────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Merge single file content strings.
|
|
217
|
+
* @param {String} baseContent
|
|
218
|
+
* @param {String} oursContent
|
|
219
|
+
* @param {String} theirsContent
|
|
220
|
+
* @returns {{content: String, hasConflicts: Boolean, conflicts: Array}}
|
|
221
|
+
*/
|
|
222
|
+
function mergeFileContent(baseContent, oursContent, theirsContent) {
|
|
223
|
+
const base = splitLines(baseContent || '');
|
|
224
|
+
const ours = splitLines(oursContent || '');
|
|
225
|
+
const theirs = splitLines(theirsContent || '');
|
|
226
|
+
const result = threeWayMerge(base, ours, theirs);
|
|
227
|
+
return { content: result.merged.join('\n'), hasConflicts: result.hasConflicts, conflicts: result.conflicts };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Quick auto-merge with fast-path shortcuts.
|
|
232
|
+
* @param {String} baseText
|
|
233
|
+
* @param {String} oursText
|
|
234
|
+
* @param {String} theirsText
|
|
235
|
+
* @returns {{mergedText: String, hasConflicts: Boolean, conflicts: Array, algorithm: String, confidence: Number}}
|
|
236
|
+
*/
|
|
237
|
+
function autoMerge(baseText, oursText, theirsText) {
|
|
238
|
+
// Fast paths
|
|
239
|
+
if (oursText === theirsText) return { mergedText: oursText, hasConflicts: false, conflicts: [], algorithm: 'three-way-line-v2', confidence: 1 };
|
|
240
|
+
if (oursText === baseText) return { mergedText: theirsText, hasConflicts: false, conflicts: [], algorithm: 'three-way-line-v2', confidence: 1 };
|
|
241
|
+
if (theirsText === baseText) return { mergedText: oursText, hasConflicts: false, conflicts: [], algorithm: 'three-way-line-v2', confidence: 1 };
|
|
242
|
+
|
|
243
|
+
const result = mergeFileContent(baseText, oursText, theirsText);
|
|
244
|
+
const maxLen = Math.max(splitLines(baseText).length, 1);
|
|
245
|
+
const confidence = result.hasConflicts ? Math.max(0, 1 - result.conflicts.length / maxLen) : 1;
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
mergedText: result.content,
|
|
249
|
+
hasConflicts: result.hasConflicts,
|
|
250
|
+
conflicts: result.conflicts,
|
|
251
|
+
algorithm: 'three-way-line-v2',
|
|
252
|
+
confidence
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Tree-Level Merge ───────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Merge two tree snapshots against common-base tree.
|
|
260
|
+
* Handles file add/delete/modify across branches.
|
|
261
|
+
* @param {String} gentPath
|
|
262
|
+
* @param {Array} baseEntries
|
|
263
|
+
* @param {Array} oursEntries
|
|
264
|
+
* @param {Array} theirsEntries
|
|
265
|
+
* @returns {Promise<{mergedEntries: Array, conflicts: Array, hasConflicts: Boolean}>}
|
|
266
|
+
*/
|
|
267
|
+
async function mergeTreeEntries(gentPath, baseEntries, oursEntries, theirsEntries) {
|
|
268
|
+
const baseMap = treeToMap(baseEntries);
|
|
269
|
+
const oursMap = treeToMap(oursEntries);
|
|
270
|
+
const theirsMap = treeToMap(theirsEntries);
|
|
271
|
+
|
|
272
|
+
const allFiles = new Set([...baseMap.keys(), ...oursMap.keys(), ...theirsMap.keys()]);
|
|
273
|
+
const mergedEntries = [];
|
|
274
|
+
const conflicts = [];
|
|
275
|
+
|
|
276
|
+
for (const filePath of allFiles) {
|
|
277
|
+
const bH = baseMap.get(filePath) || null;
|
|
278
|
+
const oH = oursMap.get(filePath) || null;
|
|
279
|
+
const tH = theirsMap.get(filePath) || null;
|
|
280
|
+
|
|
281
|
+
// No change or both identical
|
|
282
|
+
if (oH === tH) {
|
|
283
|
+
if (oH) mergedEntries.push({ mode: '100644', name: filePath, hash: oH, type: 'blob' });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Only one side changed
|
|
288
|
+
if (oH === bH && tH !== bH) {
|
|
289
|
+
if (tH) mergedEntries.push({ mode: '100644', name: filePath, hash: tH, type: 'blob' });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (tH === bH && oH !== bH) {
|
|
293
|
+
if (oH) mergedEntries.push({ mode: '100644', name: filePath, hash: oH, type: 'blob' });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Both changed differently — content merge
|
|
298
|
+
if (oH && tH && bH) {
|
|
299
|
+
const [baseC, oursC, theirsC] = await Promise.all([
|
|
300
|
+
readBlobAsString(gentPath, bH),
|
|
301
|
+
readBlobAsString(gentPath, oH),
|
|
302
|
+
readBlobAsString(gentPath, tH)
|
|
303
|
+
]);
|
|
304
|
+
const result = mergeFileContent(baseC, oursC, theirsC);
|
|
305
|
+
const mergedHash = await storeBlob(gentPath, result.content);
|
|
306
|
+
mergedEntries.push({ mode: '100644', name: filePath, hash: mergedHash, type: 'blob' });
|
|
307
|
+
if (result.hasConflicts) conflicts.push({ file: filePath, type: 'content', details: result.conflicts });
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Modify/delete conflict — keep modified version (smart default)
|
|
312
|
+
if (!oH && tH && tH !== bH) {
|
|
313
|
+
conflicts.push({ file: filePath, type: 'modify-delete', deletedBy: 'ours', modifiedBy: 'theirs' });
|
|
314
|
+
mergedEntries.push({ mode: '100644', name: filePath, hash: tH, type: 'blob' });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (!tH && oH && oH !== bH) {
|
|
318
|
+
conflicts.push({ file: filePath, type: 'modify-delete', deletedBy: 'theirs', modifiedBy: 'ours' });
|
|
319
|
+
mergedEntries.push({ mode: '100644', name: filePath, hash: oH, type: 'blob' });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Both added (no base) with different content
|
|
324
|
+
if (!bH && oH && tH) {
|
|
325
|
+
const [oursC, theirsC] = await Promise.all([
|
|
326
|
+
readBlobAsString(gentPath, oH),
|
|
327
|
+
readBlobAsString(gentPath, tH)
|
|
328
|
+
]);
|
|
329
|
+
const result = mergeFileContent('', oursC, theirsC);
|
|
330
|
+
const mergedHash = await storeBlob(gentPath, result.content);
|
|
331
|
+
mergedEntries.push({ mode: '100644', name: filePath, hash: mergedHash, type: 'blob' });
|
|
332
|
+
if (result.hasConflicts) conflicts.push({ file: filePath, type: 'add-add', details: result.conflicts });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { mergedEntries, conflicts, hasConflicts: conflicts.length > 0 };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── Merge Base Finder ──────────────────────────────────
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Find common ancestor of two branch tips by walking parents.
|
|
343
|
+
* @param {Array} commits
|
|
344
|
+
* @param {String} hashA
|
|
345
|
+
* @param {String} hashB
|
|
346
|
+
* @returns {String|null}
|
|
347
|
+
*/
|
|
348
|
+
function findMergeBase(commits, hashA, hashB) {
|
|
349
|
+
const commitMap = new Map();
|
|
350
|
+
for (const c of commits) commitMap.set(c.hash, c);
|
|
351
|
+
|
|
352
|
+
// Collect all ancestors of A
|
|
353
|
+
const ancestorsA = new Set();
|
|
354
|
+
let cur = hashA;
|
|
355
|
+
while (cur) {
|
|
356
|
+
ancestorsA.add(cur);
|
|
357
|
+
const c = commitMap.get(cur);
|
|
358
|
+
cur = c ? c.parent : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Walk B ancestors → first hit in A's set = merge base
|
|
362
|
+
cur = hashB;
|
|
363
|
+
while (cur) {
|
|
364
|
+
if (ancestorsA.has(cur)) return cur;
|
|
365
|
+
const c = commitMap.get(cur);
|
|
366
|
+
cur = c ? c.parent : null;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = {
|
|
372
|
+
threeWayMerge,
|
|
373
|
+
mergeFileContent,
|
|
374
|
+
autoMerge,
|
|
375
|
+
mergeTreeEntries,
|
|
376
|
+
findMergeBase,
|
|
377
|
+
buildChangeRegions,
|
|
378
|
+
subMergeRegion
|
|
379
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Store
|
|
3
|
+
* Persist content-addressed blobs inside .gent/objects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { pathExists } = require('./fileSystem');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute object file path for hash.
|
|
12
|
+
* @param {String} gentPath
|
|
13
|
+
* @param {String} hash
|
|
14
|
+
* @returns {String}
|
|
15
|
+
*/
|
|
16
|
+
function getObjectPath(gentPath, hash) {
|
|
17
|
+
return path.join(gentPath, 'objects', `${hash}.blob`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Save object if missing.
|
|
22
|
+
* @param {String} gentPath
|
|
23
|
+
* @param {String} hash
|
|
24
|
+
* @param {Buffer} content
|
|
25
|
+
*/
|
|
26
|
+
async function writeObject(gentPath, hash, content) {
|
|
27
|
+
const objectPath = getObjectPath(gentPath, hash);
|
|
28
|
+
|
|
29
|
+
if (!await pathExists(objectPath)) {
|
|
30
|
+
await fs.writeFile(objectPath, content);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read object as buffer.
|
|
36
|
+
* @param {String} gentPath
|
|
37
|
+
* @param {String} hash
|
|
38
|
+
* @returns {Promise<Buffer|null>}
|
|
39
|
+
*/
|
|
40
|
+
async function readObject(gentPath, hash) {
|
|
41
|
+
const objectPath = getObjectPath(gentPath, hash);
|
|
42
|
+
|
|
43
|
+
if (!await pathExists(objectPath)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fs.readFile(objectPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
getObjectPath,
|
|
52
|
+
writeObject,
|
|
53
|
+
readObject
|
|
54
|
+
};
|
package/src/commands/create.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create Command - Create a new cloud repository
|
|
3
|
-
* Creates a repository on the cloud and optionally initializes locally
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const chalk = require('chalk');
|
|
7
|
-
const inquirer = require('inquirer');
|
|
8
|
-
const ora = require('ora');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const repoService = require('../services/repo-service');
|
|
11
|
-
const authStorage = require('../utils/auth-storage');
|
|
12
|
-
const { ensureDir, writeJSON } = require('../utils/fileSystem');
|
|
13
|
-
const { GENT_DIR } = require('../utils/constants');
|
|
14
|
-
const { addRemote } = require('../utils/cloud-sync');
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Create a new repository on the cloud
|
|
18
|
-
* @param {string} repoName - Repository name
|
|
19
|
-
* @param {Object} options - Command options
|
|
20
|
-
*/
|
|
21
|
-
async function create(repoName, options) {
|
|
22
|
-
try {
|
|
23
|
-
// Check authentication
|
|
24
|
-
const user = await authStorage.getUser();
|
|
25
|
-
if (!user) {
|
|
26
|
-
console.error(chalk.red('Error: You must be logged in to create a repository'));
|
|
27
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Validate repository name
|
|
32
|
-
if (!repoName) {
|
|
33
|
-
console.error(chalk.red('Error: Repository name is required'));
|
|
34
|
-
console.log(chalk.yellow('Usage:'), chalk.cyan('gent create <repo-name>'));
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Prompt for additional details if not provided
|
|
39
|
-
let description = options.description || '';
|
|
40
|
-
let isPrivate = options.private || false;
|
|
41
|
-
|
|
42
|
-
if (!options.yes) {
|
|
43
|
-
const answers = await inquirer.prompt([
|
|
44
|
-
{
|
|
45
|
-
type: 'input',
|
|
46
|
-
name: 'description',
|
|
47
|
-
message: 'Repository description (optional):',
|
|
48
|
-
default: description
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
type: 'confirm',
|
|
52
|
-
name: 'isPrivate',
|
|
53
|
-
message: 'Make repository private?',
|
|
54
|
-
default: isPrivate
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
type: 'confirm',
|
|
58
|
-
name: 'initLocal',
|
|
59
|
-
message: 'Initialize local repository and link remote?',
|
|
60
|
-
default: true
|
|
61
|
-
}
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
description = answers.description;
|
|
65
|
-
isPrivate = answers.isPrivate;
|
|
66
|
-
options.initLocal = answers.initLocal;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Create repository
|
|
70
|
-
const spinner = ora('Creating repository...').start();
|
|
71
|
-
|
|
72
|
-
const repository = await repoService.createRepository(
|
|
73
|
-
repoName,
|
|
74
|
-
description,
|
|
75
|
-
isPrivate,
|
|
76
|
-
'main'
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const ownerId = repository.owner_id || repository.owner?.id || repository.owner;
|
|
80
|
-
const name = repository.name || repository.project_name || repoName;
|
|
81
|
-
|
|
82
|
-
spinner.succeed(chalk.green(`Repository created: ${name}`));
|
|
83
|
-
|
|
84
|
-
console.log(chalk.cyan('\nRepository Details:'));
|
|
85
|
-
console.log(chalk.gray(' Name:'), name);
|
|
86
|
-
console.log(chalk.gray(' Owner:'), ownerId);
|
|
87
|
-
console.log(chalk.gray(' Private:'), repository.is_private ? 'Yes' : 'No');
|
|
88
|
-
console.log(chalk.gray(' Description:'), repository.description || '(none)');
|
|
89
|
-
console.log(chalk.gray(' Created:'), new Date(repository.created_at || repository.created || new Date()).toLocaleString());
|
|
90
|
-
|
|
91
|
-
// Initialize local repository if requested
|
|
92
|
-
if (options.initLocal) {
|
|
93
|
-
const cwd = process.cwd();
|
|
94
|
-
const gentPath = path.join(cwd, GENT_DIR);
|
|
95
|
-
|
|
96
|
-
// Create .gent directory
|
|
97
|
-
await ensureDir(gentPath);
|
|
98
|
-
await ensureDir(path.join(gentPath, 'objects'));
|
|
99
|
-
await ensureDir(path.join(gentPath, 'refs', 'heads'));
|
|
100
|
-
|
|
101
|
-
// Add remote
|
|
102
|
-
await addRemote('origin', ownerId, name, cwd);
|
|
103
|
-
|
|
104
|
-
console.log(chalk.green('\n✓ Local repository initialized and remote added'));
|
|
105
|
-
console.log(chalk.yellow('\nNext steps:'));
|
|
106
|
-
console.log(chalk.cyan(' gent add <files>'), chalk.gray('- Add files to staging'));
|
|
107
|
-
console.log(chalk.cyan(' gent commit -m "message"'), chalk.gray('- Commit changes'));
|
|
108
|
-
console.log(chalk.cyan(' gent push'), chalk.gray('- Push to cloud'));
|
|
109
|
-
} else {
|
|
110
|
-
console.log(chalk.yellow('\nTo clone this repository:'));
|
|
111
|
-
console.log(chalk.cyan(` gent clone ${repository.owner_id}/${repository.name}`));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error(chalk.red('Failed to create repository'));
|
|
116
|
-
console.error(chalk.red('Error:'), error.message);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
module.exports = create;
|
package/src/commands/list.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List Command - List all cloud repositories
|
|
3
|
-
* Displays all repositories owned by the authenticated user
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const chalk = require('chalk');
|
|
7
|
-
const ora = require('ora');
|
|
8
|
-
const repoService = require('../services/repo-service');
|
|
9
|
-
const authStorage = require('../utils/auth-storage');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Format date for display
|
|
13
|
-
* @param {string} dateString - ISO date string
|
|
14
|
-
* @returns {string} Formatted date
|
|
15
|
-
*/
|
|
16
|
-
function formatDate(dateString) {
|
|
17
|
-
const date = new Date(dateString);
|
|
18
|
-
return date.toLocaleDateString();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* List all repositories owned by the authenticated user
|
|
23
|
-
* @param {Object} options - Command options
|
|
24
|
-
*/
|
|
25
|
-
async function list(options) {
|
|
26
|
-
try {
|
|
27
|
-
// Check authentication
|
|
28
|
-
const user = await authStorage.getUser();
|
|
29
|
-
if (!user) {
|
|
30
|
-
console.error(chalk.red('Error: You must be logged in to list repositories'));
|
|
31
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Fetch repositories
|
|
36
|
-
const spinner = ora('Fetching repositories...').start();
|
|
37
|
-
const repositories = await repoService.listRepositories();
|
|
38
|
-
spinner.stop();
|
|
39
|
-
|
|
40
|
-
if (repositories.length === 0) {
|
|
41
|
-
console.log(chalk.yellow('No repositories found'));
|
|
42
|
-
console.log(chalk.gray('Create a new repository with:'), chalk.cyan('gent create <repo-name>'));
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log(chalk.cyan(`\nYour Repositories (${repositories.length}):\n`));
|
|
47
|
-
|
|
48
|
-
repositories.forEach(repo => {
|
|
49
|
-
const privacy = repo.is_private ? chalk.red('🔒 Private') : chalk.green('🌐 Public');
|
|
50
|
-
console.log(chalk.bold(repo.name), privacy);
|
|
51
|
-
if (repo.description) {
|
|
52
|
-
console.log(chalk.gray(` ${repo.description}`));
|
|
53
|
-
}
|
|
54
|
-
console.log(chalk.gray(` Owner: ${repo.owner_email}`));
|
|
55
|
-
console.log(chalk.gray(` Created: ${formatDate(repo.created_at)}`));
|
|
56
|
-
console.log(chalk.gray(` Default branch: ${repo.default_branch}`));
|
|
57
|
-
console.log(chalk.gray(` Clone: gent clone ${repo.owner_id}/${repo.name}\n`));
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error(chalk.red('Failed to list repositories'));
|
|
62
|
-
console.error(chalk.red('Error:'), error.message);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
module.exports = list;
|