@solaqua/gji 0.6.0 → 0.6.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/README.md +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -1
- package/dist/gji-bundle.mjs +212 -124
- package/dist/repo-registry.js +32 -6
- package/dist/shell-completion.js +124 -89
- package/dist/warp.js +31 -3
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-trigger-hook.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ gji trigger-hook afterCreate # re-run setup in the current worktree
|
|
|
168
168
|
|
|
169
169
|
- **vs raw `git worktree`**: same underlying capability, but with branch-first commands, shell handoff, PR checkout, hooks, sync, and cleanup built into the workflow
|
|
170
170
|
- **vs `lazygit`**: `lazygit` is a broad Git UI; `gji` is narrower and faster for opening, jumping between, and removing isolated branch directories
|
|
171
|
-
- **vs `ghq`**: `ghq` organizes repositories; `gji` organizes
|
|
171
|
+
- **vs `ghq`**: `ghq` organizes where repositories live; `gji` organizes which branch, PR, or worktree you should be in once you are inside one
|
|
172
172
|
|
|
173
173
|
Use `gji` when your bottleneck is repeated context switching between features, reviews, and maintenance work without disturbing what is already open.
|
|
174
174
|
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const CONFIG_FILE_NAME = ".gji.json";
|
|
|
2
2
|
export declare const GLOBAL_CONFIG_DIRECTORY = ".config/gji";
|
|
3
3
|
export declare const GLOBAL_CONFIG_NAME = "config.json";
|
|
4
4
|
export declare const KNOWN_CONFIG_KEYS: ReadonlySet<string>;
|
|
5
|
+
export declare const KNOWN_GLOBAL_CONFIG_KEYS: ReadonlySet<string>;
|
|
5
6
|
export type GjiConfig = Record<string, unknown>;
|
|
6
7
|
export interface LoadedConfig {
|
|
7
8
|
config: GjiConfig;
|
package/dist/config.js
CHANGED
package/dist/gji-bundle.mjs
CHANGED
|
@@ -9740,7 +9740,7 @@ var retryifyAsync = (fn, options) => {
|
|
|
9740
9740
|
throw error;
|
|
9741
9741
|
const delay2 = Math.round(interval * Math.random());
|
|
9742
9742
|
if (delay2 > 0) {
|
|
9743
|
-
const delayPromise = new Promise((
|
|
9743
|
+
const delayPromise = new Promise((resolve6) => setTimeout(resolve6, delay2));
|
|
9744
9744
|
return delayPromise.then(() => attempt.apply(void 0, args));
|
|
9745
9745
|
} else {
|
|
9746
9746
|
return attempt.apply(void 0, args);
|
|
@@ -9999,14 +9999,14 @@ var Temp = {
|
|
|
9999
9999
|
}
|
|
10000
10000
|
},
|
|
10001
10001
|
truncate: (filePath) => {
|
|
10002
|
-
const
|
|
10003
|
-
if (
|
|
10002
|
+
const basename9 = path2.basename(filePath);
|
|
10003
|
+
if (basename9.length <= LIMIT_BASENAME_LENGTH)
|
|
10004
10004
|
return filePath;
|
|
10005
|
-
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(
|
|
10005
|
+
const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename9);
|
|
10006
10006
|
if (!truncable)
|
|
10007
10007
|
return filePath;
|
|
10008
|
-
const truncationLength =
|
|
10009
|
-
return `${filePath.slice(0, -
|
|
10008
|
+
const truncationLength = basename9.length - LIMIT_BASENAME_LENGTH;
|
|
10009
|
+
return `${filePath.slice(0, -basename9.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
|
|
10010
10010
|
}
|
|
10011
10011
|
};
|
|
10012
10012
|
node_default(Temp.purgeSyncAll);
|
|
@@ -11306,14 +11306,14 @@ var TimeoutError = class extends Error {
|
|
|
11306
11306
|
|
|
11307
11307
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
|
|
11308
11308
|
async function timeout(request, init, abortController, options) {
|
|
11309
|
-
return new Promise((
|
|
11309
|
+
return new Promise((resolve6, reject) => {
|
|
11310
11310
|
const timeoutId = setTimeout(() => {
|
|
11311
11311
|
if (abortController) {
|
|
11312
11312
|
abortController.abort();
|
|
11313
11313
|
}
|
|
11314
11314
|
reject(new TimeoutError(request));
|
|
11315
11315
|
}, options.timeout);
|
|
11316
|
-
void options.fetch(request, init).then(
|
|
11316
|
+
void options.fetch(request, init).then(resolve6).catch(reject).then(() => {
|
|
11317
11317
|
clearTimeout(timeoutId);
|
|
11318
11318
|
});
|
|
11319
11319
|
});
|
|
@@ -11321,7 +11321,7 @@ async function timeout(request, init, abortController, options) {
|
|
|
11321
11321
|
|
|
11322
11322
|
// node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
|
|
11323
11323
|
async function delay(ms, { signal }) {
|
|
11324
|
-
return new Promise((
|
|
11324
|
+
return new Promise((resolve6, reject) => {
|
|
11325
11325
|
if (signal) {
|
|
11326
11326
|
signal.throwIfAborted();
|
|
11327
11327
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -11332,7 +11332,7 @@ async function delay(ms, { signal }) {
|
|
|
11332
11332
|
}
|
|
11333
11333
|
const timeoutId = setTimeout(() => {
|
|
11334
11334
|
signal?.removeEventListener("abort", abortHandler);
|
|
11335
|
-
|
|
11335
|
+
resolve6();
|
|
11336
11336
|
}, ms);
|
|
11337
11337
|
});
|
|
11338
11338
|
}
|
|
@@ -13048,7 +13048,7 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
|
13048
13048
|
stderr("gji: hook argv command must include a non-empty command\n");
|
|
13049
13049
|
return;
|
|
13050
13050
|
}
|
|
13051
|
-
await new Promise((
|
|
13051
|
+
await new Promise((resolve6) => {
|
|
13052
13052
|
const child = spawn2(command, args, {
|
|
13053
13053
|
cwd,
|
|
13054
13054
|
shell: false,
|
|
@@ -13063,18 +13063,18 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
|
|
|
13063
13063
|
stderr(`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}
|
|
13064
13064
|
`);
|
|
13065
13065
|
}
|
|
13066
|
-
|
|
13066
|
+
resolve6();
|
|
13067
13067
|
});
|
|
13068
13068
|
child.on("error", (err) => {
|
|
13069
13069
|
stderr(`gji: hook failed to start: ${err.message}
|
|
13070
13070
|
`);
|
|
13071
|
-
|
|
13071
|
+
resolve6();
|
|
13072
13072
|
});
|
|
13073
13073
|
});
|
|
13074
13074
|
}
|
|
13075
13075
|
async function runShellHook(hookCmd, cwd, context, stderr) {
|
|
13076
13076
|
const interpolated = interpolate(hookCmd, context);
|
|
13077
|
-
await new Promise((
|
|
13077
|
+
await new Promise((resolve6) => {
|
|
13078
13078
|
const child = spawn2(interpolated, {
|
|
13079
13079
|
cwd,
|
|
13080
13080
|
shell: true,
|
|
@@ -13089,12 +13089,12 @@ async function runShellHook(hookCmd, cwd, context, stderr) {
|
|
|
13089
13089
|
stderr(`gji: hook exited with code ${code}: ${interpolated}
|
|
13090
13090
|
`);
|
|
13091
13091
|
}
|
|
13092
|
-
|
|
13092
|
+
resolve6();
|
|
13093
13093
|
});
|
|
13094
13094
|
child.on("error", (err) => {
|
|
13095
13095
|
stderr(`gji: hook failed to start: ${err.message}
|
|
13096
13096
|
`);
|
|
13097
|
-
|
|
13097
|
+
resolve6();
|
|
13098
13098
|
});
|
|
13099
13099
|
});
|
|
13100
13100
|
}
|
|
@@ -14482,7 +14482,11 @@ var TOP_LEVEL_COMMANDS = [
|
|
|
14482
14482
|
{ name: "init", description: "print or install shell integration" },
|
|
14483
14483
|
{ name: "completion", description: "print shell completion definitions" },
|
|
14484
14484
|
{ name: "pr", description: "fetch a pull request into a linked worktree" },
|
|
14485
|
+
{ name: "back", description: "navigate to the previously visited worktree" },
|
|
14486
|
+
{ name: "history", description: "show navigation history" },
|
|
14487
|
+
{ name: "open", description: "open the worktree in an editor" },
|
|
14485
14488
|
{ name: "go", description: "print or select a worktree path" },
|
|
14489
|
+
{ name: "jump", description: "alias of go" },
|
|
14486
14490
|
{ name: "root", description: "print the main repository root path" },
|
|
14487
14491
|
{ name: "status", description: "summarize repository and worktree health" },
|
|
14488
14492
|
{ name: "sync", description: "fetch and update one or all worktrees" },
|
|
@@ -14491,20 +14495,12 @@ var TOP_LEVEL_COMMANDS = [
|
|
|
14491
14495
|
{ name: "remove", description: "remove a linked worktree and delete its branch when present" },
|
|
14492
14496
|
{ name: "rm", description: "alias of remove" },
|
|
14493
14497
|
{ name: "trigger-hook", description: "run a named hook in the current worktree" },
|
|
14498
|
+
{ name: "warp", description: "jump to any worktree across all known repos" },
|
|
14494
14499
|
{ name: "config", description: "manage global config defaults" }
|
|
14495
14500
|
];
|
|
14496
14501
|
var SHELL_NAMES = ["bash", "fish", "zsh"];
|
|
14497
14502
|
var HOOK_NAMES = ["afterCreate", "afterEnter", "beforeRemove"];
|
|
14498
|
-
var CONFIG_KEYS =
|
|
14499
|
-
"branchPrefix",
|
|
14500
|
-
"syncRemote",
|
|
14501
|
-
"syncDefaultBranch",
|
|
14502
|
-
"syncFiles",
|
|
14503
|
-
"skipInstallPrompt",
|
|
14504
|
-
"installSaveTarget",
|
|
14505
|
-
"hooks",
|
|
14506
|
-
"repos"
|
|
14507
|
-
];
|
|
14503
|
+
var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
|
|
14508
14504
|
function renderShellCompletion(shell) {
|
|
14509
14505
|
switch (shell) {
|
|
14510
14506
|
case "bash":
|
|
@@ -14538,7 +14534,7 @@ _gji_completion() {
|
|
|
14538
14534
|
|
|
14539
14535
|
case "$command_name" in
|
|
14540
14536
|
new)
|
|
14541
|
-
COMPREPLY=( $(compgen -W "--detached --dry-run --json --help" -- "$cur") )
|
|
14537
|
+
COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
14542
14538
|
;;
|
|
14543
14539
|
init)
|
|
14544
14540
|
COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
|
|
@@ -14549,7 +14545,16 @@ _gji_completion() {
|
|
|
14549
14545
|
pr)
|
|
14550
14546
|
COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
|
|
14551
14547
|
;;
|
|
14552
|
-
|
|
14548
|
+
back)
|
|
14549
|
+
COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
|
|
14550
|
+
;;
|
|
14551
|
+
history)
|
|
14552
|
+
COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
|
|
14553
|
+
;;
|
|
14554
|
+
open)
|
|
14555
|
+
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
|
|
14556
|
+
;;
|
|
14557
|
+
go|jump)
|
|
14553
14558
|
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
|
|
14554
14559
|
;;
|
|
14555
14560
|
root)
|
|
@@ -14573,6 +14578,9 @@ _gji_completion() {
|
|
|
14573
14578
|
trigger-hook)
|
|
14574
14579
|
COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
|
|
14575
14580
|
;;
|
|
14581
|
+
warp)
|
|
14582
|
+
COMPREPLY=( $(compgen -W "-n --new --print --json --help" -- "$cur") )
|
|
14583
|
+
;;
|
|
14576
14584
|
config)
|
|
14577
14585
|
if [ "$COMP_CWORD" -eq 2 ]; then
|
|
14578
14586
|
COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
|
|
@@ -14636,6 +14644,9 @@ complete -c gji -f
|
|
|
14636
14644
|
${commandLines}
|
|
14637
14645
|
|
|
14638
14646
|
complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
|
|
14647
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
|
|
14648
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
|
|
14649
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, \u2026)'
|
|
14639
14650
|
complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
|
|
14640
14651
|
complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
14641
14652
|
|
|
@@ -14649,8 +14660,17 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
|
|
|
14649
14660
|
complete -c gji -n '__fish_seen_subcommand_from pr' -l dry-run -d 'show what would be created without executing any git commands or writing files'
|
|
14650
14661
|
complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
14651
14662
|
|
|
14652
|
-
complete -c gji -n '__fish_seen_subcommand_from
|
|
14653
|
-
|
|
14663
|
+
complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
|
|
14664
|
+
|
|
14665
|
+
complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
|
|
14666
|
+
|
|
14667
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)'
|
|
14668
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
|
|
14669
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
|
|
14670
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
14671
|
+
|
|
14672
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
|
|
14673
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
14654
14674
|
|
|
14655
14675
|
complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
|
|
14656
14676
|
|
|
@@ -14674,6 +14694,10 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
|
|
|
14674
14694
|
|
|
14675
14695
|
${hookLines}
|
|
14676
14696
|
|
|
14697
|
+
complete -c gji -n '__fish_seen_subcommand_from warp' -s n -l new -d 'create a new worktree in a registered repo'
|
|
14698
|
+
complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
|
|
14699
|
+
complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
14700
|
+
|
|
14677
14701
|
complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
|
|
14678
14702
|
${configKeyLines}`;
|
|
14679
14703
|
}
|
|
@@ -14684,89 +14708,99 @@ function renderZshCompletion() {
|
|
|
14684
14708
|
const configKeys = CONFIG_KEYS.join(" ");
|
|
14685
14709
|
const shells = SHELL_NAMES.join(" ");
|
|
14686
14710
|
const hooks = HOOK_NAMES.join(" ");
|
|
14687
|
-
return
|
|
14711
|
+
return `#compdef gji
|
|
14712
|
+
|
|
14713
|
+
__gji_worktree_branches() {
|
|
14688
14714
|
command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
|
|
14689
14715
|
}
|
|
14690
14716
|
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
local -a commands worktree_branches
|
|
14694
|
-
|
|
14695
|
-
commands=(
|
|
14696
|
-
${commandLines}
|
|
14697
|
-
)
|
|
14717
|
+
local context state line
|
|
14718
|
+
local -a commands worktree_branches
|
|
14698
14719
|
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
fi
|
|
14720
|
+
commands=(
|
|
14721
|
+
${commandLines}
|
|
14722
|
+
)
|
|
14703
14723
|
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
init)
|
|
14709
|
-
_arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
|
|
14710
|
-
;;
|
|
14711
|
-
completion)
|
|
14712
|
-
_arguments '2:shell:(${shells})'
|
|
14713
|
-
;;
|
|
14714
|
-
pr)
|
|
14715
|
-
_arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
|
|
14716
|
-
;;
|
|
14717
|
-
go)
|
|
14718
|
-
_arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
|
|
14719
|
-
;;
|
|
14720
|
-
root)
|
|
14721
|
-
_arguments '--print[print the resolved repository root path explicitly]'
|
|
14722
|
-
;;
|
|
14723
|
-
status)
|
|
14724
|
-
_arguments '--json[print repository and worktree health as JSON]'
|
|
14725
|
-
;;
|
|
14726
|
-
sync)
|
|
14727
|
-
_arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
14728
|
-
;;
|
|
14729
|
-
ls)
|
|
14730
|
-
_arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
|
|
14731
|
-
;;
|
|
14732
|
-
clean)
|
|
14733
|
-
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
14734
|
-
;;
|
|
14735
|
-
remove|rm)
|
|
14736
|
-
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
|
|
14737
|
-
;;
|
|
14738
|
-
trigger-hook)
|
|
14739
|
-
_arguments "2:hook:(${hooks})"
|
|
14740
|
-
;;
|
|
14741
|
-
config)
|
|
14742
|
-
if (( CURRENT == 3 )); then
|
|
14743
|
-
_values 'config action' get set unset
|
|
14744
|
-
return
|
|
14745
|
-
fi
|
|
14724
|
+
if (( CURRENT == 2 )); then
|
|
14725
|
+
_describe 'command' commands
|
|
14726
|
+
return
|
|
14727
|
+
fi
|
|
14746
14728
|
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14729
|
+
case "\${words[2]}" in
|
|
14730
|
+
new)
|
|
14731
|
+
_arguments '--detached[create a detached worktree without a branch]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, \u2026)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
14732
|
+
;;
|
|
14733
|
+
init)
|
|
14734
|
+
_arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
|
|
14735
|
+
;;
|
|
14736
|
+
completion)
|
|
14737
|
+
_arguments '2:shell:(${shells})'
|
|
14738
|
+
;;
|
|
14739
|
+
pr)
|
|
14740
|
+
_arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
|
|
14741
|
+
;;
|
|
14742
|
+
back)
|
|
14743
|
+
_arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
|
|
14744
|
+
;;
|
|
14745
|
+
history)
|
|
14746
|
+
_arguments '--json[print history as JSON]'
|
|
14747
|
+
;;
|
|
14748
|
+
open)
|
|
14749
|
+
_arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)]:editor:' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
|
|
14750
|
+
;;
|
|
14751
|
+
go|jump)
|
|
14752
|
+
_arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
|
|
14753
|
+
;;
|
|
14754
|
+
root)
|
|
14755
|
+
_arguments '--print[print the resolved repository root path explicitly]'
|
|
14756
|
+
;;
|
|
14757
|
+
status)
|
|
14758
|
+
_arguments '--json[print repository and worktree health as JSON]'
|
|
14759
|
+
;;
|
|
14760
|
+
sync)
|
|
14761
|
+
_arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
14762
|
+
;;
|
|
14763
|
+
ls)
|
|
14764
|
+
_arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
|
|
14765
|
+
;;
|
|
14766
|
+
clean)
|
|
14767
|
+
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
14768
|
+
;;
|
|
14769
|
+
remove|rm)
|
|
14770
|
+
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
|
|
14771
|
+
;;
|
|
14772
|
+
trigger-hook)
|
|
14773
|
+
_arguments "2:hook:(${hooks})"
|
|
14774
|
+
;;
|
|
14775
|
+
warp)
|
|
14776
|
+
_arguments '(-n --new)'{-n,--new}'[create a new worktree in a registered repo]:branch:' '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
14777
|
+
;;
|
|
14778
|
+
config)
|
|
14779
|
+
if (( CURRENT == 3 )); then
|
|
14780
|
+
_values 'config action' get set unset
|
|
14781
|
+
return
|
|
14782
|
+
fi
|
|
14757
14783
|
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14784
|
+
case "\${words[3]}" in
|
|
14785
|
+
get|unset)
|
|
14786
|
+
_arguments '3:key:->config_keys'
|
|
14787
|
+
;;
|
|
14788
|
+
set)
|
|
14789
|
+
_arguments '3:key:->config_keys' '4:value: '
|
|
14790
|
+
;;
|
|
14791
|
+
esac
|
|
14792
|
+
;;
|
|
14793
|
+
esac
|
|
14768
14794
|
|
|
14769
|
-
|
|
14795
|
+
case "$state" in
|
|
14796
|
+
worktrees)
|
|
14797
|
+
worktree_branches=(\${(@f)$(__gji_worktree_branches)})
|
|
14798
|
+
_describe 'worktree branch' worktree_branches
|
|
14799
|
+
;;
|
|
14800
|
+
config_keys)
|
|
14801
|
+
_values 'config key' ${configKeys}
|
|
14802
|
+
;;
|
|
14803
|
+
esac`;
|
|
14770
14804
|
}
|
|
14771
14805
|
function escapeSingleQuotes(value) {
|
|
14772
14806
|
return value.replace(/'/g, `'\\''`);
|
|
@@ -14847,7 +14881,11 @@ function writeJson(stdout, value) {
|
|
|
14847
14881
|
}
|
|
14848
14882
|
|
|
14849
14883
|
// src/go.ts
|
|
14850
|
-
import { basename as
|
|
14884
|
+
import { basename as basename6 } from "node:path";
|
|
14885
|
+
|
|
14886
|
+
// src/warp.ts
|
|
14887
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
14888
|
+
import { basename as basename5, resolve as resolve5 } from "node:path";
|
|
14851
14889
|
|
|
14852
14890
|
// src/new.ts
|
|
14853
14891
|
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
@@ -14866,9 +14904,9 @@ var EDITORS = [
|
|
|
14866
14904
|
];
|
|
14867
14905
|
async function defaultSpawnEditor(cli, args) {
|
|
14868
14906
|
const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
|
|
14869
|
-
await new Promise((
|
|
14907
|
+
await new Promise((resolve6, reject) => {
|
|
14870
14908
|
child.once("error", reject);
|
|
14871
|
-
child.once("spawn",
|
|
14909
|
+
child.once("spawn", resolve6);
|
|
14872
14910
|
});
|
|
14873
14911
|
child.unref();
|
|
14874
14912
|
}
|
|
@@ -15083,7 +15121,7 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
|
|
|
15083
15121
|
}
|
|
15084
15122
|
}
|
|
15085
15123
|
async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
15086
|
-
await new Promise((
|
|
15124
|
+
await new Promise((resolve6, reject) => {
|
|
15087
15125
|
const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
|
|
15088
15126
|
child.stderr.on("data", (chunk) => {
|
|
15089
15127
|
stderr(chunk.toString());
|
|
@@ -15092,7 +15130,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
|
15092
15130
|
if (code !== 0) {
|
|
15093
15131
|
reject(new Error(`exited with code ${code}`));
|
|
15094
15132
|
} else {
|
|
15095
|
-
|
|
15133
|
+
resolve6();
|
|
15096
15134
|
}
|
|
15097
15135
|
});
|
|
15098
15136
|
child.on("error", (err) => {
|
|
@@ -15433,7 +15471,7 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
|
|
|
15433
15471
|
}
|
|
15434
15472
|
|
|
15435
15473
|
// src/repo-registry.ts
|
|
15436
|
-
import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
|
|
15474
|
+
import { mkdir as mkdir5, readFile as readFile3, realpath, writeFile as writeFile4 } from "node:fs/promises";
|
|
15437
15475
|
import { homedir as homedir4 } from "node:os";
|
|
15438
15476
|
import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
|
|
15439
15477
|
var REGISTRY_FILE_NAME = "repos.json";
|
|
@@ -15456,21 +15494,46 @@ async function loadRegistry(home = homedir4()) {
|
|
|
15456
15494
|
return [];
|
|
15457
15495
|
}
|
|
15458
15496
|
}
|
|
15497
|
+
async function canonicalizeRepoPath(repoPath) {
|
|
15498
|
+
try {
|
|
15499
|
+
return await realpath(repoPath);
|
|
15500
|
+
} catch {
|
|
15501
|
+
return resolve4(repoPath);
|
|
15502
|
+
}
|
|
15503
|
+
}
|
|
15459
15504
|
async function registerRepo(repoPath, home = homedir4()) {
|
|
15460
15505
|
const registryPath = REGISTRY_FILE_PATH(home);
|
|
15461
|
-
const existing = await loadRegistry(home);
|
|
15462
|
-
|
|
15506
|
+
const existing = await normalizeRegistryForWrite(await loadRegistry(home));
|
|
15507
|
+
const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
|
|
15508
|
+
if (existing.length > 0 && existing[0].path === canonicalRepoPath) return;
|
|
15463
15509
|
const entry = {
|
|
15464
15510
|
lastUsed: Date.now(),
|
|
15465
|
-
name: basename4(
|
|
15466
|
-
path:
|
|
15511
|
+
name: basename4(canonicalRepoPath),
|
|
15512
|
+
path: canonicalRepoPath
|
|
15467
15513
|
};
|
|
15468
|
-
const filtered = existing.filter((e2) => e2.path !==
|
|
15514
|
+
const filtered = existing.filter((e2) => e2.path !== canonicalRepoPath);
|
|
15469
15515
|
const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
|
|
15470
15516
|
await mkdir5(dirname6(registryPath), { recursive: true });
|
|
15471
15517
|
await writeFile4(registryPath, `${JSON.stringify(next, null, 2)}
|
|
15472
15518
|
`, "utf8");
|
|
15473
15519
|
}
|
|
15520
|
+
async function normalizeRegistryForWrite(entries) {
|
|
15521
|
+
const normalized = [];
|
|
15522
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
15523
|
+
for (const entry of entries) {
|
|
15524
|
+
const canonicalPath = await canonicalizeRepoPath(entry.path);
|
|
15525
|
+
if (seenPaths.has(canonicalPath)) {
|
|
15526
|
+
continue;
|
|
15527
|
+
}
|
|
15528
|
+
seenPaths.add(canonicalPath);
|
|
15529
|
+
normalized.push({
|
|
15530
|
+
...entry,
|
|
15531
|
+
name: basename4(canonicalPath),
|
|
15532
|
+
path: canonicalPath
|
|
15533
|
+
});
|
|
15534
|
+
}
|
|
15535
|
+
return normalized;
|
|
15536
|
+
}
|
|
15474
15537
|
function isRegistryEntry(value) {
|
|
15475
15538
|
return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "name" in value && typeof value.name === "string" && "lastUsed" in value && typeof value.lastUsed === "number";
|
|
15476
15539
|
}
|
|
@@ -15515,9 +15578,10 @@ async function runWarpNavigate(options) {
|
|
|
15515
15578
|
return 0;
|
|
15516
15579
|
}
|
|
15517
15580
|
async function runWarpNew(options, registry) {
|
|
15581
|
+
const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
|
|
15518
15582
|
let targetRepoRoot;
|
|
15519
|
-
if (
|
|
15520
|
-
targetRepoRoot =
|
|
15583
|
+
if (deduplicatedRegistry.length === 1) {
|
|
15584
|
+
targetRepoRoot = deduplicatedRegistry[0].path;
|
|
15521
15585
|
} else {
|
|
15522
15586
|
if (isHeadless()) {
|
|
15523
15587
|
options.stderr(
|
|
@@ -15527,7 +15591,7 @@ async function runWarpNew(options, registry) {
|
|
|
15527
15591
|
}
|
|
15528
15592
|
const choice = await ve({
|
|
15529
15593
|
message: "Create worktree in which repo?",
|
|
15530
|
-
options:
|
|
15594
|
+
options: deduplicatedRegistry.map((entry) => ({
|
|
15531
15595
|
value: entry.path,
|
|
15532
15596
|
label: entry.name,
|
|
15533
15597
|
hint: entry.path
|
|
@@ -15568,6 +15632,30 @@ async function runWarpNew(options, registry) {
|
|
|
15568
15632
|
await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
|
|
15569
15633
|
return 0;
|
|
15570
15634
|
}
|
|
15635
|
+
async function deduplicateRegistryForNew(registry) {
|
|
15636
|
+
const deduplicated = [];
|
|
15637
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
15638
|
+
for (const entry of registry) {
|
|
15639
|
+
const canonicalPath = await canonicalizeRepoPath2(entry.path);
|
|
15640
|
+
if (seenPaths.has(canonicalPath)) {
|
|
15641
|
+
continue;
|
|
15642
|
+
}
|
|
15643
|
+
seenPaths.add(canonicalPath);
|
|
15644
|
+
deduplicated.push({
|
|
15645
|
+
...entry,
|
|
15646
|
+
name: basename5(canonicalPath),
|
|
15647
|
+
path: canonicalPath
|
|
15648
|
+
});
|
|
15649
|
+
}
|
|
15650
|
+
return deduplicated;
|
|
15651
|
+
}
|
|
15652
|
+
async function canonicalizeRepoPath2(repoPath) {
|
|
15653
|
+
try {
|
|
15654
|
+
return await realpath2(repoPath);
|
|
15655
|
+
} catch {
|
|
15656
|
+
return resolve5(repoPath);
|
|
15657
|
+
}
|
|
15658
|
+
}
|
|
15571
15659
|
function findByQuery(items, query) {
|
|
15572
15660
|
const slashIdx = query.indexOf("/");
|
|
15573
15661
|
if (slashIdx !== -1) {
|
|
@@ -15712,7 +15800,7 @@ function createGoCommand(dependencies = {}) {
|
|
|
15712
15800
|
await runHook(
|
|
15713
15801
|
hooks.afterEnter,
|
|
15714
15802
|
resolvedPath,
|
|
15715
|
-
{ branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo:
|
|
15803
|
+
{ branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename6(repository.repoRoot) },
|
|
15716
15804
|
options.stderr
|
|
15717
15805
|
);
|
|
15718
15806
|
appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
|
|
@@ -16286,7 +16374,7 @@ function sortWorktrees(worktrees) {
|
|
|
16286
16374
|
|
|
16287
16375
|
// src/pr.ts
|
|
16288
16376
|
import { mkdir as mkdir7 } from "node:fs/promises";
|
|
16289
|
-
import { basename as
|
|
16377
|
+
import { basename as basename7, dirname as dirname8 } from "node:path";
|
|
16290
16378
|
import { execFile as execFile5 } from "node:child_process";
|
|
16291
16379
|
import { promisify as promisify6 } from "node:util";
|
|
16292
16380
|
var execFileAsync5 = promisify6(execFile5);
|
|
@@ -16388,7 +16476,7 @@ function createPrCommand(dependencies = {}) {
|
|
|
16388
16476
|
await runHook(
|
|
16389
16477
|
hooks.afterCreate,
|
|
16390
16478
|
worktreePath,
|
|
16391
|
-
{ branch: branchName, path: worktreePath, repo:
|
|
16479
|
+
{ branch: branchName, path: worktreePath, repo: basename7(repository.repoRoot) },
|
|
16392
16480
|
options.stderr
|
|
16393
16481
|
);
|
|
16394
16482
|
if (options.json) {
|
|
@@ -16461,7 +16549,7 @@ async function writeOutput2(worktreePath, stdout) {
|
|
|
16461
16549
|
}
|
|
16462
16550
|
|
|
16463
16551
|
// src/remove.ts
|
|
16464
|
-
import { basename as
|
|
16552
|
+
import { basename as basename8 } from "node:path";
|
|
16465
16553
|
var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
|
|
16466
16554
|
function createRemoveCommand(dependencies = {}) {
|
|
16467
16555
|
const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree2;
|
|
@@ -16526,7 +16614,7 @@ function createRemoveCommand(dependencies = {}) {
|
|
|
16526
16614
|
await runHook(
|
|
16527
16615
|
hooks.beforeRemove,
|
|
16528
16616
|
worktree.path,
|
|
16529
|
-
{ branch: worktree.branch ?? void 0, path: worktree.path, repo:
|
|
16617
|
+
{ branch: worktree.branch ?? void 0, path: worktree.path, repo: basename8(repository.repoRoot) },
|
|
16530
16618
|
options.stderr
|
|
16531
16619
|
);
|
|
16532
16620
|
try {
|
package/dist/repo-registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { GLOBAL_CONFIG_DIRECTORY } from './config.js';
|
|
@@ -24,22 +24,48 @@ export async function loadRegistry(home = homedir()) {
|
|
|
24
24
|
return [];
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
async function canonicalizeRepoPath(repoPath) {
|
|
28
|
+
try {
|
|
29
|
+
return await realpath(repoPath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return resolve(repoPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
export async function registerRepo(repoPath, home = homedir()) {
|
|
28
36
|
const registryPath = REGISTRY_FILE_PATH(home);
|
|
29
|
-
const existing = await loadRegistry(home);
|
|
37
|
+
const existing = await normalizeRegistryForWrite(await loadRegistry(home));
|
|
38
|
+
const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
|
|
30
39
|
// Skip write if this repo is already the most-recently-used entry (common case).
|
|
31
|
-
if (existing.length > 0 && existing[0].path ===
|
|
40
|
+
if (existing.length > 0 && existing[0].path === canonicalRepoPath)
|
|
32
41
|
return;
|
|
33
42
|
const entry = {
|
|
34
43
|
lastUsed: Date.now(),
|
|
35
|
-
name: basename(
|
|
36
|
-
path:
|
|
44
|
+
name: basename(canonicalRepoPath),
|
|
45
|
+
path: canonicalRepoPath,
|
|
37
46
|
};
|
|
38
|
-
const filtered = existing.filter((e) => e.path !==
|
|
47
|
+
const filtered = existing.filter((e) => e.path !== canonicalRepoPath);
|
|
39
48
|
const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
|
|
40
49
|
await mkdir(dirname(registryPath), { recursive: true });
|
|
41
50
|
await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
42
51
|
}
|
|
52
|
+
async function normalizeRegistryForWrite(entries) {
|
|
53
|
+
const normalized = [];
|
|
54
|
+
const seenPaths = new Set();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const canonicalPath = await canonicalizeRepoPath(entry.path);
|
|
57
|
+
if (seenPaths.has(canonicalPath)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
seenPaths.add(canonicalPath);
|
|
61
|
+
normalized.push({
|
|
62
|
+
...entry,
|
|
63
|
+
name: basename(canonicalPath),
|
|
64
|
+
path: canonicalPath,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
43
69
|
function isRegistryEntry(value) {
|
|
44
70
|
return (typeof value === 'object' &&
|
|
45
71
|
value !== null &&
|
package/dist/shell-completion.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { KNOWN_GLOBAL_CONFIG_KEYS } from './config.js';
|
|
1
2
|
const TOP_LEVEL_COMMANDS = [
|
|
2
3
|
{ name: 'new', description: 'create a new branch or detached linked worktree' },
|
|
3
4
|
{ name: 'init', description: 'print or install shell integration' },
|
|
4
5
|
{ name: 'completion', description: 'print shell completion definitions' },
|
|
5
6
|
{ name: 'pr', description: 'fetch a pull request into a linked worktree' },
|
|
7
|
+
{ name: 'back', description: 'navigate to the previously visited worktree' },
|
|
8
|
+
{ name: 'history', description: 'show navigation history' },
|
|
9
|
+
{ name: 'open', description: 'open the worktree in an editor' },
|
|
6
10
|
{ name: 'go', description: 'print or select a worktree path' },
|
|
11
|
+
{ name: 'jump', description: 'alias of go' },
|
|
7
12
|
{ name: 'root', description: 'print the main repository root path' },
|
|
8
13
|
{ name: 'status', description: 'summarize repository and worktree health' },
|
|
9
14
|
{ name: 'sync', description: 'fetch and update one or all worktrees' },
|
|
@@ -12,20 +17,12 @@ const TOP_LEVEL_COMMANDS = [
|
|
|
12
17
|
{ name: 'remove', description: 'remove a linked worktree and delete its branch when present' },
|
|
13
18
|
{ name: 'rm', description: 'alias of remove' },
|
|
14
19
|
{ name: 'trigger-hook', description: 'run a named hook in the current worktree' },
|
|
20
|
+
{ name: 'warp', description: 'jump to any worktree across all known repos' },
|
|
15
21
|
{ name: 'config', description: 'manage global config defaults' },
|
|
16
22
|
];
|
|
17
23
|
const SHELL_NAMES = ['bash', 'fish', 'zsh'];
|
|
18
24
|
const HOOK_NAMES = ['afterCreate', 'afterEnter', 'beforeRemove'];
|
|
19
|
-
const CONFIG_KEYS =
|
|
20
|
-
'branchPrefix',
|
|
21
|
-
'syncRemote',
|
|
22
|
-
'syncDefaultBranch',
|
|
23
|
-
'syncFiles',
|
|
24
|
-
'skipInstallPrompt',
|
|
25
|
-
'installSaveTarget',
|
|
26
|
-
'hooks',
|
|
27
|
-
'repos',
|
|
28
|
-
];
|
|
25
|
+
const CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
|
|
29
26
|
export function renderShellCompletion(shell) {
|
|
30
27
|
switch (shell) {
|
|
31
28
|
case 'bash':
|
|
@@ -59,7 +56,7 @@ _gji_completion() {
|
|
|
59
56
|
|
|
60
57
|
case "$command_name" in
|
|
61
58
|
new)
|
|
62
|
-
COMPREPLY=( $(compgen -W "--detached --dry-run --json --help" -- "$cur") )
|
|
59
|
+
COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
63
60
|
;;
|
|
64
61
|
init)
|
|
65
62
|
COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
|
|
@@ -70,7 +67,16 @@ _gji_completion() {
|
|
|
70
67
|
pr)
|
|
71
68
|
COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
|
|
72
69
|
;;
|
|
73
|
-
|
|
70
|
+
back)
|
|
71
|
+
COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
|
|
72
|
+
;;
|
|
73
|
+
history)
|
|
74
|
+
COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
|
|
75
|
+
;;
|
|
76
|
+
open)
|
|
77
|
+
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
|
|
78
|
+
;;
|
|
79
|
+
go|jump)
|
|
74
80
|
COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
|
|
75
81
|
;;
|
|
76
82
|
root)
|
|
@@ -94,6 +100,9 @@ _gji_completion() {
|
|
|
94
100
|
trigger-hook)
|
|
95
101
|
COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
|
|
96
102
|
;;
|
|
103
|
+
warp)
|
|
104
|
+
COMPREPLY=( $(compgen -W "-n --new --print --json --help" -- "$cur") )
|
|
105
|
+
;;
|
|
97
106
|
config)
|
|
98
107
|
if [ "$COMP_CWORD" -eq 2 ]; then
|
|
99
108
|
COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
|
|
@@ -149,6 +158,9 @@ complete -c gji -f
|
|
|
149
158
|
${commandLines}
|
|
150
159
|
|
|
151
160
|
complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
|
|
161
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
|
|
162
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
|
|
163
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
|
|
152
164
|
complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
|
|
153
165
|
complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
154
166
|
|
|
@@ -162,8 +174,17 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
|
|
|
162
174
|
complete -c gji -n '__fish_seen_subcommand_from pr' -l dry-run -d 'show what would be created without executing any git commands or writing files'
|
|
163
175
|
complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
164
176
|
|
|
165
|
-
complete -c gji -n '__fish_seen_subcommand_from
|
|
166
|
-
|
|
177
|
+
complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
|
|
178
|
+
|
|
179
|
+
complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
|
|
180
|
+
|
|
181
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, …)'
|
|
182
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
|
|
183
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
|
|
184
|
+
complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
185
|
+
|
|
186
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
|
|
187
|
+
complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
|
|
167
188
|
|
|
168
189
|
complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
|
|
169
190
|
|
|
@@ -187,6 +208,10 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
|
|
|
187
208
|
|
|
188
209
|
${hookLines}
|
|
189
210
|
|
|
211
|
+
complete -c gji -n '__fish_seen_subcommand_from warp' -s n -l new -d 'create a new worktree in a registered repo'
|
|
212
|
+
complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
|
|
213
|
+
complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
214
|
+
|
|
190
215
|
complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
|
|
191
216
|
${configKeyLines}`;
|
|
192
217
|
}
|
|
@@ -195,89 +220,99 @@ function renderZshCompletion() {
|
|
|
195
220
|
const configKeys = CONFIG_KEYS.join(' ');
|
|
196
221
|
const shells = SHELL_NAMES.join(' ');
|
|
197
222
|
const hooks = HOOK_NAMES.join(' ');
|
|
198
|
-
return
|
|
223
|
+
return `#compdef gji
|
|
224
|
+
|
|
225
|
+
__gji_worktree_branches() {
|
|
199
226
|
command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
|
|
200
227
|
}
|
|
201
228
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
local -a commands worktree_branches
|
|
205
|
-
|
|
206
|
-
commands=(
|
|
207
|
-
${commandLines}
|
|
208
|
-
)
|
|
229
|
+
local context state line
|
|
230
|
+
local -a commands worktree_branches
|
|
209
231
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
fi
|
|
232
|
+
commands=(
|
|
233
|
+
${commandLines}
|
|
234
|
+
)
|
|
214
235
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
init)
|
|
220
|
-
_arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
|
|
221
|
-
;;
|
|
222
|
-
completion)
|
|
223
|
-
_arguments '2:shell:(${shells})'
|
|
224
|
-
;;
|
|
225
|
-
pr)
|
|
226
|
-
_arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
|
|
227
|
-
;;
|
|
228
|
-
go)
|
|
229
|
-
_arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
|
|
230
|
-
;;
|
|
231
|
-
root)
|
|
232
|
-
_arguments '--print[print the resolved repository root path explicitly]'
|
|
233
|
-
;;
|
|
234
|
-
status)
|
|
235
|
-
_arguments '--json[print repository and worktree health as JSON]'
|
|
236
|
-
;;
|
|
237
|
-
sync)
|
|
238
|
-
_arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
239
|
-
;;
|
|
240
|
-
ls)
|
|
241
|
-
_arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
|
|
242
|
-
;;
|
|
243
|
-
clean)
|
|
244
|
-
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
245
|
-
;;
|
|
246
|
-
remove|rm)
|
|
247
|
-
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
|
|
248
|
-
;;
|
|
249
|
-
trigger-hook)
|
|
250
|
-
_arguments "2:hook:(${hooks})"
|
|
251
|
-
;;
|
|
252
|
-
config)
|
|
253
|
-
if (( CURRENT == 3 )); then
|
|
254
|
-
_values 'config action' get set unset
|
|
255
|
-
return
|
|
256
|
-
fi
|
|
236
|
+
if (( CURRENT == 2 )); then
|
|
237
|
+
_describe 'command' commands
|
|
238
|
+
return
|
|
239
|
+
fi
|
|
257
240
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
241
|
+
case "\${words[2]}" in
|
|
242
|
+
new)
|
|
243
|
+
_arguments '--detached[create a detached worktree without a branch]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
244
|
+
;;
|
|
245
|
+
init)
|
|
246
|
+
_arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
|
|
247
|
+
;;
|
|
248
|
+
completion)
|
|
249
|
+
_arguments '2:shell:(${shells})'
|
|
250
|
+
;;
|
|
251
|
+
pr)
|
|
252
|
+
_arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
|
|
253
|
+
;;
|
|
254
|
+
back)
|
|
255
|
+
_arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
|
|
256
|
+
;;
|
|
257
|
+
history)
|
|
258
|
+
_arguments '--json[print history as JSON]'
|
|
259
|
+
;;
|
|
260
|
+
open)
|
|
261
|
+
_arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, …)]:editor:' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
|
|
262
|
+
;;
|
|
263
|
+
go|jump)
|
|
264
|
+
_arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
|
|
265
|
+
;;
|
|
266
|
+
root)
|
|
267
|
+
_arguments '--print[print the resolved repository root path explicitly]'
|
|
268
|
+
;;
|
|
269
|
+
status)
|
|
270
|
+
_arguments '--json[print repository and worktree health as JSON]'
|
|
271
|
+
;;
|
|
272
|
+
sync)
|
|
273
|
+
_arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
274
|
+
;;
|
|
275
|
+
ls)
|
|
276
|
+
_arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
|
|
277
|
+
;;
|
|
278
|
+
clean)
|
|
279
|
+
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
|
|
280
|
+
;;
|
|
281
|
+
remove|rm)
|
|
282
|
+
_arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
|
|
283
|
+
;;
|
|
284
|
+
trigger-hook)
|
|
285
|
+
_arguments "2:hook:(${hooks})"
|
|
286
|
+
;;
|
|
287
|
+
warp)
|
|
288
|
+
_arguments '(-n --new)'{-n,--new}'[create a new worktree in a registered repo]:branch:' '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
289
|
+
;;
|
|
290
|
+
config)
|
|
291
|
+
if (( CURRENT == 3 )); then
|
|
292
|
+
_values 'config action' get set unset
|
|
293
|
+
return
|
|
294
|
+
fi
|
|
268
295
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
296
|
+
case "\${words[3]}" in
|
|
297
|
+
get|unset)
|
|
298
|
+
_arguments '3:key:->config_keys'
|
|
299
|
+
;;
|
|
300
|
+
set)
|
|
301
|
+
_arguments '3:key:->config_keys' '4:value: '
|
|
302
|
+
;;
|
|
303
|
+
esac
|
|
304
|
+
;;
|
|
305
|
+
esac
|
|
279
306
|
|
|
280
|
-
|
|
307
|
+
case "$state" in
|
|
308
|
+
worktrees)
|
|
309
|
+
worktree_branches=(\${(@f)$(__gji_worktree_branches)})
|
|
310
|
+
_describe 'worktree branch' worktree_branches
|
|
311
|
+
;;
|
|
312
|
+
config_keys)
|
|
313
|
+
_values 'config key' ${configKeys}
|
|
314
|
+
;;
|
|
315
|
+
esac`;
|
|
281
316
|
}
|
|
282
317
|
function escapeSingleQuotes(value) {
|
|
283
318
|
return value.replace(/'/g, `'\\''`);
|
package/dist/warp.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isCancel, select } from '@clack/prompts';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { basename, resolve } from 'node:path';
|
|
2
4
|
import { readWorktreeHealth } from './git.js';
|
|
3
5
|
import { isHeadless } from './headless.js';
|
|
4
6
|
import { appendHistory } from './history.js';
|
|
@@ -43,9 +45,10 @@ async function runWarpNavigate(options) {
|
|
|
43
45
|
return 0;
|
|
44
46
|
}
|
|
45
47
|
async function runWarpNew(options, registry) {
|
|
48
|
+
const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
|
|
46
49
|
let targetRepoRoot;
|
|
47
|
-
if (
|
|
48
|
-
targetRepoRoot =
|
|
50
|
+
if (deduplicatedRegistry.length === 1) {
|
|
51
|
+
targetRepoRoot = deduplicatedRegistry[0].path;
|
|
49
52
|
}
|
|
50
53
|
else {
|
|
51
54
|
if (isHeadless()) {
|
|
@@ -54,7 +57,7 @@ async function runWarpNew(options, registry) {
|
|
|
54
57
|
}
|
|
55
58
|
const choice = await select({
|
|
56
59
|
message: 'Create worktree in which repo?',
|
|
57
|
-
options:
|
|
60
|
+
options: deduplicatedRegistry.map((entry) => ({
|
|
58
61
|
value: entry.path,
|
|
59
62
|
label: entry.name,
|
|
60
63
|
hint: entry.path,
|
|
@@ -98,6 +101,31 @@ async function runWarpNew(options, registry) {
|
|
|
98
101
|
await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
|
|
99
102
|
return 0;
|
|
100
103
|
}
|
|
104
|
+
async function deduplicateRegistryForNew(registry) {
|
|
105
|
+
const deduplicated = [];
|
|
106
|
+
const seenPaths = new Set();
|
|
107
|
+
for (const entry of registry) {
|
|
108
|
+
const canonicalPath = await canonicalizeRepoPath(entry.path);
|
|
109
|
+
if (seenPaths.has(canonicalPath)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
seenPaths.add(canonicalPath);
|
|
113
|
+
deduplicated.push({
|
|
114
|
+
...entry,
|
|
115
|
+
name: basename(canonicalPath),
|
|
116
|
+
path: canonicalPath,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return deduplicated;
|
|
120
|
+
}
|
|
121
|
+
async function canonicalizeRepoPath(repoPath) {
|
|
122
|
+
try {
|
|
123
|
+
return await realpath(repoPath);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return resolve(repoPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
101
129
|
function findByQuery(items, query) {
|
|
102
130
|
const slashIdx = query.indexOf('/');
|
|
103
131
|
if (slashIdx !== -1) {
|
package/man/man1/gji-back.1
CHANGED
package/man/man1/gji-clean.1
CHANGED
package/man/man1/gji-config.1
CHANGED
package/man/man1/gji-go.1
CHANGED
package/man/man1/gji-history.1
CHANGED
package/man/man1/gji-init.1
CHANGED
package/man/man1/gji-ls.1
CHANGED
package/man/man1/gji-new.1
CHANGED
package/man/man1/gji-open.1
CHANGED
package/man/man1/gji-pr.1
CHANGED
package/man/man1/gji-remove.1
CHANGED
package/man/man1/gji-root.1
CHANGED
package/man/man1/gji-status.1
CHANGED
package/man/man1/gji-sync.1
CHANGED
package/man/man1/gji-warp.1
CHANGED
package/man/man1/gji.1
CHANGED