Skip to content

Commit 922ef25

Browse files
committed
BREAKING CHANGE: completely overhaul arg parsing & allow invoking CLI w/o initialBranch if cached
...as long as the initialBranch was already provided in some previous invocation of stacked-rebase, thus cached. sometime later, we could have a config for the default initial branch.. - create generic utils for argv parsing (separate package soon?) - introduce multiple tests, which caught lots of (mostly new) bugs! - tidy up code at start of gitStackedRebase, which now makes great sense & looks nice - same in other places - clear up mental model of "parsing options" - it's "resolve" instead of "parse" - move initialBranch parsing into resolveOptions - don't want out of sync `resolvedOptions.initialBranch` vs `nameOfInitialBranch` - get rid of "intial branch is optional for some args" -- non issue - because e.g. for --continue to have any meaning, you'd have to have already started a stacked rebase & thus would have the initial branch cached.
1 parent 0ed7f81 commit 922ef25

19 files changed

+714
-260
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ git-stacked-rebase <branch>
8686
2. but will not apply the changes to partial branches until --apply is used.
8787

8888

89-
git-stacked-rebase <branch> [-a|--apply]
89+
git-stacked-rebase [-a|--apply]
9090

91-
1. will apply the changes to partial branches,
92-
2. but will not push any partial branches to a remote until --push is used.
91+
3. will apply the changes to partial branches,
92+
4. but will not push any partial branches to a remote until --push is used.
9393

9494

95-
git-stacked-rebase <branch> [-p|--push -f|--force]
95+
git-stacked-rebase [-p|--push -f|--force]
9696

97-
1. will push partial branches with --force (and extra safety).
97+
5. will push partial branches with --force (and extra safety).
9898

9999

100100

argparse/argparse.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env ts-node-dev
2+
3+
import assert from "assert";
4+
5+
import { NonPositional, NonPositionalWithValue, eatNonPositionals, eatNonPositionalsWithValues } from "./argparse";
6+
7+
export function argparse_TC() {
8+
eatNonPositionals_singleArg();
9+
eatNonPositionals_multipleArgs();
10+
11+
eatNonPositionalsWithValues_singleArg();
12+
eatNonPositionalsWithValues_multipleArgs();
13+
}
14+
15+
function eatNonPositionals_singleArg() {
16+
const argv = ["origin/master", "--autosquash", "foo", "bar"];
17+
const targetArgs = ["--autosquash", "--no-autosquash"];
18+
const expected: NonPositional[] = [{ argName: "--autosquash", origIdx: 1 }];
19+
const expectedLeftoverArgv = ["origin/master", "foo", "bar"];
20+
21+
const parsed = eatNonPositionals(targetArgs, argv);
22+
23+
assert.deepStrictEqual(parsed, expected);
24+
assert.deepStrictEqual(argv, expectedLeftoverArgv);
25+
}
26+
function eatNonPositionals_multipleArgs() {
27+
const argv = ["origin/master", "--autosquash", "foo", "bar", "--no-autosquash", "baz"];
28+
const targetArgs = ["--autosquash", "--no-autosquash"];
29+
const expected: NonPositional[] = [
30+
{ argName: "--autosquash", origIdx: 1 },
31+
{ argName: "--no-autosquash", origIdx: 4 },
32+
];
33+
const expectedLeftoverArgv = ["origin/master", "foo", "bar", "baz"];
34+
35+
const parsed = eatNonPositionals(targetArgs, argv);
36+
37+
assert.deepStrictEqual(parsed, expected);
38+
assert.deepStrictEqual(argv, expectedLeftoverArgv);
39+
}
40+
41+
function eatNonPositionalsWithValues_singleArg() {
42+
const argv = ["origin/master", "--git-dir", "~/.dotfiles", "foo", "bar"];
43+
const targetArgs = ["--git-dir", "--gd"];
44+
const expected: NonPositionalWithValue[] = [{ argName: "--git-dir", origIdx: 1, argVal: "~/.dotfiles" }];
45+
const expectedLeftoverArgv = ["origin/master", "foo", "bar"];
46+
47+
const parsed = eatNonPositionalsWithValues(targetArgs, argv);
48+
49+
assert.deepStrictEqual(parsed, expected);
50+
assert.deepStrictEqual(argv, expectedLeftoverArgv);
51+
}
52+
function eatNonPositionalsWithValues_multipleArgs() {
53+
const argv = ["origin/master", "--git-dir", "~/.dotfiles", "foo", "bar", "--misc", "miscVal", "unrelatedVal"];
54+
const targetArgs = ["--git-dir", "--gd", "--misc"];
55+
const expected: NonPositionalWithValue[] = [
56+
{ argName: "--git-dir", origIdx: 1, argVal: "~/.dotfiles" },
57+
{ argName: "--misc", origIdx: 5, argVal: "miscVal" },
58+
];
59+
const expectedLeftoverArgv = ["origin/master", "foo", "bar", "unrelatedVal"];
60+
61+
const parsed = eatNonPositionalsWithValues(targetArgs, argv);
62+
63+
assert.deepStrictEqual(parsed, expected);
64+
assert.deepStrictEqual(argv, expectedLeftoverArgv);
65+
}
66+
67+
if (!module.parent) {
68+
argparse_TC();
69+
}

argparse/argparse.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import assert from "assert";
2+
3+
export type Maybe<T> = T | undefined;
4+
5+
export type Argv = string[];
6+
export type MaybeArg = Maybe<string>;
7+
8+
/**
9+
* parses the argv.
10+
* mutates the `argv` array.
11+
*/
12+
export function createArgParse(argv: Argv) {
13+
const getArgv = (): Argv => argv;
14+
const peakNextArg = (): MaybeArg => argv[0];
15+
const eatNextArg = (): MaybeArg => argv.shift();
16+
const hasMoreArgs = (): boolean => argv.length > 0;
17+
18+
return {
19+
getArgv,
20+
peakNextArg,
21+
eatNextArg,
22+
hasMoreArgs,
23+
eatNonPositionals: (argNames: string[]) => eatNonPositionals(argNames, argv),
24+
eatNonPositionalsWithValues: (argNames: string[]) => eatNonPositionalsWithValues(argNames, argv),
25+
};
26+
}
27+
28+
export type NonPositional = {
29+
origIdx: number;
30+
argName: string;
31+
};
32+
33+
export type NonPositionalWithValue = NonPositional & {
34+
argVal: string;
35+
};
36+
37+
export function eatNonPositionals(
38+
argNames: string[],
39+
argv: Argv,
40+
{
41+
howManyItemsToTakeWhenArgMatches = 1, //
42+
} = {}
43+
): NonPositional[] {
44+
const argMatches = (idx: number) => argNames.includes(argv[idx]);
45+
let matchedArgIndexes: NonPositional["origIdx"][] = [];
46+
47+
for (let i = 0; i < argv.length; i++) {
48+
if (argMatches(i)) {
49+
for (let j = 0; j < howManyItemsToTakeWhenArgMatches; j++) {
50+
matchedArgIndexes.push(i + j);
51+
}
52+
}
53+
}
54+
55+
if (!matchedArgIndexes.length) {
56+
return [];
57+
}
58+
59+
const nonPositionalsWithValues: NonPositional[] = [];
60+
for (const idx of matchedArgIndexes) {
61+
nonPositionalsWithValues.push({
62+
origIdx: idx,
63+
argName: argv[idx],
64+
});
65+
}
66+
67+
const shouldRemoveArg = (idx: number) => matchedArgIndexes.includes(idx);
68+
const argvIndexesToRemove: number[] = [];
69+
70+
for (let i = 0; i < argv.length; i++) {
71+
if (shouldRemoveArg(i)) {
72+
argvIndexesToRemove.push(i);
73+
}
74+
}
75+
76+
removeArrayValuesAtIndices(argv, argvIndexesToRemove);
77+
78+
return nonPositionalsWithValues;
79+
}
80+
81+
export function eatNonPositionalsWithValues(argNames: string[], argv: Argv): NonPositionalWithValue[] {
82+
const argsWithTheirValueAsNextItem: NonPositional[] = eatNonPositionals(argNames, argv, {
83+
howManyItemsToTakeWhenArgMatches: 2,
84+
});
85+
86+
assert.deepStrictEqual(argsWithTheirValueAsNextItem.length % 2, 0, `expected all arguments to have a value.`);
87+
88+
const properArgsWithValues: NonPositionalWithValue[] = [];
89+
for (let i = 0; i < argsWithTheirValueAsNextItem.length; i += 2) {
90+
const arg = argsWithTheirValueAsNextItem[i];
91+
const val = argsWithTheirValueAsNextItem[i + 1];
92+
93+
properArgsWithValues.push({
94+
origIdx: arg.origIdx,
95+
argName: arg.argName,
96+
argVal: val.argName,
97+
});
98+
}
99+
100+
return properArgsWithValues;
101+
}
102+
103+
/**
104+
* internal utils
105+
*/
106+
107+
export function removeArrayValuesAtIndices<T>(arrayRef: T[], indexesToRemove: number[]): void {
108+
/**
109+
* go in reverse.
110+
*
111+
* because if went from 0 to length,
112+
* removing an item from the array would adjust all other indices,
113+
* which creates a mess & needs extra handling.
114+
*/
115+
const indexesBigToSmall = [...indexesToRemove].sort((A, B) => B - A);
116+
117+
for (const idxToRemove of indexesBigToSmall) {
118+
arrayRef.splice(idxToRemove, 1);
119+
}
120+
121+
return;
122+
}
123+
124+
/**
125+
* common utilities for dealing w/ parsed values:
126+
*/
127+
128+
export function maybe<T, S, N>(
129+
x: T, //
130+
Some: (x: T) => S,
131+
None: (x?: never) => N
132+
) {
133+
if (x instanceof Array) {
134+
return x.length ? Some(x) : None();
135+
}
136+
137+
return x !== undefined ? Some(x) : None();
138+
}
139+
140+
export const last = <T>(xs: T[]): T => xs[xs.length - 1];

config.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import Git from "nodegit";
44

5-
import { SomeOptionsForGitStackedRebase } from "./options";
5+
import { SpecifiableGitStackedRebaseOptions } from "./options";
66
import { getGitConfig__internal } from "./internal";
77

88
export const configKeyPrefix = "stackedrebase" as const;
@@ -15,25 +15,52 @@ export const configKeys = {
1515

1616
export async function loadGitConfig(
1717
repo: Git.Repository,
18-
specifiedOptions: SomeOptionsForGitStackedRebase
18+
specifiedOptions: SpecifiableGitStackedRebaseOptions
1919
): Promise<Git.Config> {
2020
return getGitConfig__internal in specifiedOptions
2121
? await specifiedOptions[getGitConfig__internal]!({ GitConfig: Git.Config, repo })
2222
: await Git.Config.openDefault();
2323
}
2424

2525
export type ConfigValues = {
26-
gpgSign: boolean;
27-
autoApplyIfNeeded: boolean;
28-
autoSquash: boolean;
26+
gpgSign: boolean | undefined;
27+
autoApplyIfNeeded: boolean | undefined;
28+
autoSquash: boolean | undefined;
2929
};
3030

31-
export async function parseGitConfigValues(config: Git.Config): Promise<ConfigValues> {
31+
export async function resolveGitConfigValues(config: Git.Config): Promise<ConfigValues> {
32+
const [
33+
gpgSign, //
34+
autoApplyIfNeeded,
35+
autoSquash,
36+
] = await Promise.all([
37+
resolveConfigBooleanValue(config.getBool(configKeys.gpgSign)),
38+
resolveConfigBooleanValue(config.getBool(configKeys.autoApplyIfNeeded)),
39+
resolveConfigBooleanValue(config.getBool(configKeys.autoSquash)),
40+
]);
41+
3242
const configValues: ConfigValues = {
33-
gpgSign: !!(await config.getBool(configKeys.gpgSign).catch(() => 0)),
34-
autoApplyIfNeeded: !!(await config.getBool(configKeys.autoApplyIfNeeded).catch(() => 0)),
35-
autoSquash: !!(await config.getBool(configKeys.autoSquash).catch(() => 0)),
43+
gpgSign,
44+
autoApplyIfNeeded,
45+
autoSquash,
3646
};
3747

3848
return configValues;
3949
}
50+
51+
/**
52+
* there's a difference between a value set to false (intentionally disabled),
53+
* vs not set at all:
54+
* can then look thru lower level options providers, and take their value.
55+
*
56+
* ```
57+
* export const handleConfigBooleanValue = (x: Promise<number>) => x.then(Boolean).catch(() => undefined);
58+
* ```
59+
*
60+
* but actually, it doesn't matter here, because when we're trying to resolve (here),
61+
* our goal is to provide a final value that will be used by the program,
62+
* thus no `undefined`.
63+
*
64+
*/
65+
//
66+
export const resolveConfigBooleanValue = (x: Promise<number>) => x.then(Boolean).catch(() => false);

filenames.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export const filenames = {
1010
gitRebaseTodo: "git-rebase-todo",
1111
//
1212
postStackedRebaseHook: "post-stacked-rebase",
13+
//
14+
initialBranch: "initial-branch",
1315

1416
/**
1517
* TODO extract others into here

0 commit comments

Comments
 (0)