Skip to content

Commit

Permalink
Merge pull request #417 from vermz99/main
Browse files Browse the repository at this point in the history
Add `conflict_resolution` input
  • Loading branch information
korthout authored May 26, 2024
2 parents 2c845b5 + 0024721 commit 8e95300
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 50 deletions.
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

0 comments on commit 8e95300

Please sign in to comment.