git-stack-cli 2.9.3 → 2.9.5

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.
@@ -10,6 +10,7 @@ import { Brackets } from "~/app/Brackets";
10
10
  import { FormatText } from "~/app/FormatText";
11
11
  import { Store } from "~/app/Store";
12
12
  import { Timer } from "~/core/Timer";
13
+ import { cache_message } from "~/core/cache_message";
13
14
  import { cli } from "~/core/cli";
14
15
  import { colors } from "~/core/colors";
15
16
  import { get_tmp_dir } from "~/core/get_tmp_dir";
@@ -40,7 +41,6 @@ export async function pr_list(): Promise<Array<PullRequest>> {
40
41
  if (actions.isDebug()) {
41
42
  actions.output(
42
43
  <FormatText
43
- wrapper={<Ink.Text dimColor />}
44
44
  message="Github cache {count} open PRs from {repo_path} authored by {username}"
45
45
  values={{
46
46
  count: (
@@ -101,7 +101,8 @@ export async function pr_status(branch: string): Promise<null | PullRequest> {
101
101
  );
102
102
  }
103
103
 
104
- const pr = await gh_json<PullRequest>(`pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`);
104
+ const commmand = `pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`;
105
+ const pr = await gh_json<PullRequest>(commmand, { branch });
105
106
 
106
107
  if (pr instanceof Error) {
107
108
  return null;
@@ -241,48 +242,37 @@ export async function pr_draft(args: DraftPullRequestArgs) {
241
242
  }
242
243
  }
243
244
 
244
- export async function pr_diff(number: number) {
245
- const state = Store.getState();
246
- const actions = state.actions;
245
+ export async function pr_diff(branch: string) {
246
+ // https://cli.github.com/manual/gh_pr_diff
247
+ const result = await gh(`pr diff --color=never ${branch}`, { branch });
247
248
 
248
- const maybe_diff = state.cache_pr_diff[number];
249
+ if (result instanceof Error) {
250
+ handle_error(result.message);
251
+ }
249
252
 
250
- if (maybe_diff) {
251
- if (actions.isDebug()) {
252
- actions.debug(
253
- cache_message({
254
- hit: true,
255
- message: "Github pr_diff cache",
256
- extra: number,
257
- }),
258
- );
259
- }
253
+ return result;
254
+ }
260
255
 
261
- return maybe_diff;
262
- }
256
+ export async function pr_compare(branch: string) {
257
+ const state = Store.getState();
258
+ const master_branch = state.master_branch;
259
+ const repo_path = state.repo_path;
260
+ invariant(master_branch, "master_branch must exist");
261
+ invariant(repo_path, "repo_path must exist");
263
262
 
264
- if (actions.isDebug()) {
265
- actions.debug(
266
- cache_message({
267
- hit: false,
268
- message: "Github pr_diff cache",
269
- extra: number,
270
- }),
271
- );
272
- }
263
+ const master_branch_name = master_branch.replace(/^origin\//, "");
273
264
 
274
- // https://cli.github.com/manual/gh_pr_diff
275
- const cli_result = await cli(`gh pr diff --color=never ${number}`);
265
+ // gh api repos/openai/openai/compare/master...chrome/publish/vine-1211---4h2xmw0o3ndvnt
266
+ const result = await gh_json<BranchCompare>(
267
+ `api repos/${repo_path}/compare/${master_branch_name}...${branch}`,
268
+ { branch },
269
+ );
276
270
 
277
- if (cli_result.code !== 0) {
278
- handle_error(cli_result.output);
271
+ if (result instanceof Error) {
272
+ handle_error(result.message);
279
273
  }
280
274
 
281
- actions.set((state) => {
282
- state.cache_pr_diff[number] = cli_result.output;
283
- });
284
-
285
- return cli_result.stdout;
275
+ return result;
286
276
  }
287
277
 
288
278
  // pull request JSON fields
@@ -290,29 +280,99 @@ export async function pr_diff(number: number) {
290
280
  // prettier-ignore
291
281
  const JSON_FIELDS = "--json id,number,state,baseRefName,headRefName,commits,title,body,url,isDraft";
292
282
 
283
+ type GhCmdOptions = {
284
+ branch?: string;
285
+ };
286
+
293
287
  // consistent handle gh cli commands returning json
294
288
  // redirect to tmp file to avoid scrollback overflow causing scrollback to be cleared
295
- async function gh_json<T>(command: string): Promise<T | Error> {
289
+ async function gh_json<T>(command: string, gh_options?: GhCmdOptions): Promise<T | Error> {
290
+ const gh_result = await gh(command, gh_options);
291
+
292
+ if (gh_result instanceof Error) {
293
+ return gh_result;
294
+ }
295
+
296
+ try {
297
+ const json = JSON.parse(gh_result);
298
+ return json as T;
299
+ } catch (error) {
300
+ return new Error(`gh_json JSON.parse: ${error}`);
301
+ }
302
+ }
303
+
304
+ // consistent handle gh cli commands
305
+ // redirect to tmp file to avoid scrollback overflow causing scrollback to be cleared
306
+ async function gh(command: string, gh_options?: GhCmdOptions): Promise<string | Error> {
307
+ const state = Store.getState();
308
+ const actions = state.actions;
309
+
310
+ if (gh_options?.branch) {
311
+ const branch = gh_options.branch;
312
+
313
+ type CacheEntryByHeadRefName = (typeof state.cache_gh_cli_by_branch)[string][string];
314
+
315
+ let cache: undefined | CacheEntryByHeadRefName = undefined;
316
+
317
+ if (branch) {
318
+ if (state.cache_gh_cli_by_branch[branch]) {
319
+ cache = state.cache_gh_cli_by_branch[branch][command];
320
+ }
321
+ }
322
+
323
+ if (cache) {
324
+ if (actions.isDebug()) {
325
+ actions.debug(
326
+ cache_message({
327
+ hit: true,
328
+ message: "gh cache",
329
+ extra: command,
330
+ }),
331
+ );
332
+ }
333
+
334
+ return cache;
335
+ }
336
+
337
+ if (actions.isDebug()) {
338
+ actions.debug(
339
+ cache_message({
340
+ hit: false,
341
+ message: "gh cache",
342
+ extra: command,
343
+ }),
344
+ );
345
+ }
346
+ }
347
+
296
348
  // hash command for unique short string
297
349
  let hash = crypto.createHash("md5").update(command).digest("hex");
298
- let tmp_filename = safe_filename(`gh_json-${hash}`);
299
- const tmp_pr_json = path.join(await get_tmp_dir(), `${tmp_filename}.json`);
350
+ let tmp_filename = safe_filename(`gh-${hash}`);
351
+ const tmp_filepath = path.join(await get_tmp_dir(), `${tmp_filename}`);
300
352
 
301
353
  const options = { ignoreExitCode: true };
302
- const cli_result = await cli(`gh ${command} > ${tmp_pr_json}`, options);
354
+ const cli_result = await cli(`gh ${command} > ${tmp_filepath}`, options);
303
355
 
304
356
  if (cli_result.code !== 0) {
305
357
  return new Error(cli_result.output);
306
358
  }
307
359
 
308
360
  // read from file
309
- const json_str = String(await fs.readFile(tmp_pr_json));
310
- try {
311
- const json = JSON.parse(json_str);
312
- return json;
313
- } catch (error) {
314
- return new Error(`gh_json JSON.parse: ${error}`);
361
+ let content = String(await fs.readFile(tmp_filepath));
362
+ content = content.trim();
363
+
364
+ if (gh_options?.branch) {
365
+ const branch = gh_options.branch;
366
+
367
+ actions.set((state) => {
368
+ if (!state.cache_gh_cli_by_branch[branch]) {
369
+ state.cache_gh_cli_by_branch[branch] = {};
370
+ }
371
+ state.cache_gh_cli_by_branch[branch][command] = content;
372
+ });
315
373
  }
374
+
375
+ return content;
316
376
  }
317
377
 
318
378
  function handle_error(output: string): never {
@@ -349,36 +409,6 @@ function safe_filename(value: string): string {
349
409
  return value.replace(RE.non_alphanumeric_dash, "-");
350
410
  }
351
411
 
352
- type CacheMessageArgs = {
353
- hit: boolean;
354
- message: React.ReactNode;
355
- extra: React.ReactNode;
356
- };
357
-
358
- function cache_message(args: CacheMessageArgs) {
359
- const status = args.hit ? (
360
- <Ink.Text bold color={colors.green}>
361
- HIT
362
- </Ink.Text>
363
- ) : (
364
- <Ink.Text bold color={colors.red}>
365
- MISS
366
- </Ink.Text>
367
- );
368
-
369
- return (
370
- <FormatText
371
- wrapper={<Ink.Text dimColor />}
372
- message="{message} {status} {extra}"
373
- values={{
374
- message: args.message,
375
- status,
376
- extra: args.extra,
377
- }}
378
- />
379
- );
380
- }
381
-
382
412
  type Commit = {
383
413
  authoredDate: string; // "2023-10-22T23:13:35Z"
384
414
  authors: [
@@ -408,6 +438,36 @@ export type PullRequest = {
408
438
  isDraft: boolean;
409
439
  };
410
440
 
441
+ type MergeBaseCommit = {
442
+ author: unknown;
443
+ comments_url: string;
444
+ commit: unknown;
445
+ committer: unknown;
446
+ html_url: string;
447
+ node_id: string;
448
+ parents: unknown;
449
+ sha: string;
450
+ url: string;
451
+ };
452
+
453
+ export type BranchCompare = {
454
+ ahead_by: number;
455
+ base_commit: unknown;
456
+ behind_by: number;
457
+ commits: unknown;
458
+ diff_url: string;
459
+
460
+ files: unknown;
461
+ html_url: string;
462
+
463
+ merge_base_commit: MergeBaseCommit;
464
+ patch_url: string;
465
+ permalink_url: string;
466
+ status: unknown;
467
+ total_commits: number;
468
+ url: string;
469
+ };
470
+
411
471
  const RE = {
412
472
  non_alphanumeric_dash: /[^a-zA-Z0-9_-]+/g,
413
473
  };
package/src/index.tsx CHANGED
@@ -5,7 +5,6 @@
5
5
  import * as React from "react";
6
6
 
7
7
  import fs from "node:fs/promises";
8
- import path from "node:path";
9
8
 
10
9
  import * as Ink from "ink-cjs";
11
10
 
@@ -61,10 +60,6 @@ import { pretty_json } from "~/core/pretty_json";
61
60
 
62
61
  actions.debug(pretty_json(argv as any));
63
62
 
64
- const PATH = process.env["PATH"];
65
- const PATH_LIST = pretty_json(PATH.split(path.delimiter));
66
- actions.debug(`process.env.PATH ${PATH_LIST}`);
67
-
68
63
  await ink.waitUntilExit();
69
64
 
70
65
  function maybe_verbose_help() {
@@ -1,6 +1,7 @@
1
1
  declare namespace NodeJS {
2
2
  interface ProcessEnv {
3
3
  PATH: string;
4
+ HOME: string;
4
5
  DEV?: "true" | "false";
5
6
  CLI_VERSION?: string;
6
7
  GIT_SEQUENCE_EDITOR_SCRIPT?: string;
@@ -1,8 +0,0 @@
1
- import * as React from "react";
2
-
3
- import * as Ink from "ink-cjs";
4
- import { DateTime } from "luxon";
5
-
6
- export function LogTimestamp() {
7
- return <Ink.Text dimColor>{DateTime.now().toFormat("[yyyy-MM-dd HH:mm:ss.SSS] ")}</Ink.Text>;
8
- }
package/src/core/git.ts DELETED
@@ -1,83 +0,0 @@
1
- import { Store } from "~/app/Store";
2
- import * as Metadata from "~/core/Metadata";
3
- import { cli } from "~/core/cli";
4
- import { invariant } from "~/core/invariant";
5
-
6
- type CommitList = Awaited<ReturnType<typeof get_commits>>;
7
-
8
- export type Commit = CommitList[0];
9
-
10
- export async function get_commits(dot_range: string) {
11
- const log_result = await cli(`git log ${dot_range} --format=${FORMAT} --color=never`);
12
-
13
- if (!log_result.stdout) {
14
- return [];
15
- }
16
-
17
- const commit_list = [];
18
-
19
- for (let record of log_result.stdout.split(SEP.record)) {
20
- record = record.replace(/^\n/, "");
21
- record = record.replace(/\n$/, "");
22
-
23
- if (!record) continue;
24
-
25
- const [sha, full_message] = record.split(SEP.field);
26
-
27
- // ensure sha is a hex string, otherwise we should throw an error
28
- if (!RE.git_sha.test(sha)) {
29
- const actions = Store.getState().actions;
30
- const sep_values = JSON.stringify(Object.values(SEP));
31
- const message = `unable to parse git commits, maybe commit message contained ${sep_values}`;
32
- actions.error(message);
33
- actions.exit(19);
34
- }
35
-
36
- const metadata = Metadata.read(full_message);
37
- const branch_id = metadata.id;
38
- const subject_line = metadata.subject || "";
39
- const title = metadata.title;
40
- const master_base = metadata.base === "master";
41
-
42
- const commit = {
43
- sha,
44
- full_message,
45
- subject_line,
46
- branch_id,
47
- title,
48
- master_base,
49
- };
50
-
51
- commit_list.push(commit);
52
- }
53
-
54
- commit_list.reverse();
55
-
56
- return commit_list;
57
- }
58
-
59
- export async function get_diff(commit_list: CommitList) {
60
- invariant(commit_list.length, "commit_list must exist");
61
- const first_commit = commit_list[0];
62
- const last_commit = commit_list[commit_list.length - 1];
63
- const sha_range = `${first_commit.sha}~1..${last_commit.sha}`;
64
- const diff_result = await cli(`git --no-pager diff --color=never ${sha_range}`);
65
- return diff_result.stdout;
66
- }
67
-
68
- // Why these separators?
69
- // - Rare in human written text
70
- // - Supported in git %xNN to write bytes
71
- // - Supported in javascript \xNN to write bytes
72
- // - Used historically as separators in unicode
73
- // https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators
74
- const SEP = {
75
- record: "\x1e",
76
- field: "\x1f",
77
- };
78
-
79
- const FORMAT = `%H${SEP.field}%B${SEP.record}`;
80
-
81
- const RE = {
82
- git_sha: /^[0-9a-fA-F]{40}$/,
83
- };