gent-cli 6.0.1 → 8.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/QUICKSTART.md +116 -85
- package/README.md +60 -3
- package/package.json +6 -4
- package/src/commands/ai.js +82 -0
- package/src/commands/ask.js +121 -0
- package/src/commands/branch.js +3 -0
- package/src/commands/changelog.js +121 -0
- package/src/commands/checkout.js +5 -0
- package/src/commands/clone.js +6 -3
- package/src/commands/commit.js +65 -2
- package/src/commands/config.js +147 -0
- package/src/commands/docs.js +141 -0
- package/src/commands/doctor.js +169 -0
- package/src/commands/explain.js +145 -0
- package/src/commands/log.js +51 -1
- package/src/commands/merge.js +4 -0
- package/src/commands/pull.js +4 -3
- package/src/commands/reset.js +10 -0
- package/src/commands/resolve.js +280 -0
- package/src/commands/review.js +157 -0
- package/src/commands/search.js +77 -0
- package/src/commands/setup.js +201 -0
- package/src/commands/share.js +63 -0
- package/src/commands/summary.js +176 -0
- package/src/commands/template.js +135 -0
- package/src/commands/undo.js +115 -0
- package/src/commands/web.js +72 -0
- package/src/index.js +189 -12
- package/src/utils/ai-service.js +219 -0
- package/src/utils/api-client.js +28 -6
- package/src/utils/constants.js +3 -1
- package/src/utils/diff-engine.js +68 -2
- package/src/utils/env-loader.js +61 -0
- package/src/utils/journal.js +215 -0
- package/src/utils/merge-engine.js +340 -140
- package/src/utils/user-config.js +225 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor Command - Health check for the gent CLI.
|
|
3
|
+
*
|
|
4
|
+
* gent doctor → run all checks
|
|
5
|
+
* gent doctor --ai → also ping Anthropic with a 1-token request
|
|
6
|
+
*
|
|
7
|
+
* Each row prints PASS / WARN / FAIL plus a hint on how to fix the issue.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const axios = require('axios');
|
|
14
|
+
const packageJson = require('../../package.json');
|
|
15
|
+
const { GENT_DIR } = require('../utils/constants');
|
|
16
|
+
const userConfig = require('../utils/user-config');
|
|
17
|
+
const apiClient = require('../utils/api-client');
|
|
18
|
+
const authStorage = require('../utils/auth-storage');
|
|
19
|
+
const ai = require('../utils/ai-service');
|
|
20
|
+
|
|
21
|
+
const MIN_NODE_MAJOR = 18;
|
|
22
|
+
|
|
23
|
+
async function doctor(options = {}) {
|
|
24
|
+
console.log(chalk.bold.cyan('\nGent CLI health check\n'));
|
|
25
|
+
|
|
26
|
+
const checks = [];
|
|
27
|
+
|
|
28
|
+
checks.push(await checkNodeVersion());
|
|
29
|
+
checks.push(await checkCliVersion());
|
|
30
|
+
checks.push(await checkRepo());
|
|
31
|
+
checks.push(await checkAuth());
|
|
32
|
+
checks.push(await checkApi());
|
|
33
|
+
checks.push(await checkAiKey(!!options.ai));
|
|
34
|
+
|
|
35
|
+
for (const c of checks) {
|
|
36
|
+
printCheck(c);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const failed = checks.filter(c => c.status === 'fail').length;
|
|
40
|
+
const warned = checks.filter(c => c.status === 'warn').length;
|
|
41
|
+
const passed = checks.filter(c => c.status === 'pass').length;
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.green(` ${passed} pass`) + ' ' +
|
|
46
|
+
chalk.yellow(`${warned} warn`) + ' ' +
|
|
47
|
+
chalk.red(`${failed} fail`)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (failed > 0) process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printCheck(c) {
|
|
54
|
+
const badge = c.status === 'pass' ? chalk.green('✓ PASS')
|
|
55
|
+
: c.status === 'warn' ? chalk.yellow('! WARN')
|
|
56
|
+
: chalk.red('✗ FAIL');
|
|
57
|
+
console.log(` ${badge} ${chalk.bold(c.name)} ${chalk.gray(`— ${c.detail}`)}`);
|
|
58
|
+
if (c.hint) console.log(chalk.gray(` hint: ${c.hint}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function checkNodeVersion() {
|
|
62
|
+
const v = process.versions.node;
|
|
63
|
+
const major = parseInt(v.split('.')[0], 10);
|
|
64
|
+
if (major >= MIN_NODE_MAJOR) {
|
|
65
|
+
return { name: 'Node version', status: 'pass', detail: `v${v}` };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
name: 'Node version',
|
|
69
|
+
status: 'fail',
|
|
70
|
+
detail: `v${v} (need ≥${MIN_NODE_MAJOR})`,
|
|
71
|
+
hint: `Install Node ${MIN_NODE_MAJOR}+ — e.g. via nvm.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function checkCliVersion() {
|
|
76
|
+
return {
|
|
77
|
+
name: 'Gent CLI',
|
|
78
|
+
status: 'pass',
|
|
79
|
+
detail: `v${packageJson.version}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function checkRepo() {
|
|
84
|
+
const gentPath = path.join(process.cwd(), GENT_DIR);
|
|
85
|
+
try {
|
|
86
|
+
const stat = await fs.stat(gentPath);
|
|
87
|
+
if (!stat.isDirectory()) throw new Error('not a directory');
|
|
88
|
+
return { name: 'Repository (.gent)', status: 'pass', detail: gentPath };
|
|
89
|
+
} catch {
|
|
90
|
+
return {
|
|
91
|
+
name: 'Repository (.gent)',
|
|
92
|
+
status: 'warn',
|
|
93
|
+
detail: 'not a gent repo (this directory)',
|
|
94
|
+
hint: 'Run `gent init` to start a repo here, or cd into one.',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function checkAuth() {
|
|
100
|
+
const isAuth = await authStorage.isAuthenticated();
|
|
101
|
+
if (!isAuth) {
|
|
102
|
+
return {
|
|
103
|
+
name: 'Authentication',
|
|
104
|
+
status: 'warn',
|
|
105
|
+
detail: 'not logged in',
|
|
106
|
+
hint: 'Run `gent login` or `gent register`.',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const user = await authStorage.getUser();
|
|
110
|
+
const who = user ? `${user.email}` : 'unknown user';
|
|
111
|
+
return { name: 'Authentication', status: 'pass', detail: who };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function checkApi() {
|
|
115
|
+
const { value: baseUrl, source } = await userConfig.getResolved('api.base_url');
|
|
116
|
+
try {
|
|
117
|
+
await axios.get(baseUrl, { timeout: 8000, validateStatus: () => true });
|
|
118
|
+
return {
|
|
119
|
+
name: 'Backend reachable',
|
|
120
|
+
status: 'pass',
|
|
121
|
+
detail: `${baseUrl} [${source}]`,
|
|
122
|
+
};
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return {
|
|
125
|
+
name: 'Backend reachable',
|
|
126
|
+
status: 'fail',
|
|
127
|
+
detail: `${baseUrl} → ${err.code || err.message}`,
|
|
128
|
+
hint: 'If running a local server: `gent config set api.base_url http://localhost:8000`.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function checkAiKey(probe) {
|
|
134
|
+
const { value: key, source } = await ai.resolveKey();
|
|
135
|
+
if (!key) {
|
|
136
|
+
return {
|
|
137
|
+
name: 'AI key',
|
|
138
|
+
status: 'warn',
|
|
139
|
+
detail: 'not configured (AI features will be skipped, not failed)',
|
|
140
|
+
hint: 'Run `gent config set ai.api_key <key>` or set ANTHROPIC_API_KEY.',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!probe) {
|
|
145
|
+
return {
|
|
146
|
+
name: 'AI key',
|
|
147
|
+
status: 'pass',
|
|
148
|
+
detail: `present [${source}], model: ${await ai.resolveModel()} (use --ai to live-test)`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await ai.complete({ prompt: 'ping', maxTokens: 4 });
|
|
154
|
+
return {
|
|
155
|
+
name: 'AI key',
|
|
156
|
+
status: 'pass',
|
|
157
|
+
detail: `verified — model ${await ai.resolveModel()} responded`,
|
|
158
|
+
};
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
name: 'AI key',
|
|
162
|
+
status: 'fail',
|
|
163
|
+
detail: err.message,
|
|
164
|
+
hint: 'Re-check the key (`gent config set ai.api_key`) or model (`gent config set ai.model`).',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = doctor;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Explain Command - Plain-language summary of a commit or pending changes
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Turn a diff into a human explanation. With an API key set this uses Claude;
|
|
8
|
+
* without one it still prints the unified diff plus a hint, so the command is
|
|
9
|
+
* useful either way.
|
|
10
|
+
*
|
|
11
|
+
* USAGE:
|
|
12
|
+
* gent explain → explain the latest commit (HEAD)
|
|
13
|
+
* gent explain <ref> → explain a specific commit (full or short hash)
|
|
14
|
+
* gent explain --staged → explain the currently staged changes
|
|
15
|
+
*
|
|
16
|
+
* ============================================================================
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const chalk = require('chalk');
|
|
21
|
+
const ora = require('ora');
|
|
22
|
+
const { getGentPath, readJSON } = require('../utils/fileSystem');
|
|
23
|
+
const { COMMITS_FILE, STAGING_FILE } = require('../utils/constants');
|
|
24
|
+
const { readBlobAsString, treeToMap } = require('../utils/hash-engine');
|
|
25
|
+
const { formatUnifiedDiff } = require('../utils/diff-engine');
|
|
26
|
+
const ai = require('../utils/ai-service');
|
|
27
|
+
|
|
28
|
+
const MAX_DIFF_CHARS = 12000;
|
|
29
|
+
|
|
30
|
+
function treeEntriesOf(commit) {
|
|
31
|
+
if (!commit) return [];
|
|
32
|
+
if (Array.isArray(commit.tree)) return commit.tree;
|
|
33
|
+
return (commit.files || []).map(f => ({ name: f.path || f.name, hash: f.hash }));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a unified-diff text between two trees (old → new).
|
|
38
|
+
*/
|
|
39
|
+
async function diffTrees(gentPath, oldEntries, newEntries) {
|
|
40
|
+
const oldMap = treeToMap(oldEntries);
|
|
41
|
+
const newMap = treeToMap(newEntries);
|
|
42
|
+
const files = new Set([...oldMap.keys(), ...newMap.keys()]);
|
|
43
|
+
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const oh = oldMap.get(file);
|
|
47
|
+
const nh = newMap.get(file);
|
|
48
|
+
if (oh === nh) continue;
|
|
49
|
+
let oldText = '', newText = '';
|
|
50
|
+
try { if (oh) oldText = await readBlobAsString(gentPath, oh); } catch { /* binary/legacy */ }
|
|
51
|
+
try { if (nh) newText = await readBlobAsString(gentPath, nh); } catch { /* binary/legacy */ }
|
|
52
|
+
const d = formatUnifiedDiff(file, oldText, newText);
|
|
53
|
+
if (d) parts.push(d);
|
|
54
|
+
}
|
|
55
|
+
return parts.join('\n\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function explain(ref, options = {}) {
|
|
59
|
+
try {
|
|
60
|
+
const gentPath = await getGentPath();
|
|
61
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
62
|
+
const commits = repository.commits || [];
|
|
63
|
+
const commitMap = new Map(commits.map(c => [c.hash, c]));
|
|
64
|
+
|
|
65
|
+
let title;
|
|
66
|
+
let diffText;
|
|
67
|
+
|
|
68
|
+
if (options.staged) {
|
|
69
|
+
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
70
|
+
const entries = staging.entries || [];
|
|
71
|
+
if (entries.length === 0) {
|
|
72
|
+
console.log(chalk.yellow('No staged changes to explain'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const headHash = repository.branches[repository.currentBranch];
|
|
76
|
+
const headTree = treeEntriesOf(commitMap.get(headHash));
|
|
77
|
+
const stagedTree = entries
|
|
78
|
+
.filter(e => e.status !== 'deleted')
|
|
79
|
+
.map(e => ({ name: e.path, hash: e.hash }));
|
|
80
|
+
// Overlay staged entries on the HEAD tree
|
|
81
|
+
const overlay = new Map(headTree.map(e => [e.name, e.hash]));
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
if (e.status === 'deleted') overlay.delete(e.path);
|
|
84
|
+
else overlay.set(e.path, e.hash);
|
|
85
|
+
}
|
|
86
|
+
title = 'Staged changes';
|
|
87
|
+
diffText = await diffTrees(
|
|
88
|
+
gentPath,
|
|
89
|
+
headTree,
|
|
90
|
+
[...overlay].map(([name, hash]) => ({ name, hash }))
|
|
91
|
+
);
|
|
92
|
+
void stagedTree;
|
|
93
|
+
} else {
|
|
94
|
+
const targetHash = ref
|
|
95
|
+
? (commits.find(c => c.hash === ref || c.hash.startsWith(ref)) || {}).hash
|
|
96
|
+
: repository.branches[repository.currentBranch];
|
|
97
|
+
const commit = targetHash ? commitMap.get(targetHash) : null;
|
|
98
|
+
if (!commit) {
|
|
99
|
+
console.log(chalk.yellow(ref ? `Commit '${ref}' not found` : 'No commits yet'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const parent = commit.parent ? commitMap.get(commit.parent) : null;
|
|
103
|
+
title = `Commit ${commit.hash.substring(0, 7)} — ${commit.message}`;
|
|
104
|
+
diffText = await diffTrees(gentPath, treeEntriesOf(parent), treeEntriesOf(commit));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!diffText) {
|
|
108
|
+
console.log(chalk.gray('No textual changes to explain.'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const trimmed = diffText.length > MAX_DIFF_CHARS
|
|
113
|
+
? diffText.slice(0, MAX_DIFF_CHARS) + '\n... (diff truncated)'
|
|
114
|
+
: diffText;
|
|
115
|
+
|
|
116
|
+
console.log(chalk.bold.cyan(`\n${title}\n`));
|
|
117
|
+
|
|
118
|
+
if (!ai.isEnabled()) {
|
|
119
|
+
console.log(trimmed);
|
|
120
|
+
console.log(chalk.gray(`\n${ai.disabledHint()}`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const spinner = ora(`Asking ${ai.getModel()} to explain...`).start();
|
|
125
|
+
try {
|
|
126
|
+
const explanation = await ai.explainChanges(trimmed);
|
|
127
|
+
spinner.stop();
|
|
128
|
+
console.log(explanation);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
spinner.fail(chalk.yellow('AI request failed — showing the raw diff instead'));
|
|
131
|
+
console.log(chalk.gray(`(${err.message})\n`));
|
|
132
|
+
console.log(trimmed);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
136
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
137
|
+
console.log(chalk.yellow('\nRun "gent init" to initialize a repository'));
|
|
138
|
+
} else {
|
|
139
|
+
console.error(chalk.red('Error:'), error.message);
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = explain;
|
package/src/commands/log.js
CHANGED
|
@@ -60,7 +60,9 @@ async function log(options) {
|
|
|
60
60
|
cur = c.parent;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (options.
|
|
63
|
+
if (options.graph) {
|
|
64
|
+
displayGraphLog(commits, headHash, repository.branches || {}, limit);
|
|
65
|
+
} else if (options.oneline) {
|
|
64
66
|
displayOnelineLog(ordered, headHash);
|
|
65
67
|
} else {
|
|
66
68
|
displayDetailedLog(ordered, headHash, currentBranch, options);
|
|
@@ -118,6 +120,54 @@ function displayDetailedLog(commits, currentCommitHash, currentBranch, options)
|
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Display a commit graph reachable from HEAD (parent + mergeParent edges),
|
|
125
|
+
* ordered newest-first, with branch/HEAD decorations and merge annotations.
|
|
126
|
+
* A simplified single-rail graph: merges are annotated rather than drawn as
|
|
127
|
+
* separate lanes, which keeps the output readable in a terminal.
|
|
128
|
+
*/
|
|
129
|
+
function displayGraphLog(allCommits, headHash, branches, limit) {
|
|
130
|
+
const map = new Map(allCommits.map(c => [c.hash, c]));
|
|
131
|
+
|
|
132
|
+
// Reachable set from HEAD via both edges.
|
|
133
|
+
const reachable = [];
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
const stack = [headHash];
|
|
136
|
+
while (stack.length) {
|
|
137
|
+
const h = stack.pop();
|
|
138
|
+
if (!h || seen.has(h)) continue;
|
|
139
|
+
const c = map.get(h);
|
|
140
|
+
if (!c) continue;
|
|
141
|
+
seen.add(h);
|
|
142
|
+
reachable.push(c);
|
|
143
|
+
if (c.parent) stack.push(c.parent);
|
|
144
|
+
if (c.mergeParent) stack.push(c.mergeParent);
|
|
145
|
+
}
|
|
146
|
+
reachable.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
147
|
+
const limited = reachable.slice(0, limit);
|
|
148
|
+
|
|
149
|
+
// Branch labels by commit hash.
|
|
150
|
+
const refs = new Map();
|
|
151
|
+
for (const [name, hash] of Object.entries(branches)) {
|
|
152
|
+
if (!hash) continue;
|
|
153
|
+
if (!refs.has(hash)) refs.set(hash, []);
|
|
154
|
+
refs.get(hash).push(name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(chalk.bold.cyan('\nCommit graph:\n'));
|
|
158
|
+
limited.forEach((c, i) => {
|
|
159
|
+
const headLabel = c.hash === headHash ? chalk.yellow.bold(' (HEAD)') : '';
|
|
160
|
+
const refLabel = refs.has(c.hash) ? chalk.green(` (${refs.get(c.hash).join(', ')})`) : '';
|
|
161
|
+
const time = chalk.gray(`(${formatDistanceToNow(new Date(c.timestamp), { addSuffix: true })})`);
|
|
162
|
+
console.log(`${chalk.yellow('*')} ${chalk.yellow(c.hash.substring(0, 7))}${headLabel}${refLabel} ${c.message} ${time}`);
|
|
163
|
+
if (c.mergeParent) {
|
|
164
|
+
console.log(chalk.gray(`|\\ merge: ${(c.parent || '').substring(0, 7)} + ${c.mergeParent.substring(0, 7)}`));
|
|
165
|
+
}
|
|
166
|
+
if (i < limited.length - 1) console.log(chalk.gray('|'));
|
|
167
|
+
});
|
|
168
|
+
console.log();
|
|
169
|
+
}
|
|
170
|
+
|
|
121
171
|
/**
|
|
122
172
|
* Display oneline commit log
|
|
123
173
|
*/
|
package/src/commands/merge.js
CHANGED
|
@@ -13,6 +13,7 @@ const { generateCommitHash } = require('../utils/helpers');
|
|
|
13
13
|
const authStorage = require('../utils/auth-storage');
|
|
14
14
|
const { findMergeBase, mergeTreeEntries, autoMerge } = require('../utils/merge-engine');
|
|
15
15
|
const { storeTree, readBlobAsString, storeBlob } = require('../utils/hash-engine');
|
|
16
|
+
const journal = require('../utils/journal');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Merge a branch into the current branch
|
|
@@ -66,6 +67,7 @@ async function merge(sourceBranch, options) {
|
|
|
66
67
|
// Fast-forward: current branch is merge base → just move pointer
|
|
67
68
|
if (baseHash === oursHash) {
|
|
68
69
|
spinner.text = 'Fast-forward merge...';
|
|
70
|
+
await journal.recordOp(gentPath, 'merge', `fast-forward '${sourceBranch}' into ${currentBranch}`, { restoreTree: true });
|
|
69
71
|
repository.branches[currentBranch] = theirsHash;
|
|
70
72
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
71
73
|
|
|
@@ -165,6 +167,8 @@ async function merge(sourceBranch, options) {
|
|
|
165
167
|
}
|
|
166
168
|
};
|
|
167
169
|
|
|
170
|
+
await journal.recordOp(gentPath, 'merge', `merge '${sourceBranch}' into ${currentBranch}`);
|
|
171
|
+
|
|
168
172
|
repository.commits.push(mergeCommit);
|
|
169
173
|
repository.branches[currentBranch] = mergeCommit.hash;
|
|
170
174
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
package/src/commands/pull.js
CHANGED
|
@@ -30,7 +30,7 @@ const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
|
30
30
|
const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
|
|
31
31
|
const apiClient = require('../utils/api-client');
|
|
32
32
|
const authStorage = require('../utils/auth-storage');
|
|
33
|
-
const { storeBlob, objectExists, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
|
|
33
|
+
const { storeBlob, objectExists, readBlob, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
|
|
34
34
|
const { findMergeBase, mergeTreeEntries } = require('../utils/merge-engine');
|
|
35
35
|
const { generateCommitHash } = require('../utils/helpers');
|
|
36
36
|
|
|
@@ -311,10 +311,11 @@ async function checkoutTree(gentPath, cwd, previousTree, nextTree) {
|
|
|
311
311
|
const relPath = entry.name || entry.path;
|
|
312
312
|
if (!relPath || !entry.hash) continue;
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
// Write the raw Buffer so binary blobs round-trip byte-exact.
|
|
315
|
+
const buf = await readBlob(gentPath, entry.hash);
|
|
315
316
|
const fullPath = path.join(cwd, relPath);
|
|
316
317
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
317
|
-
await fs.writeFile(fullPath,
|
|
318
|
+
await fs.writeFile(fullPath, buf);
|
|
318
319
|
}
|
|
319
320
|
}
|
|
320
321
|
|
package/src/commands/reset.js
CHANGED
|
@@ -30,6 +30,7 @@ const ora = require('ora');
|
|
|
30
30
|
const { getGentPath, readJSON, writeJSON, pathExists } = require('../utils/fileSystem');
|
|
31
31
|
const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
|
|
32
32
|
const { readBlobAsString } = require('../utils/hash-engine');
|
|
33
|
+
const journal = require('../utils/journal');
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Reset staging or HEAD
|
|
@@ -112,6 +113,15 @@ async function resetHead(gentPath, cwd, args, options) {
|
|
|
112
113
|
|
|
113
114
|
const spinner = ora(`Resetting to ${target.hash.substring(0, 7)}...`).start();
|
|
114
115
|
|
|
116
|
+
// Journal pre-state. A hard reset discards working-tree content, so flag it
|
|
117
|
+
// for working-tree restore on undo.
|
|
118
|
+
await journal.recordOp(
|
|
119
|
+
gentPath,
|
|
120
|
+
'reset',
|
|
121
|
+
`${options.hard ? 'hard' : 'soft'} reset to ${target.hash.substring(0, 7)}`,
|
|
122
|
+
{ restoreTree: !!options.hard }
|
|
123
|
+
);
|
|
124
|
+
|
|
115
125
|
// Move branch pointer
|
|
116
126
|
repository.branches[currentBranch] = target.hash;
|
|
117
127
|
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|