gent-cli 2.0.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 -284
- package/src/utils/cloud-sync.js +0 -323
- package/src/utils/diff.js +0 -121
package/src/commands/commit.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commit Command - Record changes to the repository
|
|
3
|
-
* Creates a new commit with staged
|
|
3
|
+
* Creates a new commit with tree object referencing staged blobs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const fs = require('fs').promises;
|
|
8
7
|
const chalk = require('chalk');
|
|
9
8
|
const inquirer = require('inquirer');
|
|
10
9
|
const ora = require('ora');
|
|
11
|
-
const { getGentPath, readJSON, writeJSON
|
|
10
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
12
11
|
const authStorage = require('../utils/auth-storage');
|
|
13
12
|
const { STAGING_FILE, COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
14
|
-
const { generateCommitHash
|
|
15
|
-
const {
|
|
13
|
+
const { generateCommitHash } = require('../utils/helpers');
|
|
14
|
+
const { storeTree, snapshotFile } = require('../utils/hash-engine');
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Create a new commit
|
|
@@ -25,9 +24,10 @@ async function commit(options) {
|
|
|
25
24
|
|
|
26
25
|
// Read staging area
|
|
27
26
|
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
27
|
+
const stagedEntries = staging.entries || [];
|
|
28
28
|
const stagedFiles = staging.files || [];
|
|
29
29
|
|
|
30
|
-
if (stagedFiles.length === 0) {
|
|
30
|
+
if (stagedEntries.length === 0 && stagedFiles.length === 0) {
|
|
31
31
|
console.log(chalk.yellow('No changes added to commit'));
|
|
32
32
|
console.log(chalk.gray('Use "gent add <file>..." to stage files'));
|
|
33
33
|
return;
|
|
@@ -59,20 +59,14 @@ async function commit(options) {
|
|
|
59
59
|
let authorName = config.user.name;
|
|
60
60
|
let authorEmail = config.user.email;
|
|
61
61
|
|
|
62
|
-
// Fallback to global auth if local config is empty
|
|
63
62
|
if (!authorName || !authorEmail) {
|
|
64
63
|
const globalUser = await authStorage.getUser();
|
|
65
64
|
if (globalUser) {
|
|
66
|
-
if (!authorName)
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
if (!authorEmail) {
|
|
70
|
-
authorEmail = globalUser.email;
|
|
71
|
-
}
|
|
65
|
+
if (!authorName) authorName = [globalUser.first_name, globalUser.last_name].filter(Boolean).join(' ');
|
|
66
|
+
if (!authorEmail) authorEmail = globalUser.email;
|
|
72
67
|
}
|
|
73
68
|
}
|
|
74
69
|
|
|
75
|
-
// Fail if still no identity
|
|
76
70
|
if (!authorName || !authorEmail) {
|
|
77
71
|
spinner.stop();
|
|
78
72
|
console.error(chalk.red('Author identity unknown'));
|
|
@@ -80,92 +74,106 @@ async function commit(options) {
|
|
|
80
74
|
return;
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
// Build tree entries from staged data
|
|
78
|
+
let treeEntries = [];
|
|
79
|
+
|
|
80
|
+
if (stagedEntries.length > 0) {
|
|
81
|
+
// New format: entries already have blob hashes from gent add
|
|
82
|
+
// Carry forward unchanged files from parent commit
|
|
83
|
+
const parentHash = repository.branches[repository.currentBranch] || null;
|
|
84
|
+
const parentCommit = parentHash
|
|
85
|
+
? (repository.commits || []).find(c => c.hash === parentHash)
|
|
86
|
+
: null;
|
|
87
|
+
const parentTree = parentCommit && parentCommit.tree
|
|
88
|
+
? parentCommit.tree
|
|
89
|
+
: (parentCommit ? parentCommit.files.map(f => ({ mode: '100644', name: f.path, hash: f.hash, type: 'blob' })) : []);
|
|
90
|
+
|
|
91
|
+
// Start from parent tree, overlay staged changes
|
|
92
|
+
const treeMap = new Map(parentTree.map(e => [e.name, e]));
|
|
93
|
+
|
|
94
|
+
for (const entry of stagedEntries) {
|
|
95
|
+
if (entry.status === 'deleted') {
|
|
96
|
+
treeMap.delete(entry.path);
|
|
97
|
+
} else {
|
|
98
|
+
treeMap.set(entry.path, {
|
|
99
|
+
mode: '100644',
|
|
100
|
+
name: entry.path,
|
|
101
|
+
hash: entry.hash,
|
|
102
|
+
type: 'blob'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
parentCommit.files.forEach(f => parentFilesMap.set(f.path, f.hash));
|
|
107
|
+
treeEntries = Array.from(treeMap.values());
|
|
108
|
+
} else {
|
|
109
|
+
// Legacy fallback: files array without blob hashes
|
|
110
|
+
for (const file of stagedFiles) {
|
|
111
|
+
const entry = await snapshotFile(gentPath, cwd, file);
|
|
112
|
+
treeEntries.push(entry);
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
// Store tree object
|
|
117
|
+
const treeHash = await storeTree(gentPath, treeEntries);
|
|
118
|
+
|
|
119
|
+
// Compute diff stats
|
|
120
|
+
let totalInsertions = 0;
|
|
121
|
+
let totalDeletions = 0;
|
|
122
|
+
if (stagedEntries.length > 0) {
|
|
123
|
+
for (const e of stagedEntries) {
|
|
124
|
+
if (e.stats) {
|
|
125
|
+
totalInsertions += e.stats.insertions || 0;
|
|
126
|
+
totalDeletions += e.stats.deletions || 0;
|
|
127
|
+
}
|
|
117
128
|
}
|
|
129
|
+
}
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
// Create commit object
|
|
132
|
+
const commitObj = {
|
|
133
|
+
hash: generateCommitHash(),
|
|
134
|
+
message,
|
|
135
|
+
author: {
|
|
136
|
+
name: authorName,
|
|
137
|
+
email: authorEmail
|
|
138
|
+
},
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
parent: repository.branches[repository.currentBranch] || null,
|
|
141
|
+
treeHash,
|
|
142
|
+
tree: treeEntries,
|
|
143
|
+
files: treeEntries.map(e => ({ path: e.name, hash: e.hash })), // backward compat
|
|
144
|
+
stats: {
|
|
145
|
+
filesChanged: stagedEntries.length || stagedFiles.length,
|
|
146
|
+
insertions: totalInsertions,
|
|
147
|
+
deletions: totalDeletions
|
|
129
148
|
}
|
|
149
|
+
};
|
|
130
150
|
|
|
131
|
-
|
|
132
|
-
const changes = computeDiff(oldContent, content);
|
|
133
|
-
|
|
134
|
-
commit.files.push({
|
|
135
|
-
path: file,
|
|
136
|
-
hash: hash,
|
|
137
|
-
diff: changes
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Add commit to repository
|
|
151
|
+
// Save commit
|
|
142
152
|
repository.commits = repository.commits || [];
|
|
143
|
-
repository.commits.push(
|
|
144
|
-
repository.branches[repository.currentBranch] =
|
|
153
|
+
repository.commits.push(commitObj);
|
|
154
|
+
repository.branches[repository.currentBranch] = commitObj.hash;
|
|
145
155
|
|
|
146
|
-
// Save repository and clear staging
|
|
147
156
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
157
|
+
|
|
158
|
+
// Clear staging
|
|
159
|
+
staging.entries = [];
|
|
148
160
|
staging.files = [];
|
|
149
161
|
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
150
162
|
|
|
151
|
-
spinner.succeed(chalk.green('
|
|
152
|
-
|
|
153
|
-
// Display commit info
|
|
154
|
-
console.log(chalk.cyan(`\n[${repository.currentBranch} ${commit.sha.substring(0, 7)}] ${message}`));
|
|
155
|
-
console.log(chalk.gray(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
156
|
-
console.log(chalk.gray(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
157
|
-
console.log(chalk.gray(`\n${commit.files.length} file(s) changed`));
|
|
163
|
+
spinner.succeed(chalk.green('Changes committed successfully!'));
|
|
158
164
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
165
|
+
console.log(chalk.cyan(`\n[${repository.currentBranch} ${commitObj.hash.substring(0, 7)}] ${message}`));
|
|
166
|
+
console.log(chalk.gray(`Author: ${commitObj.author.name} <${commitObj.author.email}>`));
|
|
167
|
+
console.log(chalk.gray(`Date: ${new Date(commitObj.timestamp).toLocaleString()}`));
|
|
168
|
+
console.log(chalk.gray(`Tree: ${treeHash.substring(0, 7)}`));
|
|
169
|
+
console.log(chalk.gray(`\n${commitObj.stats.filesChanged} file(s) changed, `) +
|
|
170
|
+
chalk.green(`+${totalInsertions} insertions`) + ', ' +
|
|
171
|
+
chalk.red(`-${totalDeletions} deletions`));
|
|
164
172
|
|
|
165
173
|
} catch (error) {
|
|
166
174
|
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
167
175
|
console.error(chalk.red('Error: Not a gent repository'));
|
|
168
|
-
console.log(chalk.yellow('\
|
|
176
|
+
console.log(chalk.yellow('\nRun "gent init" to initialize a repository'));
|
|
169
177
|
} else {
|
|
170
178
|
console.error(chalk.red('Error:'), error.message);
|
|
171
179
|
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Diff Command - Show changes between commits, staging, and working tree
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Display line-by-line differences between file versions, similar to
|
|
8
|
+
* `git diff`. Shows what changed, where, and how much.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent diff → Working tree vs staging area
|
|
12
|
+
* gent diff --staged → Staging area vs last commit
|
|
13
|
+
* gent diff <file> → Diff specific file(s)
|
|
14
|
+
* gent diff --stat → Summary only (no patch)
|
|
15
|
+
*
|
|
16
|
+
* ALGORITHM:
|
|
17
|
+
* Uses LCS (Longest Common Subsequence) line-level diff from diff-engine.js.
|
|
18
|
+
* Outputs unified diff format with context lines (3 by default).
|
|
19
|
+
* Time complexity: O(m*n) where m,n = line counts of old/new files.
|
|
20
|
+
*
|
|
21
|
+
* BACKEND EXPECTATIONS:
|
|
22
|
+
* None (local only). Backend receives final blob hashes, not diffs.
|
|
23
|
+
*
|
|
24
|
+
* ============================================================================
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs').promises;
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const chalk = require('chalk');
|
|
30
|
+
const { getGentPath, readJSON, pathExists, getAllFiles, getIgnorePatterns } = require('../utils/fileSystem');
|
|
31
|
+
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
32
|
+
const { readBlobAsString, hashBlob } = require('../utils/hash-engine');
|
|
33
|
+
const { formatUnifiedDiff, diffText } = require('../utils/diff-engine');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Show differences
|
|
37
|
+
* @param {Array} files - Optional specific files to diff
|
|
38
|
+
* @param {Object} options - Command options
|
|
39
|
+
*/
|
|
40
|
+
async function diff(files, options) {
|
|
41
|
+
try {
|
|
42
|
+
const gentPath = await getGentPath();
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
|
|
45
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
46
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
47
|
+
|
|
48
|
+
const currentBranch = repository.currentBranch || 'main';
|
|
49
|
+
const headHash = repository.branches[currentBranch] || null;
|
|
50
|
+
const headCommit = headHash
|
|
51
|
+
? (repository.commits || []).find(c => c.hash === headHash)
|
|
52
|
+
: null;
|
|
53
|
+
|
|
54
|
+
// Build HEAD tree map: path → blobHash
|
|
55
|
+
const headTree = buildTreeMap(headCommit);
|
|
56
|
+
|
|
57
|
+
// Build staging map: path → blobHash
|
|
58
|
+
const stagingEntries = staging.entries || [];
|
|
59
|
+
const stagingMap = new Map(stagingEntries.map(e => [e.path, e.hash]));
|
|
60
|
+
|
|
61
|
+
if (options.staged) {
|
|
62
|
+
// Staged vs HEAD
|
|
63
|
+
await diffStagedVsHead(gentPath, cwd, stagingEntries, headTree, files, options);
|
|
64
|
+
} else {
|
|
65
|
+
// Working tree vs staged (or HEAD if not staged)
|
|
66
|
+
await diffWorkingTree(gentPath, cwd, stagingMap, headTree, files, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
71
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
72
|
+
console.log(chalk.yellow('Run "gent init" to initialize a repository'));
|
|
73
|
+
} else {
|
|
74
|
+
console.error(chalk.red('Error:'), error.message);
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Diff working tree vs staging/HEAD
|
|
82
|
+
*/
|
|
83
|
+
async function diffWorkingTree(gentPath, cwd, stagingMap, headTree, filterFiles, options) {
|
|
84
|
+
const ignorePatterns = await getIgnorePatterns(cwd);
|
|
85
|
+
const allFiles = await getAllFiles(cwd, ignorePatterns);
|
|
86
|
+
let hasDiffs = false;
|
|
87
|
+
|
|
88
|
+
let totalInsertions = 0;
|
|
89
|
+
let totalDeletions = 0;
|
|
90
|
+
const fileSummaries = [];
|
|
91
|
+
|
|
92
|
+
for (const absPath of allFiles) {
|
|
93
|
+
const relPath = path.relative(cwd, absPath);
|
|
94
|
+
|
|
95
|
+
// Filter if specific files provided
|
|
96
|
+
if (filterFiles && filterFiles.length > 0 && !filterFiles.includes(relPath)) continue;
|
|
97
|
+
|
|
98
|
+
// Determine base hash (staging → HEAD fallback)
|
|
99
|
+
const baseHash = stagingMap.get(relPath) || headTree.get(relPath);
|
|
100
|
+
if (!baseHash) continue; // untracked
|
|
101
|
+
|
|
102
|
+
const currentContent = await fs.readFile(absPath, 'utf-8');
|
|
103
|
+
const currentHash = hashBlob(currentContent);
|
|
104
|
+
|
|
105
|
+
if (currentHash === baseHash) continue; // unchanged
|
|
106
|
+
|
|
107
|
+
let oldContent = '';
|
|
108
|
+
try {
|
|
109
|
+
oldContent = await readBlobAsString(gentPath, baseHash);
|
|
110
|
+
} catch {
|
|
111
|
+
// No blob = new file
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const d = diffText(oldContent, currentContent);
|
|
115
|
+
totalInsertions += d.stats.insertions;
|
|
116
|
+
totalDeletions += d.stats.deletions;
|
|
117
|
+
fileSummaries.push({ file: relPath, stats: d.stats });
|
|
118
|
+
|
|
119
|
+
if (!options.stat) {
|
|
120
|
+
const unified = formatUnifiedDiff(relPath, oldContent, currentContent);
|
|
121
|
+
if (unified) {
|
|
122
|
+
hasDiffs = true;
|
|
123
|
+
printColorizedDiff(unified);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
hasDiffs = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.stat || hasDiffs) {
|
|
131
|
+
printDiffStat(fileSummaries, totalInsertions, totalDeletions);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!hasDiffs) {
|
|
135
|
+
console.log(chalk.gray('No changes'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Diff staged files vs HEAD commit
|
|
141
|
+
*/
|
|
142
|
+
async function diffStagedVsHead(gentPath, cwd, stagedEntries, headTree, filterFiles, options) {
|
|
143
|
+
let hasDiffs = false;
|
|
144
|
+
let totalInsertions = 0;
|
|
145
|
+
let totalDeletions = 0;
|
|
146
|
+
const fileSummaries = [];
|
|
147
|
+
|
|
148
|
+
for (const entry of stagedEntries) {
|
|
149
|
+
if (filterFiles && filterFiles.length > 0 && !filterFiles.includes(entry.path)) continue;
|
|
150
|
+
|
|
151
|
+
const headBlobHash = headTree.get(entry.path);
|
|
152
|
+
|
|
153
|
+
if (entry.status === 'deleted') {
|
|
154
|
+
if (headBlobHash) {
|
|
155
|
+
const oldContent = await readBlobAsString(gentPath, headBlobHash);
|
|
156
|
+
const d = diffText(oldContent, '');
|
|
157
|
+
totalDeletions += d.stats.deletions;
|
|
158
|
+
fileSummaries.push({ file: entry.path, stats: d.stats });
|
|
159
|
+
if (!options.stat) {
|
|
160
|
+
printColorizedDiff(formatUnifiedDiff(entry.path, oldContent, ''));
|
|
161
|
+
}
|
|
162
|
+
hasDiffs = true;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!entry.hash) continue;
|
|
168
|
+
|
|
169
|
+
if (entry.hash === headBlobHash) continue; // unchanged
|
|
170
|
+
|
|
171
|
+
let oldContent = '';
|
|
172
|
+
try {
|
|
173
|
+
if (headBlobHash) oldContent = await readBlobAsString(gentPath, headBlobHash);
|
|
174
|
+
} catch { /* new file */ }
|
|
175
|
+
|
|
176
|
+
const newContent = await readBlobAsString(gentPath, entry.hash);
|
|
177
|
+
const d = diffText(oldContent, newContent);
|
|
178
|
+
totalInsertions += d.stats.insertions;
|
|
179
|
+
totalDeletions += d.stats.deletions;
|
|
180
|
+
fileSummaries.push({ file: entry.path, stats: d.stats });
|
|
181
|
+
|
|
182
|
+
if (!options.stat) {
|
|
183
|
+
const unified = formatUnifiedDiff(entry.path, oldContent, newContent);
|
|
184
|
+
if (unified) {
|
|
185
|
+
hasDiffs = true;
|
|
186
|
+
printColorizedDiff(unified);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
hasDiffs = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (options.stat || hasDiffs) {
|
|
194
|
+
printDiffStat(fileSummaries, totalInsertions, totalDeletions);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!hasDiffs) {
|
|
198
|
+
console.log(chalk.gray('No staged changes'));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build path → hash map from commit object
|
|
204
|
+
*/
|
|
205
|
+
function buildTreeMap(commit) {
|
|
206
|
+
const map = new Map();
|
|
207
|
+
if (!commit) return map;
|
|
208
|
+
const tree = commit.tree || commit.files || [];
|
|
209
|
+
for (const f of tree) {
|
|
210
|
+
map.set(f.path || f.name, f.hash);
|
|
211
|
+
}
|
|
212
|
+
return map;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Print colorized unified diff
|
|
217
|
+
*/
|
|
218
|
+
function printColorizedDiff(unifiedDiff) {
|
|
219
|
+
const lines = unifiedDiff.split('\n');
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
if (line.startsWith('---') || line.startsWith('+++')) {
|
|
222
|
+
console.log(chalk.bold(line));
|
|
223
|
+
} else if (line.startsWith('@@')) {
|
|
224
|
+
console.log(chalk.cyan(line));
|
|
225
|
+
} else if (line.startsWith('+')) {
|
|
226
|
+
console.log(chalk.green(line));
|
|
227
|
+
} else if (line.startsWith('-')) {
|
|
228
|
+
console.log(chalk.red(line));
|
|
229
|
+
} else {
|
|
230
|
+
console.log(line);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log('');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Print diff stat summary
|
|
238
|
+
*/
|
|
239
|
+
function printDiffStat(fileSummaries, totalIns, totalDel) {
|
|
240
|
+
if (fileSummaries.length === 0) return;
|
|
241
|
+
|
|
242
|
+
console.log('');
|
|
243
|
+
const maxLen = Math.max(...fileSummaries.map(f => f.file.length));
|
|
244
|
+
|
|
245
|
+
for (const { file, stats } of fileSummaries) {
|
|
246
|
+
const total = stats.insertions + stats.deletions;
|
|
247
|
+
const bar = chalk.green('+'.repeat(Math.min(stats.insertions, 30))) +
|
|
248
|
+
chalk.red('-'.repeat(Math.min(stats.deletions, 30)));
|
|
249
|
+
console.log(` ${file.padEnd(maxLen)} | ${String(total).padStart(4)} ${bar}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(chalk.gray(` ${fileSummaries.length} file(s) changed, `) +
|
|
253
|
+
chalk.green(`${totalIns} insertion(s)`) + ', ' +
|
|
254
|
+
chalk.red(`${totalDel} deletion(s)`));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = diff;
|
package/src/commands/init.js
CHANGED
|
@@ -28,10 +28,9 @@ async function init(options) {
|
|
|
28
28
|
// Get authenticated user profile if available
|
|
29
29
|
let defaultName = '';
|
|
30
30
|
let defaultEmail = '';
|
|
31
|
-
let user = null;
|
|
32
31
|
|
|
33
32
|
try {
|
|
34
|
-
user = await authStorage.getUser();
|
|
33
|
+
const user = await authStorage.getUser();
|
|
35
34
|
if (user) {
|
|
36
35
|
if (user.first_name || user.last_name) {
|
|
37
36
|
defaultName = [user.first_name, user.last_name].filter(Boolean).join(' ');
|
|
@@ -63,8 +62,12 @@ async function init(options) {
|
|
|
63
62
|
await ensureDir(path.join(gentPath, 'refs', 'tags'));
|
|
64
63
|
|
|
65
64
|
// Create/Update configuration
|
|
65
|
+
// Only write config if it doesn't exist OR if we have valid user info to update
|
|
66
66
|
const configPath = path.join(gentPath, CONFIG_FILE);
|
|
67
67
|
if (!(await pathExists(configPath)) || (defaultName && defaultEmail)) {
|
|
68
|
+
// If re-init, we might want to preserve existing config unless we have better info?
|
|
69
|
+
// Git re-init doesn't overwrite config usually.
|
|
70
|
+
// But for now, let's write ensuring we have a config file.
|
|
68
71
|
if (!isReinit || !(await pathExists(configPath))) {
|
|
69
72
|
await writeJSON(configPath, config);
|
|
70
73
|
}
|
|
@@ -106,40 +109,6 @@ node_modules/
|
|
|
106
109
|
console.log(chalk.gray(`Initialized empty Gent repository in ${gentPath}`));
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
// Handle --cloud option
|
|
110
|
-
if (options.cloud) {
|
|
111
|
-
if (!user) {
|
|
112
|
-
console.log(chalk.yellow('\nWarning: Skipping cloud repository creation (not logged in)'));
|
|
113
|
-
console.log(chalk.gray('Run "gent login" then "gent create <name> --init-local"'));
|
|
114
|
-
} else {
|
|
115
|
-
try {
|
|
116
|
-
const repoService = require('../services/repo-service');
|
|
117
|
-
const { addRemote } = require('../utils/cloud-sync');
|
|
118
|
-
const ora = require('ora');
|
|
119
|
-
|
|
120
|
-
const spinner = ora('Creating cloud repository...').start();
|
|
121
|
-
const repoName = path.basename(cwd);
|
|
122
|
-
|
|
123
|
-
const repository = await repoService.createRepository(
|
|
124
|
-
repoName,
|
|
125
|
-
options.description || 'A gent repository',
|
|
126
|
-
options.private || false,
|
|
127
|
-
'main'
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const ownerId = repository.owner_id || repository.owner?.id || repository.owner;
|
|
131
|
-
const name = repository.name || repository.project_name || repoName;
|
|
132
|
-
|
|
133
|
-
await addRemote('origin', ownerId, name, cwd);
|
|
134
|
-
spinner.succeed(chalk.green(`Created cloud repository: ${ownerId}/${name}`));
|
|
135
|
-
console.log(chalk.gray(`Remote 'origin' added`));
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.log(chalk.red(`\nFailed to create cloud repository: ${error.message}`));
|
|
138
|
-
console.log(chalk.gray('You can create it later with "gent create <name>"'));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
112
|
} catch (error) {
|
|
144
113
|
console.error(chalk.red('Failed to initialize repository'));
|
|
145
114
|
console.error(chalk.red('Error:'), error.message);
|
package/src/commands/log.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Log Command - Show commit history
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Display commit history with details, stats, and merge info. Like `git log`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent log → Show last 10 commits (detailed)
|
|
11
|
+
* gent log -n 20 → Show last 20 commits
|
|
12
|
+
* gent log --oneline → Condensed one-line-per-commit view
|
|
13
|
+
* gent log --stat → Include diffstat per commit
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* Reads commits.json, filters by branch HEAD → parent chain, displays
|
|
17
|
+
* in reverse chronological order.
|
|
18
|
+
*
|
|
19
|
+
* BACKEND EXPECTATIONS:
|
|
20
|
+
* GET /api/repos/:id/commits/?branch=main&limit=10
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
4
23
|
*/
|
|
5
24
|
|
|
6
25
|
const path = require('path');
|
|
@@ -27,14 +46,24 @@ async function log(options) {
|
|
|
27
46
|
return;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
// Limit number of commits to show
|
|
31
49
|
const limit = parseInt(options.number) || 10;
|
|
32
|
-
|
|
50
|
+
|
|
51
|
+
// Walk branch chain for ordered display
|
|
52
|
+
const headHash = repository.branches[currentBranch];
|
|
53
|
+
const commitMap = new Map(commits.map(c => [c.hash, c]));
|
|
54
|
+
const ordered = [];
|
|
55
|
+
let cur = headHash;
|
|
56
|
+
while (cur && ordered.length < limit) {
|
|
57
|
+
const c = commitMap.get(cur);
|
|
58
|
+
if (!c) break;
|
|
59
|
+
ordered.push(c);
|
|
60
|
+
cur = c.parent;
|
|
61
|
+
}
|
|
33
62
|
|
|
34
63
|
if (options.oneline) {
|
|
35
|
-
displayOnelineLog(
|
|
64
|
+
displayOnelineLog(ordered, headHash);
|
|
36
65
|
} else {
|
|
37
|
-
displayDetailedLog(
|
|
66
|
+
displayDetailedLog(ordered, headHash, currentBranch, options);
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
} catch (error) {
|
|
@@ -51,21 +80,36 @@ async function log(options) {
|
|
|
51
80
|
/**
|
|
52
81
|
* Display detailed commit log
|
|
53
82
|
*/
|
|
54
|
-
function displayDetailedLog(commits, currentCommitHash, currentBranch) {
|
|
55
|
-
console.log(chalk.bold.cyan(`\nCommit History (${currentBranch}
|
|
83
|
+
function displayDetailedLog(commits, currentCommitHash, currentBranch, options) {
|
|
84
|
+
console.log(chalk.bold.cyan(`\nCommit History (${currentBranch}):\n`));
|
|
56
85
|
|
|
57
86
|
commits.forEach((commit, index) => {
|
|
58
|
-
const isHead = commit.
|
|
87
|
+
const isHead = commit.hash === currentCommitHash;
|
|
59
88
|
const headLabel = isHead ? chalk.yellow.bold(' (HEAD)') : '';
|
|
60
89
|
|
|
61
|
-
console.log(chalk.yellow(`commit ${commit.
|
|
90
|
+
console.log(chalk.yellow(`commit ${commit.hash}`) + headLabel);
|
|
91
|
+
if (commit.mergeParent) {
|
|
92
|
+
console.log(chalk.gray(`Merge: ${commit.parent?.substring(0, 7)} ${commit.mergeParent.substring(0, 7)}`));
|
|
93
|
+
}
|
|
62
94
|
console.log(chalk.white(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
63
95
|
console.log(chalk.white(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
64
96
|
console.log(chalk.gray(` (${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`));
|
|
97
|
+
if (commit.treeHash) {
|
|
98
|
+
console.log(chalk.gray(`Tree: ${commit.treeHash.substring(0, 7)}`));
|
|
99
|
+
}
|
|
65
100
|
console.log();
|
|
66
101
|
console.log(chalk.white(` ${commit.message}`));
|
|
67
102
|
console.log();
|
|
68
|
-
|
|
103
|
+
|
|
104
|
+
// Show stats if --stat flag or if commit has stats
|
|
105
|
+
if (options && options.stat && commit.stats) {
|
|
106
|
+
console.log(chalk.gray(` ${commit.stats.filesChanged} file(s), `) +
|
|
107
|
+
chalk.green(`+${commit.stats.insertions}`) + ' ' +
|
|
108
|
+
chalk.red(`-${commit.stats.deletions}`));
|
|
109
|
+
} else {
|
|
110
|
+
const fileCount = commit.files ? commit.files.length : (commit.tree ? commit.tree.length : 0);
|
|
111
|
+
console.log(chalk.gray(` ${fileCount} file(s) in tree`));
|
|
112
|
+
}
|
|
69
113
|
|
|
70
114
|
if (index < commits.length - 1) {
|
|
71
115
|
console.log(chalk.gray(' │'));
|
|
@@ -79,9 +123,9 @@ function displayDetailedLog(commits, currentCommitHash, currentBranch) {
|
|
|
79
123
|
*/
|
|
80
124
|
function displayOnelineLog(commits, currentCommitHash) {
|
|
81
125
|
commits.forEach(commit => {
|
|
82
|
-
const isHead = commit.
|
|
126
|
+
const isHead = commit.hash === currentCommitHash;
|
|
83
127
|
const headLabel = isHead ? chalk.yellow(' (HEAD)') : '';
|
|
84
|
-
const shortHash = chalk.yellow(commit.
|
|
128
|
+
const shortHash = chalk.yellow(commit.hash.substring(0, 7));
|
|
85
129
|
const message = chalk.white(commit.message);
|
|
86
130
|
const timeAgo = chalk.gray(`(${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`);
|
|
87
131
|
|