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

Detect squash and rebase merge methods #399

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,30 @@ Controls whether to copy the requested reviewers from the original pull request
Note that this does not request reviews from those users who already reviewed the original pull request.
By default, the requested reviewers are not copied.

### `experimental`

Default:

```json
{
"detect_merge_method": false
}
```

Configure experimental features by passing a JSON object.
The following properties can be specified:

#### `detect_merge_method`

Default: `false`

When enabled, the action detects the method used to merge the pull request.
- For "Squash and merge", the action cherry-picks the squashed commit.
- For "Rebase and merge", the action cherry-picks the rebased commits.
- 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.

### `github_token`

Default: `${{ github.token }}`
Expand Down
18 changes: 18 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ inputs:
Note that this does not request reviews from those users who already reviewed the original pull request.
By default, the requested reviewers are not copied.
default: false
experimental:
description: >
Configure experimental features by passing a JSON object.
The following properties can be specified:

#### `detect_merge_method`

When enabled, the action detects the method used to merge the pull request.
- For "Squash and merge", the action cherry-picks the squashed commit.
- For "Rebase and merge", the action cherry-picks the rebased commits.
- 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.
default: >
{
"detect_merge_method": false
}
github_token:
description: >
Token to authenticate requests to GitHub.
Expand Down Expand Up @@ -69,6 +86,7 @@ inputs:
Note that the pull request's headref is excluded automatically.
Can be used in addition to backport labels.
By default, only backport labels are used to specify the target branches.

outputs:
was_successful:
description: >
Expand Down
78 changes: 73 additions & 5 deletions src/backport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as core from "@actions/core";
import dedent from "dedent";

import { CreatePullRequestResponse, PullRequest } from "./github";
import {
CreatePullRequestResponse,
PullRequest,
MergeStrategy,
} from "./github";
import { GithubApi } from "./github";
import { Git, GitRefNotFoundError } from "./git";
import * as utils from "./utils";
Expand All @@ -28,8 +32,17 @@ export type Config = {
copy_milestone: boolean;
copy_assignees: boolean;
copy_requested_reviewers: boolean;
experimental: Experimental;
};

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

enum Output {
wasSuccessful = "was_successful",
wasSuccessfulByTarget = "was_successful_by_target",
Expand Down Expand Up @@ -83,11 +96,67 @@ export class Backport {
);

const commitShas = await this.github.getCommits(mainpr);
console.log(`Found commits: ${commitShas}`);

let commitShasToCherryPick;

if (this.config.experimental.detect_merge_method) {
const merge_commit_sha = await this.github.getMergeCommitSha(mainpr);

// switch case to check if it is a squash, rebase, or merge commit
switch (await this.github.mergeStrategy(mainpr, merge_commit_sha)) {
case MergeStrategy.SQUASHED:
// If merged via a squash merge_commit_sha represents the SHA of the squashed commit on
// the base branch. We must fetch it and its parent in case of a shallowly cloned repo
// To store the fetched commits indefinitely we save them to a remote ref using the sha
await this.git.fetch(
`+${merge_commit_sha}:refs/remotes/origin/${merge_commit_sha}`,
this.config.pwd,
2, // +1 in case this concerns a shallowly cloned repo
);
commitShasToCherryPick = [merge_commit_sha!];
break;
case MergeStrategy.REBASED:
// If rebased merge_commit_sha represents the commit that the base branch was updated to
// We must fetch it, its parents, and one extra parent in case of a shallowly cloned repo
// To store the fetched commits indefinitely we save them to a remote ref using the sha
await this.git.fetch(
`+${merge_commit_sha}:refs/remotes/origin/${merge_commit_sha}`,
this.config.pwd,
mainpr.commits + 1, // +1 in case this concerns a shallowly cloned repo
);
const range = `${merge_commit_sha}~${mainpr.commits}..${merge_commit_sha}`;
commitShasToCherryPick = await this.git.findCommitsInRange(
range,
this.config.pwd,
);
break;
case MergeStrategy.MERGECOMMIT:
commitShasToCherryPick = commitShas;
break;
case MergeStrategy.UNKNOWN:
console.log(
"Could not detect merge strategy. Using commits from the Pull Request.",
);
commitShasToCherryPick = commitShas;
break;
default:
console.log(
"Could not detect merge strategy. Using commits from the Pull Request.",
);
commitShasToCherryPick = commitShas;
break;
}
} else {
console.log(
"Not detecting merge strategy. Using commits from the Pull Request.",
);
commitShasToCherryPick = commitShas;
}
console.log(`Found commits to backport: ${commitShasToCherryPick}`);

console.log("Checking the merged pull request for merge commits");
const mergeCommitShas = await this.git.findMergeCommits(
commitShas,
commitShasToCherryPick,
this.config.pwd,
);
console.log(
Expand All @@ -109,13 +178,12 @@ export class Backport {
return;
}

let commitShasToCherryPick = commitShas;
if (
mergeCommitShas.length > 0 &&
this.config.commits.merge_commits == "skip"
) {
console.log("Skipping merge commits: " + mergeCommitShas);
const nonMergeCommitShas = commitShas.filter(
const nonMergeCommitShas = commitShasToCherryPick.filter(
(sha) => !mergeCommitShas.includes(sha),
);
commitShasToCherryPick = nonMergeCommitShas;
Expand Down
21 changes: 21 additions & 0 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ export class Git {
}
}

public async findCommitsInRange(
range: string,
pwd: string,
): Promise<string[]> {
const { exitCode, stdout } = await this.git(
"log",
['--pretty=format:"%H"', "--reverse", range],
pwd,
);
if (exitCode !== 0) {
throw new Error(
`'git log --pretty=format:"%H" ${range}' failed with exit code ${exitCode}`,
);
}
const commitShas = stdout
.split("\n")
.map((sha) => sha.replace(/"/g, ""))
.filter((sha) => sha.trim() !== "");
return commitShas;
}

public async findMergeCommits(
commitShas: string[],
pwd: string,
Expand Down
Loading
Loading