import { fakeSha, git, logger, partial } from '~test/util.ts'; import { client as _client } from './client.ts'; import type { GerritChange, GerritLabelTypeInfo, GerritRevisionInfo, } from './schema.ts'; import { GerritScm, configureScm, nextPatchSetRef, pushForReview, } from './scm.ts'; vi.mock('./client.ts'); const clientMock = vi.mocked(_client); describe('modules/platform/gerrit/scm', () => { const gerritScm = new GerritScm(); const commitSha = fakeSha('commitSha'); const parentCommitSha = fakeSha('parentSha'); const currentRevision = fakeSha('abc123'); beforeEach(() => { configureScm('test/repo'); }); describe('pushForReview()', () => { it('pushes to refs/for/ and returns true on success', async () => { git.pushCommit.mockResolvedValueOnce(true); await expect( pushForReview({ sourceRef: 'renovate/feat', targetBranch: 'main', files: [], }), ).resolves.toBeTrue(); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'renovate/feat', targetRef: 'refs/for/main', files: [], pushOptions: ['notify=NONE', 'ready'], }); }); it('adds hashtag push options for each label', async () => { git.pushCommit.mockResolvedValueOnce(true); await expect( pushForReview({ sourceRef: 'renovate/feat', targetBranch: 'main', files: [], labels: ['team:backend', 'priority:high'], }), ).resolves.toBeTrue(); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'renovate/feat', targetRef: 'refs/for/main', files: [], pushOptions: [ 'notify=NONE', 'ready', 'hashtag=team:backend', 'hashtag=priority:high', ], }); }); it('skips voting and logs a warning when autoApprove is set but the project has no known Code-Review label definition', async () => { configureScm('test/repo'); git.pushCommit.mockResolvedValueOnce(true); await expect( pushForReview({ sourceRef: 'renovate/feat', targetBranch: 'main', files: [], autoApprove: true, }), ).resolves.toBeTrue(); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'renovate/feat', targetRef: 'refs/for/main', files: [], pushOptions: ['notify=NONE', 'ready'], }); expect(logger.logger.warn).toHaveBeenCalledWith( { repository: 'test/repo', label: 'Code-Review' }, 'Cannot auto-approve: label is not defined on the project', ); }); it('uses the maximum value configured for the Code-Review label when autoApprove is set', async () => { configureScm('test/repo', { 'Code-Review': { values: { '-1': 'disliked', '0': 'default', '1': 'looksOkay' }, default_value: 0, } satisfies GerritLabelTypeInfo, }); git.pushCommit.mockResolvedValueOnce(true); await expect( pushForReview({ sourceRef: 'renovate/feat', targetBranch: 'main', files: [], autoApprove: true, }), ).resolves.toBeTrue(); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'renovate/feat', targetRef: 'refs/for/main', files: [], pushOptions: ['notify=NONE', 'ready', 'label=Code-Review+1'], }); }); it('returns false when push fails', async () => { git.pushCommit.mockResolvedValueOnce(false); await expect( pushForReview({ sourceRef: 'renovate/feat', targetBranch: 'main', files: [], }), ).resolves.toBeFalse(); }); }); describe('commitAndPush()', () => { it('commitAndPush() - empty commit', async () => { clientMock.getBranchChange.mockResolvedValueOnce(null); git.prepareCommit.mockResolvedValueOnce(null); //empty commit await expect( gerritScm.commitAndPush({ branchName: 'renovate/dependency-1.x', baseBranch: 'main', message: 'commit msg', files: [], prTitle: 'pr title', }), ).resolves.toBeNull(); expect(clientMock.getBranchChange).toHaveBeenCalledExactlyOnceWith( 'test/repo', { branchName: 'renovate/dependency-1.x', state: 'open', targetBranch: 'main', requestDetails: ['CURRENT_REVISION'], }, ); }); it('commitAndPush() - create first commit but does not push', async () => { clientMock.getBranchChange.mockResolvedValueOnce(null); git.prepareCommit.mockResolvedValueOnce({ commitSha, parentCommitSha, files: [], }); await expect( gerritScm.commitAndPush({ branchName: 'renovate/dependency-1.x', baseBranch: 'main', message: 'commit msg', files: [], prTitle: 'pr title', }), ).resolves.toBe(commitSha); expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], message: ['pr title'], trailers: [ 'Renovate-Branch: renovate/dependency-1.x', expect.stringMatching(/^Change-Id: I[a-z0-9]{40}$/), ], prTitle: 'pr title', }); // For new changes, push should NOT be called - it will be done by createPr() expect(git.pushCommit).not.toHaveBeenCalled(); }); it('commitAndPush() - merges user trailers and overrides reserved keys', async () => { clientMock.getBranchChange.mockResolvedValueOnce(null); git.prepareCommit.mockResolvedValueOnce({ commitSha, parentCommitSha, files: [], }); await expect( gerritScm.commitAndPush({ branchName: 'renovate/dependency-1.x', baseBranch: 'main', message: 'commit msg', files: [], prTitle: 'pr title', trailers: [ 'Signed-off-by: Renovate Bot ', 'Change-Id: Iuserprovided', 'Renovate-Branch: user-provided', ], }), ).resolves.toBe(commitSha); expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], message: ['pr title'], trailers: [ 'Signed-off-by: Renovate Bot ', 'Renovate-Branch: renovate/dependency-1.x', expect.stringMatching(/^Change-Id: I[a-z0-9]{40}$/), ], prTitle: 'pr title', }); }); it('commitAndPush() - existing change keeps original target branch', async () => { const existingChange = partial({ change_id: 'Ifcd936eef0ced620040a07a337c586d0a882725b', branch: 'main', current_revision: currentRevision, revisions: { [currentRevision]: partial({ ref: 'refs/changes/56/123456/2', }), }, }); clientMock.getBranchChange.mockResolvedValueOnce(existingChange); git.prepareCommit.mockResolvedValueOnce({ commitSha, parentCommitSha, files: [], }); git.pushCommit.mockResolvedValueOnce(true); await expect( gerritScm.commitAndPush({ branchName: 'renovate/dependency-1.x', baseBranch: 'new-main', message: ['commit msg'], files: [], prTitle: 'pr title', }), ).resolves.toBe(commitSha); expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ baseBranch: 'new-main', branchName: 'renovate/dependency-1.x', files: [], message: ['pr title'], trailers: [ 'Renovate-Branch: renovate/dependency-1.x', 'Change-Id: Ifcd936eef0ced620040a07a337c586d0a882725b', ], prTitle: 'pr title', }); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ files: [], sourceRef: 'renovate/dependency-1.x', targetRef: 'refs/for/main', // not new-main pushOptions: ['notify=NONE', 'ready'], }); expect(git.setVirtualBranch).toHaveBeenCalledExactlyOnceWith( 'renovate/dependency-1.x', 'refs/changes/56/123456/3', commitSha, ); }); it('commitAndPush() - existing change without new changes', async () => { const existingChange = partial({ change_id: 'I1bf983f8f6530c44826925b1308a45fe672408a6', branch: 'main', }); clientMock.getBranchChange.mockResolvedValueOnce(existingChange); git.prepareCommit.mockResolvedValueOnce(null); //no changes await expect( gerritScm.commitAndPush({ branchName: 'renovate/dependency-1.x', baseBranch: 'main', message: ['commit msg'], files: [], prTitle: 'pr title', }), ).resolves.toBeNull(); expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], message: ['pr title'], trailers: [ 'Renovate-Branch: renovate/dependency-1.x', 'Change-Id: I1bf983f8f6530c44826925b1308a45fe672408a6', ], prTitle: 'pr title', }); expect(git.pushCommit).not.toHaveBeenCalled(); }); it('commitAndPush() - existing change with new changes - auto-approve', async () => { configureScm('test/repo', { 'Code-Review': { values: { '-2': 'rejected', '-1': 'disliked', '0': 'default', '1': 'looksOkay', '2': 'approved', }, default_value: 0, } satisfies GerritLabelTypeInfo, }); const existingChange = partial({ _number: 123456, change_id: 'I1bf983f8f6530c44826925b1308a45fe672408a6', branch: 'main', current_revision: currentRevision, revisions: { [currentRevision]: partial({ ref: 'refs/changes/56/123456/2', }), }, }); clientMock.getBranchChange.mockResolvedValueOnce(existingChange); git.prepareCommit.mockResolvedValueOnce({ commitSha, parentCommitSha, files: [], }); git.pushCommit.mockResolvedValueOnce(true); await expect( gerritScm.commitAndPush({ branchName: 'renovate/dependency-1.x', baseBranch: 'main', message: 'commit msg', files: [], prTitle: 'pr title', autoApprove: true, }), ).resolves.toBe(commitSha); expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], message: ['pr title'], trailers: [ 'Renovate-Branch: renovate/dependency-1.x', 'Change-Id: I1bf983f8f6530c44826925b1308a45fe672408a6', ], prTitle: 'pr title', autoApprove: true, }); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ files: [], sourceRef: 'renovate/dependency-1.x', targetRef: 'refs/for/main', pushOptions: ['notify=NONE', 'ready', 'label=Code-Review+2'], }); expect(git.setVirtualBranch).toHaveBeenCalledExactlyOnceWith( 'renovate/dependency-1.x', 'refs/changes/56/123456/3', commitSha, ); }); }); describe('nextPatchSetRef()', () => { it('increments patchset 1', () => { expect(nextPatchSetRef('refs/changes/45/12345/1')).toBe( 'refs/changes/45/12345/2', ); }); it('increments patchset 9 to 10', () => { expect(nextPatchSetRef('refs/changes/45/12345/9')).toBe( 'refs/changes/45/12345/10', ); }); it('increments double-digit patchset', () => { expect(nextPatchSetRef('refs/changes/00/100/42')).toBe( 'refs/changes/00/100/43', ); }); }); });