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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/commands/create.d.ts +18 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +35 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +41 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/log.d.ts +12 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +77 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/restack.d.ts +33 -0
- package/dist/commands/restack.d.ts.map +1 -0
- package/dist/commands/restack.js +190 -0
- package/dist/commands/restack.js.map +1 -0
- package/dist/commands/undo.d.ts +21 -0
- package/dist/commands/undo.d.ts.map +1 -0
- package/dist/commands/undo.js +63 -0
- package/dist/commands/undo.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +18 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/git.d.ts +69 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +184 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/state.d.ts +70 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +110 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/undo-log.d.ts +33 -0
- package/dist/lib/undo-log.d.ts.map +1 -0
- package/dist/lib/undo-log.js +37 -0
- package/dist/lib/undo-log.js.map +1 -0
- 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"}
|