feat(issue-54): backport pr commits without squash (#55)

* feat(issue-54): backport pr commits without squash

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

* feat(issue-54): fixed readme
This commit is contained in:
Andrea Lamparelli 2023-07-11 11:22:01 +02:00 committed by GitHub
parent a737aa7c4c
commit c4dbb26c1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 990 additions and 145 deletions

View file

@ -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.
<details>
<summary>
First time here?
</summary>
This project follows [git conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) pattern, therefore the commits should have the following format:
```
<type>(<optional scope>): <subject>
empty separator line
<optional body>
empty separator line
<optional footer>
```
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 <feat-branch> --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.
</details>
<details>
<summary>
How to prepare for a new release?
</summary>
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.
</details>

3
.gitignore vendored
View file

@ -11,3 +11,6 @@ report.json
.vscode/
build/
.npmrc
# temporary files created during tests
*test*.json

View file

@ -26,11 +26,10 @@ 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)**
* **[Development](#development)**
* **[Contributing](#contributing)**
* **[License](#license)**
@ -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 <sha>
```
> **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,19 +220,38 @@ 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
## 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:
> **NOTE**: these changes did not affect the tool features.
| | **v4 (after migration)** | v3 or older (before migration) |
|-------------|--------------------------|--------------------------------|
| Owner | kiegroup | lampajr |
| Repository | git-backporting | backporting |
| NPM package | @kie/git-backporting | @lampajr/bper |
| CLI tool | git-backporting | bper |
So everytime you would use older version keep in mind of these changes.
> **REMARK**: since from capabilities point of view `v3.1.1` and `v4.0.0` are equivalent we would recommend to directly start using `v4`.
## Development
### 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
#### Automatic release
The first step is to prepare the changes for the next release, this is done by running:
@ -236,7 +272,7 @@ After that you should just push the new branch and open the pull request.
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
#### 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`
@ -245,23 +281,6 @@ In case we would like to perform a manual release, it would be enough to open a
Once the release preparion pull request got merged, run [Release package](.github/workflows/release.yml) workflow.
## Repository Migration
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:
> **NOTE**: these changes did not affect the tool features.
| | **v4 (after migration)** | v3 or older (before migration) |
|-------------|--------------------------|--------------------------------|
| Owner | kiegroup | lampajr |
| Repository | git-backporting | backporting |
| NPM package | @kie/git-backporting | @lampajr/bper |
| CLI tool | git-backporting | bper |
So everytime you would use older version keep in mind of these changes.
> **REMARK**: since from capabilities point of view `v3.1.1` and `v4.0.0` are equivalent we would recommend to directly start using `v4`.
## Contributing
This is an open source project, and you are more than welcome to contribute :heart:!

View file

@ -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

72
dist/cli/index.js vendored
View file

@ -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 <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 <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));
}
async getPullRequestFromUrl(prUrl) {
catch (error) {
throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`);
}
}
return this.mapper.mapPullRequest(data, commits);
}
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: <host>/api/v4/projects/<namespace>%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());
}
getPullRequestFromUrl(mrUrl) {
catch (error) {
throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`);
}
}
return this.mapper.mapPullRequest(data, commits);
}
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,11 +1033,15 @@ class GitLabMapper {
labels: mr.labels ?? [],
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
nCommits: 1,
// 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?
commits: this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.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);

71
dist/gha/index.js vendored
View file

@ -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));
}
async getPullRequestFromUrl(prUrl) {
catch (error) {
throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`);
}
}
return this.mapper.mapPullRequest(data, commits);
}
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: <host>/api/v4/projects/<namespace>%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());
}
getPullRequestFromUrl(mrUrl) {
catch (error) {
throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`);
}
}
return this.mapper.mapPullRequest(data, commits);
}
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,11 +1006,15 @@ class GitLabMapper {
labels: mr.labels ?? [],
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
nCommits: 1,
// 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?
commits: this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.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);

View file

@ -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),
};
}
}

View file

@ -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
}

View file

@ -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 <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 <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,
};
}

View file

@ -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")),
};
}

View file

@ -17,7 +17,7 @@ export default class PullRequestConfigsParser extends ConfigsParser {
public async parse(args: Args): Promise<Configs> {
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;

View file

@ -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<PullRequest>}
*/
getPullRequest(owner: string, repo: string, prNumber: number): Promise<GitPullRequest>;
getPullRequest(owner: string, repo: string, prNumber: number, squash: boolean): Promise<GitPullRequest>;
/**
* 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<PullRequest>}
*/
getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest>;
getPullRequestFromUrl(prUrl: string, squash: boolean): Promise<GitPullRequest>;
// WRITE

View file

@ -10,6 +10,7 @@ export default interface GitResponseMapper<PR, S> {
mapPullRequest(
pr: PR,
commits?: string[],
): Promise<GitPullRequest>;
mapGitState(state: S): GitRepoState;

View file

@ -31,20 +31,36 @@ export default class GitHubClient implements GitClient {
return "noreply@github.com";
}
async getPullRequest(owner: string, repo: string, prNumber: number): Promise<GitPullRequest> {
async getPullRequest(owner: string, repo: string, prNumber: number, squash = true): Promise<GitPullRequest> {
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}`);
}
}
async getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest> {
return this.mapper.mapPullRequest(data as PullRequest, commits);
}
async getPullRequestFromUrl(prUrl: string, squash = true): Promise<GitPullRequest> {
const { owner, project, id } = this.extractPullRequestData(prUrl);
return this.getPullRequest(owner, project, id);
return this.getPullRequest(owner, project, id, squash);
}
// WRITE

View file

@ -13,7 +13,7 @@ export default class GitHubMapper implements GitResponseMapper<PullRequest, "ope
}
}
async mapPullRequest(pr: PullRequest): Promise<GitPullRequest> {
async mapPullRequest(pr: PullRequest, commits?: string[]): Promise<GitPullRequest> {
return {
number: pr.number,
author: pr.user.login,
@ -30,11 +30,16 @@ export default class GitHubMapper implements GitResponseMapper<PullRequest, "ope
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 as string]
// if commits is provided use them, otherwise fetch the single sha representing the whole pr
commits: (commits && commits.length > 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<GitRepository> {
return Promise.resolve({
owner: pr.head.repo.full_name.split("/")[0],

View file

@ -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: <host>/api/v4/projects/<namespace>%2Fbackporting-example/merge_requests/1
async getPullRequest(namespace: string, repo: string, mrNumber: number): Promise<GitPullRequest> {
async getPullRequest(namespace: string, repo: string, mrNumber: number, squash = true): Promise<GitPullRequest> {
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}`);
}
}
getPullRequestFromUrl(mrUrl: string): Promise<GitPullRequest> {
return this.mapper.mapPullRequest(data as MergeRequestSchema, commits);
}
getPullRequestFromUrl(mrUrl: string, squash = true): Promise<GitPullRequest> {
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
return this.getPullRequest(namespace, project, id);
return this.getPullRequest(namespace, project, id, squash);
}
// WRITE

View file

@ -24,7 +24,7 @@ export default class GitLabMapper implements GitResponseMapper<MergeRequestSchem
}
}
async mapPullRequest(mr: MergeRequestSchema): Promise<GitPullRequest> {
async mapPullRequest(mr: MergeRequestSchema, commits?: string[]): Promise<GitPullRequest> {
return {
number: mr.iid,
author: mr.author.username,
@ -40,11 +40,16 @@ export default class GitLabMapper implements GitResponseMapper<MergeRequestSchem
labels: mr.labels ?? [],
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
nCommits: 1, // info not present on mr
// 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)
};
}
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?
commits: this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha as string] : [mr.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<GitRepository> {

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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,
});
});
});

View file

@ -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"]
});
});
});

View file

@ -30,10 +30,6 @@ describe("github service", () => {
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);

View file

@ -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: [],
}
);
});
});

View file

@ -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: [],
}
);
});
});

View file

@ -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: [],
}
);
});
});

View file

@ -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: [],
}
);
});
});

View file

@ -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 {
@ -99,11 +101,11 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
.get({
owner: targetOwner,
repo: repo,
pull_number: sameOwnerPullRequestFixture.number
pull_number: multipleCommitsPullRequestFixture.number
})
.reply({
status: 200,
data: sameOwnerPullRequestFixture
data: multipleCommitsPullRequestFixture
});
mock.rest.pulls
@ -128,6 +130,17 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
data: notMergedPullRequestFixture
});
mock.rest.pulls
.listCommits({
owner: targetOwner,
repo: repo,
pull_number: multipleCommitsPullRequestFixture.number
})
.reply({
status: 200,
data: multipleCommitsPullRequestCommits
});
mock.rest.pulls
.create()
.reply({
@ -156,7 +169,6 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
data: {}
});
// invalid requests
mock.rest.pulls
.get({

View file

@ -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",
@ -1803,3 +1803,164 @@ export const sameOwnerPullRequestFixture = {
"deletions": 2,
"changed_files": 2
};
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 <owner@email.com> 1688651190 +0200\ncommitter GitHub <noreply@github.com> 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 <owner@email.com> 1688995424 +0200\ncommitter GitHub <noreply@github.com> 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"
}
]
}
];

View file

@ -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",