Skip to content

Commit

Permalink
feat(issue-54): backport pr commits without squash (#55)
Browse files Browse the repository at this point in the history
* feat(issue-54): backport pr commits without squash

fix #54

* feat(issue-54): fixed readme
  • Loading branch information
lampajr authored Jul 11, 2023
1 parent a737aa7 commit c4dbb26
Show file tree
Hide file tree
Showing 29 changed files with 990 additions and 145 deletions.
33 changes: 33 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,36 @@ changes that span multiple kiegroup repositories and depend on each other. -->
- [ ] Documentation updated if applicable.

> **Note:** `dist/cli/index.js` and `dist/gha/index.js` are automatically generated by git hooks and gh workflows.
<details>
<summary>
First time here?
</summary>

This project follows [git conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) pattern, therefore the commits should have the following format:

```
<type>(<optional scope>): <subject>
empty separator line
<optional body>
empty separator line
<optional footer>
```

Where the type must be one of `[build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]`

> **NOTE**: if you are still in a `work in progress` branch and you want to push your changes remotely, consider adding `--no-verify` for both `commit` and `push`, e.g., `git push origin <feat-branch> --no-verify` - this could become useful to push changes where there are still tests failures. Once the pull request is ready, please `amend` the commit and force-push it to keep following the adopted git commit standard.
</details>

<details>
<summary>
How to prepare for a new release?
</summary>

There is no need to manually update `package.json` version and `CHANGELOG.md` information. This process has been automated in [Prepare Release](./workflows/prepare-release.yml) *Github* workflow.

Therefore whenever enough changes are merged into the `main` branch, one of the maintainers will trigger this workflow that will automatically update `version` and `changelog` based on the commits on the git tree.

More details can be found in [package release](https://github.com/kiegroup/git-backporting/blob/main/README.md#package-release) section of the README.
</details>
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ report.json
.vscode/
build/
.npmrc

# temporary files created during tests
*test*.json
75 changes: 47 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ Table of content

* **[Who is this tool for](#who-is-this-tool-for)**
* **[CLI tool](#cli-tool)**
* **[Supported git services](#supported-git-services)**
* **[GitHub action](#github-action)**
* **[Future works](#future-works)**
* **[Release](#release)**
* **[Repository migration](#repository-migration)**
* **[Migrating to v4](#migrating-to-v4)**
* **[Development](#development)**
* **[Contributing](#contributing)**
* **[License](#license)**

Expand Down Expand Up @@ -72,6 +71,23 @@ This is the easiest invocation where you let the tool set / compute most of the
* Node 16 or higher, more details on Node can be found [here](https://nodejs.org/en).
* Git, see [how to install](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) if you need help.

### How it works?

The simply works in this way: given the provided `pull/merge request` it infers the git client to use (either *Github* or *Gitlab* for now) and it retrieve the corresponding pull request object (original pull/merge request to be backported into another branch).

After that it clones the corresponding git repository, check out in the provided `target branch` and create a new branch from that (name automatically generated if not provided as option).

By default the tool will try to cherry-pick the single squashed/merged commit into the newly created branch (please consider using `--no-squash` option if you want to cherry-pick all commits belonging to the provided pull request).

Based on the original pull request, creates a new one containing the backporting to the target branch. Note that most of these information can be overridden with appropriate CLI options or GHA inputs.

Right now all commits are cherry-picked using the following git-equivalent command:
```bash
$ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs <sha>
```

> **NOTE**: If there are any conflicts, the tool will block the process and exit signalling the failure as there are still no ways to interactively resolve them. In these cases a manual cherry-pick is needed, or alternatively users could manually resume the process in the cloned repository (here the user will have to resolve the conflicts, push the branch and create the pull request - all manually).
### Inputs

This tool comes with some inputs that allow users to override the default behavior, here the full list of available inputs:
Expand All @@ -96,6 +112,7 @@ This tool comes with some inputs that allow users to override the default behavi
| Backport Branch Name | --bp-branch-name | N | Name of the backporting pull request branch | bp-{target-branch}-{sha} |
| Labels | --labels | N | Provide custom labels to be added to the backporting pull request | [] |
| Inherit labels | --inherit-labels | N | If enabled inherit lables from the original pull request | false |
| No squash | --no-squash | N | If provided the backporting will try to backport all pull request commits without squashing | false |
| Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false |

> **NOTE**: `pull request` and `target branch` are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used).
Expand All @@ -114,7 +131,7 @@ This is an example of a configuration file that can be used.
```
Keep in mind that its structue MUST match the [Args](src/service/args/args.types.ts) interface, which is actually a camel-case version of the CLI options.

## Supported git services
### Supported git services

Right now **Git Backporting** supports the following git management services:
* ***GITHUB***: Introduced since the first release of this tool (version `1.0.0`). The interaction with this system is performed using [*octokit*](https://octokit.github.io/rest.js) client library.
Expand All @@ -128,7 +145,7 @@ Right now **Git Backporting** supports the following git management services:
This action can be used in any *GitHub* workflow, below you can find a simple example of manually triggered workflow backporting a specific pull request (provided as input).

```yml
name: Pull Request Backporting using BPer
name: Pull Request Backporting using Git Backporting

on:
workflow_dispatch:
Expand Down Expand Up @@ -166,7 +183,7 @@ You can also use this action with other events - you'll just need to specify `ta
For example, this configuration creates a pull request against branch `v1` once the current one is merged, provided that the label `backport-v1` is applied:

```yaml
name: Pull Request Backporting using BPer
name: Pull Request Backporting using Git Backporting
on:
pull_request_target:
Expand Down Expand Up @@ -203,19 +220,38 @@ For a complete description of all inputs see [Inputs section](#inputs).

## Future works

**BPer** is still in development mode, this means that there are still many future works and extension. I'll try to summarize the most important ones:
**Git Backporting** is still in development mode, this means that there are still many future works and extension that can be implemented. I'll try to summarize the most important ones:

- Provide a way to backport single commit too (or a set of them), even if no original pull request is present.
- Provide a way to backport single commit (or a set of them) if no original pull request is present.
- Integrate this tool with other git management services (like Bitbucket) to make it as generic as possible.
- Integrate it into other CI/CD services like gitlab CI.
- Provide some reusable *GitHub* workflows.

## Release
## Migrating to v4

From version `v4.0.0` the project has been moved under [@kiegroup](https://github.com/kiegroup) organization. During this migration we changed some things that you should be aware of. I'll try to summarize them in the following table:

> **NOTE**: these changes did not affect the tool features.

| | **v4 (after migration)** | v3 or older (before migration) |
|-------------|--------------------------|--------------------------------|
| Owner | kiegroup | lampajr |
| Repository | git-backporting | backporting |
| NPM package | @kie/git-backporting | @lampajr/bper |
| CLI tool | git-backporting | bper |

So everytime you would use older version keep in mind of these changes.

> **REMARK**: since from capabilities point of view `v3.1.1` and `v4.0.0` are equivalent we would recommend to directly start using `v4`.

## Development

### Package release

The release of this package is entirely based on [release-it](https://github.com/release-it/release-it) tool. I created some useful scripts that can make the release itself quite easy.


### Automated release
#### Automatic release

The first step is to prepare the changes for the next release, this is done by running:

Expand All @@ -236,7 +272,7 @@ After that you should just push the new branch and open the pull request.

Once the release preparion pull request got merged, you can run [Release package](.github/workflows/release.yml) workflow that automatically performs the release itself, including npm publishing, git tag and github release.

### Manual release
#### Manual release

In case we would like to perform a manual release, it would be enough to open a pull request changing the following items:
- Package version inside the `package.json`
Expand All @@ -245,23 +281,6 @@ In case we would like to perform a manual release, it would be enough to open a

Once the release preparion pull request got merged, run [Release package](.github/workflows/release.yml) workflow.

## Repository Migration

From version `v4.0.0` the project has been moved under [@kiegroup](https://github.com/kiegroup) organization. During this migration we changed some things that you should be aware of. I'll try to summarize them in the following table:

> **NOTE**: these changes did not affect the tool features.

| | **v4 (after migration)** | v3 or older (before migration) |
|-------------|--------------------------|--------------------------------|
| Owner | kiegroup | lampajr |
| Repository | git-backporting | backporting |
| NPM package | @kie/git-backporting | @lampajr/bper |
| CLI tool | git-backporting | bper |

So everytime you would use older version keep in mind of these changes.

> **REMARK**: since from capabilities point of view `v3.1.1` and `v4.0.0` are equivalent we would recommend to directly start using `v4`.

## Contributing

This is an open source project, and you are more than welcome to contribute :heart:!
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ inputs:
description: "If true the backported pull request will inherit labels from the original one"
required: false
default: "false"
no-squash:
description: "If set to true the tool will backport all commits as part of the pull request instead of the suqashed one"
required: false
default: "false"

runs:
using: node16
Expand Down
74 changes: 56 additions & 18 deletions dist/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ArgsParser {
inheritReviewers: this.getOrDefault(args.inheritReviewers, true),
labels: this.getOrDefault(args.labels, []),
inheritLabels: this.getOrDefault(args.inheritLabels, false),
squash: this.getOrDefault(args.squash, true),
};
}
}
Expand Down Expand Up @@ -185,6 +186,7 @@ class CLIArgsParser extends args_parser_1.default {
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")
.option("--labels <labels>", "comma separated list of labels to be assigned to the backported pull request", args_utils_1.getAsCommaSeparatedList)
.option("--inherit-labels", "if true the backported pull request will inherit labels from the original one")
.option("--no-squash", "if provided the tool will backport all commits as part of the pull request")
.option("-cf, --config-file <config-file>", "configuration file containing all valid options, the json must match Args interface");
}
readArgs() {
Expand Down Expand Up @@ -214,6 +216,7 @@ class CLIArgsParser extends args_parser_1.default {
inheritReviewers: opts.inheritReviewers,
labels: opts.labels,
inheritLabels: opts.inheritLabels,
squash: opts.squash,
};
}
return args;
Expand Down Expand Up @@ -280,7 +283,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
async parse(args) {
let pr;
try {
pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest);
pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash);
}
catch (error) {
this.logger.error("Something went wrong retrieving pull request");
Expand Down Expand Up @@ -619,18 +622,33 @@ class GitHubClient {
getDefaultGitEmail() {
return "noreply@github.com";
}
async getPullRequest(owner, repo, prNumber) {
async getPullRequest(owner, repo, prNumber, squash = true) {
this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`);
const { data } = await this.octokit.rest.pulls.get({
owner: owner,
repo: repo,
pull_number: prNumber
pull_number: prNumber,
});
return this.mapper.mapPullRequest(data);
const commits = [];
if (!squash) {
// fetch all commits
try {
const { data } = await this.octokit.rest.pulls.listCommits({
owner: owner,
repo: repo,
pull_number: prNumber,
});
commits.push(...data.map(c => c.sha));
}
catch (error) {
throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`);
}
}
return this.mapper.mapPullRequest(data, commits);
}
async getPullRequestFromUrl(prUrl) {
async getPullRequestFromUrl(prUrl, squash = true) {
const { owner, project, id } = this.extractPullRequestData(prUrl);
return this.getPullRequest(owner, project, id);
return this.getPullRequest(owner, project, id, squash);
}
// WRITE
async createPullRequest(backport) {
Expand Down Expand Up @@ -724,7 +742,7 @@ class GitHubMapper {
return git_types_1.GitRepoState.CLOSED;
}
}
async mapPullRequest(pr) {
async mapPullRequest(pr, commits) {
return {
number: pr.number,
author: pr.user.login,
Expand All @@ -741,10 +759,14 @@ class GitHubMapper {
sourceRepo: await this.mapSourceRepo(pr),
targetRepo: await this.mapTargetRepo(pr),
nCommits: pr.commits,
// if pr is open use latest commit sha otherwise use merge_commit_sha
commits: pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha]
// if commits is provided use them, otherwise fetch the single sha representing the whole pr
commits: (commits && commits.length > 0) ? commits : this.getSha(pr),
};
}
getSha(pr) {
// if pr is open use latest commit sha otherwise use merge_commit_sha
return pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha];
}
async mapSourceRepo(pr) {
return Promise.resolve({
owner: pr.head.repo.full_name.split("/")[0],
Expand Down Expand Up @@ -835,14 +857,26 @@ class GitLabClient {
}
// READ
// example: <host>/api/v4/projects/<namespace>%2Fbackporting-example/merge_requests/1
async getPullRequest(namespace, repo, mrNumber) {
async getPullRequest(namespace, repo, mrNumber, squash = true) {
const projectId = this.getProjectId(namespace, repo);
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`);
return this.mapper.mapPullRequest(data);
const commits = [];
if (!squash) {
// fetch all commits
try {
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}/commits`);
// gitlab returns them in reverse order
commits.push(...data.map(c => c.id).reverse());
}
catch (error) {
throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`);
}
}
return this.mapper.mapPullRequest(data, commits);
}
getPullRequestFromUrl(mrUrl) {
getPullRequestFromUrl(mrUrl, squash = true) {
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
return this.getPullRequest(namespace, project, id);
return this.getPullRequest(namespace, project, id, squash);
}
// WRITE
async createPullRequest(backport) {
Expand Down Expand Up @@ -983,7 +1017,7 @@ class GitLabMapper {
return git_types_1.GitRepoState.LOCKED;
}
}
async mapPullRequest(mr) {
async mapPullRequest(mr, commits) {
return {
number: mr.iid,
author: mr.author.username,
Expand All @@ -999,12 +1033,16 @@ class GitLabMapper {
labels: mr.labels ?? [],
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
nCommits: 1,
// if mr is merged, use merge_commit_sha otherwise use sha
// what is the difference between sha and diff_refs.head_sha?
commits: this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.sha]
// if commits list is provided use that as source
nCommits: (commits && commits.length > 1) ? commits.length : 1,
commits: (commits && commits.length > 1) ? commits : this.getSha(mr)
};
}
getSha(mr) {
// if mr is merged, use merge_commit_sha otherwise use sha
// what is the difference between sha and diff_refs.head_sha?
return this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha] : [mr.sha];
}
async mapSourceRepo(mr) {
const project = await this.getProject(mr.source_project_id);
return {
Expand Down
Loading

0 comments on commit c4dbb26

Please sign in to comment.