Skip to content

Commit 9dc430c

Browse files
committed
fix old bugs/wrong behavior in applyIfNeedsToApply & related logic
previously, in `gitStackedRebase`, there were 3 invocations of `applyIfNeedsToApply`. there are still 3, but what differs is: previously, the 3rd would be invoked if and only if the `config.autoApplyIfNeeded` was true, i.e., it was NOT mandatory. however, the 1st invocation, which also is NOT mandatory, did not have such guardrail, and, if the `config.autoApplyIfNeeded` was false, would prompt the user with the common message: `need to --apply before continuing. proceed? [Y/n/(a)lways] ` this is wrong, because this (1st) invocation is not mandatory, meanwhile the message makes it look like it is, thus confusing the user & their longer term understanding of when an --apply is actually needed. to further add to the confusion, the 1st invocation worked correctly apart from the message - it (1st invoc) was not made mandatory, i.e. would not stop the program execution if user did not allow. --- now, there's a clear distinction of mandatory vs not, and `applyIfNeedsToApply` works accordingly to it. related parts were also cleaned up. also, created a test for this -- yay! Signed-off-by: Kipras Melnikovas <kipras@kipras.org>
1 parent a4447a5 commit 9dc430c

File tree

5 files changed

+223
-99
lines changed

5 files changed

+223
-99
lines changed

apply.ts

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { combineRewrittenLists } from "./git-reconcile-rewritten-list/combineRew
66

77
import { AskQuestion, question, Questions } from "./util/createQuestion";
88
import { isDirEmptySync } from "./util/fs";
9+
import { Termination } from "./util/error";
910

1011
import { filenames } from "./filenames";
1112
import { configKeys } from "./config";
@@ -63,59 +64,81 @@ const defaultApplyAction: ActionInsideEachCheckedOutBranch = async ({
6364
export const getBackupPathOfPreviousStackedRebase = (pathToStackedRebaseDirInsideDotGit: string): string =>
6465
pathToStackedRebaseDirInsideDotGit + ".previous";
6566

66-
export type ReturnOfApplyIfNeedsToApply =
67-
| {
68-
neededToApply: false;
69-
userAllowedToApplyAndWeApplied?: never;
70-
}
71-
| {
72-
neededToApply: true;
73-
userAllowedToApplyAndWeApplied: false;
74-
}
75-
| {
76-
neededToApply: true;
77-
userAllowedToApplyAndWeApplied: true;
78-
};
7967
export async function applyIfNeedsToApply({
8068
repo,
8169
pathToStackedRebaseTodoFile,
8270
pathToStackedRebaseDirInsideDotGit, //
8371
autoApplyIfNeeded,
72+
isMandatoryIfMarkedAsNeeded,
8473
config,
8574
askQuestion = question,
8675
...rest
8776
}: BranchSequencerArgsBase & {
77+
/**
78+
* i.e., sometimes a) we need the `--apply` to go thru,
79+
* and sometimes b) it's "resumable" on the next run,
80+
* i.e. we'd prefer to apply right now,
81+
* but it's fine if user does not apply now,
82+
* because `--apply` is resumable[1],
83+
* and on the next run of stacked rebase, user will be forced to apply anyway.
84+
*
85+
* [1] resumable, unless user runs into some edge case where it no longer is.
86+
* TODO: work out when this happens & handle better.
87+
*/
88+
isMandatoryIfMarkedAsNeeded: boolean;
89+
8890
autoApplyIfNeeded: boolean; //
8991
config: Git.Config;
9092
askQuestion: AskQuestion;
91-
}): Promise<ReturnOfApplyIfNeedsToApply> {
93+
}): Promise<void> {
9294
const needsToApply: boolean = doesNeedToApply(pathToStackedRebaseDirInsideDotGit);
93-
9495
if (!needsToApply) {
95-
return {
96-
neededToApply: false,
97-
};
96+
return;
9897
}
9998

100-
const allowedToApply = autoApplyIfNeeded || (await askIfCanApply(config, askQuestion));
101-
if (!allowedToApply) {
102-
return {
103-
neededToApply: true,
104-
userAllowedToApplyAndWeApplied: false,
105-
};
106-
}
99+
if (isMandatoryIfMarkedAsNeeded) {
100+
/**
101+
* is marked as needed to apply,
102+
* and is mandatory -- try to get a confirmation that it is ok to apply.
103+
*/
104+
const userAllowedToApply = autoApplyIfNeeded || (await askIfCanApply(config, askQuestion));
105+
106+
if (!userAllowedToApply) {
107+
const msg = "\ncannot continue without mandatory --apply. Exiting.\n";
108+
throw new Termination(msg);
109+
} else {
110+
await apply({
111+
repo,
112+
pathToStackedRebaseTodoFile,
113+
pathToStackedRebaseDirInsideDotGit, //
114+
...rest,
115+
});
116+
117+
return;
118+
}
119+
} else {
120+
/**
121+
* is marked as needed to apply,
122+
* but performing the apply is NOT mandatory.
123+
*
124+
* thus, do NOT ask user if should apply -- only infer from config.
125+
*/
107126

108-
await apply({
109-
repo,
110-
pathToStackedRebaseTodoFile,
111-
pathToStackedRebaseDirInsideDotGit, //
112-
...rest,
113-
});
127+
if (autoApplyIfNeeded) {
128+
await apply({
129+
repo,
130+
pathToStackedRebaseTodoFile,
131+
pathToStackedRebaseDirInsideDotGit, //
132+
...rest,
133+
});
114134

115-
return {
116-
neededToApply: true,
117-
userAllowedToApplyAndWeApplied: true, //
118-
};
135+
return;
136+
} else {
137+
/**
138+
* not mandatory, thus do nothing.
139+
*/
140+
}
141+
}
119142
}
120143

121144
const askIfCanApply = async (config: Git.Config, askQuestion: AskQuestion = question): Promise<boolean> => {
@@ -159,6 +182,11 @@ export const markThatNeedsToApply = (
159182
)
160183
)[0];
161184

185+
export const isMarkedThatNeedsToApply = (pathToStackedRebaseDirInsideDotGit: string): boolean => {
186+
const pathToMark: string = getPaths(pathToStackedRebaseDirInsideDotGit).needsToApplyPath;
187+
return fs.existsSync(pathToMark);
188+
};
189+
162190
export const markThatApplied = (pathToStackedRebaseDirInsideDotGit: string): void =>
163191
[getPaths(pathToStackedRebaseDirInsideDotGit)].map(
164192
({ rewrittenListPath, needsToApplyPath, gitRebaseTodoPath }) => (

git-stacked-rebase.ts

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export async function gitStackedRebase(
122122
* so that the partial branches do not get out of sync.
123123
*/
124124
await applyIfNeedsToApply({
125+
isMandatoryIfMarkedAsNeeded: false,
125126
repo,
126127
pathToStackedRebaseTodoFile,
127128
pathToStackedRebaseDirInsideDotGit, //
@@ -137,7 +138,17 @@ export async function gitStackedRebase(
137138
return;
138139
}
139140

140-
const { neededToApply, userAllowedToApplyAndWeApplied } = await applyIfNeedsToApply({
141+
await applyIfNeedsToApply({
142+
/**
143+
* at this point, if an `--apply` has been marked as needed, we must perform it.
144+
*
145+
* we either a) already know that the user allows it, via options.autoApplyIfNeeded,
146+
* or b) if not -- we must ask the user directly if the apply can be performed.
147+
*
148+
* if user does not allow us to perform the apply -- we cannot continue, and will terminate.
149+
*/
150+
isMandatoryIfMarkedAsNeeded: true,
151+
141152
repo,
142153
pathToStackedRebaseTodoFile,
143154
pathToStackedRebaseDirInsideDotGit, //
@@ -150,10 +161,6 @@ export async function gitStackedRebase(
150161
askQuestion,
151162
});
152163

153-
if (neededToApply && !userAllowedToApplyAndWeApplied) {
154-
return;
155-
}
156-
157164
if (options.push) {
158165
if (!options.forcePush) {
159166
throw new Termination("\npush without --force will fail (since git rebase overrides history).\n\n");
@@ -650,66 +657,59 @@ mv -f "${preparedRegularRebaseTodoFile}" "${pathToRegularRebaseTodoFile}"
650657
}
651658

652659
/**
653-
* TODO might need to always enable,
654-
* but before more testing,
655-
* optional is good, since we ask anyway
656-
* before proceeding w/ other commands way above.
660+
* since we're invoking `git rebase --continue` directly (above),
661+
* we do not have the control over it.
662+
*
663+
* meaning that in case it's the very first rebase,
664+
* the `rewritten-list` in `.git/rebase-merge/`
665+
* (the actual git-rebase, not ours)
666+
* file is not generated yet,
667+
*
668+
* and since we depend on the `git rebase --continue` (the regular rebase)
669+
* to generate the `rewritten-list` file,
670+
* we explode trying to read the file if we try to --apply below.
671+
*
672+
* ---
673+
*
674+
* edit: oh wait nvm, it's potentially any rebase that has
675+
* `break` or `edit` or similar right??
676+
*
677+
* because if the git-rebase-todo file has `break` or `edit`
678+
* or similar commands that make `git rebase --continue` exit
679+
* before it's fully completed, (my theory now is that) our code here proceeds
680+
* and tries to --apply, but again the rewritten-list file
681+
* doesn't exist yet, so we blow up.
682+
*
683+
* ---
684+
*
685+
* let's try to account for only the 1st scenario first.
686+
* TODO implement directly in `--apply`
687+
* (e.g. if user calls `gitStackedRebase` again, while still in a rebase)
688+
*
689+
* upd: ok let's also do the 2nd one because it's useless otherwise
690+
*
657691
*/
658-
if (options.autoApplyIfNeeded) {
659-
/**
660-
* since we're invoking `git rebase --continue` directly (above),
661-
* we do not have the control over it.
662-
*
663-
* meaning that in case it's the very first rebase,
664-
* the `rewritten-list` in `.git/rebase-merge/`
665-
* (the actual git-rebase, not ours)
666-
* file is not generated yet,
667-
*
668-
* and since we depend on the `git rebase --continue` (the regular rebase)
669-
* to generate the `rewritten-list` file,
670-
* we explode trying to read the file if we try to --apply below.
671-
*
672-
* ---
673-
*
674-
* edit: oh wait nvm, it's potentially any rebase that has
675-
* `break` or `edit` or similar right??
676-
*
677-
* because if the git-rebase-todo file has `break` or `edit`
678-
* or similar commands that make `git rebase --continue` exit
679-
* before it's fully completed, (my theory now is that) our code here proceeds
680-
* and tries to --apply, but again the rewritten-list file
681-
* doesn't exist yet, so we blow up.
682-
*
683-
* ---
684-
*
685-
* let's try to account for only the 1st scenario first.
686-
* TODO implement directly in `--apply`
687-
* (e.g. if user calls `gitStackedRebase` again, while still in a rebase)
688-
*
689-
* upd: ok let's also do the 2nd one because it's useless otherwise
690-
*
691-
*/
692-
const canAndShouldBeApplying: boolean =
693-
/** part 1 */ fs.existsSync(path.join(pathToStackedRebaseDirInsideDotGit, filenames.rewrittenList)) &&
694-
/** part 2 (incomplete?) */ !fs.existsSync(pathToRegularRebaseDirInsideDotGit) &&
695-
/** part 2 (complete?) (is this even needed?) */ goodCommands.every(
696-
(cmd) => !namesOfRebaseCommandsThatMakeRebaseExitToPause.includes(cmd.commandName)
697-
);
692+
const canAndShouldBeApplying: boolean =
693+
/** part 1 */ fs.existsSync(path.join(pathToStackedRebaseDirInsideDotGit, filenames.rewrittenList)) &&
694+
/** part 2 (incomplete?) */ !fs.existsSync(pathToRegularRebaseDirInsideDotGit) &&
695+
/** part 2 (complete?) (is this even needed?) */ goodCommands.every(
696+
(cmd) => !namesOfRebaseCommandsThatMakeRebaseExitToPause.includes(cmd.commandName)
697+
);
698698

699-
if (canAndShouldBeApplying) {
700-
await applyIfNeedsToApply({
701-
repo,
702-
pathToStackedRebaseTodoFile,
703-
pathToStackedRebaseDirInsideDotGit, //
704-
rootLevelCommandName: "--apply",
705-
gitCmd: options.gitCmd,
706-
autoApplyIfNeeded: options.autoApplyIfNeeded,
707-
config,
708-
initialBranch,
709-
currentBranch,
710-
askQuestion,
711-
});
712-
}
699+
if (canAndShouldBeApplying) {
700+
await applyIfNeedsToApply({
701+
isMandatoryIfMarkedAsNeeded: false,
702+
repo,
703+
pathToStackedRebaseTodoFile,
704+
pathToStackedRebaseDirInsideDotGit, //
705+
rootLevelCommandName: "--apply",
706+
gitCmd: options.gitCmd,
707+
autoApplyIfNeeded: options.autoApplyIfNeeded,
708+
config,
709+
initialBranch,
710+
currentBranch,
711+
askQuestion,
712+
});
713713
}
714714

715715
/**

internal.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { AskQuestion } from "./util/createQuestion";
77
export const editor__internal = Symbol("editor__internal");
88
export const getGitConfig__internal = Symbol("getGitConfig__internal");
99

10+
export const noEditor = {
11+
[editor__internal]: () => void 0,
12+
};
13+
1014
export const askQuestion__internal = Symbol("askQuestion__internal");
1115

1216
/**

test/apply.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import assert from "assert";
2+
import path from "path";
3+
4+
import Git from "nodegit";
5+
6+
import { configKeys } from "../config";
7+
import { gitStackedRebase } from "../git-stacked-rebase";
8+
import { humanOpChangeCommandOfNthCommitInto } from "../humanOp";
9+
import { askQuestion__internal, editor__internal, noEditor } from "../internal";
10+
11+
import { setupRepo } from "./util/setupRepo";
12+
import { question, Questions } from "../util/createQuestion";
13+
import { isMarkedThatNeedsToApply } from "../apply";
14+
15+
export async function applyTC() {
16+
await integration__git_stacked_rebase_exits_if_apply_was_needed_but_user_disallowed();
17+
}
18+
19+
/**
20+
* create a scenario where an apply is needed, and disallow it - GSR should exit.
21+
*/
22+
async function integration__git_stacked_rebase_exits_if_apply_was_needed_but_user_disallowed() {
23+
const { initialBranch, common, commitsInLatest, config, repo } = await setupRepo();
24+
25+
/**
26+
* ensure autoApplyIfNeeded is disabled
27+
*/
28+
config.setBool(configKeys.autoApplyIfNeeded, Git.Config.MAP.FALSE);
29+
30+
/**
31+
* force modify history, so that an apply will be needed
32+
*/
33+
await gitStackedRebase(initialBranch, {
34+
...common,
35+
[editor__internal]: ({ filePath }) => {
36+
humanOpChangeCommandOfNthCommitInto("drop", {
37+
filePath, //
38+
commitSHA: commitsInLatest[2],
39+
});
40+
},
41+
});
42+
43+
// TODO take from `gitStackedRebase`:
44+
const dotGitDirPath: string = repo.path();
45+
const pathToStackedRebaseDirInsideDotGit: string = path.join(dotGitDirPath, "stacked-rebase");
46+
assert.deepStrictEqual(
47+
isMarkedThatNeedsToApply(pathToStackedRebaseDirInsideDotGit), //
48+
true,
49+
`expected a "needs-to-apply" mark to be set.`
50+
);
51+
52+
console.log("performing 2nd rebase, expecting it to throw.");
53+
54+
const threw: boolean = await didThrow(() =>
55+
/**
56+
* perform the rebase again - now that an apply is marked as needed,
57+
* and autoApplyIfNeeded is disabled,
58+
* we should get prompted to allow the apply.
59+
*/
60+
gitStackedRebase(initialBranch, {
61+
...common,
62+
...noEditor,
63+
[askQuestion__internal]: (q, ...rest) => {
64+
if (q === Questions.need_to_apply_before_continuing) {
65+
return "n";
66+
}
67+
68+
return question(q, ...rest);
69+
},
70+
})
71+
);
72+
73+
assert.deepStrictEqual(
74+
threw,
75+
true,
76+
`expected 2nd invocation of rebase to throw, because user did not allow to perform a mandatory --apply.\nbut threw = ${threw} (expected true).`
77+
);
78+
}
79+
80+
export async function didThrow(fn: Function): Promise<boolean> {
81+
try {
82+
await fn();
83+
return false;
84+
} catch (_e) {
85+
return true;
86+
}
87+
}

0 commit comments

Comments
 (0)