From a2f3ff013c3976cac4f39858876b4526b201d15f Mon Sep 17 00:00:00 2001 From: Andrea Lamparelli Date: Mon, 10 Jul 2023 16:57:38 +0200 Subject: [PATCH] feat(issue-54): backport pr commits without squash Fix https://github.com/kiegroup/git-backporting/issues/54 --- .github/pull_request_template.md | 33 ++++ .gitignore | 3 + README.md | 105 ++++++----- action.yml | 4 + dist/cli/index.js | 74 ++++++-- dist/gha/index.js | 73 ++++++-- src/service/args/args-parser.ts | 1 + src/service/args/args.types.ts | 1 + src/service/args/cli/cli-args-parser.ts | 2 + src/service/args/gha/gha-args-parser.ts | 1 + .../configs/pullrequest/pr-configs-parser.ts | 2 +- src/service/git/git-client.ts | 6 +- src/service/git/git-mapper.ts | 1 + src/service/git/github/github-client.ts | 26 ++- src/service/git/github/github-mapper.ts | 11 +- src/service/git/gitlab/gitlab-client.ts | 23 ++- src/service/git/gitlab/gitlab-mapper.ts | 15 +- test/service/args/cli/cli-args-parser.test.ts | 37 ++++ test/service/args/gha/gha-args-parser.test.ts | 33 +++- .../github-pr-configs-parser.test.ts | 133 +++++++++++++- .../gitlab-pr-configs-parser.test.ts | 112 +++++++++++- test/service/git/gitlab/gitlab-client.test.ts | 6 +- test/service/runner/cli-github-runner.test.ts | 50 +++++- test/service/runner/cli-gitlab-runner.test.ts | 51 +++++- test/service/runner/gha-github-runner.test.ts | 49 +++++- test/service/runner/gha-gitlab-runner.test.ts | 51 +++++- test/support/mock/git-client-mock-support.ts | 54 +++--- test/support/mock/github-data.ts | 165 +++++++++++++++++- test/support/mock/gitlab-data.ts | 43 +++++ 29 files changed, 1004 insertions(+), 161 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b1b4695..20ad8a1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,3 +18,36 @@ changes that span multiple kiegroup repositories and depend on each other. --> - [ ] Documentation updated if applicable. > **Note:** `dist/cli/index.js` and `dist/gha/index.js` are automatically generated by git hooks and gh workflows. + +
+ +First time here? + + +This project follows [git conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) pattern, therefore the commits should have the following format: + +``` +(): +empty separator line + +empty separator line + +``` + +Where the type must be one of `[build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]` + +> **NOTE**: if you are still in a `work in progress` branch and you want to push your changes remotely, consider adding `--no-verify` for both `commit` and `push`, e.g., `git push origin --no-verify` - this could become useful to push changes where there are still tests failures. Once the pull request is ready, please `amend` the commit and force-push it to keep following the adopted git commit standard. + +
+ +
+ +How to prepare for a new release? + + +There is no need to manually update `package.json` version and `CHANGELOG.md` information. This process has been automated in [Prepare Release](./workflows/prepare-release.yml) *Github* workflow. + +Therefore whenever enough changes are merged into the `main` branch, one of the maintainers will trigger this workflow that will automatically update `version` and `changelog` based on the commits on the git tree. + +More details can be found in [package release](https://github.com/kiegroup/git-backporting/blob/main/README.md#package-release) section of the README. +
\ No newline at end of file diff --git a/.gitignore b/.gitignore index 64a72d6..f3c41a9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ report.json .vscode/ build/ .npmrc + +# temporary files created during tests +*test*.json \ No newline at end of file diff --git a/README.md b/README.md index b3480bd..df2d3d7 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,11 @@ Table of content * **[Who is this tool for](#who-is-this-tool-for)** * **[CLI tool](#cli-tool)** -* **[Supported git services](#supported-git-services)** * **[GitHub action](#github-action)** * **[Future works](#future-works)** -* **[Release](#release)** -* **[Repository migration](#repository-migration)** +* **[Migrating to v4](#migrating-to-v4)** * **[Contributing](#contributing)** +* **[Package release](#package-release)** * **[License](#license)** ## Who is this tool for? @@ -72,6 +71,23 @@ This is the easiest invocation where you let the tool set / compute most of the * Node 16 or higher, more details on Node can be found [here](https://nodejs.org/en). * Git, see [how to install](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) if you need help. +### How it works? + +The simply works in this way: given the provided `pull/merge request` it infers the git client to use (either *Github* or *Gitlab* for now) and it retrieve the corresponding pull request object (original pull/merge request to be backported into another branch). + +After that it clones the corresponding git repository, check out in the provided `target branch` and create a new branch from that (name automatically generated if not provided as option). + +By default the tool will try to cherry-pick the single squashed/merged commit into the newly created branch (please consider using `--no-squash` option if you want to cherry-pick all commits belonging to the provided pull request). + +Based on the original pull request, creates a new one containing the backporting to the target branch. Note that most of these information can be overridden with appropriate CLI options or GHA inputs. + +Right now all commits are cherry-picked using the following git-equivalent command: +```bash +$ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs +``` + +> **NOTE**: If there are any conflicts, the tool will block the process and exit signalling the failure as there are still no ways to interactively resolve them. In these cases a manual cherry-pick is needed, or alternatively users could manually resume the process in the cloned repository (here the user will have to resolve the conflicts, push the branch and create the pull request - all manually). + ### Inputs This tool comes with some inputs that allow users to override the default behavior, here the full list of available inputs: @@ -96,6 +112,7 @@ This tool comes with some inputs that allow users to override the default behavi | Backport Branch Name | --bp-branch-name | N | Name of the backporting pull request branch | bp-{target-branch}-{sha} | | 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 | +| No squash | --no-squash | N | If provided the backporting will try to backport all pull request commits without squashing | false | | Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false | > **NOTE**: `pull request` and `target branch` are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used). @@ -114,7 +131,7 @@ This is an example of a configuration file that can be used. ``` Keep in mind that its structue MUST match the [Args](src/service/args/args.types.ts) interface, which is actually a camel-case version of the CLI options. -## Supported git services +### Supported git services Right now **Git Backporting** supports the following git management services: * ***GITHUB***: Introduced since the first release of this tool (version `1.0.0`). The interaction with this system is performed using [*octokit*](https://octokit.github.io/rest.js) client library. @@ -128,7 +145,7 @@ Right now **Git Backporting** supports the following git management services: This action can be used in any *GitHub* workflow, below you can find a simple example of manually triggered workflow backporting a specific pull request (provided as input). ```yml -name: Pull Request Backporting using BPer +name: Pull Request Backporting using Git Backporting on: workflow_dispatch: @@ -166,7 +183,7 @@ You can also use this action with other events - you'll just need to specify `ta For example, this configuration creates a pull request against branch `v1` once the current one is merged, provided that the label `backport-v1` is applied: ```yaml -name: Pull Request Backporting using BPer +name: Pull Request Backporting using Git Backporting on: pull_request_target: @@ -203,49 +220,14 @@ For a complete description of all inputs see [Inputs section](#inputs). ## Future works -**BPer** is still in development mode, this means that there are still many future works and extension. I'll try to summarize the most important ones: +**Git Backporting** is still in development mode, this means that there are still many future works and extension that can be implemented. I'll try to summarize the most important ones: -- Provide a way to backport single commit too (or a set of them), even if no original pull request is present. +- Provide a way to backport single commit (or a set of them) if no original pull request is present. - Integrate this tool with other git management services (like Bitbucket) to make it as generic as possible. - Integrate it into other CI/CD services like gitlab CI. - Provide some reusable *GitHub* workflows. -## Release - -The release of this package is entirely based on [release-it](https://github.com/release-it/release-it) tool. I created some useful scripts that can make the release itself quite easy. - - -### Automated release - -The first step is to prepare the changes for the next release, this is done by running: - -```bash -$ npm run release:prepare:all -``` - -> NOTE: running locally this requires `npm login`, please consider using `.github/workflows/prepare-release.yml` if you don't have permission on the npm package. - -This script performs the following steps: - 1. Automatically computes the next version based on the last commits - 2. Create a new branch `release/v${computed_version}` - 3. Apply all changes, like version and changelog upgrade - 4. Commit those changes: `chore: release v${compute_version}` - -After that you should just push the new branch and open the pull request. -> NOTE: if you don't want to run this preparation from you local environment, there is already a workflow that does all these steps, including the pull request. See [Prepare release](.github/workflows/prepare-release.yml) workflow. - -Once the release preparion pull request got merged, you can run [Release package](.github/workflows/release.yml) workflow that automatically performs the release itself, including npm publishing, git tag and github release. - -### Manual release - -In case we would like to perform a manual release, it would be enough to open a pull request changing the following items: -- Package version inside the `package.json` -- Provide exhaustive changelog information inside `CHANGELOG.md` -- Commit like `chore: release v` - -Once the release preparion pull request got merged, run [Release package](.github/workflows/release.yml) workflow. - -## Repository Migration +## Migrating to v4 From version `v4.0.0` the project has been moved under [@kiegroup](https://github.com/kiegroup) organization. During this migration we changed some things that you should be aware of. I'll try to summarize them in the following table: @@ -284,6 +266,41 @@ Every change must be submitted through a *GitHub* pull request (PR). Backporting **Hint**: if you are still in a `work in progress` branch and you want to push your changes remotely, consider adding `--no-verify` for both `commit` and `push`, e.g., `git push origin --no-verify` +## Package release + +The release of this package is entirely based on [release-it](https://github.com/release-it/release-it) tool. I created some useful scripts that can make the release itself quite easy. + + +### Automated release + +The first step is to prepare the changes for the next release, this is done by running: + +```bash +$ npm run release:prepare:all +``` + +> NOTE: running locally this requires `npm login`, please consider using `.github/workflows/prepare-release.yml` if you don't have permission on the npm package. + +This script performs the following steps: + 1. Automatically computes the next version based on the last commits + 2. Create a new branch `release/v${computed_version}` + 3. Apply all changes, like version and changelog upgrade + 4. Commit those changes: `chore: release v${compute_version}` + +After that you should just push the new branch and open the pull request. +> NOTE: if you don't want to run this preparation from you local environment, there is already a workflow that does all these steps, including the pull request. See [Prepare release](.github/workflows/prepare-release.yml) workflow. + +Once the release preparion pull request got merged, you can run [Release package](.github/workflows/release.yml) workflow that automatically performs the release itself, including npm publishing, git tag and github release. + +### Manual release + +In case we would like to perform a manual release, it would be enough to open a pull request changing the following items: +- Package version inside the `package.json` +- Provide exhaustive changelog information inside `CHANGELOG.md` +- Commit like `chore: release v` + +Once the release preparion pull request got merged, run [Release package](.github/workflows/release.yml) workflow. + ## License Backporting (BPer) open source project is licensed under the [MIT](./LICENSE) license. \ No newline at end of file diff --git a/action.yml b/action.yml index 43c2fcc..d774a98 100644 --- a/action.yml +++ b/action.yml @@ -55,6 +55,10 @@ inputs: description: "If true the backported pull request will inherit labels from the original one" required: false default: "false" + no-squash: + description: "If set to true the tool will backport all commits as part of the pull request instead of the suqashed one" + required: false + default: "false" runs: using: node16 diff --git a/dist/cli/index.js b/dist/cli/index.js index 5818d2a..8f41e23 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -60,6 +60,7 @@ class ArgsParser { inheritReviewers: this.getOrDefault(args.inheritReviewers, true), labels: this.getOrDefault(args.labels, []), inheritLabels: this.getOrDefault(args.inheritLabels, false), + squash: this.getOrDefault(args.squash, true), }; } } @@ -185,6 +186,7 @@ class CLIArgsParser extends args_parser_1.default { .option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request") .option("--labels ", "comma separated list of labels to be assigned to the backported pull request", args_utils_1.getAsCommaSeparatedList) .option("--inherit-labels", "if true the backported pull request will inherit labels from the original one") + .option("--no-squash", "if provided the tool will backport all commits as part of the pull request") .option("-cf, --config-file ", "configuration file containing all valid options, the json must match Args interface"); } readArgs() { @@ -214,6 +216,7 @@ class CLIArgsParser extends args_parser_1.default { inheritReviewers: opts.inheritReviewers, labels: opts.labels, inheritLabels: opts.inheritLabels, + squash: opts.squash, }; } return args; @@ -280,7 +283,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { async parse(args) { let pr; try { - pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest); + pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash); } catch (error) { this.logger.error("Something went wrong retrieving pull request"); @@ -619,18 +622,33 @@ class GitHubClient { getDefaultGitEmail() { return "noreply@github.com"; } - async getPullRequest(owner, repo, prNumber) { + async getPullRequest(owner, repo, prNumber, squash = true) { this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`); const { data } = await this.octokit.rest.pulls.get({ owner: owner, repo: repo, - pull_number: prNumber + pull_number: prNumber, }); - return this.mapper.mapPullRequest(data); + const commits = []; + if (!squash) { + // fetch all commits + try { + const { data } = await this.octokit.rest.pulls.listCommits({ + owner: owner, + repo: repo, + pull_number: prNumber, + }); + commits.push(...data.map(c => c.sha)); + } + catch (error) { + throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`); + } + } + return this.mapper.mapPullRequest(data, commits); } - async getPullRequestFromUrl(prUrl) { + async getPullRequestFromUrl(prUrl, squash = true) { const { owner, project, id } = this.extractPullRequestData(prUrl); - return this.getPullRequest(owner, project, id); + return this.getPullRequest(owner, project, id, squash); } // WRITE async createPullRequest(backport) { @@ -724,7 +742,7 @@ class GitHubMapper { return git_types_1.GitRepoState.CLOSED; } } - async mapPullRequest(pr) { + async mapPullRequest(pr, commits) { return { number: pr.number, author: pr.user.login, @@ -741,10 +759,14 @@ class GitHubMapper { sourceRepo: await this.mapSourceRepo(pr), targetRepo: await this.mapTargetRepo(pr), nCommits: pr.commits, - // if pr is open use latest commit sha otherwise use merge_commit_sha - commits: pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha] + // if commits is provided use them, otherwise fetch the single sha representing the whole pr + commits: (commits && commits.length > 0) ? commits : this.getSha(pr), }; } + getSha(pr) { + // if pr is open use latest commit sha otherwise use merge_commit_sha + return pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha]; + } async mapSourceRepo(pr) { return Promise.resolve({ owner: pr.head.repo.full_name.split("/")[0], @@ -835,14 +857,26 @@ class GitLabClient { } // READ // example: /api/v4/projects/%2Fbackporting-example/merge_requests/1 - async getPullRequest(namespace, repo, mrNumber) { + async getPullRequest(namespace, repo, mrNumber, squash = true) { const projectId = this.getProjectId(namespace, repo); const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`); - return this.mapper.mapPullRequest(data); + const commits = []; + if (!squash) { + // fetch all commits + try { + const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}/commits`); + // gitlab returns them in reverse order + commits.push(...data.map(c => c.id).reverse()); + } + catch (error) { + throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`); + } + } + return this.mapper.mapPullRequest(data, commits); } - getPullRequestFromUrl(mrUrl) { + getPullRequestFromUrl(mrUrl, squash = true) { const { namespace, project, id } = this.extractMergeRequestData(mrUrl); - return this.getPullRequest(namespace, project, id); + return this.getPullRequest(namespace, project, id, squash); } // WRITE async createPullRequest(backport) { @@ -983,7 +1017,7 @@ class GitLabMapper { return git_types_1.GitRepoState.LOCKED; } } - async mapPullRequest(mr) { + async mapPullRequest(mr, commits) { return { number: mr.iid, author: mr.author.username, @@ -999,12 +1033,16 @@ class GitLabMapper { labels: mr.labels ?? [], sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), - nCommits: 1, - // if mr is merged, use merge_commit_sha otherwise use sha - // what is the difference between sha and diff_refs.head_sha? - commits: this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.sha] + // if commits list is provided use that as source + nCommits: (commits && commits.length > 1) ? commits.length : 1, + commits: (commits && commits.length > 1) ? commits : this.getSha(mr) }; } + getSha(mr) { + // if mr is merged, use merge_commit_sha otherwise use sha + // what is the difference between sha and diff_refs.head_sha? + return this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.sha]; + } async mapSourceRepo(mr) { const project = await this.getProject(mr.source_project_id); return { diff --git a/dist/gha/index.js b/dist/gha/index.js index 20a9a92..aa36c9c 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -60,6 +60,7 @@ class ArgsParser { inheritReviewers: this.getOrDefault(args.inheritReviewers, true), labels: this.getOrDefault(args.labels, []), inheritLabels: this.getOrDefault(args.inheritLabels, false), + squash: this.getOrDefault(args.squash, true), }; } } @@ -188,6 +189,7 @@ class GHAArgsParser extends args_parser_1.default { inheritReviewers: !(0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("no-inherit-reviewers")), labels: (0, args_utils_1.getAsCommaSeparatedList)((0, core_1.getInput)("labels")), inheritLabels: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("inherit-labels")), + squash: !(0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("no-squash")), }; } return args; @@ -254,7 +256,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { async parse(args) { let pr; try { - pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest); + pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash); } catch (error) { this.logger.error("Something went wrong retrieving pull request"); @@ -593,18 +595,33 @@ class GitHubClient { getDefaultGitEmail() { return "noreply@github.com"; } - async getPullRequest(owner, repo, prNumber) { + async getPullRequest(owner, repo, prNumber, squash = true) { this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`); const { data } = await this.octokit.rest.pulls.get({ owner: owner, repo: repo, - pull_number: prNumber + pull_number: prNumber, }); - return this.mapper.mapPullRequest(data); + const commits = []; + if (!squash) { + // fetch all commits + try { + const { data } = await this.octokit.rest.pulls.listCommits({ + owner: owner, + repo: repo, + pull_number: prNumber, + }); + commits.push(...data.map(c => c.sha)); + } + catch (error) { + throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`); + } + } + return this.mapper.mapPullRequest(data, commits); } - async getPullRequestFromUrl(prUrl) { + async getPullRequestFromUrl(prUrl, squash = true) { const { owner, project, id } = this.extractPullRequestData(prUrl); - return this.getPullRequest(owner, project, id); + return this.getPullRequest(owner, project, id, squash); } // WRITE async createPullRequest(backport) { @@ -698,7 +715,7 @@ class GitHubMapper { return git_types_1.GitRepoState.CLOSED; } } - async mapPullRequest(pr) { + async mapPullRequest(pr, commits) { return { number: pr.number, author: pr.user.login, @@ -715,10 +732,14 @@ class GitHubMapper { sourceRepo: await this.mapSourceRepo(pr), targetRepo: await this.mapTargetRepo(pr), nCommits: pr.commits, - // if pr is open use latest commit sha otherwise use merge_commit_sha - commits: pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha] + // if commits is provided use them, otherwise fetch the single sha representing the whole pr + commits: (commits && commits.length > 0) ? commits : this.getSha(pr), }; } + getSha(pr) { + // if pr is open use latest commit sha otherwise use merge_commit_sha + return pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha]; + } async mapSourceRepo(pr) { return Promise.resolve({ owner: pr.head.repo.full_name.split("/")[0], @@ -809,14 +830,26 @@ class GitLabClient { } // READ // example: /api/v4/projects/%2Fbackporting-example/merge_requests/1 - async getPullRequest(namespace, repo, mrNumber) { + async getPullRequest(namespace, repo, mrNumber, squash = true) { const projectId = this.getProjectId(namespace, repo); const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`); - return this.mapper.mapPullRequest(data); + const commits = []; + if (!squash) { + // fetch all commits + try { + const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}/commits`); + // gitlab returns them in reverse order + commits.push(...data.map(c => c.id).reverse()); + } + catch (error) { + throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`); + } + } + return this.mapper.mapPullRequest(data, commits); } - getPullRequestFromUrl(mrUrl) { + getPullRequestFromUrl(mrUrl, squash = true) { const { namespace, project, id } = this.extractMergeRequestData(mrUrl); - return this.getPullRequest(namespace, project, id); + return this.getPullRequest(namespace, project, id, squash); } // WRITE async createPullRequest(backport) { @@ -957,7 +990,7 @@ class GitLabMapper { return git_types_1.GitRepoState.LOCKED; } } - async mapPullRequest(mr) { + async mapPullRequest(mr, commits) { return { number: mr.iid, author: mr.author.username, @@ -973,12 +1006,16 @@ class GitLabMapper { labels: mr.labels ?? [], sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), - nCommits: 1, - // if mr is merged, use merge_commit_sha otherwise use sha - // what is the difference between sha and diff_refs.head_sha? - commits: this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.sha] + // if commits list is provided use that as source + nCommits: (commits && commits.length > 1) ? commits.length : 1, + commits: (commits && commits.length > 1) ? commits : this.getSha(mr) }; } + getSha(mr) { + // if mr is merged, use merge_commit_sha otherwise use sha + // what is the difference between sha and diff_refs.head_sha? + return this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.sha]; + } async mapSourceRepo(mr) { const project = await this.getProject(mr.source_project_id); return { diff --git a/src/service/args/args-parser.ts b/src/service/args/args-parser.ts index c36c35c..265db2b 100644 --- a/src/service/args/args-parser.ts +++ b/src/service/args/args-parser.ts @@ -38,6 +38,7 @@ export default abstract class ArgsParser { inheritReviewers: this.getOrDefault(args.inheritReviewers, true), labels: this.getOrDefault(args.labels, []), inheritLabels: this.getOrDefault(args.inheritLabels, false), + squash: this.getOrDefault(args.squash, true), }; } } \ No newline at end of file diff --git a/src/service/args/args.types.ts b/src/service/args/args.types.ts index e1ada50..6596bc3 100644 --- a/src/service/args/args.types.ts +++ b/src/service/args/args.types.ts @@ -18,4 +18,5 @@ export interface Args { inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers from original pr labels?: string[], // backport pr labels inheritLabels?: boolean, // if true inherit labels from original pr + squash?: boolean, // if false use squashed/merged commit otherwise backport all commits as part of the pr } \ No newline at end of file diff --git a/src/service/args/cli/cli-args-parser.ts b/src/service/args/cli/cli-args-parser.ts index e3297d1..f37eee4 100644 --- a/src/service/args/cli/cli-args-parser.ts +++ b/src/service/args/cli/cli-args-parser.ts @@ -26,6 +26,7 @@ export default class CLIArgsParser extends ArgsParser { .option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request") .option("--labels ", "comma separated list of labels to be assigned to the backported pull request", getAsCommaSeparatedList) .option("--inherit-labels", "if true the backported pull request will inherit labels from the original one") + .option("--no-squash", "if provided the tool will backport all commits as part of the pull request") .option("-cf, --config-file ", "configuration file containing all valid options, the json must match Args interface"); } @@ -56,6 +57,7 @@ export default class CLIArgsParser extends ArgsParser { inheritReviewers: opts.inheritReviewers, labels: opts.labels, inheritLabels: opts.inheritLabels, + squash: opts.squash, }; } diff --git a/src/service/args/gha/gha-args-parser.ts b/src/service/args/gha/gha-args-parser.ts index d06e40e..18a6b3c 100644 --- a/src/service/args/gha/gha-args-parser.ts +++ b/src/service/args/gha/gha-args-parser.ts @@ -29,6 +29,7 @@ export default class GHAArgsParser extends ArgsParser { inheritReviewers: !getAsBooleanOrDefault(getInput("no-inherit-reviewers")), labels: getAsCommaSeparatedList(getInput("labels")), inheritLabels: getAsBooleanOrDefault(getInput("inherit-labels")), + squash: !getAsBooleanOrDefault(getInput("no-squash")), }; } diff --git a/src/service/configs/pullrequest/pr-configs-parser.ts b/src/service/configs/pullrequest/pr-configs-parser.ts index 6ca50da..057912b 100644 --- a/src/service/configs/pullrequest/pr-configs-parser.ts +++ b/src/service/configs/pullrequest/pr-configs-parser.ts @@ -17,7 +17,7 @@ export default class PullRequestConfigsParser extends ConfigsParser { public async parse(args: Args): Promise { let pr: GitPullRequest; try { - pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest); + pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash!); } catch(error) { this.logger.error("Something went wrong retrieving pull request"); throw error; diff --git a/src/service/git/git-client.ts b/src/service/git/git-client.ts index c1ff0ea..b14f603 100644 --- a/src/service/git/git-client.ts +++ b/src/service/git/git-client.ts @@ -17,16 +17,18 @@ import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types"; * @param owner repository's owner * @param repo repository's name * @param prNumber pull request number + * @param squash if true keep just one single commit, otherwise get the full list * @returns {Promise} */ - getPullRequest(owner: string, repo: string, prNumber: number): Promise; + getPullRequest(owner: string, repo: string, prNumber: number, squash: boolean): Promise; /** * Get a pull request object from the underneath git service * @param prUrl pull request html url + * @param squash if true keep just one single commit, otherwise get the full list * @returns {Promise} */ - getPullRequestFromUrl(prUrl: string): Promise; + getPullRequestFromUrl(prUrl: string, squash: boolean): Promise; // WRITE diff --git a/src/service/git/git-mapper.ts b/src/service/git/git-mapper.ts index 7ae88c2..5658f14 100644 --- a/src/service/git/git-mapper.ts +++ b/src/service/git/git-mapper.ts @@ -10,6 +10,7 @@ export default interface GitResponseMapper { mapPullRequest( pr: PR, + commits?: string[], ): Promise; mapGitState(state: S): GitRepoState; diff --git a/src/service/git/github/github-client.ts b/src/service/git/github/github-client.ts index c7d8f81..af60837 100644 --- a/src/service/git/github/github-client.ts +++ b/src/service/git/github/github-client.ts @@ -31,20 +31,36 @@ export default class GitHubClient implements GitClient { return "noreply@github.com"; } - async getPullRequest(owner: string, repo: string, prNumber: number): Promise { + async getPullRequest(owner: string, repo: string, prNumber: number, squash = true): Promise { this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`); const { data } = await this.octokit.rest.pulls.get({ owner: owner, repo: repo, - pull_number: prNumber + pull_number: prNumber, }); - return this.mapper.mapPullRequest(data as PullRequest); + const commits: string[] = []; + if (!squash) { + // fetch all commits + try { + const { data } = await this.octokit.rest.pulls.listCommits({ + owner: owner, + repo: repo, + pull_number: prNumber, + }); + + commits.push(...data.map(c => c.sha)); + } catch(error) { + throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`); + } + } + + return this.mapper.mapPullRequest(data as PullRequest, commits); } - async getPullRequestFromUrl(prUrl: string): Promise { + async getPullRequestFromUrl(prUrl: string, squash = true): Promise { const { owner, project, id } = this.extractPullRequestData(prUrl); - return this.getPullRequest(owner, project, id); + return this.getPullRequest(owner, project, id, squash); } // WRITE diff --git a/src/service/git/github/github-mapper.ts b/src/service/git/github/github-mapper.ts index caa1bb1..a79cb3d 100644 --- a/src/service/git/github/github-mapper.ts +++ b/src/service/git/github/github-mapper.ts @@ -13,7 +13,7 @@ export default class GitHubMapper implements GitResponseMapper { + async mapPullRequest(pr: PullRequest, commits?: string[]): Promise { return { number: pr.number, author: pr.user.login, @@ -30,11 +30,16 @@ export default class GitHubMapper implements GitResponseMapper 0) ? commits : this.getSha(pr), }; } + private getSha(pr: PullRequest) { + // if pr is open use latest commit sha otherwise use merge_commit_sha + return pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha as string]; + } + async mapSourceRepo(pr: PullRequest): Promise { return Promise.resolve({ owner: pr.head.repo.full_name.split("/")[0], diff --git a/src/service/git/gitlab/gitlab-client.ts b/src/service/git/gitlab/gitlab-client.ts index 427a0ac..af091ba 100644 --- a/src/service/git/gitlab/gitlab-client.ts +++ b/src/service/git/gitlab/gitlab-client.ts @@ -2,7 +2,7 @@ import LoggerService from "@bp/service/logger/logger-service"; import GitClient from "@bp/service/git/git-client"; import { GitPullRequest, BackportPullRequest } from "@bp/service/git/git.types"; import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; -import { MergeRequestSchema, UserSchema } from "@gitbeaker/rest"; +import { CommitSchema, MergeRequestSchema, UserSchema } from "@gitbeaker/rest"; import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper"; import axios, { Axios } from "axios"; import https from "https"; @@ -41,16 +41,29 @@ export default class GitLabClient implements GitClient { // READ // example: /api/v4/projects/%2Fbackporting-example/merge_requests/1 - async getPullRequest(namespace: string, repo: string, mrNumber: number): Promise { + async getPullRequest(namespace: string, repo: string, mrNumber: number, squash = true): Promise { const projectId = this.getProjectId(namespace, repo); const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`); - return this.mapper.mapPullRequest(data as MergeRequestSchema); + const commits: string[] = []; + if (!squash) { + // fetch all commits + try { + const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}/commits`); + + // gitlab returns them in reverse order + commits.push(...(data as CommitSchema[]).map(c => c.id).reverse()); + } catch(error) { + throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`); + } + } + + return this.mapper.mapPullRequest(data as MergeRequestSchema, commits); } - getPullRequestFromUrl(mrUrl: string): Promise { + getPullRequestFromUrl(mrUrl: string, squash = true): Promise { const { namespace, project, id } = this.extractMergeRequestData(mrUrl); - return this.getPullRequest(namespace, project, id); + return this.getPullRequest(namespace, project, id, squash); } // WRITE diff --git a/src/service/git/gitlab/gitlab-mapper.ts b/src/service/git/gitlab/gitlab-mapper.ts index cc917a2..5765ba6 100644 --- a/src/service/git/gitlab/gitlab-mapper.ts +++ b/src/service/git/gitlab/gitlab-mapper.ts @@ -24,7 +24,7 @@ export default class GitLabMapper implements GitResponseMapper { + async mapPullRequest(mr: MergeRequestSchema, commits?: string[]): Promise { return { number: mr.iid, author: mr.author.username, @@ -40,12 +40,17 @@ export default class GitLabMapper implements GitResponseMapper 1) ? commits.length : 1, + commits: (commits && commits.length > 1) ? commits : this.getSha(mr) }; } + + private getSha(mr: MergeRequestSchema) { + // if mr is merged, use merge_commit_sha otherwise use sha + // what is the difference between sha and diff_refs.head_sha? + return this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha as string] : [mr.sha]; + } async mapSourceRepo(mr: MergeRequestSchema): Promise { const project: ProjectSchema = await this.getProject(mr.source_project_id); diff --git a/test/service/args/cli/cli-args-parser.test.ts b/test/service/args/cli/cli-args-parser.test.ts index d7b6338..c020fab 100644 --- a/test/service/args/cli/cli-args-parser.test.ts +++ b/test/service/args/cli/cli-args-parser.test.ts @@ -76,6 +76,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expect(args.labels).toEqual([]); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("with config file [default, short]", () => { @@ -101,6 +102,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expect(args.labels).toEqual([]); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("valid execution [default, long]", () => { @@ -128,6 +130,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expect(args.labels).toEqual([]); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("with config file [default, long]", () => { @@ -153,6 +156,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expect(args.labels).toEqual([]); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("valid execution [override, short]", () => { @@ -187,6 +191,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expect(args.labels).toEqual([]); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("valid execution [override, long]", () => { @@ -237,6 +242,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(false); expectArrayEqual(args.labels!, ["cherry-pick :cherries:", "another spaced label"]); expect(args.inheritLabels).toEqual(true); + expect(args.squash).toEqual(true); }); test("override using config file", () => { @@ -262,6 +268,7 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]); expect(args.inheritLabels).toEqual(true); + expect(args.squash).toEqual(true); }); test("ignore custom option when config file is set", () => { @@ -314,5 +321,35 @@ describe("cli args parser", () => { expect(args.inheritReviewers).toEqual(true); expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]); expect(args.inheritLabels).toEqual(true); + expect(args.squash).toEqual(true); + }); + + test("override squash to false", () => { + addProcessArgs([ + "--target-branch", + "target", + "--pull-request", + "https://localhost/whatever/pulls/1", + "--no-squash" + ]); + + 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"); + 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(false); }); }); \ No newline at end of file diff --git a/test/service/args/gha/gha-args-parser.test.ts b/test/service/args/gha/gha-args-parser.test.ts index 0d42806..61a04bc 100644 --- a/test/service/args/gha/gha-args-parser.test.ts +++ b/test/service/args/gha/gha-args-parser.test.ts @@ -48,10 +48,6 @@ describe("gha args parser", () => { parser = new GHAArgsParser(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - test("valid execution [default]", () => { spyGetInput({ "target-branch": "target", @@ -73,6 +69,7 @@ describe("gha args parser", () => { expect(args.inheritReviewers).toEqual(true); expect(args.labels).toEqual([]); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("valid execution [override]", () => { @@ -111,6 +108,7 @@ describe("gha args parser", () => { expect(args.inheritReviewers).toEqual(false); expectArrayEqual(args.labels!, ["cherry-pick :cherries:", "another spaced label"]); expect(args.inheritLabels).toEqual(true); + expect(args.squash).toEqual(true); }); test("using config file", () => { @@ -135,6 +133,7 @@ describe("gha args parser", () => { expect(args.inheritReviewers).toEqual(true); expectArrayEqual(args.labels!, []); expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); }); test("ignore custom options when using config file", () => { @@ -174,5 +173,31 @@ describe("gha args parser", () => { expect(args.inheritReviewers).toEqual(true); expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]); expect(args.inheritLabels).toEqual(true); + expect(args.squash).toEqual(true); + }); + + test("override squash to false", () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://localhost/whatever/pulls/1", + "no-squash": "true", + }); + + 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"); + 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(false); }); }); \ No newline at end of file diff --git a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts index 4d5b1ba..e11097f 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts @@ -5,8 +5,10 @@ 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 { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils"; -import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data"; +import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner, multipleCommitsPullRequestFixture } from "../../../support/mock/github-data"; import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; +import GitHubMapper from "@bp/service/git/github/github-mapper"; +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 = { @@ -32,11 +34,15 @@ const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "inheritLabels": true, }; +jest.spyOn(GitHubMapper.prototype, "mapPullRequest"); +jest.spyOn(GitHubClient.prototype, "getPullRequest"); + describe("github pull request config parser", () => { const mergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`; const openPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${openPullRequestFixture.number}`; const notMergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${notMergedPullRequestFixture.number}`; + const multipleCommitsPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${multipleCommitsPullRequestFixture.number}`; let argsParser: CLIArgsParser; let configParser: PullRequestConfigsParser; @@ -67,10 +73,6 @@ describe("github pull request config parser", () => { configParser = new PullRequestConfigsParser(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - test("parse configs from pull request", async () => { const args: Args = { dryRun: false, @@ -86,6 +88,11 @@ describe("github pull request config parser", () => { 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.git).toEqual({ user: "GitHub", @@ -184,6 +191,11 @@ describe("github pull request config parser", () => { const configs: Configs = await configParser.parseAndValidate(args); + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 4444, true); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); expect(configs.targetBranch).toEqual("prod"); @@ -234,10 +246,9 @@ describe("github pull request config parser", () => { inheritReviewers: true, }; - expect(async () => await configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); + await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); }); - test("override backport pr data inheriting reviewers", async () => { const args: Args = { dryRun: false, @@ -256,6 +267,11 @@ describe("github pull request config parser", () => { 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.git).toEqual({ user: "Me", @@ -332,6 +348,11 @@ describe("github pull request config parser", () => { 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.git).toEqual({ user: "Me", @@ -408,6 +429,11 @@ describe("github pull request config parser", () => { 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.git).toEqual({ user: "Me", @@ -486,6 +512,11 @@ describe("github pull request config parser", () => { 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.git).toEqual({ user: "Me", @@ -553,6 +584,11 @@ describe("github pull request config parser", () => { const args: Args = argsParser.parse(); 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.git).toEqual({ user: "GitHub", @@ -619,6 +655,11 @@ describe("github pull request config parser", () => { const args: Args = argsParser.parse(); 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.git).toEqual({ user: "Me", @@ -676,4 +717,82 @@ describe("github pull request config parser", () => { bpBranchName: undefined, }); }); + + test("parse configs from pull request without squashing with multiple commits", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: multipleCommitsPRUrl, + targetBranch: "prod", + 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.targetBranch).toEqual("prod"); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.originalPullRequest).toEqual({ + number: 8632, + author: "gh-user", + url: "https://api.github.com/repos/owner/reponame/pulls/8632", + htmlUrl: "https://github.com/owner/reponame/pull/8632", + state: "closed", + merged: true, + mergedBy: "that-s-a-user", + title: "PR Title", + body: "Please review and merge", + reviewers: ["requested-gh-user", "gh-user"], + assignees: [], + labels: [], + targetRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + sourceRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + nCommits: 2, + commits: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"] + }); + expect(configs.backportPullRequest).toEqual({ + author: "GitHub", + url: undefined, + htmlUrl: undefined, + title: "[prod] 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: [], + targetRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + sourceRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + bpBranchName: undefined, + }); + }); }); \ No newline at end of file diff --git a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts index 9e47f1b..f180a78 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts @@ -7,6 +7,8 @@ import { getAxiosMocked } from "../../../support/mock/git-client-mock-support"; import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR } from "../../../support/mock/gitlab-data"; import GHAArgsParser from "@bp/service/args/gha/gha-args-parser"; import { createTestFile, removeTestFile, spyGetInput } from "../../../support/utils"; +import GitLabClient from "@bp/service/git/gitlab/gitlab-client"; +import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper"; const GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./gitlab-pr-configs-parser-simple-pr-merged.json"; const GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT = { @@ -32,6 +34,8 @@ const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "inheritLabels": true, }; +jest.spyOn(GitLabMapper.prototype, "mapPullRequest"); +jest.spyOn(GitLabClient.prototype, "getPullRequest"); jest.mock("axios", () => { return { @@ -70,10 +74,6 @@ describe("gitlab merge request config parser", () => { configParser = new PullRequestConfigsParser(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - test("parse configs from merge request", async () => { const args: Args = { dryRun: false, @@ -89,6 +89,11 @@ describe("gitlab merge request config parser", () => { 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.git).toEqual({ user: "Gitlab", @@ -163,6 +168,11 @@ describe("gitlab merge request config parser", () => { 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(true); expect(configs.auth).toEqual("whatever"); expect(configs.targetBranch).toEqual("prod"); @@ -188,6 +198,11 @@ describe("gitlab merge request config parser", () => { const configs: Configs = await configParser.parseAndValidate(args); + expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 2, true); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); expect(configs.targetBranch).toEqual("prod"); @@ -238,7 +253,7 @@ describe("gitlab merge request config parser", () => { inheritReviewers: true, }; - expect(async () => await configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); + await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); }); test("override backport pr data inheriting reviewers", async () => { @@ -259,6 +274,11 @@ describe("gitlab merge request config parser", () => { 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.git).toEqual({ user: "Me", @@ -334,6 +354,11 @@ describe("gitlab merge request config parser", () => { 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.git).toEqual({ user: "Me", @@ -409,6 +434,11 @@ describe("gitlab merge request config parser", () => { 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.git).toEqual({ user: "Me", @@ -486,6 +516,11 @@ describe("gitlab merge request config parser", () => { 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.git).toEqual({ user: "Me", @@ -551,6 +586,11 @@ describe("gitlab merge request config parser", () => { const args: Args = argsParser.parse(); 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.git).toEqual({ user: "Gitlab", @@ -616,6 +656,11 @@ describe("gitlab merge request config parser", () => { const args: Args = argsParser.parse(); 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.git).toEqual({ user: "Me", @@ -672,4 +717,61 @@ describe("gitlab merge request config parser", () => { bpBranchName: undefined, }); }); + + test("still open pull request without squash", async () => { + const args: Args = { + dryRun: true, + auth: "whatever", + pullRequest: openPRUrl, + targetBranch: "prod", + gitUser: "Gitlab", + gitEmail: "noreply@gitlab.com", + reviewers: [], + assignees: [], + inheritReviewers: true, + squash: false, + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 2, false); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"]); + + expect(configs.dryRun).toEqual(true); + expect(configs.auth).toEqual("whatever"); + expect(configs.targetBranch).toEqual("prod"); + expect(configs.git).toEqual({ + user: "Gitlab", + email: "noreply@gitlab.com" + }); + expect(configs.originalPullRequest).toEqual({ + number: 2, + author: "superuser", + url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2", + htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2", + state: "open", + merged: false, + mergedBy: undefined, + title: "Update test.txt opened", + body: "Still opened mr body", + reviewers: ["superuser"], + assignees: ["superuser"], + labels: [], + targetRepo: { + owner: "superuser", + project: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git" + }, + sourceRepo: { + owner: "superuser", + project: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git" + }, + bpBranchName: undefined, + nCommits: 2, + commits: ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"] + }); + }); }); \ No newline at end of file diff --git a/test/service/git/gitlab/gitlab-client.test.ts b/test/service/git/gitlab/gitlab-client.test.ts index 2e3e58d..affe096 100644 --- a/test/service/git/gitlab/gitlab-client.test.ts +++ b/test/service/git/gitlab/gitlab-client.test.ts @@ -29,11 +29,7 @@ describe("github service", () => { GitClientFactory.reset(); gitClient = GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "apiUrl") as GitLabClient; }); - - afterEach(() => { - jest.clearAllMocks(); - }); - + test("get merged pull request", async () => { const res: GitPullRequest = await gitClient.getPullRequest("superuser", "backporting-example", 1); diff --git a/test/service/runner/cli-github-runner.test.ts b/test/service/runner/cli-github-runner.test.ts index 0c06a6c..108420a 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -55,8 +55,6 @@ beforeEach(() => { }); afterEach(() => { - jest.clearAllMocks(); - // reset process.env variables resetProcessArgs(); }); @@ -292,7 +290,7 @@ describe("cli runner", () => { "https://github.com/owner/reponame/pull/6666" ]); - expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); + await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); }); test("open pull request", async () => { @@ -636,4 +634,50 @@ describe("cli runner", () => { } ); }); + + test("multiple commits pr", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://github.com/owner/reponame/pull/8632", + "--no-squash", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-0404fb922ab75c3a8aecad5c97d9af388df04695-11da4e38aa3e577ffde6d546f1c52e53b04d3151"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(0); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "0404fb922ab75c3a8aecad5c97d9af388df04695"); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "11da4e38aa3e577ffde6d546f1c52e53b04d3151"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-0404fb922ab75c3a8aecad5c97d9af388df04695-11da4e38aa3e577ffde6d546f1c52e53b04d3151"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-target-0404fb922ab75c3a8aecad5c97d9af388df04695-11da4e38aa3e577ffde6d546f1c52e53b04d3151", + base: "target", + title: "[target] PR Title", + body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"), + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + } + ); + }); }); \ No newline at end of file diff --git a/test/service/runner/cli-gitlab-runner.test.ts b/test/service/runner/cli-gitlab-runner.test.ts index 31fdb61..7f9ea43 100644 --- a/test/service/runner/cli-gitlab-runner.test.ts +++ b/test/service/runner/cli-gitlab-runner.test.ts @@ -68,8 +68,6 @@ beforeEach(() => { }); afterEach(() => { - jest.clearAllMocks(); - // reset process.env variables resetProcessArgs(); }); @@ -198,7 +196,7 @@ describe("cli runner", () => { "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/3" ]); - expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); + await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); }); test("merged pull request", async () => { @@ -501,4 +499,51 @@ describe("cli runner", () => { } ); }); + + test("multiple commits without squash", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2", + "--no-squash", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336a4a20f394df6665994df382fb1d193a11-974519f65c9e0ed65277cd71026657a09fca05e7"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(1); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11"); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "974519f65c9e0ed65277cd71026657a09fca05e7"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336a4a20f394df6665994df382fb1d193a11-974519f65c9e0ed65277cd71026657a09fca05e7"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-e4dd336a4a20f394df6665994df382fb1d193a11-974519f65c9e0ed65277cd71026657a09fca05e7", + base: "target", + title: "[target] Update test.txt opened", + body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"), + reviewers: ["superuser"], + assignees: [], + labels: [], + } + ); + }); }); \ No newline at end of file diff --git a/test/service/runner/gha-github-runner.test.ts b/test/service/runner/gha-github-runner.test.ts index 8fb8dd4..9d3bf0c 100644 --- a/test/service/runner/gha-github-runner.test.ts +++ b/test/service/runner/gha-github-runner.test.ts @@ -55,10 +55,6 @@ beforeEach(() => { runner = new Runner(parser); }); -afterEach(() => { - jest.clearAllMocks(); -}); - describe("gha runner", () => { test("with dry run", async () => { spyGetInput({ @@ -139,7 +135,7 @@ describe("gha runner", () => { "pull-request": "https://github.com/owner/reponame/pull/6666" }); - expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); + await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); }); test("open pull request", async () => { @@ -460,4 +456,47 @@ describe("gha runner", () => { ); }); + test("multiple commits pr", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://api.github.com/repos/owner/reponame/pulls/8632", + "no-squash": "true", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-0404fb922ab75c3a8aecad5c97d9af388df04695-11da4e38aa3e577ffde6d546f1c52e53b04d3151"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(0); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "0404fb922ab75c3a8aecad5c97d9af388df04695"); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "11da4e38aa3e577ffde6d546f1c52e53b04d3151"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-0404fb922ab75c3a8aecad5c97d9af388df04695-11da4e38aa3e577ffde6d546f1c52e53b04d3151"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-target-0404fb922ab75c3a8aecad5c97d9af388df04695-11da4e38aa3e577ffde6d546f1c52e53b04d3151", + base: "target", + title: "[target] PR Title", + body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"), + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + } + ); + }); }); \ No newline at end of file diff --git a/test/service/runner/gha-gitlab-runner.test.ts b/test/service/runner/gha-gitlab-runner.test.ts index a28b410..6f766c3 100644 --- a/test/service/runner/gha-gitlab-runner.test.ts +++ b/test/service/runner/gha-gitlab-runner.test.ts @@ -66,10 +66,6 @@ beforeEach(() => { runner = new Runner(parser); }); -afterEach(() => { - jest.clearAllMocks(); -}); - describe("gha runner", () => { test("with dry run", async () => { spyGetInput({ @@ -150,7 +146,7 @@ describe("gha runner", () => { "pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/3" }); - expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); + await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!"); }); test("merged pull request", async () => { @@ -423,4 +419,49 @@ describe("gha runner", () => { } ); }); + + test("multiple commits without squash", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2", + "no-squash": "true", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336a4a20f394df6665994df382fb1d193a11-974519f65c9e0ed65277cd71026657a09fca05e7"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(1); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11"); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "974519f65c9e0ed65277cd71026657a09fca05e7"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336a4a20f394df6665994df382fb1d193a11-974519f65c9e0ed65277cd71026657a09fca05e7"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-e4dd336a4a20f394df6665994df382fb1d193a11-974519f65c9e0ed65277cd71026657a09fca05e7", + base: "target", + title: "[target] Update test.txt opened", + body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"), + reviewers: ["superuser"], + assignees: [], + labels: [], + } + ); + }); }); \ No newline at end of file diff --git a/test/support/mock/git-client-mock-support.ts b/test/support/mock/git-client-mock-support.ts index c73d410..040429d 100644 --- a/test/support/mock/git-client-mock-support.ts +++ b/test/support/mock/git-client-mock-support.ts @@ -1,7 +1,7 @@ import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; import { Moctokit } from "@kie/mock-github"; -import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, sameOwnerPullRequestFixture } from "./github-data"; -import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR, PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data"; +import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, multipleCommitsPullRequestFixture, multipleCommitsPullRequestCommits } from "./github-data"; +import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data"; const logger = LoggerServiceFactory.getLogger(); @@ -22,6 +22,8 @@ export const getAxiosMocked = (url: string) => { data = PROJECT_EXAMPLE; } else if (url.endsWith("users?username=superuser")) { data = [SUPERUSER]; + } else if (url.endsWith("merge_requests/2/commits")) { + data = OPEN_PR_COMMITS; } return { @@ -96,15 +98,15 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => }); mock.rest.pulls - .get({ - owner: targetOwner, - repo: repo, - pull_number: sameOwnerPullRequestFixture.number - }) - .reply({ - status: 200, - data: sameOwnerPullRequestFixture - }); + .get({ + owner: targetOwner, + repo: repo, + pull_number: multipleCommitsPullRequestFixture.number + }) + .reply({ + status: 200, + data: multipleCommitsPullRequestFixture + }); mock.rest.pulls .get({ @@ -118,15 +120,26 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => }); mock.rest.pulls - .get({ - owner: targetOwner, - repo: repo, - pull_number: notMergedPullRequestFixture.number - }) - .reply({ - status: 200, - data: notMergedPullRequestFixture - }); + .get({ + owner: targetOwner, + repo: repo, + pull_number: notMergedPullRequestFixture.number + }) + .reply({ + status: 200, + data: notMergedPullRequestFixture + }); + + mock.rest.pulls + .listCommits({ + owner: targetOwner, + repo: repo, + pull_number: multipleCommitsPullRequestFixture.number + }) + .reply({ + status: 200, + data: multipleCommitsPullRequestCommits + }); mock.rest.pulls .create() @@ -156,7 +169,6 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => data: {} }); - // invalid requests mock.rest.pulls .get({ diff --git a/test/support/mock/github-data.ts b/test/support/mock/github-data.ts index a5f0f60..e91b723 100644 --- a/test/support/mock/github-data.ts +++ b/test/support/mock/github-data.ts @@ -1341,7 +1341,7 @@ export const notMergedPullRequestFixture = { "changed_files": 2 }; -export const sameOwnerPullRequestFixture = { +export const multipleCommitsPullRequestFixture = { "url": "https://api.github.com/repos/owner/reponame/pulls/8632", "id": 1137188271, "node_id": "PR_kwDOABTq6s5DyB2v", @@ -1802,4 +1802,165 @@ export const sameOwnerPullRequestFixture = { "additions": 2, "deletions": 2, "changed_files": 2 -}; \ No newline at end of file +}; + +export const multipleCommitsPullRequestCommits = [ + { + "sha": "0404fb922ab75c3a8aecad5c97d9af388df04695", + "node_id": "C_kwDOImgs99oAKDA0MDRmYjkyMmFiNzVjM2E4YWVjYWQ1Yzk3ZDlhZjM4OGRmMDQ2OTU", + "commit": { + "author": { + "name": "owner", + "email": "owner@email.com", + "date": "2023-07-06T13:46:30Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2023-07-06T13:46:30Z" + }, + "message": "Update file1.txt", + "tree": { + "sha": "50be1d7031b02a2ae609f432f2a1e0f818d827b2", + "url": "https://api.github.com/repos/owner/reponame/git/trees/50be1d7031b02a2ae609f432f2a1e0f818d827b2" + }, + "url": "https://api.github.com/repos/owner/reponame/git/commits/0404fb922ab75c3a8aecad5c97d9af388df04695", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nno-signature=\n=fivd\n-----END PGP SIGNATURE-----\n", + "payload": "tree 50be1d7031b02a2ae609f432f2a1e0f818d827b2\nparent c85b8fcdb741814b3e90e6e5729455cf46ff26ea\nauthor Owner 1688651190 +0200\ncommitter GitHub 1688651190 +0200\n\nUpdate file1.txt" + } + }, + "url": "https://api.github.com/repos/owner/reponame/commits/0404fb922ab75c3a8aecad5c97d9af388df04695", + "html_url": "https://github.com/owner/reponame/commit/0404fb922ab75c3a8aecad5c97d9af388df04695", + "comments_url": "https://api.github.com/repos/owner/reponame/commits/0404fb922ab75c3a8aecad5c97d9af388df04695/comments", + "author": { + "login": "owner", + "id": 26715795, + "node_id": "MDQ6VXNlcjI2NzE1Nzk1", + "avatar_url": "https://avatars.githubusercontent.com/u/26715795?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/owner", + "html_url": "https://github.com/owner", + "followers_url": "https://api.github.com/users/owner/followers", + "following_url": "https://api.github.com/users/owner/following{/other_user}", + "gists_url": "https://api.github.com/users/owner/gists{/gist_id}", + "starred_url": "https://api.github.com/users/owner/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/owner/subscriptions", + "organizations_url": "https://api.github.com/users/owner/orgs", + "repos_url": "https://api.github.com/users/owner/repos", + "events_url": "https://api.github.com/users/owner/events{/privacy}", + "received_events_url": "https://api.github.com/users/owner/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "c85b8fcdb741814b3e90e6e5729455cf46ff26ea", + "url": "https://api.github.com/repos/owner/reponame/commits/c85b8fcdb741814b3e90e6e5729455cf46ff26ea", + "html_url": "https://github.com/owner/reponame/commit/c85b8fcdb741814b3e90e6e5729455cf46ff26ea" + } + ] + }, + { + "sha": "11da4e38aa3e577ffde6d546f1c52e53b04d3151", + "node_id": "C_kwDOImgs99oAKDExZGE0ZTM4YWEzZTU3N2ZmZGU2ZDU0NmYxYzUyZTUzYjA0ZDMxNTE", + "commit": { + "author": { + "name": "Owner", + "email": "owner@email.com", + "date": "2023-07-10T13:23:44Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2023-07-10T13:23:44Z" + }, + "message": "Update file2.txt", + "tree": { + "sha": "fdd16fb791eef26fd84c3bfa34fd89eb1f7a85be", + "url": "https://api.github.com/repos/owner/reponame/git/trees/fdd16fb791eef26fd84c3bfa34fd89eb1f7a85be" + }, + "url": "https://api.github.com/repos/owner/reponame/git/commits/11da4e38aa3e577ffde6d546f1c52e53b04d3151", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nno-signature\n=//hm\n-----END PGP SIGNATURE-----\n", + "payload": "tree fdd16fb791eef26fd84c3bfa34fd89eb1f7a85be\nparent 0404fb922ab75c3a8aecad5c97d9af388df04695\nauthor Owner 1688995424 +0200\ncommitter GitHub 1688995424 +0200\n\nUpdate file2.txt" + } + }, + "url": "https://api.github.com/repos/owner/reponame/commits/11da4e38aa3e577ffde6d546f1c52e53b04d3151", + "html_url": "https://github.com/owner/reponame/commit/11da4e38aa3e577ffde6d546f1c52e53b04d3151", + "comments_url": "https://api.github.com/repos/owner/reponame/commits/11da4e38aa3e577ffde6d546f1c52e53b04d3151/comments", + "author": { + "login": "owner", + "id": 26715795, + "node_id": "MDQ6VXNlcjI2NzE1Nzk1", + "avatar_url": "https://avatars.githubusercontent.com/u/26715795?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/owner", + "html_url": "https://github.com/owner", + "followers_url": "https://api.github.com/users/owner/followers", + "following_url": "https://api.github.com/users/owner/following{/other_user}", + "gists_url": "https://api.github.com/users/owner/gists{/gist_id}", + "starred_url": "https://api.github.com/users/owner/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/owner/subscriptions", + "organizations_url": "https://api.github.com/users/owner/orgs", + "repos_url": "https://api.github.com/users/owner/repos", + "events_url": "https://api.github.com/users/owner/events{/privacy}", + "received_events_url": "https://api.github.com/users/owner/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "0404fb922ab75c3a8aecad5c97d9af388df04695", + "url": "https://api.github.com/repos/owner/reponame/commits/0404fb922ab75c3a8aecad5c97d9af388df04695", + "html_url": "https://github.com/owner/reponame/commit/0404fb922ab75c3a8aecad5c97d9af388df04695" + } + ] + } +]; \ No newline at end of file diff --git a/test/support/mock/gitlab-data.ts b/test/support/mock/gitlab-data.ts index d1af227..119913a 100644 --- a/test/support/mock/gitlab-data.ts +++ b/test/support/mock/gitlab-data.ts @@ -529,6 +529,49 @@ export const CLOSED_NOT_MERGED_MR = { } }; +export const OPEN_PR_COMMITS = [ + { + "id":"974519f65c9e0ed65277cd71026657a09fca05e7", + "short_id":"974519f6", + "created_at":"2023-07-10T19:23:04.000Z", + "parent_ids":[ + + ], + "title":"Add another file", + "message":"Add another file", + "author_name":"Super User", + "author_email":"superuser@email.com", + "authored_date":"2023-07-10T19:23:04.000Z", + "committer_name":"Super User", + "committer_email":"superuser@email.com", + "committed_date":"2023-07-10T19:23:04.000Z", + "trailers":{ + + }, + "web_url":"https://gitlab.com/superuser/backporting-example/-/commit/974519f65c9e0ed65277cd71026657a09fca05e7" + }, + { + "id":"e4dd336a4a20f394df6665994df382fb1d193a11", + "short_id":"e4dd336a", + "created_at":"2023-06-29T09:59:10.000Z", + "parent_ids":[ + + ], + "title":"Add new file", + "message":"Add new file", + "author_name":"Super User", + "author_email":"superuser@email.com", + "authored_date":"2023-06-29T09:59:10.000Z", + "committer_name":"Super User", + "committer_email":"superuser@email.com", + "committed_date":"2023-06-29T09:59:10.000Z", + "trailers":{ + + }, + "web_url":"https://gitlab.com/superuser/backporting-example/-/commit/e4dd336a4a20f394df6665994df382fb1d193a11" + } +]; + export const SUPERUSER = { "id":14041, "username":"superuser",