Skip to content

Commit 617a709

Browse files
committed
Expose cherry_picking_merge_mode input
Adds a new action input that selects the merge strategy for cherry-picks. "default" preserves existing behavior; "whitespace_tolerant" passes -Xignore-space-at-eol to git, resolving conflicts caused by trailing- whitespace divergence between branches. Empty string is coerced to "default" (GitHub Actions passes explicit "" when the input is omitted). Invalid values fail-fast with an error message before any backport PR is created.
1 parent c3eb488 commit 617a709

8 files changed

Lines changed: 94 additions & 4 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,18 @@ Specifically, those reachable from the pull request's head and not reachable fro
304304

305305
By default, the action cherry-picks the commits based on the method used to merge the pull request.
306306

307+
### `cherry_picking_merge_mode`
308+
309+
Default: `default`
310+
311+
Sets the merge strategy option for cherry-picking. Accepted values:
312+
`default` (standard cherry-pick behavior) or `whitespace_tolerant`
313+
(ignores trailing whitespace when matching context lines, equivalent
314+
to `git cherry-pick -Xignore-space-at-eol`). Use `whitespace_tolerant`
315+
when the target branch has diverged from the source branch only in
316+
trailing whitespace, which would otherwise cause cherry-pick conflicts
317+
or silent line duplication.
318+
307319
### `comment_style`
308320

309321
Default: `legacy`

action.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ inputs:
6161
6262
By default, the action cherry-picks the commits based on the method used to merge the pull request.
6363
default: auto
64+
cherry_picking_merge_mode:
65+
default: default
66+
description: >
67+
Sets the merge strategy option for cherry-picking. Accepted values:
68+
`default` (standard cherry-pick behavior) or `whitespace_tolerant`
69+
(ignores trailing whitespace when matching context lines, equivalent
70+
to `git cherry-pick -Xignore-space-at-eol`). Use `whitespace_tolerant`
71+
when the target branch has diverged from the source branch only in
72+
trailing whitespace, which would otherwise cause cherry-pick conflicts
73+
or silent line duplication.
6474
comment_style:
6575
description: >
6676
Controls the style of comments posted by the action.

src/backport.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export type Config = {
7575
target_branches?: string;
7676
commits: {
7777
cherry_picking: "auto" | "pull_request_head";
78+
cherry_picking_merge_mode: "default" | "whitespace_tolerant";
7879
merge_commits: "fail" | "skip";
7980
};
8081
copy_milestone: boolean;
@@ -412,12 +413,18 @@ export class Backport {
412413

413414
let uncommittedShas: string[] | null;
414415

416+
if (
417+
this.config.commits.cherry_picking_merge_mode === "whitespace_tolerant"
418+
) {
419+
console.log("Cherry-picking with whitespace-tolerant merge.");
420+
}
421+
415422
try {
416423
uncommittedShas = await this.git.cherryPick(
417424
commitShasToCherryPick,
418425
this.config.experimental.conflict_resolution,
419426
this.config.pwd,
420-
"default",
427+
this.config.commits.cherry_picking_merge_mode,
421428
);
422429
} catch (error) {
423430
const message =

src/main.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "./backport.js";
88
import { Github } from "./github.js";
99
import { Git } from "./git.js";
10+
import { coerceCherryPickingMergeMode } from "./utils.js";
1011
import dedent from "dedent";
1112

1213
/**
@@ -27,6 +28,7 @@ async function run(): Promise<void> {
2728
const copy_labels_pattern = core.getInput("copy_labels_pattern");
2829
const target_branches = core.getInput("target_branches");
2930
const cherry_picking = core.getInput("cherry_picking");
31+
const cherry_picking_merge_mode = core.getInput("cherry_picking_merge_mode");
3032
const merge_commits = core.getInput("merge_commits");
3133
const copy_assignees = core.getInput("copy_assignees");
3234
const copy_milestone = core.getInput("copy_milestone");
@@ -49,6 +51,16 @@ async function run(): Promise<void> {
4951
return;
5052
}
5153

54+
const coercedCherryPickingMergeMode = coerceCherryPickingMergeMode(
55+
cherry_picking_merge_mode,
56+
);
57+
if (coercedCherryPickingMergeMode === "invalid") {
58+
const message = `Invalid value for \`cherry_picking_merge_mode\`: \`${cherry_picking_merge_mode}\`. Accepted values: \`default\`, \`whitespace_tolerant\`.`;
59+
console.error(message);
60+
core.setFailed(message);
61+
return;
62+
}
63+
5264
if (merge_commits != "fail" && merge_commits != "skip") {
5365
const message = `Expected input 'merge_commits' to be either 'fail' or 'skip', but was '${merge_commits}'`;
5466
console.error(message);
@@ -120,7 +132,11 @@ async function run(): Promise<void> {
120132
copy_labels_pattern === "" ? undefined : new RegExp(copy_labels_pattern),
121133
add_labels: add_labels === "" ? [] : add_labels.split(/[,]/),
122134
target_branches: target_branches === "" ? undefined : target_branches,
123-
commits: { cherry_picking, merge_commits },
135+
commits: {
136+
cherry_picking,
137+
cherry_picking_merge_mode: coercedCherryPickingMergeMode,
138+
merge_commits,
139+
},
124140
copy_assignees: copy_assignees === "true",
125141
copy_milestone: copy_milestone === "true",
126142
copy_all_reviewers: copy_all_reviewers === "true",

src/test/backport.integration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ describe("Backport.run() orchestration", () => {
224224
const config = makeConfig({
225225
commits: {
226226
cherry_picking: "pull_request_head",
227+
cherry_picking_merge_mode: "default",
227228
merge_commits: "fail",
228229
},
229230
});

src/test/helpers/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ export function makeConfig(overrides?: Partial<Config>): Config {
1212
add_labels: [],
1313
add_reviewers: [],
1414
add_team_reviewers: [],
15-
commits: { cherry_picking: "auto", merge_commits: "fail" },
15+
commits: {
16+
cherry_picking: "auto",
17+
cherry_picking_merge_mode: "default",
18+
merge_commits: "fail",
19+
},
1620
copy_milestone: false,
1721
copy_assignees: false,
1822
copy_requested_reviewers: false,

src/test/utils.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { describe, it, expect } from "vitest";
22
import dedent from "dedent";
3-
import { getMentionedIssueRefs, replacePlaceholders } from "../utils.js";
3+
import {
4+
coerceCherryPickingMergeMode,
5+
getMentionedIssueRefs,
6+
replacePlaceholders,
7+
} from "../utils.js";
48

59
describe("get mentioned issues", () => {
610
describe("returns an empty list", () => {
@@ -244,6 +248,32 @@ describe("compose body/title", () => {
244248
});
245249
});
246250

251+
describe("coerceCherryPickingMergeMode", () => {
252+
it('coerces empty string to "default"', () => {
253+
expect(coerceCherryPickingMergeMode("")).toEqual("default");
254+
});
255+
256+
it('passes through "default"', () => {
257+
expect(coerceCherryPickingMergeMode("default")).toEqual("default");
258+
});
259+
260+
it('passes through "whitespace_tolerant"', () => {
261+
expect(coerceCherryPickingMergeMode("whitespace_tolerant")).toEqual(
262+
"whitespace_tolerant",
263+
);
264+
});
265+
266+
it('returns "invalid" for an unrecognized value', () => {
267+
expect(coerceCherryPickingMergeMode("invalid_value")).toEqual("invalid");
268+
});
269+
270+
it('returns "invalid" for wrong case (case-sensitive)', () => {
271+
expect(coerceCherryPickingMergeMode("WHITESPACE_TOLERANT")).toEqual(
272+
"invalid",
273+
);
274+
});
275+
});
276+
247277
function text({
248278
start = "",
249279
middle = "",

src/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ const patterns = {
5151
ref: /(?:^| )((?<org>[^\n #\/]+)\/(?<repo>[^\n #\/]+))?#(?<number>[1-9][0-9]*)(?: |$)/gm,
5252
};
5353

54+
export function coerceCherryPickingMergeMode(
55+
raw: string,
56+
): "default" | "whitespace_tolerant" | "invalid" {
57+
const value = raw === "" ? "default" : raw;
58+
if (value === "default" || value === "whitespace_tolerant") {
59+
return value;
60+
}
61+
return "invalid";
62+
}
63+
5464
const toRef = (url: string) => {
5565
// matchAll is not yet available to directly access the captured groups of all matches
5666
// so this maps the urls to GitHub refs by matching again without the global flag

0 commit comments

Comments
 (0)