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.
@@ -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["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
@@ -130,6 +130,7 @@ function SelectCommitRangesInternal(props: Props) {
130
130
  switch (inputLower) {
131
131
  case "s":
132
132
  state.step = "manual-rebase";
133
+ // state.step = "manual-rebase-no-sync";
133
134
  break;
134
135
  }
135
136
  });
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"],