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
package/src/commands/remote.js
CHANGED
|
@@ -1,131 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Remote Command - Manage remote repository connections
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Configure remote backend server URLs for push/pull. Like `git remote`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent remote → List remotes
|
|
11
|
+
* gent remote add <name> <url> → Add a remote (e.g. origin)
|
|
12
|
+
* gent remote remove <name> → Remove a remote
|
|
13
|
+
* gent remote set-url <name> <url> → Update remote URL
|
|
14
|
+
*
|
|
15
|
+
* STORAGE:
|
|
16
|
+
* Stored in .gent/config.json under "remotes" key:
|
|
17
|
+
* { "origin": { "url": "https://gent-api.onrender.com/api/repos/my-repo/" } }
|
|
18
|
+
*
|
|
19
|
+
* BACKEND EXPECTATIONS:
|
|
20
|
+
* The URL is the base endpoint for a repository resource:
|
|
21
|
+
* GET <url>/ → Repo metadata
|
|
22
|
+
* POST <url>/push/ → Push commits/objects
|
|
23
|
+
* GET <url>/pull/ → Pull commits/objects
|
|
24
|
+
* GET <url>/refs/ → List remote branch refs
|
|
25
|
+
*
|
|
26
|
+
* ============================================================================
|
|
4
27
|
*/
|
|
5
28
|
|
|
6
|
-
const chalk = require('chalk');
|
|
7
29
|
const path = require('path');
|
|
8
|
-
const
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
const { pathExists } = require('../utils/fileSystem');
|
|
12
|
-
const repoService = require('../services/repo-service');
|
|
13
|
-
const authStorage = require('../utils/auth-storage');
|
|
30
|
+
const chalk = require('chalk');
|
|
31
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
32
|
+
const { CONFIG_FILE } = require('../utils/constants');
|
|
14
33
|
|
|
15
34
|
/**
|
|
16
|
-
* Manage
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {Object} options - Command options
|
|
35
|
+
* Manage remotes
|
|
36
|
+
* @param {String} subcommand - add|remove|set-url (null = list)
|
|
37
|
+
* @param {Array} args
|
|
38
|
+
* @param {Object} options
|
|
21
39
|
*/
|
|
22
|
-
async function remote(
|
|
40
|
+
async function remote(subcommand, args, options) {
|
|
23
41
|
try {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!action || action === 'list' || options.verbose) {
|
|
36
|
-
const config = await getRemoteConfig(cwd);
|
|
37
|
-
const remotes = Object.keys(config.remotes || {});
|
|
38
|
-
|
|
39
|
-
if (remotes.length === 0) {
|
|
40
|
-
console.log(chalk.yellow('No remotes configured'));
|
|
41
|
-
console.log(chalk.gray('Add a remote with:'), chalk.cyan('gent remote add <name> <owner_id>/<repo_name>'));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
console.log(chalk.cyan('Configured remotes:\n'));
|
|
46
|
-
for (const remoteName of remotes) {
|
|
47
|
-
const remote = config.remotes[remoteName];
|
|
48
|
-
if (options.verbose) {
|
|
49
|
-
console.log(chalk.bold(remoteName));
|
|
50
|
-
console.log(chalk.gray(` Owner ID: ${remote.owner_id}`));
|
|
51
|
-
console.log(chalk.gray(` Repository: ${remote.repo_name}`));
|
|
52
|
-
console.log(chalk.gray(` URL: ${remote.owner_id}/${remote.repo_name}\n`));
|
|
53
|
-
} else {
|
|
54
|
-
console.log(`${remoteName}\t${remote.owner_id}/${remote.repo_name}`);
|
|
42
|
+
const gentPath = await getGentPath();
|
|
43
|
+
const configPath = path.join(gentPath, CONFIG_FILE);
|
|
44
|
+
const config = await readJSON(configPath);
|
|
45
|
+
config.remotes = config.remotes || {};
|
|
46
|
+
|
|
47
|
+
switch (subcommand) {
|
|
48
|
+
case 'add': {
|
|
49
|
+
const [name, url] = args || [];
|
|
50
|
+
if (!name || !url) {
|
|
51
|
+
console.error(chalk.red('Usage: gent remote add <name> <url>'));
|
|
52
|
+
return;
|
|
55
53
|
}
|
|
54
|
+
if (config.remotes[name]) {
|
|
55
|
+
console.error(chalk.red(`Remote '${name}' already exists`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
config.remotes[name] = { url };
|
|
59
|
+
await writeJSON(configPath, config);
|
|
60
|
+
console.log(chalk.green(`Added remote '${name}' → ${url}`));
|
|
61
|
+
break;
|
|
56
62
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
console.error(chalk.red('Error: Invalid remote URL format'));
|
|
72
|
-
console.log(chalk.yellow('Expected:'), chalk.cyan('<owner_id>/<repo_name>'));
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const ownerId = parseInt(parts[0]);
|
|
77
|
-
const repoName = parts[1];
|
|
78
|
-
|
|
79
|
-
if (isNaN(ownerId)) {
|
|
80
|
-
console.error(chalk.red('Error: Owner ID must be a number'));
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check authentication
|
|
85
|
-
const user = await authStorage.getUser();
|
|
86
|
-
if (!user) {
|
|
87
|
-
console.error(chalk.red('Error: You must be logged in to add a remote'));
|
|
88
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
89
|
-
process.exit(1);
|
|
63
|
+
case 'remove': {
|
|
64
|
+
const name = args && args[0];
|
|
65
|
+
if (!name) {
|
|
66
|
+
console.error(chalk.red('Usage: gent remote remove <name>'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!config.remotes[name]) {
|
|
70
|
+
console.error(chalk.red(`Remote '${name}' not found`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
delete config.remotes[name];
|
|
74
|
+
await writeJSON(configPath, config);
|
|
75
|
+
console.log(chalk.green(`Removed remote '${name}'`));
|
|
76
|
+
break;
|
|
90
77
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
78
|
+
case 'set-url': {
|
|
79
|
+
const [name, url] = args || [];
|
|
80
|
+
if (!name || !url) {
|
|
81
|
+
console.error(chalk.red('Usage: gent remote set-url <name> <url>'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!config.remotes[name]) {
|
|
85
|
+
console.error(chalk.red(`Remote '${name}' not found`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
config.remotes[name].url = url;
|
|
89
|
+
await writeJSON(configPath, config);
|
|
90
|
+
console.log(chalk.green(`Updated '${name}' → ${url}`));
|
|
91
|
+
break;
|
|
100
92
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
process.exit(1);
|
|
93
|
+
default: {
|
|
94
|
+
// List remotes
|
|
95
|
+
const names = Object.keys(config.remotes);
|
|
96
|
+
if (names.length === 0) {
|
|
97
|
+
console.log(chalk.gray('No remotes configured'));
|
|
98
|
+
console.log(chalk.yellow('Use "gent remote add origin <url>" to add one'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
const verbose = options.verbose ? chalk.gray(` → ${config.remotes[name].url}`) : '';
|
|
103
|
+
console.log(chalk.cyan(name) + verbose);
|
|
104
|
+
}
|
|
114
105
|
}
|
|
115
|
-
|
|
116
|
-
await removeRemote(name, cwd);
|
|
117
|
-
console.log(chalk.green(`✓ Remote '${name}' removed`));
|
|
118
|
-
return;
|
|
119
106
|
}
|
|
120
|
-
|
|
121
|
-
// Unknown action
|
|
122
|
-
console.error(chalk.red(`Error: Unknown action '${action}'`));
|
|
123
|
-
console.log(chalk.yellow('Available actions:'), chalk.cyan('add, remove, list'));
|
|
124
|
-
process.exit(1);
|
|
125
|
-
|
|
126
107
|
} catch (error) {
|
|
127
|
-
|
|
128
|
-
|
|
108
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
109
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
110
|
+
} else {
|
|
111
|
+
console.error(chalk.red('Error:'), error.message);
|
|
112
|
+
}
|
|
129
113
|
process.exit(1);
|
|
130
114
|
}
|
|
131
115
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Reset Command - Unstage files or reset HEAD to a previous commit
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Undo staging (soft) or move branch pointer back (hard). Like `git reset`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent reset <file...> → Unstage specific file(s) (keep working tree)
|
|
11
|
+
* gent reset → Unstage all files
|
|
12
|
+
* gent reset --hard <hash> → Move HEAD to commit, discard changes
|
|
13
|
+
* gent reset --soft <hash> → Move HEAD to commit, keep staging
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* Soft: removes entries from staging.entries matching given paths.
|
|
17
|
+
* Hard: resets commits.json branch pointer + restores working tree blobs.
|
|
18
|
+
*
|
|
19
|
+
* BACKEND EXPECTATIONS:
|
|
20
|
+
* POST /api/repos/:id/reset/ { mode, targetHash }
|
|
21
|
+
* Backend should update remote HEAD and prune unreachable commits.
|
|
22
|
+
*
|
|
23
|
+
* ============================================================================
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs').promises;
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const chalk = require('chalk');
|
|
29
|
+
const ora = require('ora');
|
|
30
|
+
const { getGentPath, readJSON, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
31
|
+
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
32
|
+
const { readBlobAsString } = require('../utils/hash-engine');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reset staging or HEAD
|
|
36
|
+
* @param {Array} files - Files to unstage (empty = all)
|
|
37
|
+
* @param {Object} options
|
|
38
|
+
*/
|
|
39
|
+
async function reset(files, options) {
|
|
40
|
+
try {
|
|
41
|
+
const gentPath = await getGentPath();
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
|
|
44
|
+
if (options.hard || options.soft) {
|
|
45
|
+
await resetHead(gentPath, cwd, files, options);
|
|
46
|
+
} else {
|
|
47
|
+
await unstageFiles(gentPath, files);
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
51
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
52
|
+
} else {
|
|
53
|
+
console.error(chalk.red('Error:'), error.message);
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Unstage files from staging area
|
|
61
|
+
*/
|
|
62
|
+
async function unstageFiles(gentPath, files) {
|
|
63
|
+
const stagingPath = path.join(gentPath, STAGING_FILE);
|
|
64
|
+
const staging = await readJSON(stagingPath);
|
|
65
|
+
|
|
66
|
+
if (!staging.entries || staging.entries.length === 0) {
|
|
67
|
+
console.log(chalk.yellow('Nothing to unstage'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let removed = 0;
|
|
72
|
+
|
|
73
|
+
if (!files || files.length === 0) {
|
|
74
|
+
removed = staging.entries.length;
|
|
75
|
+
staging.entries = [];
|
|
76
|
+
staging.files = [];
|
|
77
|
+
} else {
|
|
78
|
+
const removeSet = new Set(files);
|
|
79
|
+
const before = staging.entries.length;
|
|
80
|
+
staging.entries = staging.entries.filter(e => !removeSet.has(e.path));
|
|
81
|
+
staging.files = staging.entries.map(e => e.path);
|
|
82
|
+
removed = before - staging.entries.length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await writeJSON(stagingPath, staging);
|
|
86
|
+
console.log(chalk.green(`Unstaged ${removed} file(s)`));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reset HEAD to specific commit
|
|
91
|
+
*/
|
|
92
|
+
async function resetHead(gentPath, cwd, args, options) {
|
|
93
|
+
const targetHash = args && args.length > 0 ? args[0] : null;
|
|
94
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
95
|
+
const currentBranch = repository.currentBranch;
|
|
96
|
+
const commits = repository.commits || [];
|
|
97
|
+
|
|
98
|
+
if (!targetHash) {
|
|
99
|
+
console.error(chalk.red('Provide a commit hash to reset to'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find target commit (support short hashes)
|
|
104
|
+
const target = commits.find(c =>
|
|
105
|
+
c.hash === targetHash || c.hash.startsWith(targetHash)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!target) {
|
|
109
|
+
console.error(chalk.red(`Commit '${targetHash}' not found`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const spinner = ora(`Resetting to ${target.hash.substring(0, 7)}...`).start();
|
|
114
|
+
|
|
115
|
+
// Move branch pointer
|
|
116
|
+
repository.branches[currentBranch] = target.hash;
|
|
117
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
118
|
+
|
|
119
|
+
if (options.hard) {
|
|
120
|
+
// Restore working tree from target commit
|
|
121
|
+
const tree = target.tree || (target.files || []).map(f => ({
|
|
122
|
+
name: f.path || f.name, hash: f.hash
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
for (const entry of tree) {
|
|
126
|
+
try {
|
|
127
|
+
const content = await readBlobAsString(gentPath, entry.hash);
|
|
128
|
+
const fullPath = path.join(cwd, entry.name || entry.path);
|
|
129
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
130
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
131
|
+
} catch {
|
|
132
|
+
// Blob may not exist for legacy commits
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Clear staging
|
|
137
|
+
const stagingPath = path.join(gentPath, STAGING_FILE);
|
|
138
|
+
await writeJSON(stagingPath, { entries: [], files: [] });
|
|
139
|
+
|
|
140
|
+
spinner.succeed(chalk.green(`HEAD is now at ${target.hash.substring(0, 7)} (hard reset)`));
|
|
141
|
+
console.log(chalk.gray(` ${target.message}`));
|
|
142
|
+
} else {
|
|
143
|
+
// Soft reset: keep staging
|
|
144
|
+
spinner.succeed(chalk.green(`HEAD is now at ${target.hash.substring(0, 7)} (soft reset)`));
|
|
145
|
+
console.log(chalk.gray(` Staging area preserved. ${target.message}`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = reset;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Rm Command - Remove files from working tree and staging
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Remove files from tracking and optionally from disk. Like `git rm`.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* gent rm <file...> → Remove file(s) from tracking + disk
|
|
11
|
+
* gent rm --cached <file...> → Remove from staging only (keep on disk)
|
|
12
|
+
*
|
|
13
|
+
* ALGORITHM:
|
|
14
|
+
* Adds a "deleted" entry to staging so commit records the removal.
|
|
15
|
+
* Without --cached, also deletes the file from the filesystem.
|
|
16
|
+
*
|
|
17
|
+
* BACKEND EXPECTATIONS:
|
|
18
|
+
* Deletion is recorded as absence in the commit tree. Backend removes
|
|
19
|
+
* file entry from tree when processing the push.
|
|
20
|
+
*
|
|
21
|
+
* ============================================================================
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs').promises;
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const chalk = require('chalk');
|
|
27
|
+
const { getGentPath, readJSON, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
28
|
+
const { STAGING_FILE } = require('../utils/constants');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Remove files
|
|
32
|
+
* @param {Array} files
|
|
33
|
+
* @param {Object} options
|
|
34
|
+
*/
|
|
35
|
+
async function rm(files, options) {
|
|
36
|
+
try {
|
|
37
|
+
const gentPath = await getGentPath();
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const stagingPath = path.join(gentPath, STAGING_FILE);
|
|
40
|
+
const staging = await readJSON(stagingPath);
|
|
41
|
+
|
|
42
|
+
const entries = staging.entries || [];
|
|
43
|
+
const entryMap = new Map(entries.map(e => [e.path, e]));
|
|
44
|
+
let removed = 0;
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const relPath = path.relative(cwd, path.resolve(cwd, file));
|
|
48
|
+
|
|
49
|
+
// Stage deletion
|
|
50
|
+
entryMap.set(relPath, {
|
|
51
|
+
path: relPath,
|
|
52
|
+
hash: null,
|
|
53
|
+
status: 'deleted',
|
|
54
|
+
binary: false,
|
|
55
|
+
stats: { insertions: 0, deletions: 0 }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Delete from disk unless --cached
|
|
59
|
+
if (!options.cached) {
|
|
60
|
+
const fullPath = path.join(cwd, relPath);
|
|
61
|
+
if (await pathExists(fullPath)) {
|
|
62
|
+
await fs.unlink(fullPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
removed++;
|
|
67
|
+
console.log(chalk.red(` rm ${relPath}`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
staging.entries = Array.from(entryMap.values());
|
|
71
|
+
staging.files = staging.entries.filter(e => e.status !== 'deleted').map(e => e.path);
|
|
72
|
+
await writeJSON(stagingPath, staging);
|
|
73
|
+
|
|
74
|
+
console.log(chalk.green(`\nRemoved ${removed} file(s)`));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
77
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
78
|
+
} else {
|
|
79
|
+
console.error(chalk.red('Error:'), error.message);
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = rm;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Show Command - Show details of a commit, tag, or tree
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Display detailed information about a specific commit including author,
|
|
8
|
+
* message, tree hash, parent, and full diff. Like `git show`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent show → Show HEAD commit details with diff
|
|
12
|
+
* gent show <hash> → Show specific commit
|
|
13
|
+
* gent show <tag> → Show commit referenced by tag
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* Retrieves commit from commits.json, reads blob content from object store,
|
|
17
|
+
* computes diff against parent commit's tree, and displays unified diff.
|
|
18
|
+
*
|
|
19
|
+
* BACKEND EXPECTATIONS:
|
|
20
|
+
* GET /api/repos/:id/commits/:hash/ — returns commit object with tree
|
|
21
|
+
*
|
|
22
|
+
* ============================================================================
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const chalk = require('chalk');
|
|
27
|
+
const { getGentPath, readJSON } = require('../utils/fileSystem');
|
|
28
|
+
const { COMMITS_FILE } = require('../utils/constants');
|
|
29
|
+
const { readBlobAsString } = require('../utils/hash-engine');
|
|
30
|
+
const { formatUnifiedDiff } = require('../utils/diff-engine');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Show commit details
|
|
34
|
+
* @param {String} ref - Commit hash, tag name, or empty for HEAD
|
|
35
|
+
* @param {Object} options
|
|
36
|
+
*/
|
|
37
|
+
async function show(ref, options) {
|
|
38
|
+
try {
|
|
39
|
+
const gentPath = await getGentPath();
|
|
40
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
41
|
+
const commits = repository.commits || [];
|
|
42
|
+
const tags = repository.tags || {};
|
|
43
|
+
|
|
44
|
+
let targetHash = ref;
|
|
45
|
+
|
|
46
|
+
// Default to HEAD
|
|
47
|
+
if (!targetHash) {
|
|
48
|
+
targetHash = repository.branches[repository.currentBranch];
|
|
49
|
+
if (!targetHash) {
|
|
50
|
+
console.log(chalk.yellow('No commits yet'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Resolve tag name → hash
|
|
56
|
+
if (tags[targetHash]) {
|
|
57
|
+
targetHash = tags[targetHash].hash;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find commit (support short hashes)
|
|
61
|
+
const commit = commits.find(c =>
|
|
62
|
+
c.hash === targetHash || c.hash.startsWith(targetHash)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!commit) {
|
|
66
|
+
console.error(chalk.red(`Commit '${ref || 'HEAD'}' not found`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Display commit header
|
|
71
|
+
console.log(chalk.yellow(`commit ${commit.hash}`));
|
|
72
|
+
if (commit.mergeParent) {
|
|
73
|
+
console.log(chalk.gray(`Merge: ${commit.parent?.substring(0, 7)} ${commit.mergeParent.substring(0, 7)}`));
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk.white(`Author: ${commit.author.name} <${commit.author.email}>`));
|
|
76
|
+
console.log(chalk.white(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
|
|
77
|
+
if (commit.treeHash) {
|
|
78
|
+
console.log(chalk.gray(`Tree: ${commit.treeHash.substring(0, 7)}`));
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(chalk.white(` ${commit.message}`));
|
|
82
|
+
console.log('');
|
|
83
|
+
|
|
84
|
+
if (commit.stats) {
|
|
85
|
+
console.log(chalk.gray(` ${commit.stats.filesChanged} file(s), `) +
|
|
86
|
+
chalk.green(`+${commit.stats.insertions}`) + ' ' +
|
|
87
|
+
chalk.red(`-${commit.stats.deletions}`));
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Show diff against parent
|
|
92
|
+
if (!options.noPatch) {
|
|
93
|
+
await showCommitDiff(gentPath, commits, commit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
98
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
99
|
+
} else {
|
|
100
|
+
console.error(chalk.red('Error:'), error.message);
|
|
101
|
+
}
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Display diff between commit and its parent
|
|
108
|
+
*/
|
|
109
|
+
async function showCommitDiff(gentPath, commits, commit) {
|
|
110
|
+
const parentCommit = commit.parent
|
|
111
|
+
? commits.find(c => c.hash === commit.parent)
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
const currentTree = commit.tree || (commit.files || []).map(f => ({
|
|
115
|
+
name: f.path || f.name, hash: f.hash
|
|
116
|
+
}));
|
|
117
|
+
const parentTree = parentCommit
|
|
118
|
+
? (parentCommit.tree || (parentCommit.files || []).map(f => ({
|
|
119
|
+
name: f.path || f.name, hash: f.hash
|
|
120
|
+
})))
|
|
121
|
+
: [];
|
|
122
|
+
|
|
123
|
+
const parentMap = new Map(parentTree.map(f => [f.name || f.path, f.hash]));
|
|
124
|
+
const currentMap = new Map(currentTree.map(f => [f.name || f.path, f.hash]));
|
|
125
|
+
|
|
126
|
+
// Files changed
|
|
127
|
+
const allPaths = new Set([...parentMap.keys(), ...currentMap.keys()]);
|
|
128
|
+
|
|
129
|
+
for (const filePath of allPaths) {
|
|
130
|
+
const oldHash = parentMap.get(filePath);
|
|
131
|
+
const newHash = currentMap.get(filePath);
|
|
132
|
+
|
|
133
|
+
if (oldHash === newHash) continue;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const oldContent = oldHash ? await readBlobAsString(gentPath, oldHash) : '';
|
|
137
|
+
const newContent = newHash ? await readBlobAsString(gentPath, newHash) : '';
|
|
138
|
+
|
|
139
|
+
const unified = formatUnifiedDiff(filePath, oldContent, newContent);
|
|
140
|
+
if (unified) {
|
|
141
|
+
printColorizedDiff(unified);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Blob missing for legacy commits
|
|
145
|
+
if (!oldHash && newHash) {
|
|
146
|
+
console.log(chalk.green(`+ new file: ${filePath}`));
|
|
147
|
+
} else if (oldHash && !newHash) {
|
|
148
|
+
console.log(chalk.red(`- deleted: ${filePath}`));
|
|
149
|
+
} else {
|
|
150
|
+
console.log(chalk.yellow(`~ modified: ${filePath} (blob not available)`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function printColorizedDiff(unifiedDiff) {
|
|
157
|
+
for (const line of unifiedDiff.split('\n')) {
|
|
158
|
+
if (line.startsWith('---') || line.startsWith('+++')) console.log(chalk.bold(line));
|
|
159
|
+
else if (line.startsWith('@@')) console.log(chalk.cyan(line));
|
|
160
|
+
else if (line.startsWith('+')) console.log(chalk.green(line));
|
|
161
|
+
else if (line.startsWith('-')) console.log(chalk.red(line));
|
|
162
|
+
else console.log(line);
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = show;
|