Skip to content

Commit

Permalink
feat: auto-detect the value of the no-squash option (#118)
Browse files Browse the repository at this point in the history
The auto-no-squash option is added to:

* backport all the commits when the pull/merge request has been merged
* backport the squashed commit otherwise

It is equivalent to dynamically adjust the value of the no-squash
option, depending on the context.

The no-squash option is kept for backward compatibility for a single
use case: backporting the merged commit instead of backporting the
commits of the pull/merge request request.

Detecting if a pull/merge request was squashed or not depends on the
underlying forge:

* Forgejo / GitHub: use the API to count the number of parents
* GitLab: if the squash_commit_sha is set, the merge request was
  squashed

If the pull/merge request is open, always backport all the commits it
contains.

Fixes: #113

Co-authored-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
  • Loading branch information
earl-warren and lampajr committed Apr 8, 2024
1 parent fc5dba6 commit 6042bcc
Show file tree
Hide file tree
Showing 23 changed files with 324 additions and 54 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ It works in this way: given the provided `pull/merge request` it infers the serv

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).
By default the tool will try to cherry-pick the single squashed/merged commit into the newly created branch. The `--no-squash` and `--auto-no-squash` options control this behavior according the following table.

| No squash | Auto no squash |Behavior|
|---|---|---|
| unset/false | unset/false | cherry-pick a single commit, squashed or merged |
| set/true | unset/false | cherry-pick all commits found in the the original pull/merge request|
| (ignored) | set/true | cherry-pick all commits if the original pull/merge request was merged, a single commit if it was squashed |

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.

Expand Down Expand Up @@ -121,7 +127,8 @@ This tool comes with some inputs that allow users to override the default behavi
| 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} |
| 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 |
| 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. | |
| Auto no squash | --auto-no-squash | N | If the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit. | |
| Strategy | --strategy | N | Cherry pick merging strategy, see [git-merge](https://git-scm.com/docs/git-merge#_merge_strategies) doc for all possible values | "recursive" |
| Strategy Option | --strategy-option | N | Cherry pick merging strategy option, see [git-merge](https://git-scm.com/docs/git-merge#_merge_strategies) doc for all possible values | "theirs" |
| Cherry-pick Options | --cherry-pick-options | N | Additional cherry-pick options, see [git-cherry-pick](https://git-scm.com/docs/git-cherry-pick) doc for all possible values | "theirs" |
Expand Down
10 changes: 7 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,14 @@ inputs:
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
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.
required: false
auto-no-squash:
description: >
If the pull request was merged or is open, backport all commits.
If the pull request commits were squashed, backport the squashed commit.
required: false
default: "false"
strategy:
description: Cherry-pick merge strategy
required: false
Expand Down
68 changes: 61 additions & 7 deletions dist/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ArgsParser {
labels: this.getOrDefault(args.labels, []),
inheritLabels: this.getOrDefault(args.inheritLabels, false),
squash: this.getOrDefault(args.squash, true),
autoNoSquash: this.getOrDefault(args.autoNoSquash, false),
strategy: this.getOrDefault(args.strategy),
strategyOption: this.getOrDefault(args.strategyOption),
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
Expand Down Expand Up @@ -203,7 +204,8 @@ 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("--no-squash", "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")
.option("--auto-no-squash", "If the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit.")
.option("--strategy <strategy>", "cherry-pick merge strategy, default to 'recursive'", undefined)
.option("--strategy-option <strategy-option>", "cherry-pick merge strategy option, default to 'theirs'")
.option("--cherry-pick-options <options>", "additional cherry-pick options")
Expand Down Expand Up @@ -240,6 +242,7 @@ class CLIArgsParser extends args_parser_1.default {
labels: opts.labels,
inheritLabels: opts.inheritLabels,
squash: opts.squash,
autoNoSquash: opts.autoNoSquash,
strategy: opts.strategy,
strategyOption: opts.strategyOption,
cherryPickOptions: opts.cherryPickOptions,
Expand Down Expand Up @@ -332,6 +335,9 @@ class PullRequestConfigsParser extends configs_parser_1.default {
}
async parse(args) {
let pr;
if (args.autoNoSquash) {
args.squash = undefined;
}
try {
pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash);
}
Expand Down Expand Up @@ -661,12 +667,16 @@ GitClientFactory.logger = logger_service_factory_1.default.getLogger();
/***/ }),

/***/ 9080:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

"use strict";

var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getEnv = exports.getGitTokenFromEnv = exports.inferGitApiUrl = exports.inferGitClient = void 0;
exports.getEnv = exports.getGitTokenFromEnv = exports.inferSquash = exports.inferGitApiUrl = exports.inferGitClient = void 0;
const logger_service_factory_1 = __importDefault(__nccwpck_require__(8936));
const git_types_1 = __nccwpck_require__(750);
const configs_types_1 = __nccwpck_require__(4753);
const PUBLIC_GITHUB_URL = "https://github.com";
Expand Down Expand Up @@ -706,6 +716,30 @@ const inferGitApiUrl = (prUrl, apiVersion = "v4") => {
return `${baseUrl}/api/${apiVersion}`;
};
exports.inferGitApiUrl = inferGitApiUrl;
/**
* Infer the value of the squash option
* @param open true if the pull/merge request is still open
* @param squash_commit undefined if the pull/merge request was merged, the sha of the squashed commit if it was squashed
* @returns true if a single commit must be cherry-picked, false if all merged commits must be cherry-picked
*/
const inferSquash = (open, squash_commit) => {
const logger = logger_service_factory_1.default.getLogger();
if (open) {
logger.debug("cherry-pick all commits because they have not been merged (or squashed) in the base branch yet");
return false;
}
else {
if (squash_commit !== undefined) {
logger.debug(`cherry-pick the squashed commit ${squash_commit}`);
return true;
}
else {
logger.debug("cherry-pick the merged commit(s)");
return false;
}
}
};
exports.inferSquash = inferSquash;
/**
* Retrieve the git token from env variable, the default is taken from GIT_TOKEN env.
* All specific git env variable have precedence and override the default one.
Expand Down Expand Up @@ -781,6 +815,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const git_util_1 = __nccwpck_require__(9080);
const git_types_1 = __nccwpck_require__(750);
const github_mapper_1 = __importDefault(__nccwpck_require__(5764));
const octokit_factory_1 = __importDefault(__nccwpck_require__(4257));
Expand All @@ -803,13 +838,28 @@ class GitHubClient {
getDefaultGitEmail() {
return "noreply@github.com";
}
async getPullRequest(owner, repo, prNumber, squash = true) {
async getPullRequest(owner, repo, prNumber, squash) {
this.logger.debug(`Fetching pull request ${owner}/${repo}/${prNumber}`);
const { data } = await this.octokit.rest.pulls.get({
owner: owner,
repo: repo,
pull_number: prNumber,
});
if (squash === undefined) {
let commit_sha = undefined;
const open = data.state == "open";
if (!open) {
const commit = await this.octokit.rest.git.getCommit({
owner: owner,
repo: repo,
commit_sha: data.merge_commit_sha,
});
if (commit.data.parents.length === 1) {
commit_sha = data.merge_commit_sha;
}
}
squash = (0, git_util_1.inferSquash)(open, commit_sha);
}
const commits = [];
if (!squash) {
// fetch all commits
Expand All @@ -827,7 +877,7 @@ class GitHubClient {
}
return this.mapper.mapPullRequest(data, commits);
}
async getPullRequestFromUrl(prUrl, squash = true) {
async getPullRequestFromUrl(prUrl, squash) {
const { owner, project, id } = this.extractPullRequestData(prUrl);
return this.getPullRequest(owner, project, id, squash);
}
Expand Down Expand Up @@ -1006,6 +1056,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const git_util_1 = __nccwpck_require__(9080);
const git_types_1 = __nccwpck_require__(750);
const logger_service_factory_1 = __importDefault(__nccwpck_require__(8936));
const gitlab_mapper_1 = __importDefault(__nccwpck_require__(2675));
Expand Down Expand Up @@ -1038,9 +1089,12 @@ class GitLabClient {
}
// READ
// example: <host>/api/v4/projects/<namespace>%2Fbackporting-example/merge_requests/1
async getPullRequest(namespace, repo, mrNumber, squash = true) {
async getPullRequest(namespace, repo, mrNumber, squash) {
const projectId = this.getProjectId(namespace, repo);
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`);
if (squash === undefined) {
squash = (0, git_util_1.inferSquash)(data.state == "opened", data.squash_commit_sha);
}
const commits = [];
if (!squash) {
// fetch all commits
Expand All @@ -1055,7 +1109,7 @@ class GitLabClient {
}
return this.mapper.mapPullRequest(data, commits);
}
getPullRequestFromUrl(mrUrl, squash = true) {
getPullRequestFromUrl(mrUrl, squash) {
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
return this.getPullRequest(namespace, project, id, squash);
}
Expand Down
65 changes: 59 additions & 6 deletions dist/gha/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class ArgsParser {
labels: this.getOrDefault(args.labels, []),
inheritLabels: this.getOrDefault(args.inheritLabels, false),
squash: this.getOrDefault(args.squash, true),
autoNoSquash: this.getOrDefault(args.autoNoSquash, false),
strategy: this.getOrDefault(args.strategy),
strategyOption: this.getOrDefault(args.strategyOption),
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
Expand Down Expand Up @@ -207,6 +208,7 @@ class GHAArgsParser extends args_parser_1.default {
labels: (0, args_utils_1.getAsCommaSeparatedList)((0, core_1.getInput)("labels")),
inheritLabels: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("inherit-labels")),
squash: !(0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("no-squash")),
autoNoSquash: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("auto-no-squash")),
strategy: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("strategy")),
strategyOption: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("strategy-option")),
cherryPickOptions: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("cherry-pick-options")),
Expand Down Expand Up @@ -299,6 +301,9 @@ class PullRequestConfigsParser extends configs_parser_1.default {
}
async parse(args) {
let pr;
if (args.autoNoSquash) {
args.squash = undefined;
}
try {
pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash);
}
Expand Down Expand Up @@ -628,12 +633,16 @@ GitClientFactory.logger = logger_service_factory_1.default.getLogger();
/***/ }),

/***/ 9080:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

"use strict";

var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getEnv = exports.getGitTokenFromEnv = exports.inferGitApiUrl = exports.inferGitClient = void 0;
exports.getEnv = exports.getGitTokenFromEnv = exports.inferSquash = exports.inferGitApiUrl = exports.inferGitClient = void 0;
const logger_service_factory_1 = __importDefault(__nccwpck_require__(8936));
const git_types_1 = __nccwpck_require__(750);
const configs_types_1 = __nccwpck_require__(4753);
const PUBLIC_GITHUB_URL = "https://github.com";
Expand Down Expand Up @@ -673,6 +682,30 @@ const inferGitApiUrl = (prUrl, apiVersion = "v4") => {
return `${baseUrl}/api/${apiVersion}`;
};
exports.inferGitApiUrl = inferGitApiUrl;
/**
* Infer the value of the squash option
* @param open true if the pull/merge request is still open
* @param squash_commit undefined if the pull/merge request was merged, the sha of the squashed commit if it was squashed
* @returns true if a single commit must be cherry-picked, false if all merged commits must be cherry-picked
*/
const inferSquash = (open, squash_commit) => {
const logger = logger_service_factory_1.default.getLogger();
if (open) {
logger.debug("cherry-pick all commits because they have not been merged (or squashed) in the base branch yet");
return false;
}
else {
if (squash_commit !== undefined) {
logger.debug(`cherry-pick the squashed commit ${squash_commit}`);
return true;
}
else {
logger.debug("cherry-pick the merged commit(s)");
return false;
}
}
};
exports.inferSquash = inferSquash;
/**
* Retrieve the git token from env variable, the default is taken from GIT_TOKEN env.
* All specific git env variable have precedence and override the default one.
Expand Down Expand Up @@ -748,6 +781,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const git_util_1 = __nccwpck_require__(9080);
const git_types_1 = __nccwpck_require__(750);
const github_mapper_1 = __importDefault(__nccwpck_require__(5764));
const octokit_factory_1 = __importDefault(__nccwpck_require__(4257));
Expand All @@ -770,13 +804,28 @@ class GitHubClient {
getDefaultGitEmail() {
return "noreply@github.com";
}
async getPullRequest(owner, repo, prNumber, squash = true) {
async getPullRequest(owner, repo, prNumber, squash) {
this.logger.debug(`Fetching pull request ${owner}/${repo}/${prNumber}`);
const { data } = await this.octokit.rest.pulls.get({
owner: owner,
repo: repo,
pull_number: prNumber,
});
if (squash === undefined) {
let commit_sha = undefined;
const open = data.state == "open";
if (!open) {
const commit = await this.octokit.rest.git.getCommit({
owner: owner,
repo: repo,
commit_sha: data.merge_commit_sha,
});
if (commit.data.parents.length === 1) {
commit_sha = data.merge_commit_sha;
}
}
squash = (0, git_util_1.inferSquash)(open, commit_sha);
}
const commits = [];
if (!squash) {
// fetch all commits
Expand All @@ -794,7 +843,7 @@ class GitHubClient {
}
return this.mapper.mapPullRequest(data, commits);
}
async getPullRequestFromUrl(prUrl, squash = true) {
async getPullRequestFromUrl(prUrl, squash) {
const { owner, project, id } = this.extractPullRequestData(prUrl);
return this.getPullRequest(owner, project, id, squash);
}
Expand Down Expand Up @@ -973,6 +1022,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const git_util_1 = __nccwpck_require__(9080);
const git_types_1 = __nccwpck_require__(750);
const logger_service_factory_1 = __importDefault(__nccwpck_require__(8936));
const gitlab_mapper_1 = __importDefault(__nccwpck_require__(2675));
Expand Down Expand Up @@ -1005,9 +1055,12 @@ class GitLabClient {
}
// READ
// example: <host>/api/v4/projects/<namespace>%2Fbackporting-example/merge_requests/1
async getPullRequest(namespace, repo, mrNumber, squash = true) {
async getPullRequest(namespace, repo, mrNumber, squash) {
const projectId = this.getProjectId(namespace, repo);
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`);
if (squash === undefined) {
squash = (0, git_util_1.inferSquash)(data.state == "opened", data.squash_commit_sha);
}
const commits = [];
if (!squash) {
// fetch all commits
Expand All @@ -1022,7 +1075,7 @@ class GitLabClient {
}
return this.mapper.mapPullRequest(data, commits);
}
getPullRequestFromUrl(mrUrl, squash = true) {
getPullRequestFromUrl(mrUrl, squash) {
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
return this.getPullRequest(namespace, project, id, squash);
}
Expand Down
1 change: 1 addition & 0 deletions src/service/args/args-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default abstract class ArgsParser {
labels: this.getOrDefault(args.labels, []),
inheritLabels: this.getOrDefault(args.inheritLabels, false),
squash: this.getOrDefault(args.squash, true),
autoNoSquash: this.getOrDefault(args.autoNoSquash, false),
strategy: this.getOrDefault(args.strategy),
strategyOption: this.getOrDefault(args.strategyOption),
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
Expand Down

0 comments on commit 6042bcc

Please sign in to comment.