isomorphic-jj 1.4.3 → 1.8.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/CHANGELOG.md +428 -0
- package/README.md +138 -29
- package/bin/isojj.js +273 -102
- package/dist/index.cjs +3401 -1683
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +3401 -1683
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -7
- package/src/api/repository.js +1301 -745
- package/src/backend/isomorphic-git-backend.js +241 -138
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +8 -4
- package/src/core/background-ops.js +58 -41
- package/src/core/bookmark-store.js +12 -8
- package/src/core/change-graph.js +26 -29
- package/src/core/conflict-model.js +66 -26
- package/src/core/jj-checkout.js +8 -4
- package/src/core/jj-operation-store.js +21 -14
- package/src/core/jj-tree-state.js +16 -11
- package/src/core/jj-view-store.js +14 -7
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1114 -485
- package/src/core/storage-manager.js +73 -30
- package/src/core/tag-store.js +42 -12
- package/src/core/user-config.js +3 -1
- package/src/core/working-copy.js +125 -9
- package/src/core/workspace-manager.js +2 -2
- package/src/drivers/built-in-drivers.js +66 -33
- package/src/types.d.ts +67 -1
- package/src/utils/atomic-write.js +44 -0
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/mutex.js +63 -0
- package/src/utils/validation.js +6 -10
package/bin/isojj.js
CHANGED
|
@@ -8,10 +8,34 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createJJ } from '../src/api/repository.js';
|
|
11
|
-
import
|
|
11
|
+
import nodeFs from 'fs';
|
|
12
|
+
import nodePath from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
// Flags that are always boolean and must never consume the next token as a
|
|
16
|
+
// value — otherwise `isojj describe --dryRun "fix typo"` would swallow the
|
|
17
|
+
// commit message as --dryRun's value instead of leaving it as a positional.
|
|
18
|
+
const BOOLEAN_FLAGS = new Set([
|
|
19
|
+
'json',
|
|
20
|
+
'dryRun',
|
|
21
|
+
'force',
|
|
22
|
+
'interactive',
|
|
23
|
+
'includeResolved',
|
|
24
|
+
'noTags',
|
|
25
|
+
'allowNew',
|
|
26
|
+
'preserveSnapshot',
|
|
27
|
+
'nameOnly',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** Coerce a raw flag-value string to a real boolean when it looks like one. */
|
|
31
|
+
function coerceBoolean(value) {
|
|
32
|
+
if (value === 'true') return true;
|
|
33
|
+
if (value === 'false') return false;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
12
36
|
|
|
13
37
|
// Parse command line arguments
|
|
14
|
-
function parseArgs(argv) {
|
|
38
|
+
export function parseArgs(argv) {
|
|
15
39
|
const args = argv.slice(2);
|
|
16
40
|
const command = args[0];
|
|
17
41
|
const opts = {};
|
|
@@ -21,24 +45,34 @@ function parseArgs(argv) {
|
|
|
21
45
|
|
|
22
46
|
if (arg.startsWith('--')) {
|
|
23
47
|
const key = arg.slice(2);
|
|
24
|
-
const nextArg = args[i + 1];
|
|
25
48
|
|
|
26
49
|
// Handle --flag and --key=value
|
|
27
50
|
if (arg.includes('=')) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
opts[
|
|
51
|
+
const eq = arg.indexOf('=');
|
|
52
|
+
const k = arg.slice(2, eq);
|
|
53
|
+
const v = arg.slice(eq + 1);
|
|
54
|
+
opts[k] = coerceBoolean(v);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
59
|
+
opts[key] = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nextArg = args[i + 1];
|
|
64
|
+
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
65
|
+
opts[key] = coerceBoolean(nextArg);
|
|
32
66
|
i++;
|
|
33
67
|
} else {
|
|
34
68
|
opts[key] = true;
|
|
35
69
|
}
|
|
36
|
-
} else if (arg.startsWith('-')) {
|
|
70
|
+
} else if (arg.startsWith('-') && arg !== '-') {
|
|
37
71
|
// Handle short flags like -m "message"
|
|
38
72
|
const key = arg.slice(1);
|
|
39
73
|
const nextArg = args[i + 1];
|
|
40
74
|
|
|
41
|
-
if (nextArg && !nextArg.startsWith('-')) {
|
|
75
|
+
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
42
76
|
opts[key] = nextArg;
|
|
43
77
|
i++;
|
|
44
78
|
} else {
|
|
@@ -55,7 +89,7 @@ function parseArgs(argv) {
|
|
|
55
89
|
}
|
|
56
90
|
|
|
57
91
|
// Format output for different commands
|
|
58
|
-
function formatOutput(command, result) {
|
|
92
|
+
export function formatOutput(command, result, log = console.log) {
|
|
59
93
|
if (result === undefined || result === null) {
|
|
60
94
|
return;
|
|
61
95
|
}
|
|
@@ -63,16 +97,16 @@ function formatOutput(command, result) {
|
|
|
63
97
|
// Handle special output formatting for common commands
|
|
64
98
|
if (command === 'log') {
|
|
65
99
|
if (Array.isArray(result)) {
|
|
66
|
-
|
|
100
|
+
log('\nChange History:\n');
|
|
67
101
|
result.forEach((change, idx) => {
|
|
68
102
|
const shortId = change.changeId.slice(0, 12);
|
|
69
103
|
const desc = change.description || '(no description)';
|
|
70
104
|
const author = change.author?.name || 'unknown';
|
|
71
105
|
const timestamp = change.timestamp ? new Date(change.timestamp).toLocaleString() : '';
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (idx < result.length - 1)
|
|
107
|
+
log(` ${shortId} ${desc}`);
|
|
108
|
+
log(` ${author} ${timestamp}`);
|
|
109
|
+
if (idx < result.length - 1) log('');
|
|
76
110
|
});
|
|
77
111
|
return;
|
|
78
112
|
}
|
|
@@ -80,29 +114,31 @@ function formatOutput(command, result) {
|
|
|
80
114
|
|
|
81
115
|
if (command === 'status') {
|
|
82
116
|
if (result.workingCopy) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
log(`Working copy change: ${result.workingCopy.changeId.slice(0, 12)}`);
|
|
118
|
+
log(
|
|
119
|
+
`Parent change(s): ${(result.workingCopy.parents || []).map((p) => p.slice(0, 12)).join(', ')}`
|
|
120
|
+
);
|
|
121
|
+
log('');
|
|
86
122
|
}
|
|
87
123
|
|
|
88
124
|
if (result.modified?.length > 0) {
|
|
89
|
-
|
|
90
|
-
result.modified.forEach(file =>
|
|
125
|
+
log('Modified files:');
|
|
126
|
+
result.modified.forEach((file) => log(` M ${file}`));
|
|
91
127
|
}
|
|
92
128
|
|
|
93
129
|
if (result.added?.length > 0) {
|
|
94
|
-
|
|
95
|
-
result.added.forEach(file =>
|
|
130
|
+
log('Added files:');
|
|
131
|
+
result.added.forEach((file) => log(` A ${file}`));
|
|
96
132
|
}
|
|
97
133
|
|
|
98
134
|
if (result.removed?.length > 0) {
|
|
99
|
-
|
|
100
|
-
result.removed.forEach(file =>
|
|
135
|
+
log('Removed files:');
|
|
136
|
+
result.removed.forEach((file) => log(` D ${file}`));
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
if (result.conflicts?.length > 0) {
|
|
104
|
-
|
|
105
|
-
result.conflicts.forEach(file =>
|
|
140
|
+
log('Conflicted files:');
|
|
141
|
+
result.conflicts.forEach((file) => log(` C ${file}`));
|
|
106
142
|
}
|
|
107
143
|
|
|
108
144
|
return;
|
|
@@ -110,82 +146,201 @@ function formatOutput(command, result) {
|
|
|
110
146
|
|
|
111
147
|
if (command === 'diff') {
|
|
112
148
|
if (typeof result === 'string') {
|
|
113
|
-
|
|
149
|
+
log(result);
|
|
114
150
|
return;
|
|
115
151
|
}
|
|
116
152
|
}
|
|
117
153
|
|
|
118
154
|
// Default: pretty-print JSON
|
|
119
|
-
|
|
155
|
+
log(JSON.stringify(result, null, 2));
|
|
120
156
|
}
|
|
121
157
|
|
|
122
|
-
|
|
123
|
-
async function main() {
|
|
124
|
-
try {
|
|
125
|
-
const { command, opts } = parseArgs(process.argv);
|
|
126
|
-
|
|
127
|
-
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
128
|
-
console.log(`
|
|
158
|
+
export const HELP_TEXT = `
|
|
129
159
|
isojj - CLI for isomorphic-jj
|
|
130
160
|
|
|
131
161
|
Usage: isojj <command> [options]
|
|
132
162
|
|
|
133
|
-
|
|
134
|
-
init
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
describe -m <message>
|
|
138
|
-
new -m <message>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
Change management:
|
|
164
|
+
init Initialize a new repository
|
|
165
|
+
status Show working copy status
|
|
166
|
+
log [<revset> | --revset <e>] Show change history
|
|
167
|
+
describe -m <message> Set change description
|
|
168
|
+
new -m <message> Create a new change
|
|
169
|
+
commit -m <message> Describe + start a new change
|
|
170
|
+
diff [--revision <id>] Show changes
|
|
171
|
+
abandon [--revision <id>] Abandon a change
|
|
172
|
+
edit --revision <id> Edit an existing change
|
|
173
|
+
|
|
174
|
+
History editing:
|
|
175
|
+
squash / split / rebase / duplicate / parallelize
|
|
176
|
+
absorb [--dryRun] Absorb working-copy changes into ancestors
|
|
177
|
+
revert --revision <id> Create a change that undoes another (was: backout)
|
|
178
|
+
undo / redo Undo or redo the last operation
|
|
179
|
+
|
|
180
|
+
Files:
|
|
181
|
+
file.list [--revision <id>] List tracked files
|
|
182
|
+
file.show <path> Show file contents
|
|
183
|
+
file.search --pattern <re> [--nameOnly] Search file contents (jj file search)
|
|
184
|
+
file.annotate --path <p> Blame-style annotation
|
|
185
|
+
|
|
186
|
+
Bookmarks & tags:
|
|
187
|
+
bookmark.set --name <n> --to <id>
|
|
188
|
+
bookmark.advance --name <n> --to <id> Move a bookmark forward only
|
|
142
189
|
bookmark.list
|
|
190
|
+
tag.set --name <n> --changeId <id> Create or move a tag
|
|
191
|
+
tag.list
|
|
192
|
+
tag.track --name <n> [--remote <r>] Track a remote tag (jj v0.44)
|
|
193
|
+
tag.untrack --name <n> Stop tracking a remote tag
|
|
143
194
|
|
|
144
|
-
|
|
195
|
+
Signing:
|
|
196
|
+
sign [--revision <id>] [--backend ssh --key <k>]
|
|
197
|
+
unsign [--revision <id>]
|
|
145
198
|
|
|
199
|
+
Bisect:
|
|
146
200
|
bisect.start --good <id> --bad <id>
|
|
147
|
-
bisect.good
|
|
148
|
-
bisect.bad
|
|
149
|
-
bisect.status
|
|
150
|
-
bisect.reset
|
|
201
|
+
bisect.good / bisect.bad / bisect.status / bisect.reset
|
|
151
202
|
|
|
152
203
|
Options:
|
|
153
|
-
--dir <path> Repository directory (default: current directory)
|
|
204
|
+
--dir <path> Repository directory (default: current directory).
|
|
205
|
+
Like git/jj, the actual repo root is found by
|
|
206
|
+
searching this directory and its parents for .jj.
|
|
154
207
|
--json Output raw JSON (skip formatting)
|
|
208
|
+
-m, --message <msg> Message / description
|
|
209
|
+
-r, --revision <id> Target revision
|
|
210
|
+
|
|
211
|
+
Any additional --flags are passed straight through to the API method, so every
|
|
212
|
+
isomorphic-jj method is reachable (e.g. isojj file.search --pattern TODO).
|
|
155
213
|
|
|
156
214
|
Examples:
|
|
157
215
|
isojj init
|
|
158
|
-
isojj log
|
|
216
|
+
isojj log 'author(alice)'
|
|
159
217
|
isojj describe -m "Fix bug"
|
|
160
|
-
isojj
|
|
161
|
-
isojj
|
|
162
|
-
isojj
|
|
163
|
-
|
|
164
|
-
|
|
218
|
+
isojj revert --revision abc123
|
|
219
|
+
isojj file.search --pattern "TODO"
|
|
220
|
+
isojj bookmark.advance --name main --to def456
|
|
221
|
+
isojj tag.set --name v1.0 --changeId abc123
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find the repository root by searching `startDir` and its parents for a
|
|
226
|
+
* `.jj` entry, mirroring how `git`/`jj` locate a repo from any subdirectory.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} startDir - Directory to start searching from
|
|
229
|
+
* @param {any} fs - Filesystem implementation (must expose fs.promises.stat)
|
|
230
|
+
* @returns {Promise<string|null>} The directory containing `.jj`, or null
|
|
231
|
+
*/
|
|
232
|
+
export async function findRepoRoot(startDir, fs) {
|
|
233
|
+
let current = nodePath.resolve(startDir);
|
|
234
|
+
|
|
235
|
+
while (true) {
|
|
236
|
+
try {
|
|
237
|
+
await fs.promises.stat(nodePath.join(current, '.jj'));
|
|
238
|
+
return current;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error.code !== 'ENOENT') throw error;
|
|
165
241
|
}
|
|
166
242
|
|
|
167
|
-
|
|
168
|
-
|
|
243
|
+
const parent = nodePath.dirname(current);
|
|
244
|
+
if (parent === current) {
|
|
245
|
+
return null; // Reached the filesystem root without finding .jj
|
|
246
|
+
}
|
|
247
|
+
current = parent;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
169
250
|
|
|
170
|
-
|
|
171
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Dynamically load isomorphic-git (an optional peer dependency) so the CLI
|
|
253
|
+
* can create real Git-backed repositories when it's installed, while still
|
|
254
|
+
* working (storage-only "mock" mode) when it isn't.
|
|
255
|
+
*
|
|
256
|
+
* @param {(specifier: string) => Promise<any>} [importer] - Override for the
|
|
257
|
+
* dynamic `import()` call (used by tests to simulate a missing package
|
|
258
|
+
* without needing real module-resolution mocking).
|
|
259
|
+
* @returns {Promise<{git: any, http: any}>} git/http instances, or undefined
|
|
260
|
+
* for both if isomorphic-git isn't installed.
|
|
261
|
+
*/
|
|
262
|
+
export async function loadGitBackend(importer = (specifier) => import(specifier)) {
|
|
263
|
+
try {
|
|
264
|
+
const [{ default: git }, { default: http }] = await Promise.all([
|
|
265
|
+
importer('isomorphic-git'),
|
|
266
|
+
importer('isomorphic-git/http/node'),
|
|
267
|
+
]);
|
|
268
|
+
return { git, http };
|
|
269
|
+
} catch {
|
|
270
|
+
return { git: undefined, http: undefined };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Run the CLI for a given argv, returning a process exit code instead of
|
|
276
|
+
* calling process.exit() directly, so this is testable in-process.
|
|
277
|
+
*
|
|
278
|
+
* @param {string[]} argv - Full argv (e.g. process.argv)
|
|
279
|
+
* @param {object} [io]
|
|
280
|
+
* @param {any} [io.fs] - Filesystem implementation (defaults to Node's fs)
|
|
281
|
+
* @param {string} [io.cwd] - Working directory (defaults to process.cwd())
|
|
282
|
+
* @param {(msg: string) => void} [io.stdout] - stdout writer
|
|
283
|
+
* @param {(msg: string) => void} [io.stderr] - stderr writer
|
|
284
|
+
* @param {() => Promise<{git: any, http: any}>} [io.loadGitBackend] - override
|
|
285
|
+
* for loading the optional isomorphic-git backend (used by tests)
|
|
286
|
+
* @returns {Promise<number>} Exit code
|
|
287
|
+
*/
|
|
288
|
+
export async function run(argv, io = {}) {
|
|
289
|
+
const fs = io.fs || nodeFs;
|
|
290
|
+
const cwd = io.cwd || process.cwd();
|
|
291
|
+
const stdout = io.stdout || console.log;
|
|
292
|
+
const stderr = io.stderr || console.error;
|
|
293
|
+
const resolveGitBackend = io.loadGitBackend || loadGitBackend;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const { command, opts } = parseArgs(argv);
|
|
297
|
+
|
|
298
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
299
|
+
// Always read package.json via real Node fs — it's outside the
|
|
300
|
+
// repository, so the (possibly injected/mock) repo `fs` doesn't apply.
|
|
301
|
+
const pkg = JSON.parse(
|
|
302
|
+
nodeFs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
303
|
+
);
|
|
304
|
+
stdout(`isojj ${pkg.version}`);
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
309
|
+
stdout(HELP_TEXT);
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Resolve the repository directory. Like git/jj, search the given
|
|
314
|
+
// directory (or cwd) and its parents for the .jj marker so the CLI works
|
|
315
|
+
// from any subdirectory of a repo, not just its root.
|
|
316
|
+
const startDir = opts.dir || cwd;
|
|
317
|
+
let dir = startDir;
|
|
172
318
|
|
|
173
|
-
// If not initializing, load existing repository state
|
|
174
319
|
if (command !== 'init') {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await jj.graph.load();
|
|
181
|
-
await jj.workingCopy.load();
|
|
182
|
-
await jj.bookmarks.load();
|
|
183
|
-
} catch (error) {
|
|
184
|
-
if (error.code === 'ENOENT') {
|
|
185
|
-
throw new Error(`Not a jj repository (or any parent up to mount point /)\nStopped at: ${dir}\nRun 'isojj init' to initialize a repository`);
|
|
186
|
-
}
|
|
187
|
-
throw error;
|
|
320
|
+
const root = await findRepoRoot(startDir, fs);
|
|
321
|
+
if (!root) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Not a jj repository (or any parent up to mount point /)\nStopped at: ${startDir}\nRun 'isojj init' to initialize a repository`
|
|
324
|
+
);
|
|
188
325
|
}
|
|
326
|
+
dir = root;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Wire up the real isomorphic-git backend when it's installed (it's an
|
|
330
|
+
// optional peer dependency), so `isojj init` creates a real .git
|
|
331
|
+
// directory instead of silently falling back to the storage-only "mock"
|
|
332
|
+
// backend. Attempted for every command (not just init) so an already
|
|
333
|
+
// Git-backed repo is loaded consistently.
|
|
334
|
+
const { git, http } = await resolveGitBackend();
|
|
335
|
+
|
|
336
|
+
// Initialize repository instance
|
|
337
|
+
const jj = await createJJ({ fs, dir, git, http });
|
|
338
|
+
|
|
339
|
+
if (command !== 'init') {
|
|
340
|
+
// Load existing repository state
|
|
341
|
+
await jj.graph.load();
|
|
342
|
+
await jj.workingCopy.load();
|
|
343
|
+
await jj.bookmarks.load();
|
|
189
344
|
}
|
|
190
345
|
|
|
191
346
|
// Handle nested commands (e.g., bisect.start, bookmark.set)
|
|
@@ -193,8 +348,8 @@ Examples:
|
|
|
193
348
|
let method = jj;
|
|
194
349
|
|
|
195
350
|
for (const part of parts) {
|
|
196
|
-
method = method[part];
|
|
197
|
-
if (
|
|
351
|
+
method = method?.[part];
|
|
352
|
+
if (method === undefined || method === null) {
|
|
198
353
|
throw new Error(`Unknown command: ${command}`);
|
|
199
354
|
}
|
|
200
355
|
}
|
|
@@ -203,29 +358,36 @@ Examples:
|
|
|
203
358
|
throw new Error(`${command} is not a valid command`);
|
|
204
359
|
}
|
|
205
360
|
|
|
206
|
-
// Map
|
|
207
|
-
|
|
361
|
+
// Map CLI flags to API options. Every parsed flag is passed through so new
|
|
362
|
+
// commands work without special-casing; a few common shorthands are then
|
|
363
|
+
// normalized on top.
|
|
364
|
+
const { dir: _dir, json: _json, _positional, ...passthrough } = opts;
|
|
365
|
+
const apiOpts = { ...passthrough };
|
|
208
366
|
|
|
209
|
-
// Common mappings
|
|
367
|
+
// Common shorthand mappings
|
|
210
368
|
if (opts.m || opts.message) apiOpts.message = opts.m || opts.message;
|
|
211
|
-
if (opts.
|
|
212
|
-
if (opts.revset) apiOpts.revset = opts.revset;
|
|
213
|
-
if (opts.name) apiOpts.name = opts.name;
|
|
214
|
-
if (opts.path) apiOpts.path = opts.path;
|
|
215
|
-
if (opts.good) apiOpts.good = opts.good;
|
|
216
|
-
if (opts.bad) apiOpts.bad = opts.bad;
|
|
369
|
+
if (opts.r || opts.revision) apiOpts.revision = opts.r || opts.revision;
|
|
217
370
|
if (opts.dryRun) apiOpts.dryRun = true;
|
|
218
|
-
if (opts.parents) {
|
|
219
|
-
apiOpts.parents = opts.parents.includes(',')
|
|
220
|
-
? opts.parents.split(',')
|
|
221
|
-
: [opts.parents];
|
|
371
|
+
if (typeof opts.parents === 'string') {
|
|
372
|
+
apiOpts.parents = opts.parents.includes(',') ? opts.parents.split(',') : [opts.parents];
|
|
222
373
|
}
|
|
223
374
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
375
|
+
// Positional arguments: map the first positional to the most natural
|
|
376
|
+
// parameter for common file/query commands.
|
|
377
|
+
if (_positional?.length > 0) {
|
|
378
|
+
const first = _positional[0];
|
|
379
|
+
if (
|
|
380
|
+
(command === 'read' || command === 'remove' || command.startsWith('file.')) &&
|
|
381
|
+
!apiOpts.path
|
|
382
|
+
) {
|
|
383
|
+
apiOpts.path = first;
|
|
384
|
+
} else if (command === 'log' && !apiOpts.revset) {
|
|
385
|
+
apiOpts.revset = first;
|
|
386
|
+
} else if (
|
|
387
|
+
(command === 'describe' || command === 'new' || command === 'commit') &&
|
|
388
|
+
!apiOpts.message
|
|
389
|
+
) {
|
|
390
|
+
apiOpts.message = first;
|
|
229
391
|
}
|
|
230
392
|
}
|
|
231
393
|
|
|
@@ -234,29 +396,38 @@ Examples:
|
|
|
234
396
|
|
|
235
397
|
// Format and output result
|
|
236
398
|
if (opts.json) {
|
|
237
|
-
|
|
399
|
+
stdout(JSON.stringify(result, null, 2));
|
|
238
400
|
} else {
|
|
239
|
-
formatOutput(command, result);
|
|
401
|
+
formatOutput(command, result, stdout);
|
|
240
402
|
}
|
|
241
403
|
|
|
242
|
-
|
|
404
|
+
return 0;
|
|
243
405
|
} catch (error) {
|
|
244
|
-
|
|
406
|
+
stderr(`Error: ${error.message}`);
|
|
245
407
|
|
|
246
408
|
if (error.code) {
|
|
247
|
-
|
|
409
|
+
stderr(`Code: ${error.code}`);
|
|
248
410
|
}
|
|
249
411
|
|
|
250
412
|
if (error.context?.suggestion) {
|
|
251
|
-
|
|
413
|
+
stderr(`Suggestion: ${error.context.suggestion}`);
|
|
252
414
|
}
|
|
253
415
|
|
|
254
416
|
if (process.env.DEBUG) {
|
|
255
|
-
|
|
417
|
+
stderr(error.stack);
|
|
256
418
|
}
|
|
257
419
|
|
|
258
|
-
|
|
420
|
+
return 1;
|
|
259
421
|
}
|
|
260
422
|
}
|
|
261
423
|
|
|
262
|
-
|
|
424
|
+
// Only run (and exit the process) when this file is executed directly, not
|
|
425
|
+
// when it's imported by tests. Comparing `import.meta.url` (a file:// URL,
|
|
426
|
+
// e.g. "file:///C:/repo/bin/isojj.js" on Windows) against the raw
|
|
427
|
+
// `process.argv[1]` string (a native path, e.g. "C:\repo\bin\isojj.js") is
|
|
428
|
+
// never equal on Windows — convert the URL to a native path first.
|
|
429
|
+
const isMainModule =
|
|
430
|
+
!!process.argv[1] && fileURLToPath(import.meta.url) === nodePath.resolve(process.argv[1]);
|
|
431
|
+
if (isMainModule) {
|
|
432
|
+
run(process.argv).then((code) => process.exit(code));
|
|
433
|
+
}
|