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 2 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
54 changes: 51 additions & 3 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 Down Expand Up @@ -83,6 +87,51 @@ export class Backport {
);

const commitShas = await this.github.getCommits(mainpr);

let commitShasToCherryPick: string[];

/*

squashed:
- > single parent
- > the parent of the `merge_commit_sha` is NOT associated with the PR

=> use merge_commit_sha

rebased single-commit:
- > single parent
- > the parent of the `merge_commit_sha` is associated with the PR

=> use commits associated with the PR

rebased multi-commit:
- > single parent,
- > the parent of the `merge_commit_sha` is associated with the PR

=> use commits associated with the PR

merge-commit:
- > multiple parents

=> use commits associated with the PR
korthout marked this conversation as resolved.
Show resolved Hide resolved
*/
// switch case to check if it is a squash, rebase, or merge commit
switch (await this.github.mergeStrategy(mainpr)) {
case MergeStrategy.SQUASHED:
commitShasToCherryPick = [
await this.github.getMergeCommitSha(mainpr),
]?.filter(Boolean) as string[];
case MergeStrategy.REBASED:
commitShasToCherryPick = commitShas;
case MergeStrategy.MERGECOMMIT:
commitShasToCherryPick = commitShas;
case MergeStrategy.UNKNOWN:
// probably write a comment
console.log("Could not detect merge strategy.");
return;
default:
commitShasToCherryPick = [];
jschmid1 marked this conversation as resolved.
Show resolved Hide resolved
}
console.log(`Found commits: ${commitShas}`);

console.log("Checking the merged pull request for merge commits");
Expand All @@ -109,7 +158,6 @@ export class Backport {
return;
}

let commitShasToCherryPick = commitShas;
if (
mergeCommitShas.length > 0 &&
this.config.commits.merge_commits == "skip"
Expand Down Expand Up @@ -145,7 +193,7 @@ export class Backport {
console.log(`Backporting to target branch '${target}...'`);

try {
await this.git.fetch(target, this.config.pwd, 1);
await this.git.fetch(target, this.config.pwd, 2);
} catch (error) {
if (error instanceof GitRefNotFoundError) {
const message = this.composeMessageForFetchTargetFailure(error.ref);
Expand Down
139 changes: 139 additions & 0 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface GithubApi {
requestReviewers(request: ReviewRequest): Promise<RequestReviewersResponse>;
setAssignees(pr: number, assignees: string[]): Promise<GenericResponse>;
setMilestone(pr: number, milestone: number): Promise<GenericResponse>;
mergeStrategy(pull: PullRequest): Promise<string | null>;
getMergeCommitSha(pull: PullRequest): Promise<string | null>;
}

export class Github implements GithubApi {
Expand Down Expand Up @@ -146,12 +148,149 @@ export class Github implements GithubApi {
milestone: milestone,
});
}

/**
* Retrieves the SHA of the merge commit for a given pull request.
* @param pull - The pull request object.
* @returns The SHA of the merge commit.
*/
public async getMergeCommitSha(pull: PullRequest) {
return pull.merge_commit_sha;
}
korthout marked this conversation as resolved.
Show resolved Hide resolved

/**
* Retrieves a commit from the repository.
* @param sha - The SHA of the commit to retrieve.
* @returns A promise that resolves to the retrieved commit.
*/
public async getCommit(sha: string) {
const commit = this.#octokit.rest.repos.getCommit({
...this.getRepo(),
ref: sha,
});
return commit;
}

/**
* Retrieves the parents of a commit.
* @param sha - The SHA of the commit.
* @returns A promise that resolves to the parents of the commit.
*/
public async getParents(sha: string) {
const commit = await this.getCommit(sha);
return commit.data.parents;
}

/**
* Retrieves the pull requests associated with a specific commit.
* @param sha The SHA of the commit.
* @returns A promise that resolves to the pull requests associated with the commit.
*/
public async getPullRequestsAssociatedWithCommit(sha: string) {
const pr = this.#octokit.rest.repos.listPullRequestsAssociatedWithCommit({
...this.getRepo(),
commit_sha: sha,
});
return pr;
}

/**
* Checks if a given SHA is associated with a specific pull request.
* @param sha - The SHA of the commit.
* @param pull - The pull request to check against.
* @returns A boolean indicating whether the SHA is associated with the pull request.
*/
public async isShaAssociatedWithPullRequest(sha: string, pull: PullRequest) {
const assoc_pr = await this.getPullRequestsAssociatedWithCommit(sha);
const assoc_pr_data = assoc_pr.data;
// commits can be associated with multiple PRs
// checks if any of the assoc_prs is the same as the pull
return assoc_pr_data.some((pr) => pr.number == pull.number);
}

public async getMergeCommitShaAndParents(pull: PullRequest) {
const merge_commit_sha = await this.getMergeCommitSha(pull);
if (!merge_commit_sha) {
console.log("likely not merged yet.");
return null;
}
const parents = await this.getParents(merge_commit_sha);
return { merge_commit_sha, parents };
}

public async isMergeCommit(parents: any[]): Promise<boolean> {
return parents.length > 1;
}

public async isRebased(
first_parent_sha: string,
merge_commit_sha: string,
pull: PullRequest,
): Promise<boolean> {
const parent_belongs_to_pr = await this.isShaAssociatedWithPullRequest(
first_parent_sha,
pull,
);
const merge_belongs_to_pr = await this.isShaAssociatedWithPullRequest(
merge_commit_sha,
pull,
);
return parent_belongs_to_pr && merge_belongs_to_pr;
}

public async isSquashed(
parent_belongs_to_pr: boolean,
merge_belongs_to_pr: boolean,
): Promise<boolean> {
return !parent_belongs_to_pr && merge_belongs_to_pr;
}

public async mergeStrategy(pull: PullRequest) {
const result = await this.getMergeCommitShaAndParents(pull);
if (!result) return null;

const { merge_commit_sha, parents } = result;

if (await this.isMergeCommit(parents)) {
console.log("PR was merged using a merge commit");
return MergeStrategy.MERGECOMMIT;
}

const first_parent_sha = parents[0].sha;
if (await this.isRebased(first_parent_sha, merge_commit_sha, pull)) {
console.log("PR was merged using a rebase");
return MergeStrategy.REBASED;
}

const parent_belongs_to_pr = await this.isShaAssociatedWithPullRequest(
first_parent_sha,
pull,
);
const merge_belongs_to_pr = await this.isShaAssociatedWithPullRequest(
merge_commit_sha,
pull,
);
if (await this.isSquashed(parent_belongs_to_pr, merge_belongs_to_pr)) {
console.log("PR was merged using a squash");
return MergeStrategy.SQUASHED;
}

return MergeStrategy.UNKNOWN;
}
}

export enum MergeStrategy {
SQUASHED = "squashed",
REBASED = "rebased",
MERGECOMMIT = "mergecommit",
UNKNOWN = "unknown",
}

export type PullRequest = {
number: number;
title: string;
body: string | null;
merge_commit_sha: string | null;
head: {
sha: string;
ref: string;
Expand Down