dubstack 0.1.3

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/commands/create.d.ts +18 -0
  4. package/dist/commands/create.d.ts.map +1 -0
  5. package/dist/commands/create.js +35 -0
  6. package/dist/commands/create.js.map +1 -0
  7. package/dist/commands/init.d.ts +18 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/init.js +41 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/log.d.ts +12 -0
  12. package/dist/commands/log.d.ts.map +1 -0
  13. package/dist/commands/log.js +77 -0
  14. package/dist/commands/log.js.map +1 -0
  15. package/dist/commands/restack.d.ts +33 -0
  16. package/dist/commands/restack.d.ts.map +1 -0
  17. package/dist/commands/restack.js +190 -0
  18. package/dist/commands/restack.js.map +1 -0
  19. package/dist/commands/undo.d.ts +21 -0
  20. package/dist/commands/undo.d.ts.map +1 -0
  21. package/dist/commands/undo.js +63 -0
  22. package/dist/commands/undo.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +125 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/errors.d.ts +15 -0
  28. package/dist/lib/errors.d.ts.map +1 -0
  29. package/dist/lib/errors.js +18 -0
  30. package/dist/lib/errors.js.map +1 -0
  31. package/dist/lib/git.d.ts +69 -0
  32. package/dist/lib/git.d.ts.map +1 -0
  33. package/dist/lib/git.js +184 -0
  34. package/dist/lib/git.js.map +1 -0
  35. package/dist/lib/state.d.ts +70 -0
  36. package/dist/lib/state.d.ts.map +1 -0
  37. package/dist/lib/state.js +110 -0
  38. package/dist/lib/state.js.map +1 -0
  39. package/dist/lib/undo-log.d.ts +33 -0
  40. package/dist/lib/undo-log.d.ts.map +1 -0
  41. package/dist/lib/undo-log.js +37 -0
  42. package/dist/lib/undo-log.js.map +1 -0
  43. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wiseiodev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # DubStack
2
+
3
+ A local-first CLI for managing **stacked diffs** — chains of dependent git branches that build on each other. Stop juggling complex rebase chains by hand.
4
+
5
+ ## Why Stacked Diffs?
6
+
7
+ Stacked diffs let you break large features into small, reviewable PRs that depend on each other. Instead of one 2,000-line monster PR, you get a clean chain:
8
+
9
+ ```
10
+ (main)
11
+ └─ feat/api-models
12
+ └─ feat/api-endpoint
13
+ └─ feat/ui-component
14
+ ```
15
+
16
+ When `main` updates or you amend an earlier branch, `dub restack` cascades rebases through the entire chain for you.
17
+
18
+ ## Install
19
+
20
+ > Requires **Node ≥ 22** and **pnpm**.
21
+
22
+ ```bash
23
+ # Clone and install
24
+ git clone <repo-url> && cd dubstack
25
+ pnpm install
26
+
27
+ # Link globally so `dub` is available everywhere
28
+ pnpm build
29
+ pnpm link --global
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # 1. Initialize in any git repo
36
+ cd my-project
37
+ dub init
38
+
39
+ # 2. Start stacking branches
40
+ git checkout main
41
+ dub create feat/api-models
42
+ # hack hack hack, commit...
43
+
44
+ dub create feat/api-endpoint
45
+ # hack hack hack, commit...
46
+
47
+ dub create feat/ui-component
48
+ # hack hack hack, commit...
49
+
50
+ # 3. See your stack
51
+ dub log
52
+
53
+ # 4. Rebase the whole chain after main updates
54
+ git checkout main && git pull
55
+ dub restack
56
+
57
+ # 5. Made a mistake? Undo it
58
+ dub undo
59
+ ```
60
+
61
+ ## Commands
62
+
63
+ ### `dub init`
64
+
65
+ Initializes DubStack in the current git repository.
66
+
67
+ ```bash
68
+ dub init
69
+ ```
70
+
71
+ - Creates `.git/dubstack/state.json` with an empty state
72
+ - Adds `.git/dubstack` to `.gitignore`
73
+ - **Idempotent** — safe to run multiple times
74
+
75
+ ```
76
+ ✔ DubStack initialized # first run
77
+ ⚠ DubStack already initialized # subsequent runs
78
+ ```
79
+
80
+ ---
81
+
82
+ ### `dub create <branch-name>`
83
+
84
+ Creates a new branch stacked on top of the current branch.
85
+
86
+ ```bash
87
+ # On main
88
+ dub create feat/api-models
89
+
90
+ # On feat/api-models
91
+ dub create feat/api-endpoint
92
+ ```
93
+
94
+ - Checks out the new branch at the current HEAD
95
+ - Records the parent → child relationship in state
96
+ - Auto-creates a new stack if the parent isn't already tracked
97
+ - Saves an undo snapshot before any mutation
98
+
99
+ **Errors:**
100
+ | Condition | Message |
101
+ |---|---|
102
+ | Not initialized | `DubStack is not initialized. Run 'dub init' first.` |
103
+ | Branch already exists | `Branch '<name>' already exists.` |
104
+ | Detached HEAD | `HEAD is detached. Check out a branch first.` |
105
+
106
+ ---
107
+
108
+ ### `dub log`
109
+
110
+ Displays an ASCII tree of all tracked stacks.
111
+
112
+ ```bash
113
+ dub log
114
+ ```
115
+
116
+ Example output:
117
+
118
+ ```
119
+ (main)
120
+ ├─ feat/api-models
121
+ │ └─ feat/api-endpoint (Current)
122
+ └─ feat/auth
123
+ └─ feat/auth-ui ⚠ (missing)
124
+ ```
125
+
126
+ - **Current branch** is highlighted and marked `(Current)`
127
+ - **Root branches** are shown in parentheses, e.g. `(main)`
128
+ - **Deleted branches** still tracked in state show `⚠ (missing)`
129
+ - Multiple stacks are separated by blank lines
130
+
131
+ ---
132
+
133
+ ### `dub restack`
134
+
135
+ Rebases all branches in the current stack onto their updated parents.
136
+
137
+ ```bash
138
+ dub restack
139
+ ```
140
+
141
+ **How it works:**
142
+
143
+ 1. Snapshots every branch tip _before_ starting
144
+ 2. Walks the tree in topological order (parents first)
145
+ 3. For each child branch, runs `git rebase --onto <parent_new_tip> <parent_old_tip> <child>`
146
+ 4. Skips branches whose parent hasn't moved
147
+ 5. Returns you to the branch you started on
148
+
149
+ **Conflict handling:**
150
+
151
+ If a rebase hits a conflict, DubStack pauses and tells you:
152
+
153
+ ```
154
+ ⚠ Conflict while restacking 'feat/api-endpoint'
155
+ Resolve conflicts, stage changes, then run: dub restack --continue
156
+ ```
157
+
158
+ After resolving:
159
+
160
+ ```bash
161
+ # Fix the conflicting files
162
+ git add .
163
+ dub restack --continue
164
+ ```
165
+
166
+ Progress is saved to `.git/dubstack/restack-progress.json`, so the resume picks up exactly where it left off.
167
+
168
+ **Output examples:**
169
+
170
+ ```
171
+ ✔ Stack is already up to date
172
+
173
+ ✔ Restacked 2 branch(es)
174
+ ↳ feat/api-models
175
+ ↳ feat/api-endpoint
176
+ ```
177
+
178
+ **Errors:**
179
+ | Condition | Message |
180
+ |---|---|
181
+ | Uncommitted changes | `Working tree has uncommitted changes. Commit or stash them before restacking.` |
182
+ | Branch not in a stack | `Branch '<name>' is not part of any stack.` |
183
+ | Tracked branch deleted | `Branch '<name>' is tracked in state but no longer exists in git.` |
184
+
185
+ ---
186
+
187
+ ### `dub undo`
188
+
189
+ Rolls back the last `dub create` or `dub restack` operation.
190
+
191
+ ```bash
192
+ dub undo
193
+ ```
194
+
195
+ **Undo strategies:**
196
+
197
+ - **After `create`:** Deletes the created branch, restores state, checks out the previous branch
198
+ - **After `restack`:** Force-resets every rebased branch to its pre-rebase commit, restores state
199
+
200
+ Only **one level** of undo is supported. After undo, the undo entry is cleared.
201
+
202
+ ```
203
+ ✔ Undid 'create': Deleted branch 'feat/api-endpoint'
204
+ ✔ Undid 'restack': Reset 3 branches to pre-restack state
205
+ ```
206
+
207
+ **Errors:**
208
+ | Condition | Message |
209
+ |---|---|
210
+ | Nothing to undo | `Nothing to undo.` |
211
+ | Uncommitted changes | `Working tree has uncommitted changes. Commit or stash them before undoing.` |
212
+
213
+ ---
214
+
215
+ ## Typical Workflow
216
+
217
+ ```bash
218
+ # Start a feature stack off main
219
+ git checkout main
220
+ dub create feat/data-layer
221
+ # write code, commit
222
+
223
+ dub create feat/api-routes
224
+ # write code, commit
225
+
226
+ dub create feat/frontend
227
+ # write code, commit
228
+
229
+ # View the stack
230
+ dub log
231
+ # (main)
232
+ # └─ feat/data-layer
233
+ # └─ feat/api-routes
234
+ # └─ feat/frontend (Current)
235
+
236
+ # Later: main gets updated, or you amend feat/data-layer
237
+ git checkout feat/data-layer
238
+ # amend your commits...
239
+ dub restack
240
+ # ✔ Restacked 2 branch(es)
241
+ # ↳ feat/api-routes
242
+ # ↳ feat/frontend
243
+
244
+ # Oops, that restack went wrong
245
+ dub undo
246
+ # ✔ Undid 'restack': Reset 3 branches to pre-restack state
247
+ ```
248
+
249
+ ## How State Works
250
+
251
+ DubStack stores all state locally inside your git repo:
252
+
253
+ ```
254
+ .git/dubstack/
255
+ ├── state.json # branch relationships and stack metadata
256
+ ├── undo.json # snapshot for single-level undo
257
+ └── restack-progress.json # in-flight restack state (temporary)
258
+ ```
259
+
260
+ Nothing is pushed to your remote. State is per-repo and git-ignored.
261
+
262
+ ## Development
263
+
264
+ ```bash
265
+ pnpm install # install deps
266
+ pnpm dev # run via tsx (no build step)
267
+ pnpm build # compile TypeScript to dist/
268
+ pnpm test # run tests (vitest)
269
+ pnpm typecheck # type-check without emitting
270
+ pnpm checks # lint + format check (biome)
271
+ pnpm checks:fix # auto-fix lint + format issues
272
+ ```
273
+
274
+ ## License
275
+
276
+ MIT
@@ -0,0 +1,18 @@
1
+ interface CreateResult {
2
+ branch: string;
3
+ parent: string;
4
+ }
5
+ /**
6
+ * Creates a new branch stacked on top of the current branch.
7
+ *
8
+ * Records the parent-child relationship in dubstack state so the stack
9
+ * can be restacked later. Saves an undo entry before mutating.
10
+ *
11
+ * @param name - Name of the new branch to create
12
+ * @param cwd - Working directory (must be inside an initialized dubstack repo)
13
+ * @returns The created branch name and its parent
14
+ * @throws {DubError} If not initialized, branch exists, HEAD is detached, or repo is empty
15
+ */
16
+ export declare function create(name: string, cwd: string): Promise<CreateResult>;
17
+ export {};
18
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAKA,UAAU,YAAY;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAyB7E"}
@@ -0,0 +1,35 @@
1
+ import { DubError } from "../lib/errors.js";
2
+ import { branchExists, createBranch, getCurrentBranch } from "../lib/git.js";
3
+ import { addBranchToStack, readState, writeState } from "../lib/state.js";
4
+ import { saveUndoEntry } from "../lib/undo-log.js";
5
+ /**
6
+ * Creates a new branch stacked on top of the current branch.
7
+ *
8
+ * Records the parent-child relationship in dubstack state so the stack
9
+ * can be restacked later. Saves an undo entry before mutating.
10
+ *
11
+ * @param name - Name of the new branch to create
12
+ * @param cwd - Working directory (must be inside an initialized dubstack repo)
13
+ * @returns The created branch name and its parent
14
+ * @throws {DubError} If not initialized, branch exists, HEAD is detached, or repo is empty
15
+ */
16
+ export async function create(name, cwd) {
17
+ const state = await readState(cwd);
18
+ const parent = await getCurrentBranch(cwd);
19
+ if (await branchExists(name, cwd)) {
20
+ throw new DubError(`Branch '${name}' already exists.`);
21
+ }
22
+ await saveUndoEntry({
23
+ operation: "create",
24
+ timestamp: new Date().toISOString(),
25
+ previousBranch: parent,
26
+ previousState: structuredClone(state),
27
+ branchTips: {},
28
+ createdBranches: [name],
29
+ }, cwd);
30
+ await createBranch(name, cwd);
31
+ addBranchToStack(state, name, parent);
32
+ await writeState(state, cwd);
33
+ return { branch: name, parent };
34
+ }
35
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.js","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAOnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAY,EAAE,GAAW;IACrD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAE3C,IAAI,MAAM,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,QAAQ,CAAC,WAAW,IAAI,mBAAmB,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,aAAa,CAClB;QACC,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,cAAc,EAAE,MAAM;QACtB,aAAa,EAAE,eAAe,CAAC,KAAK,CAAC;QACrC,UAAU,EAAE,EAAE;QACd,eAAe,EAAE,CAAC,IAAI,CAAC;KACvB,EACD,GAAG,CACH,CAAC;IAEF,MAAM,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9B,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAE7B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACjC,CAAC"}
@@ -0,0 +1,18 @@
1
+ interface InitResult {
2
+ status: "created" | "already_exists";
3
+ gitignoreUpdated: boolean;
4
+ }
5
+ /**
6
+ * Initializes DubStack in the current git repository.
7
+ *
8
+ * Creates `.git/dubstack/state.json` with an empty state and ensures
9
+ * `.git/dubstack` is listed in `.gitignore`. Idempotent — safe to run
10
+ * multiple times.
11
+ *
12
+ * @param cwd - Working directory (must be inside a git repo)
13
+ * @returns Status indicating whether state was created or already existed
14
+ * @throws {DubError} If not inside a git repository
15
+ */
16
+ export declare function init(cwd: string): Promise<InitResult>;
17
+ export {};
18
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAMA,UAAU,UAAU;IACnB,MAAM,EAAE,SAAS,GAAG,gBAAgB,CAAC;IACrC,gBAAgB,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA2B3D"}
@@ -0,0 +1,41 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DubError } from "../lib/errors.js";
4
+ import { getRepoRoot, isGitRepo } from "../lib/git.js";
5
+ import { initState } from "../lib/state.js";
6
+ /**
7
+ * Initializes DubStack in the current git repository.
8
+ *
9
+ * Creates `.git/dubstack/state.json` with an empty state and ensures
10
+ * `.git/dubstack` is listed in `.gitignore`. Idempotent — safe to run
11
+ * multiple times.
12
+ *
13
+ * @param cwd - Working directory (must be inside a git repo)
14
+ * @returns Status indicating whether state was created or already existed
15
+ * @throws {DubError} If not inside a git repository
16
+ */
17
+ export async function init(cwd) {
18
+ if (!(await isGitRepo(cwd))) {
19
+ throw new DubError("Not a git repository. Run this command inside a git repo.");
20
+ }
21
+ const status = await initState(cwd);
22
+ const repoRoot = await getRepoRoot(cwd);
23
+ const gitignorePath = path.join(repoRoot, ".gitignore");
24
+ const entry = ".git/dubstack";
25
+ let gitignoreUpdated = false;
26
+ if (fs.existsSync(gitignorePath)) {
27
+ const content = fs.readFileSync(gitignorePath, "utf-8");
28
+ const lines = content.split("\n");
29
+ if (!lines.some((line) => line.trim() === entry)) {
30
+ const separator = content.endsWith("\n") ? "" : "\n";
31
+ fs.writeFileSync(gitignorePath, `${content}${separator}${entry}\n`);
32
+ gitignoreUpdated = true;
33
+ }
34
+ }
35
+ else {
36
+ fs.writeFileSync(gitignorePath, `${entry}\n`);
37
+ gitignoreUpdated = true;
38
+ }
39
+ return { status, gitignoreUpdated };
40
+ }
41
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAW;IACrC,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,QAAQ,CACjB,2DAA2D,CAC3D,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,eAAe,CAAC;IAC9B,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC;YAClD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YACrD,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,IAAI,CAAC,CAAC;YACpE,gBAAgB,GAAG,IAAI,CAAC;QACzB,CAAC;IACF,CAAC;SAAM,CAAC;QACP,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC;QAC9C,gBAAgB,GAAG,IAAI,CAAC;IACzB,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Renders an ASCII tree view of all tracked stacks.
3
+ *
4
+ * Highlights the current branch, marks branches missing from git,
5
+ * and handles multiple stacks separated by blank lines.
6
+ *
7
+ * @param cwd - Working directory (must be inside an initialized dubstack repo)
8
+ * @returns Formatted ASCII tree string (no ANSI colors — caller adds chalk)
9
+ * @throws {DubError} If not initialized
10
+ */
11
+ export declare function log(cwd: string): Promise<string>;
12
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/commands/log.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBtD"}
@@ -0,0 +1,77 @@
1
+ import { branchExists, getCurrentBranch } from "../lib/git.js";
2
+ import { readState } from "../lib/state.js";
3
+ /**
4
+ * Renders an ASCII tree view of all tracked stacks.
5
+ *
6
+ * Highlights the current branch, marks branches missing from git,
7
+ * and handles multiple stacks separated by blank lines.
8
+ *
9
+ * @param cwd - Working directory (must be inside an initialized dubstack repo)
10
+ * @returns Formatted ASCII tree string (no ANSI colors — caller adds chalk)
11
+ * @throws {DubError} If not initialized
12
+ */
13
+ export async function log(cwd) {
14
+ const state = await readState(cwd);
15
+ if (state.stacks.length === 0) {
16
+ return "No stacks. Run 'dub create' to start.";
17
+ }
18
+ let currentBranch = null;
19
+ try {
20
+ currentBranch = await getCurrentBranch(cwd);
21
+ }
22
+ catch {
23
+ // Detached HEAD or empty repo — no branch highlighted
24
+ }
25
+ const sections = [];
26
+ for (const stack of state.stacks) {
27
+ const tree = await renderStack(stack, currentBranch, cwd);
28
+ sections.push(tree);
29
+ }
30
+ return sections.join("\n\n");
31
+ }
32
+ async function renderStack(stack, currentBranch, cwd) {
33
+ const root = stack.branches.find((b) => b.type === "root");
34
+ if (!root)
35
+ return "";
36
+ const childMap = new Map();
37
+ for (const branch of stack.branches) {
38
+ if (branch.parent) {
39
+ const children = childMap.get(branch.parent) ?? [];
40
+ children.push(branch);
41
+ childMap.set(branch.parent, children);
42
+ }
43
+ }
44
+ const lines = [];
45
+ await renderNode(root, currentBranch, childMap, "", true, true, lines, cwd);
46
+ return lines.join("\n");
47
+ }
48
+ async function renderNode(branch, currentBranch, childMap, prefix, isRoot, isLast, lines, cwd) {
49
+ let label;
50
+ const exists = await branchExists(branch.name, cwd);
51
+ if (isRoot) {
52
+ label = `(${branch.name})`;
53
+ }
54
+ else if (branch.name === currentBranch) {
55
+ label = `*${branch.name} (Current)*`;
56
+ }
57
+ else if (!exists) {
58
+ label = `${branch.name} ⚠ (missing)`;
59
+ }
60
+ else {
61
+ label = branch.name;
62
+ }
63
+ if (isRoot) {
64
+ lines.push(label);
65
+ }
66
+ else {
67
+ const connector = isLast ? "└─ " : "├─ ";
68
+ lines.push(`${prefix}${connector}${label}`);
69
+ }
70
+ const children = childMap.get(branch.name) ?? [];
71
+ const childPrefix = isRoot ? " " : `${prefix}${isLast ? " " : "│ "}`;
72
+ for (let i = 0; i < children.length; i++) {
73
+ const isChildLast = i === children.length - 1;
74
+ await renderNode(children[i], currentBranch, childMap, childPrefix, false, isChildLast, lines, cwd);
75
+ }
76
+ }
77
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/commands/log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ interface RestackResult {
2
+ status: "success" | "conflict" | "up-to-date";
3
+ rebased: string[];
4
+ conflictBranch?: string;
5
+ }
6
+ /**
7
+ * Rebases all branches in the current stack onto their updated parents.
8
+ *
9
+ * Uses a snapshot-before-rebase strategy: captures every branch's tip
10
+ * BEFORE starting any rebases, then uses `git rebase --onto <parent_new_tip>
11
+ * <parent_old_tip> <child>`. This prevents the duplication bug where a child
12
+ * replays its parent's already-rebased commits.
13
+ *
14
+ * On conflict, writes progress to `restack-progress.json` so
15
+ * `dub restack --continue` can resume.
16
+ *
17
+ * @param cwd - Working directory
18
+ * @returns Result with status, list of rebased branches, and optional conflict branch
19
+ * @throws {DubError} If not initialized, dirty tree, not in a stack, or branch missing
20
+ */
21
+ export declare function restack(cwd: string): Promise<RestackResult>;
22
+ /**
23
+ * Continues a restack after conflict resolution.
24
+ *
25
+ * Reads the saved progress file, finishes the in-progress rebase,
26
+ * then resumes with remaining branches.
27
+ *
28
+ * @param cwd - Working directory
29
+ * @throws {DubError} If no restack is in progress
30
+ */
31
+ export declare function restackContinue(cwd: string): Promise<RestackResult>;
32
+ export {};
33
+ //# sourceMappingURL=restack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restack.d.ts","sourceRoot":"","sources":["../../src/commands/restack.ts"],"names":[],"mappings":"AA6BA,UAAU,aAAa;IACtB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAwDjE;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAezE"}