git-stack-cli 2.7.8 → 2.8.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/dist/js/index.js +75 -75
- package/package.json +1 -1
- package/scripts/bun-build.ts +3 -2
- package/scripts/release-npm.ts +6 -4
- package/src/app/ManualRebase.tsx +69 -14
- package/src/app/MultiSelect.tsx +19 -0
- package/src/app/SelectCommitRanges.tsx +63 -8
- package/src/app/SyncGithub.tsx +96 -47
- package/src/core/CommitMetadata.ts +30 -5
- package/src/core/GitReviseTodo.test.ts +46 -0
- package/src/core/GitReviseTodo.ts +6 -1
- package/src/core/Metadata.test.ts +51 -0
- package/src/core/Metadata.ts +24 -1
- package/src/core/__snapshots__/git.test.ts.snap +4 -0
- package/src/core/git.ts +15 -1
- package/src/core/github.tsx +21 -4
package/package.json
CHANGED
package/scripts/bun-build.ts
CHANGED
|
@@ -67,10 +67,11 @@ async function run_build() {
|
|
|
67
67
|
const result = await Bun.build(BUILD_CONFIG);
|
|
68
68
|
|
|
69
69
|
const duration_ms = Date.now() - start;
|
|
70
|
+
const status = result.success ? "✅" : "❌";
|
|
70
71
|
|
|
71
|
-
log(
|
|
72
|
+
log(`${status} build (${duration_ms}ms)`);
|
|
72
73
|
|
|
73
|
-
if (VERBOSE) {
|
|
74
|
+
if (VERBOSE || !result.success) {
|
|
74
75
|
log({ result });
|
|
75
76
|
}
|
|
76
77
|
}
|
package/scripts/release-npm.ts
CHANGED
|
@@ -18,10 +18,9 @@ process.chdir(REPO_ROOT);
|
|
|
18
18
|
|
|
19
19
|
// require clean git status besides changes to package.json version
|
|
20
20
|
const git_status = await spawn.sync("git status --porcelain");
|
|
21
|
-
if (!/^M\s+package.json
|
|
22
|
-
console.error(
|
|
23
|
-
|
|
24
|
-
);
|
|
21
|
+
if (!/^M\s+package.json$/.test(git_status.stdout)) {
|
|
22
|
+
console.error("please commit local changes");
|
|
23
|
+
console.error("only update package.json version before running release");
|
|
25
24
|
process.exit(4);
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -55,6 +54,9 @@ for (const filepath of package_json.files) {
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
57
|
+
// ensure npm is authenticated
|
|
58
|
+
await spawn(`npm whoami`);
|
|
59
|
+
|
|
58
60
|
process.chdir(REPO_ROOT);
|
|
59
61
|
|
|
60
62
|
await spawn.sync(`git commit -a -m ${version}`);
|
package/src/app/ManualRebase.tsx
CHANGED
|
@@ -57,6 +57,7 @@ async function run() {
|
|
|
57
57
|
const group_from_map = commit_map[commit.sha];
|
|
58
58
|
commit.branch_id = group_from_map.id;
|
|
59
59
|
commit.title = group_from_map.title;
|
|
60
|
+
commit.master_base = group_from_map.master_base;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// // capture commit_range for GitReviseTodo test
|
|
@@ -78,21 +79,12 @@ async function run() {
|
|
|
78
79
|
let rebase_merge_base = merge_base;
|
|
79
80
|
let rebase_group_index = 0;
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
const group = commit_range.group_list[i];
|
|
82
|
+
inplace_order_commit_range_groups(commit_range);
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (i > 0) {
|
|
89
|
-
const prev_group = commit_range.group_list[i - 1];
|
|
90
|
-
const prev_commit = prev_group.commits[prev_group.commits.length - 1];
|
|
91
|
-
rebase_merge_base = prev_commit.sha;
|
|
92
|
-
rebase_group_index = i;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
break;
|
|
84
|
+
const dirty = find_first_dirty_group(commit_range);
|
|
85
|
+
if (dirty) {
|
|
86
|
+
rebase_merge_base = dirty.sha;
|
|
87
|
+
rebase_group_index = dirty.index;
|
|
96
88
|
}
|
|
97
89
|
|
|
98
90
|
actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
|
|
@@ -185,3 +177,66 @@ async function run() {
|
|
|
185
177
|
);
|
|
186
178
|
}
|
|
187
179
|
}
|
|
180
|
+
|
|
181
|
+
function inplace_order_commit_range_groups(commit_range: CommitMetadata.CommitRange) {
|
|
182
|
+
const state = Store.getState();
|
|
183
|
+
const actions = state.actions;
|
|
184
|
+
|
|
185
|
+
// order groups with group.master_base first
|
|
186
|
+
const group_list_master: CommitGroupList = [];
|
|
187
|
+
const group_list_others: CommitGroupList = [];
|
|
188
|
+
for (const group of commit_range.group_list) {
|
|
189
|
+
if (group.master_base) {
|
|
190
|
+
group_list_master.push(group);
|
|
191
|
+
} else {
|
|
192
|
+
group_list_others.push(group);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ordered_group_list = [...group_list_master, ...group_list_others];
|
|
197
|
+
|
|
198
|
+
// detect if group list order differs
|
|
199
|
+
let differs = false;
|
|
200
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
201
|
+
const original_group = commit_range.group_list[i];
|
|
202
|
+
const ordered_group = ordered_group_list[i];
|
|
203
|
+
if (original_group.id !== ordered_group.id) {
|
|
204
|
+
ordered_group.dirty = true;
|
|
205
|
+
const debug = JSON.stringify({ original_group, ordered_group });
|
|
206
|
+
actions.debug(`inplace_order_commit_range_groups ${debug}`);
|
|
207
|
+
|
|
208
|
+
differs = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (differs) {
|
|
214
|
+
commit_range.group_list = ordered_group_list;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return differs;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function find_first_dirty_group(commit_range: CommitMetadata.CommitRange) {
|
|
221
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
222
|
+
const group = commit_range.group_list[i];
|
|
223
|
+
|
|
224
|
+
if (!group.dirty) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (i > 0) {
|
|
229
|
+
const prev_group = commit_range.group_list[i - 1];
|
|
230
|
+
const prev_commit = prev_group.commits[prev_group.commits.length - 1];
|
|
231
|
+
const sha = prev_commit.sha;
|
|
232
|
+
const index = i;
|
|
233
|
+
return { sha, index };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
type CommitGroupList = CommitMetadata.CommitRange["group_list"];
|
package/src/app/MultiSelect.tsx
CHANGED
|
@@ -87,6 +87,25 @@ export function MultiSelect<T>(props: Props<T>) {
|
|
|
87
87
|
},
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
+
// ensure current index is valid and not disabled
|
|
91
|
+
React.useEffect(ensure_selectable_focus, [props.items]);
|
|
92
|
+
function ensure_selectable_focus() {
|
|
93
|
+
const current = props.items[index];
|
|
94
|
+
if (current && !current.disabled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
99
|
+
// search items backwards
|
|
100
|
+
const index = props.items.length - 1 - i;
|
|
101
|
+
const item = props.items[index];
|
|
102
|
+
if (!item.disabled) {
|
|
103
|
+
set_index(index);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
const selectRef = React.useRef(false);
|
|
91
110
|
|
|
92
111
|
React.useEffect(() => {
|
|
@@ -15,6 +15,9 @@ import { short_id } from "~/core/short_id";
|
|
|
15
15
|
import { wrap_index } from "~/core/wrap_index";
|
|
16
16
|
|
|
17
17
|
import type { State } from "~/app/Store";
|
|
18
|
+
import type * as CommitMetadata from "~/core/CommitMetadata";
|
|
19
|
+
|
|
20
|
+
type CommitRangeGroup = NonNullable<Parameters<typeof CommitMetadata.range>[0]>[string];
|
|
18
21
|
|
|
19
22
|
export function SelectCommitRanges() {
|
|
20
23
|
const commit_range = Store.useState((state) => state.commit_range);
|
|
@@ -62,6 +65,23 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
62
65
|
[],
|
|
63
66
|
);
|
|
64
67
|
|
|
68
|
+
const [group_master_base, set_group_master_base] = React.useReducer(
|
|
69
|
+
(set: Set<string>, group_id: string) => {
|
|
70
|
+
set.has(group_id) ? set.delete(group_id) : set.add(group_id);
|
|
71
|
+
return new Set(set);
|
|
72
|
+
},
|
|
73
|
+
new Set<string>(),
|
|
74
|
+
(set) => {
|
|
75
|
+
for (const group of props.commit_range.group_list) {
|
|
76
|
+
if (group.master_base) {
|
|
77
|
+
set.add(group.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Set(set);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
65
85
|
const [commit_map, update_commit_map] = React.useReducer(
|
|
66
86
|
(map: Map<string, null | string>, args: { key: string; value: null | string }) => {
|
|
67
87
|
map.set(args.key, args.value);
|
|
@@ -84,18 +104,18 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
84
104
|
Ink.useInput((input, key) => {
|
|
85
105
|
const input_lower = input.toLowerCase();
|
|
86
106
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
107
|
+
// do not allow input when inputting group title
|
|
108
|
+
if (group_input) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
92
111
|
|
|
112
|
+
if (input_lower === SYMBOL.s) {
|
|
93
113
|
if (sync_status === "disabled") {
|
|
94
114
|
return;
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
actions.set((state) => {
|
|
98
|
-
const state_commit_map: Record<string,
|
|
118
|
+
const state_commit_map: Record<string, CommitRangeGroup> = {};
|
|
99
119
|
|
|
100
120
|
for (let [sha, id] of commit_map.entries()) {
|
|
101
121
|
// console.debug({ sha, id });
|
|
@@ -104,14 +124,15 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
104
124
|
if (!id) {
|
|
105
125
|
id = props.commit_range.UNASSIGNED;
|
|
106
126
|
const title = "allow_unassigned";
|
|
107
|
-
state_commit_map[sha] = { id, title };
|
|
127
|
+
state_commit_map[sha] = { id, title, master_base: false };
|
|
108
128
|
continue;
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
const group = group_list.find((g) => g.id === id);
|
|
112
132
|
invariant(group, "group must exist");
|
|
113
133
|
// console.debug({ group });
|
|
114
|
-
|
|
134
|
+
const master_base = group_master_base.has(id);
|
|
135
|
+
state_commit_map[sha] = { ...group, master_base };
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
state.commit_map = state_commit_map;
|
|
@@ -127,6 +148,13 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
127
148
|
return;
|
|
128
149
|
}
|
|
129
150
|
|
|
151
|
+
// only allow setting base branch when on a created group
|
|
152
|
+
if (group.id !== props.commit_range.UNASSIGNED && input_lower === SYMBOL.m) {
|
|
153
|
+
const group = group_list[current_index];
|
|
154
|
+
set_group_master_base(group.id);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
130
158
|
if (key.leftArrow) {
|
|
131
159
|
const new_index = wrap_index(current_index - 1, group_list);
|
|
132
160
|
const next_group = group_list[new_index];
|
|
@@ -196,6 +224,7 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
196
224
|
// console.debug({ sync_status });
|
|
197
225
|
|
|
198
226
|
const group = group_list[current_index];
|
|
227
|
+
const is_master_base = group_master_base.has(group.id);
|
|
199
228
|
|
|
200
229
|
const multiselect_disabled = group_input;
|
|
201
230
|
const multiselect_disableSelect = group.id === props.commit_range.UNASSIGNED;
|
|
@@ -281,6 +310,10 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
281
310
|
<Ink.Text wrap="truncate-end" bold color={colors.white}>
|
|
282
311
|
{group.title}
|
|
283
312
|
</Ink.Text>
|
|
313
|
+
{!is_master_base ? null : (
|
|
314
|
+
// show base master
|
|
315
|
+
<Ink.Text color={colors.yellow}> (base: master)</Ink.Text>
|
|
316
|
+
)}
|
|
284
317
|
</Ink.Box>
|
|
285
318
|
</React.Fragment>
|
|
286
319
|
)}
|
|
@@ -445,6 +478,27 @@ function SelectCommitRangesInternal(props: Props) {
|
|
|
445
478
|
}}
|
|
446
479
|
/>
|
|
447
480
|
</Ink.Box>
|
|
481
|
+
|
|
482
|
+
{group.id === props.commit_range.UNASSIGNED ? null : (
|
|
483
|
+
<Ink.Box>
|
|
484
|
+
<FormatText
|
|
485
|
+
wrapper={<Ink.Text color={colors.gray} />}
|
|
486
|
+
message={
|
|
487
|
+
is_master_base
|
|
488
|
+
? "Press {m} to {reset} current PR base to stack position"
|
|
489
|
+
: "Press {m} to set current PR base to master"
|
|
490
|
+
}
|
|
491
|
+
values={{
|
|
492
|
+
m: (
|
|
493
|
+
<Ink.Text bold color={colors.green}>
|
|
494
|
+
{SYMBOL.m}
|
|
495
|
+
</Ink.Text>
|
|
496
|
+
),
|
|
497
|
+
reset: <Ink.Text color={colors.yellow}>reset</Ink.Text>,
|
|
498
|
+
}}
|
|
499
|
+
/>
|
|
500
|
+
</Ink.Box>
|
|
501
|
+
)}
|
|
448
502
|
</Ink.Box>
|
|
449
503
|
);
|
|
450
504
|
|
|
@@ -518,6 +572,7 @@ const SYMBOL = {
|
|
|
518
572
|
enter: "Enter",
|
|
519
573
|
c: "c",
|
|
520
574
|
s: "s",
|
|
575
|
+
m: "m",
|
|
521
576
|
};
|
|
522
577
|
|
|
523
578
|
const S_TO_SYNC_VALUES = {
|
package/src/app/SyncGithub.tsx
CHANGED
|
@@ -54,6 +54,17 @@ async function run() {
|
|
|
54
54
|
// 2 create PR / edit PR
|
|
55
55
|
// --------------------------------------
|
|
56
56
|
|
|
57
|
+
function create_git_push_command(base: string, target: string) {
|
|
58
|
+
const command = [`${base} push -f origin`];
|
|
59
|
+
|
|
60
|
+
if (argv.verify === false) {
|
|
61
|
+
command.push("--no-verify");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
command.push(target);
|
|
65
|
+
return command;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
try {
|
|
58
69
|
const before_push_tasks = [];
|
|
59
70
|
for (const group of push_group_list) {
|
|
@@ -62,16 +73,18 @@ async function run() {
|
|
|
62
73
|
|
|
63
74
|
await Promise.all(before_push_tasks);
|
|
64
75
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
if (argv.verify === false) {
|
|
68
|
-
git_push_command.push("--no-verify");
|
|
69
|
-
}
|
|
76
|
+
const git_push_target_list: Array<string> = [];
|
|
70
77
|
|
|
71
78
|
for (const group of push_group_list) {
|
|
72
79
|
const last_commit = last(group.commits);
|
|
73
80
|
invariant(last_commit, "last_commit must exist");
|
|
74
81
|
|
|
82
|
+
// push group in isolation if master_base is set
|
|
83
|
+
if (group.master_base) {
|
|
84
|
+
await push_master_group(group);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
// explicit refs/heads head branch to avoid push failing
|
|
76
89
|
//
|
|
77
90
|
// ❯ git push -f origin --no-verify f6e249051b4820a03deb957ddebc19acfd7dfd7c:gs-ED2etrzv2
|
|
@@ -90,29 +103,49 @@ async function run() {
|
|
|
90
103
|
// error: failed to push some refs to 'github.com:magus/git-multi-diff-playground.git'
|
|
91
104
|
//
|
|
92
105
|
const target = `${last_commit.sha}:refs/heads/${group.id}`;
|
|
93
|
-
|
|
106
|
+
git_push_target_list.push(target);
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
|
|
109
|
+
if (git_push_target_list.length > 0) {
|
|
110
|
+
const push_target = git_push_target_list.join(" ");
|
|
111
|
+
const git_push_command = create_git_push_command("git", push_target);
|
|
112
|
+
await cli(git_push_command);
|
|
113
|
+
}
|
|
97
114
|
|
|
98
|
-
const
|
|
115
|
+
const pr_url_by_group_id: Record<string, string> = {};
|
|
99
116
|
|
|
100
117
|
const after_push_tasks = [];
|
|
101
118
|
for (const group of push_group_list) {
|
|
102
|
-
after_push_tasks.push(after_push({ group,
|
|
119
|
+
after_push_tasks.push(after_push({ group, pr_url_by_group_id }));
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
await Promise.all(after_push_tasks);
|
|
106
123
|
|
|
107
|
-
// finally, ensure all prs have the updated stack table from updated
|
|
124
|
+
// finally, ensure all prs have the updated stack table from updated pr_url_by_group_id
|
|
108
125
|
// this step must come after the after_push since that step may create new PRs
|
|
109
126
|
// we need the urls for all prs at this step so we run it after the after_push
|
|
127
|
+
const all_pr_groups: Array<CommitMetadataGroup> = [];
|
|
128
|
+
// collect all groups and existing pr urls
|
|
129
|
+
for (const group of commit_range.group_list) {
|
|
130
|
+
if (group.id !== commit_range.UNASSIGNED) {
|
|
131
|
+
// collect all groups
|
|
132
|
+
all_pr_groups.push(group);
|
|
133
|
+
|
|
134
|
+
if (group.pr) {
|
|
135
|
+
pr_url_by_group_id[group.id] = group.pr.url;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// get pr url list for all pr groups
|
|
141
|
+
const pr_url_list = all_pr_groups.map((g) => pr_url_by_group_id[g.id]);
|
|
142
|
+
|
|
143
|
+
// update PR body for all pr groups (not just push_group_list)
|
|
110
144
|
const update_pr_body_tasks = [];
|
|
111
|
-
for (let i = 0; i <
|
|
112
|
-
const group =
|
|
145
|
+
for (let i = 0; i < all_pr_groups.length; i++) {
|
|
146
|
+
const group = all_pr_groups[i];
|
|
113
147
|
|
|
114
|
-
|
|
115
|
-
const selected_url = pr_url_list[i];
|
|
148
|
+
const selected_url = pr_url_by_group_id[group.id];
|
|
116
149
|
|
|
117
150
|
const task = update_pr_body({ group, selected_url, pr_url_list });
|
|
118
151
|
update_pr_body_tasks.push(task);
|
|
@@ -183,7 +216,7 @@ async function run() {
|
|
|
183
216
|
// Unable to sync.
|
|
184
217
|
// ```
|
|
185
218
|
//
|
|
186
|
-
if (
|
|
219
|
+
if (!is_master_base(group)) {
|
|
187
220
|
await github.pr_edit({
|
|
188
221
|
branch: group.id,
|
|
189
222
|
base: master_branch,
|
|
@@ -192,34 +225,20 @@ async function run() {
|
|
|
192
225
|
}
|
|
193
226
|
}
|
|
194
227
|
|
|
195
|
-
async function after_push(args: {
|
|
196
|
-
|
|
228
|
+
async function after_push(args: {
|
|
229
|
+
group: CommitMetadataGroup;
|
|
230
|
+
pr_url_by_group_id: Record<string, string>;
|
|
231
|
+
}) {
|
|
232
|
+
const { group } = args;
|
|
197
233
|
|
|
198
234
|
invariant(group.base, "group.base must exist");
|
|
199
235
|
|
|
200
|
-
const selected_url = get_group_url(group);
|
|
201
|
-
|
|
202
236
|
if (group.pr) {
|
|
203
|
-
if (
|
|
237
|
+
if (!is_master_base(group)) {
|
|
204
238
|
// ensure base matches pr in github
|
|
205
|
-
await github.pr_edit({
|
|
206
|
-
branch: group.id,
|
|
207
|
-
base: group.base,
|
|
208
|
-
body: StackSummaryTable.write({
|
|
209
|
-
body: group.pr.body,
|
|
210
|
-
pr_url_list,
|
|
211
|
-
selected_url,
|
|
212
|
-
}),
|
|
213
|
-
});
|
|
239
|
+
await github.pr_edit({ branch: group.id, base: group.base });
|
|
214
240
|
} else {
|
|
215
|
-
await github.pr_edit({
|
|
216
|
-
branch: group.id,
|
|
217
|
-
body: StackSummaryTable.write({
|
|
218
|
-
body: group.pr.body,
|
|
219
|
-
pr_url_list,
|
|
220
|
-
selected_url,
|
|
221
|
-
}),
|
|
222
|
-
});
|
|
241
|
+
await github.pr_edit({ branch: group.id });
|
|
223
242
|
}
|
|
224
243
|
} else {
|
|
225
244
|
// create pr in github
|
|
@@ -235,13 +254,8 @@ async function run() {
|
|
|
235
254
|
throw new Error("unable to create pr");
|
|
236
255
|
}
|
|
237
256
|
|
|
238
|
-
// update
|
|
239
|
-
|
|
240
|
-
const url = pr_url_list[i];
|
|
241
|
-
if (url === selected_url) {
|
|
242
|
-
pr_url_list[i] = pr_url;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
257
|
+
// update pr_url_by_group_id with created pr_url
|
|
258
|
+
args.pr_url_by_group_id[group.id] = pr_url;
|
|
245
259
|
}
|
|
246
260
|
}
|
|
247
261
|
|
|
@@ -262,10 +276,12 @@ async function run() {
|
|
|
262
276
|
selected_url,
|
|
263
277
|
});
|
|
264
278
|
|
|
279
|
+
const debug_meta = `${group.id} ${selected_url}`;
|
|
280
|
+
|
|
265
281
|
if (update_body === body) {
|
|
266
|
-
actions.debug(`Skipping body update
|
|
282
|
+
actions.debug(`Skipping body update ${debug_meta}`);
|
|
267
283
|
} else {
|
|
268
|
-
actions.debug(`Update body
|
|
284
|
+
actions.debug(`Update body ${debug_meta}`);
|
|
269
285
|
|
|
270
286
|
await github.pr_edit({
|
|
271
287
|
branch: group.id,
|
|
@@ -274,7 +290,40 @@ async function run() {
|
|
|
274
290
|
});
|
|
275
291
|
}
|
|
276
292
|
}
|
|
293
|
+
|
|
294
|
+
function is_master_base(group: CommitMetadataGroup) {
|
|
295
|
+
if (!group.pr) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return group.master_base || `origin/${group.pr.baseRefName}` === master_branch;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function push_master_group(group: CommitMetadataGroup) {
|
|
303
|
+
const worktree_path = `.git/git-stack-worktrees/push_master_group`;
|
|
304
|
+
|
|
305
|
+
// ensure previous instance of worktree is removed
|
|
306
|
+
await cli(`git worktree remove --force ${worktree_path}`, { ignoreExitCode: true });
|
|
307
|
+
|
|
308
|
+
// create temp worktree at master (or group.base if you prefer)
|
|
309
|
+
await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// cherry-pick the group commits onto that base
|
|
313
|
+
const cp_commit_list = group.commits.map((c) => c.sha);
|
|
314
|
+
await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
|
|
315
|
+
|
|
316
|
+
`git -C ${worktree_path} push -f origin HEAD:refs/heads/${group.id}`;
|
|
317
|
+
|
|
318
|
+
const push_target = `HEAD:refs/heads/${group.id}`;
|
|
319
|
+
const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
|
|
320
|
+
|
|
321
|
+
await cli(git_push_command);
|
|
322
|
+
} finally {
|
|
323
|
+
// clean up even if push fails
|
|
324
|
+
await cli(`git worktree remove --force ${worktree_path}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
277
327
|
}
|
|
278
328
|
|
|
279
329
|
type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
|
|
280
|
-
const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
|
|
@@ -14,10 +14,16 @@ type CommitGroup = {
|
|
|
14
14
|
base: null | string;
|
|
15
15
|
dirty: boolean;
|
|
16
16
|
commits: Array<git.Commit>;
|
|
17
|
+
master_base: boolean;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
type CommitRangeGroup = {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
master_base: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CommitGroupMap = { [sha: string]: CommitRangeGroup };
|
|
21
27
|
|
|
22
28
|
export async function range(commit_group_map?: CommitGroupMap) {
|
|
23
29
|
// gather all open prs in repo first
|
|
@@ -25,6 +31,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
25
31
|
await github.pr_list();
|
|
26
32
|
|
|
27
33
|
const master_branch = Store.getState().master_branch;
|
|
34
|
+
const master_branch_name = master_branch.replace(/^origin\//, "");
|
|
28
35
|
const commit_list = await git.get_commits(`${master_branch}..HEAD`);
|
|
29
36
|
|
|
30
37
|
const pr_lookup: Record<string, void | PullRequest> = {};
|
|
@@ -37,6 +44,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
37
44
|
for (const commit of commit_list) {
|
|
38
45
|
let id = commit.branch_id;
|
|
39
46
|
let title = commit.title || id;
|
|
47
|
+
let master_base = commit.master_base;
|
|
40
48
|
|
|
41
49
|
// console.debug({ commit, id });
|
|
42
50
|
|
|
@@ -47,6 +55,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
47
55
|
if (group) {
|
|
48
56
|
id = group.id;
|
|
49
57
|
title = group.title;
|
|
58
|
+
master_base = group.master_base;
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -75,6 +84,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
75
84
|
const group = group_map.get(id) || {
|
|
76
85
|
id,
|
|
77
86
|
title,
|
|
87
|
+
master_base,
|
|
78
88
|
pr: null,
|
|
79
89
|
base: null,
|
|
80
90
|
dirty: false,
|
|
@@ -130,17 +140,20 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
if (i === 0) {
|
|
133
|
-
const master_branch_name = master_branch.replace(/^origin\//, "");
|
|
134
143
|
group.base = master_branch_name;
|
|
135
144
|
} else {
|
|
136
145
|
const last_group = group_value_list[i - 1];
|
|
137
146
|
// console.debug(" ", "last_group", last_group.pr?.title.substring(0, 40));
|
|
138
147
|
// console.debug(" ", "last_group.id", last_group.id);
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
if (group.master_base) {
|
|
150
|
+
// explicitly set base to master when master_base is true
|
|
151
|
+
group.base = master_branch_name;
|
|
152
|
+
} else if (group.id === UNASSIGNED) {
|
|
153
|
+
// null out base when unassigned and after unassigned
|
|
142
154
|
group.base = null;
|
|
143
155
|
} else if (last_group.base === null) {
|
|
156
|
+
// null out base when last group base is null
|
|
144
157
|
group.base = null;
|
|
145
158
|
} else {
|
|
146
159
|
group.base = last_group.id;
|
|
@@ -155,7 +168,19 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
155
168
|
group.dirty = true;
|
|
156
169
|
} else if (group.pr.baseRefName !== group.base) {
|
|
157
170
|
group.dirty = true;
|
|
171
|
+
} else if (group.master_base) {
|
|
172
|
+
group.pr.number;
|
|
173
|
+
// master_base groups cannot be compared by commit sha
|
|
174
|
+
// instead compare the literal diff local against origin
|
|
175
|
+
// gh pr diff --color=never 110
|
|
176
|
+
// git --no-pager diff --color=never 00c8fe0~1..00c8fe0 | pbcopy
|
|
177
|
+
const diff_github = await github.pr_diff(group.pr.number);
|
|
178
|
+
const diff_local = await git.get_diff(group.commits);
|
|
179
|
+
if (diff_github !== diff_local) {
|
|
180
|
+
group.dirty = true;
|
|
181
|
+
}
|
|
158
182
|
} else {
|
|
183
|
+
// comapre literal commit shas in group
|
|
159
184
|
for (let i = 0; i < group.pr.commits.length; i++) {
|
|
160
185
|
const pr_commit = group.pr.commits[i];
|
|
161
186
|
const local_commit = group.commits[i];
|