@solaqua/gji 0.14.0 → 0.16.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 +63 -120
- package/dist/cli/commands/back.d.ts +1 -1
- package/dist/cli/commands/back.js +1 -0
- package/dist/cli/commands/clean.d.ts +2 -1
- package/dist/cli/commands/clean.js +66 -18
- package/dist/cli/commands/done.js +3 -2
- package/dist/cli/commands/go.js +18 -5
- package/dist/cli/commands/init.js +0 -14
- package/dist/cli/commands/new.d.ts +2 -1
- package/dist/cli/commands/new.js +71 -63
- package/dist/cli/commands/open.js +1 -1
- package/dist/cli/commands/pr-open.js +2 -2
- package/dist/cli/commands/pr.js +17 -6
- package/dist/cli/commands/run-hook.js +4 -0
- package/dist/cli/commands/undo.js +10 -74
- package/dist/cli/dependencies.d.ts +5 -1
- package/dist/cli/dependencies.js +7 -2
- package/dist/cli/program.js +6 -56
- package/dist/cli/runtime/headless.d.ts +3 -3
- package/dist/cli/runtime/headless.js +6 -4
- package/dist/domain/worktree/path-filter.d.ts +13 -0
- package/dist/domain/worktree/path-filter.js +72 -0
- package/dist/domain/worktree/policy.d.ts +1 -0
- package/dist/domain/worktree/policy.js +1 -1
- package/dist/gji-bundle.mjs +738 -969
- package/dist/infrastructure/filesystem/locked-file.d.ts +4 -0
- package/dist/infrastructure/filesystem/locked-file.js +53 -0
- package/dist/infrastructure/git/runner.d.ts +7 -0
- package/dist/infrastructure/git/runner.js +35 -1
- package/dist/infrastructure/persistence/history.js +12 -10
- package/dist/infrastructure/persistence/slots.js +24 -21
- package/dist/infrastructure/process/hooks.js +2 -0
- package/dist/infrastructure/repository/registry.js +11 -41
- package/dist/infrastructure/worktree/lifecycle.d.ts +25 -0
- package/dist/infrastructure/worktree/lifecycle.js +120 -19
- package/dist/ports/git.d.ts +1 -0
- package/dist/ports/hooks.d.ts +7 -0
- package/dist/presentation/shell/completion.js +14 -32
- package/dist/presentation/terminal/context-card.js +1 -1
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +4 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.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-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +3 -3
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +1 -1
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji.1 +2 -12
- package/package.json +3 -2
- package/dist/cli/commands/remove.d.ts +0 -21
- package/dist/cli/commands/remove.js +0 -189
- package/dist/cli/commands/warp.d.ts +0 -28
- package/dist/cli/commands/warp.js +0 -123
- package/man/man1/gji-remove.1 +0 -19
- package/man/man1/gji-warp.1 +0 -16
package/README.md
CHANGED
|
@@ -1,157 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="website/static/img/logo-dark.svg" />
|
|
3
|
+
<img src="website/static/img/logo.svg" alt="gji branch mascot" width="72" height="68" />
|
|
4
|
+
</picture>
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# gji — Switch tasks. Keep your place.
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
own directory, dependencies, editor context, and terminal — so moving to a
|
|
7
|
-
pull request or experiment does not require stashing, checking out, or
|
|
8
|
-
reinstalling.
|
|
8
|
+
Open a new task or review a PR. Your current work stays right where you left it.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
`gji` is a Git worktree CLI for developers who keep more than one task open.
|
|
11
|
+
Each task gets a separate working directory and dependency installation, so
|
|
12
|
+
switching tasks doesn't mean packing up your changes first. Keep using your
|
|
13
|
+
usual Git commands, editor, and coding tools inside each worktree.
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
[Get started](#get-started) · [Documentation](https://gji.solaqua.dev/docs/intro) · [Is gji a fit?](https://gji.solaqua.dev/docs/comparison)
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
gji new feature/payment-refactor
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
If you prefer to reuse the current worktree, create a branch in place and
|
|
26
|
-
refresh the configured default branch first:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
gji new --branch-only feature/payment-refactor
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
This creates the new branch from the latest configured remote default branch
|
|
33
|
-
without creating another worktree or installing dependencies. The current
|
|
34
|
-
worktree must be clean; use `--no-fetch` to create from the local default
|
|
35
|
-
branch as-is.
|
|
36
|
-
|
|
37
|
-
## The problem
|
|
38
|
-
|
|
39
|
-
You are halfway through a feature when someone asks you to review a pull
|
|
40
|
-
request. Without worktrees, a quick review often becomes:
|
|
41
|
-
|
|
42
|
-
```text
|
|
43
|
-
stash → checkout → reinstall → review → checkout back → stash pop
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
With `gji`, the review gets its own workspace and your current task stays
|
|
47
|
-
untouched:
|
|
17
|
+
<a href="https://gji.solaqua.dev/#workflow">
|
|
18
|
+
<picture>
|
|
19
|
+
<source media="(prefers-reduced-motion: reduce)" srcset=".github/assets/readme-workflow.png" />
|
|
20
|
+
<img src=".github/assets/readme-workflow.gif" alt="Start a new task, open a PR in a separate worktree, then return to feature/payments with the same two modified files." width="960" height="336" />
|
|
21
|
+
</picture>
|
|
22
|
+
</a>
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
gji pr 1234
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
<table>
|
|
54
|
-
<tr>
|
|
55
|
-
<td width="50%" valign="top">
|
|
56
|
-
<strong>Before</strong><br />
|
|
57
|
-
<img src=".github/assets/readme-before.gif" alt="Traditional branch switching with stash and reinstall steps" />
|
|
58
|
-
</td>
|
|
59
|
-
<td width="50%" valign="top">
|
|
60
|
-
<strong>After</strong><br />
|
|
61
|
-
<img src=".github/assets/readme-after.gif" alt="Creating an isolated pull request worktree with gji" />
|
|
62
|
-
</td>
|
|
63
|
-
</tr>
|
|
64
|
-
</table>
|
|
65
|
-
|
|
66
|
-
## Remember four commands
|
|
67
|
-
|
|
68
|
-
```sh
|
|
69
|
-
gji new # interactively start a task
|
|
70
|
-
gji pr 1234 # open a pull request in isolation
|
|
71
|
-
gji go main # jump to another worktree
|
|
72
|
-
gji done feature/payment-refactor # finish and clean up a task
|
|
73
|
-
```
|
|
24
|
+
*Illustrated workflow. Shell integration is enabled; setup time is omitted.*
|
|
74
25
|
|
|
75
|
-
|
|
76
|
-
selected worktree automatically.
|
|
26
|
+
## Get started
|
|
77
27
|
|
|
78
|
-
|
|
28
|
+
You need **Node.js 18+**, Git with worktree support, and **zsh, bash, or fish**
|
|
29
|
+
for automatic directory changes.
|
|
79
30
|
|
|
80
31
|
```sh
|
|
81
32
|
npm install -g @solaqua/gji
|
|
82
33
|
gji init
|
|
83
34
|
```
|
|
84
35
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
wizard, then run:
|
|
36
|
+
Restart your shell, or load the shell configuration file shown by the wizard.
|
|
37
|
+
Then, inside a repository you already work in:
|
|
88
38
|
|
|
89
39
|
```sh
|
|
90
40
|
gji doctor
|
|
41
|
+
gji new feature/try-gji
|
|
42
|
+
gji back
|
|
91
43
|
```
|
|
92
44
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
## A typical day
|
|
45
|
+
You just opened a separate worktree and returned to your previous one. Any
|
|
46
|
+
uncommitted files you left there stay there. New worktrees install dependencies
|
|
47
|
+
automatically when a supported lockfile is detected, so the first setup can take
|
|
48
|
+
time. Use `--no-install` to skip it for one task.
|
|
98
49
|
|
|
99
|
-
|
|
100
|
-
# Start feature work
|
|
101
|
-
gji new
|
|
102
|
-
|
|
103
|
-
# Check what is active
|
|
104
|
-
gji
|
|
50
|
+
[Installation and shell setup](https://gji.solaqua.dev/docs/installation) · [Complete the first-worktree walkthrough](https://gji.solaqua.dev/docs/quick-start)
|
|
105
51
|
|
|
106
|
-
|
|
107
|
-
gji pr 1234
|
|
52
|
+
## Keep several tasks open
|
|
108
53
|
|
|
109
|
-
|
|
110
|
-
gji go feature/payment-refactor
|
|
111
|
-
gji back
|
|
54
|
+
For example, while working on `feature/payments`:
|
|
112
55
|
|
|
113
|
-
|
|
114
|
-
gji
|
|
115
|
-
gji
|
|
56
|
+
```sh
|
|
57
|
+
gji new feature/search # give another task its own directory
|
|
58
|
+
gji pr 1234 # open a real PR from your repository
|
|
59
|
+
gji go feature/payments # return to the changes you left open
|
|
116
60
|
```
|
|
117
61
|
|
|
118
|
-
|
|
62
|
+
Replace `1234` with a GitHub pull request in your repository. Each task stays in
|
|
63
|
+
its own worktree. Navigation moves your shell; it doesn't move changes between
|
|
64
|
+
branches or keep files synchronized.
|
|
119
65
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
66
|
+
| When you want to… | Run |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Start a task | `gji new` |
|
|
69
|
+
| Review a PR locally | `gji pr 1234` |
|
|
70
|
+
| Find an open task | `gji` or `gji go` |
|
|
71
|
+
| Return to the last worktree | `gji back` |
|
|
72
|
+
| Open the current worktree in an editor | `gji open` |
|
|
73
|
+
| Finish a linked worktree | `gji done` |
|
|
125
74
|
|
|
126
|
-
|
|
75
|
+
With `gji go`, press **Tab** to browse all registered repositories. Worktrees
|
|
76
|
+
have predictable paths: `../worktrees/<repo>/<branch>`.
|
|
127
77
|
|
|
128
|
-
|
|
78
|
+
## Make the next worktree ready for your project
|
|
129
79
|
|
|
130
|
-
- [
|
|
131
|
-
- [
|
|
132
|
-
- [
|
|
133
|
-
- [
|
|
134
|
-
- [
|
|
135
|
-
- [
|
|
136
|
-
- [Comparison](https://gji.solaqua.dev/docs/comparison) — `gji` vs `git worktree`, `lazygit`, and `ghq`
|
|
137
|
-
- [FAQ and troubleshooting](https://gji.solaqua.dev/docs/faq) — common questions and fixes
|
|
80
|
+
- [Local files and defaults](https://gji.solaqua.dev/docs/configuration) — copy `.env.local`, choose an editor, or adjust dependency setup.
|
|
81
|
+
- [Hooks](https://gji.solaqua.dev/docs/hooks) — run project-specific setup after the built-in dependency install.
|
|
82
|
+
- [Daily workflow](https://gji.solaqua.dev/docs/daily-workflow) — review, experiment, navigate, and open editors.
|
|
83
|
+
- [Sync and cleanup](https://gji.solaqua.dev/docs/sync-and-cleanup) — rebase active worktrees and remove finished ones. Cleanup can delete branches; undo does not recover uncommitted files.
|
|
84
|
+
- [Command reference](https://gji.solaqua.dev/docs/commands) — options, JSON output, and scripting.
|
|
85
|
+
- [Troubleshooting](https://gji.solaqua.dev/docs/troubleshooting) — recover from setup, navigation, or install problems.
|
|
138
86
|
|
|
139
|
-
|
|
87
|
+
## Contributing
|
|
140
88
|
|
|
141
89
|
```sh
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
90
|
+
pnpm install
|
|
91
|
+
pnpm test
|
|
92
|
+
pnpm build
|
|
145
93
|
```
|
|
146
94
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
If `gji` has saved you from a `git stash` spiral, a ⭐ on
|
|
153
|
-
[GitHub](https://github.com/sjquant/gji) helps other developers find it.
|
|
95
|
+
The CLI source is in `src/`. See [website/README.md](website/README.md) for site
|
|
96
|
+
and documentation development.
|
|
154
97
|
|
|
155
98
|
## License
|
|
156
99
|
|
|
157
|
-
MIT
|
|
100
|
+
[MIT](LICENSE)
|
|
@@ -9,7 +9,7 @@ export interface BackCommandOptions {
|
|
|
9
9
|
commandName?: string;
|
|
10
10
|
outputEnv?: string;
|
|
11
11
|
print?: boolean;
|
|
12
|
-
runtime?: CliRuntime<"config" | "historyStore" | "hooks" | "repositoryContext">;
|
|
12
|
+
runtime?: CliRuntime<"config" | "historyStore" | "hooks" | "repositoryContext" | "slots">;
|
|
13
13
|
stderr: (chunk: string) => void;
|
|
14
14
|
stdout: (chunk: string) => void;
|
|
15
15
|
}
|
|
@@ -7,8 +7,9 @@ export interface CleanCommandOptions {
|
|
|
7
7
|
dryRun?: boolean;
|
|
8
8
|
force?: boolean;
|
|
9
9
|
json?: boolean;
|
|
10
|
+
paths?: string[];
|
|
10
11
|
stale?: boolean;
|
|
11
|
-
runtime?: CliRuntime<"git" | "config" | "configStore" | "slots" | "repositoryContext" | "repositoryRegistry" | "worktrees" | "worktreeInfo" | "worktreeLifecycle" | "worktreeCatalog">;
|
|
12
|
+
runtime?: CliRuntime<"git" | "config" | "configStore" | "hooks" | "slots" | "repositoryContext" | "repositoryRegistry" | "worktrees" | "worktreeInfo" | "worktreeLifecycle" | "worktreeCatalog">;
|
|
12
13
|
stderr: (chunk: string) => void;
|
|
13
14
|
stdout: (chunk: string) => void;
|
|
14
15
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
1
3
|
import { confirm, isCancel } from "@clack/prompts";
|
|
2
4
|
import { loadLinkedWorktrees, } from "../../application/worktree/catalog.js";
|
|
3
5
|
import { deduplicateWorktreeSources, listRegisteredWorktreeSources, } from "../../application/worktree/sources.js";
|
|
4
6
|
import { mapWithConcurrency } from "../../domain/shared/concurrency.js";
|
|
7
|
+
import { createWorktreePathFilter, escapePathPattern, findGlobIndex, unescapePathPattern, } from "../../domain/worktree/path-filter.js";
|
|
8
|
+
import { expandTildeInPath } from "../../domain/worktree/policy.js";
|
|
5
9
|
import { buildWorktreePromptEntries, promptForMultipleWorktrees, } from "../../presentation/worktree/picker.js";
|
|
6
10
|
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "../../presentation/worktree/prompts.js";
|
|
7
11
|
import { defaultCliDependencies, } from "../dependencies.js";
|
|
@@ -26,7 +30,8 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
26
30
|
const runtime = options.runtime ?? defaultCliDependencies;
|
|
27
31
|
const { readWorktreeHealth, isBranchMergedInto, resolveRemoteBase, runGit, } = runtime.git;
|
|
28
32
|
const { loadEffectiveConfig } = runtime.config;
|
|
29
|
-
const { releaseWorktreeSlot } = runtime.slots;
|
|
33
|
+
const { getWorktreeSlot, releaseWorktreeSlot } = runtime.slots;
|
|
34
|
+
const { extractHooks, runHook } = runtime.hooks;
|
|
30
35
|
const sourceDependencies = {
|
|
31
36
|
...runtime.repositoryContext,
|
|
32
37
|
...runtime.repositoryRegistry,
|
|
@@ -35,8 +40,27 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
35
40
|
const { formatLastCommit, formatUpstreamState, readWorktreeInfos, serializeWorktreeInfo, } = runtime.worktreeInfo;
|
|
36
41
|
const { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, removeWorktree, } = runtime.worktreeLifecycle;
|
|
37
42
|
const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd, sourceDependencies);
|
|
43
|
+
const pathPatterns = await canonicalizePathPatterns(options.cwd, options.paths ?? []);
|
|
44
|
+
const matchesPathFilter = createWorktreePathFilter(pathPatterns);
|
|
45
|
+
// An explicit path may point at another registered repository, so
|
|
46
|
+
// --path starts in the all-repositories scope.
|
|
47
|
+
const searchAllRepositories = pathPatterns.length > 0;
|
|
48
|
+
let allSourceCount = 0;
|
|
49
|
+
const emitNoCandidates = (sourceCount) => {
|
|
50
|
+
if (pathPatterns.length > 0 && sourceCount === 0) {
|
|
51
|
+
emitError(options, "No linked worktrees match --path");
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (options.stale) {
|
|
55
|
+
emitNoStaleCandidates(options);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
emitError(options, "No linked worktrees to clean");
|
|
59
|
+
return 1;
|
|
60
|
+
};
|
|
38
61
|
const currentSources = linkedWorktrees
|
|
39
|
-
.filter((worktree) => worktree.path !== repository.currentRoot
|
|
62
|
+
.filter((worktree) => worktree.path !== repository.currentRoot &&
|
|
63
|
+
matchesPathFilter(worktree.path))
|
|
40
64
|
.map((worktree) => ({
|
|
41
65
|
repoName: repository.repoName,
|
|
42
66
|
repoRoot: repository.repoRoot,
|
|
@@ -77,7 +101,9 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
77
101
|
...registered.sources,
|
|
78
102
|
]).filter((source) => source.repoRoot !== undefined &&
|
|
79
103
|
source.worktree.path !== source.repoRoot &&
|
|
80
|
-
source.worktree.path !== repository.currentRoot
|
|
104
|
+
source.worktree.path !== repository.currentRoot &&
|
|
105
|
+
matchesPathFilter(source.worktree.path));
|
|
106
|
+
allSourceCount = sources.length;
|
|
81
107
|
const otherRepositorySources = sources.filter((source) => source.repoRoot !== repository.repoRoot);
|
|
82
108
|
const otherRepositoryCandidates = await resolveCleanupCandidates({
|
|
83
109
|
sources: otherRepositorySources,
|
|
@@ -110,28 +136,19 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
110
136
|
};
|
|
111
137
|
},
|
|
112
138
|
};
|
|
113
|
-
if (
|
|
139
|
+
if (searchAllRepositories ||
|
|
140
|
+
(interactiveSelection && currentCandidates.length === 0)) {
|
|
114
141
|
const loadedAllCandidates = await loadAllCandidates();
|
|
115
142
|
if (loadedAllCandidates.length === 0) {
|
|
116
|
-
|
|
117
|
-
emitNoStaleCandidates(options);
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
emitError(options, "No linked worktrees to clean");
|
|
121
|
-
return 1;
|
|
143
|
+
return emitNoCandidates(allSourceCount);
|
|
122
144
|
}
|
|
123
145
|
currentScope = false;
|
|
124
146
|
activeCandidates = loadedAllCandidates;
|
|
125
147
|
scope.label = "all repositories";
|
|
126
148
|
scope.toggleLabel = "current repository";
|
|
127
149
|
}
|
|
128
|
-
if (currentCandidates.length === 0
|
|
129
|
-
|
|
130
|
-
emitNoStaleCandidates(options);
|
|
131
|
-
return 0;
|
|
132
|
-
}
|
|
133
|
-
emitError(options, "No linked worktrees to clean");
|
|
134
|
-
return 1;
|
|
150
|
+
else if (currentCandidates.length === 0) {
|
|
151
|
+
return emitNoCandidates(currentSources.length);
|
|
135
152
|
}
|
|
136
153
|
if (!options.dryRun && !options.force && (options.json || isHeadless())) {
|
|
137
154
|
const message = "--force is required";
|
|
@@ -139,7 +156,8 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
139
156
|
emitError(options, message);
|
|
140
157
|
}
|
|
141
158
|
else {
|
|
142
|
-
options.stderr(`gji clean: ${message} in non-interactive mode
|
|
159
|
+
options.stderr(`gji clean: ${message} in non-interactive mode\n`);
|
|
160
|
+
options.stderr("Hint: --force removes every candidate; use --dry-run to preview them first\n");
|
|
143
161
|
}
|
|
144
162
|
return 1;
|
|
145
163
|
}
|
|
@@ -209,6 +227,13 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
209
227
|
options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate\n`);
|
|
210
228
|
continue;
|
|
211
229
|
}
|
|
230
|
+
const hooks = extractHooks(await loadEffectiveConfig(repoRoot));
|
|
231
|
+
await runHook(hooks["before-remove"], worktree.path, {
|
|
232
|
+
branch: worktree.branch ?? undefined,
|
|
233
|
+
path: worktree.path,
|
|
234
|
+
repo: basename(repoRoot),
|
|
235
|
+
slot: await getWorktreeSlot(worktree.path),
|
|
236
|
+
}, options.stderr, options.json ? () => undefined : options.stdout);
|
|
212
237
|
try {
|
|
213
238
|
await removeWorktree(repoRoot, worktree.path);
|
|
214
239
|
}
|
|
@@ -314,6 +339,29 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
314
339
|
};
|
|
315
340
|
}
|
|
316
341
|
export const runCleanCommand = createCleanCommand();
|
|
342
|
+
async function canonicalizePathPatterns(cwd, patterns) {
|
|
343
|
+
return Promise.all(patterns.map((pattern) => canonicalizeLiteralPrefix(resolve(cwd, expandTildeInPath(pattern)))));
|
|
344
|
+
}
|
|
345
|
+
// Git records canonical worktree paths, so resolve symlinks in the pattern's
|
|
346
|
+
// longest existing literal prefix before matching.
|
|
347
|
+
async function canonicalizeLiteralPrefix(pattern) {
|
|
348
|
+
const globIndex = findGlobIndex(pattern);
|
|
349
|
+
let prefix = globIndex === -1 ? pattern : dirname(`${pattern.slice(0, globIndex)}x`);
|
|
350
|
+
let suffix = pattern.slice(prefix.length);
|
|
351
|
+
for (;;) {
|
|
352
|
+
try {
|
|
353
|
+
const canonicalPrefix = escapePathPattern(await realpath(unescapePathPattern(prefix)));
|
|
354
|
+
return `${canonicalPrefix}${suffix}`.replace(/^\/\//, "/");
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
const parent = dirname(prefix);
|
|
358
|
+
if (parent === prefix)
|
|
359
|
+
return pattern;
|
|
360
|
+
suffix = join("/", basename(prefix), suffix);
|
|
361
|
+
prefix = parent;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
317
365
|
async function resolveCleanupCandidates(options, dependencies) {
|
|
318
366
|
const { sources, stale, stderr, signal } = options;
|
|
319
367
|
const grouped = new Map();
|
|
@@ -10,7 +10,7 @@ export async function runDoneCommand(options) {
|
|
|
10
10
|
const { readWorktreeHealth, isBranchMergedInto, resolveRemoteDefaultBranch, runGit, } = runtime.git;
|
|
11
11
|
const { loadEffectiveConfig, resolveConfigString } = runtime.config;
|
|
12
12
|
const { loadHistory } = runtime.historyStore;
|
|
13
|
-
const { releaseWorktreeSlot } = runtime.slots;
|
|
13
|
+
const { getWorktreeSlot, releaseWorktreeSlot } = runtime.slots;
|
|
14
14
|
const { extractHooks, runHook } = runtime.hooks;
|
|
15
15
|
const { detectRepository } = runtime.repositoryContext;
|
|
16
16
|
const { listWorktrees } = runtime.worktrees;
|
|
@@ -69,7 +69,8 @@ export async function runDoneCommand(options) {
|
|
|
69
69
|
branch: target.branch ?? undefined,
|
|
70
70
|
path: target.path,
|
|
71
71
|
repo: basename(repository.repoRoot),
|
|
72
|
-
|
|
72
|
+
slot: await getWorktreeSlot(target.path),
|
|
73
|
+
}, options.stderr, options.json ? () => undefined : options.stdout);
|
|
73
74
|
try {
|
|
74
75
|
await removeWorktree(repository.repoRoot, target.path);
|
|
75
76
|
}
|
package/dist/cli/commands/go.js
CHANGED
|
@@ -10,7 +10,7 @@ import { buildWorktreePromptEntries, promptForSingleWorktree, } from "../../pres
|
|
|
10
10
|
import { defaultCliDependencies, withPullRequestQueries, } from "../dependencies.js";
|
|
11
11
|
import { isHeadless } from "../runtime/headless.js";
|
|
12
12
|
import { runBackCommand } from "./back.js";
|
|
13
|
-
import { runNewCommand } from "./new.js";
|
|
13
|
+
import { applyConfiguredBranchPrefix, runNewCommand } from "./new.js";
|
|
14
14
|
import { runPrCommand } from "./pr.js";
|
|
15
15
|
const GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
|
|
16
16
|
export function createGoCommand(dependencies = {}) {
|
|
@@ -42,7 +42,7 @@ export function createGoCommand(dependencies = {}) {
|
|
|
42
42
|
: [];
|
|
43
43
|
if (!options.branch) {
|
|
44
44
|
if (options.json || isHeadless()) {
|
|
45
|
-
return emitError(options, "branch argument is required in non-interactive mode
|
|
45
|
+
return emitError(options, "branch argument is required in non-interactive mode");
|
|
46
46
|
}
|
|
47
47
|
let skippedRegisteredRepos = 0;
|
|
48
48
|
let registeredSources = null;
|
|
@@ -150,8 +150,8 @@ async function handleGoBranchResolution(options, resolution, confirmBranchCreati
|
|
|
150
150
|
case "create":
|
|
151
151
|
return createExistingBranchWorktree(options, confirmBranchCreation, resolution.repository, resolution.branch, resolution.mode, resolution.remote, runtime);
|
|
152
152
|
case "pull-request":
|
|
153
|
-
if (options.json ||
|
|
154
|
-
return emitError(options, "PR navigation creates a worktree; use `gji pr <ref>`
|
|
153
|
+
if (options.json || options.print) {
|
|
154
|
+
return emitError(options, "PR navigation creates a worktree; use `gji pr <ref>`");
|
|
155
155
|
}
|
|
156
156
|
return runPrCommand({
|
|
157
157
|
cwd: resolution.repository.repoRoot,
|
|
@@ -197,7 +197,8 @@ function deduplicateSources(sources) {
|
|
|
197
197
|
}
|
|
198
198
|
async function createExistingBranchWorktree(options, confirmBranchCreation, repository, branch, mode, remote, runtime) {
|
|
199
199
|
if (options.json || isHeadless() || options.print) {
|
|
200
|
-
|
|
200
|
+
const command = await resolveNonInteractiveCreateCommand(repository, branch, mode, remote, runtime ?? defaultCliDependencies);
|
|
201
|
+
return emitError(options, `branch "${branch}" exists but has no worktree; run ${command} to create one`);
|
|
201
202
|
}
|
|
202
203
|
if (!(await confirmBranchCreation(branch))) {
|
|
203
204
|
options.stderr("Aborted\n");
|
|
@@ -214,6 +215,18 @@ async function createExistingBranchWorktree(options, confirmBranchCreation, repo
|
|
|
214
215
|
runtime,
|
|
215
216
|
});
|
|
216
217
|
}
|
|
218
|
+
async function resolveNonInteractiveCreateCommand(repository, branch, mode, remote, runtime) {
|
|
219
|
+
if (mode === "track") {
|
|
220
|
+
return `git worktree add --track -b ${branch} <path> ${remote ?? "origin"}/${branch}`;
|
|
221
|
+
}
|
|
222
|
+
// gji new applies branchPrefix, so it would create a different branch.
|
|
223
|
+
const config = await runtime.config
|
|
224
|
+
.loadEffectiveConfig(repository.repoRoot)
|
|
225
|
+
.catch(() => null);
|
|
226
|
+
return applyConfiguredBranchPrefix(branch, config?.branchPrefix) === branch
|
|
227
|
+
? `gji new ${branch}`
|
|
228
|
+
: `git worktree add <path> ${branch}`;
|
|
229
|
+
}
|
|
217
230
|
async function navigateToExistingWorktree(options, path, worktree, runtime) {
|
|
218
231
|
const repository = await runtime.repositoryContext.detectRepository(path);
|
|
219
232
|
if (options.json) {
|
|
@@ -45,13 +45,6 @@ const SHELL_WRAPPED_COMMANDS = [
|
|
|
45
45
|
names: ["root"],
|
|
46
46
|
tempPrefix: "gji-root",
|
|
47
47
|
},
|
|
48
|
-
{
|
|
49
|
-
bypassOptions: ["--help", "-h", "--json"],
|
|
50
|
-
commandName: "remove",
|
|
51
|
-
envVar: "GJI_REMOVE_OUTPUT_FILE",
|
|
52
|
-
names: ["remove", "rm"],
|
|
53
|
-
tempPrefix: "gji-remove",
|
|
54
|
-
},
|
|
55
48
|
{
|
|
56
49
|
bypassOptions: ["--help", "-h", "--json"],
|
|
57
50
|
commandName: "done",
|
|
@@ -59,13 +52,6 @@ const SHELL_WRAPPED_COMMANDS = [
|
|
|
59
52
|
names: ["done"],
|
|
60
53
|
tempPrefix: "gji-done",
|
|
61
54
|
},
|
|
62
|
-
{
|
|
63
|
-
bypassOptions: ["--print", "--json", "--help", "-h"],
|
|
64
|
-
commandName: "warp",
|
|
65
|
-
envVar: "GJI_WARP_OUTPUT_FILE",
|
|
66
|
-
names: ["warp"],
|
|
67
|
-
tempPrefix: "gji-warp",
|
|
68
|
-
},
|
|
69
55
|
];
|
|
70
56
|
export async function runInitCommand(options) {
|
|
71
57
|
const runtime = options.runtime ?? defaultCliDependencies;
|
|
@@ -26,7 +26,7 @@ export interface NewCommandOptions {
|
|
|
26
26
|
task?: string;
|
|
27
27
|
runtime?: NewRuntime;
|
|
28
28
|
}
|
|
29
|
-
type NewRuntime = CliRuntime<"bootstrap" | "config" | "git" | "history" | "integrations" | "repositoryContext" | "tasks">;
|
|
29
|
+
type NewRuntime = CliRuntime<"bootstrap" | "config" | "git" | "history" | "hooks" | "integrations" | "repositoryContext" | "slots" | "tasks">;
|
|
30
30
|
export interface NewCommandDependencies {
|
|
31
31
|
createBranchPlaceholder: () => string;
|
|
32
32
|
promptForBranch: (placeholder: string) => Promise<string | null>;
|
|
@@ -38,3 +38,4 @@ export interface NewCommandDependencies {
|
|
|
38
38
|
export declare function createNewCommand(dependencies?: Partial<NewCommandDependencies>): (options: NewCommandOptions) => Promise<number>;
|
|
39
39
|
export declare const runNewCommand: (options: NewCommandOptions) => Promise<number>;
|
|
40
40
|
export declare function generateBranchPlaceholder(random?: () => number): string;
|
|
41
|
+
export declare function applyConfiguredBranchPrefix(branch: string, branchPrefix: unknown): string;
|