Skip to content

Commit 8e95300

Browse files
authored
Merge pull request #417 from vermz99/main
Add `conflict_resolution` input
2 parents 2c845b5 + 0024721 commit 8e95300

File tree

9 files changed

+408
-50
lines changed

9 files changed

+408
-50
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,19 @@ Default:
158158
Configure experimental features by passing a JSON object.
159159
The following properties can be specified:
160160

161+
#### `conflict_resolution`
162+
163+
Default: `fail`
164+
165+
Specifies how the action will handle a conflict occuring during the cherry-pick.
166+
In all cases, the action will stop the cherry-pick at the first conflict encountered.
167+
168+
Behavior is defined by the option selected.
169+
- When set to `fail` the backport fails when the cherry-pick encounters a conflict.
170+
- When set to `draft_commit_conflicts` the backport will always create a draft pull request with the first conflict encountered committed.
171+
172+
Instructions are provided on the original pull request on how to resolve the conflict and continue the cherry-pick.
173+
161174
#### `detect_merge_method`
162175

163176
Default: `false`

action.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ inputs:
3636
Configure experimental features by passing a JSON object.
3737
The following properties can be specified:
3838
39+
#### `conflict_resolution`
40+
41+
Specifies how the action will handle a conflict occuring during the cherry-pick.
42+
In all cases, the action will stop the cherry-pick at the first conflict encountered.
43+
44+
Behavior is defined by the option selected.
45+
- When set to `fail` the backport fails when the cherry-pick encounters a conflict.
46+
- When set to `draft_commit_conflicts` the backport will always create a draft pull request with the first conflict encountered committed.
47+
48+
Instructions are provided on the original pull request on how to resolve the conflict and continue the cherry-pick.
49+
3950
#### `detect_merge_method`
4051
4152
When enabled, the action detects the method used to merge the pull request.
@@ -59,7 +70,8 @@ inputs:
5970
By default, uses the owner of the repository in which the workflow runs.
6071
default: >
6172
{
62-
"detect_merge_method": false
73+
"detect_merge_method": false,
74+
"conflict_resolution": "fail"
6375
}
6476
github_token:
6577
description: >
@@ -125,5 +137,5 @@ runs:
125137
using: "node20"
126138
main: "dist/index.js"
127139
branding:
128-
icon: 'copy'
129-
color: 'yellow'
140+
icon: "copy"
141+
color: "yellow"

dist/index.js

Lines changed: 111 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const git_1 = __nccwpck_require__(3374);
5050
const utils = __importStar(__nccwpck_require__(918));
5151
const experimentalDefaults = {
5252
detect_merge_method: false,
53+
conflict_resolution: `fail`,
5354
downstream_repo: undefined,
5455
downstream_owner: undefined,
5556
};
@@ -227,8 +228,9 @@ class Backport {
227228
});
228229
continue;
229230
}
231+
let uncommitedShas;
230232
try {
231-
yield this.git.cherryPick(commitShasToCherryPick, this.config.pwd);
233+
uncommitedShas = yield this.git.cherryPick(commitShasToCherryPick, this.config.experimental.conflict_resolution, this.config.pwd);
232234
}
233235
catch (error) {
234236
const message = this.composeMessageForCherryPickFailure(target, branchname, commitShasToCherryPick);
@@ -266,6 +268,7 @@ class Backport {
266268
head: branchname,
267269
base: target,
268270
maintainer_can_modify: true,
271+
draft: uncommitedShas !== null,
269272
});
270273
if (new_pr_response.status != 201) {
271274
console.error(JSON.stringify(new_pr_response));
@@ -329,15 +332,30 @@ class Backport {
329332
// The PR was still created so let's still comment on the original.
330333
}
331334
}
332-
const message = this.composeMessageForSuccess(new_pr.number, target, this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "");
333-
successByTarget.set(target, true);
334-
createdPullRequestNumbers.push(new_pr.number);
335-
yield this.github.createComment({
336-
owner: workflowOwner,
337-
repo: workflowRepo,
338-
issue_number: pull_number,
339-
body: message,
340-
});
335+
// post success message to original pr
336+
{
337+
const message = uncommitedShas !== null
338+
? this.composeMessageForSuccessWithConflicts(new_pr.number, target, this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "", branchname, uncommitedShas, this.config.experimental.conflict_resolution)
339+
: this.composeMessageForSuccess(new_pr.number, target, this.shouldUseDownstreamRepo() ? `${owner}/${repo}` : "");
340+
successByTarget.set(target, true);
341+
createdPullRequestNumbers.push(new_pr.number);
342+
yield this.github.createComment({
343+
owner: workflowOwner,
344+
repo: workflowRepo,
345+
issue_number: pull_number,
346+
body: message,
347+
});
348+
}
349+
// post message to new pr to resolve conflict
350+
if (uncommitedShas !== null) {
351+
const message = this.composeMessageToResolveCommittedConflicts(target, branchname, uncommitedShas, this.config.experimental.conflict_resolution);
352+
yield this.github.createComment({
353+
owner,
354+
repo,
355+
issue_number: new_pr.number,
356+
body: message,
357+
});
358+
}
341359
}
342360
catch (error) {
343361
if (error instanceof Error) {
@@ -384,28 +402,51 @@ class Backport {
384402
}
385403
composeMessageForCheckoutFailure(target, branchname, commitShasToCherryPick) {
386404
const reason = "because it was unable to create a new branch";
387-
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick);
405+
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, false);
388406
return (0, dedent_1.default) `Backport failed for \`${target}\`, ${reason}.
389407

390408
Please cherry-pick the changes locally.
391409
${suggestion}`;
392410
}
393411
composeMessageForCherryPickFailure(target, branchname, commitShasToCherryPick) {
394412
const reason = "because it was unable to cherry-pick the commit(s)";
395-
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick);
413+
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, false, "fail");
396414
return (0, dedent_1.default) `Backport failed for \`${target}\`, ${reason}.
397415

398416
Please cherry-pick the changes locally and resolve any conflicts.
399417
${suggestion}`;
400418
}
401-
composeSuggestion(target, branchname, commitShasToCherryPick) {
402-
return (0, dedent_1.default) `\`\`\`bash
419+
composeMessageToResolveCommittedConflicts(target, branchname, commitShasToCherryPick, confictResolution) {
420+
const suggestion = this.composeSuggestion(target, branchname, commitShasToCherryPick, true, confictResolution);
421+
return (0, dedent_1.default) `Please cherry-pick the changes locally and resolve any conflicts.
422+
${suggestion}`;
423+
}
424+
composeSuggestion(target, branchname, commitShasToCherryPick, branchExist, confictResolution = "fail") {
425+
if (branchExist) {
426+
if (confictResolution === "draft_commit_conflicts") {
427+
return (0, dedent_1.default) `\`\`\`bash
428+
git fetch origin ${target}
429+
git worktree add -d .worktree/${branchname} origin/${target}
430+
cd .worktree/${branchname}
431+
git switch ${branchname}
432+
git reset --hard HEAD^
433+
git cherry-pick -x ${commitShasToCherryPick.join(" ")}
434+
git push --force-with-lease
435+
\`\`\``;
436+
}
437+
else {
438+
return "";
439+
}
440+
}
441+
else {
442+
return (0, dedent_1.default) `\`\`\`bash
403443
git fetch origin ${target}
404444
git worktree add -d .worktree/${branchname} origin/${target}
405445
cd .worktree/${branchname}
406446
git switch --create ${branchname}
407447
git cherry-pick -x ${commitShasToCherryPick.join(" ")}
408448
\`\`\``;
449+
}
409450
}
410451
composeMessageForGitPushFailure(target, exitcode) {
411452
//TODO better error messages depending on exit code
@@ -421,6 +462,13 @@ class Backport {
421462
return (0, dedent_1.default) `Successfully created backport PR for \`${target}\`:
422463
- ${downstream}#${pr_number}`;
423464
}
465+
composeMessageForSuccessWithConflicts(pr_number, target, downstream, branchname, commitShasToCherryPick, conflictResolution) {
466+
const suggestionToResolve = this.composeMessageToResolveCommittedConflicts(target, branchname, commitShasToCherryPick, conflictResolution);
467+
return (0, dedent_1.default) `Created backport PR for \`${target}\`:
468+
- ${downstream}#${pr_number} with remaining conflicts!
469+
470+
${suggestionToResolve}`;
471+
}
424472
createOutput(successByTarget, createdPullRequestNumbers) {
425473
const anyTargetFailed = Array.from(successByTarget.values()).includes(false);
426474
core.setOutput(Output.wasSuccessful, !anyTargetFailed);
@@ -593,12 +641,49 @@ class Git {
593641
}
594642
});
595643
}
596-
cherryPick(commitShas, pwd) {
644+
cherryPick(commitShas, conflictResolution, pwd) {
597645
return __awaiter(this, void 0, void 0, function* () {
598-
const { exitCode } = yield this.git("cherry-pick", ["-x", ...commitShas], pwd);
599-
if (exitCode !== 0) {
646+
const abortCherryPickAndThrow = (commitShas, exitCode) => __awaiter(this, void 0, void 0, function* () {
600647
yield this.git("cherry-pick", ["--abort"], pwd);
601648
throw new Error(`'git cherry-pick -x ${commitShas}' failed with exit code ${exitCode}`);
649+
});
650+
if (conflictResolution === `fail`) {
651+
const { exitCode } = yield this.git("cherry-pick", ["-x", ...commitShas], pwd);
652+
if (exitCode !== 0) {
653+
yield abortCherryPickAndThrow(commitShas, exitCode);
654+
}
655+
return null;
656+
}
657+
else {
658+
let uncommittedShas = [...commitShas];
659+
// Cherry-pick commit one by one.
660+
while (uncommittedShas.length > 0) {
661+
const { exitCode } = yield this.git("cherry-pick", ["-x", uncommittedShas[0]], pwd);
662+
if (exitCode !== 0) {
663+
if (exitCode === 1) {
664+
// conflict encountered
665+
if (conflictResolution === `draft_commit_conflicts`) {
666+
// Commit the conflict, resolution of this commit is left to the user.
667+
// Allow creating PR for cherry-pick with only 1 commit and it results in a conflict.
668+
const { exitCode } = yield this.git("commit", ["--all", `-m BACKPORT-CONFLICT`], pwd);
669+
if (exitCode !== 0) {
670+
yield abortCherryPickAndThrow(commitShas, exitCode);
671+
}
672+
return uncommittedShas;
673+
}
674+
else {
675+
throw new Error(`'Unsupported conflict_resolution method ${conflictResolution}`);
676+
}
677+
}
678+
else {
679+
// other fail reasons
680+
yield abortCherryPickAndThrow([uncommittedShas[0]], exitCode);
681+
}
682+
}
683+
// pop sha
684+
uncommittedShas.shift();
685+
}
686+
return null;
602687
}
603688
});
604689
}
@@ -1003,6 +1088,15 @@ function run() {
10031088
No experimental config options known for key '${key}'.\
10041089
Please check the documentation for details about experimental features.`);
10051090
}
1091+
if (key == "conflict_resolution") {
1092+
if (experimental[key] !== "fail" &&
1093+
experimental[key] !== "draft_commit_conflicts") {
1094+
const message = `Expected input 'conflict_resolution' to be either 'fail' or 'draft_commit_conflicts', but was '${experimental[key]}'`;
1095+
console.error(message);
1096+
core.setFailed(message);
1097+
return;
1098+
}
1099+
}
10061100
}
10071101
const github = new github_1.Github(token);
10081102
const git = new git_1.Git(execa_1.execa);

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)