From 5375933ceacbcd7ea0295dd5fd786d982042f6af Mon Sep 17 00:00:00 2001
From: Colin O'Dell <colinodell@gmail.com>
Date: Sun, 2 Oct 2022 01:56:52 -0400
Subject: [PATCH] feat(git): prune branches sequentially (#18068)

---
 lib/util/git/error.ts       |  6 ++++++
 lib/util/git/errors.spec.ts | 12 ++++++++++++
 lib/util/git/index.spec.ts  | 24 ++++++++++++++++++++++++
 lib/util/git/index.ts       | 26 +++++++++++++++++++++++---
 4 files changed, 65 insertions(+), 3 deletions(-)
 create mode 100644 lib/util/git/errors.spec.ts

diff --git a/lib/util/git/error.ts b/lib/util/git/error.ts
index e2b1aad028..19b8ab6a52 100644
--- a/lib/util/git/error.ts
+++ b/lib/util/git/error.ts
@@ -138,3 +138,9 @@ export function handleCommitError(
   // We don't know why this happened, so this will cause bubble up to a branch error
   throw err;
 }
+
+export function bulkChangesDisallowed(err: Error): boolean {
+  return err.message.includes(
+    'remote: Repository policies do not allow pushes that update more than'
+  );
+}
diff --git a/lib/util/git/errors.spec.ts b/lib/util/git/errors.spec.ts
new file mode 100644
index 0000000000..39d3f95faa
--- /dev/null
+++ b/lib/util/git/errors.spec.ts
@@ -0,0 +1,12 @@
+import { bulkChangesDisallowed } from './error';
+
+describe('util/git/errors', () => {
+  describe('bulkChangesDisallowed', () => {
+    it('should match the expected error', () => {
+      const err = new Error(
+        "To https://github.com/the-org/st-mono.git\n!\t:refs/renovate/branches/renovate/Dependencies-mobile-ios-minor\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/database-mongodb-4.x\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-add-field-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-add-field-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-count-characters-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-count-characters-pin-dependencies\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-count-characters-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-counter-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-counter-pin-dependencies\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-counter-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-header-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-header-pin-dependencies\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-header-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-mega-menu-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-mega-menu-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-nav-scroller-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-nav-scroller-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-quantity-counter-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-scrollspy-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-scrollspy-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-show-animation-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-show-animation-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-step-form-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-sticky-block-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-sticky-block-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-switch-major-web-shared-norm\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-switch-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-toggle-state-major-web-shared-norm\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-toggle-state-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-video-bg-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-video-bg-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-video-player-major-web-shared-norm\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/hs-video-player-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/infrastructure-mongodbatlas-1.x\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/infrastructure-random-3.x\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/web-blog-vapor-leaf-4.x\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/web-blog-vapor-vapor-4.x\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/web-shared-major-web-shared-dev\t[remote failure] (remote failed to report status)\n!\t:refs/renovate/branches/renovate/web-shared-web-shared-dev\t[remote failure] (remote failed to report status)\nDone\nPushing to https://github.com/the-org/st-mono.git\nPOST git-receive-pack (5863 bytes)\nremote: Repository policies do not allow pushes that update more than 2 branches or tags.\nerror: failed to push some refs to 'https://github.com/the-org/st-mono.git'\n"
+      );
+      expect(bulkChangesDisallowed(err)).toBe(true);
+    });
+  });
+});
diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts
index 8822a6eb10..4bf559a99f 100644
--- a/lib/util/git/index.spec.ts
+++ b/lib/util/git/index.spec.ts
@@ -984,8 +984,13 @@ describe('util/git/index', () => {
 
       await git.pushCommitToRenovateRef(commit, 'bbb');
       await git.pushCommitToRenovateRef(commit, 'ccc', 'branches');
+
+      const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
+
+      expect(await lsRenovateRefs()).not.toBeEmpty();
       await git.clearRenovateRefs();
       expect(await lsRenovateRefs()).toBeEmpty();
+      expect(pushSpy).toHaveBeenCalledOnce();
     });
 
     it('preserves unknown sections by default', async () => {
@@ -996,6 +1001,25 @@ describe('util/git/index', () => {
       await git.clearRenovateRefs();
       expect(await lsRenovateRefs()).toEqual(['refs/renovate/foo/bar']);
     });
+
+    it('falls back to sequential ref deletion if bulk changes are disallowed', async () => {
+      const commit = git.getBranchCommit('develop')!;
+      await git.pushCommitToRenovateRef(commit, 'foo');
+      await git.pushCommitToRenovateRef(commit, 'bar');
+      await git.pushCommitToRenovateRef(commit, 'baz');
+
+      const pushSpy = jest.spyOn(SimpleGit.prototype, 'push');
+      pushSpy.mockImplementationOnce(() => {
+        throw new Error(
+          'remote: Repository policies do not allow pushes that update more than 2 branches or tags.'
+        );
+      });
+
+      expect(await lsRenovateRefs()).not.toBeEmpty();
+      await git.clearRenovateRefs();
+      expect(await lsRenovateRefs()).toBeEmpty();
+      expect(pushSpy).toHaveBeenCalledTimes(4);
+    });
   });
 
   describe('listCommitTree', () => {
diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts
index d357441b60..93cfad07e0 100644
--- a/lib/util/git/index.ts
+++ b/lib/util/git/index.ts
@@ -36,7 +36,11 @@ import {
   getCachedConflictResult,
   setCachedConflictResult,
 } from './conflicts-cache';
-import { checkForPlatformFailure, handleCommitError } from './error';
+import {
+  bulkChangesDisallowed,
+  checkForPlatformFailure,
+  handleCommitError,
+} from './error';
 import {
   getCachedModifiedResult,
   setCachedModifiedResult,
@@ -1149,6 +1153,11 @@ export async function pushCommitToRenovateRef(
  *
  *   $ git push --delete origin refs/renovate/foo refs/renovate/bar refs/renovate/baz
  *
+ * If Step 2 fails because the repo doesn't allow bulk changes, we'll remove them one by one instead:
+ *
+ *   $ git push --delete origin refs/renovate/foo
+ *   $ git push --delete origin refs/renovate/bar
+ *   $ git push --delete origin refs/renovate/baz
  */
 export async function clearRenovateRefs(): Promise<void> {
   if (!gitInitialized || !remoteRefsExist) {
@@ -1181,8 +1190,19 @@ export async function clearRenovateRefs(): Promise<void> {
   obsoleteRefs.push(...renovateBranchRefs);
 
   if (obsoleteRefs.length) {
-    const pushOpts = ['--delete', 'origin', ...obsoleteRefs];
-    await git.push(pushOpts);
+    try {
+      const pushOpts = ['--delete', 'origin', ...obsoleteRefs];
+      await git.push(pushOpts);
+    } catch (err) /* istanbul ignore next */ {
+      if (bulkChangesDisallowed(err)) {
+        for (const ref of obsoleteRefs) {
+          const pushOpts = ['--delete', 'origin', ref];
+          await git.push(pushOpts);
+        }
+      } else {
+        throw err;
+      }
+    }
   }
 
   remoteRefsExist = false;
-- 
GitLab