@solaqua/gji 0.15.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 +1 -1
- package/dist/cli/commands/clean.js +11 -2
- 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 +1 -56
- package/dist/cli/runtime/headless.d.ts +3 -3
- package/dist/cli/runtime/headless.js +6 -4
- package/dist/gji-bundle.mjs +500 -842
- 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 +7 -30
- package/dist/presentation/terminal/context-card.js +1 -1
- 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-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
|
}
|
|
@@ -9,7 +9,7 @@ export interface CleanCommandOptions {
|
|
|
9
9
|
json?: boolean;
|
|
10
10
|
paths?: string[];
|
|
11
11
|
stale?: boolean;
|
|
12
|
-
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">;
|
|
13
13
|
stderr: (chunk: string) => void;
|
|
14
14
|
stdout: (chunk: string) => void;
|
|
15
15
|
}
|
|
@@ -30,7 +30,8 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
30
30
|
const runtime = options.runtime ?? defaultCliDependencies;
|
|
31
31
|
const { readWorktreeHealth, isBranchMergedInto, resolveRemoteBase, runGit, } = runtime.git;
|
|
32
32
|
const { loadEffectiveConfig } = runtime.config;
|
|
33
|
-
const { releaseWorktreeSlot } = runtime.slots;
|
|
33
|
+
const { getWorktreeSlot, releaseWorktreeSlot } = runtime.slots;
|
|
34
|
+
const { extractHooks, runHook } = runtime.hooks;
|
|
34
35
|
const sourceDependencies = {
|
|
35
36
|
...runtime.repositoryContext,
|
|
36
37
|
...runtime.repositoryRegistry,
|
|
@@ -155,7 +156,8 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
155
156
|
emitError(options, message);
|
|
156
157
|
}
|
|
157
158
|
else {
|
|
158
|
-
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");
|
|
159
161
|
}
|
|
160
162
|
return 1;
|
|
161
163
|
}
|
|
@@ -225,6 +227,13 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
225
227
|
options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate\n`);
|
|
226
228
|
continue;
|
|
227
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);
|
|
228
237
|
try {
|
|
229
238
|
await removeWorktree(repoRoot, worktree.path);
|
|
230
239
|
}
|
|
@@ -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;
|
package/dist/cli/commands/new.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { access, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
1
|
+
import { access, mkdir, rmdir } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
import { confirm, isCancel, text } from "@clack/prompts";
|
|
4
4
|
import { resolveWorktreePath, validateBranchName, } from "../../domain/worktree/policy.js";
|
|
5
5
|
import { createBootstrapReporter } from "../../presentation/bootstrap/output.js";
|
|
@@ -56,7 +56,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
56
56
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
57
57
|
}
|
|
58
58
|
else {
|
|
59
|
-
options.stderr(`gji new: ${message} in non-interactive mode
|
|
59
|
+
options.stderr(`gji new: ${message} in non-interactive mode\n`);
|
|
60
60
|
}
|
|
61
61
|
return 1;
|
|
62
62
|
}
|
|
@@ -103,7 +103,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
103
103
|
if (options.force) {
|
|
104
104
|
if (!options.dryRun) {
|
|
105
105
|
try {
|
|
106
|
-
await runtime.git.
|
|
106
|
+
await runtime.git.runGitRetryingContention(repository.repoRoot, [
|
|
107
107
|
"worktree",
|
|
108
108
|
"remove",
|
|
109
109
|
"--force",
|
|
@@ -111,6 +111,8 @@ export function createNewCommand(dependencies = {}) {
|
|
|
111
111
|
]);
|
|
112
112
|
}
|
|
113
113
|
catch (err) {
|
|
114
|
+
// The worktree claims its path below, which an empty leftover would block.
|
|
115
|
+
await rmdir(worktreePath).catch(() => undefined);
|
|
114
116
|
if (!isNotRegisteredWorktreeError(err)) {
|
|
115
117
|
const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
|
|
116
118
|
if (options.json) {
|
|
@@ -141,7 +143,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
141
143
|
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
142
144
|
}
|
|
143
145
|
else {
|
|
144
|
-
options.stderr(`gji new: ${message} in non-interactive mode
|
|
146
|
+
options.stderr(`gji new: ${message} in non-interactive mode\n`);
|
|
145
147
|
options.stderr(`Hint: Use 'gji done ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
|
|
146
148
|
options.stderr(`Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks\n`);
|
|
147
149
|
}
|
|
@@ -229,13 +231,14 @@ export function createNewCommand(dependencies = {}) {
|
|
|
229
231
|
if (options.take && submoduleFiles.length > 0)
|
|
230
232
|
options.stderr(`Warning: submodule changes are not transferred: ${submoduleFiles.join(", ")}\n`);
|
|
231
233
|
const existingLocalBranch = await localBranchExists(repository.repoRoot, worktreeName, runtime);
|
|
232
|
-
const
|
|
233
|
-
!options.detached &&
|
|
234
|
-
!options.fromCurrent &&
|
|
235
|
-
!options.take &&
|
|
234
|
+
const createsBranch = !options.detached &&
|
|
236
235
|
options.mode !== "track" &&
|
|
237
236
|
options.mode !== "checkout" &&
|
|
238
237
|
!existingLocalBranch;
|
|
238
|
+
const shouldRefreshBase = createsBranch &&
|
|
239
|
+
!options.noFetch &&
|
|
240
|
+
!options.fromCurrent &&
|
|
241
|
+
!options.take;
|
|
239
242
|
const freshBaseRef = shouldRefreshBase
|
|
240
243
|
? await resolveFreshBaseRef(repository.repoRoot, config, options, promptForFetchFailure, runtime)
|
|
241
244
|
: undefined;
|
|
@@ -253,42 +256,21 @@ export function createNewCommand(dependencies = {}) {
|
|
|
253
256
|
return emitNewError(options, `could not stash changes safely: ${toExecMessage(error)}`);
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
|
-
const
|
|
257
|
-
? [
|
|
258
|
-
"worktree",
|
|
259
|
-
"add",
|
|
260
|
-
"--detach",
|
|
261
|
-
worktreePath,
|
|
262
|
-
...(takeStartPoint ? [takeStartPoint] : []),
|
|
263
|
-
]
|
|
264
|
-
: options.mode === "track"
|
|
265
|
-
? [
|
|
266
|
-
"worktree",
|
|
267
|
-
"add",
|
|
268
|
-
"--track",
|
|
269
|
-
"-b",
|
|
270
|
-
worktreeName,
|
|
271
|
-
worktreePath,
|
|
272
|
-
`${options.remote ?? "origin"}/${worktreeName}`,
|
|
273
|
-
]
|
|
274
|
-
: options.mode === "checkout" || existingLocalBranch
|
|
275
|
-
? ["worktree", "add", worktreePath, worktreeName]
|
|
276
|
-
: [
|
|
277
|
-
"worktree",
|
|
278
|
-
"add",
|
|
279
|
-
"-b",
|
|
280
|
-
worktreeName,
|
|
281
|
-
worktreePath,
|
|
282
|
-
...(takeStartPoint
|
|
283
|
-
? [takeStartPoint]
|
|
284
|
-
: startPoint
|
|
285
|
-
? [startPoint]
|
|
286
|
-
: freshBaseRef
|
|
287
|
-
? [freshBaseRef]
|
|
288
|
-
: []),
|
|
289
|
-
];
|
|
259
|
+
const addsNewBranch = createsBranch || (!options.detached && options.mode === "track");
|
|
290
260
|
try {
|
|
291
|
-
await runtime.git.
|
|
261
|
+
await runtime.git.addWorktree(repository.repoRoot, worktreePath, options.detached
|
|
262
|
+
? { detachedAt: takeStartPoint }
|
|
263
|
+
: addsNewBranch
|
|
264
|
+
? {
|
|
265
|
+
branch: worktreeName,
|
|
266
|
+
create: {
|
|
267
|
+
startPoint: options.mode === "track"
|
|
268
|
+
? `${options.remote ?? "origin"}/${worktreeName}`
|
|
269
|
+
: (takeStartPoint ?? startPoint ?? freshBaseRef),
|
|
270
|
+
track: options.mode === "track",
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
: { branch: worktreeName });
|
|
292
274
|
}
|
|
293
275
|
catch (error) {
|
|
294
276
|
if (stashSha)
|
|
@@ -297,17 +279,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
297
279
|
}
|
|
298
280
|
if (stashSha &&
|
|
299
281
|
!(await applyTakeStash(worktreePath, options.cwd, stashSha, !!options.copy, options.stderr, runtime.git.runGit))) {
|
|
300
|
-
|
|
301
|
-
await runtime.git.runGit(repository.repoRoot, [
|
|
302
|
-
"worktree",
|
|
303
|
-
"remove",
|
|
304
|
-
"--force",
|
|
305
|
-
worktreePath,
|
|
306
|
-
]);
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
/* rollback best effort */
|
|
310
|
-
}
|
|
282
|
+
await runtime.git.removeCreatedWorktree(repository.repoRoot, worktreePath, addsNewBranch ? worktreeName : undefined);
|
|
311
283
|
return emitNewError(options, `could not apply taken changes; your changes are safe in stash: ${stashSha}`);
|
|
312
284
|
}
|
|
313
285
|
if (options.task !== undefined) {
|
|
@@ -338,6 +310,17 @@ export function createNewCommand(dependencies = {}) {
|
|
|
338
310
|
syncFiles: bootstrap.syncFileFailures,
|
|
339
311
|
});
|
|
340
312
|
}
|
|
313
|
+
if (createsBranch) {
|
|
314
|
+
await runNewHooks({
|
|
315
|
+
branch: worktreeName,
|
|
316
|
+
config,
|
|
317
|
+
hookNames: ["after-branch-create"],
|
|
318
|
+
options,
|
|
319
|
+
path: worktreePath,
|
|
320
|
+
repository,
|
|
321
|
+
runtime,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
341
324
|
if (options.json) {
|
|
342
325
|
const navigation = createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, worktreeName);
|
|
343
326
|
const output = options.take
|
|
@@ -438,7 +421,7 @@ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
|
|
|
438
421
|
try {
|
|
439
422
|
let startPoint = baseBranch;
|
|
440
423
|
if (!options.noFetch) {
|
|
441
|
-
await runtime.git.
|
|
424
|
+
await runtime.git.runGitRetryingContention(repository.repoRoot, [
|
|
442
425
|
"fetch",
|
|
443
426
|
"--prune",
|
|
444
427
|
remote,
|
|
@@ -446,16 +429,29 @@ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
|
|
|
446
429
|
]);
|
|
447
430
|
startPoint = `${remote}/${baseBranch}`;
|
|
448
431
|
}
|
|
449
|
-
await runtime.git.
|
|
450
|
-
"switch",
|
|
451
|
-
"-c",
|
|
452
|
-
branchName,
|
|
432
|
+
await runtime.git.createBranch(repository.repoRoot, branchName, {
|
|
453
433
|
startPoint,
|
|
454
|
-
|
|
434
|
+
});
|
|
435
|
+
try {
|
|
436
|
+
await runtime.git.runGit(options.cwd, ["switch", branchName]);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
await runtime.git.deleteCreatedBranch(repository.repoRoot, branchName);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
455
442
|
}
|
|
456
443
|
catch (error) {
|
|
457
444
|
return emitNewError(options, `failed to create branch in the current worktree: ${toExecMessage(error)}`);
|
|
458
445
|
}
|
|
446
|
+
await runNewHooks({
|
|
447
|
+
branch: branchName,
|
|
448
|
+
config,
|
|
449
|
+
hookNames: ["after-branch-create", "after-switch"],
|
|
450
|
+
options,
|
|
451
|
+
path: repository.currentRoot,
|
|
452
|
+
repository,
|
|
453
|
+
runtime,
|
|
454
|
+
});
|
|
459
455
|
if (options.json) {
|
|
460
456
|
const navigation = createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), repository.currentRoot, branchName);
|
|
461
457
|
options.stdout(`${JSON.stringify({ ...navigation, baseBranch, branchOnly: true }, null, 2)}\n`);
|
|
@@ -467,6 +463,18 @@ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
|
|
|
467
463
|
}
|
|
468
464
|
return 0;
|
|
469
465
|
}
|
|
466
|
+
async function runNewHooks({ branch, config, hookNames, options, path, repository, runtime, }) {
|
|
467
|
+
const hooks = runtime.hooks.extractHooks(config);
|
|
468
|
+
const context = {
|
|
469
|
+
branch,
|
|
470
|
+
path,
|
|
471
|
+
repo: basename(repository.repoRoot),
|
|
472
|
+
slot: await runtime.slots.getWorktreeSlot(path),
|
|
473
|
+
};
|
|
474
|
+
for (const hookName of hookNames) {
|
|
475
|
+
await runtime.hooks.runHook(hooks[hookName], path, context, options.json ? () => undefined : options.stderr, options.json ? () => undefined : options.stdout);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
470
478
|
export const runNewCommand = createNewCommand();
|
|
471
479
|
async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFailure, runtime) {
|
|
472
480
|
const remote = runtime.config.resolveConfigString(config, "syncRemote") ?? "origin";
|
|
@@ -480,7 +488,7 @@ async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFail
|
|
|
480
488
|
throw new Error("the remote default branch is unknown");
|
|
481
489
|
}
|
|
482
490
|
baseBranch = remoteBase.branch;
|
|
483
|
-
await runtime.git.
|
|
491
|
+
await runtime.git.runGitRetryingContention(repoRoot, [
|
|
484
492
|
"fetch",
|
|
485
493
|
"--prune",
|
|
486
494
|
remote,
|
|
@@ -640,7 +648,7 @@ function generateBranchPlaceholderSuffix(random) {
|
|
|
640
648
|
}
|
|
641
649
|
return suffix;
|
|
642
650
|
}
|
|
643
|
-
function applyConfiguredBranchPrefix(branch, branchPrefix) {
|
|
651
|
+
export function applyConfiguredBranchPrefix(branch, branchPrefix) {
|
|
644
652
|
if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
|
|
645
653
|
return branch;
|
|
646
654
|
}
|
|
@@ -26,7 +26,7 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
26
26
|
return 1;
|
|
27
27
|
}
|
|
28
28
|
if (options.select && isHeadless()) {
|
|
29
|
-
options.stderr("gji open --select: selector is unavailable in non-interactive mode
|
|
29
|
+
options.stderr("gji open --select: selector is unavailable in non-interactive mode; pass a branch instead\n");
|
|
30
30
|
return 1;
|
|
31
31
|
}
|
|
32
32
|
const [worktrees, repository] = await Promise.all([
|