feat: integrate tool with gitlab service (#39)

* feat: integrate tool with gitlab service

Fix https://github.com/lampajr/backporting/issues/30
This commit is contained in:
Andrea Lamparelli 2023-07-02 00:05:17 +02:00 committed by GitHub
parent 8a007941d1
commit 107f5e52d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 17821 additions and 1553 deletions

3
.gitignore vendored
View file

@ -6,7 +6,8 @@ test/**/_temp/**/*
yarn.lock
coverage/
test-report.xml
report.json
.idea/
.vscode/
build/
# dist/
# dist/

View file

@ -15,7 +15,7 @@
---
**BPer** is a [NodeJS](https://nodejs.org/) command line tool that provides capabilities to *backport* [1] pull requests in an automated way. This tool also comes with a predefined GitHub action in order to make CI/CD integration easier for all users.
**Git Backporter**, also referenced as **bper**, is a [NodeJS](https://nodejs.org/) command line tool that provides capabilities to *backport* [1] pull requests (on *GitHub*) and merge requests (on *GitLab*) in an automated way. This tool also comes with a predefined *GitHub* action in order to make CI/CD integration easier for all users.
[1] *backporting* is an action aiming to move a change (usually a commit) from a branch (usually the main one) to another one, which is generally referring to a still maintained release branch. Keeping it simple: it is about to move a specific change or a set of them from one branch to another.
@ -23,12 +23,12 @@ Table of content
----------------
* **[Usage](#usage)**
* **[Supported Git Services](#supported-git-services)**
* **[GitHub Action](#github-action)**
* **[Limitations](#limitations)**
* **[Contributions](#contributing)**
* **[License](#license)**
## Usage
This tool is released on the [public npm registry](https://www.npmjs.com/), therefore it can be easily installed using `npm`:
@ -40,7 +40,7 @@ $ npm install -g @lampajr/bper
Then it can be used as any other command line tool:
```bash
$ bper -tb <branch> -pr <pull-request-url> -a <github-token> [-f <your-folder>]
$ bper -tb <branch> -pr <pull-request-url> -a <git-token> [-f <your-folder>]
```
### Inputs
@ -66,10 +66,18 @@ This toold comes with some inputs that allow users to override the default behav
| Backport Branch Name | --bp-branch-name | N | Name of the backporting pull request branch | bp-{target-branch}-{sha} |
| Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false |
## Supported Git Services
Right now **bper** 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.
* ***GITLAB***: This has been introduced since version `3.0.0`, it works for both public and private *GitLab* servers. The interaction with this service is performed using plain [*axios*](https://axios-http.com) requests. The *gitlab* api version that is used to make requests is `v4`, at the moment there is no possibility to override it.
> **NOTE**: by default, all gitlab requests are performed setting `rejectUnauthorized=false`, planning to make this configurable too.
## GitHub Action
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).
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
@ -145,25 +153,20 @@ jobs:
For a complete description of all inputs see [Inputs section](#inputs).
## Limitations
## Future Works
**BPer** is in development mode, this means that it has many limitations right now. I'll try to summarize the most importan ones:
**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:
- You can backport pull requests only.
- It only works for [GitHub](https://github.com/).
- Integrated in GitHub Actions CI/CD only.
Based on these limitations, the next **Future Works** could be the following:
- Provide a way to backport single commit too (or a set of them), even if no original pull request is present.
- Integrate this tool with other git management services (like GitLab and Bitbucket) to make it as generic as possible.
- Integrate it into other CI/CD services like gitlab CI (once GitLab will be integrated as well).
- Provide some reusable GitHub workflows.
- 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.
## Contributing
This is an open source project, and you are more than welcome to contribute :heart:!
Every change must be submitted through a GitHub pull request (PR). Backporting uses continuous integration (CI). The CI runs checks against your branch after you submit the PR to ensure that your PR doesnt introduce errors. If the CI identifies a potential problem, our friendly PR maintainers will help you resolve it.
Every change must be submitted through a *GitHub* pull request (PR). Backporting uses continuous integration (CI). The CI runs checks against your branch after you submit the PR to ensure that your PR doesnt introduce errors. If the CI identifies a potential problem, our friendly PR maintainers will help you resolve it.
> **Note**: this project follows [git-conventional-commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) standards, thanks to the [commit-msg hook](./.husky/commit-msg) you are not allowed to use commits that do not follow those standards.

7045
dist/cli/index.js vendored

File diff suppressed because one or more lines are too long

7035
dist/gha/index.js vendored

File diff suppressed because one or more lines are too long

2445
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -45,6 +45,7 @@
"devDependencies": {
"@commitlint/cli": "^17.4.0",
"@commitlint/config-conventional": "^17.4.0",
"@gitbeaker/rest": "^39.1.0",
"@kie/mock-github": "^1.1.0",
"@release-it/conventional-changelog": "^5.1.1",
"@types/fs-extra": "^9.0.13",
@ -55,11 +56,11 @@
"@vercel/ncc": "^0.36.0",
"eslint": "^8.30.0",
"husky": "^8.0.2",
"jest": "^29.3.1",
"jest": "^29.0.0",
"jest-sonar-reporter": "^2.0.0",
"release-it": "^15.6.0",
"semver": "^7.3.8",
"ts-jest": "^29.0.3",
"ts-jest": "^29.0.0",
"ts-node": "^10.8.1",
"tsc-alias": "^1.8.2",
"tsconfig-paths": "^4.1.0",
@ -69,8 +70,10 @@
"@actions/core": "^1.10.0",
"@octokit/rest": "^18.12.0",
"@octokit/webhooks-types": "^6.8.0",
"axios": "^1.4.0",
"commander": "^9.3.0",
"fs-extra": "^11.1.0",
"https": "^1.0.0",
"simple-git": "^3.15.1"
}
}

View file

@ -9,7 +9,7 @@ import LoggerServiceFactory from "../logger/logger-service-factory";
*/
export default abstract class ConfigsParser {
private readonly logger: LoggerService;
protected readonly logger: LoggerService;
constructor() {
this.logger = LoggerServiceFactory.getLogger();

View file

@ -1,21 +1,28 @@
import { Args } from "@bp/service/args/args.types";
import ConfigsParser from "@bp/service/configs/configs-parser";
import { Configs } from "@bp/service/configs/configs.types";
import GitService from "@bp/service/git/git-service";
import GitServiceFactory from "@bp/service/git/git-service-factory";
import GitClient from "@bp/service/git/git-client";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitPullRequest } from "@bp/service/git/git.types";
export default class PullRequestConfigsParser extends ConfigsParser {
private gitService: GitService;
private gitService: GitClient;
constructor() {
super();
this.gitService = GitServiceFactory.getService();
this.gitService = GitClientFactory.getClient();
}
public async parse(args: Args): Promise<Configs> {
const pr: GitPullRequest = await this.gitService.getPullRequestFromUrl(args.pullRequest);
let pr: GitPullRequest;
try {
pr = await this.gitService.getPullRequestFromUrl(args.pullRequest);
} catch(error) {
this.logger.error("Something went wrong retrieving pull request");
throw error;
}
const folder: string = args.folder ?? this.getDefaultFolder();
return {

View file

@ -0,0 +1,56 @@
import GitClient from "@bp/service/git/git-client";
import { GitClientType } from "@bp/service/git/git.types";
import GitHubService from "@bp/service/git/github/github-client";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import GitLabClient from "./gitlab/gitlab-client";
/**
* Singleton git service factory class
*/
export default class GitClientFactory {
private static logger: LoggerService = LoggerServiceFactory.getLogger();
private static instance?: GitClient;
public static getClient(): GitClient {
if (!GitClientFactory.instance) {
throw new Error("You must call `getOrCreate` method first!");
}
return GitClientFactory.instance;
}
/**
* Initialize the singleton git management service
* @param type git management service type
* @param authToken authentication token, like github/gitlab token
*/
public static getOrCreate(type: GitClientType, authToken: string, apiUrl: string): GitClient {
if (GitClientFactory.instance) {
GitClientFactory.logger.warn("Git service already initialized!");
return GitClientFactory.instance;
}
this.logger.debug(`Setting up ${type} client: apiUrl=${apiUrl}, token=****`);
switch(type) {
case GitClientType.GITHUB:
GitClientFactory.instance = new GitHubService(authToken, apiUrl);
break;
case GitClientType.GITLAB:
GitClientFactory.instance = new GitLabClient(authToken, apiUrl);
break;
default:
throw new Error(`Invalid git service type received: ${type}`);
}
return GitClientFactory.instance;
}
public static reset(): void {
GitClientFactory.logger.warn("Resetting git service!");
GitClientFactory.instance = undefined;
}
}

View file

@ -4,7 +4,7 @@ import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
* Git management service interface, which provides a common API for interacting
* with several git management services like GitHub, Gitlab or Bitbucket.
*/
export default interface GitService {
export default interface GitClient {
// READ
@ -29,6 +29,7 @@ import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
/**
* Create a new pull request on the underneath git service
* @param backport backport pull request data
* @returns {Promise<string>} the pull request url
*/
createPullRequest(backport: BackportPullRequest): Promise<void>;
createPullRequest(backport: BackportPullRequest): Promise<string>;
}

View file

@ -0,0 +1,20 @@
import { GitPullRequest, GitRepoState, GitRepository } from "@bp/service/git/git.types";
/**
* Generic git client response mapper
*
* PR - full pull request schema type
* S - pull request state type
*/
export default interface GitResponseMapper<PR, S> {
mapPullRequest(
pr: PR,
): Promise<GitPullRequest>;
mapGitState(state: S): GitRepoState;
mapSourceRepo(pull: PR): Promise<GitRepository>;
mapTargetRepo (pull: PR): Promise<GitRepository>;
}

View file

@ -1,43 +0,0 @@
import GitService from "@bp/service/git/git-service";
import { GitServiceType } from "@bp/service/git/git.types";
import GitHubService from "@bp/service/git/github/github-service";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
/**
* Singleton git service factory class
*/
export default class GitServiceFactory {
private static logger: LoggerService = LoggerServiceFactory.getLogger();
private static instance?: GitService;
public static getService(): GitService {
if (!GitServiceFactory.instance) {
throw new Error("You must call `init` method first!");
}
return GitServiceFactory.instance;
}
/**
* Initialize the singleton git management service
* @param type git management service type
* @param auth authentication, like github token
*/
public static getOrCreate(type: GitServiceType, auth: string): void {
if (GitServiceFactory.instance) {
GitServiceFactory.logger.warn("Git service already initialized!");
return;
}
switch(type) {
case GitServiceType.GITHUB:
GitServiceFactory.instance = new GitHubService(auth);
break;
default:
throw new Error(`Invalid git service type received: ${type}`);
}
}
}

View file

@ -0,0 +1,39 @@
import { GitClientType } from "@bp/service/git/git.types";
const PUBLIC_GITHUB_URL = "https://github.com";
const PUBLIC_GITHUB_API = "https://api.github.com";
/**
* Infer the remote GIT service to interact with based on the provided
* pull request URL
* @param prUrl provided pull request URL
* @returns {GitClientType}
*/
export const inferGitClient = (prUrl: string): GitClientType => {
const stdPrUrl = prUrl.toLowerCase().trim();
if (stdPrUrl.includes(GitClientType.GITHUB.toString())) {
return GitClientType.GITHUB;
} else if (stdPrUrl.includes(GitClientType.GITLAB.toString())) {
return GitClientType.GITLAB;
}
throw new Error(`Remote git service not recognized from pr url: ${prUrl}`);
};
/**
* Infer the host git service from the pull request url
* @param prUrl pull/merge request url
* @param apiVersion the api version, ignored in case of public github
* @returns api URL like https://api.github.com or https://gitlab.com/api/v4
*/
export const inferGitApiUrl = (prUrl: string, apiVersion = "v4"): string => {
const url = new URL(prUrl);
const baseUrl = `${url.protocol}//${url.host}`;
if (baseUrl.includes(PUBLIC_GITHUB_URL)) {
return PUBLIC_GITHUB_API;
}
return `${baseUrl}/api/${apiVersion}`;
};

View file

@ -3,7 +3,7 @@ export interface GitPullRequest {
author: string,
url?: string,
htmlUrl?: string,
state?: "open" | "closed",
state?: GitRepoState,
merged?: boolean,
mergedBy?: string,
title: string,
@ -35,6 +35,14 @@ export interface BackportPullRequest {
branchName?: string,
}
export enum GitServiceType {
GITHUB = "github"
export enum GitClientType {
GITHUB = "github",
GITLAB = "gitlab",
}
export enum GitRepoState {
OPEN = "open",
CLOSED = "closed",
LOCKED = "locked", // just on gitlab
MERGED = "merged", // just on gitlab
}

View file

@ -1,4 +1,4 @@
import GitService from "@bp/service/git/git-service";
import GitClient from "@bp/service/git/git-client";
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
import GitHubMapper from "@bp/service/git/github/github-mapper";
import OctokitFactory from "@bp/service/git/github/octokit-factory";
@ -7,15 +7,17 @@ import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Octokit } from "@octokit/rest";
import { PullRequest } from "@octokit/webhooks-types";
export default class GitHubService implements GitService {
export default class GitHubClient implements GitClient {
private logger: LoggerService;
private apiUrl: string;
private octokit: Octokit;
private mapper: GitHubMapper;
constructor(token: string) {
constructor(token: string, apiUrl: string) {
this.apiUrl = apiUrl;
this.logger = LoggerServiceFactory.getLogger();
this.octokit = OctokitFactory.getOctokit(token);
this.octokit = OctokitFactory.getOctokit(token, this.apiUrl);
this.mapper = new GitHubMapper();
}
@ -33,13 +35,13 @@ export default class GitHubService implements GitService {
}
async getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest> {
const {owner, project} = this.getRepositoryFromPrUrl(prUrl);
return this.getPullRequest(owner, project, parseInt(prUrl.substring(prUrl.lastIndexOf("/") + 1, prUrl.length)));
const { owner, project, id } = this.extractPullRequestData(prUrl);
return this.getPullRequest(owner, project, id);
}
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<void> {
async createPullRequest(backport: BackportPullRequest): Promise<string> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}.`);
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
@ -52,6 +54,10 @@ export default class GitHubService implements GitService {
body: backport.body
});
if (!data) {
throw new Error("Pull request creation failed");
}
if (backport.reviewers.length > 0) {
try {
await this.octokit.pulls.requestReviewers({
@ -77,6 +83,8 @@ export default class GitHubService implements GitService {
this.logger.error(`Error setting assignees: ${error}`);
}
}
return data.html_url;
}
// UTILS
@ -86,11 +94,12 @@ export default class GitHubService implements GitService {
* @param prUrl pull request url
* @returns {{owner: string, project: string}}
*/
private getRepositoryFromPrUrl(prUrl: string): {owner: string, project: string} {
private extractPullRequestData(prUrl: string): {owner: string, project: string, id: number} {
const elems: string[] = prUrl.split("/");
return {
owner: elems[elems.length - 4],
project: elems[elems.length - 3]
project: elems[elems.length - 3],
id: parseInt(prUrl.substring(prUrl.lastIndexOf("/") + 1, prUrl.length)),
};
}
}
}

View file

@ -1,9 +1,19 @@
import { GitPullRequest } from "@bp/service/git/git.types";
import { GitPullRequest, GitRepoState, GitRepository } from "@bp/service/git/git.types";
import { PullRequest, User } from "@octokit/webhooks-types";
import GitResponseMapper from "@bp/service/git/git-mapper";
export default class GitHubMapper {
export default class GitHubMapper implements GitResponseMapper<PullRequest, "open" | "closed"> {
mapPullRequest(pr: PullRequest): GitPullRequest {
mapGitState(state: "open" | "closed"): GitRepoState {
switch (state) {
case "open":
return GitRepoState.OPEN;
default:
return GitRepoState.CLOSED;
}
}
async mapPullRequest(pr: PullRequest): Promise<GitPullRequest> {
return {
number: pr.number,
author: pr.user.login,
@ -11,24 +21,32 @@ export default class GitHubMapper {
htmlUrl: pr.html_url,
title: pr.title,
body: pr.body ?? "",
state: pr.state,
state: this.mapGitState(pr.state), // TODO fix using custom mapper
merged: pr.merged ?? false,
mergedBy: pr.merged_by?.login,
reviewers: pr.requested_reviewers.filter(r => "login" in r).map((r => (r as User)?.login)),
assignees: pr.assignees.filter(r => "login" in r).map(r => r.login),
sourceRepo: {
owner: pr.head.repo.full_name.split("/")[0],
project: pr.head.repo.full_name.split("/")[1],
cloneUrl: pr.head.repo.clone_url
},
targetRepo: {
owner: pr.base.repo.full_name.split("/")[0],
project: pr.base.repo.full_name.split("/")[1],
cloneUrl: pr.base.repo.clone_url
},
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]
};
}
async mapSourceRepo(pr: PullRequest): Promise<GitRepository> {
return Promise.resolve({
owner: pr.head.repo.full_name.split("/")[0],
project: pr.head.repo.full_name.split("/")[1],
cloneUrl: pr.head.repo.clone_url
});
}
async mapTargetRepo(pr: PullRequest): Promise<GitRepository> {
return Promise.resolve({
owner: pr.base.repo.full_name.split("/")[0],
project: pr.base.repo.full_name.split("/")[1],
cloneUrl: pr.base.repo.clone_url
});
}
}

View file

@ -10,12 +10,13 @@ export default class OctokitFactory {
private static logger: LoggerService = LoggerServiceFactory.getLogger();
private static octokit?: Octokit;
public static getOctokit(token: string): Octokit {
public static getOctokit(token: string, apiUrl: string): Octokit {
if (!OctokitFactory.octokit) {
OctokitFactory.logger.info("Creating octokit instance.");
OctokitFactory.octokit = new Octokit({
auth: token,
userAgent: "lampajr/backporting"
userAgent: "lampajr/backporting",
baseUrl: apiUrl
});
}

View file

@ -0,0 +1,157 @@
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 GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper";
import axios, { Axios } from "axios";
import https from "https";
export default class GitLabClient implements GitClient {
private readonly logger: LoggerService;
private readonly apiUrl: string;
private readonly mapper: GitLabMapper;
private readonly client: Axios;
constructor(token: string, apiUrl: string, rejectUnauthorized = false) {
this.logger = LoggerServiceFactory.getLogger();
this.apiUrl = apiUrl;
this.client = axios.create({
baseURL: this.apiUrl,
headers: {
Authorization: `Bearer ${token}`,
"User-Agent": "lampajr/backporting",
},
httpsAgent: new https.Agent({
rejectUnauthorized
})
});
this.mapper = new GitLabMapper(this.client);
}
// READ
// example: <host>/api/v4/projects/alampare%2Fbackporting-example/merge_requests/1
async getPullRequest(namespace: string, repo: string, mrNumber: number): 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);
}
getPullRequestFromUrl(mrUrl: string): Promise<GitPullRequest> {
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
return this.getPullRequest(namespace, project, id);
}
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<string> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}.`);
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
const projectId = this.getProjectId(backport.owner, backport.repo);
const { data } = await this.client.post(`/projects/${projectId}/merge_requests`, {
source_branch: backport.head,
target_branch: backport.base,
title: backport.title,
description: backport.body,
reviewer_ids: [],
assignee_ids: [],
});
const mr = data as MergeRequestSchema;
// reviewers
const reviewerIds: number[] = [];
for(const r of backport.reviewers) {
try {
this.logger.debug("Retrieving user: " + r);
const user = await this.getUser(r);
reviewerIds.push(user.id);
} catch(error) {
this.logger.warn(`Failed to retrieve reviewer ${r}`);
}
}
if (reviewerIds.length > 0) {
try {
this.logger.info("Setting reviewers: " + reviewerIds);
await this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, {
reviewer_ids: reviewerIds.filter(r => r !== undefined),
});
} catch(error) {
this.logger.warn("Failure trying to update reviewers. " + error);
}
}
// assignees
const assigneeIds: number[] = [];
for(const a of backport.assignees) {
try {
this.logger.debug("Retrieving user: " + a);
const user = await this.getUser(a);
assigneeIds.push(user.id);
} catch(error) {
this.logger.warn(`Failed to retrieve assignee ${a}`);
}
}
if (assigneeIds.length > 0) {
try {
this.logger.info("Setting assignees: " + assigneeIds);
await this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, {
assignee_ids: assigneeIds.filter(a => a !== undefined),
});
} catch(error) {
this.logger.warn("Failure trying to update assignees. " + error);
}
}
return mr.web_url;
}
/**
* Retrieve a gitlab user given its username
* @param username
* @returns UserSchema
*/
private async getUser(username: string): Promise<UserSchema> {
const { data } = await this.client.get(`/users?username=${username}`);
const users = data as UserSchema[];
if (users.length > 1) {
throw new Error("Too many users found with username=" + username);
}
if (users.length == 0) {
throw new Error("User " + username + " not found");
}
return users[0];
}
/**
* Extract repository namespace, project and mr number from the merge request url
* example: <host>/alampare/backporting-example/-/merge_requests/1
* note: "-/" could be omitted
* @param mrUrl merge request url
* @returns {{owner: string, project: string}}
*/
private extractMergeRequestData(mrUrl: string): {namespace: string, project: string, id: number} {
const elems: string[] = mrUrl.replace("/-/", "/").split("/");
return {
namespace: elems[elems.length - 4],
project: elems[elems.length - 3],
id: parseInt(mrUrl.substring(mrUrl.lastIndexOf("/") + 1, mrUrl.length)),
};
}
private getProjectId(namespace: string, repo: string) {
// e.g., <namespace>%2F<repo>
return encodeURIComponent(`${namespace}/${repo}`);
}
}

View file

@ -0,0 +1,83 @@
import { GitPullRequest, GitRepoState, GitRepository } from "@bp/service/git/git.types";
import GitResponseMapper from "@bp/service/git/git-mapper";
import { MergeRequestSchema, ProjectSchema } from "@gitbeaker/rest";
import { Axios } from "axios";
export default class GitLabMapper implements GitResponseMapper<MergeRequestSchema, string> {
private readonly client;
// needs client to perform additional requests
constructor(client: Axios) {
this.client = client;
}
mapGitState(state: string): GitRepoState {
switch (state) {
case "opened":
return GitRepoState.OPEN;
case "closed":
return GitRepoState.CLOSED;
case "merged":
return GitRepoState.MERGED;
default:
return GitRepoState.LOCKED;
}
}
async mapPullRequest(mr: MergeRequestSchema): Promise<GitPullRequest> {
// throw new Error("Method not implemented.");
return {
number: mr.iid,
author: mr.author.username,
url: mr.web_url,
htmlUrl: mr.web_url,
title: mr.title,
body: mr.description,
state: this.mapGitState(mr.state),
merged: this.isMerged(mr),
mergedBy: mr.merged_by?.username,
reviewers: mr.reviewers?.map((r => r.username)) ?? [],
assignees: mr.assignees?.map((r => r.username)) ?? [],
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
nCommits: 1, // info not present on 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 as string] : [mr.sha]
};
}
async mapSourceRepo(mr: MergeRequestSchema): Promise<GitRepository> {
const project: ProjectSchema = await this.getProject(mr.source_project_id);
return {
owner: project.namespace.full_path, // or just proj.path?
project: project.path,
cloneUrl: project.http_url_to_repo,
};
}
async mapTargetRepo(mr: MergeRequestSchema): Promise<GitRepository> {
const project: ProjectSchema = await this.getProject(mr.target_project_id);
return {
owner: project.namespace.full_path, // or just proj.path?
project: project.path,
cloneUrl: project.http_url_to_repo,
};
}
private isMerged(mr: MergeRequestSchema) {
return this.mapGitState(mr.state) === GitRepoState.MERGED;
}
private async getProject(projectId: number): Promise<ProjectSchema> {
const { data } = await this.client.get(`/projects/${projectId}`);
if (!data) {
throw new Error(`Project ${projectId} not found`);
}
return data as ProjectSchema;
}
}

View file

@ -3,10 +3,12 @@ import LoggerService from "@bp/service/logger/logger-service";
export default class ConsoleLoggerService implements LoggerService {
private readonly logger;
private readonly logger: Logger;
private readonly verbose: boolean;
constructor() {
constructor(verbose = true) {
this.logger = new Logger();
this.verbose = verbose;
}
trace(message: string): void {
@ -14,7 +16,9 @@ export default class ConsoleLoggerService implements LoggerService {
}
debug(message: string): void {
this.logger.log("[DEBUG]", message);
if (this.verbose) {
this.logger.log("[DEBUG]", message);
}
}
info(message: string): void {

View file

@ -3,11 +3,12 @@ import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitCLIService from "@bp/service/git/git-cli";
import GitService from "@bp/service/git/git-service";
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { BackportPullRequest, GitPullRequest, GitServiceType } from "@bp/service/git/git.types";
import GitClient from "@bp/service/git/git-client";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { inferGitClient, inferGitApiUrl } from "@bp/service/git/git-util";
/**
* Main runner implementation, it implements the core logic flow
@ -22,22 +23,6 @@ export default class Runner {
this.argsParser = parser;
}
/**
* Infer the remote GIT service to interact with based on the provided
* pull request URL
* @param prUrl provided pull request URL
* @returns {GitServiceType}
*/
private inferRemoteGitService(prUrl: string): GitServiceType {
const stdPrUrl = prUrl.toLowerCase().trim();
if (stdPrUrl.includes(GitServiceType.GITHUB.toString())) {
return GitServiceType.GITHUB;
}
throw new Error(`Remote GIT service not recognixed from PR url: ${prUrl}`);
}
/**
* Entry point invoked by the command line or gha
*/
@ -69,10 +54,13 @@ export default class Runner {
}
// 2. init git service
GitServiceFactory.getOrCreate(this.inferRemoteGitService(args.pullRequest), args.auth);
const gitApi: GitService = GitServiceFactory.getService();
const gitClientType: GitClientType = inferGitClient(args.pullRequest);
// right now the apiVersion is set to v4
const apiUrl = inferGitApiUrl(args.pullRequest);
const gitApi: GitClient = GitClientFactory.getOrCreate(gitClientType, args.auth, apiUrl);
// 3. parse configs
this.logger.debug("Parsing configs..");
const configs: Configs = await new PullRequestConfigsParser().parseAndValidate(args);
const originalPR: GitPullRequest = configs.originalPullRequest;
const backportPR: GitPullRequest = configs.backportPullRequest;
@ -81,19 +69,24 @@ export default class Runner {
const git: GitCLIService = new GitCLIService(configs.auth, configs.git);
// 4. clone the repository
this.logger.debug("Cloning repo..");
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch);
// 5. create new branch from target one and checkout
this.logger.debug("Creating local branch..");
const backportBranch = backportPR.branchName ?? `bp-${configs.targetBranch}-${originalPR.commits!.join("-")}`;
await git.createLocalBranch(configs.folder, backportBranch);
// 6. fetch pull request remote if source owner != target owner or pull request still open
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
configs.originalPullRequest.state === "open") {
await git.fetch(configs.folder, `pull/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
this.logger.debug("Fetching pull request remote..");
const prefix = gitClientType === GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab
await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
}
// 7. apply all changes to the new branch
this.logger.debug("Cherry picking commits..");
for (const sha of originalPR.commits!) {
await git.cherryPick(configs.folder, sha);
}
@ -114,7 +107,8 @@ export default class Runner {
await git.push(configs.folder, backportBranch);
// 9. create pull request new branch -> target branch (using octokit)
await gitApi.createPullRequest(backport);
const prUrl = await gitApi.createPullRequest(backport);
this.logger.info(`Pull request created: ${prUrl}`);
} else {
this.logger.warn("Pull request creation and remote push skipped!");

View file

@ -1,12 +1,12 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { GitServiceType } from "@bp/service/git/git.types";
import { setupMoctokit } from "../../../support/moctokit/moctokit-support";
import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner } from "../../../support/moctokit/moctokit-data";
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 { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data";
describe("pull request config parser", () => {
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}`;
@ -15,11 +15,12 @@ describe("pull request config parser", () => {
let parser: PullRequestConfigsParser;
beforeAll(() => {
GitServiceFactory.getOrCreate(GitServiceType.GITHUB, "whatever");
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3");
});
beforeEach(() => {
setupMoctokit();
mockGitHubClient("http://localhost/api/v3");
parser = new PullRequestConfigsParser();
});
@ -124,30 +125,6 @@ describe("pull request config parser", () => {
});
});
test("override author", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
});
test("still open pull request", async () => {
const args: Args = {
dryRun: true,
@ -177,7 +154,7 @@ describe("pull request config parser", () => {
htmlUrl: "https://github.com/owner/reponame/pull/4444",
state: "open",
merged: false,
mergedBy: "that-s-a-user",
mergedBy: undefined,
title: "PR Title",
body: "Please review and merge",
reviewers: ["gh-user"],

View file

@ -0,0 +1,420 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { getAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR } from "../../../support/mock/gitlab-data";
jest.mock("axios", () => {
return {
create: jest.fn(() => ({
get: getAxiosMocked,
})),
};
});
describe("gitlab merge request config parser", () => {
const mergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`;
const openPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${OPEN_MR.iid}`;
const notMergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${CLOSED_NOT_MERGED_MR.iid}`;
let parser: PullRequestConfigsParser;
beforeAll(() => {
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "my.gitlab.host.com");
});
beforeEach(() => {
parser = new PullRequestConfigsParser();
});
afterEach(() => {
jest.clearAllMocks();
});
test("parse configs from merge request", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "GitLab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitLab",
email: "noreply@gitlab.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
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"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "GitLab",
url: undefined,
htmlUrl: undefined,
title: "[prod] Update test.txt",
body: "**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1\r\n\r\nThis is the body",
reviewers: ["superuser"],
assignees: [],
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,
});
});
test("override folder", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: mergedPRUrl,
targetBranch: "prod",
folder: "/tmp/test",
gitUser: "GitLab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual("/tmp/test");
expect(configs.git).toEqual({
user: "GitLab",
email: "noreply@gitlab.com"
});
});
test("still open pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: openPRUrl,
targetBranch: "prod",
gitUser: "GitLab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
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"],
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: 1,
// taken from mr.sha
commits: ["9e15674ebd48e05c6e428a1fa31dbb60a778d644"]
});
});
test("closed pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: notMergedPRUrl,
targetBranch: "prod",
gitUser: "GitLab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
expect(async () => await parser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!");
});
test("override backport pr data inherting reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
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"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["superuser"],
assignees: [],
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,
});
});
test("override backport pr reviewers and assignees", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
inheritReviewers: true, // not taken into account
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
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"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
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,
});
});
test("override backport pr empty reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
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"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
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,
});
});
});

View file

@ -0,0 +1,34 @@
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import GitHubClient from "@bp/service/git/github/github-client";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
describe("git client factory test", () => {
beforeEach(() => {
// reset git service
GitClientFactory.reset();
});
test("correctly create github client", () => {
const client = GitClientFactory.getOrCreate(GitClientType.GITHUB, "auth", "apiurl");
expect(client).toBeInstanceOf(GitHubClient);
});
test("correctly create gitlab client", () => {
const client = GitClientFactory.getOrCreate(GitClientType.GITLAB, "auth", "apiurl");
expect(client).toBeInstanceOf(GitLabClient);
});
test("check get service github", () => {
const create = GitClientFactory.getOrCreate(GitClientType.GITHUB, "auth", "apiurl");
const get = GitClientFactory.getClient();
expect(create).toStrictEqual(get);
});
test("check get service gitlab", () => {
const create = GitClientFactory.getOrCreate(GitClientType.GITLAB, "auth", "apiurl");
const get = GitClientFactory.getClient();
expect(create).toStrictEqual(get);
});
});

View file

@ -0,0 +1,37 @@
import { inferGitApiUrl, inferGitClient } from "@bp/service/git/git-util";
import { GitClientType } from "@bp/service/git/git.types";
describe("check git utilities", () => {
test("check infer gitlab api", ()=> {
expect(inferGitApiUrl("https://my.gitlab.awesome.com/superuser/backporting-example/-/merge_requests/4")).toStrictEqual("https://my.gitlab.awesome.com/api/v4");
});
test("check infer gitlab api with different version", ()=> {
expect(inferGitApiUrl("http://my.gitlab.awesome.com/superuser/backporting-example/-/merge_requests/4", "v2")).toStrictEqual("http://my.gitlab.awesome.com/api/v2");
});
test("check infer github api", ()=> {
expect(inferGitApiUrl("https://github.com/superuser/backporting-example/pull/4")).toStrictEqual("https://api.github.com");
});
test("check infer custom github api", ()=> {
expect(inferGitApiUrl("http://github.acme-inc.com/superuser/backporting-example/pull/4")).toStrictEqual("http://github.acme-inc.com/api/v4");
});
test("check infer custom github api with different version", ()=> {
expect(inferGitApiUrl("http://github.acme-inc.com/superuser/backporting-example/pull/4", "v3")).toStrictEqual("http://github.acme-inc.com/api/v3");
});
test("check infer github client", ()=> {
expect(inferGitClient("https://github.com/superuser/backporting-example/-/merge_requests/4")).toStrictEqual(GitClientType.GITHUB);
});
test("check infer gitlab client", ()=> {
expect(inferGitClient("https://my.gitlab.awesome.com/superuser/backporting-example/-/merge_requests/4")).toStrictEqual(GitClientType.GITLAB);
});
test("Not recognized git client type", ()=> {
expect(() => inferGitClient("https://not.recognized/superuser/backporting-example/-/merge_requests/4")).toThrowError("Remote git service not recognized from pr url: https://not.recognized/superuser/backporting-example/-/merge_requests/4");
});
});

View file

@ -1,27 +1,28 @@
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { GitPullRequest, GitServiceType } from "@bp/service/git/git.types";
import GitHubService from "@bp/service/git/github/github-service";
import { mergedPullRequestFixture, repo, targetOwner } from "../../../support/moctokit/moctokit-data";
import { setupMoctokit } from "../../../support/moctokit/moctokit-support";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitPullRequest, GitClientType } from "@bp/service/git/git.types";
import GitHubClient from "@bp/service/git/github/github-client";
import { mergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
describe("github service", () => {
let gitService: GitHubService;
let gitClient: GitHubClient;
beforeAll(() => {
// init git service
GitServiceFactory.getOrCreate(GitServiceType.GITHUB, "whatever");
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3");
});
beforeEach(() => {
// mock github api calls
setupMoctokit();
mockGitHubClient("http://localhost/api/v3");
gitService = GitServiceFactory.getService() as GitHubService;
gitClient = GitClientFactory.getClient() as GitHubClient;
});
test("get pull request: success", async () => {
const res: GitPullRequest = await gitService.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number);
const res: GitPullRequest = await gitClient.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number);
expect(res.sourceRepo).toEqual({
owner: "fork",
project: "reponame",

View file

@ -0,0 +1,249 @@
import GitClientFactory from "@bp/service/git/git-client-factory";
import { NEW_GITLAB_MR_ID, SECOND_NEW_GITLAB_MR_ID, getAxiosMocked, postAxiosMocked, putAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import axios from "axios";
jest.mock("axios");
const axiosSpy = axios.create as jest.Mock;
let axiosInstanceSpy: {[key: string]: jest.Func};
function setupAxiosSpy() {
const getSpy = jest.fn(getAxiosMocked);
const postSpy = jest.fn(postAxiosMocked);
const putSpy = jest.fn(putAxiosMocked);
const axiosInstance = {
get: getSpy,
post: postSpy,
put: putSpy,
};
axiosSpy.mockImplementation(() => (axiosInstance));
return axiosInstance;
}
describe("github service", () => {
let gitClient: GitLabClient;
beforeEach(() => {
axiosInstanceSpy = setupAxiosSpy();
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);
// check content
expect(res.sourceRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.targetRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.title).toBe("Update test.txt");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["ebb1eca696c42fd067658bd9b5267709f78ef38e"]);
// check axios invocation
expect(axiosInstanceSpy.get).toBeCalledTimes(3); // merge request and 2 repos
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/1");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
});
test("get open pull request", async () => {
const res: GitPullRequest = await gitClient.getPullRequest("superuser", "backporting-example", 2);
expect(res.sourceRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.targetRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.title).toBe("Update test.txt opened");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["9e15674ebd48e05c6e428a1fa31dbb60a778d644"]);
// check axios invocation
expect(axiosInstanceSpy.get).toBeCalledTimes(3); // merge request and 2 repos
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/2");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
});
test("create backport pull request without reviewers and assignees", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch",
reviewers: [],
assignees: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(0); // no reviewers nor assignees
expect(axiosInstanceSpy.put).toBeCalledTimes(0); // no reviewers nor assignees
});
test("create backport pull request with reviewers", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch",
reviewers: ["superuser", "invalid"],
assignees: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just reviewers, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just reviewers
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + NEW_GITLAB_MR_ID, {
reviewer_ids: [14041],
});
});
test("create backport pull request with assignees", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch",
reviewers: [],
assignees: ["superuser", "invalid"],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just assignees, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just assignees
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + NEW_GITLAB_MR_ID, {
assignee_ids: [14041],
});
});
test("create backport pull request with failure assigning reviewers", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch-2",
reviewers: ["superuser", "invalid"],
assignees: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch-2",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just reviewers, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just reviewers
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID, {
reviewer_ids: [14041],
});
});
test("create backport pull request with failure assigning assignees", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch-2",
reviewers: [],
assignees: ["superuser", "invalid"],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch-2",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just assignees, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just assignees
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID, {
assignee_ids: [14041],
});
});
});

View file

@ -1,19 +1,19 @@
import ArgsParser from "@bp/service/args/args-parser";
import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitHubService from "@bp/service/git/github/github-service";
import GitHubClient from "@bp/service/git/github/github-client";
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import { addProcessArgs, resetProcessArgs } from "../../support/utils";
import { setupMoctokit } from "../../support/moctokit/moctokit-support";
import { mockGitHubClient } from "../../support/mock/git-client-mock-support";
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitHubService.prototype, "createPullRequest");
jest.spyOn(GitHubClient.prototype, "createPullRequest");
let parser: ArgsParser;
let runner: Runner;
beforeEach(() => {
setupMoctokit();
mockGitHubClient();
// create CLI arguments parser
parser = new CLIArgsParser();
@ -30,6 +30,7 @@ afterEach(() => {
});
describe("cli runner", () => {
test("with dry run", async () => {
addProcessArgs([
"-d",
@ -56,7 +57,7 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("overriding author", async () => {
@ -85,7 +86,7 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("with relative folder", async () => {
@ -119,7 +120,7 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("with absolute folder", async () => {
@ -150,7 +151,7 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
@ -180,8 +181,8 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
@ -220,8 +221,8 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
@ -272,15 +273,15 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-91748965051fae1330ad58d15cf694e103267c87",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
reviewers: ["gh-user", "that-s-a-user"],
reviewers: ["gh-user"],
assignees: [],
}
);
@ -325,8 +326,8 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp_branch_name",
@ -377,8 +378,8 @@ describe("cli runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp_branch_name",

View file

@ -0,0 +1,309 @@
import ArgsParser from "@bp/service/args/args-parser";
import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import { addProcessArgs, resetProcessArgs } from "../../support/utils";
import { getAxiosMocked } from "../../support/mock/git-client-mock-support";
jest.mock("axios", () => {
return {
create: () => ({
get: getAxiosMocked,
post: () => ({
data: {
iid: 1, // FIXME: I am not testing this atm
}
}),
put: jest.fn(),
}),
};
});
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitLabClient.prototype, "createPullRequest");
let parser: ArgsParser;
let runner: Runner;
beforeEach(() => {
// create CLI arguments parser
parser = new CLIArgsParser();
// create runner
runner = new Runner(parser);
});
afterEach(() => {
jest.clearAllMocks();
// reset process.env variables
resetProcessArgs();
});
describe("cli runner", () => {
test("with dry run", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
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-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("dry run with relative folder", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"-f",
"folder"
]);
await runner.execute();
const cwd = process.cwd() + "/folder";
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-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
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-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-9e15674ebd48e05c6e428a1fa31dbb60a778d644",
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: [],
}
);
});
test("closed and not merged pull request", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"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!");
});
test("merged pull request", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
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-ebb1eca696c42fd067658bd9b5267709f78ef38e");
// 0 occurrences as the mr is already merged and the owner is the same for
// both source and target repositories
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
}
);
});
test("override backporting pr data", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"--title",
"New Title",
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"--bp-branch-name",
"bp_branch_name",
"--reviewers",
"user1,user2",
"--assignees",
"user3,user4"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
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_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
}
);
});
test("set empty reviewers", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"--title",
"New Title",
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"--bp-branch-name",
"bp_branch_name",
"--no-inherit-reviewers",
"--assignees",
"user3,user4",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
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_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: [],
assignees: ["user3", "user4"],
}
);
});
});

View file

@ -1,19 +1,19 @@
import ArgsParser from "@bp/service/args/args-parser";
import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitHubService from "@bp/service/git/github/github-service";
import GitHubClient from "@bp/service/git/github/github-client";
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
import { spyGetInput } from "../../support/utils";
import { setupMoctokit } from "../../support/moctokit/moctokit-support";
import { mockGitHubClient } from "../../support/mock/git-client-mock-support";
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitHubService.prototype, "createPullRequest");
jest.spyOn(GitHubClient.prototype, "createPullRequest");
let parser: ArgsParser;
let runner: Runner;
beforeEach(() => {
setupMoctokit();
mockGitHubClient();
// create GHA arguments parser
parser = new GHAArgsParser();
@ -51,7 +51,7 @@ describe("gha runner", () => {
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
@ -79,8 +79,8 @@ describe("gha runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
@ -127,15 +127,15 @@ describe("gha runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-91748965051fae1330ad58d15cf694e103267c87",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
reviewers: ["gh-user", "that-s-a-user"],
reviewers: ["gh-user"],
assignees: [],
}
);
@ -172,8 +172,8 @@ describe("gha runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp_branch_name",
@ -218,8 +218,8 @@ describe("gha runner", () => {
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp_branch_name",

View file

@ -0,0 +1,245 @@
import ArgsParser from "@bp/service/args/args-parser";
import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
import { spyGetInput } from "../../support/utils";
import { getAxiosMocked } from "../../support/mock/git-client-mock-support";
jest.mock("axios", () => {
return {
create: () => ({
get: getAxiosMocked,
post: () => ({
data: {
iid: 1, // FIXME: I am not testing this atm
}
}),
put: jest.fn(),
}),
};
});
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitLabClient.prototype, "createPullRequest");
let parser: ArgsParser;
let runner: Runner;
beforeEach(() => {
// create GHA arguments parser
parser = new GHAArgsParser();
// create runner
runner = new Runner(parser);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("gha runner", () => {
test("with dry run", async () => {
spyGetInput({
"dry-run": "true",
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
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-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
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-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-9e15674ebd48e05c6e428a1fa31dbb60a778d644",
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: [],
}
);
});
test("closed and not merged pull request", async () => {
spyGetInput({
"target-branch": "target",
"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!");
});
test("merged pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
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-ebb1eca696c42fd067658bd9b5267709f78ef38e");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
}
);
});
test("override backporting pr data", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "user1, user2",
"assignees": "user3, user4",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
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_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
}
);
});
test("set empty reviewers", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "",
"assignees": "user3, user4",
"no-inherit-reviewers": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
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_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: [],
assignees: ["user3", "user4"],
}
);
});
});

View file

@ -0,0 +1,168 @@
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";
const logger = LoggerServiceFactory.getLogger();
// AXIOS
export const getAxiosMocked = (url: string) => {
let data = undefined;
// gitlab
if (url.endsWith("merge_requests/1")) {
data = MERGED_SQUASHED_MR;
} else if (url.endsWith("merge_requests/2")) {
data = OPEN_MR;
} else if (url.endsWith("merge_requests/3")) {
data = CLOSED_NOT_MERGED_MR;
} else if (url.endsWith("projects/76316")) {
data = PROJECT_EXAMPLE;
} else if (url.endsWith("users?username=superuser")) {
data = [SUPERUSER];
}
return {
data,
status: data ? 200 : 404,
};
};
export const NEW_GITLAB_MR_ID = 999;
export const SECOND_NEW_GITLAB_MR_ID = 1000;
export const postAxiosMocked = (_url: string, data?: {source_branch: string,}) => {
let responseData = undefined;
// gitlab
if (data?.source_branch === "bp-branch") {
responseData = {
// we do not need the whole response
iid: NEW_GITLAB_MR_ID,
web_url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID
};
} if (data?.source_branch === "bp-branch-2") {
responseData = {
// we do not need the whole response
iid: SECOND_NEW_GITLAB_MR_ID,
web_url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID
};
}
return {
data: responseData,
status: responseData ? 200 : 404,
};
};
export const putAxiosMocked = (url: string, _data?: unknown) => {
const responseData = undefined;
// gitlab
if (url.endsWith(`merge_requests/${NEW_GITLAB_MR_ID}`)) {
return {
data: {
iid: NEW_GITLAB_MR_ID,
},
status: responseData ? 200 : 404,
};
}
throw new Error("Error updating merge request: " + url);
};
// GITHUB - OCTOKIT
export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => {
logger.debug("Setting up moctokit.");
const mock = new Moctokit(apiUrl);
// setup the mock requests here
// valid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: mergedPullRequestFixture.number
})
.reply({
status: 200,
data: mergedPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: sameOwnerPullRequestFixture.number
})
.reply({
status: 200,
data: sameOwnerPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: openPullRequestFixture.number
})
.reply({
status: 200,
data: openPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notMergedPullRequestFixture.number
})
.reply({
status: 200,
data: notMergedPullRequestFixture
});
mock.rest.pulls
.create()
.reply({
status: 201,
data: mergedPullRequestFixture
});
mock.rest.pulls
.requestReviewers()
.reply({
status: 201,
data: mergedPullRequestFixture
});
mock.rest.issues
.addAssignees()
.reply({
status: 201,
data: {}
});
// invalid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notFoundPullRequestNumber
})
.reply({
status: 404,
data: {
message: "Not found"
}
});
return mock;
};

View file

@ -880,26 +880,7 @@ export const openPullRequestFixture = {
"mergeable": null,
"rebaseable": null,
"mergeable_state": "unknown",
"merged_by": {
"login": "that-s-a-user",
"id": 17157711,
"node_id": "MDQ6VXNlcjE3MTU3NzEx",
"avatar_url": "https://avatars.githubusercontent.com/u/17157711?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/that-s-a-user",
"html_url": "https://github.com/that-s-a-user",
"followers_url": "https://api.github.com/users/that-s-a-user/followers",
"following_url": "https://api.github.com/users/that-s-a-user/following{/other_user}",
"gists_url": "https://api.github.com/users/that-s-a-user/gists{/gist_id}",
"starred_url": "https://api.github.com/users/that-s-a-user/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/that-s-a-user/subscriptions",
"organizations_url": "https://api.github.com/users/that-s-a-user/orgs",
"repos_url": "https://api.github.com/users/that-s-a-user/repos",
"events_url": "https://api.github.com/users/that-s-a-user/events{/privacy}",
"received_events_url": "https://api.github.com/users/that-s-a-user/received_events",
"type": "User",
"site_admin": false
},
"merged_by": {},
"comments": 0,
"review_comments": 0,
"maintainer_can_modify": false,

View file

@ -0,0 +1,539 @@
// <host>/api/v4/projects/superuser%2Fbackporting-example/merge_requests/1
// <host>/api/v4/projects/76316
export const PROJECT_EXAMPLE = {
"id":76316,
"description":null,
"name":"Backporting Example",
"name_with_namespace":"Super User / Backporting Example",
"path":"backporting-example",
"path_with_namespace":"superuser/backporting-example",
"created_at":"2023-06-23T13:45:15.121Z",
"default_branch":"main",
"tag_list":[
],
"topics":[
],
"ssh_url_to_repo":"git@my.gitlab.host.com:superuser/backporting-example.git",
"http_url_to_repo":"https://my.gitlab.host.com/superuser/backporting-example.git",
"web_url":"https://my.gitlab.host.com/superuser/backporting-example",
"readme_url":"https://my.gitlab.host.com/superuser/backporting-example/-/blob/main/README.md",
"forks_count":0,
"avatar_url":null,
"star_count":0,
"last_activity_at":"2023-06-28T14:05:42.596Z",
"namespace":{
"id":70747,
"name":"Super User",
"path":"superuser",
"kind":"user",
"full_path":"superuser",
"parent_id":null,
"avatar_url":"/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"_links":{
"self":"https://my.gitlab.host.com/api/v4/projects/76316",
"issues":"https://my.gitlab.host.com/api/v4/projects/76316/issues",
"merge_requests":"https://my.gitlab.host.com/api/v4/projects/76316/merge_requests",
"repo_branches":"https://my.gitlab.host.com/api/v4/projects/76316/repository/branches",
"labels":"https://my.gitlab.host.com/api/v4/projects/76316/labels",
"events":"https://my.gitlab.host.com/api/v4/projects/76316/events",
"members":"https://my.gitlab.host.com/api/v4/projects/76316/members",
"cluster_agents":"https://my.gitlab.host.com/api/v4/projects/76316/cluster_agents"
},
"packages_enabled":true,
"empty_repo":false,
"archived":false,
"visibility":"private",
"owner":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"resolve_outdated_diff_discussions":false,
"container_expiration_policy":{
"cadence":"1d",
"enabled":false,
"keep_n":10,
"older_than":"90d",
"name_regex":".*",
"name_regex_keep":null,
"next_run_at":"2023-06-24T13:45:15.167Z"
},
"issues_enabled":true,
"merge_requests_enabled":true,
"wiki_enabled":true,
"jobs_enabled":true,
"snippets_enabled":true,
"container_registry_enabled":true,
"service_desk_enabled":false,
"service_desk_address":null,
"can_create_merge_request_in":true,
"issues_access_level":"enabled",
"repository_access_level":"enabled",
"merge_requests_access_level":"enabled",
"forking_access_level":"enabled",
"wiki_access_level":"enabled",
"builds_access_level":"enabled",
"snippets_access_level":"enabled",
"pages_access_level":"private",
"analytics_access_level":"enabled",
"container_registry_access_level":"enabled",
"security_and_compliance_access_level":"private",
"releases_access_level":"enabled",
"environments_access_level":"enabled",
"feature_flags_access_level":"enabled",
"infrastructure_access_level":"enabled",
"monitor_access_level":"enabled",
"emails_disabled":null,
"shared_runners_enabled":true,
"lfs_enabled":true,
"creator_id":14041,
"import_url":null,
"import_type":null,
"import_status":"none",
"import_error":null,
"open_issues_count":0,
"description_html":"",
"updated_at":"2023-06-28T14:05:42.596Z",
"ci_default_git_depth":20,
"ci_forward_deployment_enabled":true,
"ci_job_token_scope_enabled":false,
"ci_separated_caches":true,
"ci_allow_fork_pipelines_to_run_in_parent_project":true,
"build_git_strategy":"fetch",
"keep_latest_artifact":true,
"restrict_user_defined_variables":false,
"runners_token":"GR13489419z7QQ54AUgJaNMFD5asU",
"runner_token_expiration_interval":null,
"group_runners_enabled":true,
"auto_cancel_pending_pipelines":"enabled",
"build_timeout":3600,
"auto_devops_enabled":false,
"auto_devops_deploy_strategy":"continuous",
"ci_config_path":"",
"public_jobs":true,
"shared_with_groups":[
],
"only_allow_merge_if_pipeline_succeeds":false,
"allow_merge_on_skipped_pipeline":null,
"request_access_enabled":true,
"only_allow_merge_if_all_discussions_are_resolved":false,
"remove_source_branch_after_merge":true,
"printing_merge_request_link_enabled":true,
"merge_method":"merge",
"squash_option":"default_off",
"enforce_auth_checks_on_uploads":true,
"suggestion_commit_message":null,
"merge_commit_template":null,
"squash_commit_template":null,
"issue_branch_template":null,
"autoclose_referenced_issues":true,
"approvals_before_merge":0,
"mirror":false,
"external_authorization_classification_label":null,
"marked_for_deletion_at":null,
"marked_for_deletion_on":null,
"requirements_enabled":false,
"requirements_access_level":"enabled",
"security_and_compliance_enabled":true,
"compliance_frameworks":[
],
"issues_template":null,
"merge_requests_template":null,
"merge_pipelines_enabled":false,
"merge_trains_enabled":false,
"allow_pipeline_trigger_approve_deployment":false,
"permissions":{
"project_access":{
"access_level":50,
"notification_level":3
},
"group_access":null
}
};
export const MERGED_SQUASHED_MR = {
"id":807106,
"iid":1,
"project_id":76316,
"title":"Update test.txt",
"description":"This is the body",
"state":"merged",
"created_at":"2023-06-28T14:32:40.943Z",
"updated_at":"2023-06-28T14:37:12.108Z",
"merged_by":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"merge_user":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"merged_at":"2023-06-28T14:37:11.667Z",
"closed_by":null,
"closed_at":null,
"target_branch":"main",
"source_branch":"feature",
"user_notes_count":0,
"upvotes":0,
"downvotes":0,
"author":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"assignees":[
{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"assignee":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"reviewers":[
{
"id":1404188,
"username":"superuser1",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
{
"id":1404199,
"username":"superuser2",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"source_project_id":76316,
"target_project_id":76316,
"labels":[
],
"draft":false,
"work_in_progress":false,
"milestone":null,
"merge_when_pipeline_succeeds":false,
"merge_status":"can_be_merged",
"detailed_merge_status":"not_open",
"sha":"9e15674ebd48e05c6e428a1fa31dbb60a778d644",
"merge_commit_sha":"4d369c3e9a8d1d5b7e56c892a8ab2a7666583ac3",
"squash_commit_sha":"ebb1eca696c42fd067658bd9b5267709f78ef38e",
"discussion_locked":null,
"should_remove_source_branch":true,
"force_remove_source_branch":true,
"reference":"!2",
"references":{
"short":"!2",
"relative":"!2",
"full":"superuser/backporting-example!2"
},
"web_url":"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"time_stats":{
"time_estimate":0,
"total_time_spent":0,
"human_time_estimate":null,
"human_total_time_spent":null
},
"squash":true,
"squash_on_merge":true,
"task_completion_status":{
"count":0,
"completed_count":0
},
"has_conflicts":false,
"blocking_discussions_resolved":true,
"approvals_before_merge":null,
"subscribed":true,
"changes_count":"1",
"latest_build_started_at":null,
"latest_build_finished_at":null,
"first_deployed_to_production_at":null,
"pipeline":null,
"head_pipeline":null,
"diff_refs":{
"base_sha":"2c553a0c4c133a51806badce5fa4842b7253cb3b",
"head_sha":"9e15674ebd48e05c6e428a1fa31dbb60a778d644",
"start_sha":"2c553a0c4c133a51806badce5fa4842b7253cb3b"
},
"merge_error":null,
"first_contribution":false,
"user":{
"can_merge":true
}
};
export const OPEN_MR = {
"id":807106,
"iid":2,
"project_id":76316,
"title":"Update test.txt opened",
"description":"Still opened mr body",
"state":"opened",
"created_at":"2023-06-28T14:32:40.943Z",
"updated_at":"2023-06-28T14:35:56.433Z",
"merged_by":null,
"merge_user":null,
"merged_at":null,
"closed_by":null,
"closed_at":null,
"target_branch":"main",
"source_branch":"feature",
"user_notes_count":0,
"upvotes":0,
"downvotes":0,
"author":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"assignees":[
{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"assignee":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"reviewers":[
{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"source_project_id":76316,
"target_project_id":76316,
"labels":[
],
"draft":false,
"work_in_progress":false,
"milestone":null,
"merge_when_pipeline_succeeds":false,
"merge_status":"checking",
"detailed_merge_status":"checking",
"sha":"9e15674ebd48e05c6e428a1fa31dbb60a778d644",
"merge_commit_sha":null,
"squash_commit_sha":null,
"discussion_locked":null,
"should_remove_source_branch":null,
"force_remove_source_branch":true,
"reference":"!2",
"references":{
"short":"!2",
"relative":"!2",
"full":"superuser/backporting-example!2"
},
"web_url":"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"time_stats":{
"time_estimate":0,
"total_time_spent":0,
"human_time_estimate":null,
"human_total_time_spent":null
},
"squash":false,
"squash_on_merge":false,
"task_completion_status":{
"count":0,
"completed_count":0
},
"has_conflicts":false,
"blocking_discussions_resolved":true,
"approvals_before_merge":null,
"subscribed":true,
"changes_count":"1",
"latest_build_started_at":null,
"latest_build_finished_at":null,
"first_deployed_to_production_at":null,
"pipeline":null,
"head_pipeline":null,
"diff_refs":{
"base_sha":"2c553a0c4c133a51806badce5fa4842b7253cb3b",
"head_sha":"9e15674ebd48e05c6e428a1fa31dbb60a778d644",
"start_sha":"2c553a0c4c133a51806badce5fa4842b7253cb3b"
},
"merge_error":null,
"first_contribution":false,
"user":{
"can_merge":true
}
};
export const CLOSED_NOT_MERGED_MR = {
"id":807191,
"iid":3,
"project_id":76316,
"title":"Update test.txt",
"description":"",
"state":"closed",
"created_at":"2023-06-28T15:44:50.549Z",
"updated_at":"2023-06-28T15:44:58.318Z",
"merged_by":null,
"merge_user":null,
"merged_at":null,
"closed_by":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"closed_at":"2023-06-28T15:44:58.349Z",
"target_branch":"main",
"source_branch":"closed",
"user_notes_count":0,
"upvotes":0,
"downvotes":0,
"author":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"assignees":[
{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"assignee":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"reviewers":[
{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"source_project_id":76316,
"target_project_id":76316,
"labels":[
],
"draft":false,
"work_in_progress":false,
"milestone":null,
"merge_when_pipeline_succeeds":false,
"merge_status":"can_be_merged",
"detailed_merge_status":"not_open",
"sha":"c8ce0ffdd372c2ed89d65f9e3f6f3681e6d16eb3",
"merge_commit_sha":null,
"squash_commit_sha":null,
"discussion_locked":null,
"should_remove_source_branch":null,
"force_remove_source_branch":true,
"reference":"!3",
"references":{
"short":"!3",
"relative":"!3",
"full":"superuser/backporting-example!3"
},
"web_url":"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/3",
"time_stats":{
"time_estimate":0,
"total_time_spent":0,
"human_time_estimate":null,
"human_total_time_spent":null
},
"squash":false,
"squash_on_merge":false,
"task_completion_status":{
"count":0,
"completed_count":0
},
"has_conflicts":false,
"blocking_discussions_resolved":true,
"approvals_before_merge":null,
"subscribed":true,
"changes_count":"1",
"latest_build_started_at":null,
"latest_build_finished_at":null,
"first_deployed_to_production_at":null,
"pipeline":null,
"head_pipeline":null,
"diff_refs":{
"base_sha":"4d369c3e9a8d1d5b7e56c892a8ab2a7666583ac3",
"head_sha":"c8ce0ffdd372c2ed89d65f9e3f6f3681e6d16eb3",
"start_sha":"4d369c3e9a8d1d5b7e56c892a8ab2a7666583ac3"
},
"merge_error":null,
"first_contribution":false,
"user":{
"can_merge":true
}
};
export const SUPERUSER = {
"id":14041,
"username":"superuser",
"name":"Super USer",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
};

View file

@ -1,89 +0,0 @@
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Moctokit } from "@kie/mock-github";
import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, sameOwnerPullRequestFixture } from "./moctokit-data";
const logger = LoggerServiceFactory.getLogger();
export const setupMoctokit = (): Moctokit => {
logger.debug("Setting up moctokit.");
const mock = new Moctokit();
// setup the mock requests here
// valid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: mergedPullRequestFixture.number
})
.reply({
status: 200,
data: mergedPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: sameOwnerPullRequestFixture.number
})
.reply({
status: 200,
data: sameOwnerPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: openPullRequestFixture.number
})
.reply({
status: 200,
data: openPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notMergedPullRequestFixture.number
})
.reply({
status: 200,
data: notMergedPullRequestFixture
});
mock.rest.pulls
.create()
.reply({
status: 201,
data: mergedPullRequestFixture
});
mock.rest.pulls
.requestReviewers()
.reply({
status: 201,
data: mergedPullRequestFixture
});
// invalid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notFoundPullRequestNumber
})
.reply({
status: 404,
data: {
message: "Not found"
}
});
return mock;
};