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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ Default:
Configure experimental features by passing a JSON object.
The following properties can be specified:

#### `conflict_resolution`

Default: `fail`

Specifies how the action will handle a conflict occuring during the cherry-pick.
In all cases, the action will stop the cherry-pick at the first conflict encountered.

Behavior is defined by the option selected.
- When set to `fail` the backport fails when the cherry-pick encounters a conflict.
- When set to `draft_commit_conflicts` the backport will always create a draft pull request with the first conflict encountered committed.

Instructions are provided on the original pull request on how to resolve the conflict and continue the cherry-pick.

#### `detect_merge_method`

Default: `false`
Expand Down
18 changes: 15 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ inputs:
Configure experimental features by passing a JSON object.
The following properties can be specified:

#### `conflict_resolution`

Specifies how the action will handle a conflict occuring during the cherry-pick.
In all cases, the action will stop the cherry-pick at the first conflict encountered.

Behavior is defined by the option selected.
- When set to `fail` the backport fails when the cherry-pick encounters a conflict.
- When set to `draft_commit_conflicts` the backport will always create a draft pull request with the first conflict encountered committed.

Instructions are provided on the original pull request on how to resolve the conflict and continue the cherry-pick.

#### `detect_merge_method`

When enabled, the action detects the method used to merge the pull request.
Expand All @@ -59,7 +70,8 @@ inputs:
By default, uses the owner of the repository in which the workflow runs.
default: >
{
"detect_merge_method": false
"detect_merge_method": false,
"conflict_resolution": "fail"
}
github_token:
description: >
Expand Down Expand Up @@ -125,5 +137,5 @@ runs:
using: "node20"
main: "dist/index.js"
branding:
icon: 'copy'
color: 'yellow'
icon: "copy"
color: "yellow"
128 changes: 111 additions & 17 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,
conflict_resolution: `fail`,
downstream_repo: undefined,
downstream_owner: undefined,
};
Expand Down Expand Up @@ -227,8 +228,9 @@ class Backport {
});
continue;
}
let uncommitedShas;
try {
yield this.git.cherryPick(commitShasToCherryPick, this.config.pwd);
uncommitedShas = yield this.git.cherryPick(commitShasToCherryPick, this.config.experimental.conflict_resolution, this.config.pwd);
}
catch (error) {
const message = this.composeMessageForCherryPickFailure(target, branchname, commitShasToCherryPick);
Expand Down Expand Up @@ -266,6 +268,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 @@ -329,15 +332,30 @@ class Backport {
// The PR was still created so let's still comment on the original.
}
}
const message = this.composeMessageForSuccess(new_pr.number, target, this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "");
successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
yield this.github.createComment({
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
// post success message to original pr
{
const message = uncommitedShas !== null
? this.composeMessageForSuccessWithConflicts(new_pr.number, target, this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "", branchname, uncommitedShas, this.config.experimental.conflict_resolution)
: this.composeMessageForSuccess(new_pr.number, target, this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "");
successByTarget.set(target, true);
createdPullRequestNumbers.push(new_pr.number);
yield this.github.createComment({
owner: workflowOwner,
repo: workflowRepo,
issue_number: pull_number,
body: message,
});
}
// post message to new pr to resolve conflict
if (uncommitedShas !== null) {
const message = this.composeMessageToResolveCommittedConflicts(target, branchname, uncommitedShas, this.config.experimental.conflict_resolution);
yield this.github.createComment({
owner,
repo,
issue_number: new_pr.number,
body: message,
});
}
}
catch (error) {
if (error instanceof Error) {
Expand Down Expand Up @@ -384,28 +402,51 @@ 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 suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, false, "fail");
return (0, dedent_1.default) `Backport failed for \`${target}\`, ${reason}.

Please cherry-pick the changes locally and resolve any conflicts.
${suggestion}`;
}
composeSuggestion(target, branchname, commitShasToCherryPick) {
return (0, dedent_1.default) `\`\`\`bash
composeMessageToResolveCommittedConflicts(target, branchname, commitShasToCherryPick, confictResolution) {
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, true, confictResolution);
return (0, dedent_1.default) `Please cherry-pick the changes locally and resolve any conflicts.
${suggestion}`;
}
composeSuggestion(target, branchname, commitShasToCherryPick, branchExist, confictResolution = "fail") {
if (branchExist) {
if (confictResolution === "draft_commit_conflicts") {
return (0, dedent_1.default) `\`\`\`bash
git fetch origin ${target}
git worktree add -d .worktree/${branchname} origin/${target}
cd .worktree/${branchname}
git switch ${branchname}
git reset --hard HEAD^
git cherry-pick -x ${commitShasToCherryPick.join(" ")}
git push --force-with-lease
\`\`\``;
}
else {
return "";
}
}
else {
return (0, dedent_1.default) `\`\`\`bash
git fetch origin ${target}
git worktree add -d .worktree/${branchname} origin/${target}
cd .worktree/${branchname}
git switch --create ${branchname}
git cherry-pick -x ${commitShasToCherryPick.join(" ")}
\`\`\``;
}
}
composeMessageForGitPushFailure(target, exitcode) {
//TODO better error messages depending on exit code
Expand All @@ -421,6 +462,13 @@ class Backport {
return (0, dedent_1.default) `Successfully created backport PR for \`${target}\`:
- ${downstream}#${pr_number}`;
}
composeMessageForSuccessWithConflicts(pr_number, target, downstream, branchname, commitShasToCherryPick, conflictResolution) {
const suggestionToResolve = this.composeMessageToResolveCommittedConflicts(target, branchname, commitShasToCherryPick, conflictResolution);
return (0, dedent_1.default) `Created backport PR for \`${target}\`:
- ${downstream}#${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 @@ -593,12 +641,49 @@ class Git {
}
});
}
cherryPick(commitShas, pwd) {
cherryPick(commitShas, conflictResolution, 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 (conflictResolution === `fail`) {
const { exitCode } = yield this.git("cherry-pick", ["-x", ...commitShas], pwd);
if (exitCode !== 0) {
yield abortCherryPickAndThrow(commitShas, exitCode);
}
return null;
}
else {
let uncommittedShas = [...commitShas];
// Cherry-pick commit one by one.
while (uncommittedShas.length > 0) {
const { exitCode } = yield this.git("cherry-pick", ["-x", uncommittedShas[0]], pwd);
if (exitCode !== 0) {
if (exitCode === 1) {
// conflict encountered
if (conflictResolution === `draft_commit_conflicts`) {
// Commit the conflict, resolution of this commit is left to the user.
// Allow creating PR for cherry-pick with only 1 commit and it results in a conflict.
const { exitCode } = yield this.git("commit", ["--all", `-m BACKPORT-CONFLICT`], pwd);
if (exitCode !== 0) {
yield abortCherryPickAndThrow(commitShas, exitCode);
}
return uncommittedShas;
}
else {
throw new Error(`'Unsupported conflict_resolution method ${conflictResolution}`);
}
}
else {
// other fail reasons
yield abortCherryPickAndThrow([uncommittedShas[0]], exitCode);
}
}
// pop sha
uncommittedShas.shift();
}
return null;
}
});
}
Expand Down Expand Up @@ -1003,6 +1088,15 @@ function run() {
No experimental config options known for key '${key}'.\
Please check the documentation for details about experimental features.`);
}
if (key == "conflict_resolution") {
if (experimental[key] !== "fail" &&
experimental[key] !== "draft_commit_conflicts") {
const message = `Expected input 'conflict_resolution' to be either 'fail' or 'draft_commit_conflicts', but was '${experimental[key]}'`;
console.error(message);
core.setFailed(message);
return;
}
}
}
const github = new github_1.Github(token);
const git = new git_1.Git(execa_1.execa);
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

Loading
Loading