git-stack-cli 1.9.0 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "chalk": "^5.3.0",
43
43
  "immer": "^10.0.3",
44
44
  "ink-cjs": "4.4.1",
45
+ "lodash": "^4.17.21",
45
46
  "luxon": "^3.4.4",
46
47
  "react": "^18.2.0",
47
48
  "react-devtools-core": "^4.19.1",
@@ -57,6 +58,7 @@
57
58
  "@rollup/plugin-replace": "^5.0.5",
58
59
  "@rollup/plugin-typescript": "^11.1.6",
59
60
  "@types/chalk": "^2.2.0",
61
+ "@types/lodash": "^4.17.7",
60
62
  "@types/luxon": "^3.4.2",
61
63
  "@types/node": "^20.8.7",
62
64
  "@types/react": "^18.2.33",
package/src/app/App.tsx CHANGED
@@ -4,6 +4,7 @@ import { AutoUpdate } from "~/app/AutoUpdate";
4
4
  import { CherryPickCheck } from "~/app/CherryPickCheck";
5
5
  import { Debug } from "~/app/Debug";
6
6
  import { DependencyCheck } from "~/app/DependencyCheck";
7
+ import { DetectInitialPR } from "~/app/DetectInitialPR";
7
8
  import { DirtyCheck } from "~/app/DirtyCheck";
8
9
  import { GatherMetadata } from "~/app/GatherMetadata";
9
10
  import { GithubApiError } from "~/app/GithubApiError";
@@ -80,7 +81,9 @@ function MaybeMain() {
80
81
 
81
82
  <GatherMetadata>
82
83
  <LocalCommitStatus>
83
- <Main />
84
+ <DetectInitialPR>
85
+ <Main />
86
+ </DetectInitialPR>
84
87
  </LocalCommitStatus>
85
88
  </GatherMetadata>
86
89
  </DirtyCheck>
@@ -57,17 +57,17 @@ export function CherryPickCheck(props: Props) {
57
57
  default:
58
58
  return (
59
59
  <Await
60
+ function={run}
60
61
  fallback={
61
62
  <Ink.Text color={colors.yellow}>
62
63
  Checking for <Command>git cherry-pick</Command>…
63
64
  </Ink.Text>
64
65
  }
65
- function={cherry_pick_check}
66
66
  />
67
67
  );
68
68
  }
69
69
 
70
- async function cherry_pick_check() {
70
+ async function run() {
71
71
  const actions = Store.getState().actions;
72
72
 
73
73
  try {
@@ -88,7 +88,7 @@ export function CherryPickCheck(props: Props) {
88
88
  }
89
89
  }
90
90
 
91
- actions.exit(9);
91
+ actions.exit(11);
92
92
  }
93
93
  }
94
94
  }
@@ -0,0 +1,189 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+ import cloneDeep from "lodash/cloneDeep";
5
+
6
+ import { Await } from "~/app/Await";
7
+ import { Brackets } from "~/app/Brackets";
8
+ import { Command } from "~/app/Command";
9
+ import { FormatText } from "~/app/FormatText";
10
+ import { Store } from "~/app/Store";
11
+ import { Url } from "~/app/Url";
12
+ import { YesNoPrompt } from "~/app/YesNoPrompt";
13
+ import * as CommitMetadata from "~/core/CommitMetadata";
14
+ import { GitReviseTodo } from "~/core/GitReviseTodo";
15
+ import { cli } from "~/core/cli";
16
+ import { colors } from "~/core/colors";
17
+ import * as github from "~/core/github";
18
+ import { invariant } from "~/core/invariant";
19
+
20
+ type Props = {
21
+ children: React.ReactNode;
22
+ };
23
+
24
+ type PullRequest = NonNullable<Awaited<ReturnType<typeof github.pr_status>>>;
25
+
26
+ type State = {
27
+ status: "init" | "prompt" | "revise" | "done";
28
+ pr: null | PullRequest;
29
+ };
30
+
31
+ function reducer(state: State, patch: Partial<State>) {
32
+ return { ...state, ...patch };
33
+ }
34
+
35
+ export function DetectInitialPR(props: Props) {
36
+ const actions = Store.useActions();
37
+ const branch_name = Store.useState((state) => state.branch_name);
38
+
39
+ const [state, patch] = React.useReducer(reducer, {
40
+ status: "init",
41
+ pr: null,
42
+ });
43
+
44
+ switch (state.status) {
45
+ case "done":
46
+ return props.children;
47
+
48
+ case "revise":
49
+ return (
50
+ <Await
51
+ function={run_revise}
52
+ fallback={
53
+ <Ink.Text color={colors.yellow}>
54
+ Synchronizing local commit metadata with remote branch on Github…
55
+ </Ink.Text>
56
+ }
57
+ />
58
+ );
59
+
60
+ case "prompt":
61
+ return (
62
+ <YesNoPrompt
63
+ message={
64
+ <Ink.Box flexDirection="column">
65
+ <FormatText
66
+ wrapper={<Ink.Text color={colors.yellow} />}
67
+ message="{branch_name} exists on Github and was not generated with {git_stack}."
68
+ values={{
69
+ branch_name: <Brackets>{branch_name}</Brackets>,
70
+ git_stack: <Command>git stack</Command>,
71
+ }}
72
+ />
73
+ <Ink.Text> </Ink.Text>
74
+ <FormatText
75
+ message=" {url}"
76
+ values={{
77
+ url: <Url>{state.pr?.url}</Url>,
78
+ }}
79
+ />
80
+ <Ink.Text> </Ink.Text>
81
+ <FormatText
82
+ wrapper={<Ink.Text color={colors.yellow} />}
83
+ message="In order to synchronize we need to rename your local branch, would you like to proceed?"
84
+ />
85
+ </Ink.Box>
86
+ }
87
+ onYes={async () => {
88
+ patch({ status: "revise" });
89
+ }}
90
+ onNo={async () => {
91
+ actions.exit(0);
92
+ }}
93
+ />
94
+ );
95
+
96
+ default:
97
+ return (
98
+ <Await
99
+ function={run}
100
+ fallback={
101
+ <Ink.Text color={colors.yellow}>
102
+ Checking for existing PR on Github…
103
+ </Ink.Text>
104
+ }
105
+ />
106
+ );
107
+ }
108
+
109
+ async function run() {
110
+ const actions = Store.getState().actions;
111
+ const branch_name = Store.getState().branch_name;
112
+ const commit_range = Store.getState().commit_range;
113
+
114
+ invariant(branch_name, "branch_name must exist");
115
+ invariant(commit_range, "branch_name must exist");
116
+
117
+ try {
118
+ let has_existing_metadata = false;
119
+ for (const commit of commit_range.commit_list) {
120
+ if (commit.branch_id) {
121
+ has_existing_metadata = true;
122
+ break;
123
+ }
124
+ }
125
+
126
+ if (!has_existing_metadata) {
127
+ // check for pr with matching branch name to initialize group
128
+ const pr = await github.pr_status(branch_name);
129
+ if (pr) {
130
+ return patch({ status: "prompt", pr });
131
+ }
132
+ }
133
+
134
+ patch({ status: "done" });
135
+ } catch (err) {
136
+ actions.error("Must be run from within a git repository.");
137
+
138
+ if (err instanceof Error) {
139
+ if (actions.isDebug()) {
140
+ actions.error(err.message);
141
+ }
142
+ }
143
+
144
+ actions.exit(9);
145
+ }
146
+ }
147
+
148
+ async function run_revise() {
149
+ const actions = Store.getState().actions;
150
+ const master_branch = Store.getState().master_branch;
151
+ const branch_name = Store.getState().branch_name;
152
+ const commit_range = cloneDeep(Store.getState().commit_range);
153
+
154
+ invariant(branch_name, "branch_name must exist");
155
+ invariant(commit_range, "branch_name must exist");
156
+
157
+ for (const group of commit_range.group_list) {
158
+ group.id = branch_name;
159
+ group.title = state.pr?.title || "-";
160
+ }
161
+
162
+ // get latest merge_base relative to local master
163
+ const rebase_group_index = 0;
164
+
165
+ const rebase_merge_base = (
166
+ await cli(`git merge-base HEAD ${master_branch}`)
167
+ ).stdout;
168
+
169
+ await GitReviseTodo.execute({
170
+ rebase_group_index,
171
+ rebase_merge_base,
172
+ commit_range,
173
+ });
174
+
175
+ const new_branch_name = `${branch_name}-sync`;
176
+ await cli(`git checkout -b ${new_branch_name}`);
177
+
178
+ await cli(`git branch -D ${branch_name}`);
179
+
180
+ const commit_range_new = await CommitMetadata.range();
181
+
182
+ actions.set((state) => {
183
+ state.branch_name = new_branch_name;
184
+ state.commit_range = commit_range_new;
185
+ });
186
+
187
+ patch({ status: "done" });
188
+ }
189
+ }
@@ -68,17 +68,17 @@ export function DirtyCheck(props: Props) {
68
68
  default:
69
69
  return (
70
70
  <Await
71
+ function={run}
71
72
  fallback={
72
73
  <Ink.Text color={colors.yellow}>
73
74
  Ensuring <Command>git status --porcelain</Command>…
74
75
  </Ink.Text>
75
76
  }
76
- function={rebase_check}
77
77
  />
78
78
  );
79
79
  }
80
80
 
81
- async function rebase_check() {
81
+ async function run() {
82
82
  const actions = Store.getState().actions;
83
83
 
84
84
  try {
@@ -95,7 +95,7 @@ export function DirtyCheck(props: Props) {
95
95
  }
96
96
  }
97
97
 
98
- actions.exit(9);
98
+ actions.exit(12);
99
99
  }
100
100
  }
101
101
  }
@@ -5,7 +5,7 @@ import { FormattedMessage } from "react-intl";
5
5
 
6
6
  type Props = {
7
7
  message: string;
8
- values: React.ComponentProps<typeof FormattedMessage>["values"];
8
+ values?: React.ComponentProps<typeof FormattedMessage>["values"];
9
9
  wrapper?: React.ReactNode;
10
10
  };
11
11
 
@@ -20,13 +20,13 @@ export function GatherMetadata(props: Props) {
20
20
  );
21
21
 
22
22
  return (
23
- <Await fallback={fallback} function={gather_metadata}>
23
+ <Await fallback={fallback} function={run}>
24
24
  {props.children}
25
25
  </Await>
26
26
  );
27
27
  }
28
28
 
29
- async function gather_metadata() {
29
+ async function run() {
30
30
  const actions = Store.getState().actions;
31
31
  const argv = Store.getState().argv;
32
32
 
@@ -28,7 +28,7 @@ export function LocalCommitStatus(props: Props) {
28
28
  }
29
29
 
30
30
  return (
31
- <Await fallback={fallback} function={gather_metadata}>
31
+ <Await fallback={fallback} function={run}>
32
32
  {props.children}
33
33
  </Await>
34
34
  );
@@ -41,12 +41,11 @@ async function mock_metadata() {
41
41
 
42
42
  Store.setState((state) => {
43
43
  Object.assign(state, deserialized);
44
-
45
44
  state.step = "status";
46
45
  });
47
46
  }
48
47
 
49
- async function gather_metadata() {
48
+ async function run() {
50
49
  const actions = Store.getState().actions;
51
50
 
52
51
  try {
@@ -5,6 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
7
  import * as Ink from "ink-cjs";
8
+ import cloneDeep from "lodash/cloneDeep";
8
9
 
9
10
  import { Await } from "~/app/Await";
10
11
  import { Brackets } from "~/app/Brackets";
@@ -34,18 +35,37 @@ async function run() {
34
35
  const actions = state.actions;
35
36
  const argv = state.argv;
36
37
  const branch_name = state.branch_name;
38
+ const original_commit_range = cloneDeep(state.commit_range);
37
39
  const commit_map = state.commit_map;
38
40
  const master_branch = state.master_branch;
39
41
  const cwd = state.cwd;
40
42
  const repo_root = state.repo_root;
41
43
 
42
44
  invariant(branch_name, "branch_name must exist");
45
+ invariant(original_commit_range, "original_commit_range must exist");
43
46
  invariant(commit_map, "commit_map must exist");
44
47
  invariant(repo_root, "repo_root must exist");
45
48
 
46
49
  // always listen for SIGINT event and restore git state
47
50
  process.once("SIGINT", handle_exit);
48
51
 
52
+ // get latest merge_base relative to local master
53
+ const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
54
+
55
+ // immediately paint all commit to preserve selected commit ranges
56
+ original_commit_range.group_list.reverse();
57
+ for (const commit of original_commit_range.commit_list) {
58
+ const group_from_map = commit_map[commit.sha];
59
+ commit.branch_id = group_from_map.id;
60
+ commit.title = group_from_map.title;
61
+ }
62
+
63
+ await GitReviseTodo.execute({
64
+ rebase_group_index: 0,
65
+ rebase_merge_base: merge_base,
66
+ commit_range: original_commit_range,
67
+ });
68
+
49
69
  let DEFAULT_PR_BODY = "";
50
70
  if (state.pr_template_body) {
51
71
  DEFAULT_PR_BODY = state.pr_template_body;
@@ -58,9 +78,6 @@ async function run() {
58
78
  // reverse commit list so that we can cherry-pick in order
59
79
  commit_range.group_list.reverse();
60
80
 
61
- // get latest merge_base relative to local master
62
- const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
63
-
64
81
  let rebase_merge_base = merge_base;
65
82
  let rebase_group_index = 0;
66
83
 
@@ -150,19 +167,11 @@ async function run() {
150
167
  // create temporary branch
151
168
  await cli(`git checkout -b ${temp_branch_name}`);
152
169
 
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
- [
159
- `GIT_EDITOR="${tmp_git_sequence_editor_path}"`,
160
- `GIT_REVISE_TODO="${git_revise_todo}"`,
161
- `git`,
162
- `revise --edit -i ${rebase_merge_base}`,
163
- ],
164
- { stdio: ["ignore", "ignore", "ignore"] }
165
- );
170
+ await GitReviseTodo.execute({
171
+ rebase_group_index,
172
+ rebase_merge_base,
173
+ commit_range,
174
+ });
166
175
 
167
176
  // early return since we do not need to sync
168
177
  if (!argv.sync) {
@@ -57,17 +57,17 @@ export function RebaseCheck(props: Props) {
57
57
  default:
58
58
  return (
59
59
  <Await
60
+ function={run}
60
61
  fallback={
61
62
  <Ink.Text color={colors.yellow}>
62
63
  Checking for <Command>git rebase</Command>…
63
64
  </Ink.Text>
64
65
  }
65
- function={rebase_check}
66
66
  />
67
67
  );
68
68
  }
69
69
 
70
- async function rebase_check() {
70
+ async function run() {
71
71
  const actions = Store.getState().actions;
72
72
 
73
73
  try {
@@ -88,7 +88,7 @@ export function RebaseCheck(props: Props) {
88
88
  }
89
89
  }
90
90
 
91
- actions.exit(9);
91
+ actions.exit(13);
92
92
  }
93
93
  }
94
94
  }
@@ -9,8 +9,8 @@ import { Parens } from "~/app/Parens";
9
9
  import { Store } from "~/app/Store";
10
10
  import { TextInput } from "~/app/TextInput";
11
11
  import { colors } from "~/core/colors";
12
+ import { gs_short_id } from "~/core/gs_short_id";
12
13
  import { invariant } from "~/core/invariant";
13
- import { short_id } from "~/core/short_id";
14
14
  import { wrap_index } from "~/core/wrap_index";
15
15
 
16
16
  import type { State } from "~/app/Store";
@@ -388,7 +388,7 @@ function SelectCommitRangesInternal(props: Props) {
388
388
  );
389
389
 
390
390
  function submit_group_input(title: string) {
391
- const id = `gs-${short_id()}`;
391
+ const id = gs_short_id();
392
392
 
393
393
  actions.output(
394
394
  <FormatText
@@ -2,7 +2,6 @@ import { Store } from "~/app/Store";
2
2
  import * as Metadata from "~/core/Metadata";
3
3
  import { cli } from "~/core/cli";
4
4
  import * as github from "~/core/github";
5
- import { invariant } from "~/core/invariant";
6
5
 
7
6
  export type CommitMetadata = Awaited<ReturnType<typeof commit>>;
8
7
  export type CommitRange = Awaited<ReturnType<typeof range>>;
@@ -18,7 +17,7 @@ type CommitGroup = {
18
17
  commits: Array<CommitMetadata>;
19
18
  };
20
19
 
21
- type SimpleGroup = { id: string; title: string };
20
+ export type SimpleGroup = { id: string; title: string };
22
21
  type CommitGroupMap = { [sha: string]: SimpleGroup };
23
22
 
24
23
  export async function range(commit_group_map?: CommitGroupMap) {
@@ -167,10 +166,6 @@ export async function range(commit_group_map?: CommitGroupMap) {
167
166
 
168
167
  async function get_commit_list() {
169
168
  const master_branch = Store.getState().master_branch;
170
- const branch_name = Store.getState().branch_name;
171
-
172
- invariant(branch_name, "branch_name must exist");
173
-
174
169
  const log_result = await cli(
175
170
  `git log ${master_branch}..HEAD --oneline --format=%H --color=never`
176
171
  );
@@ -183,30 +178,12 @@ async function get_commit_list() {
183
178
 
184
179
  const commit_metadata_list = [];
185
180
 
186
- let has_metadata = false;
187
-
188
181
  for (let i = 0; i < sha_list.length; i++) {
189
182
  const sha = sha_list[i];
190
183
  const commit_metadata = await commit(sha);
191
-
192
- if (commit_metadata.branch_id) {
193
- has_metadata = true;
194
- }
195
-
196
184
  commit_metadata_list.push(commit_metadata);
197
185
  }
198
186
 
199
- if (!has_metadata) {
200
- // check for pr with matching branch name to initialize group
201
- const pr_result = await github.pr_status(branch_name);
202
- if (pr_result) {
203
- for (const commit_metadata of commit_metadata_list) {
204
- commit_metadata.branch_id = branch_name;
205
- commit_metadata.title = pr_result.title;
206
- }
207
- }
208
- }
209
-
210
187
  return commit_metadata_list;
211
188
  }
212
189