Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ This tool comes with some inputs that allow users to override the default behavi
| No Reviewers Inheritance | --no-inherit-reviewers | N | Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request | false |
| Backport Branch Names | --bp-branch-name | N | Comma separated lists of the backporting pull request branch names, if they exceeds 250 chars they will be truncated | bp-{target-branch}-{sha1}...{shaN} |
| Backport Repository | --bp-repo | N | Optional source repository (format owner/repo) where the backport branch is pushed, useful to open the PR from a fork | {target-owner}/{target-repo} |
| Target Repository | --tb-repo | N | Optional target repository (format owner/repo) where the backport pull request should be opened against, useful to backport to a different repository than the original one | {original-pr-target-owner}/{original-pr-target-repo} |
| 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 | Backport all commits found in the pull request. The default behavior is to only backport the first commit that was merged in the base branch. | |
Expand Down Expand Up @@ -161,6 +162,23 @@ $ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a ***

In this mode you should provide a PAT with enough permissions on the fork repository.

#### Backport to a different target repository

By default, the backport pull request is opened against the same repository targeted by the original pull request.
If you want to open the backport PR against a different repository altogether, set `--tb-repo` (or action input `tb-repo`) to `owner/repo`. The repository is cloned from and the backport branch is pushed to `--tb-repo` instead of the original pull request's repository.

```bash
$ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a ***** --tb-repo my-org/downstream-project
```

`--tb-repo` and `--bp-repo` can be combined: `--tb-repo` selects where the backport PR is opened, while `--bp-repo` selects the fork the backport branch is pushed from.

```bash
$ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a ***** --tb-repo my-org/downstream-project --bp-repo my-user/downstream-project
```

When using `--tb-repo` alone, the backport branch is pushed directly to the target repository, so your PAT needs push access there. When combining it with `--bp-repo`, the branch is pushed to the fork instead, so your PAT needs push access on the fork and only the ability to open a pull request on the target repository.

#### Configuration file example

This is an example of a configuration file that can be used.
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ inputs:
description: >
Optional backport repository as owner/repo where the backport branch is pushed, useful to create PRs from a fork
required: false
tb-repo:
description: >
Optional target repository as owner/repo where the backport pull request should be opened against, useful to backport to a different repository than the original one
required: false
reviewers:
description: >
Comma separated list of reviewers for the backporting pull request
Expand Down
74 changes: 61 additions & 13 deletions dist/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class ArgsParser {
bodyPrefix: this.getOrDefault(args.bodyPrefix),
bpBranchName: this.getOrDefault(args.bpBranchName),
bpRepo: this.getOrDefault(args.bpRepo),
tbRepo: this.getOrDefault(args.tbRepo),
reviewers: this.getOrDefault(args.reviewers, []),
assignees: this.getOrDefault(args.assignees, []),
inheritReviewers: this.getOrDefault(args.inheritReviewers, true),
Expand Down Expand Up @@ -202,6 +203,7 @@ class CLIArgsParser extends args_parser_1.default {
.option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`")
.option("--bp-branch-name <bp-branch-names>", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch")
.option("--bp-repo <owner/repo>", "optional backport repository where the branch should be pushed, e.g. my-fork/my-repo")
.option("--tb-repo <owner/repo>", "optional target repository where the backport pull request should be opened against, e.g. my-org/my-repo")
.option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList)
.option("--assignees <assignees>", "comma separated list of assignees for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList)
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")
Expand Down Expand Up @@ -241,6 +243,7 @@ class CLIArgsParser extends args_parser_1.default {
bodyPrefix: opts.bodyPrefix,
bpBranchName: opts.bpBranchName,
bpRepo: opts.bpRepo,
tbRepo: opts.tbRepo,
reviewers: opts.reviewers,
assignees: opts.assignees,
inheritReviewers: opts.inheritReviewers,
Expand Down Expand Up @@ -427,7 +430,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
* @returns {GitPullRequest}
*/
generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) {
const targetRepo = originalPullRequest.targetRepo;
const targetRepo = this.getBackportTargetRepo(args.tbRepo, originalPullRequest.targetRepo);
const sourceRepo = this.getBackportSourceRepo(args.bpRepo, targetRepo);
const reviewers = args.reviewers ?? [];
if (reviewers.length == 0 && args.inheritReviewers) {
Expand Down Expand Up @@ -466,6 +469,7 @@ class PullRequestConfigsParser extends configs_parser_1.default {
return {
owner: targetRepo.owner,
repo: targetRepo.project,
cloneUrl: targetRepo.cloneUrl,
head: backportBranch,
headRepo: sourceRepo,
base: tb,
Expand All @@ -483,12 +487,29 @@ class PullRequestConfigsParser extends configs_parser_1.default {
if (!bpRepo || bpRepo.trim() === "") {
return undefined;
}
const sanitized = bpRepo.trim();
return this.parseRepo(bpRepo, "bp", targetRepo);
}
getBackportTargetRepo(tbRepo, targetRepo) {
if (!tbRepo || tbRepo.trim() === "") {
return targetRepo;
}
return this.parseRepo(tbRepo, "tb", targetRepo);
}
/**
* Parse a "owner/repo" formatted repository override and derive its clone url
* by reusing the scheme/host of the provided reference repository
* @param repo owner/repo formatted repository override
* @param optionName name of the option the override came from, used in the error message
* @param referenceRepo repository whose clone url is used to derive the scheme/host
* @returns {GitRepository}
*/
parseRepo(repo, optionName, referenceRepo) {
const sanitized = repo.trim();
const parts = sanitized.split("/").map(p => p.trim()).filter(p => p.length > 0);
if (parts.length < 2) {
throw new Error(`Invalid bp repo format "${bpRepo}", expected "owner/repo"`);
throw new Error(`Invalid ${optionName} repo format "${repo}", expected "owner/repo"`);
}
const cloneUrl = new URL(targetRepo.cloneUrl);
const cloneUrl = new URL(referenceRepo.cloneUrl);
cloneUrl.pathname = `/${parts.join("/")}.git`;
return {
owner: parts[0],
Expand Down Expand Up @@ -588,13 +609,20 @@ class GitCLIService {
await this.git(cwd).checkoutLocalBranch(newBranch);
}
/**
* Add a new remote to the current repository
* Add a new remote to the current repository, or update its url if a remote
* with the same name already exists, e.g., because the working folder is
* reused across multiple backports
* @param cwd repository in which addRemote should be performed
* @param remote remote git link
* @param remoteName [optional] name of the remote, by default 'fork' is used
*/
async addRemote(cwd, remote, remoteName = "fork") {
this.logger.info(`Adding new remote ${remote}`);
const existingRemotes = await this.git(cwd).getRemotes();
if (existingRemotes.some(r => r.name === remoteName)) {
await this.git(cwd).remote(["set-url", remoteName, this.remoteWithAuth(remote)]);
return;
}
await this.git(cwd).addRemote(remoteName, this.remoteWithAuth(remote));
}
/**
Expand Down Expand Up @@ -1732,26 +1760,46 @@ class Runner {
exports["default"] = Runner;
function* backportSteps(logger, configs, backportPR, git) {
// every failible operation should be in one dedicated closure
// whether the backport pr targets a different repository than the original pull request's one (--tb-repo),
// in which case the original pr's commits are not reachable from a clone of the backport target repo alone
const usingDifferentTargetRepo = backportPR.cloneUrl !== configs.originalPullRequest.targetRepo.cloneUrl;
// 4. clone the repository
yield async () => {
logger.debug("Cloning repo..");
await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base);
await git.gitCli.clone(backportPR.cloneUrl, configs.folder, backportPR.base);
};
// 5. create new branch from target one and checkout
yield async () => {
logger.debug("Creating local branch..");
await git.gitCli.createLocalBranch(configs.folder, backportPR.head);
};
// 6. fetch pull request remote if source owner != target owner or pull request still open
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
let commitsRemote = undefined;
if (usingDifferentTargetRepo) {
// 6. add a remote pointing to the original pull request's repository, needed to fetch
// commits that only exist there, since the backport target repo won't have them
commitsRemote = "upstream";
yield async () => {
await git.gitCli.addRemote(configs.folder, configs.originalPullRequest.targetRepo.cloneUrl, commitsRemote);
};
}
// 7. fetch pull request remote if source owner != target owner, pull request still open,
// or backporting to a different repository than the original pull request's one
if (usingDifferentTargetRepo ||
configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
configs.originalPullRequest.state === "open") {
yield async () => {
logger.debug("Fetching pull request remote..");
const prefix = git.gitClientType === git_types_1.GitClientType.GITLAB ? "merge-requests" : "pull"; // default is for gitlab
await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
const ref = `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`;
if (commitsRemote) {
await git.gitCli.fetch(configs.folder, ref, commitsRemote);
}
else {
await git.gitCli.fetch(configs.folder, ref);
}
};
}
// 7. apply all changes to the new branch
// 8. apply all changes to the new branch
yield async () => {
logger.debug("Cherry picking commits..");
};
Expand All @@ -1762,18 +1810,18 @@ function* backportSteps(logger, configs, backportPR, git) {
}
let target_remote = undefined;
if (backportPR.headRepo) {
// 8. add fork-remote to push backport branch to
// 9. add fork-remote to push backport branch to
target_remote = "fork";
yield async () => {
await git.gitCli.addRemote(configs.folder, backportPR.headRepo.cloneUrl, target_remote);
};
}
if (!configs.dryRun) {
// 9. push the new branch to origin
// 10. push the new branch to origin
yield async () => {
await git.gitCli.push(configs.folder, backportPR.head, target_remote);
};
// 10. create pull request new branch -> target branch (using octokit)
// 11. create pull request new branch -> target branch (using octokit)
yield async () => {
const prUrl = await git.gitClientApi.createPullRequest(backportPR);
logger.info(`Pull request created: ${prUrl}`);
Expand Down
Loading