git-stack-cli 1.0.7 → 1.2.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.
@@ -0,0 +1,35 @@
1
+ #!/bin/sh
2
+
3
+ # Example
4
+ #
5
+ # GIT_REVISE_TODO="abc" GIT_EDITOR="$(pwd)/scripts/git-sequence-editor.sh" git revise --edit -i head~4
6
+ #
7
+ # Note
8
+ # ----------------
9
+ # Use `GIT_EDITOR` above instead of `GIT_SEQUENCE_EDITOR` because `git revise` does not use
10
+ # `GIT_SEQUENCE_EDITOR` when passing the `--edit` flag, but does work without the `--edit` flag
11
+ #
12
+ #
13
+
14
+ # debug print env variables
15
+ echo "GIT_REVISE_TODO=$GIT_REVISE_TODO"
16
+ echo "CLI=$0 $*"
17
+ echo "PWD=$(pwd)"
18
+
19
+ # ensure `GIT_REVISE_TODO` is not empty
20
+ if [ -z "$GIT_REVISE_TODO" ]; then
21
+ echo "🚨 GIT_REVISE_TODO environment variable is empty" >&2
22
+ exit 1
23
+ fi
24
+
25
+ # first argument into git sequence editor is git-revise-todo file
26
+ git_revise_todo_path="$1"
27
+
28
+ # debug print git-revise-todo file passed into command
29
+ echo "$git_revise_todo_path"
30
+ echo "----- START -----"
31
+ cat "$git_revise_todo_path"
32
+ echo "------ END ------"
33
+
34
+ # write content of `GIT_REVISE_TODO` env variable to `git_revise_todo_path`
35
+ echo "$GIT_REVISE_TODO" > "$git_revise_todo_path"
@@ -10,7 +10,6 @@ import { Url } from "~/app/Url";
10
10
  import { cli } from "~/core/cli";
11
11
  import { colors } from "~/core/colors";
12
12
  import { is_command_available } from "~/core/is_command_available";
13
- import { semver_compare } from "~/core/semver_compare";
14
13
  import * as gh from "~/github/gh";
15
14
 
16
15
  type Props = {
@@ -18,6 +17,21 @@ type Props = {
18
17
  };
19
18
 
20
19
  export function DependencyCheck(props: Props) {
20
+ return (
21
+ <CheckGit>
22
+ <CheckGithubCli>
23
+ <CheckGithubCliAuth>
24
+ <CheckGitRevise>
25
+ {/* force line break */}
26
+ {props.children}
27
+ </CheckGitRevise>
28
+ </CheckGithubCliAuth>
29
+ </CheckGithubCli>
30
+ </CheckGit>
31
+ );
32
+ }
33
+
34
+ function CheckGit(props: Props) {
21
35
  const actions = Store.useActions();
22
36
 
23
37
  return (
@@ -46,106 +60,147 @@ export function DependencyCheck(props: Props) {
46
60
  actions.exit(2);
47
61
  }}
48
62
  >
49
- <Await
50
- fallback={
51
- <Ink.Text color={colors.yellow}>
52
- Checking <Command>node</Command> install...
63
+ {props.children}
64
+ </Await>
65
+ );
66
+ }
67
+
68
+ function CheckGithubCli(props: Props) {
69
+ const actions = Store.useActions();
70
+
71
+ return (
72
+ <Await
73
+ fallback={
74
+ <Ink.Text color={colors.yellow}>
75
+ <Ink.Text>
76
+ Checking <Command>gh</Command> install...
53
77
  </Ink.Text>
78
+ </Ink.Text>
79
+ }
80
+ function={async () => {
81
+ if (is_command_available("gh")) {
82
+ return;
54
83
  }
55
- function={async () => {
56
- const process_version = process.version.substring(1);
57
- const semver_result = semver_compare(process_version, "14.0.0");
58
84
 
59
- if (semver_result >= 0) {
85
+ actions.output(
86
+ <Ink.Text color={colors.yellow}>
87
+ <Command>gh</Command> must be installed.
88
+ </Ink.Text>
89
+ );
90
+
91
+ actions.output(
92
+ <Ink.Text color={colors.yellow}>
93
+ <Ink.Text>{"Visit "}</Ink.Text>
94
+ <Url>https://cli.github.com</Url>
95
+ <Ink.Text>{" to install the github cli "}</Ink.Text>
96
+
97
+ <Parens>
98
+ <Command>gh</Command>
99
+ </Parens>
100
+ </Ink.Text>
101
+ );
102
+
103
+ actions.exit(3);
104
+ }}
105
+ >
106
+ {props.children}
107
+ </Await>
108
+ );
109
+ }
110
+
111
+ function CheckGithubCliAuth(props: Props) {
112
+ const actions = Store.useActions();
113
+
114
+ return (
115
+ <Await
116
+ fallback={
117
+ <Ink.Text color={colors.yellow}>
118
+ <Ink.Text>
119
+ Checking <Command>gh auth status</Command>...
120
+ </Ink.Text>
121
+ </Ink.Text>
122
+ }
123
+ function={async () => {
124
+ const options = { ignoreExitCode: true };
125
+ const auth_status = await cli(`gh auth status`, options);
126
+
127
+ if (auth_status.code === 0) {
128
+ const username = gh.auth_status(auth_status.stdout);
129
+
130
+ if (username) {
131
+ actions.set((state) => {
132
+ state.username = username;
133
+ });
134
+
60
135
  return;
61
136
  }
137
+ }
62
138
 
63
- actions.output(
64
- <Ink.Text color={colors.yellow}>
65
- <Command>node</Command> must be installed.
66
- </Ink.Text>
67
- );
68
-
69
- actions.exit(2);
70
- }}
71
- >
72
- <Await
73
- fallback={
74
- <Ink.Text color={colors.yellow}>
75
- <Ink.Text>
76
- Checking <Command>gh</Command> install...
77
- </Ink.Text>
78
- </Ink.Text>
79
- }
80
- function={async () => {
81
- if (is_command_available("gh")) {
82
- return;
83
- }
84
-
85
- actions.output(
86
- <Ink.Text color={colors.yellow}>
87
- <Command>gh</Command> must be installed.
88
- </Ink.Text>
89
- );
90
-
91
- actions.output(
92
- <Ink.Text color={colors.yellow}>
93
- <Ink.Text>{"Visit "}</Ink.Text>
94
- <Url>https://cli.github.com</Url>
95
- <Ink.Text>{" to install the github cli "}</Ink.Text>
96
-
97
- <Parens>
98
- <Command>gh</Command>
99
- </Parens>
100
- </Ink.Text>
101
- );
102
-
103
- actions.exit(3);
104
- }}
105
- >
106
- <Await
107
- fallback={
108
- <Ink.Text color={colors.yellow}>
109
- <Ink.Text>
110
- Checking <Command>gh auth status</Command>...
111
- </Ink.Text>
112
- </Ink.Text>
113
- }
114
- function={async () => {
115
- const options = { ignoreExitCode: true };
116
- const auth_status = await cli(`gh auth status`, options);
117
-
118
- if (auth_status.code === 0) {
119
- const username = gh.auth_status(auth_status.stdout);
120
-
121
- if (username) {
122
- actions.set((state) => {
123
- state.username = username;
124
- });
125
-
126
- return;
127
- }
128
- }
129
-
130
- if (actions.isDebug()) {
131
- actions.error("gh auth status could not find username");
132
- }
133
-
134
- actions.output(
135
- <Ink.Text color={colors.yellow}>
136
- <Command>gh</Command>
137
- <Ink.Text>{" requires login, please run "}</Ink.Text>
138
- <Command>gh auth login</Command>
139
- </Ink.Text>
140
- );
141
-
142
- actions.exit(4);
143
- }}
144
- >
145
- {props.children}
146
- </Await>
147
- </Await>
148
- </Await>
139
+ if (actions.isDebug()) {
140
+ actions.error("gh auth status could not find username");
141
+ }
142
+
143
+ actions.output(
144
+ <Ink.Text color={colors.yellow}>
145
+ <Command>gh</Command>
146
+ <Ink.Text>{" requires login, please run "}</Ink.Text>
147
+ <Command>gh auth login</Command>
148
+ </Ink.Text>
149
+ );
150
+
151
+ actions.exit(4);
152
+ }}
153
+ >
154
+ {props.children}
155
+ </Await>
156
+ );
157
+ }
158
+
159
+ function CheckGitRevise(props: Props) {
160
+ const actions = Store.useActions();
161
+ const argv = Store.useState((state) => state.argv);
162
+
163
+ // skip git revise check when `rebase` is not git-revise
164
+ if (argv?.["rebase"] !== "git-revise") {
165
+ return props.children;
166
+ }
167
+
168
+ return (
169
+ <Await
170
+ fallback={
171
+ <Ink.Text color={colors.yellow}>
172
+ <Ink.Text>
173
+ Checking <Command>git-revise</Command> install...
174
+ </Ink.Text>
175
+ </Ink.Text>
176
+ }
177
+ function={async () => {
178
+ if (is_command_available("git-revise")) {
179
+ return;
180
+ }
181
+
182
+ actions.output(
183
+ <Ink.Text color={colors.yellow}>
184
+ <Command>git-revise</Command> must be installed.
185
+ </Ink.Text>
186
+ );
187
+
188
+ actions.output(
189
+ <Ink.Text color={colors.yellow}>
190
+ <Ink.Text>{"Visit "}</Ink.Text>
191
+ <Url>https://github.com/mystor/git-revise#install</Url>
192
+ <Ink.Text>{" to install the git revise cli "}</Ink.Text>
193
+
194
+ <Parens>
195
+ <Command>git-revise</Command>
196
+ </Parens>
197
+ </Ink.Text>
198
+ );
199
+
200
+ actions.exit(10);
201
+ }}
202
+ >
203
+ {props.children}
149
204
  </Await>
150
205
  );
151
206
  }
@@ -112,10 +112,7 @@ async function run() {
112
112
 
113
113
  let new_message;
114
114
  if (commit.branch_id) {
115
- new_message = await Metadata.write(
116
- commit.full_message,
117
- commit.branch_id
118
- );
115
+ new_message = Metadata.write(commit.full_message, commit.branch_id);
119
116
  } else {
120
117
  new_message = commit.full_message;
121
118
  }
@@ -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 last_group = commit_range.group_list[i - 1];
72
- const last_commit = last_group.commits[last_group.commits.length - 1];
73
- rebase_merge_base = last_commit.sha;
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["rebase"] === "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 = await Metadata.write(commit.full_message, group.id);
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
- actions.output(
133
- <FormatText
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
- if (!props.skipSync) {
145
- // push to origin since github requires commit shas to line up perfectly
146
- const git_push_command = [`git push -f origin HEAD:${group.id}`];
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
- if (argv.verify === false) {
149
- git_push_command.push("--no-verify");
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
- await cli(git_push_command);
153
-
154
- if (group.pr) {
155
- // ensure base matches pr in github
156
- await github.pr_edit({
157
- branch: group.id,
158
- base: group.base,
159
- body: StackSummaryTable.write({
160
- body: group.pr.body,
161
- pr_url_list,
162
- selected_url,
163
- }),
164
- });
165
- } else {
166
- // delete local group branch if leftover
167
- await cli(`git branch -D ${group.id}`, { ignoreExitCode: true });
168
-
169
- // move to temporary branch for creating pr
170
- await cli(`git checkout -b ${group.id}`);
171
-
172
- // create pr in github
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
- // finally, ensure all prs have the updated stack table from updated pr_url_list
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