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/push.js
CHANGED
|
@@ -1,113 +1,206 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Push Command - Upload local commits and objects to remote
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Send local commits, blobs, and tree objects to the backend API server.
|
|
8
|
+
* Like `git push`.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* gent push → Push current branch to origin
|
|
12
|
+
* gent push <remote> <branch> → Push specific branch to remote
|
|
13
|
+
* gent push --force → Force push (overwrite remote)
|
|
14
|
+
*
|
|
15
|
+
* ALGORITHM:
|
|
16
|
+
* 1. Read local commits since last known remote HEAD
|
|
17
|
+
* 2. Collect all blob objects referenced by those commits
|
|
18
|
+
* 3. POST packfile (commits + blobs + trees) to remote /push/ endpoint
|
|
19
|
+
* 4. Remote updates branch pointer
|
|
20
|
+
*
|
|
21
|
+
* DATA FORMAT SENT TO BACKEND:
|
|
22
|
+
* POST /api/repos/:id/push/
|
|
23
|
+
* {
|
|
24
|
+
* branch: "main",
|
|
25
|
+
* force: false,
|
|
26
|
+
* commits: [ { hash, message, author, timestamp, parent, treeHash, tree, files, stats } ],
|
|
27
|
+
* objects: [ { hash, type: "blob", data: "<base64>" } ],
|
|
28
|
+
* tags: { "v1.0": { hash, message, ... } }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* BACKEND EXPECTATIONS:
|
|
32
|
+
* - Validate auth (JWT Bearer token)
|
|
33
|
+
* - Verify fast-forward (reject non-ff unless force=true)
|
|
34
|
+
* - Store blob objects in backend object store
|
|
35
|
+
* - Append commits to branch history
|
|
36
|
+
* - Update branch refs
|
|
37
|
+
* - Return { success, ref, hash }
|
|
38
|
+
*
|
|
39
|
+
* ============================================================================
|
|
4
40
|
*/
|
|
5
41
|
|
|
6
|
-
const
|
|
42
|
+
const fs = require('fs').promises;
|
|
7
43
|
const path = require('path');
|
|
44
|
+
const chalk = require('chalk');
|
|
8
45
|
const ora = require('ora');
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
const
|
|
46
|
+
const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
47
|
+
const { COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
|
|
48
|
+
const apiClient = require('../utils/api-client');
|
|
12
49
|
const authStorage = require('../utils/auth-storage');
|
|
13
|
-
const
|
|
50
|
+
const { readBlob, objectExists } = require('../utils/hash-engine');
|
|
14
51
|
|
|
15
52
|
/**
|
|
16
|
-
* Push
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {Object} options
|
|
53
|
+
* Push commits to remote
|
|
54
|
+
* @param {String} remoteName
|
|
55
|
+
* @param {String} branchName
|
|
56
|
+
* @param {Object} options
|
|
20
57
|
*/
|
|
21
58
|
async function push(remoteName, branchName, options) {
|
|
22
|
-
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
const gentPath = path.join(cwd, GENT_DIR);
|
|
25
|
-
|
|
26
|
-
// Check if in a gent repository
|
|
27
|
-
if (!(await pathExists(gentPath))) {
|
|
28
|
-
console.error(chalk.red('Error: Not a gent repository'));
|
|
29
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent init'), chalk.yellow('to initialize a repository'));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check authentication
|
|
34
|
-
const user = await authStorage.getUser();
|
|
35
|
-
if (!user) {
|
|
36
|
-
console.error(chalk.red('Error: You must be logged in to push'));
|
|
37
|
-
console.log(chalk.yellow('Run'), chalk.cyan('gent login'), chalk.yellow('to authenticate'));
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
59
|
+
const spinner = ora('Preparing push...').start();
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(chalk.yellow('Add a remote with:'), chalk.cyan('gent remote add origin <owner_id>/<repo_name>'));
|
|
49
|
-
console.log(chalk.yellow('Or list remotes with:'), chalk.cyan('gent remote'));
|
|
50
|
-
process.exit(1);
|
|
61
|
+
try {
|
|
62
|
+
// Auth check
|
|
63
|
+
const isAuth = await authStorage.isAuthenticated();
|
|
64
|
+
if (!isAuth) {
|
|
65
|
+
spinner.fail(chalk.red('Not authenticated'));
|
|
66
|
+
console.log(chalk.yellow('Run "gent login" first'));
|
|
67
|
+
return;
|
|
51
68
|
}
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
const gentPath = await getGentPath();
|
|
71
|
+
const configPath = path.join(gentPath, CONFIG_FILE);
|
|
72
|
+
const config = await readJSON(configPath);
|
|
73
|
+
config.remotes = config.remotes || {};
|
|
74
|
+
|
|
75
|
+
// Resolve remote
|
|
76
|
+
const remote = remoteName || 'origin';
|
|
77
|
+
const remoteConfig = config.remotes[remote];
|
|
78
|
+
if (!remoteConfig) {
|
|
79
|
+
spinner.fail(chalk.red(`Remote '${remote}' not found`));
|
|
80
|
+
console.log(chalk.yellow('Use "gent remote add origin <url>" to configure'));
|
|
81
|
+
return;
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
84
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
85
|
+
const branch = branchName || repository.currentBranch;
|
|
86
|
+
const localHead = repository.branches[branch];
|
|
65
87
|
|
|
66
|
-
if (!
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
if (!localHead) {
|
|
89
|
+
spinner.fail(chalk.red(`Branch '${branch}' has no commits`));
|
|
90
|
+
return;
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const commit = commits.find(c => (c.sha || c.hash) === currentSha);
|
|
77
|
-
if (!commit) break;
|
|
78
|
-
|
|
79
|
-
commitsToPush.unshift(commit); // Add to beginning to maintain order
|
|
80
|
-
const parents = Array.isArray(commit.parent) ? commit.parent : (commit.parent ? [commit.parent] : []);
|
|
81
|
-
currentSha = parents.length > 0 ? parents[0] : null;
|
|
82
|
-
}
|
|
93
|
+
// Determine which commits to push (since last pushed ref)
|
|
94
|
+
config.remoteRefs = config.remoteRefs || {};
|
|
95
|
+
const lastPushed = config.remoteRefs[`${remote}/${branch}`] || null;
|
|
96
|
+
const commits = repository.commits || [];
|
|
97
|
+
const commitsToPush = getCommitsSince(commits, localHead, lastPushed);
|
|
83
98
|
|
|
84
99
|
if (commitsToPush.length === 0) {
|
|
85
|
-
|
|
100
|
+
spinner.succeed(chalk.green('Everything up-to-date'));
|
|
86
101
|
return;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
|
|
104
|
+
spinner.text = `Pushing ${commitsToPush.length} commit(s) to ${remote}/${branch}...`;
|
|
90
105
|
|
|
91
|
-
//
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
// Collect all blob hashes from commits to push
|
|
107
|
+
const blobHashes = new Set();
|
|
108
|
+
for (const commit of commitsToPush) {
|
|
109
|
+
const tree = commit.tree || commit.files || [];
|
|
110
|
+
for (const entry of tree) {
|
|
111
|
+
const h = entry.hash;
|
|
112
|
+
if (h) blobHashes.add(h);
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
//
|
|
102
|
-
|
|
116
|
+
// Read blob data for transfer
|
|
117
|
+
const objects = [];
|
|
118
|
+
for (const hash of blobHashes) {
|
|
119
|
+
try {
|
|
120
|
+
if (await objectExists(gentPath, hash)) {
|
|
121
|
+
const data = await readBlob(gentPath, hash);
|
|
122
|
+
objects.push({
|
|
123
|
+
hash,
|
|
124
|
+
type: 'blob',
|
|
125
|
+
data: data.toString('base64')
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Skip missing blobs
|
|
130
|
+
}
|
|
131
|
+
}
|
|
103
132
|
|
|
104
|
-
|
|
133
|
+
// Build push payload
|
|
134
|
+
const payload = {
|
|
135
|
+
branch,
|
|
136
|
+
force: !!options.force,
|
|
137
|
+
commits: commitsToPush.map(c => ({
|
|
138
|
+
hash: c.hash,
|
|
139
|
+
message: c.message,
|
|
140
|
+
author: c.author,
|
|
141
|
+
timestamp: c.timestamp,
|
|
142
|
+
parent: c.parent,
|
|
143
|
+
mergeParent: c.mergeParent || null,
|
|
144
|
+
treeHash: c.treeHash || null,
|
|
145
|
+
tree: c.tree || null,
|
|
146
|
+
files: c.files || [],
|
|
147
|
+
stats: c.stats || {}
|
|
148
|
+
})),
|
|
149
|
+
objects,
|
|
150
|
+
tags: repository.tags || {}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Send to backend
|
|
154
|
+
const response = await apiClient.post(
|
|
155
|
+
`${remoteConfig.url}/push/`,
|
|
156
|
+
payload
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Update remote ref
|
|
160
|
+
config.remoteRefs[`${remote}/${branch}`] = localHead;
|
|
161
|
+
await writeJSON(configPath, config);
|
|
162
|
+
|
|
163
|
+
spinner.succeed(chalk.green(`Pushed ${commitsToPush.length} commit(s) to ${remote}/${branch}`));
|
|
164
|
+
console.log(chalk.gray(` ${localHead.substring(0, 7)} → ${remote}/${branch}`));
|
|
165
|
+
console.log(chalk.gray(` ${objects.length} object(s) transferred`));
|
|
105
166
|
|
|
106
167
|
} catch (error) {
|
|
107
|
-
|
|
108
|
-
|
|
168
|
+
spinner.fail(chalk.red('Push failed'));
|
|
169
|
+
|
|
170
|
+
if (error.response?.status === 409) {
|
|
171
|
+
console.error(chalk.red('Remote has changes you don\'t have locally'));
|
|
172
|
+
console.log(chalk.yellow('Run "gent pull" first, or use "gent push --force"'));
|
|
173
|
+
} else if (error.response?.status === 401) {
|
|
174
|
+
console.error(chalk.red('Authentication failed — run "gent login"'));
|
|
175
|
+
} else if (error.response?.data?.message) {
|
|
176
|
+
console.error(chalk.red(error.response.data.message));
|
|
177
|
+
} else {
|
|
178
|
+
console.error(chalk.red('Error:'), error.message);
|
|
179
|
+
}
|
|
109
180
|
process.exit(1);
|
|
110
181
|
}
|
|
111
182
|
}
|
|
112
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Get commits from tip back to (but excluding) stopHash.
|
|
186
|
+
* @param {Array} allCommits
|
|
187
|
+
* @param {String} tipHash
|
|
188
|
+
* @param {String|null} stopHash
|
|
189
|
+
* @returns {Array}
|
|
190
|
+
*/
|
|
191
|
+
function getCommitsSince(allCommits, tipHash, stopHash) {
|
|
192
|
+
const commitMap = new Map(allCommits.map(c => [c.hash, c]));
|
|
193
|
+
const result = [];
|
|
194
|
+
let current = tipHash;
|
|
195
|
+
|
|
196
|
+
while (current && current !== stopHash) {
|
|
197
|
+
const commit = commitMap.get(current);
|
|
198
|
+
if (!commit) break;
|
|
199
|
+
result.push(commit);
|
|
200
|
+
current = commit.parent;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result.reverse(); // oldest first
|
|
204
|
+
}
|
|
205
|
+
|
|
113
206
|
module.exports = push;
|
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;
|