Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conflict_resolution input #417

Merged
merged 9 commits into from
May 26, 2024
15 changes: 12 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,18 @@ inputs:
- For "Merged as a merge commit", the action cherry-picks the commits from the pull request.

By default, the action always cherry-picks the commits from the pull request.

#### `backport_on_conflicts`

When enabled, the action will create the pull request even if conflict are detected during the cherry-pick.
The first conflict encountered will be committed and the remaining of the cherry-pick will need to be done by the users.
Instructions are provided on the original pull request on how to resolve the conflict and continue the cherry-pick.

By default, the action fails on the first conflict encountered and the pull request is not created.
default: >
{
"detect_merge_method": false
"detect_merge_method": false,
"backport_on_conflicts": false
}
github_token:
description: >
Expand Down Expand Up @@ -112,5 +121,5 @@ runs:
using: "node20"
main: "dist/index.js"
branding:
icon: 'copy'
color: 'yellow'
icon: "copy"
color: "yellow"
101 changes: 83 additions & 18 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const git_1 = __nccwpck_require__(3374);
const utils = __importStar(__nccwpck_require__(918));
const experimentalDefaults = {
detect_merge_method: false,
backport_on_conflicts: false,
};
exports.experimentalDefaults = experimentalDefaults;
var Output;
Expand Down Expand Up @@ -204,8 +205,9 @@ class Backport {
});
continue;
}
let uncommitedShas;
try {
yield this.git.cherryPick(commitShasToCherryPick, this.config.pwd);
uncommitedShas = yield this.git.cherryPick(commitShasToCherryPick, this.config.experimental.backport_on_conflicts, this.config.pwd);
}
catch (error) {
const message = this.composeMessageForCherryPickFailure(target, branchname, commitShasToCherryPick);
Expand Down Expand Up @@ -243,6 +245,7 @@ class Backport {
head: branchname,
base: target,
maintainer_can_modify: true,
draft: uncommitedShas !== null,
});
if (new_pr_response.status != 201) {
console.error(JSON.stringify(new_pr_response));
Expand Down Expand Up @@ -295,15 +298,30 @@ class Backport {
// The PR was still created so let's still comment on the original.
}
}
const message = this.composeMessageForSuccess(new_pr.number, target);
successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
yield this.github.createComment({
owner,
repo,
issue_number: pull_number,
body: message,
});
// post success message to original pr
{
const message = uncommitedShas !== null
? this.composeMessageForSuccessWithConflicts(new_pr.number, target, branchname, uncommitedShas)
: this.composeMessageForSuccess(new_pr.number, target);
successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
yield this.github.createComment({
owner,
repo,
issue_number: pull_number,
body: message,
});
}
// post message to new pr to resolve conflict
if (uncommitedShas !== null) {
const message = this.composeMessageToResolveConflicts(target, branchname, uncommitedShas, true);
yield this.github.createComment({
owner,
repo,
issue_number: new_pr.number,
body: message,
});
}
}
catch (error) {
if (error instanceof Error) {
Expand Down Expand Up @@ -350,22 +368,34 @@ class Backport {
}
composeMessageForCheckoutFailure(target, branchname, commitShasToCherryPick) {
const reason = "because it was unable to create a new branch";
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick);
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, false);
return (0, dedent_1.default) `Backport failed for \`${target}\`, ${reason}.

Please cherry-pick the changes locally.
${suggestion}`;
}
composeMessageForCherryPickFailure(target, branchname, commitShasToCherryPick) {
const reason = "because it was unable to cherry-pick the commit(s)";
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick);
const suggestionToResolve = this.composeMessageToResolveConflicts(target, branchname, commitShasToCherryPick, false);
return (0, dedent_1.default) `Backport failed for \`${target}\`, ${reason}.

Please cherry-pick the changes locally and resolve any conflicts.
${suggestionToResolve}`;
}
composeMessageToResolveConflicts(target, branchname, commitShasToCherryPick, branchExist) {
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, branchExist);
return (0, dedent_1.default) `Please cherry-pick the changes locally and resolve any conflicts.
${suggestion}`;
}
composeSuggestion(target, branchname, commitShasToCherryPick) {
return (0, dedent_1.default) `\`\`\`bash
composeSuggestion(target, branchname, commitShasToCherryPick, branchExist) {
return branchExist
? (0, dedent_1.default) `\`\`\`bash
git fetch origin ${target}
git worktree add -d .worktree/${branchname} origin/${target}
cd .worktree/${branchname}
git switch ${branchname}
git cherry-pick -x ${commitShasToCherryPick.join(" ")}
\`\`\``
: (0, dedent_1.default) `\`\`\`bash
git fetch origin ${target}
git worktree add -d .worktree/${branchname} origin/${target}
cd .worktree/${branchname}
Expand All @@ -387,6 +417,13 @@ class Backport {
return (0, dedent_1.default) `Successfully created backport PR for \`${target}\`:
- #${pr_number}`;
}
composeMessageForSuccessWithConflicts(pr_number, target, branchname, commitShasToCherryPick) {
const suggestionToResolve = this.composeMessageToResolveConflicts(target, branchname, commitShasToCherryPick, true);
return (0, dedent_1.default) `Created backport PR for \`${target}\`:
- #${pr_number} with remaining conflicts!

${suggestionToResolve}`;
}
createOutput(successByTarget, createdPullRequestNumbers) {
const anyTargetFailed = Array.from(successByTarget.values()).includes(false);
core.setOutput(Output.wasSuccessful, !anyTargetFailed);
Expand Down Expand Up @@ -542,12 +579,40 @@ class Git {
}
});
}
cherryPick(commitShas, pwd) {
cherryPick(commitShas, allowPartialCherryPick, pwd) {
return __awaiter(this, void 0, void 0, function* () {
const { exitCode } = yield this.git("cherry-pick", ["-x", ...commitShas], pwd);
if (exitCode !== 0) {
const abortCherryPickAndThrow = (commitShas, exitCode) => __awaiter(this, void 0, void 0, function* () {
yield this.git("cherry-pick", ["--abort"], pwd);
throw new Error(`'git cherry-pick -x ${commitShas}' failed with exit code ${exitCode}`);
});
if (!allowPartialCherryPick) {
const { exitCode } = yield this.git("cherry-pick", ["-x", ...commitShas], pwd);
if (exitCode !== 0) {
yield abortCherryPickAndThrow(commitShas, exitCode);
}
return null;
}
else {
let uncommitedShas = [...commitShas];
// Cherry-pick commit one by one.
for (const sha of commitShas) {
const { exitCode } = yield this.git("cherry-pick", ["-x", sha], pwd);
if (exitCode !== 0) {
if (exitCode === 1) {
// conflict encountered
// abort conflict cherry-pick
yield this.git("cherry-pick", ["--abort"], pwd);
return uncommitedShas;
}
else {
// other fail reasons
yield abortCherryPickAndThrow([sha], exitCode);
}
}
// pop sha
uncommitedShas.shift();
}
return null;
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

108 changes: 95 additions & 13 deletions src/backport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ export type Config = {

type Experimental = {
detect_merge_method: boolean;
backport_on_conflicts: boolean;
};
const experimentalDefaults: Experimental = {
detect_merge_method: false,
backport_on_conflicts: false,
};
export { experimentalDefaults };

Expand Down Expand Up @@ -265,8 +267,14 @@ export class Backport {
continue;
}

let uncommitedShas: string[] | null;

try {
await this.git.cherryPick(commitShasToCherryPick, this.config.pwd);
uncommitedShas = await this.git.cherryPick(
commitShasToCherryPick,
this.config.experimental.backport_on_conflicts,
this.config.pwd,
);
} catch (error) {
const message = this.composeMessageForCherryPickFailure(
target,
Expand Down Expand Up @@ -312,6 +320,7 @@ export class Backport {
head: branchname,
base: target,
maintainer_can_modify: true,
draft: uncommitedShas !== null,
});

if (new_pr_response.status != 201) {
Expand Down Expand Up @@ -387,15 +396,43 @@ export class Backport {
}
}

const message = this.composeMessageForSuccess(new_pr.number, target);
successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
await this.github.createComment({
owner,
repo,
issue_number: pull_number,
body: message,
});
// post success message to original pr
{
const message =
uncommitedShas !== null
? this.composeMessageForSuccessWithConflicts(
new_pr.number,
target,
branchname,
uncommitedShas,
)
: this.composeMessageForSuccess(new_pr.number, target);

successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
await this.github.createComment({
owner,
repo,
issue_number: pull_number,
body: message,
});
}
// post message to new pr to resolve conflict
if (uncommitedShas !== null) {
const message: string = this.composeMessageToResolveConflicts(
target,
branchname,
uncommitedShas,
true,
);

await this.github.createComment({
owner,
repo,
issue_number: new_pr.number,
body: message,
});
}
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
Expand Down Expand Up @@ -460,6 +497,7 @@ export class Backport {
target,
branchname,
commitShasToCherryPick,
false,
);
return dedent`Backport failed for \`${target}\`, ${reason}.

Expand All @@ -473,23 +511,49 @@ export class Backport {
commitShasToCherryPick: string[],
): string {
const reason = "because it was unable to cherry-pick the commit(s)";
const suggestion = this.composeSuggestion(
const suggestionToResolve = this.composeMessageToResolveConflicts(
target,
branchname,
commitShasToCherryPick,
false,
);
return dedent`Backport failed for \`${target}\`, ${reason}.

Please cherry-pick the changes locally and resolve any conflicts.
${suggestionToResolve}`;
}

private composeMessageToResolveConflicts(
target: string,
branchname: string,
commitShasToCherryPick: string[],
branchExist: boolean,
): string {
const suggestion = this.composeSuggestion(
target,
branchname,
commitShasToCherryPick,
branchExist,
);

return dedent`Please cherry-pick the changes locally and resolve any conflicts.
${suggestion}`;
}

private composeSuggestion(
target: string,
branchname: string,
commitShasToCherryPick: string[],
branchExist: boolean,
) {
return dedent`\`\`\`bash
return branchExist
? dedent`\`\`\`bash
git fetch origin ${target}
git worktree add -d .worktree/${branchname} origin/${target}
cd .worktree/${branchname}
git switch ${branchname}
git cherry-pick -x ${commitShasToCherryPick.join(" ")}
\`\`\``
: dedent`\`\`\`bash
vermz99 marked this conversation as resolved.
Show resolved Hide resolved
git fetch origin ${target}
git worktree add -d .worktree/${branchname} origin/${target}
cd .worktree/${branchname}
Expand Down Expand Up @@ -520,6 +584,24 @@ export class Backport {
- #${pr_number}`;
}

private composeMessageForSuccessWithConflicts(
pr_number: number,
target: string,
branchname: string,
commitShasToCherryPick: string[],
): string {
const suggestionToResolve = this.composeMessageToResolveConflicts(
target,
branchname,
commitShasToCherryPick,
true,
);
return dedent`Created backport PR for \`${target}\`:
- #${pr_number} with remaining conflicts!

${suggestionToResolve}`;
}

private createOutput(
successByTarget: Map<string, boolean>,
createdPullRequestNumbers: Array<number>,
Expand Down
Loading