@n-seiji/nuthatch 0.1.5 → 0.1.7

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.
@@ -0,0 +1,195 @@
1
+ import {
2
+ messageOf
3
+ } from "./cli-qrxgkskr.js";
4
+ import {
5
+ withRepoLock
6
+ } from "./cli-rcw5adqd.js";
7
+ import {
8
+ loadRepoContext,
9
+ isWorktreeDirty,
10
+ resolveBranchCheckout
11
+ } from "./cli-7jg5qqs9.js";
12
+ import {
13
+ EXIT_SAFE_REJECTION,
14
+ ok,
15
+ fail
16
+ } from "./cli-2bhb9p0x.js";
17
+
18
+ // src/commands/root-holder-swap.ts
19
+ 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.`);
20
+ var prunableHolderRejection = (branch, holderPath) => fail(EXIT_SAFE_REJECTION, `Branch "${branch}" is held by a stale worktree registration at ${holderPath} (git reports it prunable). Not swapping — if that worktree was moved, re-link it with "git worktree repair <new path>"; otherwise drop the registration first ("hop rm ${branch}" or "git worktree prune").`);
21
+ var multipleHoldersRejection = (branch, holders) => fail(EXIT_SAFE_REJECTION, `Branch "${branch}" is checked out at ${holders.length} worktrees at once (${holders.map((holder) => holder.path).join(", ")}). Not swapping — hop won't guess which one to detach.`);
22
+ var resolveHolderSwap = async ({
23
+ git,
24
+ fresh,
25
+ target,
26
+ switchOptions,
27
+ policy
28
+ }) => {
29
+ if (target === "-") {
30
+ return { detachedHolder: null };
31
+ }
32
+ const freshHolders = fresh.worktrees.filter((wt) => wt.branch === target && wt.kind !== "root");
33
+ if (freshHolders.length > 1) {
34
+ return { rejection: multipleHoldersRejection(target, freshHolders) };
35
+ }
36
+ const [freshHolder] = freshHolders;
37
+ if (freshHolder === undefined) {
38
+ return { detachedHolder: null };
39
+ }
40
+ 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
+ };
44
+ }
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
+ };
49
+ }
50
+ 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
+ };
54
+ }
55
+ if (freshHolder.locked) {
56
+ return {
57
+ rejection: holderRejection(target, freshHolder.path, `is locked by git${freshHolder.lockReason === null ? "" : ` (${freshHolder.lockReason})`}`)
58
+ };
59
+ }
60
+ if (freshHolder.prunable) {
61
+ return { rejection: prunableHolderRejection(target, freshHolder.path) };
62
+ }
63
+ const holderDirty = await isWorktreeDirty(git, fresh.worktrees, freshHolder.path);
64
+ if (holderDirty) {
65
+ return {
66
+ rejection: holderRejection(target, freshHolder.path, "has uncommitted or untracked changes")
67
+ };
68
+ }
69
+ await git.detachHead(freshHolder.path);
70
+ return { detachedHolder: { path: freshHolder.path, branch: target } };
71
+ };
72
+
73
+ // src/commands/root.ts
74
+ var root2 = async (git, fs, options) => {
75
+ const context = await loadRepoContext(git, fs, options.cwd);
76
+ if (options.target === undefined) {
77
+ return ok({
78
+ path: context.rootPath,
79
+ data: {
80
+ branch: rootBranchOf(context),
81
+ switched: false,
82
+ detachedHolder: null
83
+ }
84
+ });
85
+ }
86
+ const dirtyRejection = await dirtyRootRejection(git, context);
87
+ if (dirtyRejection !== null) {
88
+ return dirtyRejection;
89
+ }
90
+ if (options.target === "-") {
91
+ return switchAndReport({
92
+ git,
93
+ fs,
94
+ context,
95
+ target: "-",
96
+ switchOptions: {},
97
+ holderPolicy: { allowExternal: true }
98
+ });
99
+ }
100
+ const checkout = await resolveBranchCheckout(git, context.rootPath, options.target, options.track);
101
+ if (!checkout.ok) {
102
+ return checkout.rejection;
103
+ }
104
+ return switchAndReport({
105
+ git,
106
+ fs,
107
+ context,
108
+ target: options.target,
109
+ switchOptions: checkout.options,
110
+ holderPolicy: {
111
+ allowExternal: options.allowExternalHolderSwap ?? true,
112
+ ...options.expectedHolderPath === undefined ? {} : { expectedPath: options.expectedHolderPath }
113
+ }
114
+ });
115
+ };
116
+ var rootBranchOf = ({ worktrees }) => worktrees.find((wt) => wt.kind === "root")?.branch ?? null;
117
+ var dirtyRootRejection = async (git, context) => {
118
+ const dirty = await isWorktreeDirty(git, context.worktrees, context.rootPath);
119
+ if (!dirty) {
120
+ return null;
121
+ }
122
+ return fail(EXIT_SAFE_REJECTION, "Root clone has uncommitted or untracked changes. Commit, stash, or discard them before switching.");
123
+ };
124
+ var rollBackSwitch = async (git, { rootPath, previousBranch, detachedHolder }) => {
125
+ const warnings = [];
126
+ if (previousBranch !== null) {
127
+ try {
128
+ await git.switchBranch(rootPath, previousBranch, {});
129
+ } catch {
130
+ warnings.push(`Could not restore ${rootPath} to its original branch (${previousBranch})`);
131
+ }
132
+ }
133
+ if (detachedHolder !== null) {
134
+ try {
135
+ await git.switchBranch(detachedHolder.path, detachedHolder.branch, {});
136
+ } catch {
137
+ warnings.push(`Could not restore ${detachedHolder.path} to its original branch (${detachedHolder.branch}); it remains in detached HEAD`);
138
+ }
139
+ }
140
+ return warnings;
141
+ };
142
+ var switchAndReport = ({
143
+ git,
144
+ fs,
145
+ context,
146
+ target,
147
+ switchOptions,
148
+ holderPolicy
149
+ }) => withRepoLock(context.commonDir, async () => {
150
+ let detachedHolder = null;
151
+ let previousBranch = null;
152
+ try {
153
+ const fresh = await loadRepoContext(git, fs, context.rootPath);
154
+ previousBranch = rootBranchOf(fresh);
155
+ const stillDirty = await dirtyRootRejection(git, fresh);
156
+ if (stillDirty !== null) {
157
+ return stillDirty;
158
+ }
159
+ const holderSwap = await resolveHolderSwap({
160
+ git,
161
+ fresh,
162
+ target,
163
+ switchOptions,
164
+ policy: holderPolicy
165
+ });
166
+ if ("rejection" in holderSwap) {
167
+ return holderSwap.rejection;
168
+ }
169
+ ({ detachedHolder } = holderSwap);
170
+ await git.switchBranch(fresh.rootPath, target, switchOptions);
171
+ let resolvedBranch = target;
172
+ if (target === "-") {
173
+ const afterSwitch = await loadRepoContext(git, fs, fresh.rootPath);
174
+ resolvedBranch = rootBranchOf(afterSwitch);
175
+ }
176
+ return ok({
177
+ path: fresh.rootPath,
178
+ data: {
179
+ branch: resolvedBranch,
180
+ switched: true,
181
+ detachedHolder: detachedHolder?.path ?? null
182
+ },
183
+ warnings: detachedHolder === null ? [] : [`Put ${detachedHolder.path} into detached HEAD`]
184
+ });
185
+ } catch (error) {
186
+ const warnings = await rollBackSwitch(git, {
187
+ rootPath: context.rootPath,
188
+ previousBranch,
189
+ detachedHolder
190
+ });
191
+ return fail(EXIT_SAFE_REJECTION, `Failed to switch root: ${messageOf(error)}`, warnings);
192
+ }
193
+ });
194
+
195
+ export { root2 };