git-stack-cli 1.0.6 → 1.1.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 +4 -7
- package/dist/cjs/index.cjs +355 -129
- package/package.json +4 -4
- package/{rollup.config.mjs → rollup.config.js} +11 -3
- package/scripts/git-sequence-editor.sh +35 -0
- package/scripts/release-brew.ts +7 -1
- package/src/app/DependencyCheck.tsx +150 -95
- package/src/app/LocalMergeRebase.tsx +1 -4
- package/src/app/ManualRebase.tsx +221 -87
- package/src/app/SelectCommitRanges.tsx +1 -0
- package/src/app/Store.tsx +14 -2
- package/src/command.ts +6 -0
- package/src/core/GitReviseTodo.test.ts +572 -0
- package/src/core/GitReviseTodo.ts +79 -0
- package/src/types/global.d.ts +2 -0
package/src/app/ManualRebase.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
4
6
|
|
|
5
7
|
import * as Ink from "ink-cjs";
|
|
6
8
|
|
|
@@ -9,6 +11,7 @@ import { Brackets } from "~/app/Brackets";
|
|
|
9
11
|
import { FormatText } from "~/app/FormatText";
|
|
10
12
|
import { Store } from "~/app/Store";
|
|
11
13
|
import * as CommitMetadata from "~/core/CommitMetadata";
|
|
14
|
+
import { GitReviseTodo } from "~/core/GitReviseTodo";
|
|
12
15
|
import * as Metadata from "~/core/Metadata";
|
|
13
16
|
import * as StackSummaryTable from "~/core/StackSummaryTable";
|
|
14
17
|
import { cli } from "~/core/cli";
|
|
@@ -68,20 +71,149 @@ async function run(props: Props) {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
if (i > 0) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
rebase_merge_base =
|
|
74
|
+
const prev_group = commit_range.group_list[i - 1];
|
|
75
|
+
const prev_commit = prev_group.commits[prev_group.commits.length - 1];
|
|
76
|
+
rebase_merge_base = prev_commit.sha;
|
|
74
77
|
rebase_group_index = i;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
break;
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
actions.debug(`rebase_merge_base=${rebase_merge_base}`);
|
|
84
|
+
actions.debug(`rebase_group_index=${rebase_group_index}`);
|
|
85
|
+
actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
|
|
86
|
+
|
|
80
87
|
try {
|
|
81
88
|
// must perform rebase from repo root for applying git patch
|
|
82
89
|
process.chdir(repo_root);
|
|
83
90
|
await cli(`pwd`);
|
|
84
91
|
|
|
92
|
+
if (argv["git-revise"]) {
|
|
93
|
+
await rebase_git_revise();
|
|
94
|
+
} else {
|
|
95
|
+
await rebase_cherry_pick();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// after all commits have been cherry-picked and amended
|
|
99
|
+
// move the branch pointer to the newly created temporary branch
|
|
100
|
+
// now we are in locally in sync with github and on the original branch
|
|
101
|
+
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
102
|
+
|
|
103
|
+
restore_git();
|
|
104
|
+
|
|
105
|
+
actions.set((state) => {
|
|
106
|
+
state.step = "post-rebase-status";
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
actions.error("Unable to rebase.");
|
|
110
|
+
|
|
111
|
+
if (err instanceof Error) {
|
|
112
|
+
if (actions.isDebug()) {
|
|
113
|
+
actions.error(err.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
handle_exit();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function rebase_git_revise() {
|
|
121
|
+
invariant(argv, "argv must exist");
|
|
122
|
+
|
|
123
|
+
actions.debug(`rebase_git_revise`);
|
|
124
|
+
|
|
125
|
+
actions.output(
|
|
126
|
+
<Ink.Text color={colors.yellow} wrap="truncate-end">
|
|
127
|
+
Rebasing…
|
|
128
|
+
</Ink.Text>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// generate temporary directory and drop sequence editor script
|
|
132
|
+
const tmp_git_sequence_editor_path = path.join(
|
|
133
|
+
os.tmpdir(),
|
|
134
|
+
"git-sequence-editor.sh"
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
actions.debug(
|
|
138
|
+
`tmp_git_sequence_editor_path=${tmp_git_sequence_editor_path}`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// replaced at build time with literal contents of `scripts/git-sequence-editor.sh`
|
|
142
|
+
const GIT_SEQUENCE_EDITOR_SCRIPT = `process.env.GIT_SEQUENCE_EDITOR_SCRIPT`;
|
|
143
|
+
|
|
144
|
+
// write script to temporary path
|
|
145
|
+
fs.writeFileSync(tmp_git_sequence_editor_path, GIT_SEQUENCE_EDITOR_SCRIPT);
|
|
146
|
+
|
|
147
|
+
// ensure script is executable
|
|
148
|
+
fs.chmodSync(tmp_git_sequence_editor_path, "755");
|
|
149
|
+
|
|
150
|
+
// create temporary branch
|
|
151
|
+
await cli(`git checkout -b ${temp_branch_name}`);
|
|
152
|
+
|
|
153
|
+
const git_revise_todo = GitReviseTodo({ rebase_group_index, commit_range });
|
|
154
|
+
|
|
155
|
+
// execute cli with temporary git sequence editor script
|
|
156
|
+
// revise from merge base to pick correct commits
|
|
157
|
+
await cli([
|
|
158
|
+
`GIT_EDITOR="${tmp_git_sequence_editor_path}"`,
|
|
159
|
+
`GIT_REVISE_TODO="${git_revise_todo}"`,
|
|
160
|
+
`git`,
|
|
161
|
+
`revise --edit -i ${rebase_merge_base}`,
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
// start from HEAD and work backward to rebase_group_index
|
|
165
|
+
const push_group_list = [];
|
|
166
|
+
let lookback_index = 0;
|
|
167
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
168
|
+
const index = commit_range.group_list.length - 1 - i;
|
|
169
|
+
|
|
170
|
+
// do not go past rebase_group_index
|
|
171
|
+
if (index < rebase_group_index) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const group = commit_range.group_list[index];
|
|
176
|
+
// console.debug({ i, index, group });
|
|
177
|
+
|
|
178
|
+
if (i > 0) {
|
|
179
|
+
const prev_group = commit_range.group_list[index + 1];
|
|
180
|
+
lookback_index += prev_group.commits.length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// console.debug(`git show head~${lookback_index}`);
|
|
184
|
+
|
|
185
|
+
// push group and lookback_index onto front of push_group_list
|
|
186
|
+
push_group_list.unshift({ group, lookback_index });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const pr_url_list = commit_range.group_list.map(get_group_url);
|
|
190
|
+
|
|
191
|
+
// use push_group_list to sync each group HEAD to github
|
|
192
|
+
for (const push_group of push_group_list) {
|
|
193
|
+
const { group } = push_group;
|
|
194
|
+
|
|
195
|
+
// move to temporary branch for resetting to lookback_index to create PR
|
|
196
|
+
await cli(`git checkout -b ${group.id}`);
|
|
197
|
+
|
|
198
|
+
// prepare branch for sync, reset to commit at lookback index
|
|
199
|
+
await cli(`git reset --hard HEAD~${push_group.lookback_index}`);
|
|
200
|
+
|
|
201
|
+
await sync_group_github({ group, pr_url_list, skip_checkout: true });
|
|
202
|
+
|
|
203
|
+
// done, remove temp push branch and move back to temp branch
|
|
204
|
+
await cli(`git checkout ${temp_branch_name}`);
|
|
205
|
+
await cli(`git branch -D ${group.id}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// finally, ensure all prs have the updated stack table from updated pr_url_list
|
|
209
|
+
await update_pr_tables(pr_url_list);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function rebase_cherry_pick() {
|
|
213
|
+
invariant(argv, "argv must exist");
|
|
214
|
+
|
|
215
|
+
actions.debug("rebase_cherry_pick");
|
|
216
|
+
|
|
85
217
|
// create temporary branch based on merge base
|
|
86
218
|
await cli(`git checkout -b ${temp_branch_name} ${rebase_merge_base}`);
|
|
87
219
|
|
|
@@ -104,8 +236,6 @@ async function run(props: Props) {
|
|
|
104
236
|
/>
|
|
105
237
|
);
|
|
106
238
|
|
|
107
|
-
const selected_url = get_group_url(group);
|
|
108
|
-
|
|
109
239
|
// cherry-pick and amend commits one by one
|
|
110
240
|
for (const commit of group.commits) {
|
|
111
241
|
// ensure clean base to avoid conflicts when applying patch
|
|
@@ -119,7 +249,7 @@ async function run(props: Props) {
|
|
|
119
249
|
// add all changes to stage
|
|
120
250
|
await cli(`git add --all`);
|
|
121
251
|
|
|
122
|
-
const new_message =
|
|
252
|
+
const new_message = Metadata.write(commit.full_message, group.id);
|
|
123
253
|
const git_commit_comand = [`git commit -m "${new_message}"`];
|
|
124
254
|
|
|
125
255
|
if (argv.verify === false) {
|
|
@@ -129,73 +259,98 @@ async function run(props: Props) {
|
|
|
129
259
|
await cli(git_commit_comand);
|
|
130
260
|
}
|
|
131
261
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
135
|
-
message="Syncing {group}…"
|
|
136
|
-
values={{
|
|
137
|
-
group: (
|
|
138
|
-
<Brackets>{group.pr?.title || group.title || group.id}</Brackets>
|
|
139
|
-
),
|
|
140
|
-
}}
|
|
141
|
-
/>
|
|
142
|
-
);
|
|
262
|
+
await sync_group_github({ group, pr_url_list, skip_checkout: false });
|
|
263
|
+
}
|
|
143
264
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
265
|
+
// finally, ensure all prs have the updated stack table from updated pr_url_list
|
|
266
|
+
await update_pr_tables(pr_url_list);
|
|
267
|
+
}
|
|
147
268
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
269
|
+
async function sync_group_github(args: {
|
|
270
|
+
group: CommitMetadataGroup;
|
|
271
|
+
pr_url_list: Array<string>;
|
|
272
|
+
skip_checkout: boolean;
|
|
273
|
+
}) {
|
|
274
|
+
if (props.skipSync) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { group, pr_url_list } = args;
|
|
279
|
+
|
|
280
|
+
invariant(argv, "argv must exist");
|
|
281
|
+
invariant(group.base, "group.base must exist");
|
|
282
|
+
|
|
283
|
+
actions.output(
|
|
284
|
+
<FormatText
|
|
285
|
+
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
286
|
+
message="Syncing {group}…"
|
|
287
|
+
values={{
|
|
288
|
+
group: (
|
|
289
|
+
<Brackets>{group.pr?.title || group.title || group.id}</Brackets>
|
|
290
|
+
),
|
|
291
|
+
}}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// push to origin since github requires commit shas to line up perfectly
|
|
296
|
+
const git_push_command = [`git push -f origin HEAD:${group.id}`];
|
|
297
|
+
|
|
298
|
+
if (argv.verify === false) {
|
|
299
|
+
git_push_command.push("--no-verify");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await cli(git_push_command);
|
|
303
|
+
|
|
304
|
+
const selected_url = get_group_url(group);
|
|
305
|
+
|
|
306
|
+
if (group.pr) {
|
|
307
|
+
// ensure base matches pr in github
|
|
308
|
+
await github.pr_edit({
|
|
309
|
+
branch: group.id,
|
|
310
|
+
base: group.base,
|
|
311
|
+
body: StackSummaryTable.write({
|
|
312
|
+
body: group.pr.body,
|
|
313
|
+
pr_url_list,
|
|
314
|
+
selected_url,
|
|
315
|
+
}),
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
if (!args.skip_checkout) {
|
|
319
|
+
// delete local group branch if leftover
|
|
320
|
+
await cli(`git branch -D ${group.id}`, { ignoreExitCode: true });
|
|
151
321
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const pr_url = await github.pr_create({
|
|
174
|
-
branch: group.id,
|
|
175
|
-
base: group.base,
|
|
176
|
-
title: group.title,
|
|
177
|
-
body: "",
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (!pr_url) {
|
|
181
|
-
throw new Error("unable to create pr");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// update pr_url_list with created pr_url
|
|
185
|
-
for (let i = 0; i < pr_url_list.length; i++) {
|
|
186
|
-
const url = pr_url_list[i];
|
|
187
|
-
if (url === selected_url) {
|
|
188
|
-
pr_url_list[i] = pr_url;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// move back to temp branch
|
|
193
|
-
await cli(`git checkout ${temp_branch_name}`);
|
|
322
|
+
// move to temporary branch for creating pr
|
|
323
|
+
await cli(`git checkout -b ${group.id}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// create pr in github
|
|
327
|
+
const pr_url = await github.pr_create({
|
|
328
|
+
branch: group.id,
|
|
329
|
+
base: group.base,
|
|
330
|
+
title: group.title,
|
|
331
|
+
body: "",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (!pr_url) {
|
|
335
|
+
throw new Error("unable to create pr");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// update pr_url_list with created pr_url
|
|
339
|
+
for (let i = 0; i < pr_url_list.length; i++) {
|
|
340
|
+
const url = pr_url_list[i];
|
|
341
|
+
if (url === selected_url) {
|
|
342
|
+
pr_url_list[i] = pr_url;
|
|
194
343
|
}
|
|
195
344
|
}
|
|
345
|
+
|
|
346
|
+
// move back to temp branch
|
|
347
|
+
if (!args.skip_checkout) {
|
|
348
|
+
await cli(`git checkout ${temp_branch_name}`);
|
|
349
|
+
}
|
|
196
350
|
}
|
|
351
|
+
}
|
|
197
352
|
|
|
198
|
-
|
|
353
|
+
async function update_pr_tables(pr_url_list: Array<string>) {
|
|
199
354
|
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
200
355
|
const group = commit_range.group_list[i];
|
|
201
356
|
|
|
@@ -224,27 +379,6 @@ async function run(props: Props) {
|
|
|
224
379
|
});
|
|
225
380
|
}
|
|
226
381
|
}
|
|
227
|
-
|
|
228
|
-
// after all commits have been cherry-picked and amended
|
|
229
|
-
// move the branch pointer to the newly created temporary branch
|
|
230
|
-
// now we are in locally in sync with github and on the original branch
|
|
231
|
-
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
232
|
-
|
|
233
|
-
restore_git();
|
|
234
|
-
|
|
235
|
-
actions.set((state) => {
|
|
236
|
-
state.step = "post-rebase-status";
|
|
237
|
-
});
|
|
238
|
-
} catch (err) {
|
|
239
|
-
actions.error("Unable to rebase.");
|
|
240
|
-
|
|
241
|
-
if (err instanceof Error) {
|
|
242
|
-
if (actions.isDebug()) {
|
|
243
|
-
actions.error(err.message);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
handle_exit();
|
|
248
382
|
}
|
|
249
383
|
|
|
250
384
|
// cleanup git operations if cancelled during manual rebase
|
package/src/app/Store.tsx
CHANGED
|
@@ -21,6 +21,7 @@ type MutateOutputArgs = {
|
|
|
21
21
|
node: React.ReactNode;
|
|
22
22
|
id?: string;
|
|
23
23
|
debug?: boolean;
|
|
24
|
+
withoutTimestamp?: boolean;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export type State = {
|
|
@@ -194,11 +195,22 @@ const BaseStore = createStore<State>()(
|
|
|
194
195
|
return;
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
// set `withoutTimestamp` to skip <LogTimestamp> for all subsequent pending outputs
|
|
199
|
+
// we only want to timestamp for the first part (when we initialize the [])
|
|
200
|
+
// if we have many incremental outputs on the same line we do not want multiple timestamps
|
|
201
|
+
//
|
|
202
|
+
// await Promise.all([
|
|
203
|
+
// cli(`for i in $(seq 1 5); do echo $i; sleep 1; done`),
|
|
204
|
+
// cli(`for i in $(seq 5 1); do printf "$i "; sleep 1; done; echo`),
|
|
205
|
+
// ]);
|
|
206
|
+
//
|
|
207
|
+
let withoutTimestamp = true;
|
|
197
208
|
if (!state.pending_output[id]) {
|
|
209
|
+
withoutTimestamp = false;
|
|
198
210
|
state.pending_output[id] = [];
|
|
199
211
|
}
|
|
200
212
|
|
|
201
|
-
const renderOutput = renderOutputArgs(args);
|
|
213
|
+
const renderOutput = renderOutputArgs({ ...args, withoutTimestamp });
|
|
202
214
|
state.pending_output[id].push(renderOutput);
|
|
203
215
|
},
|
|
204
216
|
|
|
@@ -228,7 +240,7 @@ function renderOutputArgs(args: MutateOutputArgs) {
|
|
|
228
240
|
if (args.debug) {
|
|
229
241
|
return (
|
|
230
242
|
<React.Fragment>
|
|
231
|
-
<LogTimestamp />
|
|
243
|
+
{args.withoutTimestamp ? null : <LogTimestamp />}
|
|
232
244
|
{output}
|
|
233
245
|
</React.Fragment>
|
|
234
246
|
);
|
package/src/command.ts
CHANGED
|
@@ -29,6 +29,12 @@ export async function command() {
|
|
|
29
29
|
description: "Skip git hooks such as pre-commit and pre-push",
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
.option("git-revise", {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
default: false,
|
|
35
|
+
description: `Use git-revise to perform in-memory rebase, (macOS + Linux only)`,
|
|
36
|
+
})
|
|
37
|
+
|
|
32
38
|
.option("verbose", {
|
|
33
39
|
type: "boolean",
|
|
34
40
|
alias: ["v"],
|