From 6c42022f78c9ff73983e95c97099bb0cc4442b27 Mon Sep 17 00:00:00 2001
From: Markus Schulz <msc@onesty-tech.de>
Date: Mon, 7 Aug 2023 17:39:43 +0200
Subject: [PATCH] refactor(scm): use scm for mergeBranch, mergeToLocal (#23448)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/modules/platform/default-scm.spec.ts      | 12 +++++
 lib/modules/platform/default-scm.ts           |  8 ++++
 lib/modules/platform/local/scm.spec.ts        |  8 ++++
 lib/modules/platform/local/scm.ts             |  8 ++++
 lib/modules/platform/types.ts                 |  2 +
 lib/util/git/index.spec.ts                    | 18 ++++++--
 lib/util/git/index.ts                         | 45 ++++++++++++++-----
 .../onboarding/branch/index.spec.ts           | 18 ++++----
 .../repository/onboarding/branch/index.ts     |  9 ++--
 .../update/branch/automerge.spec.ts           |  4 +-
 .../repository/update/branch/automerge.ts     |  3 +-
 11 files changed, 101 insertions(+), 34 deletions(-)

diff --git a/lib/modules/platform/default-scm.spec.ts b/lib/modules/platform/default-scm.spec.ts
index 7c9f7128d8..2f97a3c4ef 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 f210500a9a..92ceef5421 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 6549475ad5..92efed3cf5 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 5b01391572..51ba447610 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 47deb04e5f..3962a08b8d 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 7fddad6143..8d8c38225c 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 81811c059d..34c85c9b7c 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 926ab508f7..c739f57c44 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 c2031ab1ef..af7c2cd71d 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 c675d1619d..3abe8d0dd5 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 b6595e2ec1..e355a4cf14 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
-- 
GitLab