isomorphic-jj 1.0.0 → 1.1.2
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/bin/isojj.js +262 -0
- package/dist/index.cjs +93 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +93 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/api/repository.js +55 -6
- package/src/core/tag-store.js +9 -2
- package/src/core/working-copy.js +3 -3
- package/src/utils/validation.js +26 -12
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isomorphic-jj",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "A pure-JavaScript library that brings Jujutsu (jj) version control semantics to Node.js and browsers",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"isojj": "./bin/isojj.js"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.cjs",
|
|
7
10
|
"module": "./dist/index.mjs",
|
|
8
11
|
"types": "./src/types.d.ts",
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
}
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
24
|
+
"bin",
|
|
21
25
|
"dist",
|
|
22
26
|
"src",
|
|
23
27
|
"README.md",
|
package/src/api/repository.js
CHANGED
|
@@ -821,8 +821,9 @@ export async function createJJ(options) {
|
|
|
821
821
|
async _moveChange(args) {
|
|
822
822
|
await graph.load();
|
|
823
823
|
|
|
824
|
-
|
|
825
|
-
const
|
|
824
|
+
// Support multiple parameter names for flexibility
|
|
825
|
+
const changeId = args.changeId || args.from || args.source;
|
|
826
|
+
const newParent = args.newParent || args.to || args.destination;
|
|
826
827
|
|
|
827
828
|
if (!changeId || typeof changeId !== 'string') {
|
|
828
829
|
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid changeId', {
|
|
@@ -1162,11 +1163,14 @@ export async function createJJ(options) {
|
|
|
1162
1163
|
/**
|
|
1163
1164
|
* Edit change metadata without modifying content
|
|
1164
1165
|
*
|
|
1165
|
-
* Allows updating author, committer
|
|
1166
|
+
* Allows updating author, committer, description, and optionally regenerating the change ID.
|
|
1166
1167
|
* The file content and snapshot remain unchanged.
|
|
1167
1168
|
*
|
|
1168
1169
|
* @param {Object} [args={}] - Arguments
|
|
1169
1170
|
* @param {string} [args.revision] - Change ID to edit (defaults to working copy @)
|
|
1171
|
+
* @param {string} [args.change] - Alias for revision
|
|
1172
|
+
* @param {string} [args.description] - Update the change description
|
|
1173
|
+
* @param {string} [args.message] - Alias for description
|
|
1170
1174
|
* @param {Object} [args.author] - Author information to update
|
|
1171
1175
|
* @param {string} [args.author.name] - Author name
|
|
1172
1176
|
* @param {string} [args.author.email] - Author email
|
|
@@ -1177,6 +1181,16 @@ export async function createJJ(options) {
|
|
|
1177
1181
|
* @returns {Promise<Object>} Updated change object
|
|
1178
1182
|
*/
|
|
1179
1183
|
async metaedit(args = {}) {
|
|
1184
|
+
// Normalize: accept 'change' as alias for 'revision'
|
|
1185
|
+
if (args.change && !args.revision) {
|
|
1186
|
+
args = { ...args, revision: args.change };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Normalize: accept 'message' as alias for 'description'
|
|
1190
|
+
if (args.message && !args.description) {
|
|
1191
|
+
args = { ...args, description: args.message };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1180
1194
|
await graph.load();
|
|
1181
1195
|
await workingCopy.load();
|
|
1182
1196
|
await userConfig.load();
|
|
@@ -1194,13 +1208,14 @@ export async function createJJ(options) {
|
|
|
1194
1208
|
const hasAuthor = args.author && (args.author.name || args.author.email);
|
|
1195
1209
|
const hasCommitter = args.committer && (args.committer.name || args.committer.email);
|
|
1196
1210
|
const hasResetChangeId = args.resetChangeId === true;
|
|
1211
|
+
const hasDescription = args.description !== undefined;
|
|
1197
1212
|
|
|
1198
|
-
if (!hasAuthor && !hasCommitter && !hasResetChangeId) {
|
|
1213
|
+
if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
|
|
1199
1214
|
throw new JJError(
|
|
1200
1215
|
'INVALID_ARGUMENT',
|
|
1201
1216
|
'No metadata provided to update',
|
|
1202
1217
|
{
|
|
1203
|
-
suggestion: 'Provide author, committer, or resetChangeId: true',
|
|
1218
|
+
suggestion: 'Provide author, committer, description, or resetChangeId: true',
|
|
1204
1219
|
}
|
|
1205
1220
|
);
|
|
1206
1221
|
}
|
|
@@ -1245,6 +1260,11 @@ export async function createJJ(options) {
|
|
|
1245
1260
|
change.committer.timestamp = new Date().toISOString();
|
|
1246
1261
|
}
|
|
1247
1262
|
|
|
1263
|
+
// Update description if provided
|
|
1264
|
+
if (args.description !== undefined) {
|
|
1265
|
+
change.description = args.description;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1248
1268
|
// Regenerate change ID if requested
|
|
1249
1269
|
if (args.resetChangeId) {
|
|
1250
1270
|
// For now, resetChangeId is not fully implemented as it requires complex graph operations
|
|
@@ -1678,9 +1698,14 @@ export async function createJJ(options) {
|
|
|
1678
1698
|
* @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
|
|
1679
1699
|
*/
|
|
1680
1700
|
async edit(args) {
|
|
1701
|
+
// Normalize: accept both 'change' and 'changeId' parameters
|
|
1702
|
+
if (args && args.change && !args.changeId) {
|
|
1703
|
+
args = { ...args, changeId: args.change };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1681
1706
|
if (!args || !args.changeId) {
|
|
1682
1707
|
throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
|
|
1683
|
-
suggestion: 'Provide a changeId to edit',
|
|
1708
|
+
suggestion: 'Provide a changeId (or change) to edit',
|
|
1684
1709
|
});
|
|
1685
1710
|
}
|
|
1686
1711
|
|
|
@@ -3522,6 +3547,23 @@ export async function createJJ(options) {
|
|
|
3522
3547
|
* @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
|
|
3523
3548
|
*/
|
|
3524
3549
|
async unabandon(args) {
|
|
3550
|
+
// Normalize: accept both 'change' and 'changeId' parameters
|
|
3551
|
+
if (args && args.change && !args.changeId) {
|
|
3552
|
+
args = { ...args, changeId: args.change };
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
if (!args || !args.changeId) {
|
|
3556
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing changeId argument', {
|
|
3557
|
+
suggestion: 'Provide a changeId (or change) to unabandon',
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
if (typeof args.changeId !== 'string') {
|
|
3562
|
+
throw new JJError('INVALID_ARGUMENT', 'Change ID must be a string', {
|
|
3563
|
+
changeId: args.changeId,
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3525
3567
|
await graph.load();
|
|
3526
3568
|
await userConfig.load();
|
|
3527
3569
|
|
|
@@ -4886,6 +4928,13 @@ export async function createJJ(options) {
|
|
|
4886
4928
|
*/
|
|
4887
4929
|
async add(args) {
|
|
4888
4930
|
await workspaces.load();
|
|
4931
|
+
|
|
4932
|
+
// If no changeId specified, default to current working copy change
|
|
4933
|
+
if (!args.changeId) {
|
|
4934
|
+
const currentChangeId = workingCopy.getCurrentChangeId();
|
|
4935
|
+
args = { ...args, changeId: currentChangeId };
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4889
4938
|
const workspace = await workspaces.add(args);
|
|
4890
4939
|
|
|
4891
4940
|
// If a change was specified, check it out in the new workspace
|
package/src/core/tag-store.js
CHANGED
|
@@ -64,7 +64,7 @@ export class TagStore {
|
|
|
64
64
|
*/
|
|
65
65
|
async load() {
|
|
66
66
|
try {
|
|
67
|
-
const data = await this.fs.readFile(this.tagsFile, 'utf8');
|
|
67
|
+
const data = await this.fs.promises.readFile(this.tagsFile, 'utf8');
|
|
68
68
|
return JSON.parse(data);
|
|
69
69
|
} catch (error) {
|
|
70
70
|
if (error.code === 'ENOENT') {
|
|
@@ -80,7 +80,14 @@ export class TagStore {
|
|
|
80
80
|
* @returns {Promise<void>}
|
|
81
81
|
*/
|
|
82
82
|
async save(tags) {
|
|
83
|
-
|
|
83
|
+
// Ensure the store directory exists
|
|
84
|
+
const storeDir = `${this.jjDir}/store`;
|
|
85
|
+
try {
|
|
86
|
+
await this.fs.promises.mkdir(storeDir, { recursive: true });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// Directory might already exist, ignore
|
|
89
|
+
}
|
|
90
|
+
await this.fs.promises.writeFile(this.tagsFile, JSON.stringify(tags, null, 2));
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/**
|
package/src/core/working-copy.js
CHANGED
|
@@ -132,7 +132,7 @@ export class WorkingCopy {
|
|
|
132
132
|
* @param {Object} fileState - File state (mtime, size, mode, hash)
|
|
133
133
|
*/
|
|
134
134
|
async trackFile(path, fileState) {
|
|
135
|
-
validatePath(path);
|
|
135
|
+
path = validatePath(path); // Normalize path
|
|
136
136
|
|
|
137
137
|
if (!this.state) {
|
|
138
138
|
await this.load();
|
|
@@ -144,11 +144,11 @@ export class WorkingCopy {
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* Untrack a file from the working copy
|
|
147
|
-
*
|
|
147
|
+
*
|
|
148
148
|
* @param {string} path - File path to untrack
|
|
149
149
|
*/
|
|
150
150
|
async untrackFile(path) {
|
|
151
|
-
validatePath(path);
|
|
151
|
+
path = validatePath(path); // Normalize path
|
|
152
152
|
|
|
153
153
|
if (!this.state) {
|
|
154
154
|
await this.load();
|
package/src/utils/validation.js
CHANGED
|
@@ -32,10 +32,26 @@ export function validateChangeId(changeId) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a file path by stripping leading slashes
|
|
37
|
+
*
|
|
38
|
+
* @param {string} path - File path to normalize
|
|
39
|
+
* @returns {string} Normalized path (without leading slash)
|
|
40
|
+
*/
|
|
41
|
+
export function normalizePath(path) {
|
|
42
|
+
if (typeof path !== 'string') {
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Strip leading slashes for compatibility with REST APIs and absolute paths
|
|
47
|
+
return path.replace(/^\/+/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
51
|
* Validate a file path
|
|
37
|
-
*
|
|
52
|
+
*
|
|
38
53
|
* @param {string} path - File path to validate
|
|
54
|
+
* @returns {string} Normalized path
|
|
39
55
|
* @throws {JJError} If path is invalid or contains path traversal
|
|
40
56
|
*/
|
|
41
57
|
export function validatePath(path) {
|
|
@@ -45,27 +61,25 @@ export function validatePath(path) {
|
|
|
45
61
|
});
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
// Auto-normalize: strip leading slashes (common with REST APIs)
|
|
65
|
+
const normalizedPath = normalizePath(path);
|
|
66
|
+
|
|
67
|
+
if (normalizedPath.includes('..')) {
|
|
49
68
|
throw new JJError('INVALID_PATH', 'Path traversal (..) not allowed', {
|
|
50
|
-
path,
|
|
69
|
+
path: normalizedPath,
|
|
51
70
|
suggestion: 'Use relative paths without .. components',
|
|
52
71
|
});
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
if (
|
|
56
|
-
throw new JJError('INVALID_PATH', 'Absolute paths not allowed', {
|
|
57
|
-
path,
|
|
58
|
-
suggestion: 'Use relative paths from repository root',
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (path.length > 4096) {
|
|
74
|
+
if (normalizedPath.length > 4096) {
|
|
63
75
|
throw new JJError('INVALID_PATH', 'Path exceeds maximum length', {
|
|
64
|
-
path,
|
|
76
|
+
path: normalizedPath,
|
|
65
77
|
maxLength: 4096,
|
|
66
78
|
suggestion: 'Use shorter file paths',
|
|
67
79
|
});
|
|
68
80
|
}
|
|
81
|
+
|
|
82
|
+
return normalizedPath;
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
/**
|