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.
@@ -1,113 +1,206 @@
1
1
  /**
2
- * Push Command - Push local commits to cloud repository
3
- * Uploads commits, trees, and blobs to the cloud
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 chalk = require('chalk');
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 { pathExists, readJSON } = require('../utils/fileSystem');
10
- const { GENT_DIR, COMMITS_FILE } = require('../utils/constants');
11
- const { getRemote, syncCommitsToCloud } = require('../utils/cloud-sync');
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 repoService = require('../services/repo-service');
50
+ const { readBlob, objectExists } = require('../utils/hash-engine');
14
51
 
15
52
  /**
16
- * Push local commits to cloud
17
- * @param {string} remoteName - Remote name (default: origin)
18
- * @param {string} branchName - Branch name (optional)
19
- * @param {Object} options - Command 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
- try {
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
- // Default to 'origin' if no remote specified
42
- remoteName = remoteName || 'origin';
43
-
44
- // Get remote configuration
45
- const remote = await getRemote(remoteName, cwd);
46
- if (!remote) {
47
- console.error(chalk.red(`Error: Remote '${remoteName}' not found`));
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
- // Get current branch if not specified
54
- const commitsData = await readJSON(path.join(gentPath, COMMITS_FILE));
55
- branchName = branchName || commitsData.currentBranch;
56
-
57
- if (!branchName) {
58
- console.error(chalk.red('Error: No branch specified and no current branch found'));
59
- process.exit(1);
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
- // Get commits for the branch
63
- const commits = commitsData.commits || [];
64
- const branchCommitSha = commitsData.branches[branchName];
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 (!branchCommitSha) {
67
- console.error(chalk.red(`Error: Branch '${branchName}' not found`));
68
- process.exit(1);
88
+ if (!localHead) {
89
+ spinner.fail(chalk.red(`Branch '${branch}' has no commits`));
90
+ return;
69
91
  }
70
92
 
71
- // Get commits to push (find all commits in the branch)
72
- const commitsToPush = [];
73
- let currentSha = branchCommitSha;
74
-
75
- while (currentSha) {
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
- console.log(chalk.yellow('Nothing to push'));
100
+ spinner.succeed(chalk.green('Everything up-to-date'));
86
101
  return;
87
102
  }
88
103
 
89
- console.log(chalk.cyan(`Pushing ${commitsToPush.length} commit(s) to ${remoteName}/${branchName}...`));
104
+ spinner.text = `Pushing ${commitsToPush.length} commit(s) to ${remote}/${branch}...`;
90
105
 
91
- // Verify repository exists
92
- const spinner = ora('Verifying remote repository...').start();
93
- try {
94
- await repoService.getRepository(remote.owner_id, remote.repo_name);
95
- spinner.succeed('Remote repository verified');
96
- } catch (error) {
97
- spinner.fail('Remote repository not found or access denied');
98
- throw error;
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
- // Push commits
102
- await syncCommitsToCloud(commitsToPush, remote.owner_id, remote.repo_name, branchName, cwd);
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
- console.log(chalk.green(`\n✓ Successfully pushed to ${remoteName}/${branchName}`));
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
- console.error(chalk.red('Failed to push'));
108
- console.error(chalk.red('Error:'), error.message);
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;
@@ -1,131 +1,115 @@
1
1
  /**
2
- * Remote Command - Manage remote repositories
3
- * Add, remove, and list remote repository configurations
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 ora = require('ora');
9
- const { getRemoteConfig, addRemote, removeRemote } = require('../utils/cloud-sync');
10
- const { GENT_DIR } = require('../utils/constants');
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 remote repositories
17
- * @param {string} action - Action to perform (add, remove, or list)
18
- * @param {string} name - Remote name
19
- * @param {string} url - Remote URL (owner_id/repo_name)
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(action, name, url, options) {
40
+ async function remote(subcommand, args, options) {
23
41
  try {
24
- const cwd = process.cwd();
25
- const gentPath = path.join(cwd, GENT_DIR);
26
-
27
- // Check if in a gent repository
28
- if (!(await pathExists(gentPath))) {
29
- console.error(chalk.red('Error: Not a gent repository'));
30
- console.log(chalk.yellow('Run'), chalk.cyan('gent init'), chalk.yellow('to initialize a repository'));
31
- process.exit(1);
32
- }
33
-
34
- // List remotes (default action)
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
- return;
58
- }
59
-
60
- // Add remote
61
- if (action === 'add') {
62
- if (!name || !url) {
63
- console.error(chalk.red('Error: Remote name and URL are required'));
64
- console.log(chalk.yellow('Usage:'), chalk.cyan('gent remote add <name> <owner_id>/<repo_name>'));
65
- process.exit(1);
66
- }
67
-
68
- // Parse owner_id/repo_name
69
- const parts = url.split('/');
70
- if (parts.length !== 2) {
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
- // Verify repository exists
93
- const spinner = ora('Verifying repository...').start();
94
- try {
95
- await repoService.getRepository(ownerId, repoName);
96
- spinner.succeed('Repository verified');
97
- } catch (error) {
98
- spinner.fail('Repository not found or access denied');
99
- throw error;
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
- // Add remote
103
- await addRemote(name, ownerId, repoName, cwd);
104
- console.log(chalk.green(`✓ Remote '${name}' added: ${ownerId}/${repoName}`));
105
- return;
106
- }
107
-
108
- // Remove remote
109
- if (action === 'remove' || action === 'rm') {
110
- if (!name) {
111
- console.error(chalk.red('Error: Remote name is required'));
112
- console.log(chalk.yellow('Usage:'), chalk.cyan('gent remote remove <name>'));
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
- console.error(chalk.red('Failed to manage remote'));
128
- console.error(chalk.red('Error:'), error.message);
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;