gent-cli 2.1.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -65
- 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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Stash Command - Temporarily shelve working tree changes
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Save uncommitted changes on a stack so you can switch branches cleanly,
|
|
8
|
+
* then restore them later. Like `git stash`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent stash → Stash all modified tracked files
|
|
12
|
+
* gent stash pop → Restore most recent stash and remove it
|
|
13
|
+
* gent stash list → List all stashed entries
|
|
14
|
+
* gent stash drop [index] → Drop a specific stash entry
|
|
15
|
+
* gent stash apply [index] → Apply stash without removing it
|
|
16
|
+
*
|
|
17
|
+
* ALGORITHM:
|
|
18
|
+
* Saved as JSON stack in .gent/stash.json. Each entry stores:
|
|
19
|
+
* - Snapshot of staged entries (staging.entries)
|
|
20
|
+
* - Blob hashes of modified working tree files
|
|
21
|
+
* After stashing, staging is cleared and files are restored to HEAD state.
|
|
22
|
+
*
|
|
23
|
+
* BACKEND EXPECTATIONS:
|
|
24
|
+
* Stash is local only. Backend does not need stash support.
|
|
25
|
+
*
|
|
26
|
+
* ============================================================================
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs').promises;
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const chalk = require('chalk');
|
|
32
|
+
const ora = require('ora');
|
|
33
|
+
const { getGentPath, readJSON, writeJSON, pathExists, getAllFiles, getIgnorePatterns } = require('../utils/fileSystem');
|
|
34
|
+
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
35
|
+
const { storeBlob, readBlobAsString, hashBlob } = require('../utils/hash-engine');
|
|
36
|
+
|
|
37
|
+
const STASH_FILE = 'stash.json';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stash management
|
|
41
|
+
* @param {String} subcommand - pop|list|drop|apply (null = stash push)
|
|
42
|
+
* @param {Object} options
|
|
43
|
+
*/
|
|
44
|
+
async function stash(subcommand, options) {
|
|
45
|
+
try {
|
|
46
|
+
const gentPath = await getGentPath();
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
|
|
49
|
+
switch (subcommand) {
|
|
50
|
+
case 'pop':
|
|
51
|
+
await stashPop(gentPath, cwd, options);
|
|
52
|
+
break;
|
|
53
|
+
case 'list':
|
|
54
|
+
await stashList(gentPath);
|
|
55
|
+
break;
|
|
56
|
+
case 'drop':
|
|
57
|
+
await stashDrop(gentPath, options);
|
|
58
|
+
break;
|
|
59
|
+
case 'apply':
|
|
60
|
+
await stashApply(gentPath, cwd, options, false);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
await stashPush(gentPath, cwd, options);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
68
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
69
|
+
} else {
|
|
70
|
+
console.error(chalk.red('Error:'), error.message);
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Push current changes onto stash stack
|
|
78
|
+
*/
|
|
79
|
+
async function stashPush(gentPath, cwd, options) {
|
|
80
|
+
const spinner = ora('Stashing changes...').start();
|
|
81
|
+
|
|
82
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
83
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
84
|
+
const headHash = repository.branches[repository.currentBranch] || null;
|
|
85
|
+
const headCommit = headHash ? (repository.commits || []).find(c => c.hash === headHash) : null;
|
|
86
|
+
|
|
87
|
+
// Get HEAD tree
|
|
88
|
+
const headTree = new Map();
|
|
89
|
+
if (headCommit) {
|
|
90
|
+
const tree = headCommit.tree || headCommit.files || [];
|
|
91
|
+
for (const f of tree) headTree.set(f.path || f.name, f.hash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Collect modified working tree files
|
|
95
|
+
const ignorePatterns = await getIgnorePatterns(cwd);
|
|
96
|
+
const allFiles = await getAllFiles(cwd, ignorePatterns);
|
|
97
|
+
const workingChanges = [];
|
|
98
|
+
|
|
99
|
+
for (const absPath of allFiles) {
|
|
100
|
+
const relPath = path.relative(cwd, absPath);
|
|
101
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
102
|
+
const currentHash = hashBlob(content);
|
|
103
|
+
const headBlobHash = headTree.get(relPath);
|
|
104
|
+
|
|
105
|
+
if (headBlobHash && currentHash !== headBlobHash) {
|
|
106
|
+
const blobHash = await storeBlob(gentPath, content);
|
|
107
|
+
workingChanges.push({ path: relPath, hash: blobHash });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const stagedEntries = staging.entries || [];
|
|
112
|
+
|
|
113
|
+
if (workingChanges.length === 0 && stagedEntries.length === 0) {
|
|
114
|
+
spinner.info(chalk.yellow('No local changes to stash'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Save stash entry
|
|
119
|
+
const stashes = await readStashes(gentPath);
|
|
120
|
+
stashes.unshift({
|
|
121
|
+
message: options.message || `WIP on ${repository.currentBranch}`,
|
|
122
|
+
branch: repository.currentBranch,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
stagedEntries: stagedEntries,
|
|
125
|
+
workingChanges: workingChanges
|
|
126
|
+
});
|
|
127
|
+
await writeStashes(gentPath, stashes);
|
|
128
|
+
|
|
129
|
+
// Clear staging
|
|
130
|
+
staging.entries = [];
|
|
131
|
+
staging.files = [];
|
|
132
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
133
|
+
|
|
134
|
+
// Restore working tree to HEAD state
|
|
135
|
+
for (const change of workingChanges) {
|
|
136
|
+
const headBlobHash = headTree.get(change.path);
|
|
137
|
+
if (headBlobHash) {
|
|
138
|
+
try {
|
|
139
|
+
const content = await readBlobAsString(gentPath, headBlobHash);
|
|
140
|
+
await fs.writeFile(path.join(cwd, change.path), content, 'utf-8');
|
|
141
|
+
} catch { /* best effort */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
spinner.succeed(chalk.green(`Stashed ${stagedEntries.length} staged + ${workingChanges.length} working tree changes`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pop: apply and remove most recent stash
|
|
150
|
+
*/
|
|
151
|
+
async function stashPop(gentPath, cwd, options) {
|
|
152
|
+
await stashApply(gentPath, cwd, options, true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Apply stash entry to working tree
|
|
157
|
+
*/
|
|
158
|
+
async function stashApply(gentPath, cwd, options, removAfter) {
|
|
159
|
+
const index = options.index ? parseInt(options.index) : 0;
|
|
160
|
+
const stashes = await readStashes(gentPath);
|
|
161
|
+
|
|
162
|
+
if (stashes.length === 0) {
|
|
163
|
+
console.log(chalk.yellow('No stash entries'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (index >= stashes.length) {
|
|
168
|
+
console.error(chalk.red(`stash@{${index}} does not exist`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const entry = stashes[index];
|
|
173
|
+
|
|
174
|
+
// Restore working tree changes
|
|
175
|
+
for (const change of (entry.workingChanges || [])) {
|
|
176
|
+
try {
|
|
177
|
+
const content = await readBlobAsString(gentPath, change.hash);
|
|
178
|
+
const fullPath = path.join(cwd, change.path);
|
|
179
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
180
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
181
|
+
} catch { /* blob missing */ }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Restore staged entries
|
|
185
|
+
if (entry.stagedEntries && entry.stagedEntries.length > 0) {
|
|
186
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
187
|
+
staging.entries = entry.stagedEntries;
|
|
188
|
+
staging.files = entry.stagedEntries.map(e => e.path);
|
|
189
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (removAfter) {
|
|
193
|
+
stashes.splice(index, 1);
|
|
194
|
+
await writeStashes(gentPath, stashes);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(chalk.green(`Applied stash@{${index}}: ${entry.message}`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* List stash entries
|
|
202
|
+
*/
|
|
203
|
+
async function stashList(gentPath) {
|
|
204
|
+
const stashes = await readStashes(gentPath);
|
|
205
|
+
|
|
206
|
+
if (stashes.length === 0) {
|
|
207
|
+
console.log(chalk.gray('No stash entries'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
stashes.forEach((entry, i) => {
|
|
212
|
+
const ts = new Date(entry.timestamp).toLocaleString();
|
|
213
|
+
console.log(
|
|
214
|
+
chalk.yellow(`stash@{${i}}: `) +
|
|
215
|
+
chalk.white(entry.message) +
|
|
216
|
+
chalk.gray(` (${entry.branch}, ${ts})`)
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Drop stash entry
|
|
223
|
+
*/
|
|
224
|
+
async function stashDrop(gentPath, options) {
|
|
225
|
+
const index = options.index ? parseInt(options.index) : 0;
|
|
226
|
+
const stashes = await readStashes(gentPath);
|
|
227
|
+
|
|
228
|
+
if (index >= stashes.length) {
|
|
229
|
+
console.error(chalk.red(`stash@{${index}} does not exist`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const removed = stashes.splice(index, 1);
|
|
234
|
+
await writeStashes(gentPath, stashes);
|
|
235
|
+
console.log(chalk.green(`Dropped stash@{${index}}: ${removed[0].message}`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read stash stack from disk
|
|
240
|
+
*/
|
|
241
|
+
async function readStashes(gentPath) {
|
|
242
|
+
const stashPath = path.join(gentPath, STASH_FILE);
|
|
243
|
+
if (!await pathExists(stashPath)) return [];
|
|
244
|
+
const data = await readJSON(stashPath);
|
|
245
|
+
return data.stashes || [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Write stash stack to disk
|
|
250
|
+
*/
|
|
251
|
+
async function writeStashes(gentPath, stashes) {
|
|
252
|
+
await writeJSON(path.join(gentPath, STASH_FILE), { stashes });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = stash;
|
package/src/commands/status.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Status Command - Show working tree status
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Display staged, modified, untracked, and deleted files. Like `git status`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent status → Detailed status
|
|
11
|
+
* gent status -s → Short format
|
|
12
|
+
*
|
|
13
|
+
* ALGORITHM:
|
|
14
|
+
* Computes SHA-256 blob hash for each working file, compares against:
|
|
15
|
+
* - Staging entries (from gent add)
|
|
16
|
+
* - Last commit tree (from commits.json)
|
|
17
|
+
* Classifies files as staged/modified/untracked/deleted.
|
|
18
|
+
*
|
|
19
|
+
* BACKEND EXPECTATIONS:
|
|
20
|
+
* None (local only).
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
4
23
|
*/
|
|
5
24
|
|
|
6
25
|
const fs = require('fs').promises;
|
|
7
26
|
const path = require('path');
|
|
8
27
|
const chalk = require('chalk');
|
|
9
|
-
const { getGentPath, readJSON, pathExists,
|
|
28
|
+
const { getGentPath, readJSON, pathExists, getIgnorePatterns, getAllFiles } = require('../utils/fileSystem');
|
|
10
29
|
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
11
|
-
const {
|
|
30
|
+
const { hashBlob } = require('../utils/hash-engine');
|
|
12
31
|
|
|
13
32
|
/**
|
|
14
33
|
* Show repository status
|
|
@@ -19,59 +38,66 @@ async function status(options) {
|
|
|
19
38
|
const gentPath = await getGentPath();
|
|
20
39
|
const cwd = process.cwd();
|
|
21
40
|
|
|
22
|
-
// Read staging area and commits
|
|
23
41
|
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
24
42
|
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
25
43
|
|
|
26
|
-
const
|
|
44
|
+
const stagedEntries = staging.entries || [];
|
|
45
|
+
const stagedFiles = stagedEntries.map(e => e.path);
|
|
27
46
|
const currentBranch = repository.currentBranch || 'main';
|
|
28
|
-
const
|
|
47
|
+
const lastCommitHash = repository.branches[currentBranch];
|
|
48
|
+
|
|
49
|
+
// Build HEAD tree map: path → blobHash
|
|
50
|
+
const lastCommit = lastCommitHash
|
|
51
|
+
? (repository.commits || []).find(c => c.hash === lastCommitHash)
|
|
52
|
+
: null;
|
|
53
|
+
const trackedMap = new Map();
|
|
54
|
+
if (lastCommit) {
|
|
55
|
+
const tree = lastCommit.tree || lastCommit.files || [];
|
|
56
|
+
for (const f of tree) trackedMap.set(f.path || f.name, f.hash);
|
|
57
|
+
}
|
|
29
58
|
|
|
30
|
-
// Get all
|
|
59
|
+
// Get all working directory files
|
|
31
60
|
const ignorePatterns = await getIgnorePatterns(cwd);
|
|
32
61
|
const allFiles = await getAllFiles(cwd, ignorePatterns);
|
|
33
62
|
|
|
34
|
-
// Get tracked files from last commit
|
|
35
|
-
const trackedFiles = await getTrackedFiles(gentPath, lastCommit);
|
|
36
|
-
|
|
37
|
-
// Categorize files
|
|
38
63
|
const stagedSet = new Set(stagedFiles);
|
|
39
|
-
const trackedSet = new Set(trackedFiles.map(f => f.path));
|
|
40
|
-
|
|
41
64
|
const modified = [];
|
|
42
65
|
const untracked = [];
|
|
43
66
|
const deleted = [];
|
|
44
67
|
|
|
45
|
-
// Check
|
|
46
|
-
for (const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
modified.push(
|
|
68
|
+
// Check working tree against HEAD
|
|
69
|
+
for (const absFile of allFiles) {
|
|
70
|
+
const relPath = path.relative(cwd, absFile);
|
|
71
|
+
const headHash = trackedMap.get(relPath);
|
|
72
|
+
|
|
73
|
+
if (headHash) {
|
|
74
|
+
// Tracked file — check modification via blob hash
|
|
75
|
+
const content = await fs.readFile(absFile);
|
|
76
|
+
const currentHash = hashBlob(content);
|
|
77
|
+
if (currentHash !== headHash && !stagedSet.has(relPath)) {
|
|
78
|
+
modified.push(relPath);
|
|
56
79
|
}
|
|
57
|
-
} else if (!stagedSet.has(
|
|
58
|
-
untracked.push(
|
|
80
|
+
} else if (!stagedSet.has(relPath)) {
|
|
81
|
+
untracked.push(relPath);
|
|
59
82
|
}
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
// Check for deleted files
|
|
63
|
-
for (const
|
|
64
|
-
const fullPath = path.join(cwd,
|
|
65
|
-
if (!await pathExists(fullPath) && !stagedSet.has(
|
|
66
|
-
deleted.push(
|
|
86
|
+
for (const [trackedPath] of trackedMap) {
|
|
87
|
+
const fullPath = path.join(cwd, trackedPath);
|
|
88
|
+
if (!await pathExists(fullPath) && !stagedSet.has(trackedPath)) {
|
|
89
|
+
deleted.push(trackedPath);
|
|
67
90
|
}
|
|
68
91
|
}
|
|
69
92
|
|
|
70
|
-
//
|
|
93
|
+
// Check for merge in progress
|
|
94
|
+
const mergeState = staging.mergeState || null;
|
|
95
|
+
|
|
96
|
+
// Display
|
|
71
97
|
if (options.short) {
|
|
72
|
-
displayShortStatus(
|
|
98
|
+
displayShortStatus(stagedEntries, modified, untracked, deleted);
|
|
73
99
|
} else {
|
|
74
|
-
displayDetailedStatus(currentBranch,
|
|
100
|
+
displayDetailedStatus(currentBranch, stagedEntries, modified, untracked, deleted, lastCommitHash, mergeState);
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
} catch (error) {
|
|
@@ -88,9 +114,13 @@ async function status(options) {
|
|
|
88
114
|
/**
|
|
89
115
|
* Display detailed status output
|
|
90
116
|
*/
|
|
91
|
-
function displayDetailedStatus(branch,
|
|
117
|
+
function displayDetailedStatus(branch, stagedEntries, modified, untracked, deleted, lastCommit, mergeState) {
|
|
92
118
|
console.log(chalk.bold(`On branch ${chalk.cyan(branch)}`));
|
|
93
119
|
|
|
120
|
+
if (mergeState) {
|
|
121
|
+
console.log(chalk.yellow(`Merging branch '${mergeState.sourceBranch}'`));
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
if (!lastCommit) {
|
|
95
125
|
console.log(chalk.gray('No commits yet\n'));
|
|
96
126
|
} else {
|
|
@@ -98,12 +128,15 @@ function displayDetailedStatus(branch, staged, modified, untracked, deleted, las
|
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
// Staged files
|
|
101
|
-
if (
|
|
131
|
+
if (stagedEntries.length > 0) {
|
|
102
132
|
console.log(chalk.green.bold('Changes to be committed:'));
|
|
103
133
|
console.log(chalk.gray(' (use "gent reset <file>..." to unstage)\n'));
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
for (const entry of stagedEntries) {
|
|
135
|
+
const icon = entry.status === 'added' ? 'new file: '
|
|
136
|
+
: entry.status === 'deleted' ? 'deleted: '
|
|
137
|
+
: 'modified: ';
|
|
138
|
+
console.log(chalk.green(`\t${icon}${entry.path}`));
|
|
139
|
+
}
|
|
107
140
|
console.log();
|
|
108
141
|
}
|
|
109
142
|
|
|
@@ -136,9 +169,9 @@ function displayDetailedStatus(branch, staged, modified, untracked, deleted, las
|
|
|
136
169
|
}
|
|
137
170
|
|
|
138
171
|
// Status summary
|
|
139
|
-
if (
|
|
140
|
-
console.log(chalk.green('
|
|
141
|
-
} else if (
|
|
172
|
+
if (stagedEntries.length === 0 && modified.length === 0 && untracked.length === 0 && deleted.length === 0) {
|
|
173
|
+
console.log(chalk.green('Working tree clean'));
|
|
174
|
+
} else if (stagedEntries.length === 0) {
|
|
142
175
|
console.log(chalk.yellow('No changes added to commit (use "gent add" to track files)'));
|
|
143
176
|
}
|
|
144
177
|
}
|
|
@@ -146,10 +179,11 @@ function displayDetailedStatus(branch, staged, modified, untracked, deleted, las
|
|
|
146
179
|
/**
|
|
147
180
|
* Display short status output
|
|
148
181
|
*/
|
|
149
|
-
function displayShortStatus(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
function displayShortStatus(stagedEntries, modified, untracked, deleted) {
|
|
183
|
+
for (const entry of stagedEntries) {
|
|
184
|
+
const code = entry.status === 'added' ? 'A ' : entry.status === 'deleted' ? 'D ' : 'M ';
|
|
185
|
+
console.log(chalk.green(code) + entry.path);
|
|
186
|
+
}
|
|
153
187
|
|
|
154
188
|
modified.forEach(file => {
|
|
155
189
|
console.log(chalk.red(' M ') + file);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Tag Command - Create, list, and delete named references to commits
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Mark specific commits with version labels (e.g. v1.0.0). Like `git tag`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent tag → List all tags
|
|
11
|
+
* gent tag <name> → Create lightweight tag on HEAD
|
|
12
|
+
* gent tag <name> -m <msg> → Create annotated tag with message
|
|
13
|
+
* gent tag -d <name> → Delete a tag
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* Tags stored in commits.json under "tags" map: { name → { hash, message, ... } }
|
|
17
|
+
* Lightweight tag = just a name pointing to commit hash.
|
|
18
|
+
* Annotated tag = includes tagger info, message, timestamp.
|
|
19
|
+
*
|
|
20
|
+
* BACKEND EXPECTATIONS:
|
|
21
|
+
* POST /api/repos/:id/tags/ { name, hash, message, annotated }
|
|
22
|
+
* GET /api/repos/:id/tags/
|
|
23
|
+
* DELETE /api/repos/:id/tags/:name/
|
|
24
|
+
*
|
|
25
|
+
* ============================================================================
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const chalk = require('chalk');
|
|
30
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
31
|
+
const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
32
|
+
const authStorage = require('../utils/auth-storage');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Manage tags
|
|
36
|
+
* @param {String} name - Tag name (optional)
|
|
37
|
+
* @param {Object} options
|
|
38
|
+
*/
|
|
39
|
+
async function tag(name, options) {
|
|
40
|
+
try {
|
|
41
|
+
const gentPath = await getGentPath();
|
|
42
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
43
|
+
repository.tags = repository.tags || {};
|
|
44
|
+
|
|
45
|
+
if (options.delete) {
|
|
46
|
+
await deleteTag(options.delete, repository, gentPath);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (name) {
|
|
51
|
+
await createTag(name, repository, gentPath, options);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// List tags
|
|
56
|
+
listTags(repository);
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
60
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
61
|
+
} else {
|
|
62
|
+
console.error(chalk.red('Error:'), error.message);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List all tags
|
|
70
|
+
*/
|
|
71
|
+
function listTags(repository) {
|
|
72
|
+
const tags = Object.keys(repository.tags).sort();
|
|
73
|
+
|
|
74
|
+
if (tags.length === 0) {
|
|
75
|
+
console.log(chalk.gray('No tags'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const tagName of tags) {
|
|
80
|
+
const t = repository.tags[tagName];
|
|
81
|
+
const short = t.hash ? t.hash.substring(0, 7) : '???????';
|
|
82
|
+
const msg = t.message ? chalk.gray(` — ${t.message}`) : '';
|
|
83
|
+
console.log(chalk.yellow(tagName) + chalk.gray(` → ${short}`) + msg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a tag
|
|
89
|
+
*/
|
|
90
|
+
async function createTag(name, repository, gentPath, options) {
|
|
91
|
+
if (repository.tags[name]) {
|
|
92
|
+
console.error(chalk.red(`Tag '${name}' already exists`));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const currentBranch = repository.currentBranch;
|
|
97
|
+
const commitHash = repository.branches[currentBranch];
|
|
98
|
+
|
|
99
|
+
if (!commitHash) {
|
|
100
|
+
console.error(chalk.red('No commits to tag'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tagObj = { hash: commitHash };
|
|
105
|
+
|
|
106
|
+
// Annotated tag
|
|
107
|
+
if (options.message) {
|
|
108
|
+
const config = await readJSON(path.join(gentPath, CONFIG_FILE));
|
|
109
|
+
let taggerName = config.user.name;
|
|
110
|
+
let taggerEmail = config.user.email;
|
|
111
|
+
|
|
112
|
+
if (!taggerName || !taggerEmail) {
|
|
113
|
+
const user = await authStorage.getUser();
|
|
114
|
+
if (user) {
|
|
115
|
+
taggerName = taggerName || [user.first_name, user.last_name].filter(Boolean).join(' ');
|
|
116
|
+
taggerEmail = taggerEmail || user.email;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
tagObj.message = options.message;
|
|
121
|
+
tagObj.annotated = true;
|
|
122
|
+
tagObj.tagger = { name: taggerName || 'Unknown', email: taggerEmail || '' };
|
|
123
|
+
tagObj.timestamp = new Date().toISOString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
repository.tags[name] = tagObj;
|
|
127
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
128
|
+
|
|
129
|
+
console.log(chalk.green(`Created tag '${name}' → ${commitHash.substring(0, 7)}`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delete a tag
|
|
134
|
+
*/
|
|
135
|
+
async function deleteTag(name, repository, gentPath) {
|
|
136
|
+
if (!repository.tags[name]) {
|
|
137
|
+
console.error(chalk.red(`Tag '${name}' not found`));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
delete repository.tags[name];
|
|
142
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
143
|
+
console.log(chalk.green(`Deleted tag '${name}'`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = tag;
|