Skip to content

Commit 2f25a04

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 2f25a04

File tree

7 files changed

+129
-28
lines changed

7 files changed

+129
-28
lines changed

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

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

.ng-dev/pull-request.mts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ 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',
7+
labels: [{pattern: 'merge: squash commits', method: 'squash'}],
8+
},
9+
conditionalAutosquashMerge: true,
610
requiredStatuses: [{name: 'test', type: 'check'}],
711

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

ng-dev/pr/config/index.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface GithubApiMergeStrategyConfig {
2323
}
2424

2525
/** Configuration for the merge script. */
26-
export interface PullRequestConfig {
26+
export type PullRequestConfig = {
2727
/**
2828
* Configuration for the upstream remote. All of these options are optional as
2929
* defaults are provided by the common dev-infra github configuration.
@@ -36,6 +36,13 @@ export interface PullRequestConfig {
3636
/** List of statuses that are required before a pull request can be merged. */
3737
requiredStatuses?: {type: 'check' | 'status'; name: string}[];
3838

39+
/**
40+
* Whether pull requests should be merged using a conditional autosquash strategy.
41+
* If a pull request contains fixup or squash commits, the autosquash strategy
42+
* will be used. Otherwise, the Github API merge strategy will be used.
43+
*/
44+
conditionalAutosquashMerge?: boolean;
45+
3946
/**
4047
* Whether pull requests should be merged using the Github API. This can be enabled
4148
* if projects want to have their pull requests show up as `Merged` in the Github UI.
@@ -63,7 +70,7 @@ export interface PullRequestConfig {
6370
* follow the canonical branching/versioning.
6471
*/
6572
__noTargetLabeling?: boolean;
66-
}
73+
};
6774

6875
/** Loads and validates the merge configuration. */
6976
export function assertValidPullRequestConfig<T extends NgDevConfig>(
@@ -76,10 +83,18 @@ export function assertValidPullRequestConfig<T extends NgDevConfig>(
7683
);
7784
}
7885

79-
if (config.pullRequest.githubApiMerge === undefined) {
86+
const {conditionalAutosquashMerge, githubApiMerge} = config.pullRequest;
87+
if (githubApiMerge === undefined) {
8088
errors.push('No explicit choice of merge strategy. Please set `githubApiMerge`.');
8189
}
8290

91+
if (conditionalAutosquashMerge && !githubApiMerge) {
92+
errors.push(
93+
'`conditionalAutosquashMerge` requires a GitHub API merge strategy to inspect commit history. ' +
94+
'Please configure `githubApiMerge` or disable `conditionalAutosquashMerge`.',
95+
);
96+
}
97+
8398
if (errors.length) {
8499
throw new ConfigValidationError('Invalid `pullRequest` configuration', errors);
85100
}

ng-dev/pr/merge/merge-tool.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import {loadAndValidatePullRequest, PullRequest} from './pull-request.js';
2020
import {GithubApiMergeStrategy} from './strategies/api-merge.js';
2121
import {AutosquashMergeStrategy} from './strategies/autosquash-merge.js';
22+
import {ConditionalAutosquashMergeStrategy} from './strategies/conditional-autosquash-merge.js';
2223
import {GithubConfig, NgDevConfig} from '../../utils/config.js';
2324
import {assertValidReleaseConfig} from '../../release/config/index.js';
2425
import {
@@ -139,9 +140,7 @@ export class MergeTool {
139140
throw new UserAbortedMergeToolError();
140141
}
141142

142-
const strategy = this.config.pullRequest.githubApiMerge
143-
? new GithubApiMergeStrategy(this.git, this.config.pullRequest.githubApiMerge)
144-
: new AutosquashMergeStrategy(this.git);
143+
const strategy = this.getMergeStrategy();
145144

146145
// Branch or revision that is currently checked out so that we can switch back to
147146
// it once the pull request has been merged.
@@ -194,6 +193,23 @@ export class MergeTool {
194193
}
195194
}
196195

196+
private getMergeStrategy():
197+
| AutosquashMergeStrategy
198+
| ConditionalAutosquashMergeStrategy
199+
| GithubApiMergeStrategy {
200+
const {conditionalAutosquashMerge, githubApiMerge} = this.config.pullRequest;
201+
202+
if (conditionalAutosquashMerge && githubApiMerge) {
203+
return new ConditionalAutosquashMergeStrategy(this.git, githubApiMerge);
204+
}
205+
206+
if (githubApiMerge) {
207+
return new GithubApiMergeStrategy(this.git, githubApiMerge);
208+
}
209+
210+
return new AutosquashMergeStrategy(this.git);
211+
}
212+
197213
/**
198214
* Modifies the pull request in place with new target branches based on user
199215
* selection from the available active branches.

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

Lines changed: 3 additions & 19 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

@@ -52,7 +48,7 @@ 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

5854
const mergeOptions: OctokitMergeParams = {
@@ -195,10 +191,7 @@ export class GithubApiMergeStrategy extends MergeStrategy {
195191
* behavior here so that we have a default commit message that can be fixed up.
196192
*/
197193
private async _getDefaultSquashCommitMessage(pullRequest: PullRequest): Promise<string> {
198-
const commits = (await this._getPullRequestCommitMessages(pullRequest)).map((message) => ({
199-
message,
200-
parsed: parseCommitMessage(message),
201-
}));
194+
const commits = await this.getPullRequestCommits(pullRequest);
202195
const messageBase = `${pullRequest.title}${COMMIT_HEADER_SEPARATOR}`;
203196
if (commits.length <= 1) {
204197
return `${messageBase}${commits[0].parsed.body}`;
@@ -207,17 +200,8 @@ export class GithubApiMergeStrategy extends MergeStrategy {
207200
return `${messageBase}${joinedMessages}`;
208201
}
209202

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-
219203
/** Determines the merge action from the given pull request. */
220-
private _getMergeActionFromPullRequest({labels}: PullRequest): GithubApiMergeMethod {
204+
getMergeActionFromPullRequest({labels}: PullRequest): GithubApiMergeMethod {
221205
if (this._config.labels) {
222206
const matchingLabel = this._config.labels.find(({pattern}) => labels.includes(pattern));
223207
if (matchingLabel !== undefined) {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {AuthenticatedGitClient} from '../../../utils/git/authenticated-git-client.js';
10+
import {GithubApiMergeStrategyConfig} from '../../config/index.js';
11+
import {PullRequest} from '../pull-request.js';
12+
13+
import {AutosquashMergeStrategy} from './autosquash-merge.js';
14+
import {GithubApiMergeStrategy} from './api-merge.js';
15+
import {MergeStrategy} from './strategy.js';
16+
17+
/**
18+
* Merge strategy that conditionally uses autosquash or the Github API merge.
19+
* If a pull request contains fixup or squash commits, the autosquash strategy
20+
* will be used. Otherwise, the Github API merge strategy will be used.
21+
*/
22+
export class ConditionalAutosquashMergeStrategy extends MergeStrategy {
23+
private readonly githubApiMergeStrategy: GithubApiMergeStrategy;
24+
25+
constructor(
26+
git: AuthenticatedGitClient,
27+
private config: GithubApiMergeStrategyConfig,
28+
) {
29+
super(git);
30+
this.githubApiMergeStrategy = new GithubApiMergeStrategy(this.git, this.config);
31+
}
32+
33+
override async merge(pullRequest: PullRequest): Promise<void> {
34+
const mergeAction = this.githubApiMergeStrategy.getMergeActionFromPullRequest(pullRequest);
35+
36+
// Squash and Merge will create a single commit message and thus we can use the API to merge.
37+
return mergeAction === 'rebase' &&
38+
(pullRequest.needsCommitMessageFixup || (await this.hasFixupOrSquashCommits(pullRequest)))
39+
? new AutosquashMergeStrategy(this.git).merge(pullRequest)
40+
: this.githubApiMergeStrategy.merge(pullRequest);
41+
}
42+
43+
/** Checks whether the pull request contains fixup or squash commits. */
44+
private async hasFixupOrSquashCommits(pullRequest: PullRequest): Promise<boolean> {
45+
const commits = await this.getPullRequestCommits(pullRequest);
46+
47+
return commits.some(({parsed: {isFixup, isSquash}}) => isFixup || isSquash);
48+
}
49+
}

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)