feat(issue-77): handle multiple target branches (#78)

fix: https://github.com/kiegroup/git-backporting/issues/77

This enhancement allow users to backport the same change to multiple
branches with one single tool invocation
This commit is contained in:
Andrea Lamparelli 2023-08-03 21:57:11 +02:00 committed by GitHub
parent c19a56a9ad
commit 5fc72e127b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1774 additions and 234 deletions

View file

@ -94,7 +94,7 @@ This tool comes with some inputs that allow users to override the default behavi
|---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------| |---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| Version | -V, --version | - | Current version of the tool | | | Version | -V, --version | - | Current version of the tool | |
| Help | -h, --help | - | Display the help message | | | Help | -h, --help | - | Display the help message | |
| Target Branch | -tb, --target-branch | N | Branch where the changes must be backported to | | | Target Branches | -tb, --target-branch | N | Comma separated list of branches where the changes must be backported to | |
| Pull Request | -pr, --pull-request | N | Original pull request url, the one that must be backported, e.g., https://github.com/kiegroup/git-backporting/pull/1 | | | Pull Request | -pr, --pull-request | N | Original pull request url, the one that must be backported, e.g., https://github.com/kiegroup/git-backporting/pull/1 | |
| Configuration File | -cf, --config-file | N | Configuration file, in JSON format, containing all options to be overridded, note that if provided all other CLI options will be ignored | | | Configuration File | -cf, --config-file | N | Configuration file, in JSON format, containing all options to be overridded, note that if provided all other CLI options will be ignored | |
| Auth | -a, --auth | N | `GITHUB_TOKEN`, `GITLAB_TOKEN` or a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | "" | | Auth | -a, --auth | N | `GITHUB_TOKEN`, `GITLAB_TOKEN` or a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | "" |
@ -107,7 +107,7 @@ This tool comes with some inputs that allow users to override the default behavi
| Reviewers | --reviewers | N | Backporting pull request comma-separated reviewers list | [] | | Reviewers | --reviewers | N | Backporting pull request comma-separated reviewers list | [] |
| Assignees | --assignes | N | Backporting pull request comma-separated assignees list | [] | | Assignees | --assignes | N | Backporting pull request comma-separated assignees list | [] |
| No Reviewers Inheritance | --no-inherit-reviewers | N | Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request | false | | No Reviewers Inheritance | --no-inherit-reviewers | N | Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request | false |
| Backport Branch Name | --bp-branch-name | N | Name of the backporting pull request branch, if it exceeds 250 chars it will be truncated | bp-{target-branch}-{sha1}...{shaN} | | Backport Branch Names | --bp-branch-name | N | Comma separated lists of the backporting pull request branch names, if they exceeds 250 chars they will be truncated | bp-{target-branch}-{sha1}...{shaN} |
| Labels | --labels | N | Provide custom labels to be added to the backporting pull request | [] | | Labels | --labels | N | Provide custom labels to be added to the backporting pull request | [] |
| Inherit labels | --inherit-labels | N | If enabled inherit lables from the original pull request | false | | Inherit labels | --inherit-labels | N | If enabled inherit lables from the original pull request | false |
| No squash | --no-squash | N | If provided the backporting will try to backport all pull request commits without squashing | false | | No squash | --no-squash | N | If provided the backporting will try to backport all pull request commits without squashing | false |

View file

@ -5,7 +5,7 @@ inputs:
description: "URL of the pull request to backport, e.g., https://github.com/kiegroup/git-backporting/pull/1" description: "URL of the pull request to backport, e.g., https://github.com/kiegroup/git-backporting/pull/1"
required: false required: false
target-branch: target-branch:
description: "Branch where the pull request must be backported to" description: "Comma separated list of branches where the pull request must be backported to"
required: false required: false
config-file: config-file:
description: "Path to a file containing the json configuration for this tool, the object must match the Args interface" description: "Path to a file containing the json configuration for this tool, the object must match the Args interface"
@ -36,7 +36,7 @@ inputs:
description: "Backporting PR body. Default is the original PR body" description: "Backporting PR body. Default is the original PR body"
required: false required: false
bp-branch-name: bp-branch-name:
description: "Backporting PR branch name. Default is auto-generated from commit" description: "Comma separated list of backporting PR branch names. Default is auto-generated from commit and target branches"
required: false required: false
reviewers: reviewers:
description: "Comma separated list of reviewers for the backporting pull request" description: "Comma separated list of reviewers for the backporting pull request"

103
dist/cli/index.js vendored
View file

@ -40,8 +40,8 @@ class ArgsParser {
parse() { parse() {
const args = this.readArgs(); const args = this.readArgs();
// validate and fill with defaults // validate and fill with defaults
if (!args.pullRequest || !args.targetBranch) { if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) {
throw new Error("Missing option: pull request and target branch must be provided"); throw new Error("Missing option: pull request and target branches must be provided");
} }
return { return {
pullRequest: args.pullRequest, pullRequest: args.pullRequest,
@ -179,7 +179,7 @@ class CLIArgsParser extends args_parser_1.default {
return new commander_1.Command(package_json_1.name) return new commander_1.Command(package_json_1.name)
.version(package_json_1.version) .version(package_json_1.version)
.description(package_json_1.description) .description(package_json_1.description)
.option("-tb, --target-branch <branch>", "branch where changes must be backported to") .option("-tb, --target-branch <branches>", "comma separated list of branches where changes must be backported to")
.option("-pr, --pull-request <pr-url>", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1") .option("-pr, --pull-request <pr-url>", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1")
.option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely") .option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely")
.option("-a, --auth <auth>", "git service authentication string, e.g., github token") .option("-a, --auth <auth>", "git service authentication string, e.g., github token")
@ -189,7 +189,7 @@ class CLIArgsParser extends args_parser_1.default {
.option("--title <bp-title>", "backport pr title, default original pr title prefixed by target branch") .option("--title <bp-title>", "backport pr title, default original pr title prefixed by target branch")
.option("--body <bp-body>", "backport pr title, default original pr body prefixed by bodyPrefix") .option("--body <bp-body>", "backport pr title, default original pr body prefixed by bodyPrefix")
.option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`") .option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`")
.option("--bp-branch-name <bp-branch-name>", "backport pr branch name, default auto-generated by the commit") .option("--bp-branch-name <bp-branch-names>", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch")
.option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList) .option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList)
.option("--assignees <assignees>", "comma separated list of assignees for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList) .option("--assignees <assignees>", "comma separated list of assignees for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList)
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request") .option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")
@ -288,6 +288,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
const args_utils_1 = __nccwpck_require__(8048);
const configs_parser_1 = __importDefault(__nccwpck_require__(5799)); const configs_parser_1 = __importDefault(__nccwpck_require__(5799));
const git_client_factory_1 = __importDefault(__nccwpck_require__(8550)); const git_client_factory_1 = __importDefault(__nccwpck_require__(8550));
class PullRequestConfigsParser extends configs_parser_1.default { class PullRequestConfigsParser extends configs_parser_1.default {
@ -305,15 +306,19 @@ class PullRequestConfigsParser extends configs_parser_1.default {
throw error; throw error;
} }
const folder = args.folder ?? this.getDefaultFolder(); const folder = args.folder ?? this.getDefaultFolder();
const targetBranches = [...new Set((0, args_utils_1.getAsCommaSeparatedList)(args.targetBranch))];
const bpBranchNames = [...new Set(args.bpBranchName ? ((0, args_utils_1.getAsCleanedCommaSeparatedList)(args.bpBranchName) ?? []) : [])];
if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) {
throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`);
}
return { return {
dryRun: args.dryRun, dryRun: args.dryRun,
auth: args.auth, auth: args.auth,
folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`,
targetBranch: args.targetBranch,
mergeStrategy: args.strategy, mergeStrategy: args.strategy,
mergeStrategyOption: args.strategyOption, mergeStrategyOption: args.strategyOption,
originalPullRequest: pr, originalPullRequest: pr,
backportPullRequest: this.getDefaultBackportPullRequest(pr, args), backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames),
git: { git: {
user: args.gitUser ?? this.gitClient.getDefaultGitUser(), user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
@ -330,7 +335,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
* @param targetBranch target branch where the backport should be applied * @param targetBranch target branch where the backport should be applied
* @returns {GitPullRequest} * @returns {GitPullRequest}
*/ */
getDefaultBackportPullRequest(originalPullRequest, args) { generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) {
const reviewers = args.reviewers ?? []; const reviewers = args.reviewers ?? [];
if (reviewers.length == 0 && args.inheritReviewers) { if (reviewers.length == 0 && args.inheritReviewers) {
// inherit only if args.reviewers is empty and args.inheritReviewers set to true // inherit only if args.reviewers is empty and args.inheritReviewers set to true
@ -345,11 +350,18 @@ class PullRequestConfigsParser extends configs_parser_1.default {
if (args.inheritLabels) { if (args.inheritLabels) {
labels.push(...originalPullRequest.labels); labels.push(...originalPullRequest.labels);
} }
let backportBranch = args.bpBranchName; return targetBranches.map((tb, idx) => {
// if there multiple branch names take the corresponding one, otherwise get the the first one if it exists
let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0];
if (backportBranch === undefined || backportBranch.trim() === "") { if (backportBranch === undefined || backportBranch.trim() === "") {
// for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects
const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-"); const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-");
backportBranch = `bp-${args.targetBranch}-${concatenatedCommits}`; backportBranch = `bp-${tb}-${concatenatedCommits}`;
}
else if (bpBranchNames.length == 1 && targetBranches.length > 1) {
// multiple targets and single custom backport branch name we need to differentiate branch names
// so append "-${tb}" to the provided name
backportBranch = backportBranch + `-${tb}`;
} }
if (backportBranch.length > 250) { if (backportBranch.length > 250) {
this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`);
@ -359,8 +371,8 @@ class PullRequestConfigsParser extends configs_parser_1.default {
owner: originalPullRequest.targetRepo.owner, owner: originalPullRequest.targetRepo.owner,
repo: originalPullRequest.targetRepo.project, repo: originalPullRequest.targetRepo.project,
head: backportBranch, head: backportBranch,
base: args.targetBranch, base: tb,
title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, title: args.title ?? `[${tb}] ${originalPullRequest.title}`,
// preserve new line chars // preserve new line chars
body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"),
reviewers: [...new Set(reviewers)], reviewers: [...new Set(reviewers)],
@ -368,6 +380,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
labels: [...new Set(labels)], labels: [...new Set(labels)],
comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [],
}; };
});
} }
} }
exports["default"] = PullRequestConfigsParser; exports["default"] = PullRequestConfigsParser;
@ -436,10 +449,12 @@ class GitCLIService {
this.logger.info(`Cloning repository ${from} to ${to}`); this.logger.info(`Cloning repository ${from} to ${to}`);
if (!fs_1.default.existsSync(to)) { if (!fs_1.default.existsSync(to)) {
await (0, simple_git_1.default)().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]); await (0, simple_git_1.default)().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]);
return;
} }
else { this.logger.info(`Folder ${to} already exist. Won't clone`);
this.logger.warn(`Folder ${to} already exist. Won't clone`); // checkout to the proper branch
} this.logger.info(`Checking out branch ${branch}`);
await this.git(to).checkout(branch);
} }
/** /**
* Create a new branch starting from the current one and checkout in it * Create a new branch starting from the current one and checkout in it
@ -1115,22 +1130,31 @@ class ConsoleLoggerService {
this.logger = new logger_1.default(); this.logger = new logger_1.default();
this.verbose = verbose; this.verbose = verbose;
} }
setContext(newContext) {
this.context = newContext;
}
clearContext() {
this.context = undefined;
}
trace(message) { trace(message) {
this.logger.log("TRACE", message); this.logger.log("TRACE", this.fromContext(message));
} }
debug(message) { debug(message) {
if (this.verbose) { if (this.verbose) {
this.logger.log("DEBUG", message); this.logger.log("DEBUG", this.fromContext(message));
} }
} }
info(message) { info(message) {
this.logger.log("INFO", message); this.logger.log("INFO", this.fromContext(message));
} }
warn(message) { warn(message) {
this.logger.log("WARN", message); this.logger.log("WARN", this.fromContext(message));
} }
error(message) { error(message) {
this.logger.log("ERROR", message); this.logger.log("ERROR", this.fromContext(message));
}
fromContext(msg) {
return this.context ? `[${this.context}] ${msg}` : msg;
} }
} }
exports["default"] = ConsoleLoggerService; exports["default"] = ConsoleLoggerService;
@ -1242,39 +1266,62 @@ class Runner {
// 3. parse configs // 3. parse configs
this.logger.debug("Parsing configs.."); this.logger.debug("Parsing configs..");
const configs = await new pr_configs_parser_1.default().parseAndValidate(args); const configs = await new pr_configs_parser_1.default().parseAndValidate(args);
const originalPR = configs.originalPullRequest; const backportPRs = configs.backportPullRequests;
const backportPR = configs.backportPullRequest;
// start local git operations // start local git operations
const git = new git_cli_1.default(configs.auth, configs.git); const git = new git_cli_1.default(configs.auth, configs.git);
const failures = [];
// we need sequential backporting as they will operate on the same folder
// avoid cloning the same repo multiple times
for (const pr of backportPRs) {
try {
await this.executeBackport(configs, pr, {
gitClientType: gitClientType,
gitClientApi: gitApi,
gitCli: git,
});
}
catch (error) {
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
failures.push(error);
}
}
if (failures.length > 0) {
throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`);
}
}
async executeBackport(configs, backportPR, git) {
this.logger.setContext(backportPR.base);
const originalPR = configs.originalPullRequest;
// 4. clone the repository // 4. clone the repository
this.logger.debug("Cloning repo.."); this.logger.debug("Cloning repo..");
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch); await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base);
// 5. create new branch from target one and checkout // 5. create new branch from target one and checkout
this.logger.debug("Creating local branch.."); this.logger.debug("Creating local branch..");
await git.createLocalBranch(configs.folder, backportPR.head); await git.gitCli.createLocalBranch(configs.folder, backportPR.head);
// 6. fetch pull request remote if source owner != target owner or pull request still open // 6. fetch pull request remote if source owner != target owner or pull request still open
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
configs.originalPullRequest.state === "open") { configs.originalPullRequest.state === "open") {
this.logger.debug("Fetching pull request remote.."); this.logger.debug("Fetching pull request remote..");
const prefix = gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab const prefix = git.gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab
await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
} }
// 7. apply all changes to the new branch // 7. apply all changes to the new branch
this.logger.debug("Cherry picking commits.."); this.logger.debug("Cherry picking commits..");
for (const sha of originalPR.commits) { for (const sha of originalPR.commits) {
await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption);
} }
if (!configs.dryRun) { if (!configs.dryRun) {
// 8. push the new branch to origin // 8. push the new branch to origin
await git.push(configs.folder, backportPR.head); await git.gitCli.push(configs.folder, backportPR.head);
// 9. create pull request new branch -> target branch (using octokit) // 9. create pull request new branch -> target branch (using octokit)
const prUrl = await gitApi.createPullRequest(backportPR); const prUrl = await git.gitClientApi.createPullRequest(backportPR);
this.logger.info(`Pull request created: ${prUrl}`); this.logger.info(`Pull request created: ${prUrl}`);
} }
else { else {
this.logger.warn("Pull request creation and remote push skipped"); this.logger.warn("Pull request creation and remote push skipped");
this.logger.info(`${JSON.stringify(backportPR, null, 2)}`); this.logger.info(`${JSON.stringify(backportPR, null, 2)}`);
} }
this.logger.clearContext();
} }
} }
exports["default"] = Runner; exports["default"] = Runner;

99
dist/gha/index.js vendored
View file

@ -40,8 +40,8 @@ class ArgsParser {
parse() { parse() {
const args = this.readArgs(); const args = this.readArgs();
// validate and fill with defaults // validate and fill with defaults
if (!args.pullRequest || !args.targetBranch) { if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) {
throw new Error("Missing option: pull request and target branch must be provided"); throw new Error("Missing option: pull request and target branches must be provided");
} }
return { return {
pullRequest: args.pullRequest, pullRequest: args.pullRequest,
@ -258,6 +258,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
const args_utils_1 = __nccwpck_require__(8048);
const configs_parser_1 = __importDefault(__nccwpck_require__(5799)); const configs_parser_1 = __importDefault(__nccwpck_require__(5799));
const git_client_factory_1 = __importDefault(__nccwpck_require__(8550)); const git_client_factory_1 = __importDefault(__nccwpck_require__(8550));
class PullRequestConfigsParser extends configs_parser_1.default { class PullRequestConfigsParser extends configs_parser_1.default {
@ -275,15 +276,19 @@ class PullRequestConfigsParser extends configs_parser_1.default {
throw error; throw error;
} }
const folder = args.folder ?? this.getDefaultFolder(); const folder = args.folder ?? this.getDefaultFolder();
const targetBranches = [...new Set((0, args_utils_1.getAsCommaSeparatedList)(args.targetBranch))];
const bpBranchNames = [...new Set(args.bpBranchName ? ((0, args_utils_1.getAsCleanedCommaSeparatedList)(args.bpBranchName) ?? []) : [])];
if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) {
throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`);
}
return { return {
dryRun: args.dryRun, dryRun: args.dryRun,
auth: args.auth, auth: args.auth,
folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`,
targetBranch: args.targetBranch,
mergeStrategy: args.strategy, mergeStrategy: args.strategy,
mergeStrategyOption: args.strategyOption, mergeStrategyOption: args.strategyOption,
originalPullRequest: pr, originalPullRequest: pr,
backportPullRequest: this.getDefaultBackportPullRequest(pr, args), backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames),
git: { git: {
user: args.gitUser ?? this.gitClient.getDefaultGitUser(), user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
@ -300,7 +305,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
* @param targetBranch target branch where the backport should be applied * @param targetBranch target branch where the backport should be applied
* @returns {GitPullRequest} * @returns {GitPullRequest}
*/ */
getDefaultBackportPullRequest(originalPullRequest, args) { generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) {
const reviewers = args.reviewers ?? []; const reviewers = args.reviewers ?? [];
if (reviewers.length == 0 && args.inheritReviewers) { if (reviewers.length == 0 && args.inheritReviewers) {
// inherit only if args.reviewers is empty and args.inheritReviewers set to true // inherit only if args.reviewers is empty and args.inheritReviewers set to true
@ -315,11 +320,18 @@ class PullRequestConfigsParser extends configs_parser_1.default {
if (args.inheritLabels) { if (args.inheritLabels) {
labels.push(...originalPullRequest.labels); labels.push(...originalPullRequest.labels);
} }
let backportBranch = args.bpBranchName; return targetBranches.map((tb, idx) => {
// if there multiple branch names take the corresponding one, otherwise get the the first one if it exists
let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0];
if (backportBranch === undefined || backportBranch.trim() === "") { if (backportBranch === undefined || backportBranch.trim() === "") {
// for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects
const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-"); const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-");
backportBranch = `bp-${args.targetBranch}-${concatenatedCommits}`; backportBranch = `bp-${tb}-${concatenatedCommits}`;
}
else if (bpBranchNames.length == 1 && targetBranches.length > 1) {
// multiple targets and single custom backport branch name we need to differentiate branch names
// so append "-${tb}" to the provided name
backportBranch = backportBranch + `-${tb}`;
} }
if (backportBranch.length > 250) { if (backportBranch.length > 250) {
this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`);
@ -329,8 +341,8 @@ class PullRequestConfigsParser extends configs_parser_1.default {
owner: originalPullRequest.targetRepo.owner, owner: originalPullRequest.targetRepo.owner,
repo: originalPullRequest.targetRepo.project, repo: originalPullRequest.targetRepo.project,
head: backportBranch, head: backportBranch,
base: args.targetBranch, base: tb,
title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, title: args.title ?? `[${tb}] ${originalPullRequest.title}`,
// preserve new line chars // preserve new line chars
body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"),
reviewers: [...new Set(reviewers)], reviewers: [...new Set(reviewers)],
@ -338,6 +350,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
labels: [...new Set(labels)], labels: [...new Set(labels)],
comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [],
}; };
});
} }
} }
exports["default"] = PullRequestConfigsParser; exports["default"] = PullRequestConfigsParser;
@ -406,10 +419,12 @@ class GitCLIService {
this.logger.info(`Cloning repository ${from} to ${to}`); this.logger.info(`Cloning repository ${from} to ${to}`);
if (!fs_1.default.existsSync(to)) { if (!fs_1.default.existsSync(to)) {
await (0, simple_git_1.default)().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]); await (0, simple_git_1.default)().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]);
return;
} }
else { this.logger.info(`Folder ${to} already exist. Won't clone`);
this.logger.warn(`Folder ${to} already exist. Won't clone`); // checkout to the proper branch
} this.logger.info(`Checking out branch ${branch}`);
await this.git(to).checkout(branch);
} }
/** /**
* Create a new branch starting from the current one and checkout in it * Create a new branch starting from the current one and checkout in it
@ -1085,22 +1100,31 @@ class ConsoleLoggerService {
this.logger = new logger_1.default(); this.logger = new logger_1.default();
this.verbose = verbose; this.verbose = verbose;
} }
setContext(newContext) {
this.context = newContext;
}
clearContext() {
this.context = undefined;
}
trace(message) { trace(message) {
this.logger.log("TRACE", message); this.logger.log("TRACE", this.fromContext(message));
} }
debug(message) { debug(message) {
if (this.verbose) { if (this.verbose) {
this.logger.log("DEBUG", message); this.logger.log("DEBUG", this.fromContext(message));
} }
} }
info(message) { info(message) {
this.logger.log("INFO", message); this.logger.log("INFO", this.fromContext(message));
} }
warn(message) { warn(message) {
this.logger.log("WARN", message); this.logger.log("WARN", this.fromContext(message));
} }
error(message) { error(message) {
this.logger.log("ERROR", message); this.logger.log("ERROR", this.fromContext(message));
}
fromContext(msg) {
return this.context ? `[${this.context}] ${msg}` : msg;
} }
} }
exports["default"] = ConsoleLoggerService; exports["default"] = ConsoleLoggerService;
@ -1212,39 +1236,62 @@ class Runner {
// 3. parse configs // 3. parse configs
this.logger.debug("Parsing configs.."); this.logger.debug("Parsing configs..");
const configs = await new pr_configs_parser_1.default().parseAndValidate(args); const configs = await new pr_configs_parser_1.default().parseAndValidate(args);
const originalPR = configs.originalPullRequest; const backportPRs = configs.backportPullRequests;
const backportPR = configs.backportPullRequest;
// start local git operations // start local git operations
const git = new git_cli_1.default(configs.auth, configs.git); const git = new git_cli_1.default(configs.auth, configs.git);
const failures = [];
// we need sequential backporting as they will operate on the same folder
// avoid cloning the same repo multiple times
for (const pr of backportPRs) {
try {
await this.executeBackport(configs, pr, {
gitClientType: gitClientType,
gitClientApi: gitApi,
gitCli: git,
});
}
catch (error) {
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
failures.push(error);
}
}
if (failures.length > 0) {
throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`);
}
}
async executeBackport(configs, backportPR, git) {
this.logger.setContext(backportPR.base);
const originalPR = configs.originalPullRequest;
// 4. clone the repository // 4. clone the repository
this.logger.debug("Cloning repo.."); this.logger.debug("Cloning repo..");
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch); await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base);
// 5. create new branch from target one and checkout // 5. create new branch from target one and checkout
this.logger.debug("Creating local branch.."); this.logger.debug("Creating local branch..");
await git.createLocalBranch(configs.folder, backportPR.head); await git.gitCli.createLocalBranch(configs.folder, backportPR.head);
// 6. fetch pull request remote if source owner != target owner or pull request still open // 6. fetch pull request remote if source owner != target owner or pull request still open
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
configs.originalPullRequest.state === "open") { configs.originalPullRequest.state === "open") {
this.logger.debug("Fetching pull request remote.."); this.logger.debug("Fetching pull request remote..");
const prefix = gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab const prefix = git.gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab
await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
} }
// 7. apply all changes to the new branch // 7. apply all changes to the new branch
this.logger.debug("Cherry picking commits.."); this.logger.debug("Cherry picking commits..");
for (const sha of originalPR.commits) { for (const sha of originalPR.commits) {
await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption);
} }
if (!configs.dryRun) { if (!configs.dryRun) {
// 8. push the new branch to origin // 8. push the new branch to origin
await git.push(configs.folder, backportPR.head); await git.gitCli.push(configs.folder, backportPR.head);
// 9. create pull request new branch -> target branch (using octokit) // 9. create pull request new branch -> target branch (using octokit)
const prUrl = await gitApi.createPullRequest(backportPR); const prUrl = await git.gitClientApi.createPullRequest(backportPR);
this.logger.info(`Pull request created: ${prUrl}`); this.logger.info(`Pull request created: ${prUrl}`);
} }
else { else {
this.logger.warn("Pull request creation and remote push skipped"); this.logger.warn("Pull request creation and remote push skipped");
this.logger.info(`${JSON.stringify(backportPR, null, 2)}`); this.logger.info(`${JSON.stringify(backportPR, null, 2)}`);
} }
this.logger.clearContext();
} }
} }
exports["default"] = Runner; exports["default"] = Runner;

View file

@ -17,8 +17,8 @@ export default abstract class ArgsParser {
const args = this.readArgs(); const args = this.readArgs();
// validate and fill with defaults // validate and fill with defaults
if (!args.pullRequest || !args.targetBranch) { if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) {
throw new Error("Missing option: pull request and target branch must be provided"); throw new Error("Missing option: pull request and target branches must be provided");
} }
return { return {

View file

@ -1,8 +1,9 @@
/** /**
* Input arguments * Tool's input arguments interface
*/ */
export interface Args { export interface Args {
targetBranch: string, // branch on the target repo where the change should be backported to // NOTE: keep targetBranch as singular and of type string for backward compatibilities
targetBranch: string, // comma separated list of branches on the target repo where the change should be backported to
pullRequest: string, // url of the pull request to backport pullRequest: string, // url of the pull request to backport
dryRun?: boolean, // if enabled do not push anything remotely dryRun?: boolean, // if enabled do not push anything remotely
auth?: string, // git service auth, like github token auth?: string, // git service auth, like github token
@ -12,7 +13,8 @@ export interface Args {
title?: string, // backport pr title, default original pr title prefixed by target branch title?: string, // backport pr title, default original pr title prefixed by target branch
body?: string, // backport pr title, default original pr body prefixed by bodyPrefix body?: string, // backport pr title, default original pr body prefixed by bodyPrefix
bodyPrefix?: string, // backport pr body prefix, default `backport <original-pr-link>` bodyPrefix?: string, // backport pr body prefix, default `backport <original-pr-link>`
bpBranchName?: string, // backport pr branch name, default computed from commit // NOTE: keep bpBranchName as singular and of type string for backward compatibilities
bpBranchName?: string, // comma separated list of backport pr branch names, default computed from commit and target branches
reviewers?: string[], // backport pr reviewers reviewers?: string[], // backport pr reviewers
assignees?: string[], // backport pr assignees assignees?: string[], // backport pr assignees
inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers from original pr inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers from original pr

View file

@ -10,7 +10,7 @@ export default class CLIArgsParser extends ArgsParser {
return new Command(name) return new Command(name)
.version(version) .version(version)
.description(description) .description(description)
.option("-tb, --target-branch <branch>", "branch where changes must be backported to") .option("-tb, --target-branch <branches>", "comma separated list of branches where changes must be backported to")
.option("-pr, --pull-request <pr-url>", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1") .option("-pr, --pull-request <pr-url>", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1")
.option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely") .option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely")
.option("-a, --auth <auth>", "git service authentication string, e.g., github token") .option("-a, --auth <auth>", "git service authentication string, e.g., github token")
@ -20,7 +20,7 @@ export default class CLIArgsParser extends ArgsParser {
.option("--title <bp-title>", "backport pr title, default original pr title prefixed by target branch") .option("--title <bp-title>", "backport pr title, default original pr title prefixed by target branch")
.option("--body <bp-body>", "backport pr title, default original pr body prefixed by bodyPrefix") .option("--body <bp-body>", "backport pr title, default original pr body prefixed by bodyPrefix")
.option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`") .option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`")
.option("--bp-branch-name <bp-branch-name>", "backport pr branch name, default auto-generated by the commit") .option("--bp-branch-name <bp-branch-names>", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch")
.option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request", getAsCleanedCommaSeparatedList) .option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request", getAsCleanedCommaSeparatedList)
.option("--assignees <assignees>", "comma separated list of assignees for the backporting pull request", getAsCleanedCommaSeparatedList) .option("--assignees <assignees>", "comma separated list of assignees for the backporting pull request", getAsCleanedCommaSeparatedList)
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request") .option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")

View file

@ -15,10 +15,9 @@ export interface Configs {
auth?: string, auth?: string,
git: LocalGit, git: LocalGit,
folder: string, folder: string,
targetBranch: string,
mergeStrategy?: string, // cherry-pick merge strategy mergeStrategy?: string, // cherry-pick merge strategy
mergeStrategyOption?: string, // cherry-pick merge strategy option mergeStrategyOption?: string, // cherry-pick merge strategy option
originalPullRequest: GitPullRequest, originalPullRequest: GitPullRequest,
backportPullRequest: BackportPullRequest, backportPullRequests: BackportPullRequest[],
} }

View file

@ -1,3 +1,4 @@
import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList } from "@bp/service/args/args-utils";
import { Args } from "@bp/service/args/args.types"; import { Args } from "@bp/service/args/args.types";
import ConfigsParser from "@bp/service/configs/configs-parser"; import ConfigsParser from "@bp/service/configs/configs-parser";
import { Configs } from "@bp/service/configs/configs.types"; import { Configs } from "@bp/service/configs/configs.types";
@ -25,15 +26,21 @@ export default class PullRequestConfigsParser extends ConfigsParser {
const folder: string = args.folder ?? this.getDefaultFolder(); const folder: string = args.folder ?? this.getDefaultFolder();
const targetBranches: string[] = [...new Set(getAsCommaSeparatedList(args.targetBranch)!)];
const bpBranchNames: string[] = [...new Set(args.bpBranchName ? (getAsCleanedCommaSeparatedList(args.bpBranchName) ?? []) : [])];
if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) {
throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`);
}
return { return {
dryRun: args.dryRun!, dryRun: args.dryRun!,
auth: args.auth, auth: args.auth,
folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`,
targetBranch: args.targetBranch,
mergeStrategy: args.strategy, mergeStrategy: args.strategy,
mergeStrategyOption: args.strategyOption, mergeStrategyOption: args.strategyOption,
originalPullRequest: pr, originalPullRequest: pr,
backportPullRequest: this.getDefaultBackportPullRequest(pr, args), backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames),
git: { git: {
user: args.gitUser ?? this.gitClient.getDefaultGitUser(), user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
@ -52,7 +59,13 @@ export default class PullRequestConfigsParser extends ConfigsParser {
* @param targetBranch target branch where the backport should be applied * @param targetBranch target branch where the backport should be applied
* @returns {GitPullRequest} * @returns {GitPullRequest}
*/ */
private getDefaultBackportPullRequest(originalPullRequest: GitPullRequest, args: Args): BackportPullRequest { private generateBackportPullRequestsData(
originalPullRequest: GitPullRequest,
args: Args,
targetBranches: string[],
bpBranchNames: string[]
): BackportPullRequest[] {
const reviewers = args.reviewers ?? []; const reviewers = args.reviewers ?? [];
if (reviewers.length == 0 && args.inheritReviewers) { if (reviewers.length == 0 && args.inheritReviewers) {
// inherit only if args.reviewers is empty and args.inheritReviewers set to true // inherit only if args.reviewers is empty and args.inheritReviewers set to true
@ -70,11 +83,18 @@ export default class PullRequestConfigsParser extends ConfigsParser {
labels.push(...originalPullRequest.labels); labels.push(...originalPullRequest.labels);
} }
let backportBranch = args.bpBranchName; return targetBranches.map((tb, idx) => {
// if there multiple branch names take the corresponding one, otherwise get the the first one if it exists
let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0];
if (backportBranch === undefined || backportBranch.trim() === "") { if (backportBranch === undefined || backportBranch.trim() === "") {
// for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects
const concatenatedCommits: string = originalPullRequest.commits!.map(c => c.slice(0, 7)).join("-"); const concatenatedCommits: string = originalPullRequest.commits!.map(c => c.slice(0, 7)).join("-");
backportBranch = `bp-${args.targetBranch}-${concatenatedCommits}`; backportBranch = `bp-${tb}-${concatenatedCommits}`;
} else if (bpBranchNames.length == 1 && targetBranches.length > 1) {
// multiple targets and single custom backport branch name we need to differentiate branch names
// so append "-${tb}" to the provided name
backportBranch = backportBranch + `-${tb}`;
} }
if (backportBranch.length > 250) { if (backportBranch.length > 250) {
@ -86,8 +106,8 @@ export default class PullRequestConfigsParser extends ConfigsParser {
owner: originalPullRequest.targetRepo.owner, owner: originalPullRequest.targetRepo.owner,
repo: originalPullRequest.targetRepo.project, repo: originalPullRequest.targetRepo.project,
head: backportBranch, head: backportBranch,
base: args.targetBranch, base: tb,
title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, title: args.title ?? `[${tb}] ${originalPullRequest.title}`,
// preserve new line chars // preserve new line chars
body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"),
reviewers: [...new Set(reviewers)], reviewers: [...new Set(reviewers)],
@ -95,5 +115,6 @@ export default class PullRequestConfigsParser extends ConfigsParser {
labels: [...new Set(labels)], labels: [...new Set(labels)],
comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [],
}; };
}) as BackportPullRequest[];
} }
} }

View file

@ -63,9 +63,13 @@ export default class GitCLIService {
this.logger.info(`Cloning repository ${from} to ${to}`); this.logger.info(`Cloning repository ${from} to ${to}`);
if (!fs.existsSync(to)) { if (!fs.existsSync(to)) {
await simpleGit().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]); await simpleGit().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]);
} else { return;
this.logger.warn(`Folder ${to} already exist. Won't clone`);
} }
this.logger.info(`Folder ${to} already exist. Won't clone`);
// checkout to the proper branch
this.logger.info(`Checking out branch ${branch}`);
await this.git(to).checkout(branch);
} }
/** /**

View file

@ -5,32 +5,44 @@ export default class ConsoleLoggerService implements LoggerService {
private readonly logger: Logger; private readonly logger: Logger;
private readonly verbose: boolean; private readonly verbose: boolean;
private context?: string;
constructor(verbose = true) { constructor(verbose = true) {
this.logger = new Logger(); this.logger = new Logger();
this.verbose = verbose; this.verbose = verbose;
} }
setContext(newContext: string) {
this.context = newContext;
}
clearContext() {
this.context = undefined;
}
trace(message: string): void { trace(message: string): void {
this.logger.log("TRACE", message); this.logger.log("TRACE", this.fromContext(message));
} }
debug(message: string): void { debug(message: string): void {
if (this.verbose) { if (this.verbose) {
this.logger.log("DEBUG", message); this.logger.log("DEBUG", this.fromContext(message));
} }
} }
info(message: string): void { info(message: string): void {
this.logger.log("INFO", message); this.logger.log("INFO", this.fromContext(message));
} }
warn(message: string): void { warn(message: string): void {
this.logger.log("WARN", message); this.logger.log("WARN", this.fromContext(message));
} }
error(message: string): void { error(message: string): void {
this.logger.log("ERROR", message); this.logger.log("ERROR", this.fromContext(message));
} }
private fromContext(msg: string): string {
return this.context ? `[${this.context}] ${msg}` : msg;
}
} }

View file

@ -3,6 +3,10 @@
*/ */
export default interface LoggerService { export default interface LoggerService {
setContext(newContext: string): void;
clearContext(): void;
trace(message: string): void; trace(message: string): void;
debug(message: string): void; debug(message: string): void;

View file

@ -10,6 +10,12 @@ import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { inferGitClient, inferGitApiUrl } from "@bp/service/git/git-util"; import { inferGitClient, inferGitApiUrl } from "@bp/service/git/git-util";
interface Git {
gitClientType: GitClientType;
gitClientApi: GitClient;
gitCli: GitCLIService;
}
/** /**
* Main runner implementation, it implements the core logic flow * Main runner implementation, it implements the core logic flow
*/ */
@ -62,47 +68,73 @@ export default class Runner {
// 3. parse configs // 3. parse configs
this.logger.debug("Parsing configs.."); this.logger.debug("Parsing configs..");
const configs: Configs = await new PullRequestConfigsParser().parseAndValidate(args); const configs: Configs = await new PullRequestConfigsParser().parseAndValidate(args);
const originalPR: GitPullRequest = configs.originalPullRequest; const backportPRs: BackportPullRequest[] = configs.backportPullRequests;
const backportPR: BackportPullRequest = configs.backportPullRequest;
// start local git operations // start local git operations
const git: GitCLIService = new GitCLIService(configs.auth, configs.git); const git: GitCLIService = new GitCLIService(configs.auth, configs.git);
const failures: string[] = [];
// we need sequential backporting as they will operate on the same folder
// avoid cloning the same repo multiple times
for(const pr of backportPRs) {
try {
await this.executeBackport(configs, pr, {
gitClientType: gitClientType,
gitClientApi: gitApi,
gitCli: git,
});
} catch(error) {
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
failures.push(error as string);
}
}
if (failures.length > 0) {
throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`);
}
}
async executeBackport(configs: Configs, backportPR: BackportPullRequest, git: Git): Promise<void> {
this.logger.setContext(backportPR.base);
const originalPR: GitPullRequest = configs.originalPullRequest;
// 4. clone the repository // 4. clone the repository
this.logger.debug("Cloning repo.."); this.logger.debug("Cloning repo..");
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch); await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base);
// 5. create new branch from target one and checkout // 5. create new branch from target one and checkout
this.logger.debug("Creating local branch.."); this.logger.debug("Creating local branch..");
await git.createLocalBranch(configs.folder, backportPR.head); await git.gitCli.createLocalBranch(configs.folder, backportPR.head);
// 6. fetch pull request remote if source owner != target owner or pull request still open // 6. fetch pull request remote if source owner != target owner or pull request still open
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
configs.originalPullRequest.state === "open") { configs.originalPullRequest.state === "open") {
this.logger.debug("Fetching pull request remote.."); this.logger.debug("Fetching pull request remote..");
const prefix = gitClientType === GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab const prefix = git.gitClientType === GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab
await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
} }
// 7. apply all changes to the new branch // 7. apply all changes to the new branch
this.logger.debug("Cherry picking commits.."); this.logger.debug("Cherry picking commits..");
for (const sha of originalPR.commits!) { for (const sha of originalPR.commits!) {
await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption);
} }
if (!configs.dryRun) { if (!configs.dryRun) {
// 8. push the new branch to origin // 8. push the new branch to origin
await git.push(configs.folder, backportPR.head); await git.gitCli.push(configs.folder, backportPR.head);
// 9. create pull request new branch -> target branch (using octokit) // 9. create pull request new branch -> target branch (using octokit)
const prUrl = await gitApi.createPullRequest(backportPR); const prUrl = await git.gitClientApi.createPullRequest(backportPR);
this.logger.info(`Pull request created: ${prUrl}`); this.logger.info(`Pull request created: ${prUrl}`);
} else { } else {
this.logger.warn("Pull request creation and remote push skipped"); this.logger.warn("Pull request creation and remote push skipped");
this.logger.info(`${JSON.stringify(backportPR, null, 2)}`); this.logger.info(`${JSON.stringify(backportPR, null, 2)}`);
} }
}
this.logger.clearContext();
}
} }

View file

@ -433,4 +433,63 @@ describe("cli args parser", () => {
expect(args.squash).toEqual(true); expect(args.squash).toEqual(true);
expectArrayEqual(args.comments!,["first comment", "second comment"]); expectArrayEqual(args.comments!,["first comment", "second comment"]);
}); });
test("valid execution with multiple branches", () => {
addProcessArgs([
"-tb",
"target, old",
"-pr",
"https://localhost/whatever/pulls/1"
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitUser).toEqual(undefined);
expect(args.gitEmail).toEqual(undefined);
expect(args.folder).toEqual(undefined);
expect(args.targetBranch).toEqual("target, old");
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
expect(args.title).toEqual(undefined);
expect(args.body).toEqual(undefined);
expect(args.bodyPrefix).toEqual(undefined);
expect(args.bpBranchName).toEqual(undefined);
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
});
test("invalid execution with empty target branch", () => {
addProcessArgs([
"-tb",
" ",
"-pr",
"https://localhost/whatever/pulls/1"
]);
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
test("invalid execution with missing mandatory target branch", () => {
addProcessArgs([
"-pr",
"https://localhost/whatever/pulls/1"
]);
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
test("invalid execution with missin mandatory pull request", () => {
addProcessArgs([
"-tb",
"target",
]);
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
}); });

View file

@ -262,4 +262,56 @@ describe("gha args parser", () => {
expect(args.squash).toEqual(true); expect(args.squash).toEqual(true);
expectArrayEqual(args.comments!,["first comment", "second comment"]); expectArrayEqual(args.comments!,["first comment", "second comment"]);
}); });
test("valid execution with multiple branches", () => {
spyGetInput({
"target-branch": "target,old",
"pull-request": "https://localhost/whatever/pulls/1"
});
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitUser).toEqual(undefined);
expect(args.gitEmail).toEqual(undefined);
expect(args.folder).toEqual(undefined);
expect(args.targetBranch).toEqual("target,old");
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
expect(args.title).toEqual(undefined);
expect(args.body).toEqual(undefined);
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
});
test("invalid execution with empty target branch", () => {
spyGetInput({
"target-branch": " ",
"pull-request": "https://localhost/whatever/pulls/1"
});
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
test("invalid execution with missing mandatory target branch", () => {
spyGetInput({
"pull-request": "https://localhost/whatever/pulls/1"
});
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
test("invalid execution with missin mandatory pull request", () => {
spyGetInput({
"target-branch": "target,old",
});
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
}); });

View file

@ -0,0 +1,422 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
import { resetProcessArgs } from "../../../support/utils";
import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER, MULT_COMMITS_PR_FIXTURE } from "../../../support/mock/github-data";
import GitHubMapper from "@bp/service/git/github/github-mapper";
import GitHubClient from "@bp/service/git/github/github-client";
jest.spyOn(GitHubMapper.prototype, "mapPullRequest");
jest.spyOn(GitHubClient.prototype, "getPullRequest");
describe("github pull request config parser", () => {
const mergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`;
const multipleCommitsPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MULT_COMMITS_PR_FIXTURE.number}`;
let configParser: PullRequestConfigsParser;
beforeAll(() => {
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3");
});
beforeEach(() => {
// reset process.env variables
resetProcessArgs();
// mock octokit
mockGitHubClient("http://localhost/api/v3");
// create a fresh new instance every time
configParser = new PullRequestConfigsParser();
});
test("multiple backports", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports ignore duplicates", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports with custom branch name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch",
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "custom-branch-v1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch-v2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch-v3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports with multiple custom branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch1, custom-branch2, custom-branch3",
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "custom-branch1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports with incorrect number of bp branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch1, custom-branch2",
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("The number of backport branch names, if provided, must match the number of target branches or just one, provided 2 branch names instead");
});
test("multiple backports and multiple commits", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: multipleCommitsPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: false,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 8632, false);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v1-0404fb9-11da4e3",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v2-0404fb9-11da4e3",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v3-0404fb9-11da4e3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
])
);
});
});

View file

@ -5,7 +5,7 @@ import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types"; import { GitClientType } from "@bp/service/git/git.types";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support"; import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils"; import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils";
import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner, multipleCommitsPullRequestFixture } from "../../../support/mock/github-data"; import { MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, REPO, TARGET_OWNER, MULT_COMMITS_PR_FIXTURE } from "../../../support/mock/github-data";
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import GitHubMapper from "@bp/service/git/github/github-mapper"; import GitHubMapper from "@bp/service/git/github/github-mapper";
import GitHubClient from "@bp/service/git/github/github-client"; import GitHubClient from "@bp/service/git/github/github-client";
@ -13,14 +13,14 @@ import GitHubClient from "@bp/service/git/github/github-client";
const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-simple-pr-merged.json"; const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-simple-pr-merged.json";
const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT = { const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT = {
"targetBranch": "prod", "targetBranch": "prod",
"pullRequest": `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`, "pullRequest": `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`,
}; };
const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-complex-pr-merged.json"; const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-complex-pr-merged.json";
const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = {
"dryRun": false, "dryRun": false,
"auth": "my-auth-token", "auth": "my-auth-token",
"pullRequest": `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`, "pullRequest": `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`,
"targetBranch": "prod", "targetBranch": "prod",
"gitUser": "Me", "gitUser": "Me",
"gitEmail": "me@email.com", "gitEmail": "me@email.com",
@ -39,10 +39,10 @@ jest.spyOn(GitHubClient.prototype, "getPullRequest");
describe("github pull request config parser", () => { describe("github pull request config parser", () => {
const mergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`; const mergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`;
const openPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${openPullRequestFixture.number}`; const openPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${OPEN_PR_FIXTURE.number}`;
const notMergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${notMergedPullRequestFixture.number}`; const notMergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${NOT_MERGED_PR_FIXTURE.number}`;
const multipleCommitsPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${multipleCommitsPullRequestFixture.number}`; const multipleCommitsPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MULT_COMMITS_PR_FIXTURE.number}`;
let argsParser: CLIArgsParser; let argsParser: CLIArgsParser;
let configParser: PullRequestConfigsParser; let configParser: PullRequestConfigsParser;
@ -100,7 +100,6 @@ describe("github pull request config parser", () => {
email: "noreply@github.com" email: "noreply@github.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -128,7 +127,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"] commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",
@ -160,7 +160,6 @@ describe("github pull request config parser", () => {
expect(configs.dryRun).toEqual(true); expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever"); expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual("/tmp/test"); expect(configs.folder).toEqual("/tmp/test");
expect(configs.git).toEqual({ expect(configs.git).toEqual({
user: "GitHub", user: "GitHub",
@ -190,7 +189,6 @@ describe("github pull request config parser", () => {
expect(configs.dryRun).toEqual(true); expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever"); expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({ expect(configs.git).toEqual({
user: "GitHub", user: "GitHub",
email: "noreply@github.com" email: "noreply@github.com"
@ -271,7 +269,6 @@ describe("github pull request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -300,7 +297,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "custom-branch", head: "custom-branch",
@ -314,6 +312,48 @@ describe("github pull request config parser", () => {
}); });
}); });
test("override backport with empty bp branch name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: [],
inheritReviewers: true,
bpBranchName: " "
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
});
test("override backport pr reviewers and assignees", async () => { test("override backport pr reviewers and assignees", async () => {
const args: Args = { const args: Args = {
dryRun: false, dryRun: false,
@ -343,7 +383,6 @@ describe("github pull request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -372,7 +411,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",
@ -415,7 +455,6 @@ describe("github pull request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -444,7 +483,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",
@ -489,7 +529,6 @@ describe("github pull request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -518,7 +557,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",
@ -552,7 +592,6 @@ describe("github pull request config parser", () => {
email: "noreply@github.com" email: "noreply@github.com"
}); });
expect(configs.auth).toEqual(undefined); expect(configs.auth).toEqual(undefined);
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -580,7 +619,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"] commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",
@ -614,7 +654,6 @@ describe("github pull request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual("my-auth-token"); expect(configs.auth).toEqual("my-auth-token");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -643,7 +682,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",
@ -684,7 +724,6 @@ describe("github pull request config parser", () => {
email: "noreply@github.com" email: "noreply@github.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 8632, number: 8632,
@ -712,7 +751,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"] commits: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-0404fb9-11da4e3", head: "bp-prod-0404fb9-11da4e3",
@ -758,7 +798,6 @@ describe("github pull request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 2368, number: 2368,
@ -787,7 +826,8 @@ describe("github pull request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner", owner: "owner",
repo: "reponame", repo: "reponame",
head: "bp-prod-28f63db", head: "bp-prod-28f63db",

View file

@ -0,0 +1,349 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { getAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { MERGED_SQUASHED_MR } from "../../../support/mock/gitlab-data";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper";
jest.spyOn(GitLabMapper.prototype, "mapPullRequest");
jest.spyOn(GitLabClient.prototype, "getPullRequest");
jest.mock("axios", () => {
return {
create: jest.fn(() => ({
get: getAxiosMocked,
})),
};
});
describe("gitlab merge request config parser", () => {
const mergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`;
let configParser: PullRequestConfigsParser;
beforeAll(() => {
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "my.gitlab.host.com");
});
beforeEach(() => {
configParser = new PullRequestConfigsParser();
});
test("multiple backports", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v1-ebb1eca",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v2-ebb1eca",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v3-ebb1eca",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports ignore duplicates", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3, v1",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v1-ebb1eca",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v2-ebb1eca",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v3-ebb1eca",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports with custom branch name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch"
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "custom-branch-v1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom-branch-v2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom-branch-v3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports with multiple custom branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom1, custom2, custom3"
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "custom1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports with incorrect number of bp branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch1, custom-branch2, custom-branch2, custom-branch3, custom-branch4",
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("The number of backport branch names, if provided, must match the number of target branches or just one, provided 4 branch names instead");
});
});

View file

@ -100,7 +100,6 @@ describe("gitlab merge request config parser", () => {
email: "noreply@gitlab.com" email: "noreply@gitlab.com"
}); });
expect(configs.auth).toEqual(undefined); expect(configs.auth).toEqual(undefined);
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -128,7 +127,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -166,7 +166,6 @@ describe("gitlab merge request config parser", () => {
expect(configs.dryRun).toEqual(true); expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever"); expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual("/tmp/test"); expect(configs.folder).toEqual("/tmp/test");
expect(configs.git).toEqual({ expect(configs.git).toEqual({
user: "Gitlab", user: "Gitlab",
@ -196,7 +195,6 @@ describe("gitlab merge request config parser", () => {
expect(configs.dryRun).toEqual(true); expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever"); expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({ expect(configs.git).toEqual({
user: "Gitlab", user: "Gitlab",
email: "noreply@gitlab.com" email: "noreply@gitlab.com"
@ -276,7 +274,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -304,7 +301,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -347,7 +345,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -375,7 +372,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -418,7 +416,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -446,7 +443,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -491,7 +489,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -519,7 +516,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -552,7 +550,6 @@ describe("gitlab merge request config parser", () => {
email: "noreply@gitlab.com" email: "noreply@gitlab.com"
}); });
expect(configs.auth).toEqual(undefined); expect(configs.auth).toEqual(undefined);
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -580,7 +577,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -613,7 +611,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual("my-token"); expect(configs.auth).toEqual("my-token");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -641,7 +638,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",
@ -678,7 +676,6 @@ describe("gitlab merge request config parser", () => {
expect(configs.dryRun).toEqual(true); expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever"); expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({ expect(configs.git).toEqual({
user: "Gitlab", user: "Gitlab",
email: "noreply@gitlab.com" email: "noreply@gitlab.com"
@ -710,7 +707,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 2, nCommits: 2,
commits: ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"] commits: ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-e4dd336-974519f", head: "bp-prod-e4dd336-974519f",
@ -756,7 +754,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com" email: "me@email.com"
}); });
expect(configs.auth).toEqual(""); expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({ expect(configs.originalPullRequest).toEqual({
number: 1, number: 1,
@ -784,7 +781,8 @@ describe("gitlab merge request config parser", () => {
nCommits: 1, nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
}); });
expect(configs.backportPullRequest).toEqual({ expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser", owner: "superuser",
repo: "backporting-example", repo: "backporting-example",
head: "bp-prod-ebb1eca", head: "bp-prod-ebb1eca",

View file

@ -114,4 +114,13 @@ describe("git cli service", () => {
const output = spawnSync("git", ["cherry", "-v"], { cwd }).stdout.toString(); const output = spawnSync("git", ["cherry", "-v"], { cwd }).stdout.toString();
expect(output.includes(expressionToTest)).toBe(false); expect(output.includes(expressionToTest)).toBe(false);
}); });
test("git clone on already created repo", async () => {
await git.clone("remote", cwd, "tbranch");
// use rev-parse to double check the current branch is the expected one
const post = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).stdout.toString().trim();
expect(post).toEqual("tbranch");
});
}); });

View file

@ -1,7 +1,7 @@
import GitClientFactory from "@bp/service/git/git-client-factory"; import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitPullRequest, GitClientType } from "@bp/service/git/git.types"; import { GitPullRequest, GitClientType } from "@bp/service/git/git.types";
import GitHubClient from "@bp/service/git/github/github-client"; import GitHubClient from "@bp/service/git/github/github-client";
import { mergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data"; import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER } from "../../../support/mock/github-data";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support"; import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
describe("github service", () => { describe("github service", () => {
@ -22,7 +22,7 @@ describe("github service", () => {
}); });
test("get pull request: success", async () => { test("get pull request: success", async () => {
const res: GitPullRequest = await gitClient.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number); const res: GitPullRequest = await gitClient.getPullRequest(TARGET_OWNER, REPO, MERGED_PR_FIXTURE.number);
expect(res.sourceRepo).toEqual({ expect(res.sourceRepo).toEqual({
owner: "fork", owner: "fork",
project: "reponame", project: "reponame",

View file

@ -6,7 +6,7 @@ import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../support/utils"; import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../support/utils";
import { mockGitHubClient } from "../../support/mock/git-client-mock-support"; import { mockGitHubClient } from "../../support/mock/git-client-mock-support";
import GitClientFactory from "@bp/service/git/git-client-factory"; import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types"; import { BackportPullRequest, GitClientType } from "@bp/service/git/git.types";
const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME = "./cli-github-runner-pr-merged-with-overrides.json"; const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME = "./cli-github-runner-pr-merged-with-overrides.json";
const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = { const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = {
@ -236,6 +236,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("same owner", async () => { test("same owner", async () => {
@ -281,6 +282,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("closed and not merged pull request", async () => { test("closed and not merged pull request", async () => {
@ -338,6 +340,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("override backporting pr data", async () => { test("override backporting pr data", async () => {
@ -396,6 +399,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("set empty reviewers", async () => { test("set empty reviewers", async () => {
@ -453,6 +457,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("set custom labels with inheritance", async () => { test("set custom labels with inheritance", async () => {
@ -502,6 +507,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("set custom labels without inheritance", async () => { test("set custom labels without inheritance", async () => {
@ -550,6 +556,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("using config file with overrides", async () => { test("using config file with overrides", async () => {
@ -594,6 +601,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
// to check: https://github.com/kiegroup/git-backporting/issues/52 // to check: https://github.com/kiegroup/git-backporting/issues/52
@ -641,6 +649,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("multiple commits pr", async () => { test("multiple commits pr", async () => {
@ -688,6 +697,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("too long bp branch name", async () => { test("too long bp branch name", async () => {
@ -741,6 +751,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("multiple commits pr with different strategy", async () => { test("multiple commits pr with different strategy", async () => {
@ -792,6 +803,7 @@ describe("cli runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("additional pr comments", async () => { test("additional pr comments", async () => {
@ -841,5 +853,255 @@ describe("cli runner", () => {
comments: ["first comment", "second comment"], comments: ["first comment", "second comment"],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("with multiple target branches", async () => {
addProcessArgs([
"-tb",
"v1, v2, v3",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/tmp/folder"
]);
await runner.execute();
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v1-28f63db");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v2-28f63db");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v3-28f63db");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v1-28f63db");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v2-28f63db");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v3-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3);
});
test("with multiple target branches and multiple bp names", async () => {
addProcessArgs([
"-tb",
"v1, v2, v3",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/tmp/folder",
"--bp-branch-name",
"custom1, custom1, custom2, custom3",
]);
await runner.execute();
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom1");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom2");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom3");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom1");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom2");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom1",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom2",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3);
});
test("with multiple target branches and one failure", async () => {
jest.spyOn(GitHubClient.prototype, "createPullRequest").mockImplementation((_backport: BackportPullRequest) => {
throw new Error("Mocked error");
});
addProcessArgs([
"-tb",
"v1, v2, v3",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/tmp/folder",
"--bp-branch-name",
"custom-failure-head",
]);
await expect(() => runner.execute()).rejects.toThrowError("Failure occurred during one of the backports: [Error: Mocked error ; Error: Mocked error ; Error: Mocked error]");
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v1");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v2");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v3");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v1");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v2");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-failure-head-v1",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-failure-head-v2",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-failure-head-v3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toThrowError();
}); });
}); });

View file

@ -128,6 +128,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("closed and not merged pull request", async () => { test("closed and not merged pull request", async () => {
@ -181,6 +182,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("override backporting pr data", async () => { test("override backporting pr data", async () => {
@ -231,6 +233,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("set empty reviewers", async () => { test("set empty reviewers", async () => {
@ -282,6 +285,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("set custom labels with inheritance", async () => { test("set custom labels with inheritance", async () => {
@ -328,6 +332,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("set custom labels without inheritance", async () => { test("set custom labels without inheritance", async () => {
@ -374,6 +379,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("using config file with overrides", async () => { test("using config file with overrides", async () => {
@ -417,6 +423,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
// to check: https://github.com/kiegroup/git-backporting/issues/52 // to check: https://github.com/kiegroup/git-backporting/issues/52
@ -462,6 +469,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("multiple commits pr", async () => { test("multiple commits pr", async () => {
@ -507,6 +515,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("using github api url and different strategy", async () => { test("using github api url and different strategy", async () => {
@ -553,6 +562,7 @@ describe("gha runner", () => {
comments: [], comments: [],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
}); });
test("additional pr comments", async () => { test("additional pr comments", async () => {
@ -598,5 +608,161 @@ describe("gha runner", () => {
comments: ["first comment", "second comment"], comments: ["first comment", "second comment"],
} }
); );
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("with multiple target branches", async () => {
spyGetInput({
"target-branch": "v1, v2, v3",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"folder": "/tmp/folder",
});
await runner.execute();
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v1-28f63db");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v2-28f63db");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v3-28f63db");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v1-28f63db");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v2-28f63db");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v3-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3);
});
test("with multiple target branches and single custom bp branch", async () => {
spyGetInput({
"target-branch": "v1, v2, v3",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"folder": "/tmp/folder",
"bp-branch-name": "custom"
});
await runner.execute();
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v1");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v2");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v3");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v1");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v2");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-v1",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-v2",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-v3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3);
}); });
}); });

View file

@ -1,8 +1,12 @@
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Moctokit } from "@kie/mock-github"; import { Moctokit } from "@kie/mock-github";
import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, multipleCommitsPullRequestFixture, multipleCommitsPullRequestCommits } from "./github-data"; import { TARGET_OWNER, REPO, MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, NOT_FOUND_PR_NUMBER, MULT_COMMITS_PR_FIXTURE, MULT_COMMITS_PR_COMMITS, NEW_PR_URL, NEW_PR_NUMBER } from "./github-data";
import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data"; import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data";
// high number, for each test we are not expecting
// to send more than 3 reqs per api endpoint
const REPEAT = 20;
const logger = LoggerServiceFactory.getLogger(); const logger = LoggerServiceFactory.getLogger();
// AXIOS // AXIOS
@ -94,76 +98,82 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
// valid requests // valid requests
mock.rest.pulls mock.rest.pulls
.get({ .get({
owner: targetOwner, owner: TARGET_OWNER,
repo: repo, repo: REPO,
pull_number: mergedPullRequestFixture.number pull_number: MERGED_PR_FIXTURE.number
}) })
.reply({ .reply({
status: 200, status: 200,
data: mergedPullRequestFixture data: MERGED_PR_FIXTURE
}); });
mock.rest.pulls mock.rest.pulls
.get({ .get({
owner: targetOwner, owner: TARGET_OWNER,
repo: repo, repo: REPO,
pull_number: multipleCommitsPullRequestFixture.number pull_number: MULT_COMMITS_PR_FIXTURE.number
}) })
.reply({ .reply({
status: 200, status: 200,
data: multipleCommitsPullRequestFixture data: MULT_COMMITS_PR_FIXTURE
}); });
mock.rest.pulls mock.rest.pulls
.get({ .get({
owner: targetOwner, owner: TARGET_OWNER,
repo: repo, repo: REPO,
pull_number: openPullRequestFixture.number pull_number: OPEN_PR_FIXTURE.number
}) })
.reply({ .reply({
status: 200, status: 200,
data: openPullRequestFixture data: OPEN_PR_FIXTURE
}); });
mock.rest.pulls mock.rest.pulls
.get({ .get({
owner: targetOwner, owner: TARGET_OWNER,
repo: repo, repo: REPO,
pull_number: notMergedPullRequestFixture.number pull_number: NOT_MERGED_PR_FIXTURE.number
}) })
.reply({ .reply({
status: 200, status: 200,
data: notMergedPullRequestFixture data: NOT_MERGED_PR_FIXTURE
}); });
mock.rest.pulls mock.rest.pulls
.listCommits({ .listCommits({
owner: targetOwner, owner: TARGET_OWNER,
repo: repo, repo: REPO,
pull_number: multipleCommitsPullRequestFixture.number pull_number: MULT_COMMITS_PR_FIXTURE.number
}) })
.reply({ .reply({
status: 200, status: 200,
data: multipleCommitsPullRequestCommits data: MULT_COMMITS_PR_COMMITS
}); });
mock.rest.pulls mock.rest.pulls
.create() .create()
.reply({ .reply({
repeat: REPEAT,
status: 201, status: 201,
data: mergedPullRequestFixture data: {
number: NEW_PR_NUMBER,
html_url: NEW_PR_URL,
}
}); });
mock.rest.pulls mock.rest.pulls
.requestReviewers() .requestReviewers()
.reply({ .reply({
repeat: REPEAT,
status: 201, status: 201,
data: mergedPullRequestFixture data: MERGED_PR_FIXTURE
}); });
mock.rest.issues mock.rest.issues
.addAssignees() .addAssignees()
.reply({ .reply({
repeat: REPEAT,
status: 201, status: 201,
data: {} data: {}
}); });
@ -171,6 +181,7 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
mock.rest.issues mock.rest.issues
.addLabels() .addLabels()
.reply({ .reply({
repeat: REPEAT,
status: 200, status: 200,
data: {} data: {}
}); });
@ -178,6 +189,7 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
mock.rest.issues mock.rest.issues
.createComment() .createComment()
.reply({ .reply({
repeat: REPEAT,
status: 201, status: 201,
data: {} data: {}
}); });
@ -185,11 +197,12 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
// invalid requests // invalid requests
mock.rest.pulls mock.rest.pulls
.get({ .get({
owner: targetOwner, owner: TARGET_OWNER,
repo: repo, repo: REPO,
pull_number: notFoundPullRequestNumber pull_number: NOT_FOUND_PR_NUMBER
}) })
.reply({ .reply({
repeat: REPEAT,
status: 404, status: 404,
data: { data: {
message: "Not found" message: "Not found"

View file

@ -1,9 +1,11 @@
export const targetOwner = "owner"; export const TARGET_OWNER = "owner";
export const sourceOwner = "fork"; export const SOURCE_OWNER = "fork";
export const repo = "reponame"; export const REPO = "reponame";
export const notFoundPullRequestNumber = 1; export const NOT_FOUND_PR_NUMBER = 1;
export const NEW_PR_URL = "new_pr_url";
export const NEW_PR_NUMBER = 9999;
export const mergedPullRequestFixture = { export const MERGED_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/2368", "url": "https://api.github.com/repos/owner/reponame/pulls/2368",
"id": 1137188271, "id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v", "node_id": "PR_kwDOABTq6s5DyB2v",
@ -474,7 +476,7 @@ export const mergedPullRequestFixture = {
"changed_files": 2 "changed_files": 2
}; };
export const openPullRequestFixture = { export const OPEN_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/4444", "url": "https://api.github.com/repos/owner/reponame/pulls/4444",
"id": 1137188271, "id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v", "node_id": "PR_kwDOABTq6s5DyB2v",
@ -898,7 +900,7 @@ export const openPullRequestFixture = {
"changed_files": 2 "changed_files": 2
}; };
export const notMergedPullRequestFixture = { export const NOT_MERGED_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/6666", "url": "https://api.github.com/repos/owner/reponame/pulls/6666",
"id": 1137188271, "id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v", "node_id": "PR_kwDOABTq6s5DyB2v",
@ -1341,7 +1343,7 @@ export const notMergedPullRequestFixture = {
"changed_files": 2 "changed_files": 2
}; };
export const multipleCommitsPullRequestFixture = { export const MULT_COMMITS_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/8632", "url": "https://api.github.com/repos/owner/reponame/pulls/8632",
"id": 1137188271, "id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v", "node_id": "PR_kwDOABTq6s5DyB2v",
@ -1804,7 +1806,7 @@ export const multipleCommitsPullRequestFixture = {
"changed_files": 2 "changed_files": 2
}; };
export const multipleCommitsPullRequestCommits = [ export const MULT_COMMITS_PR_COMMITS = [
{ {
"sha": "0404fb922ab75c3a8aecad5c97d9af388df04695", "sha": "0404fb922ab75c3a8aecad5c97d9af388df04695",
"node_id": "C_kwDOImgs99oAKDA0MDRmYjkyMmFiNzVjM2E4YWVjYWQ1Yzk3ZDlhZjM4OGRmMDQ2OTU", "node_id": "C_kwDOImgs99oAKDA0MDRmYjkyMmFiNzVjM2E4YWVjYWQ1Yzk3ZDlhZjM4OGRmMDQ2OTU",