git-stack-cli 2.9.7 → 2.9.9

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.7",
3
+ "version": "2.9.9",
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() {
@@ -3,6 +3,8 @@ import * as React from "react";
3
3
  import * as Ink from "ink-cjs";
4
4
  import { DateTime } from "luxon";
5
5
 
6
+ import { render_node } from "~/core/render_node";
7
+
6
8
  type Props = {
7
9
  node: React.ReactNode;
8
10
  };
@@ -14,17 +16,7 @@ export function DebugOutput(props: Props) {
14
16
  const timestamp = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss.SSS");
15
17
  const content_width = available_width - timestamp.length - 2;
16
18
 
17
- const content = (function () {
18
- switch (typeof props.node) {
19
- case "boolean":
20
- case "number":
21
- case "string": {
22
- return <Ink.Text dimColor>{String(props.node)}</Ink.Text>;
23
- }
24
- default:
25
- return props.node;
26
- }
27
- })();
19
+ const content = render_node(props.node);
28
20
 
29
21
  return (
30
22
  <Ink.Box flexDirection="column">
@@ -4,6 +4,7 @@ import * as Ink from "ink-cjs";
4
4
 
5
5
  import { DebugOutput } from "~/app/DebugOutput";
6
6
  import { Store } from "~/app/Store";
7
+ import { render_node } from "~/core/render_node";
7
8
 
8
9
  export function Output() {
9
10
  const output = Store.useState((state) => state.output);
@@ -14,7 +15,7 @@ export function Output() {
14
15
  <Ink.Static items={output}>
15
16
  {(entry) => {
16
17
  const [id, node] = entry;
17
- return <Ink.Box key={id}>{node}</Ink.Box>;
18
+ return <Ink.Box key={id}>{render_node(node)}</Ink.Box>;
18
19
  }}
19
20
  </Ink.Static>
20
21
 
@@ -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,6 +169,12 @@ 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 = {
@@ -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) {
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { Store } from "~/app/Store";
6
+
7
+ export function render_node(node: React.ReactNode) {
8
+ const actions = Store.getState().actions;
9
+
10
+ if (node == null) {
11
+ return null;
12
+ }
13
+
14
+ if (React.isValidElement(node)) {
15
+ return node;
16
+ }
17
+
18
+ if (Array.isArray(node)) {
19
+ return (
20
+ <React.Fragment>
21
+ {node.map((entry, index) => (
22
+ <React.Fragment key={index}>{render_node(entry)}</React.Fragment>
23
+ ))}
24
+ </React.Fragment>
25
+ );
26
+ }
27
+
28
+ switch (typeof node) {
29
+ case "string":
30
+ case "number":
31
+ case "boolean":
32
+ return <Ink.Text>{String(node)}</Ink.Text>;
33
+ default:
34
+ actions.debug(`unhandled node ${typeof node}`);
35
+ return null;
36
+ }
37
+ }