isomorphic-jj 1.1.3 → 1.3.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 +48 -0
- package/dist/index.cjs +327 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +327 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +246 -150
- package/src/core/bookmark-store.js +16 -4
- package/src/core/revset-engine.js +18 -0
- package/src/core/user-config.js +47 -2
- package/src/types.d.ts +165 -2
package/package.json
CHANGED
package/src/api/repository.js
CHANGED
|
@@ -756,69 +756,116 @@ export async function createJJ(options) {
|
|
|
756
756
|
* @param {string[]} [args.paths] - Paths to move between changes (for history operations)
|
|
757
757
|
* @returns {Promise<Object>} Returns change object for history operations, file info for file operations
|
|
758
758
|
*/
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
759
|
+
/**
|
|
760
|
+
* Move a file in the working copy
|
|
761
|
+
*
|
|
762
|
+
* @param {Object} args - Move file arguments
|
|
763
|
+
* @param {string} args.from - Source file path
|
|
764
|
+
* @param {string} args.to - Destination file path
|
|
765
|
+
* @returns {Promise<Object>} Operation result including from, to paths and file stats
|
|
766
|
+
*/
|
|
767
|
+
async moveFile(args) {
|
|
768
|
+
if (!args.from || typeof args.from !== 'string') {
|
|
769
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid from path', {
|
|
770
|
+
suggestion: 'Provide a valid source file path',
|
|
771
|
+
provided: { from: args.from, type: typeof args.from },
|
|
763
772
|
});
|
|
764
773
|
}
|
|
765
774
|
|
|
766
|
-
|
|
767
|
-
|
|
775
|
+
if (!args.to || typeof args.to !== 'string') {
|
|
776
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid to path', {
|
|
777
|
+
suggestion: 'Provide a valid destination file path',
|
|
778
|
+
provided: { to: args.to, type: typeof args.to },
|
|
779
|
+
});
|
|
780
|
+
}
|
|
768
781
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
782
|
+
// Prevent moving to the same location
|
|
783
|
+
if (args.from === args.to) {
|
|
784
|
+
throw new JJError('INVALID_OPERATION', 'Source and destination paths are the same', {
|
|
785
|
+
path: args.from,
|
|
786
|
+
suggestion: 'Specify a different destination path',
|
|
787
|
+
});
|
|
773
788
|
}
|
|
774
|
-
},
|
|
775
789
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
*/
|
|
783
|
-
_detectHistoryOperation(args) {
|
|
784
|
-
// Explicit history operation indicators
|
|
785
|
-
if (args.paths || args.changeId || args.newParent) {
|
|
786
|
-
return true;
|
|
790
|
+
// Prevent absolute paths
|
|
791
|
+
if (args.from.startsWith('/') || args.to.startsWith('/')) {
|
|
792
|
+
throw new JJError('INVALID_PATH', 'Absolute paths are not allowed', {
|
|
793
|
+
suggestion: 'Use paths relative to the repository root',
|
|
794
|
+
provided: { from: args.from, to: args.to },
|
|
795
|
+
});
|
|
787
796
|
}
|
|
788
797
|
|
|
789
|
-
//
|
|
790
|
-
if (args.from
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
798
|
+
// Prevent parent directory traversal
|
|
799
|
+
if (args.from.includes('..') || args.to.includes('..')) {
|
|
800
|
+
throw new JJError('INVALID_PATH', 'Parent directory traversal (..) is not allowed', {
|
|
801
|
+
suggestion: 'Use paths within the repository',
|
|
802
|
+
provided: { from: args.from, to: args.to },
|
|
803
|
+
});
|
|
804
|
+
}
|
|
795
805
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
// Users should use explicit { changeId, newParent } for clarity
|
|
799
|
-
if (fromLooksLikeChangeId && toLooksLikeChangeId) {
|
|
800
|
-
// This is ambiguous - throw a helpful error
|
|
801
|
-
throw new JJError('AMBIGUOUS_OPERATION',
|
|
802
|
-
'Ambiguous move operation: arguments look like change IDs', {
|
|
803
|
-
suggestion: 'Use { changeId, newParent } for history operations or ensure file paths don\'t match change ID pattern',
|
|
804
|
-
args,
|
|
805
|
-
});
|
|
806
|
-
}
|
|
806
|
+
const fromPath = `${dir}/${args.from}`;
|
|
807
|
+
const toPath = `${dir}/${args.to}`;
|
|
807
808
|
|
|
808
|
-
|
|
809
|
+
// Check if source file exists
|
|
810
|
+
try {
|
|
811
|
+
await fs.promises.stat(fromPath);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
throw new JJError('FILE_NOT_FOUND', `Source file not found: ${args.from}`, {
|
|
814
|
+
path: args.from,
|
|
815
|
+
suggestion: 'Check that the source file exists',
|
|
816
|
+
});
|
|
809
817
|
}
|
|
810
818
|
|
|
811
|
-
|
|
819
|
+
// Ensure destination directory exists
|
|
820
|
+
const toDir = toPath.substring(0, toPath.lastIndexOf('/'));
|
|
821
|
+
try {
|
|
822
|
+
await fs.promises.mkdir(toDir, { recursive: true });
|
|
823
|
+
} catch (error) {
|
|
824
|
+
throw new JJError('FILE_SYSTEM_ERROR', `Failed to create destination directory: ${error.message}`, {
|
|
825
|
+
path: toDir,
|
|
826
|
+
error: error.message,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Move the file
|
|
831
|
+
try {
|
|
832
|
+
await fs.promises.rename(fromPath, toPath);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
throw new JJError('FILE_SYSTEM_ERROR', `Failed to move file: ${error.message}`, {
|
|
835
|
+
from: args.from,
|
|
836
|
+
to: args.to,
|
|
837
|
+
error: error.message,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Update working copy tracking
|
|
842
|
+
await workingCopy.load();
|
|
843
|
+
await workingCopy.untrackFile(args.from);
|
|
844
|
+
const stats = await fs.promises.stat(toPath);
|
|
845
|
+
await workingCopy.trackFile(args.to, {
|
|
846
|
+
mtime: stats.mtime,
|
|
847
|
+
size: stats.size,
|
|
848
|
+
mode: stats.mode,
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
from: args.from,
|
|
853
|
+
to: args.to,
|
|
854
|
+
size: stats.size,
|
|
855
|
+
mode: stats.mode,
|
|
856
|
+
mtime: stats.mtime,
|
|
857
|
+
};
|
|
812
858
|
},
|
|
813
859
|
|
|
814
860
|
/**
|
|
815
|
-
* Move
|
|
861
|
+
* Move (rebase) a change to a new parent
|
|
816
862
|
*
|
|
817
|
-
* @
|
|
818
|
-
* @param {
|
|
863
|
+
* @param {Object} args - Move change arguments
|
|
864
|
+
* @param {string} args.changeId - Change ID to move (also accepts 'from' or 'source')
|
|
865
|
+
* @param {string} args.newParent - New parent change ID (also accepts 'to' or 'destination')
|
|
819
866
|
* @returns {Promise<Object>} Updated change object
|
|
820
867
|
*/
|
|
821
|
-
async
|
|
868
|
+
async moveChange(args) {
|
|
822
869
|
await graph.load();
|
|
823
870
|
|
|
824
871
|
// Support multiple parameter names for flexibility
|
|
@@ -906,110 +953,62 @@ export async function createJJ(options) {
|
|
|
906
953
|
},
|
|
907
954
|
|
|
908
955
|
/**
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
* @private
|
|
912
|
-
* @param {Object} args - Move arguments
|
|
913
|
-
* @returns {Promise<Object>} Operation result including from, to paths and file stats
|
|
956
|
+
* @deprecated Use moveFile() for files or moveChange() for history operations
|
|
957
|
+
* Move operation - delegates to moveFile or moveChange based on arguments
|
|
914
958
|
*/
|
|
915
|
-
async
|
|
916
|
-
if (!args
|
|
917
|
-
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid
|
|
918
|
-
suggestion: '
|
|
919
|
-
provided: { from: args.from, type: typeof args.from },
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (!args.to || typeof args.to !== 'string') {
|
|
924
|
-
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid to path', {
|
|
925
|
-
suggestion: 'Provide a valid destination file path',
|
|
926
|
-
provided: { to: args.to, type: typeof args.to },
|
|
959
|
+
async move(args) {
|
|
960
|
+
if (!args || typeof args !== 'object') {
|
|
961
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid arguments', {
|
|
962
|
+
suggestion: 'Use moveFile({ from, to }) for files or moveChange({ changeId, newParent }) for history',
|
|
927
963
|
});
|
|
928
964
|
}
|
|
929
965
|
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
throw new JJError('INVALID_OPERATION', 'Source and destination paths are the same', {
|
|
933
|
-
path: args.from,
|
|
934
|
-
suggestion: 'Specify a different destination path',
|
|
935
|
-
});
|
|
936
|
-
}
|
|
966
|
+
// Detect operation type based on arguments
|
|
967
|
+
const isHistoryOp = this._detectHistoryOperation(args);
|
|
937
968
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
provided: { from: args.from, to: args.to },
|
|
943
|
-
});
|
|
969
|
+
if (isHistoryOp) {
|
|
970
|
+
return await this.moveChange(args);
|
|
971
|
+
} else {
|
|
972
|
+
return await this.moveFile(args);
|
|
944
973
|
}
|
|
974
|
+
},
|
|
945
975
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
976
|
+
/**
|
|
977
|
+
* Detect if move operation is for history (rebase) or files
|
|
978
|
+
*
|
|
979
|
+
* @private
|
|
980
|
+
* @param {Object} args - Move arguments
|
|
981
|
+
* @returns {boolean} True if history operation, false if file operation
|
|
982
|
+
*/
|
|
983
|
+
_detectHistoryOperation(args) {
|
|
984
|
+
// Explicit history operation indicators
|
|
985
|
+
if (args.paths || args.changeId || args.newParent) {
|
|
986
|
+
return true;
|
|
952
987
|
}
|
|
953
988
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
} catch (error) {
|
|
961
|
-
throw new JJError('FILE_NOT_FOUND', `Source file not found: ${args.from}`, {
|
|
962
|
-
path: args.from,
|
|
963
|
-
fullPath: fromPath,
|
|
964
|
-
suggestion: 'Check that the file exists',
|
|
965
|
-
});
|
|
966
|
-
}
|
|
989
|
+
// If only from/to are present, check if they look like change IDs
|
|
990
|
+
if (args.from && args.to && !args.changeId && !args.newParent && !args.paths) {
|
|
991
|
+
// Change IDs are 32-character hex strings
|
|
992
|
+
const changeIdPattern = /^[0-9a-f]{32}$/;
|
|
993
|
+
const fromLooksLikeChangeId = changeIdPattern.test(args.from);
|
|
994
|
+
const toLooksLikeChangeId = changeIdPattern.test(args.to);
|
|
967
995
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
`Failed to create destination directory: ${dirPath}`, {
|
|
978
|
-
directory: dirPath,
|
|
979
|
-
originalError: error.message,
|
|
996
|
+
// If both look like change IDs, it's likely a history operation
|
|
997
|
+
// However, we err on the side of file operations to be safe
|
|
998
|
+
// Users should use explicit { changeId, newParent } for clarity
|
|
999
|
+
if (fromLooksLikeChangeId && toLooksLikeChangeId) {
|
|
1000
|
+
// This is ambiguous - throw a helpful error
|
|
1001
|
+
throw new JJError('AMBIGUOUS_OPERATION',
|
|
1002
|
+
'Ambiguous move operation: arguments look like change IDs', {
|
|
1003
|
+
suggestion: 'Use { changeId, newParent } for history operations or ensure file paths don\'t match change ID pattern',
|
|
1004
|
+
args,
|
|
980
1005
|
});
|
|
981
1006
|
}
|
|
982
|
-
}
|
|
983
1007
|
|
|
984
|
-
|
|
985
|
-
try {
|
|
986
|
-
await fs.promises.rename(fromPath, toPath);
|
|
987
|
-
} catch (error) {
|
|
988
|
-
throw new JJError('FILE_MOVE_FAILED', `Failed to move file: ${error.message}`, {
|
|
989
|
-
from: args.from,
|
|
990
|
-
to: args.to,
|
|
991
|
-
originalError: error.message,
|
|
992
|
-
});
|
|
1008
|
+
return false; // Treat as file operation
|
|
993
1009
|
}
|
|
994
1010
|
|
|
995
|
-
|
|
996
|
-
await workingCopy.load();
|
|
997
|
-
await workingCopy.untrackFile(args.from);
|
|
998
|
-
const stats = await fs.promises.stat(toPath);
|
|
999
|
-
await workingCopy.trackFile(args.to, {
|
|
1000
|
-
mtime: stats.mtime,
|
|
1001
|
-
size: stats.size,
|
|
1002
|
-
mode: stats.mode,
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
// Return useful information about the move operation
|
|
1006
|
-
return {
|
|
1007
|
-
from: args.from,
|
|
1008
|
-
to: args.to,
|
|
1009
|
-
size: stats.size,
|
|
1010
|
-
mode: stats.mode,
|
|
1011
|
-
mtime: stats.mtime,
|
|
1012
|
-
};
|
|
1011
|
+
return false;
|
|
1013
1012
|
},
|
|
1014
1013
|
|
|
1015
1014
|
/**
|
|
@@ -1554,7 +1553,8 @@ export async function createJJ(options) {
|
|
|
1554
1553
|
* @param {Object} args - Arguments
|
|
1555
1554
|
* @param {string} [args.revset='all()'] - Revset expression to filter changes
|
|
1556
1555
|
* @param {number} [args.limit] - Maximum number of changes to return
|
|
1557
|
-
* @
|
|
1556
|
+
* @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
|
|
1557
|
+
* @returns {Promise<Array|number>} Array of changes or count if args.count is true
|
|
1558
1558
|
*/
|
|
1559
1559
|
async log(args = {}) {
|
|
1560
1560
|
await graph.load();
|
|
@@ -1563,6 +1563,11 @@ export async function createJJ(options) {
|
|
|
1563
1563
|
const revsetExpr = args.revset || 'all()';
|
|
1564
1564
|
const changeIds = await revset.evaluate(revsetExpr);
|
|
1565
1565
|
|
|
1566
|
+
// If count mode, return count early (v0.36.0 feature)
|
|
1567
|
+
if (args.count) {
|
|
1568
|
+
return changeIds.length;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1566
1571
|
let changes = [];
|
|
1567
1572
|
for (const changeId of changeIds) {
|
|
1568
1573
|
const change = await graph.getChange(changeId);
|
|
@@ -2510,7 +2515,7 @@ export async function createJJ(options) {
|
|
|
2510
2515
|
* });
|
|
2511
2516
|
*/
|
|
2512
2517
|
async rebase(args) {
|
|
2513
|
-
return await this.
|
|
2518
|
+
return await this.moveChange(args);
|
|
2514
2519
|
},
|
|
2515
2520
|
|
|
2516
2521
|
/**
|
|
@@ -2583,9 +2588,18 @@ export async function createJJ(options) {
|
|
|
2583
2588
|
* @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
|
|
2584
2589
|
*/
|
|
2585
2590
|
async backout(args) {
|
|
2591
|
+
// Normalize: accept common parameter aliases
|
|
2592
|
+
if (args && args.change && !args.revision) {
|
|
2593
|
+
args = { ...args, revision: args.change };
|
|
2594
|
+
} else if (args && args.changeId && !args.revision) {
|
|
2595
|
+
args = { ...args, revision: args.changeId };
|
|
2596
|
+
} else if (args && args.target && !args.revision) {
|
|
2597
|
+
args = { ...args, revision: args.target };
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2586
2600
|
if (!args || !args.revision) {
|
|
2587
2601
|
throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
|
|
2588
|
-
suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
|
|
2602
|
+
suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
|
|
2589
2603
|
});
|
|
2590
2604
|
}
|
|
2591
2605
|
|
|
@@ -3620,6 +3634,15 @@ export async function createJJ(options) {
|
|
|
3620
3634
|
* @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
|
|
3621
3635
|
*/
|
|
3622
3636
|
async split(args) {
|
|
3637
|
+
// Normalize: accept common parameter aliases
|
|
3638
|
+
if (args && args.change && !args.changeId) {
|
|
3639
|
+
args = { ...args, changeId: args.change };
|
|
3640
|
+
} else if (args && args.revision && !args.changeId) {
|
|
3641
|
+
args = { ...args, changeId: args.revision };
|
|
3642
|
+
} else if (args && args.target && !args.changeId) {
|
|
3643
|
+
args = { ...args, changeId: args.target };
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3623
3646
|
// Check for interactive mode (not supported)
|
|
3624
3647
|
if (args.interactive) {
|
|
3625
3648
|
throw new JJError(
|
|
@@ -5861,21 +5884,13 @@ export async function createJJ(options) {
|
|
|
5861
5884
|
});
|
|
5862
5885
|
}
|
|
5863
5886
|
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
const parts = key.split('.');
|
|
5868
|
-
let value = userConfig.config;
|
|
5869
|
-
|
|
5870
|
-
for (const part of parts) {
|
|
5871
|
-
if (value && typeof value === 'object') {
|
|
5872
|
-
value = value[part];
|
|
5873
|
-
} else {
|
|
5874
|
-
return null;
|
|
5875
|
-
}
|
|
5887
|
+
// Ensure config is loaded at least once
|
|
5888
|
+
if (!userConfig.config) {
|
|
5889
|
+
await userConfig.load();
|
|
5876
5890
|
}
|
|
5877
5891
|
|
|
5878
|
-
|
|
5892
|
+
// Use userConfig.get() to preserve programmatic config
|
|
5893
|
+
return userConfig.get(key);
|
|
5879
5894
|
},
|
|
5880
5895
|
|
|
5881
5896
|
/**
|
|
@@ -5924,6 +5939,87 @@ export async function createJJ(options) {
|
|
|
5924
5939
|
await userConfig.load();
|
|
5925
5940
|
return userConfig.config;
|
|
5926
5941
|
},
|
|
5942
|
+
|
|
5943
|
+
/**
|
|
5944
|
+
* Reload configuration (including workspace-specific config)
|
|
5945
|
+
* v0.35.0: Loads workspace-config.json if present
|
|
5946
|
+
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
5947
|
+
*
|
|
5948
|
+
* @param {Object} [opts] - Optional config options
|
|
5949
|
+
* @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
|
|
5950
|
+
* @param {Object} [opts.override] - Config object to merge over loaded config
|
|
5951
|
+
* @returns {Promise<void>}
|
|
5952
|
+
*
|
|
5953
|
+
* @example
|
|
5954
|
+
* // Load from files only
|
|
5955
|
+
* await repo.config.load();
|
|
5956
|
+
*
|
|
5957
|
+
* @example
|
|
5958
|
+
* // Programmatic workspace config (no file I/O needed)
|
|
5959
|
+
* await repo.config.load({
|
|
5960
|
+
* workspace: { user: { email: 'test@example.com' } }
|
|
5961
|
+
* });
|
|
5962
|
+
*
|
|
5963
|
+
* @example
|
|
5964
|
+
* // Override global config programmatically
|
|
5965
|
+
* await repo.config.load({
|
|
5966
|
+
* override: { test: { value: 'override' } }
|
|
5967
|
+
* });
|
|
5968
|
+
*/
|
|
5969
|
+
async load(opts) {
|
|
5970
|
+
await userConfig.load(opts);
|
|
5971
|
+
},
|
|
5972
|
+
},
|
|
5973
|
+
|
|
5974
|
+
/**
|
|
5975
|
+
* Template helper functions (v0.36.0)
|
|
5976
|
+
*
|
|
5977
|
+
* Utilities for displaying and formatting JJ data
|
|
5978
|
+
*/
|
|
5979
|
+
template: {
|
|
5980
|
+
/**
|
|
5981
|
+
* Get files modified in a change
|
|
5982
|
+
*
|
|
5983
|
+
* @param {string} changeId - Change ID
|
|
5984
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
5985
|
+
*/
|
|
5986
|
+
async files(changeId) {
|
|
5987
|
+
await graph.load();
|
|
5988
|
+
const change = await graph.getChange(changeId);
|
|
5989
|
+
if (!change || !change.fileSnapshot) {
|
|
5990
|
+
return [];
|
|
5991
|
+
}
|
|
5992
|
+
return Object.keys(change.fileSnapshot);
|
|
5993
|
+
},
|
|
5994
|
+
|
|
5995
|
+
/**
|
|
5996
|
+
* Join array elements with a separator
|
|
5997
|
+
*
|
|
5998
|
+
* @param {string[]} array - Array of strings
|
|
5999
|
+
* @param {string} separator - Separator string
|
|
6000
|
+
* @returns {string} Joined string
|
|
6001
|
+
*/
|
|
6002
|
+
join(array, separator) {
|
|
6003
|
+
if (!Array.isArray(array)) {
|
|
6004
|
+
return '';
|
|
6005
|
+
}
|
|
6006
|
+
return array.join(separator);
|
|
6007
|
+
},
|
|
6008
|
+
|
|
6009
|
+
/**
|
|
6010
|
+
* Format path for display
|
|
6011
|
+
*
|
|
6012
|
+
* @param {string} path - File path
|
|
6013
|
+
* @returns {string} Formatted path
|
|
6014
|
+
*/
|
|
6015
|
+
format_path(path) {
|
|
6016
|
+
if (!path) {
|
|
6017
|
+
return '';
|
|
6018
|
+
}
|
|
6019
|
+
// Simple formatting: just return the path as-is for now
|
|
6020
|
+
// In real JJ, this might apply colors, truncation, etc.
|
|
6021
|
+
return path;
|
|
6022
|
+
},
|
|
5927
6023
|
},
|
|
5928
6024
|
|
|
5929
6025
|
/**
|
|
@@ -15,6 +15,7 @@ export class BookmarkStore {
|
|
|
15
15
|
this.storage = storage;
|
|
16
16
|
this.local = new Map();
|
|
17
17
|
this.remote = new Map(); // remote → (bookmark → changeId)
|
|
18
|
+
this.tracking = {}; // bookmark → { remote, ref } tracking info (v0.35.0)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -57,6 +58,7 @@ export class BookmarkStore {
|
|
|
57
58
|
|
|
58
59
|
this.local.clear();
|
|
59
60
|
this.remote.clear();
|
|
61
|
+
this.tracking = data.tracked || {}; // Load tracking info (v0.35.0)
|
|
60
62
|
|
|
61
63
|
// Load local bookmarks
|
|
62
64
|
for (const [name, target] of Object.entries(data.local)) {
|
|
@@ -94,7 +96,7 @@ export class BookmarkStore {
|
|
|
94
96
|
version: 1,
|
|
95
97
|
local,
|
|
96
98
|
remote,
|
|
97
|
-
tracked: {},
|
|
99
|
+
tracked: this.tracking || {}, // Save tracking info (v0.35.0)
|
|
98
100
|
});
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -207,13 +209,23 @@ export class BookmarkStore {
|
|
|
207
209
|
async list() {
|
|
208
210
|
const result = [];
|
|
209
211
|
|
|
210
|
-
// Add local bookmarks
|
|
212
|
+
// Add local bookmarks with tracking info (v0.35.0)
|
|
211
213
|
for (const [name, target] of this.local.entries()) {
|
|
212
|
-
|
|
214
|
+
const bookmark = {
|
|
213
215
|
name,
|
|
214
216
|
changeId: target,
|
|
215
217
|
remote: null,
|
|
216
|
-
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Include tracking info if available
|
|
221
|
+
if (this.tracking && this.tracking[name]) {
|
|
222
|
+
bookmark.tracking = {
|
|
223
|
+
remote: this.tracking[name].remote,
|
|
224
|
+
ref: this.tracking[name].remoteName || name,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
result.push(bookmark);
|
|
217
229
|
}
|
|
218
230
|
|
|
219
231
|
// Add remote bookmarks
|
|
@@ -129,6 +129,24 @@ export class RevsetEngine {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// v0.36.0: visible() - non-abandoned changes
|
|
133
|
+
if (trimmed === 'visible()') {
|
|
134
|
+
await this.graph.load();
|
|
135
|
+
const all = this.graph.getAll();
|
|
136
|
+
return all
|
|
137
|
+
.filter(c => !c.abandoned)
|
|
138
|
+
.map(c => c.changeId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// v0.36.0: hidden() - abandoned changes
|
|
142
|
+
if (trimmed === 'hidden()') {
|
|
143
|
+
await this.graph.load();
|
|
144
|
+
const all = this.graph.getAll();
|
|
145
|
+
return all
|
|
146
|
+
.filter(c => c.abandoned === true)
|
|
147
|
+
.map(c => c.changeId);
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
// ancestors(changeId) - all ancestors including the change itself
|
|
133
151
|
const ancestorsMatch = trimmed.match(/^ancestors\(([0-9a-f]{32})\)$/);
|
|
134
152
|
if (ancestorsMatch) {
|
package/src/core/user-config.js
CHANGED
|
@@ -37,9 +37,15 @@ export class UserConfig {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Load config from storage
|
|
40
|
+
* Load config from storage and/or programmatic object
|
|
41
|
+
* v0.35.0: Also loads workspace-specific config from workspace-config.json
|
|
42
|
+
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} [opts] - Optional config options
|
|
45
|
+
* @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
|
|
46
|
+
* @param {Object} [opts.override] - Config object to merge over loaded config
|
|
41
47
|
*/
|
|
42
|
-
async load() {
|
|
48
|
+
async load(opts = {}) {
|
|
43
49
|
try {
|
|
44
50
|
const data = await this.storage.read('config.json');
|
|
45
51
|
if (data) {
|
|
@@ -49,6 +55,45 @@ export class UserConfig {
|
|
|
49
55
|
// No config file yet, use defaults
|
|
50
56
|
await this.init();
|
|
51
57
|
}
|
|
58
|
+
|
|
59
|
+
// Merge programmatic override config if provided
|
|
60
|
+
if (opts.override) {
|
|
61
|
+
this.config = this._deepMerge(this.config, opts.override);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Load workspace-specific config from file (v0.35.0)
|
|
65
|
+
try {
|
|
66
|
+
const workspaceData = await this.storage.read('workspace-config.json');
|
|
67
|
+
if (workspaceData) {
|
|
68
|
+
// Merge workspace config over global config (workspace takes precedence)
|
|
69
|
+
this.config = this._deepMerge(this.config, workspaceData);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// No workspace config, that's fine
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Merge programmatic workspace config if provided (highest priority)
|
|
76
|
+
if (opts.workspace) {
|
|
77
|
+
this.config = this._deepMerge(this.config, opts.workspace);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Deep merge two config objects (workspace overrides global)
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
_deepMerge(target, source) {
|
|
86
|
+
const result = { ...target };
|
|
87
|
+
|
|
88
|
+
for (const key in source) {
|
|
89
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
90
|
+
result[key] = this._deepMerge(target[key] || {}, source[key]);
|
|
91
|
+
} else {
|
|
92
|
+
result[key] = source[key];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
52
97
|
}
|
|
53
98
|
|
|
54
99
|
/**
|