@n-seiji/nuthatch 0.1.7 → 0.2.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.ja.md +21 -21
- package/README.md +21 -21
- package/dist/{clean-n2f9z8n3.js → clean-dkrfmz85.js} +24 -25
- package/dist/cli-26d2drt6.js +30 -0
- package/dist/{cli-2bhb9p0x.js → cli-77yanq37.js} +3 -1
- package/dist/{cli-ygt2w4e1.js → cli-9y31xrsx.js} +20 -28
- package/dist/cli-a56b2ar0.js +20 -0
- package/dist/{cli-g72ra4e5.js → cli-b0tk5wcx.js} +1 -19
- package/dist/{cli-pqd2qe7v.js → cli-btqjpf1q.js} +1 -1
- package/dist/{cli-71jgs14h.js → cli-fhta3tr8.js} +15 -1
- package/dist/{cli-7jg5qqs9.js → cli-fqz7emfm.js} +12 -12
- package/dist/{cli-3ya15je0.js → cli-mfz1rx2x.js} +6 -33
- package/dist/{cli-qrxgkskr.js → cli-ph0gp56k.js} +6 -21
- package/dist/{cli-pick-w9qk71fy.js → cli-pick-wdtja21m.js} +60 -151
- package/dist/{cli-23z98dw1.js → cli-r39xsdym.js} +17 -30
- package/dist/cli-s5qcjcwk.js +25 -0
- package/dist/{cli-update-7exha1em.js → cli-update-kepe7h0z.js} +82 -334
- package/dist/{cli-rcw5adqd.js → cli-wcnyhk4w.js} +1 -1
- package/dist/cli.js +52 -54
- package/dist/{init-s0dafa83.js → init-2x8g9295.js} +1 -10
- package/dist/{jump-nvdkeega.js → jump-tnpgxcsz.js} +5 -5
- package/dist/{ls-kvz4wmc9.js → ls-ky84e8ep.js} +3 -3
- package/dist/{render-ls-6r3vv7y0.js → render-ls-x4hpnsvy.js} +16 -27
- package/dist/rm-0g8cxees.js +7 -0
- package/dist/root-czmzc98y.js +7 -0
- package/package.json +3 -4
- package/dist/rm-231qjgd8.js +0 -7
- package/dist/root-mmyazwc3.js +0 -7
- package/shell/init.zsh +0 -53
package/README.ja.md
CHANGED
|
@@ -40,8 +40,8 @@ hop -- <branch> # 予約コマンドと被る branch 名をエスケープ
|
|
|
40
40
|
hop --help # usage を表示 (-h / hop help でも同じ)
|
|
41
41
|
|
|
42
42
|
hop --version # hop の version を表示
|
|
43
|
-
hop --update #
|
|
44
|
-
hop --update --check #
|
|
43
|
+
hop --update # standalone バイナリを最新リリースへ置換 (それ以外は更新方法を表示)
|
|
44
|
+
hop --update --check # 更新があるかを報告するだけ — 何も変更しない
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
同じ branch が複数の worktree で checkout されている場合、branch だけを指定する
|
|
@@ -92,7 +92,7 @@ action を選ぶ間も一覧が見え続ける。横に並べられない幅 (
|
|
|
92
92
|
| `Tab`, `→`, `Ctrl+L`, `Ctrl+F` | 選択中の候補の action panel を開く (ここで cd / 削除 / root 切替) |
|
|
93
93
|
| `Ctrl+X` | 選択中の worktree を削除 (常に先に y/N を確認) |
|
|
94
94
|
| `Ctrl+R` | root clone を選択中の branch へ切替 (`external` は先に y/N を確認) |
|
|
95
|
-
| `↑`/`↓`, `Ctrl+P`/`Ctrl+N`, `Ctrl+K
|
|
95
|
+
| `↑`/`↓`, `Ctrl+P`/`Ctrl+N`, `Ctrl+K` | 選択を移動 (矢印・emacs・vim キーがすべて併用可) |
|
|
96
96
|
| `Esc` | 静かにキャンセル — exit 0、stdout は空のまま (shell wrapper は cd しない) |
|
|
97
97
|
| `Ctrl+C` | 割り込みとしてキャンセル — exit 130、実際の SIGINT と同じ |
|
|
98
98
|
|
|
@@ -145,27 +145,27 @@ install script は `hop` を `~/.local/bin` に置き (`HOP_INSTALL_DIR` で
|
|
|
145
145
|
### 更新
|
|
146
146
|
|
|
147
147
|
```sh
|
|
148
|
-
hop --update #
|
|
149
|
-
hop --update --check # 報告だけ: current / latest /
|
|
148
|
+
hop --update # standalone バイナリを最新リリースへ置換
|
|
149
|
+
hop --update --check # 報告だけ: current / latest / インストール方法 / すべきこと
|
|
150
150
|
hop --version # 今どの version か
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
`hop --update`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
hop
|
|
168
|
-
|
|
153
|
+
`hop --update` が置換するのは `install.sh` で入れたバイナリだけで、置き場の
|
|
154
|
+
ディレクトリに書き込めて (バイナリと checksum のダウンロードの前に確認する)、
|
|
155
|
+
かつリリースに添付された `.sha256` と SHA-256 が一致した場合に限る — 不一致なら
|
|
156
|
+
何も置換しない。それ以外のインストールでは、何も実行せず、リクエストも送らず、
|
|
157
|
+
更新方法を 1 行表示して終了する (exit 1):
|
|
158
|
+
|
|
159
|
+
| インストール方法 | 更新方法 |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `install.sh` (`~/.local/bin` のバイナリ) | `hop --update`、または `install.sh` を再実行 |
|
|
162
|
+
| mise | `mise upgrade` |
|
|
163
|
+
| npm / bun / pnpm / yarn の global | `npm i -g @n-seiji/nuthatch@latest` (または使っているパッケージマネージャの同等コマンド) |
|
|
164
|
+
| Homebrew・Nix・aqua・proto・ソースチェックアウト・その他のツール | そのツール自身の upgrade コマンド |
|
|
165
|
+
|
|
166
|
+
`hop --update --check` はどのインストール方法でも使え、同じ案内を報告する。
|
|
167
|
+
hop がネットワークに出るのは GitHub の固定アドレスへの HTTPS リクエストだけで、
|
|
168
|
+
タイムアウトとサイズ上限つき。
|
|
169
169
|
|
|
170
170
|
## トラブルシューティング
|
|
171
171
|
|
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ hop -- <branch> # escape a branch name that collides with a reserved command
|
|
|
42
42
|
hop --help # print usage (also -h / hop help)
|
|
43
43
|
|
|
44
44
|
hop --version # print hop's version
|
|
45
|
-
hop --update #
|
|
46
|
-
hop --update --check # only report
|
|
45
|
+
hop --update # replace a standalone binary with the latest release (other installs: prints how to update)
|
|
46
|
+
hop --update --check # only report whether an update is available — changes nothing
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
If Git reports the same branch checked out in multiple worktrees, the
|
|
@@ -100,7 +100,7 @@ below the list instead.
|
|
|
100
100
|
| `Tab`, `→`, `Ctrl+L`, `Ctrl+F` | Open the action panel for the selected candidate (cd / delete / switch root here) |
|
|
101
101
|
| `Ctrl+X` | Delete the selected worktree (always asks y/N first) |
|
|
102
102
|
| `Ctrl+R` | Switch the root clone to the selected branch (asks y/N first for an `external` worktree) |
|
|
103
|
-
| `↑`/`↓`, `Ctrl+P`/`Ctrl+N`, `Ctrl+K
|
|
103
|
+
| `↑`/`↓`, `Ctrl+P`/`Ctrl+N`, `Ctrl+K` | Move the selection (arrow, emacs, and vim keys all work side by side) |
|
|
104
104
|
| `Esc` | Cancel quietly — exit 0, stdout stays empty (the shell wrapper just doesn't cd) |
|
|
105
105
|
| `Ctrl+C` | Cancel like an interrupt — exit 130, same as a real SIGINT |
|
|
106
106
|
|
|
@@ -151,27 +151,27 @@ binary yet — use the npm install instead.
|
|
|
151
151
|
### Updating
|
|
152
152
|
|
|
153
153
|
```sh
|
|
154
|
-
hop --update #
|
|
155
|
-
hop --update --check # just report: current, latest,
|
|
154
|
+
hop --update # replace a standalone binary with the latest release
|
|
155
|
+
hop --update --check # just report: current, latest, how hop was installed, what to do
|
|
156
156
|
hop --version # which version do I have?
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
`hop --update`
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
`hop --update` only replaces the binary that `install.sh` installs, and only if
|
|
160
|
+
its directory is writable (checked before the binary or its checksum is
|
|
161
|
+
downloaded) and its SHA-256 matches the `.sha256` attached to the release; on a
|
|
162
|
+
mismatch nothing is replaced. For any other install it runs nothing, makes no
|
|
163
|
+
request, and prints one line saying how to update it (exit 1):
|
|
164
|
+
|
|
165
|
+
| Installed with | Update with |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `install.sh` (binary in `~/.local/bin`) | `hop --update`, or re-run `install.sh` |
|
|
168
|
+
| mise | `mise upgrade` |
|
|
169
|
+
| npm / bun / pnpm / yarn global | `npm i -g @n-seiji/nuthatch@latest` (or your package manager's equivalent) |
|
|
170
|
+
| Homebrew, Nix, aqua, proto, a source checkout, any other tool | that tool's own upgrade command |
|
|
171
|
+
|
|
172
|
+
`hop --update --check` works for every install and reports the same hint. The
|
|
173
|
+
only network traffic is HTTPS requests to fixed GitHub addresses, each with a
|
|
174
|
+
timeout and a size cap.
|
|
175
175
|
|
|
176
176
|
## Troubleshooting
|
|
177
177
|
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
messageOf
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-ph0gp56k.js";
|
|
4
4
|
import {
|
|
5
5
|
upstreamStateOf
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-btqjpf1q.js";
|
|
7
7
|
import {
|
|
8
8
|
withRepoLock
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-wcnyhk4w.js";
|
|
10
10
|
import {
|
|
11
11
|
loadRepoContext,
|
|
12
12
|
nestedWorktrees,
|
|
13
13
|
worktreeDirtyState
|
|
14
|
-
} from "./cli-
|
|
14
|
+
} from "./cli-fqz7emfm.js";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_USAGE_ERROR,
|
|
17
17
|
ok,
|
|
18
18
|
fail
|
|
19
|
-
} from "./cli-
|
|
19
|
+
} from "./cli-77yanq37.js";
|
|
20
20
|
|
|
21
21
|
// src/domain/garbage.ts
|
|
22
22
|
var classifyGarbage = (input) => {
|
|
@@ -51,6 +51,17 @@ var buildCleanCandidates = async (git, fs, context, ext) => {
|
|
|
51
51
|
}));
|
|
52
52
|
return results.filter((candidate) => candidate !== null);
|
|
53
53
|
};
|
|
54
|
+
var branchMergeFacts = async (git, rootPath, branch, { defaultRef, upstreams }) => {
|
|
55
|
+
const upstreamGone = upstreamStateOf(upstreams, branch).kind === "gone";
|
|
56
|
+
const mergedIntoDefault = defaultRef === null ? "unknown" : await git.isAncestor(rootPath, branch, defaultRef);
|
|
57
|
+
const allCommitsReachableFromDefault = defaultRef === null || mergedIntoDefault === true || !upstreamGone ? "unknown" : await git.hasEquivalentCommits(rootPath, branch, defaultRef);
|
|
58
|
+
return {
|
|
59
|
+
prunable: false,
|
|
60
|
+
upstreamGone,
|
|
61
|
+
mergedIntoDefault,
|
|
62
|
+
allCommitsReachableFromDefault
|
|
63
|
+
};
|
|
64
|
+
};
|
|
54
65
|
var classifyWorktree = async (wt, { git, fs, defaultRef, upstreams, context }) => {
|
|
55
66
|
if (wt.prunable) {
|
|
56
67
|
return classifyGarbage({
|
|
@@ -65,15 +76,7 @@ var classifyWorktree = async (wt, { git, fs, defaultRef, upstreams, context }) =
|
|
|
65
76
|
return null;
|
|
66
77
|
}
|
|
67
78
|
const { rootPath, worktrees } = context;
|
|
68
|
-
const
|
|
69
|
-
const mergedIntoDefault = defaultRef === null ? "unknown" : await git.isAncestor(rootPath, wt.branch, defaultRef);
|
|
70
|
-
const allCommitsReachableFromDefault = defaultRef === null || mergedIntoDefault === true || !upstreamGone ? "unknown" : await git.hasEquivalentCommits(rootPath, wt.branch, defaultRef);
|
|
71
|
-
const facts = {
|
|
72
|
-
prunable: false,
|
|
73
|
-
mergedIntoDefault,
|
|
74
|
-
upstreamGone,
|
|
75
|
-
allCommitsReachableFromDefault
|
|
76
|
-
};
|
|
79
|
+
const facts = await branchMergeFacts(git, rootPath, wt.branch, { defaultRef, upstreams });
|
|
77
80
|
if (classifyGarbage({ ...facts, clean: true }) === null) {
|
|
78
81
|
return null;
|
|
79
82
|
}
|
|
@@ -132,23 +135,19 @@ var removeCandidate = async ({
|
|
|
132
135
|
return "failed";
|
|
133
136
|
}
|
|
134
137
|
};
|
|
135
|
-
var canDeletePrunableBranch = async (git, rootPath, candidate,
|
|
138
|
+
var canDeletePrunableBranch = async (git, rootPath, candidate, facts) => {
|
|
136
139
|
if (candidate.reason !== "prunable") {
|
|
137
140
|
return true;
|
|
138
141
|
}
|
|
139
|
-
if (candidate.branch.length === 0
|
|
142
|
+
if (candidate.branch.length === 0) {
|
|
140
143
|
return false;
|
|
141
144
|
}
|
|
142
145
|
try {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return await git.hasEquivalentCommits(rootPath, candidate.branch, defaultRef) === true;
|
|
149
|
-
}
|
|
150
|
-
} catch {}
|
|
151
|
-
return false;
|
|
146
|
+
const branchFacts = await branchMergeFacts(git, rootPath, candidate.branch, facts);
|
|
147
|
+
return classifyGarbage({ ...branchFacts, clean: true }) !== null;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
152
151
|
};
|
|
153
152
|
var executeClean = ({
|
|
154
153
|
git,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/infra/term.ts
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
var colorEnabled = (stream) => stream.isTTY === true && (process.env["NO_COLOR"] ?? "") === "";
|
|
4
|
+
var createTermPort = () => ({
|
|
5
|
+
isTTY() {
|
|
6
|
+
return process.stderr.isTTY === true && process.stdin.isTTY === true;
|
|
7
|
+
},
|
|
8
|
+
logStderr(message) {
|
|
9
|
+
process.stderr.write(`${message}
|
|
10
|
+
`);
|
|
11
|
+
},
|
|
12
|
+
confirm(message) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const rl = createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stderr,
|
|
17
|
+
terminal: false
|
|
18
|
+
});
|
|
19
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
20
|
+
rl.close();
|
|
21
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
22
|
+
});
|
|
23
|
+
rl.once("close", () => {
|
|
24
|
+
resolve(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export { colorEnabled, createTermPort };
|
|
@@ -15,5 +15,7 @@ var fail = (exitCode, errorMessage, warnings) => ({
|
|
|
15
15
|
errorMessage,
|
|
16
16
|
...warnings === undefined || warnings.length === 0 ? {} : { warnings }
|
|
17
17
|
});
|
|
18
|
+
var pass = (value) => ({ ok: true, value });
|
|
19
|
+
var reject = (rejection) => ({ ok: false, rejection });
|
|
18
20
|
|
|
19
|
-
export { EXIT_SUCCESS, EXIT_GENERAL_ERROR, EXIT_USAGE_ERROR, EXIT_SAFE_REJECTION, EXIT_CANCELLED, ok, fail };
|
|
21
|
+
export { EXIT_SUCCESS, EXIT_GENERAL_ERROR, EXIT_USAGE_ERROR, EXIT_SAFE_REJECTION, EXIT_CANCELLED, ok, fail, pass, reject };
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
messageOf
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-ph0gp56k.js";
|
|
4
4
|
import {
|
|
5
5
|
withRepoLock
|
|
6
|
-
} from "./cli-
|
|
6
|
+
} from "./cli-wcnyhk4w.js";
|
|
7
7
|
import {
|
|
8
8
|
loadRepoContext,
|
|
9
9
|
isWorktreeDirty,
|
|
10
10
|
resolveBranchCheckout
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-fqz7emfm.js";
|
|
12
12
|
import {
|
|
13
13
|
EXIT_SAFE_REJECTION,
|
|
14
14
|
ok,
|
|
15
|
-
fail
|
|
16
|
-
|
|
15
|
+
fail,
|
|
16
|
+
pass,
|
|
17
|
+
reject
|
|
18
|
+
} from "./cli-77yanq37.js";
|
|
17
19
|
|
|
18
20
|
// src/commands/root-holder-swap.ts
|
|
19
21
|
var holderRejection = (branch, holderPath, reason) => fail(EXIT_SAFE_REJECTION, `Branch "${branch}" is already checked out at ${holderPath}, and it ${reason}. Not swapping — cd there instead of using hop root.`);
|
|
@@ -27,47 +29,37 @@ var resolveHolderSwap = async ({
|
|
|
27
29
|
policy
|
|
28
30
|
}) => {
|
|
29
31
|
if (target === "-") {
|
|
30
|
-
return
|
|
32
|
+
return pass(null);
|
|
31
33
|
}
|
|
32
34
|
const freshHolders = fresh.worktrees.filter((wt) => wt.branch === target && wt.kind !== "root");
|
|
33
35
|
if (freshHolders.length > 1) {
|
|
34
|
-
return
|
|
36
|
+
return reject(multipleHoldersRejection(target, freshHolders));
|
|
35
37
|
}
|
|
36
38
|
const [freshHolder] = freshHolders;
|
|
37
39
|
if (freshHolder === undefined) {
|
|
38
|
-
return
|
|
40
|
+
return pass(null);
|
|
39
41
|
}
|
|
40
42
|
if (policy.expectedPath !== undefined && freshHolder.path !== policy.expectedPath) {
|
|
41
|
-
return {
|
|
42
|
-
rejection: holderRejection(target, freshHolder.path, `changed from the picker selection at ${policy.expectedPath}`)
|
|
43
|
-
};
|
|
43
|
+
return reject(holderRejection(target, freshHolder.path, `changed from the picker selection at ${policy.expectedPath}`));
|
|
44
44
|
}
|
|
45
45
|
if (freshHolder.kind === "external" && !policy.allowExternal) {
|
|
46
|
-
return
|
|
47
|
-
rejection: holderRejection(target, freshHolder.path, "is external and requires confirmation before it can be put into detached HEAD")
|
|
48
|
-
};
|
|
46
|
+
return reject(holderRejection(target, freshHolder.path, "is external and requires confirmation before it can be put into detached HEAD"));
|
|
49
47
|
}
|
|
50
48
|
if (switchOptions.createBranch === true) {
|
|
51
|
-
return
|
|
52
|
-
rejection: holderRejection(target, freshHolder.path, "wasn't visible when hop checked existing local branches (e.g. it has no commits yet, or was created after that check)")
|
|
53
|
-
};
|
|
49
|
+
return reject(holderRejection(target, freshHolder.path, "wasn't visible when hop checked existing local branches (e.g. it has no commits yet, or was created after that check)"));
|
|
54
50
|
}
|
|
55
51
|
if (freshHolder.locked) {
|
|
56
|
-
return {
|
|
57
|
-
rejection: holderRejection(target, freshHolder.path, `is locked by git${freshHolder.lockReason === null ? "" : ` (${freshHolder.lockReason})`}`)
|
|
58
|
-
};
|
|
52
|
+
return reject(holderRejection(target, freshHolder.path, `is locked by git${freshHolder.lockReason === null ? "" : ` (${freshHolder.lockReason})`}`));
|
|
59
53
|
}
|
|
60
54
|
if (freshHolder.prunable) {
|
|
61
|
-
return
|
|
55
|
+
return reject(prunableHolderRejection(target, freshHolder.path));
|
|
62
56
|
}
|
|
63
57
|
const holderDirty = await isWorktreeDirty(git, fresh.worktrees, freshHolder.path);
|
|
64
58
|
if (holderDirty) {
|
|
65
|
-
return
|
|
66
|
-
rejection: holderRejection(target, freshHolder.path, "has uncommitted or untracked changes")
|
|
67
|
-
};
|
|
59
|
+
return reject(holderRejection(target, freshHolder.path, "has uncommitted or untracked changes"));
|
|
68
60
|
}
|
|
69
61
|
await git.detachHead(freshHolder.path);
|
|
70
|
-
return {
|
|
62
|
+
return pass({ path: freshHolder.path, branch: target });
|
|
71
63
|
};
|
|
72
64
|
|
|
73
65
|
// src/commands/root.ts
|
|
@@ -106,7 +98,7 @@ var root2 = async (git, fs, options) => {
|
|
|
106
98
|
fs,
|
|
107
99
|
context,
|
|
108
100
|
target: options.target,
|
|
109
|
-
switchOptions: checkout.
|
|
101
|
+
switchOptions: checkout.value,
|
|
110
102
|
holderPolicy: {
|
|
111
103
|
allowExternal: options.allowExternalHolderSwap ?? true,
|
|
112
104
|
...options.expectedHolderPath === undefined ? {} : { expectedPath: options.expectedHolderPath }
|
|
@@ -163,10 +155,10 @@ var switchAndReport = ({
|
|
|
163
155
|
switchOptions,
|
|
164
156
|
policy: holderPolicy
|
|
165
157
|
});
|
|
166
|
-
if (
|
|
158
|
+
if (!holderSwap.ok) {
|
|
167
159
|
return holderSwap.rejection;
|
|
168
160
|
}
|
|
169
|
-
|
|
161
|
+
detachedHolder = holderSwap.value;
|
|
170
162
|
await git.switchBranch(fresh.rootPath, target, switchOptions);
|
|
171
163
|
let resolvedBranch = target;
|
|
172
164
|
if (target === "-") {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/domain/classify.ts
|
|
2
|
+
var isWithin = (parent, child) => {
|
|
3
|
+
if (parent === child) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const parentWithSlash = parent.endsWith("/") ? parent : `${parent}/`;
|
|
7
|
+
return child.startsWith(parentWithSlash);
|
|
8
|
+
};
|
|
9
|
+
var isWithinOrSelf = (parent, child) => parent === child || isWithin(parent, child);
|
|
10
|
+
var classifyWorktreePath = (path, rootPath, managedRoot) => {
|
|
11
|
+
if (path === rootPath) {
|
|
12
|
+
return "root";
|
|
13
|
+
}
|
|
14
|
+
if (path === managedRoot || isWithin(managedRoot, path)) {
|
|
15
|
+
return "managed";
|
|
16
|
+
}
|
|
17
|
+
return "external";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { isWithin, isWithinOrSelf, classifyWorktreePath };
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
// src/domain/classify.ts
|
|
2
|
-
var isWithin = (parent, child) => {
|
|
3
|
-
if (parent === child) {
|
|
4
|
-
return false;
|
|
5
|
-
}
|
|
6
|
-
const parentWithSlash = parent.endsWith("/") ? parent : `${parent}/`;
|
|
7
|
-
return child.startsWith(parentWithSlash);
|
|
8
|
-
};
|
|
9
|
-
var classifyWorktreePath = (path, rootPath, managedRoot) => {
|
|
10
|
-
if (path === rootPath) {
|
|
11
|
-
return "root";
|
|
12
|
-
}
|
|
13
|
-
if (path === managedRoot || isWithin(managedRoot, path)) {
|
|
14
|
-
return "managed";
|
|
15
|
-
}
|
|
16
|
-
return "external";
|
|
17
|
-
};
|
|
18
|
-
|
|
19
1
|
// src/domain/porcelain.ts
|
|
20
2
|
var REFS_HEADS_PREFIX = "refs/heads/";
|
|
21
3
|
var branchNameFromRef = (ref) => ref.startsWith(REFS_HEADS_PREFIX) ? ref.slice(REFS_HEADS_PREFIX.length) : ref;
|
|
@@ -101,4 +83,4 @@ var parsePorcelain = (output) => {
|
|
|
101
83
|
return result;
|
|
102
84
|
};
|
|
103
85
|
|
|
104
|
-
export {
|
|
86
|
+
export { REFS_HEADS_PREFIX, parsePorcelain };
|
|
@@ -109,4 +109,18 @@ var truncateToWidthKeepingTail = (value, width, ellipsis = DEFAULT_ELLIPSIS) =>
|
|
|
109
109
|
return ellipsis + keptFromEnd.toReversed().join("");
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
// src/domain/ansi.ts
|
|
113
|
+
var ESC = "\x1B";
|
|
114
|
+
var SGR = {
|
|
115
|
+
bold: `${ESC}[1m`,
|
|
116
|
+
dim: `${ESC}[2m`,
|
|
117
|
+
inverse: `${ESC}[7m`,
|
|
118
|
+
red: `${ESC}[31m`,
|
|
119
|
+
green: `${ESC}[32m`,
|
|
120
|
+
yellow: `${ESC}[33m`,
|
|
121
|
+
magenta: `${ESC}[35m`,
|
|
122
|
+
cyan: `${ESC}[36m`
|
|
123
|
+
};
|
|
124
|
+
var SGR_RESET = `${ESC}[0m`;
|
|
125
|
+
|
|
126
|
+
export { graphemes, displayWidth, padToWidth, truncateToWidth, truncateToWidthKeepingTail, ESC, SGR, SGR_RESET };
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
isWithin,
|
|
3
|
-
classifyWorktreePath,
|
|
4
2
|
parsePorcelain
|
|
5
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-b0tk5wcx.js";
|
|
6
4
|
import {
|
|
7
5
|
EXIT_USAGE_ERROR,
|
|
8
|
-
fail
|
|
9
|
-
|
|
6
|
+
fail,
|
|
7
|
+
pass,
|
|
8
|
+
reject
|
|
9
|
+
} from "./cli-77yanq37.js";
|
|
10
|
+
import {
|
|
11
|
+
isWithin,
|
|
12
|
+
classifyWorktreePath
|
|
13
|
+
} from "./cli-a56b2ar0.js";
|
|
10
14
|
|
|
11
15
|
// src/infra/repo.ts
|
|
12
16
|
import { basename, dirname, join } from "node:path";
|
|
@@ -49,9 +53,8 @@ var loadRepoContext = async (git, fs, cwd) => {
|
|
|
49
53
|
}));
|
|
50
54
|
return { rootPath, managedRoot, commonDir, worktrees };
|
|
51
55
|
};
|
|
52
|
-
var nestedWorktreePaths = (worktrees, path) => nestedWorktrees(worktrees, path).map((wt) => wt.path);
|
|
53
56
|
var nestedWorktrees = (worktrees, path) => worktrees.filter((wt) => wt.path !== path && !wt.prunable && isWithin(path, wt.path));
|
|
54
|
-
var isWorktreeDirty = (git, worktrees, path) => git.isDirty(path,
|
|
57
|
+
var isWorktreeDirty = (git, worktrees, path) => git.isDirty(path, nestedWorktrees(worktrees, path).map((wt) => wt.path));
|
|
55
58
|
var worktreeDirtyState = async (git, fs, worktrees, wt) => {
|
|
56
59
|
if (wt.bare || wt.prunable || !await fs.exists(wt.path)) {
|
|
57
60
|
return null;
|
|
@@ -66,16 +69,13 @@ var resolveBranchCheckout = async (git, rootPath, branch, explicitTrack) => {
|
|
|
66
69
|
const remotes = await git.remotesWithBranch(rootPath, branch);
|
|
67
70
|
const resolution = resolveTrackingRef(branch, remotes);
|
|
68
71
|
if (resolution.kind === "ambiguous") {
|
|
69
|
-
return {
|
|
70
|
-
ok: false,
|
|
71
|
-
rejection: fail(EXIT_USAGE_ERROR, `Branch "${branch}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`)
|
|
72
|
-
};
|
|
72
|
+
return reject(fail(EXIT_USAGE_ERROR, `Branch "${branch}" exists on multiple remotes (${remotes.join(", ")}). Use --track to disambiguate.`));
|
|
73
73
|
}
|
|
74
74
|
if (resolution.kind === "track") {
|
|
75
75
|
track = resolution.ref;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
return {
|
|
78
|
+
return pass({ createBranch, ...track === undefined ? {} : { track } });
|
|
79
79
|
};
|
|
80
80
|
var realpathOrRaw = async (fs, path) => {
|
|
81
81
|
try {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
describeFatalError
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-ph0gp56k.js";
|
|
4
4
|
import {
|
|
5
5
|
reportResult
|
|
6
6
|
} from "./cli-dbzx9nzg.js";
|
|
7
7
|
import {
|
|
8
8
|
fail
|
|
9
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-77yanq37.js";
|
|
10
10
|
|
|
11
11
|
// src/cli-dispatch.ts
|
|
12
12
|
var normalizeCliArgs = (rawArgs, argv0, isCompiledBinary) => isCompiledBinary && rawArgs.length === 1 && rawArgs[0] === argv0 ? [] : rawArgs;
|
|
@@ -37,7 +37,7 @@ var reportFatalError = (command, json, error) => {
|
|
|
37
37
|
// package.json
|
|
38
38
|
var package_default = {
|
|
39
39
|
name: "@n-seiji/nuthatch",
|
|
40
|
-
version: "0.
|
|
40
|
+
version: "0.2.0",
|
|
41
41
|
description: "git worktree manager (hop)",
|
|
42
42
|
homepage: "https://github.com/n-seiji/nuthatch#readme",
|
|
43
43
|
bugs: {
|
|
@@ -52,8 +52,7 @@ var package_default = {
|
|
|
52
52
|
hop: "dist/cli.js"
|
|
53
53
|
},
|
|
54
54
|
files: [
|
|
55
|
-
"dist"
|
|
56
|
-
"shell"
|
|
55
|
+
"dist"
|
|
57
56
|
],
|
|
58
57
|
type: "module",
|
|
59
58
|
publishConfig: {
|
|
@@ -73,7 +72,7 @@ var package_default = {
|
|
|
73
72
|
},
|
|
74
73
|
devDependencies: {
|
|
75
74
|
"@types/bun": "latest",
|
|
76
|
-
oxfmt: "^0.
|
|
75
|
+
oxfmt: "^0.72.0",
|
|
77
76
|
oxlint: "^1.81.0",
|
|
78
77
|
typescript: "^5.6.0"
|
|
79
78
|
},
|
|
@@ -85,30 +84,4 @@ var package_default = {
|
|
|
85
84
|
// src/version.ts
|
|
86
85
|
var VERSION = package_default.version;
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
import { constants } from "node:fs";
|
|
90
|
-
import { access, stat } from "node:fs/promises";
|
|
91
|
-
var isExecutableFile = async (path) => {
|
|
92
|
-
try {
|
|
93
|
-
const stats = await stat(path);
|
|
94
|
-
if (!stats.isFile()) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
await access(path, constants.X_OK);
|
|
98
|
-
return true;
|
|
99
|
-
} catch {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
var firstExecutable = async (candidates) => {
|
|
104
|
-
const probes = candidates.map(async (candidate) => await isExecutableFile(candidate) ? candidate : null);
|
|
105
|
-
for (const probe of probes) {
|
|
106
|
-
const found = await probe;
|
|
107
|
-
if (found !== null) {
|
|
108
|
-
return found;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export { normalizeCliArgs, isRunningAsCompiledBinary, isUpdateRequest, isHelpRequest, isVersionRequest, dispatchCliArgs, wantsJson, reportFatalError, firstExecutable, VERSION };
|
|
87
|
+
export { normalizeCliArgs, isRunningAsCompiledBinary, isUpdateRequest, isHelpRequest, isVersionRequest, dispatchCliArgs, wantsJson, reportFatalError, VERSION };
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EXIT_GENERAL_ERROR,
|
|
3
3
|
EXIT_SAFE_REJECTION
|
|
4
|
-
} from "./cli-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const trimmed = withoutTrailingSlash(path);
|
|
10
|
-
const index = trimmed.lastIndexOf("/");
|
|
11
|
-
if (index === -1) {
|
|
12
|
-
return ".";
|
|
13
|
-
}
|
|
14
|
-
return index === 0 ? "/" : trimmed.slice(0, index);
|
|
15
|
-
};
|
|
16
|
-
var baseName = (path) => {
|
|
17
|
-
const trimmed = withoutTrailingSlash(path);
|
|
18
|
-
return trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
19
|
-
};
|
|
20
|
-
var absoluteCandidates = (dirs, name) => [
|
|
21
|
-
...new Set(dirs.map((entry) => entry.trim()).filter((entry) => entry.startsWith("/")).map((dir) => `${dir.endsWith("/") ? dir.slice(0, -1) : dir}/${name}`))
|
|
22
|
-
];
|
|
23
|
-
var splitPathEnv = (pathEnv) => (pathEnv ?? "").split(":");
|
|
4
|
+
} from "./cli-77yanq37.js";
|
|
5
|
+
import {
|
|
6
|
+
absoluteCandidates,
|
|
7
|
+
splitPathEnv
|
|
8
|
+
} from "./cli-s5qcjcwk.js";
|
|
24
9
|
|
|
25
10
|
// src/domain/git-executable.ts
|
|
26
11
|
var GIT_EXECUTABLE_OVERRIDE_ENV = "HOP_GIT";
|
|
@@ -66,4 +51,4 @@ var describeFatalError = (error) => {
|
|
|
66
51
|
return { message: messageOf(error), exitCode: EXIT_GENERAL_ERROR };
|
|
67
52
|
};
|
|
68
53
|
|
|
69
|
-
export {
|
|
54
|
+
export { GIT_EXECUTABLE_OVERRIDE_ENV, GIT_NOT_FOUND_ERROR_CODE, gitExecutableCandidates, gitNotFoundMessage, messageOf, describeFatalError };
|