git-stack-cli 2.9.8 → 2.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": "2.9.8",
3
+ "version": "2.10.0",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -92,6 +92,10 @@ core = core.replace(re_token("tarball_sha256"), tarball_asset.sha256);
92
92
 
93
93
  await file.write_text(path.join("Formula", "git-stack.core.rb"), core);
94
94
 
95
+ // write latest.json containing latest version
96
+ const latest_json = JSON.stringify({ version });
97
+ await file.write_text(path.join("latest.json"), latest_json);
98
+
95
99
  // commit homebrew repo changes
96
100
  process.chdir(HOMEBREW_DIR);
97
101
  await spawn.sync(`git add .`);
@@ -161,20 +161,32 @@ export function AutoUpdate(props: Props) {
161
161
  if (state.latest_version !== null) return;
162
162
 
163
163
  const local_version = process.env.CLI_VERSION;
164
- const latest_version = await get_latest_version();
165
164
  const is_brew_bun_standalone = get_is_brew_bun_standalone();
166
- patch({ local_version, latest_version, is_brew_bun_standalone });
167
- }
168
165
 
169
- async function get_latest_version() {
170
166
  const timeout_ms = is_finite_value(props.timeoutMs) ? props.timeoutMs : 2 * 1000;
171
167
  const timeout = get_timeout_fn(timeout_ms, "AutoUpdate timeout");
172
- const npm_json = await timeout(fetch_json(`https://registry.npmjs.org/${props.name}`));
173
- const maybe_version = npm_json?.["dist-tags"]?.latest;
174
- if (typeof maybe_version === "string") {
175
- return maybe_version;
168
+ const latest_version = await timeout(get_latest_version(is_brew_bun_standalone));
169
+
170
+ patch({ local_version, latest_version, is_brew_bun_standalone });
171
+ }
172
+
173
+ async function get_latest_version(is_brew_bun_standalone: boolean) {
174
+ if (is_brew_bun_standalone) {
175
+ // prettier-ignore
176
+ const brew_json = await fetch_json("https://raw.githubusercontent.com/magus/homebrew-git-stack/refs/heads/master/latest.json");
177
+ const maybe_version = brew_json.version;
178
+ if (typeof maybe_version === "string") {
179
+ return maybe_version;
180
+ }
181
+ throw new Error("Unable to retrieve latest version from brew");
182
+ } else {
183
+ const npm_json = await fetch_json(`https://registry.npmjs.org/${props.name}`);
184
+ const maybe_version = npm_json?.["dist-tags"]?.latest;
185
+ if (typeof maybe_version === "string") {
186
+ return maybe_version;
187
+ }
188
+ throw new Error("Unable to retrieve latest version from npm");
176
189
  }
177
- throw new Error("Unable to retrieve latest version from npm");
178
190
  }
179
191
 
180
192
  function get_is_brew_bun_standalone() {
@@ -28,6 +28,7 @@ async function run() {
28
28
  const master_branch = state.master_branch;
29
29
  const repo_path = state.repo_path;
30
30
  const sync_github = state.sync_github;
31
+ const labels = argv.label ?? [];
31
32
 
32
33
  invariant(branch_name, "branch_name must exist");
33
34
  invariant(commit_map, "commit_map must exist");
@@ -140,17 +141,17 @@ async function run() {
140
141
  const pr_url_list = all_pr_groups.map((g) => pr_url_by_group_id[g.id]);
141
142
 
142
143
  // update PR body for all pr groups (not just push_group_list)
143
- const update_pr_body_tasks = [];
144
+ const update_pr_tasks = [];
144
145
  for (let i = 0; i < all_pr_groups.length; i++) {
145
146
  const group = all_pr_groups[i];
146
147
 
147
148
  const selected_url = pr_url_by_group_id[group.id];
148
149
 
149
- const task = update_pr_body({ group, selected_url, pr_url_list });
150
- update_pr_body_tasks.push(task);
150
+ const task = update_pr({ group, selected_url, pr_url_list, labels });
151
+ update_pr_tasks.push(task);
151
152
  }
152
153
 
153
- await Promise.all(update_pr_body_tasks);
154
+ await Promise.all(update_pr_tasks);
154
155
 
155
156
  actions.unregister_abort_handler();
156
157
 
@@ -262,6 +263,7 @@ async function run() {
262
263
  title: group.title,
263
264
  body: DEFAULT_PR_BODY,
264
265
  draft: argv.draft,
266
+ labels,
265
267
  });
266
268
 
267
269
  if (!pr_url) {
@@ -273,10 +275,11 @@ async function run() {
273
275
  }
274
276
  }
275
277
 
276
- async function update_pr_body(args: {
278
+ async function update_pr(args: {
277
279
  group: CommitMetadataGroup;
278
280
  selected_url: string;
279
281
  pr_url_list: Array<string>;
282
+ labels: Array<string>;
280
283
  }) {
281
284
  const { group, selected_url, pr_url_list } = args;
282
285
 
@@ -292,17 +295,30 @@ async function run() {
292
295
 
293
296
  const debug_meta = `${group.id} ${selected_url}`;
294
297
 
295
- if (update_body === body) {
296
- actions.debug(`Skipping body update ${debug_meta}`);
297
- } else {
298
- actions.debug(`Update body ${debug_meta}`);
298
+ const body_changed = update_body !== body;
299
+ const needs_labels = args.labels.length > 0;
299
300
 
300
- await github.pr_edit({
301
- branch: group.id,
302
- base: group.base,
303
- body: update_body,
304
- });
301
+ if (!body_changed && !needs_labels) {
302
+ actions.debug(`Skipping update ${debug_meta}`);
303
+ return;
305
304
  }
305
+
306
+ actions.debug(`Update PR ${debug_meta}`);
307
+
308
+ const edit_args: Parameters<typeof github.pr_edit>[0] = {
309
+ branch: group.id,
310
+ base: group.base,
311
+ };
312
+
313
+ if (body_changed) {
314
+ edit_args.body = update_body;
315
+ }
316
+
317
+ if (needs_labels) {
318
+ edit_args.add_labels = args.labels;
319
+ }
320
+
321
+ await github.pr_edit(edit_args);
306
322
  }
307
323
 
308
324
  function is_pr_master_base(group: CommitMetadataGroup) {
package/src/command.ts CHANGED
@@ -18,7 +18,7 @@ export async function command(argv: string[], options: CommandOptions = {}) {
18
18
  builder = builder.parserConfiguration(options.parserConfiguration);
19
19
  }
20
20
 
21
- const parsed = await builder
21
+ const parsed = builder
22
22
  .scriptName("git stack")
23
23
  .usage("Usage: git stack [command] [options]")
24
24
 
@@ -122,6 +122,24 @@ const DefaultOptions = {
122
122
  description: "Sync commit ranges to Github, disable with --no-sync",
123
123
  },
124
124
 
125
+ "label": {
126
+ type: "array",
127
+ alias: ["labels"],
128
+ coerce: (label_input: Array<string | number>) => label_input.map((v) => String(v)),
129
+ description: [
130
+ // force line break
131
+ "Apply labels to all PRs in the stack (repeatable)",
132
+ "Example: --label backend --label needs-review",
133
+ ].join("\n"),
134
+ },
135
+
136
+ "draft": {
137
+ type: "boolean",
138
+ alias: ["d"],
139
+ default: false,
140
+ description: "Open all PRs as drafts",
141
+ },
142
+
125
143
  "verify": {
126
144
  type: "boolean",
127
145
  default: true,
@@ -142,19 +160,6 @@ const DefaultOptions = {
142
160
  ].join("\n"),
143
161
  },
144
162
 
145
- "draft": {
146
- type: "boolean",
147
- alias: ["d"],
148
- default: false,
149
- description: "Open all PRs as drafts",
150
- },
151
-
152
- "revise-sign": {
153
- type: "boolean",
154
- default: true,
155
- description: "Disable GPG signing for git revise with --no-revise-sign",
156
- },
157
-
158
163
  "template": {
159
164
  type: "boolean",
160
165
  default: true,
@@ -164,12 +169,18 @@ const DefaultOptions = {
164
169
  "Disable with --no-template",
165
170
  ].join("\n"),
166
171
  },
172
+
173
+ "revise-sign": {
174
+ type: "boolean",
175
+ default: true,
176
+ description: "Disable GPG signing for git revise with --no-revise-sign",
177
+ },
167
178
  } satisfies YargsOptions;
168
179
 
169
180
  const FixupOptions = {
170
181
  commit: {
171
182
  type: "number",
172
- default: 1,
183
+ default: 0,
173
184
  description: [
174
185
  "Relative number of commit to amend staged changes.",
175
186
  "Most recent is 1, next is 2, etc.",
@@ -3,11 +3,13 @@ import * as React from "react";
3
3
  import * as Ink from "ink-cjs";
4
4
 
5
5
  import { Await } from "~/app/Await";
6
+ import { Command } from "~/app/Command";
6
7
  import { FormatText } from "~/app/FormatText";
7
8
  import { Parens } from "~/app/Parens";
8
9
  import { Store } from "~/app/Store";
9
10
  import { cli } from "~/core/cli";
10
11
  import { colors } from "~/core/colors";
12
+ import { is_finite_value } from "~/core/is_finite_value";
11
13
 
12
14
  export function Fixup() {
13
15
  return (
@@ -25,27 +27,48 @@ async function run() {
25
27
 
26
28
  const relative_number = argv.commit;
27
29
 
28
- if (!relative_number) {
30
+ if (!is_finite_value(relative_number)) {
29
31
  actions.output(
30
- <Ink.Text color={colors.red}>❗️ Usage: git fixup {"<relative-commit-number>"}</Ink.Text>,
32
+ <Ink.Box flexDirection="column">
33
+ <Ink.Text color={colors.red}>❗️ Usage: git fixup {"<relative-commit-number>"}</Ink.Text>
34
+ <Ink.Text color={colors.gray}>
35
+ Automates the process of adding staged changes to a previous commit.
36
+ </Ink.Text>
37
+ <FormatText
38
+ wrapper={<Ink.Text color={colors.gray} />}
39
+ message="You can use {git_stack_log} to get the relative commit number."
40
+ values={{ git_stack_log: <Command>git stack log</Command> }}
41
+ />
42
+ <Ink.Box height={1} />
43
+ <FormatText
44
+ message=" {prompt} git stack log"
45
+ values={{ prompt: <Ink.Text color={colors.green}>❯</Ink.Text> }}
46
+ />
47
+ <FormatText
48
+ message=" 0 * {sha} 18 hours ago noah homebrew-git-stack 2.9.9"
49
+ values={{ sha: <Ink.Text color={colors.green}>e329794</Ink.Text> }}
50
+ />
51
+ <FormatText
52
+ message=" 1 * {sha} 18 hours ago noah 2.9.9"
53
+ values={{ sha: <Ink.Text color={colors.green}>c7e4065</Ink.Text> }}
54
+ />
55
+ <FormatText
56
+ message=" 2 * {sha} 18 hours ago noah command: --label + github add labels"
57
+ values={{ sha: <Ink.Text color={colors.green}>f82ac73</Ink.Text> }}
58
+ />
59
+ <Ink.Box height={1} />
60
+ <FormatText
61
+ wrapper={<Ink.Text color={colors.gray} />}
62
+ message="To target {sha} above, use {command}"
63
+ values={{
64
+ sha: <Ink.Text color={colors.green}>838e878</Ink.Text>,
65
+ command: <Command>git stack fixup 2</Command>,
66
+ }}
67
+ />
68
+ </Ink.Box>,
31
69
  );
70
+
32
71
  actions.output("");
33
- actions.output("This script automates the process of adding staged changes as a fixup commit");
34
- actions.output(
35
- "and the subsequent git rebase to flatten the commits based on relative commit number",
36
- );
37
- actions.output("You can use a `git log` like below to get the relative commit number");
38
- actions.output("");
39
- actions.output(" ❯ git stack log");
40
- actions.output(
41
- " 1\te329794d5f881cbf0fc3f26d2108cf6f3fdebabe enable drop_error_subtask test param",
42
- );
43
- actions.output(
44
- " 2\t57f43b596e5c6b97bc47e2a591f82ccc81651156 test drop_error_subtask baseline",
45
- );
46
- actions.output(" 3\t838e878d483c6a2d5393063fc59baf2407225c6d ErrorSubtask test baseline");
47
- actions.output("");
48
- actions.output("To target `838e87` above, you would call `fixup 3`");
49
72
 
50
73
  actions.exit(0);
51
74
  }
@@ -64,11 +87,8 @@ async function run() {
64
87
  // );
65
88
  }
66
89
 
67
- // Calculate commit SHA based on the relative commit number
68
- const adjusted_number = Number(relative_number) - 1;
69
-
70
90
  // get the commit SHA of the target commit
71
- const commit_sha = (await cli(`git rev-parse HEAD~${adjusted_number}`)).stdout;
91
+ const commit_sha = (await cli(`git rev-parse HEAD~${relative_number}`)).stdout;
72
92
 
73
93
  actions.output(
74
94
  <FormatText
@@ -104,7 +124,7 @@ async function run() {
104
124
 
105
125
  try {
106
126
  // rebase target needs to account for new commit created above
107
- const rebase_target = Number(relative_number) + 1;
127
+ const rebase_target = Number(relative_number) + 2;
108
128
 
109
129
  await cli(`git rebase -i --autosquash HEAD~${rebase_target}`, {
110
130
  env: {
@@ -65,7 +65,7 @@ async function run(args: Args) {
65
65
  const command = [
66
66
  `git log --pretty=format:"${format}" -n20 --graph --color ${rest_args}`,
67
67
  `cut -c 1-"${truncation_width}"`,
68
- `nl -w3 -s' '`,
68
+ `nl -v0 -w3 -s' '`,
69
69
  ].join(" | ");
70
70
 
71
71
  const result = await cli(command);
@@ -1,5 +1,5 @@
1
1
  export function get_timeout_fn(ms: number, message: string) {
2
- return function timeout<T>(promise: Promise<T>) {
2
+ return async function timeout<T>(promise: Promise<T>) {
3
3
  let id: ReturnType<typeof setTimeout>;
4
4
 
5
5
  const timeout = new Promise<never>((_resolve, reject) => {
@@ -121,6 +121,7 @@ type CreatePullRequestArgs = {
121
121
  title: string;
122
122
  body: string;
123
123
  draft: boolean;
124
+ labels?: Array<string>;
124
125
  };
125
126
 
126
127
  export async function pr_create(args: CreatePullRequestArgs) {
@@ -147,6 +148,13 @@ export async function pr_create(args: CreatePullRequestArgs) {
147
148
  command_parts.push("--draft");
148
149
  }
149
150
 
151
+ if (args.labels && args.labels.length > 0) {
152
+ for (const label of args.labels) {
153
+ if (!label) continue;
154
+ command_parts.push(`--label="${safe_quote(label)}"`);
155
+ }
156
+ }
157
+
150
158
  const cli_result = await cli(command_parts);
151
159
 
152
160
  if (cli_result.code !== 0) {
@@ -161,14 +169,17 @@ type EditPullRequestArgs = {
161
169
  branch: string;
162
170
  base?: string;
163
171
  body?: string;
172
+ add_labels?: Array<string>;
164
173
  };
165
174
 
166
175
  export async function pr_edit(args: EditPullRequestArgs) {
176
+ // https://cli.github.com/manual/gh_pr_edit
177
+
167
178
  // const state = Store.getState();
168
179
  // const actions = state.actions;
169
180
  // actions.debug(`github.pr_edit ${JSON.stringify(args)}`);
170
181
 
171
- if (!args.base && !args.body) {
182
+ if (!args.base && !args.body && !(args.add_labels && args.add_labels.length > 0)) {
172
183
  return;
173
184
  }
174
185
 
@@ -186,6 +197,13 @@ export async function pr_edit(args: EditPullRequestArgs) {
186
197
  command_parts.push(`--body-file="${body_file}"`);
187
198
  }
188
199
 
200
+ if (args.add_labels && args.add_labels.length > 0) {
201
+ for (const label of args.add_labels) {
202
+ if (!label) continue;
203
+ command_parts.push(`--add-label="${safe_quote(label)}"`);
204
+ }
205
+ }
206
+
189
207
  const cli_result = await cli(command_parts);
190
208
 
191
209
  if (cli_result.code !== 0) {