diff --git a/lib/modules/platform/default-scm.spec.ts b/lib/modules/platform/default-scm.spec.ts index 7c9f7128d8de3ab5aae05bc92ecf06b428f62ba9..2f97a3c4efa6380c6cf4ae14457caca2a4fcb16a 100644 --- a/lib/modules/platform/default-scm.spec.ts +++ b/lib/modules/platform/default-scm.spec.ts @@ -60,4 +60,16 @@ describe('modules/platform/default-scm', () => { await defaultGitScm.checkoutBranch('branchName'); expect(git.checkoutBranch).toHaveBeenCalledTimes(1); }); + + it('delegate mergeAndPush to util/git', async () => { + git.mergeBranch.mockResolvedValueOnce(); + await defaultGitScm.mergeAndPush('branchName'); + expect(git.mergeBranch).toHaveBeenCalledWith('branchName'); + }); + + it('delegate mergeBranch to util/git', async () => { + git.mergeToLocal.mockResolvedValueOnce(); + await defaultGitScm.mergeToLocal('branchName'); + expect(git.mergeToLocal).toHaveBeenCalledWith('branchName'); + }); }); diff --git a/lib/modules/platform/default-scm.ts b/lib/modules/platform/default-scm.ts index f210500a9a0d42b9a85c786a9e20e68d4c437312..92ceef5421685bb863eca6c6aa2484999151d629 100644 --- a/lib/modules/platform/default-scm.ts +++ b/lib/modules/platform/default-scm.ts @@ -38,4 +38,12 @@ export class DefaultGitScm implements PlatformScm { checkoutBranch(branchName: string): Promise<CommitSha> { return git.checkoutBranch(branchName); } + + mergeAndPush(branchName: string): Promise<void> { + return git.mergeBranch(branchName); + } + + mergeToLocal(branchName: string): Promise<void> { + return git.mergeToLocal(branchName); + } } diff --git a/lib/modules/platform/local/scm.spec.ts b/lib/modules/platform/local/scm.spec.ts index 6549475ad55d937b40bd9780dfe994c71d44756f..92efed3cf54760d915057a099238f78ce52a4467 100644 --- a/lib/modules/platform/local/scm.spec.ts +++ b/lib/modules/platform/local/scm.spec.ts @@ -65,4 +65,12 @@ describe('modules/platform/local/scm', () => { expect(await localFs.getFileList()).toHaveLength(2); }); }); + + it('mergeAndPush', async () => { + await expect(localFs.mergeAndPush('branchName')).resolves.toBeUndefined(); + }); + + it('mergeBranch', async () => { + await expect(localFs.mergeToLocal('branchName')).resolves.toBeUndefined(); + }); }); diff --git a/lib/modules/platform/local/scm.ts b/lib/modules/platform/local/scm.ts index 5b01391572c11048149fa97dcde36df50644cdce..51ba447610890427f77d9492642dbff56fe0ae74 100644 --- a/lib/modules/platform/local/scm.ts +++ b/lib/modules/platform/local/scm.ts @@ -48,4 +48,12 @@ export class LocalFs implements PlatformScm { checkoutBranch(branchName: string): Promise<CommitSha> { return Promise.resolve(''); } + + mergeAndPush(branchName: string): Promise<void> { + return Promise.resolve(); + } + + mergeToLocal(branchName: string): Promise<void> { + return Promise.resolve(); + } } diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 47deb04e5f4a479032f5728637790d35a705cdfd..3962a08b8dee2a691d2086e52d13db9188466dfe 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -235,4 +235,6 @@ export interface PlatformScm { commitAndPush(commitConfig: CommitFilesConfig): Promise<CommitSha | null>; getFileList(): Promise<string[]>; checkoutBranch(branchName: string): Promise<CommitSha>; + mergeToLocal(branchName: string): Promise<void>; + mergeAndPush(branchName: string): Promise<void>; } diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts index 7fddad6143282f842ee1e4316b19212bafd53639..8d8c38225c9db402d4b982860543eb47b213ef6e 100644 --- a/lib/util/git/index.spec.ts +++ b/lib/util/git/index.spec.ts @@ -344,14 +344,24 @@ describe('util/git/index', () => { expect(merged.all).toContain('renovate/future_branch'); }); - it('does not push if localOnly=true', async () => { + it('should throw if branch merge throws', async () => { + await expect(git.mergeBranch('not_found')).rejects.toThrow(); + }); + }); + + describe('mergeToLocal(branchName)', () => { + it('should perform a branch merge without push', async () => { + expect(fs.existsSync(`${tmpDir.path}/future_file`)).toBeFalse(); const pushSpy = jest.spyOn(SimpleGit.prototype, 'push'); - await git.mergeBranch('renovate/future_branch', true); + + await git.mergeToLocal('renovate/future_branch'); + + expect(fs.existsSync(`${tmpDir.path}/future_file`)).toBeTrue(); expect(pushSpy).toHaveBeenCalledTimes(0); }); - it('should throw if branch merge throws', async () => { - await expect(git.mergeBranch('not_found')).rejects.toThrow(); + it('should throw', async () => { + await expect(git.mergeToLocal('not_found')).rejects.toThrow(); }); }); diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index 81811c059decabd7185487b1806e977404b970de..34c85c9b7ce05cd6519b709926542d53c4cba8ba 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -779,10 +779,38 @@ export async function deleteBranch(branchName: string): Promise<void> { delete config.branchCommits[branchName]; } -export async function mergeBranch( - branchName: string, - localOnly = false -): Promise<void> { +export async function mergeToLocal(refSpecToMerge: string): Promise<void> { + let status: StatusResult | undefined; + try { + await syncGit(); + await writeGitAuthor(); + await git.reset(ResetMode.HARD); + await gitRetry(() => + git.checkout([ + '-B', + config.currentBranch, + 'origin/' + config.currentBranch, + ]) + ); + status = await git.status(); + await fetchRevSpec(refSpecToMerge); + await gitRetry(() => git.merge(['FETCH_HEAD'])); + } catch (err) { + logger.debug( + { + baseBranch: config.currentBranch, + baseSha: config.currentBranchSha, + refSpecToMerge, + status, + err, + }, + 'mergeLocally error' + ); + throw err; + } +} + +export async function mergeBranch(branchName: string): Promise<void> { let status: StatusResult | undefined; try { await syncGit(); @@ -799,13 +827,8 @@ export async function mergeBranch( ]) ); status = await git.status(); - if (localOnly) { - // merge commit, don't push to origin - await gitRetry(() => git.merge([branchName])); - } else { - await gitRetry(() => git.merge(['--ff-only', branchName])); - await gitRetry(() => git.push('origin', config.currentBranch)); - } + await gitRetry(() => git.merge(['--ff-only', branchName])); + await gitRetry(() => git.push('origin', config.currentBranch)); incLimitedValue('Commits'); } catch (err) { logger.debug( diff --git a/lib/workers/repository/onboarding/branch/index.spec.ts b/lib/workers/repository/onboarding/branch/index.spec.ts index 926ab508f7f17e048ad5560f2d1531fa27160443..c739f57c44961029f2c6ec276faae7f6fef3fefa 100644 --- a/lib/workers/repository/onboarding/branch/index.spec.ts +++ b/lib/workers/repository/onboarding/branch/index.spec.ts @@ -259,7 +259,7 @@ describe('workers/repository/onboarding/branch/index', () => { const res = await checkOnboardingBranch(config); expect(res.repoIsOnboarded).toBeFalse(); expect(res.branchList).toEqual(['renovate/configure']); - expect(git.mergeBranch).toHaveBeenCalledOnce(); + expect(scm.mergeToLocal).toHaveBeenCalledOnce(); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -287,7 +287,7 @@ describe('workers/repository/onboarding/branch/index', () => { .mockReturnValueOnce('onboarding-sha'); config.onboardingRebaseCheckbox = true; await checkOnboardingBranch(config); - expect(git.mergeBranch).not.toHaveBeenCalled(); + expect(scm.mergeToLocal).not.toHaveBeenCalled(); }); it('processes modified onboarding branch and invalidates extract cache', async () => { @@ -312,7 +312,7 @@ describe('workers/repository/onboarding/branch/index', () => { onboardingCache.isOnboardingBranchConflicted.mockResolvedValueOnce(false); config.baseBranch = 'master'; await checkOnboardingBranch(config); - expect(git.mergeBranch).toHaveBeenCalledOnce(); + expect(scm.mergeToLocal).toHaveBeenCalledOnce(); expect(onboardingCache.setOnboardingCache).toHaveBeenCalledWith( 'default-sha', 'new-onboarding-sha', @@ -336,7 +336,7 @@ describe('workers/repository/onboarding/branch/index', () => { onboardingCache.hasOnboardingBranchChanged.mockReturnValueOnce(true); onboardingCache.isOnboardingBranchConflicted.mockResolvedValueOnce(true); await checkOnboardingBranch(config); - expect(git.mergeBranch).not.toHaveBeenCalled(); + expect(scm.mergeToLocal).not.toHaveBeenCalled(); expect(onboardingCache.setOnboardingCache).toHaveBeenCalledWith( 'default-sha', 'onboarding-sha', @@ -354,7 +354,7 @@ describe('workers/repository/onboarding/branch/index', () => { .mockReturnValueOnce('onboarding-sha'); onboardingCache.isOnboardingBranchModified.mockResolvedValueOnce(false); await checkOnboardingBranch(config); - expect(git.mergeBranch).toHaveBeenCalled(); + expect(scm.mergeToLocal).toHaveBeenCalled(); expect(onboardingCache.setOnboardingCache).toHaveBeenCalledWith( 'default-sha', 'onboarding-sha', @@ -383,7 +383,7 @@ describe('workers/repository/onboarding/branch/index', () => { `Platform '${pl}' does not support extended markdown` ); expect(OnboardingState.prUpdateRequested).toBeTrue(); - expect(git.mergeBranch).toHaveBeenCalledOnce(); + expect(scm.mergeToLocal).toHaveBeenCalledOnce(); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -397,7 +397,7 @@ describe('workers/repository/onboarding/branch/index', () => { `No rebase checkbox was found in the onboarding PR` ); expect(OnboardingState.prUpdateRequested).toBeTrue(); - expect(git.mergeBranch).toHaveBeenCalledOnce(); + expect(scm.mergeToLocal).toHaveBeenCalledOnce(); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -411,7 +411,7 @@ describe('workers/repository/onboarding/branch/index', () => { `Manual onboarding PR update requested` ); expect(OnboardingState.prUpdateRequested).toBeTrue(); - expect(git.mergeBranch).toHaveBeenCalledOnce(); + expect(scm.mergeToLocal).toHaveBeenCalledOnce(); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); @@ -422,7 +422,7 @@ describe('workers/repository/onboarding/branch/index', () => { await checkOnboardingBranch(config); expect(OnboardingState.prUpdateRequested).toBeFalse(); - expect(git.mergeBranch).toHaveBeenCalledOnce(); + expect(scm.mergeToLocal).toHaveBeenCalledOnce(); expect(scm.commitAndPush).toHaveBeenCalledTimes(0); }); }); diff --git a/lib/workers/repository/onboarding/branch/index.ts b/lib/workers/repository/onboarding/branch/index.ts index c2031ab1efb76e211a4646cb504c708a7921ff0d..af7c2cd71d20377f4ae25e66f05767edc91521a0 100644 --- a/lib/workers/repository/onboarding/branch/index.ts +++ b/lib/workers/repository/onboarding/branch/index.ts @@ -8,12 +8,9 @@ import { } from '../../../../constants/error-messages'; import { logger } from '../../../../logger'; import type { Pr } from '../../../../modules/platform'; +import { scm } from '../../../../modules/platform/scm'; import { getCache } from '../../../../util/cache/repository'; -import { - getBranchCommit, - mergeBranch, - setGitAuthor, -} from '../../../../util/git'; +import { getBranchCommit, setGitAuthor } from '../../../../util/git'; import { extractAllDependencies } from '../../extract'; import { mergeRenovateConfig } from '../../init/merge'; import { OnboardingState } from '../common'; @@ -114,7 +111,7 @@ export async function checkOnboardingBranch( // TODO #7154 if (!isConflicted) { logger.debug('Merge onboarding branch in default branch'); - await mergeBranch(onboardingBranch!, true); + await scm.mergeToLocal(onboardingBranch!); } } setOnboardingCache( diff --git a/lib/workers/repository/update/branch/automerge.spec.ts b/lib/workers/repository/update/branch/automerge.spec.ts index c675d1619d9c68100b2d4a8b588a76adaabce4e3..3abe8d0dd5a74e8e6b04309f40578158f35246ca 100644 --- a/lib/workers/repository/update/branch/automerge.spec.ts +++ b/lib/workers/repository/update/branch/automerge.spec.ts @@ -1,4 +1,4 @@ -import { git, partial, platform, scm } from '../../../../../test/util'; +import { partial, platform, scm } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; import type { RenovateConfig } from '../../../../config/types'; import type { Pr } from '../../../../modules/platform/types'; @@ -64,7 +64,7 @@ describe('workers/repository/update/branch/automerge', () => { config.automergeType = 'branch'; config.baseBranch = 'test-branch'; platform.getBranchStatus.mockResolvedValueOnce('green'); - git.mergeBranch.mockImplementationOnce(() => { + scm.mergeAndPush.mockImplementationOnce(() => { throw new Error('merge error'); }); diff --git a/lib/workers/repository/update/branch/automerge.ts b/lib/workers/repository/update/branch/automerge.ts index b6595e2ec118e98b10ae7dda565624221cc8f876..e355a4cf141b122498a2ba646cafd3656d8d563b 100644 --- a/lib/workers/repository/update/branch/automerge.ts +++ b/lib/workers/repository/update/branch/automerge.ts @@ -3,7 +3,6 @@ import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; import { platform } from '../../../../modules/platform'; import { scm } from '../../../../modules/platform/scm'; -import { mergeBranch } from '../../../../util/git'; import { isScheduledNow } from './schedule'; import { resolveBranchStatus } from './status-checks'; @@ -44,7 +43,7 @@ export async function tryBranchAutomerge( logger.info(`DRY-RUN: Would automerge branch ${config.branchName!}`); } else { await scm.checkoutBranch(config.baseBranch!); - await mergeBranch(config.branchName!); + await scm.mergeAndPush(config.branchName!); } logger.info({ branch: config.branchName }, 'Branch automerged'); return 'automerged'; // Branch no longer exists