Skip to content

Commit 4f576d1

Browse files
committed
feat(ng-dev): add conditional autosquash merge strategy
Introduces a new conditional autosquash merge strategy. This strategy uses the autosquash merge strategy if the pull request contains fixup or squash commits, and the GitHub API merge strategy otherwise. This allows pull requests that do not need to be squashed to be closed as "merged" on GitHub, rather than "closed".
1 parent b02901c commit 4f576d1

File tree

5 files changed

+67
-31
lines changed

5 files changed

+67
-31
lines changed

.github/local-actions/branch-manager/main.js

Lines changed: 13 additions & 2 deletions
Large diffs are not rendered by default.

.ng-dev/pull-request.mts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import {PullRequestConfig} from '../ng-dev/pr/config/index.js';
22

33
/** Configuration for interacting with pull requests in the repo. */
44
export const pullRequest: PullRequestConfig = {
5-
githubApiMerge: false,
5+
githubApiMerge: {
6+
default: 'rebase-with-fixup',
7+
labels: [{pattern: 'merge: squash commits', method: 'squash'}],
8+
},
69
requiredStatuses: [{name: 'test', type: 'check'}],
710

811
// Disable target labeling in the dev-infra repo as we don't have

ng-dev/pr/config/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88

99
import {ConfigValidationError, GithubConfig, NgDevConfig} from '../../utils/config.js';
1010

11+
// TODO(alanagius): remove `rebase-with-fixup` and replace it's logic with `rebase`.
12+
// This is just temporary to allow testing in the dev-infra repo. Without breaking workflows in other repos.
13+
1114
/**
1215
* Possible merge methods supported by the Github API.
1316
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button.
1417
*/
15-
export type GithubApiMergeMethod = 'merge' | 'squash' | 'rebase';
18+
export type GithubApiMergeMethod = 'merge' | 'squash' | 'rebase' | 'rebase-with-fixup';
1619

1720
/** Configuration for the Github API merge strategy. */
1821
export interface GithubApiMergeStrategyConfig {
@@ -23,7 +26,7 @@ export interface GithubApiMergeStrategyConfig {
2326
}
2427

2528
/** Configuration for the merge script. */
26-
export interface PullRequestConfig {
29+
export type PullRequestConfig = {
2730
/**
2831
* Configuration for the upstream remote. All of these options are optional as
2932
* defaults are provided by the common dev-infra github configuration.
@@ -63,7 +66,7 @@ export interface PullRequestConfig {
6366
* follow the canonical branching/versioning.
6467
*/
6568
__noTargetLabeling?: boolean;
66-
}
69+
};
6770

6871
/** Loads and validates the merge configuration. */
6972
export function assertValidPullRequestConfig<T extends NgDevConfig>(
@@ -76,7 +79,8 @@ export function assertValidPullRequestConfig<T extends NgDevConfig>(
7679
);
7780
}
7881

79-
if (config.pullRequest.githubApiMerge === undefined) {
82+
const {githubApiMerge} = config.pullRequest;
83+
if (githubApiMerge === undefined) {
8084
errors.push('No explicit choice of merge strategy. Please set `githubApiMerge`.');
8185
}
8286

ng-dev/pr/merge/strategies/api-merge.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods';
1010

11-
import {parseCommitMessage} from '../../../commit-message/parse.js';
1211
import {AuthenticatedGitClient} from '../../../utils/git/authenticated-git-client.js';
1312
import {GithubApiMergeMethod, GithubApiMergeStrategyConfig} from '../../config/index.js';
1413
import {PullRequest} from '../pull-request.js';
@@ -21,9 +20,6 @@ import {Prompt} from '../../../utils/prompt.js';
2120
/** Type describing the parameters for the Octokit `merge` API endpoint. */
2221
type OctokitMergeParams = RestEndpointMethodTypes['pulls']['merge']['parameters'];
2322

24-
type OctokitPullRequestCommitsList =
25-
RestEndpointMethodTypes['pulls']['listCommits']['response']['data'];
26-
2723
/** Separator between commit message header and body. */
2824
const COMMIT_HEADER_SEPARATOR = '\n\n';
2925

@@ -37,7 +33,7 @@ const COMMIT_HEADER_SEPARATOR = '\n\n';
3733
export class GithubApiMergeStrategy extends MergeStrategy {
3834
constructor(
3935
git: AuthenticatedGitClient,
40-
private _config: GithubApiMergeStrategyConfig,
36+
private config: GithubApiMergeStrategyConfig,
4137
) {
4238
super(git);
4339
}
@@ -52,12 +48,20 @@ export class GithubApiMergeStrategy extends MergeStrategy {
5248
*/
5349
override async merge(pullRequest: PullRequest): Promise<void> {
5450
const {githubTargetBranch, prNumber, needsCommitMessageFixup, targetBranches} = pullRequest;
55-
const method = this._getMergeActionFromPullRequest(pullRequest);
51+
const method = this.getMergeActionFromPullRequest(pullRequest);
5652
const cherryPickTargetBranches = targetBranches.filter((b) => b !== githubTargetBranch);
5753

54+
// Squash and Merge will create a single commit message and thus we can use the API to merge.
55+
if (
56+
method === 'rebase-with-fixup' &&
57+
(pullRequest.needsCommitMessageFixup || (await this.hasFixupOrSquashCommits(pullRequest)))
58+
) {
59+
return new GithubApiMergeStrategy(this.git, this.config).merge(pullRequest);
60+
}
61+
5862
const mergeOptions: OctokitMergeParams = {
5963
pull_number: prNumber,
60-
merge_method: method,
64+
merge_method: method === 'rebase-with-fixup' ? 'rebase' : method,
6165
...this.git.remoteParams,
6266
};
6367

@@ -195,10 +199,7 @@ export class GithubApiMergeStrategy extends MergeStrategy {
195199
* behavior here so that we have a default commit message that can be fixed up.
196200
*/
197201
private async _getDefaultSquashCommitMessage(pullRequest: PullRequest): Promise<string> {
198-
const commits = (await this._getPullRequestCommitMessages(pullRequest)).map((message) => ({
199-
message,
200-
parsed: parseCommitMessage(message),
201-
}));
202+
const commits = await this.getPullRequestCommits(pullRequest);
202203
const messageBase = `${pullRequest.title}${COMMIT_HEADER_SEPARATOR}`;
203204
if (commits.length <= 1) {
204205
return `${messageBase}${commits[0].parsed.body}`;
@@ -207,23 +208,21 @@ export class GithubApiMergeStrategy extends MergeStrategy {
207208
return `${messageBase}${joinedMessages}`;
208209
}
209210

210-
/** Gets all commit messages of commits in the pull request. */
211-
private async _getPullRequestCommitMessages({prNumber}: PullRequest) {
212-
const allCommits = await this.git.github.paginate(this.git.github.pulls.listCommits, {
213-
...this.git.remoteParams,
214-
pull_number: prNumber,
215-
});
216-
return allCommits.map(({commit}) => commit.message);
217-
}
218-
219211
/** Determines the merge action from the given pull request. */
220-
private _getMergeActionFromPullRequest({labels}: PullRequest): GithubApiMergeMethod {
221-
if (this._config.labels) {
222-
const matchingLabel = this._config.labels.find(({pattern}) => labels.includes(pattern));
212+
getMergeActionFromPullRequest({labels}: PullRequest): GithubApiMergeMethod {
213+
if (this.config.labels) {
214+
const matchingLabel = this.config.labels.find(({pattern}) => labels.includes(pattern));
223215
if (matchingLabel !== undefined) {
224216
return matchingLabel.method;
225217
}
226218
}
227-
return this._config.default;
219+
return this.config.default;
220+
}
221+
222+
/** Checks whether the pull request contains fixup or squash commits. */
223+
private async hasFixupOrSquashCommits(pullRequest: PullRequest): Promise<boolean> {
224+
const commits = await this.getPullRequestCommits(pullRequest);
225+
226+
return commits.some(({parsed: {isFixup, isSquash}}) => isFixup || isSquash);
228227
}
229228
}

ng-dev/pr/merge/strategies/strategy.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {Commit, parseCommitMessage} from '../../../commit-message/parse.js';
910
import {AuthenticatedGitClient} from '../../../utils/git/authenticated-git-client.js';
1011
import {
1112
FatalMergeToolError,
@@ -200,4 +201,22 @@ export abstract class MergeStrategy {
200201
throw new MergeConflictsFatalError(failedBranches);
201202
}
202203
}
204+
205+
/** Gets all commit messages of commits in the pull request. */
206+
protected async getPullRequestCommits({prNumber}: PullRequest): Promise<
207+
{
208+
message: string;
209+
parsed: Commit;
210+
}[]
211+
> {
212+
const allCommits = await this.git.github.paginate(this.git.github.pulls.listCommits, {
213+
...this.git.remoteParams,
214+
pull_number: prNumber,
215+
});
216+
217+
return allCommits.map(({commit: {message}}) => ({
218+
message,
219+
parsed: parseCommitMessage(message),
220+
}));
221+
}
203222
}

0 commit comments

Comments
 (0)