@@ -16,19 +16,29 @@ import {isGithubApiError} from '../../../utils/git/github.js';
1616import { FatalMergeToolError , MergeConflictsFatalError } from '../failures.js' ;
1717import { Prompt } from '../../../utils/prompt.js' ;
1818import { AutosquashMergeStrategy } from './autosquash-merge.js' ;
19+ import { Commit , parseCommitMessage } from '../../../commit-message/parse.js' ;
1920
2021/** Type describing the parameters for the Octokit `merge` API endpoint. */
2122type OctokitMergeParams = RestEndpointMethodTypes [ 'pulls' ] [ 'merge' ] [ 'parameters' ] ;
2223
2324/** Separator between commit message header and body. */
2425const COMMIT_HEADER_SEPARATOR = '\n\n' ;
2526
27+ /** Interface describing a pull request commit. */
28+ interface PullRequestCommit {
29+ message : string ;
30+ parsed : Commit ;
31+ }
32+
2633/**
2734 * Merge strategy that primarily leverages the Github API. The strategy merges a given
2835 * pull request into a target branch using the API. This ensures that Github displays
2936 * the pull request as merged. The merged commits are then cherry-picked into the remaining
3037 * target branches using the local Git instance. The benefit is that the Github merged state
31- * is properly set, but a notable downside is that PRs cannot use fixup or squash commits.
38+ * is properly set.
39+ *
40+ * A notable downside is that fixup or squash commits are not supported when `auto` merge
41+ * method is not used, as the Github API does not support this.
3242 */
3343export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
3444 constructor (
@@ -48,23 +58,50 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
4858 */
4959 override async merge ( pullRequest : PullRequest ) : Promise < void > {
5060 const { githubTargetBranch, prNumber, needsCommitMessageFixup, targetBranches} = pullRequest ;
51- const method = this . getMergeActionFromPullRequest ( pullRequest ) ;
5261 const cherryPickTargetBranches = targetBranches . filter ( ( b ) => b !== githubTargetBranch ) ;
53-
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 super . merge ( pullRequest ) ;
60- }
62+ const commits = await this . getPullRequestCommits ( pullRequest ) ;
63+ const { squashCount, fixupCount, normalCommitsCount} = await this . getCommitsInfo ( pullRequest ) ;
64+ const method = this . getMergeActionFromPullRequest ( pullRequest ) ;
6165
6266 const mergeOptions : OctokitMergeParams = {
6367 pull_number : prNumber ,
64- merge_method : method === 'rebase-with-fixup ' ? 'rebase' : method ,
68+ merge_method : method === 'auto ' ? 'rebase' : method ,
6569 ...this . git . remoteParams ,
6670 } ;
6771
72+ // When the merge method is `auto`, the merge strategy will determine the best merge method
73+ // based on the pull request's commits.
74+ if ( method === 'auto' ) {
75+ const hasFixUpOrSquashAndMultipleCommits =
76+ normalCommitsCount > 1 && ( fixupCount > 0 || squashCount > 0 ) ;
77+
78+ // If the PR has fixup/squash commits against multiple normal commits, or if the
79+ // commit message needs to be fixed up, delegate to the autosquash merge strategy.
80+ if ( needsCommitMessageFixup || hasFixUpOrSquashAndMultipleCommits ) {
81+ return super . merge ( pullRequest ) ;
82+ }
83+
84+ const hasOnlyFixUpForOneCommit =
85+ normalCommitsCount === 1 && fixupCount > 0 && squashCount === 0 ;
86+
87+ const hasOnlySquashForOneCommit = normalCommitsCount === 1 && squashCount > 1 ;
88+
89+ // If the PR has only one normal commit and some fixup commits, the PR is squashed.
90+ // The commit message from the single normal commit is used.
91+ if ( hasOnlyFixUpForOneCommit ) {
92+ mergeOptions . merge_method = 'squash' ;
93+ const [ title , message ] = commits [ 0 ] . message . split ( COMMIT_HEADER_SEPARATOR ) ;
94+
95+ mergeOptions . commit_title = title ;
96+ mergeOptions . commit_message = message ;
97+ // If the PR has only one normal commit and more than one squash commit, the PR is
98+ // squashed and the user is prompted to edit the commit message.
99+ } else if ( hasOnlySquashForOneCommit ) {
100+ mergeOptions . merge_method = 'squash' ;
101+ await this . _promptCommitMessageEdit ( pullRequest , mergeOptions ) ;
102+ }
103+ }
104+
68105 if ( needsCommitMessageFixup ) {
69106 // Commit message fixup does not work with other merge methods as the Github API only
70107 // allows commit message modifications for squash merging.
@@ -74,6 +111,7 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
74111 `modified if the PR is merged using squash.` ,
75112 ) ;
76113 }
114+
77115 await this . _promptCommitMessageEdit ( pullRequest , mergeOptions ) ;
78116 }
79117
@@ -113,6 +151,8 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
113151 // If the PR does not need to be merged into any other target branches,
114152 // we exit here as we already completed the merge.
115153 if ( ! cherryPickTargetBranches . length ) {
154+ await this . createMergeComment ( pullRequest , targetBranches ) ;
155+
116156 return ;
117157 }
118158
@@ -146,27 +186,7 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
146186 }
147187
148188 this . pushTargetBranchesUpstream ( cherryPickTargetBranches ) ;
149-
150- /** The local branch names of the github targeted branches. */
151- const banchesAndSha : [ branchName : string , commitSha : string ] [ ] = targetBranches . map (
152- ( targetBranch ) => {
153- const localBranch = this . getLocalTargetBranchName ( targetBranch ) ;
154-
155- /** The SHA of the commit pushed to github which represents closing the PR. */
156- const sha = this . git . run ( [ 'rev-parse' , localBranch ] ) . stdout . trim ( ) ;
157- return [ targetBranch , sha ] ;
158- } ,
159- ) ;
160- // Because our process brings changes into multiple branchces, we include a comment which
161- // expresses all of the branches the changes were merged into.
162- await this . git . github . issues . createComment ( {
163- ...this . git . remoteParams ,
164- issue_number : pullRequest . prNumber ,
165- body :
166- 'This PR was merged into the repository. ' +
167- 'The changes were merged into the following branches:\n\n' +
168- `${ banchesAndSha . map ( ( [ branch , sha ] ) => `- ${ branch } : ${ sha } ` ) . join ( '\n' ) } ` ,
169- } ) ;
189+ await this . createMergeComment ( pullRequest , targetBranches ) ;
170190 }
171191
172192 /**
@@ -178,7 +198,7 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
178198 pullRequest : PullRequest ,
179199 mergeOptions : OctokitMergeParams ,
180200 ) {
181- const commitMessage = await this . _getDefaultSquashCommitMessage ( pullRequest ) ;
201+ const commitMessage = await this . getDefaultSquashCommitMessage ( pullRequest ) ;
182202 const result = await Prompt . editor ( {
183203 message : 'Please update the commit message' ,
184204 default : commitMessage ,
@@ -198,7 +218,7 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
198218 * multiple commit messages if a PR is merged in squash mode. We try to replicate this
199219 * behavior here so that we have a default commit message that can be fixed up.
200220 */
201- private async _getDefaultSquashCommitMessage ( pullRequest : PullRequest ) : Promise < string > {
221+ private async getDefaultSquashCommitMessage ( pullRequest : PullRequest ) : Promise < string > {
202222 const commits = await this . getPullRequestCommits ( pullRequest ) ;
203223 const messageBase = `${ pullRequest . title } ${ COMMIT_HEADER_SEPARATOR } ` ;
204224 if ( commits . length <= 1 ) {
@@ -219,10 +239,60 @@ export class GithubApiMergeStrategy extends AutosquashMergeStrategy {
219239 return this . config . default ;
220240 }
221241
222- /** Checks whether the pull request contains fixup or squash commits. */
223- private async hasFixupOrSquashCommits ( pullRequest : PullRequest ) : Promise < boolean > {
242+ /** Returns information about the commits in the pull request. */
243+ private async getCommitsInfo ( pullRequest : PullRequest ) : Promise <
244+ Readonly < {
245+ fixupCount : number ;
246+ squashCount : number ;
247+ normalCommitsCount : number ;
248+ } >
249+ > {
224250 const commits = await this . getPullRequestCommits ( pullRequest ) ;
251+ const commitsInfo = {
252+ fixupCount : 0 ,
253+ squashCount : 0 ,
254+ normalCommitsCount : 1 ,
255+ } ;
256+
257+ if ( commits . length === 1 ) {
258+ return commitsInfo ;
259+ }
260+
261+ for ( let index = 1 ; index < commits . length ; index ++ ) {
262+ const {
263+ parsed : { isFixup, isSquash} ,
264+ } = commits [ index ] ;
265+
266+ if ( isFixup ) {
267+ commitsInfo . fixupCount ++ ;
268+ } else if ( isSquash ) {
269+ commitsInfo . squashCount ++ ;
270+ } else {
271+ commitsInfo . normalCommitsCount ++ ;
272+ }
273+ }
274+
275+ return commitsInfo ;
276+ }
277+
278+ /** Commits of the pull request. */
279+ private commits : PullRequestCommit [ ] | undefined ;
280+ /** Gets all commit messages of commits in the pull request. */
281+ protected async getPullRequestCommits ( { prNumber} : PullRequest ) : Promise < PullRequestCommit [ ] > {
282+ if ( this . commits ) {
283+ return this . commits ;
284+ }
285+
286+ const allCommits = await this . git . github . paginate ( this . git . github . pulls . listCommits , {
287+ ...this . git . remoteParams ,
288+ pull_number : prNumber ,
289+ } ) ;
290+
291+ this . commits = allCommits . map ( ( { commit : { message} } ) => ( {
292+ message,
293+ parsed : parseCommitMessage ( message ) ,
294+ } ) ) ;
225295
226- return commits . some ( ( { parsed : { isFixup , isSquash } } ) => isFixup || isSquash ) ;
296+ return this . commits ;
227297 }
228298}
0 commit comments