git-stack-cli 1.8.2 → 1.9.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/src/command.ts CHANGED
@@ -1,103 +1,33 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
 
4
- export type Argv = Awaited<ReturnType<typeof command>> & {
5
- ["rebase"]: keyof typeof Rebase;
6
- };
4
+ import type { Options, InferredOptionTypes, Arguments } from "yargs";
5
+
6
+ export type Argv = Arguments & TGlobalOptions & TFixupOptions & TDefaultOptions;
7
7
 
8
8
  export async function command() {
9
9
  // https://yargs.js.org/docs/#api-reference-optionkey-opt
10
10
  return (
11
11
  yargs(hideBin(process.argv))
12
- .usage("Usage: git stack [options]")
13
-
14
- .option("force", {
15
- type: "boolean",
16
- alias: ["f"],
17
- default: false,
18
- description: "Force sync even if no changes are detected",
19
- })
20
-
21
- .option("check", {
22
- type: "boolean",
23
- alias: ["c"],
24
- default: false,
25
- description: "Print status table and exit without syncing",
26
- })
27
-
28
- .option("sync", {
29
- type: "boolean",
30
- alias: ["s"],
31
- default: true,
32
- description: "Sync commit ranges to Github, disable with --no-sync",
33
- })
34
-
35
- .option("verify", {
36
- type: "boolean",
37
- default: true,
38
- description:
39
- "Run git hooks such as pre-commit and pre-push, disable with --no-verify",
40
- })
41
-
42
- .option("rebase", {
43
- type: "string",
44
- choices: [Rebase["git-revise"], Rebase["cherry-pick"]],
45
- default: Rebase["git-revise"],
46
- description: [
47
- "Strategy used for syncing branches",
48
- `${Rebase["git-revise"]}: perform faster in-memory rebase`,
49
- `${Rebase["cherry-pick"]}: use disk and incrementally rebase each commit`,
50
- ].join(" | "),
51
- })
52
-
53
- .option("verbose", {
54
- type: "boolean",
55
- alias: ["v"],
56
- default: false,
57
- description: "Print more detailed logs for debugging internals",
58
- })
59
-
60
- .option("update", {
61
- type: "boolean",
62
- alias: ["u", "upgrade"],
63
- default: false,
64
- description: "Check and install the latest version",
65
- })
66
-
67
- .option("branch", {
68
- type: "string",
69
- alias: ["b"],
70
- description:
71
- 'Set the master branch name, defaults to "master" (or "main" if "master" is not found)',
72
- })
73
-
74
- .option("draft", {
75
- type: "boolean",
76
- alias: ["d"],
77
- default: false,
78
- description: "Open all PRs as drafts",
79
- })
80
-
81
- .option("write-state-json", {
82
- hidden: true,
83
- type: "boolean",
84
- default: false,
85
- description: "Write state to local json file for debugging",
86
- })
87
-
88
- .option("template", {
89
- type: "boolean",
90
- default: true,
91
- description:
92
- "Use automatic Github PR template, e.g. .github/pull_request_template.md, disable with --no-template",
93
- })
94
-
95
- .option("mock-metadata", {
96
- hidden: true,
97
- type: "boolean",
98
- default: false,
99
- description: "Mock local store metadata for testing",
100
- })
12
+ .usage("Usage: git stack [command] [options]")
13
+
14
+ .command("$0", "Sync commit ranges to Github", (yargs) =>
15
+ yargs.options(DefaultOptions)
16
+ )
17
+
18
+ .command(
19
+ "fixup [commit]",
20
+ "Amend staged changes to a specific commit in history",
21
+ (yargs) => yargs.positional("commit", FixupOptions.commit)
22
+ )
23
+
24
+ .command(
25
+ "log [args...]",
26
+ "Print an abbreviated log with numbered commits, useful for git stack fixup",
27
+ (yargs) => yargs.strict(false)
28
+ )
29
+
30
+ .option("verbose", GlobalOptions.verbose)
101
31
 
102
32
  // yargs default wraps to 80 columns
103
33
  // passing null will wrap to terminal width
@@ -111,7 +41,8 @@ export async function command() {
111
41
  "show-hidden",
112
42
  "Show hidden options via `git stack help --show-hidden`"
113
43
  )
114
- .help("help", "Show usage via `git stack help`").argv
44
+ .help("help", "Show usage via `git stack help`")
45
+ .argv as unknown as Promise<Argv>
115
46
  );
116
47
  }
117
48
 
@@ -119,3 +50,112 @@ const Rebase = Object.freeze({
119
50
  "git-revise": "git-revise",
120
51
  "cherry-pick": "cherry-pick",
121
52
  });
53
+
54
+ const GlobalOptions = {
55
+ verbose: {
56
+ type: "boolean",
57
+ alias: ["v"],
58
+ default: false,
59
+ description: "Print more detailed logs for debugging internals",
60
+ },
61
+ } satisfies YargsOptions;
62
+
63
+ const DefaultOptions = {
64
+ "force": {
65
+ type: "boolean",
66
+ alias: ["f"],
67
+ default: false,
68
+ description: "Force sync even if no changes are detected",
69
+ },
70
+
71
+ "check": {
72
+ type: "boolean",
73
+ alias: ["c"],
74
+ default: false,
75
+ description: "Print status table and exit without syncing",
76
+ },
77
+
78
+ "sync": {
79
+ type: "boolean",
80
+ alias: ["s"],
81
+ default: true,
82
+ description: "Sync commit ranges to Github, disable with --no-sync",
83
+ },
84
+
85
+ "verify": {
86
+ type: "boolean",
87
+ default: true,
88
+ description:
89
+ "Run git hooks such as pre-commit and pre-push, disable with --no-verify",
90
+ },
91
+
92
+ "rebase": {
93
+ type: "string",
94
+ choices: [Rebase["git-revise"], Rebase["cherry-pick"]],
95
+ default: Rebase["git-revise"],
96
+ description: [
97
+ "Strategy used for syncing branches",
98
+ `${Rebase["git-revise"]}: perform faster in-memory rebase`,
99
+ `${Rebase["cherry-pick"]}: use disk and incrementally rebase each commit`,
100
+ ].join(" | "),
101
+ },
102
+
103
+ "update": {
104
+ type: "boolean",
105
+ alias: ["u", "upgrade"],
106
+ default: false,
107
+ description: "Check and install the latest version",
108
+ },
109
+
110
+ "branch": {
111
+ type: "string",
112
+ alias: ["b"],
113
+ description:
114
+ 'Set the master branch name, defaults to "master" (or "main" if "master" is not found)',
115
+ },
116
+
117
+ "draft": {
118
+ type: "boolean",
119
+ alias: ["d"],
120
+ default: false,
121
+ description: "Open all PRs as drafts",
122
+ },
123
+
124
+ "write-state-json": {
125
+ hidden: true,
126
+ type: "boolean",
127
+ default: false,
128
+ description: "Write state to local json file for debugging",
129
+ },
130
+
131
+ "template": {
132
+ type: "boolean",
133
+ default: true,
134
+ description:
135
+ "Use automatic Github PR template, e.g. .github/pull_request_template.md, disable with --no-template",
136
+ },
137
+
138
+ "mock-metadata": {
139
+ hidden: true,
140
+ type: "boolean",
141
+ default: false,
142
+ description: "Mock local store metadata for testing",
143
+ },
144
+ } satisfies YargsOptions;
145
+
146
+ const FixupOptions = {
147
+ commit: {
148
+ type: "number",
149
+ default: 1,
150
+ description: [
151
+ "Relative number of commit to amend staged changes.",
152
+ "Most recent is 1, next is 2, etc.",
153
+ ].join("\n"),
154
+ },
155
+ } satisfies YargsOptions;
156
+
157
+ type YargsOptions = { [key: string]: Options };
158
+
159
+ type TGlobalOptions = InferredOptionTypes<typeof GlobalOptions>;
160
+ type TFixupOptions = InferredOptionTypes<typeof FixupOptions>;
161
+ type TDefaultOptions = InferredOptionTypes<typeof DefaultOptions>;
@@ -0,0 +1,121 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { Await } from "~/app/Await";
6
+ import { FormatText } from "~/app/FormatText";
7
+ import { Parens } from "~/app/Parens";
8
+ import { Store } from "~/app/Store";
9
+ import { cli } from "~/core/cli";
10
+ import { colors } from "~/core/colors";
11
+
12
+ export function Fixup() {
13
+ return <Await fallback={null} function={run} />;
14
+ }
15
+
16
+ async function run() {
17
+ const state = Store.getState();
18
+ const actions = state.actions;
19
+ const argv = state.argv;
20
+
21
+ const relative_number = argv.commit;
22
+
23
+ if (!relative_number) {
24
+ actions.output(
25
+ <Ink.Text color={colors.red}>
26
+ ❗️ Usage: git fixup {"<relative-commit-number>"}
27
+ </Ink.Text>
28
+ );
29
+ actions.output("");
30
+ actions.output(
31
+ "This script automates the process of adding staged changes as a fixup commit"
32
+ );
33
+ actions.output(
34
+ "and the subsequent git rebase to flatten the commits based on relative commit number"
35
+ );
36
+ actions.output(
37
+ "You can use a `git log` like below to get the relative commit number"
38
+ );
39
+ actions.output("");
40
+ actions.output(" ❯ git stack log");
41
+ actions.output(
42
+ " 1\te329794d5f881cbf0fc3f26d2108cf6f3fdebabe enable drop_error_subtask test param"
43
+ );
44
+ actions.output(
45
+ " 2\t57f43b596e5c6b97bc47e2a591f82ccc81651156 test drop_error_subtask baseline"
46
+ );
47
+ actions.output(
48
+ " 3\t838e878d483c6a2d5393063fc59baf2407225c6d ErrorSubtask test baseline"
49
+ );
50
+ actions.output("");
51
+ actions.output("To target `838e87` above, you would call `fixup 3`");
52
+
53
+ actions.exit(0);
54
+ }
55
+
56
+ const diff_staged_cmd = await cli("git diff --cached --quiet", {
57
+ ignoreExitCode: true,
58
+ });
59
+
60
+ if (!diff_staged_cmd.code) {
61
+ actions.error("🚨 Stage changes before calling fixup");
62
+ actions.exit(1);
63
+ // actions.output(
64
+ // <Ink.Text color={colors.red}>
65
+ // ❗️ Usage: git fixup {"<relative-commit-number>"}
66
+ // </Ink.Text>
67
+ // );
68
+ }
69
+
70
+ // Calculate commit SHA based on the relative commit number
71
+ const adjusted_number = Number(relative_number) - 1;
72
+
73
+ // get the commit SHA of the target commit
74
+ const commit_sha = (await cli(`git rev-parse HEAD~${adjusted_number}`))
75
+ .stdout;
76
+
77
+ await cli(`git commit --fixup ${commit_sha}`);
78
+
79
+ // check if stash required
80
+ let save_stash = false;
81
+
82
+ const diff_cmd = await cli("git diff-index --quiet HEAD --", {
83
+ ignoreExitCode: true,
84
+ });
85
+
86
+ if (diff_cmd.code) {
87
+ save_stash = true;
88
+
89
+ await cli("git stash -q");
90
+
91
+ actions.output(<Ink.Text>📦 Changes saved to stash</Ink.Text>);
92
+ }
93
+
94
+ // rebase target needs to account for new commit created above
95
+ const rebase_target = Number(relative_number) + 1;
96
+ await cli(`git rebase -i --autosquash HEAD~${rebase_target}`, {
97
+ env: {
98
+ PATH: process.env.PATH,
99
+ GIT_EDITOR: "true",
100
+ },
101
+ });
102
+
103
+ actions.output(
104
+ <FormatText
105
+ wrapper={<Ink.Text color={colors.yellow} />}
106
+ message="🛠️ fixup {relative_number} {commit_sha}"
107
+ values={{
108
+ commit_sha: <Parens>{commit_sha}</Parens>,
109
+ relative_number: relative_number,
110
+ }}
111
+ />
112
+ );
113
+
114
+ if (save_stash) {
115
+ await cli("git stash pop -q");
116
+
117
+ actions.output(
118
+ <Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>
119
+ );
120
+ }
121
+ }
@@ -0,0 +1,72 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { Await } from "~/app/Await";
6
+ import { Store } from "~/app/Store";
7
+ import { cli } from "~/core/cli";
8
+ import { invariant } from "~/core/invariant";
9
+
10
+ export function Log() {
11
+ const { stdout } = Ink.useStdout();
12
+ const available_width = stdout.columns || 80;
13
+
14
+ return <Await fallback={null} function={() => run({ available_width })} />;
15
+ }
16
+
17
+ type Args = {
18
+ available_width: number;
19
+ };
20
+
21
+ async function run(args: Args) {
22
+ const state = Store.getState();
23
+ const actions = state.actions;
24
+ const process_argv = state.process_argv;
25
+
26
+ invariant(actions, "actions must exist");
27
+
28
+ // estimate the number of color characters per line
29
+ // assuming an average of 5 color changes per line and 5 characters per color code
30
+ const color_buffer = 12 * 5;
31
+ const truncation_width = args.available_width + color_buffer;
32
+
33
+ // get the number of characters in the short sha for this repo
34
+ const short_sha = (await cli(`git log -1 --format=%h`)).stdout.trim();
35
+ const short_sha_length = short_sha.length + 1;
36
+
37
+ // SHA hash - At least 9 characters wide, truncated
38
+ const sha_format = `%C(green)%<(${short_sha_length},trunc)%h`;
39
+
40
+ // relative commit date - 15 characters wide, truncated
41
+ const date_format = `%C(white)%<(15,trunc)%cr`;
42
+
43
+ // author's abbreviated name - 12 characters wide, truncated
44
+ const author_format = `%C(white)%<(8,trunc)%al`;
45
+
46
+ // decorative information like branch heads or tags
47
+ const decoration_format = `%C(auto)%d`;
48
+
49
+ // commit subject - 80 characters wide, truncated
50
+ const subject_format = `%<(60,trunc)%s`;
51
+
52
+ // combine all the above formats into one
53
+ const format = [
54
+ sha_format,
55
+ date_format,
56
+ author_format,
57
+ decoration_format,
58
+ subject_format,
59
+ ].join(" ");
60
+
61
+ // view the SHA, description and history graph of last 20 commits
62
+ const rest_args = process_argv.slice(3).join(" ");
63
+ const command = [
64
+ `git log --pretty=format:"${format}" -n20 --graph --color ${rest_args}`,
65
+ `cut -c 1-"${truncation_width}"`,
66
+ `nl -w3 -s' '`,
67
+ ].join(" | ");
68
+
69
+ const result = await cli(command);
70
+
71
+ actions.output(result.stdout);
72
+ }
@@ -2,6 +2,7 @@ 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";
5
6
 
6
7
  export type CommitMetadata = Awaited<ReturnType<typeof commit>>;
7
8
  export type CommitRange = Awaited<ReturnType<typeof range>>;
@@ -166,6 +167,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
166
167
 
167
168
  async function get_commit_list() {
168
169
  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
+
169
174
  const log_result = await cli(
170
175
  `git log ${master_branch}..HEAD --oneline --format=%H --color=never`
171
176
  );
@@ -178,12 +183,30 @@ async function get_commit_list() {
178
183
 
179
184
  const commit_metadata_list = [];
180
185
 
186
+ let has_metadata = false;
187
+
181
188
  for (let i = 0; i < sha_list.length; i++) {
182
189
  const sha = sha_list[i];
183
190
  const commit_metadata = await commit(sha);
191
+
192
+ if (commit_metadata.branch_id) {
193
+ has_metadata = true;
194
+ }
195
+
184
196
  commit_metadata_list.push(commit_metadata);
185
197
  }
186
198
 
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
+
187
210
  return commit_metadata_list;
188
211
  }
189
212