diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index 59a5dcd788ca21..4901128bb858b1 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -98,7 +98,11 @@ describe('modules/platform/gerrit/client', () => { ['owner:self', { branchName: 'dependency-xyz' }], ['project:repo', { branchName: 'dependency-xyz' }], ['-is:wip', { branchName: 'dependency-xyz' }], - ['hashtag:sourceBranch-dependency-xyz', { branchName: 'dependency-xyz' }], + [ + 'footer:Renovate-Branch=dependency-xyz', + { branchName: 'dependency-xyz' }, + ], + ['hashtag:sourceBranch-dependency-xyz', { branchName: 'dependency-xyz' }], // for backwards compatibility ['label:Code-Review=-2', { branchName: 'dependency-xyz', label: '-2' }], [ 'branch:otherTarget', diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 3182db64db37b2..cd2dd1fc30bec6 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -231,8 +231,17 @@ class GerritClient { ): string[] { const filterState = mapPrStateToGerritFilter(searchConfig.state); const filters = ['owner:self', 'project:' + repository, filterState]; - if (searchConfig.branchName !== '') { - filters.push(`hashtag:sourceBranch-${searchConfig.branchName}`); + if (searchConfig.branchName) { + filters.push( + ...[ + '(', + `footer:Renovate-Branch=${searchConfig.branchName}`, + // for backwards compatibility + 'OR', + `hashtag:sourceBranch-${searchConfig.branchName}`, + ')', + ], + ); } if (searchConfig.targetBranch) { filters.push(`branch:${searchConfig.targetBranch}`); diff --git a/lib/modules/platform/gerrit/readme.md b/lib/modules/platform/gerrit/readme.md index 01a539311cf65d..7246f3f946dc78 100644 --- a/lib/modules/platform/gerrit/readme.md +++ b/lib/modules/platform/gerrit/readme.md @@ -3,11 +3,16 @@ ## Supported Gerrit versions Renovate supports all Gerrit 3.x versions. + Support for Gerrit is currently _experimental_, meaning that it _might_ still have some undiscovered bugs or design limitations, and that we _might_ need to change functionality in a non-backwards compatible manner in a non-major release. -The current implementation uses Gerrit's "hashtags" feature. -Therefore you must use a Gerrit version that uses the [NoteDB](https://gerrit-review.googlesource.com/Documentation/note-db.html) backend. -We did not test Gerrit `2.x` with NoteDB (only in `2.15` and `2.16`), but could work. +Renovate stores its metadata in the _commit message footer_. + +Previously Renovate stored metadata in Gerrit's _hashtags_. +To keep backwards compatibility, Renovate still reads metadata from hashtags. +But Renovate _always_ puts its metadata in the _commit message footer_! +When the Renovate maintainers mark Gerrit support as stable, the maintainers will remove the "read metadata from hashtags" feature. +This means changes without metadata in the commit message footer will be "forgotten" by Renovate. ## Authentication diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 18667a04c4d4c5..9be155245ad15e 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -300,13 +300,18 @@ describe('modules/platform/gerrit/scm', () => { baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], - message: ['commit msg', expect.stringMatching(/Change-Id: I.{32}/)], + message: [ + 'commit msg', + expect.stringMatching( + /^Renovate-Branch: renovate\/dependency-1\.x\nChange-Id: I[a-z0-9]{40}$/, + ), + ], force: true, }); expect(git.pushCommit).toHaveBeenCalledWith({ files: [], sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main%t=sourceBranch-renovate/dependency-1.x', + targetRef: 'refs/for/main', }); }); @@ -339,7 +344,10 @@ describe('modules/platform/gerrit/scm', () => { baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], - message: ['commit msg', 'Change-Id: ...'], + message: [ + 'commit msg', + 'Renovate-Branch: renovate/dependency-1.x\nChange-Id: ...', + ], force: true, }); expect(git.fetchRevSpec).toHaveBeenCalledWith('refs/changes/1/2'); @@ -377,14 +385,17 @@ describe('modules/platform/gerrit/scm', () => { baseBranch: 'main', branchName: 'renovate/dependency-1.x', files: [], - message: ['commit msg', 'Change-Id: ...'], + message: [ + 'commit msg', + 'Renovate-Branch: renovate/dependency-1.x\nChange-Id: ...', + ], force: true, }); expect(git.fetchRevSpec).toHaveBeenCalledWith('refs/changes/1/2'); expect(git.pushCommit).toHaveBeenCalledWith({ files: [], sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main%t=sourceBranch-renovate/dependency-1.x', + targetRef: 'refs/for/main', }); expect(clientMock.wasApprovedBy).toHaveBeenCalledWith( existingChange, diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index f4369fa39e2877..d8500e95205251 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -109,7 +109,7 @@ export class GerritScm extends DefaultGitScm { typeof commit.message === 'string' ? [commit.message] : commit.message; commit.message = [ ...origMsg, - `Change-Id: ${existingChange?.change_id ?? generateChangeId()}`, + `Renovate-Branch: ${commit.branchName}\nChange-Id: ${existingChange?.change_id ?? generateChangeId()}`, ]; const commitResult = await git.prepareCommit({ ...commit, force: true }); if (commitResult) { @@ -123,9 +123,7 @@ export class GerritScm extends DefaultGitScm { if (hasChanges || commit.force) { const pushResult = await git.pushCommit({ sourceRef: commit.branchName, - targetRef: `refs/for/${commit.baseBranch!}%t=sourceBranch-${ - commit.branchName - }`, + targetRef: `refs/for/${commit.baseBranch!}`, files: commit.files, }); if (pushResult) { diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index 3aae7d47e6831e..0d1b5d90fed354 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -34,6 +34,9 @@ export type GerritReviewersType = 'REVIEWER' | 'CC' | 'REMOVED'; export interface GerritChange { branch: string; + /** + * for backwards compatibility + */ hashtags?: string[]; change_id: string; subject: string; diff --git a/lib/modules/platform/gerrit/utils.spec.ts b/lib/modules/platform/gerrit/utils.spec.ts index f5159804473bea..b609fdf7a88c0e 100644 --- a/lib/modules/platform/gerrit/utils.spec.ts +++ b/lib/modules/platform/gerrit/utils.spec.ts @@ -10,6 +10,7 @@ import type { GerritChangeMessageInfo, GerritChangeStatus, GerritLabelTypeInfo, + GerritRevisionInfo, } from './types'; import * as utils from './utils'; import { mapBranchStatusToLabel } from './utils'; @@ -83,7 +84,6 @@ describe('modules/platform/gerrit/utils', () => { const change = partial({ _number: 123456, status: 'NEW', - hashtags: ['other', 'sourceBranch-renovate/dependency-1.x'], branch: 'main', subject: 'Fix for', reviewers: { @@ -91,6 +91,15 @@ describe('modules/platform/gerrit/utils', () => { REMOVED: [], CC: [], }, + current_revision: 'abc', + revisions: { + abc: partial({ + commit: { + message: + 'Some change\n\nRenovate-Branch: renovate/dependency-1.x\nChange-Id: ...', + }, + }), + }, messages: [ partial({ id: '9d78ac236714cee8c2d86e95d638358925cf6853', @@ -122,11 +131,10 @@ describe('modules/platform/gerrit/utils', () => { }); }); - it('map a gerrit change without sourceBranch-tag and reviewers to Pr', () => { + it('map a gerrit change without source branch info and reviewers to Pr', () => { const change = partial({ _number: 123456, status: 'NEW', - hashtags: ['other'], branch: 'main', subject: 'Fix for', }); @@ -145,26 +153,80 @@ describe('modules/platform/gerrit/utils', () => { }); describe('extractSourceBranch()', () => { - it('without hashtags', () => { + it('no commit message', () => { + const change = partial(); + expect(utils.extractSourceBranch(change)).toBeUndefined(); + }); + + it('commit message with no footer', () => { const change = partial({ - hashtags: undefined, + current_revision: 'abc', + revisions: { + abc: partial({ + commit: { + message: 'some message...', + }, + }), + }, }); expect(utils.extractSourceBranch(change)).toBeUndefined(); }); - it('no hashtag with "sourceBranch-" prefix', () => { + it('commit message with footer', () => { const change = partial({ - hashtags: ['other', 'another'], + current_revision: 'abc', + revisions: { + abc: partial({ + commit: { + message: + 'Some change\n\nRenovate-Branch: renovate/dependency-1.x\nChange-Id: ...', + }, + }), + }, }); - expect(utils.extractSourceBranch(change)).toBeUndefined(); + expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); + }); + + // for backwards compatibility + it('no commit message but with hashtags', () => { + const change = partial({ + hashtags: ['sourceBranch-renovate/dependency-1.x'], + }); + expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); }); - it('hashtag with "sourceBranch-" prefix', () => { + // for backwards compatibility + it('commit message with no footer but with hashtags', () => { const change = partial({ - hashtags: ['other', 'sourceBranch-renovate/dependency-1.x', 'another'], + hashtags: ['sourceBranch-renovate/dependency-1.x'], + current_revision: 'abc', + revisions: { + abc: partial({ + commit: { + message: 'some message...', + }, + }), + }, }); expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); }); + + // for backwards compatibility + it('prefers the footer over the hashtags', () => { + const change = partial({ + hashtags: ['sourceBranch-renovate/dependency-1.x'], + current_revision: 'abc', + revisions: { + abc: partial({ + commit: { + message: + 'Some change\n\nRenovate-Branch: renovate/dependency-2.x\nChange-Id: ...', + }, + }), + }, + }); + expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-2.x'); + }); }); describe('findPullRequestBody()', () => { diff --git a/lib/modules/platform/gerrit/utils.ts b/lib/modules/platform/gerrit/utils.ts index d42ec4a463b2e1..3eb28e4b8ece47 100644 --- a/lib/modules/platform/gerrit/utils.ts +++ b/lib/modules/platform/gerrit/utils.ts @@ -2,6 +2,7 @@ import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import type { BranchStatus, PrState } from '../../../types'; import * as hostRules from '../../../util/host-rules'; +import { regEx } from '../../../util/regex'; import { joinUrlParts, parseUrl } from '../../../util/url'; import { hashBody } from '../pr-body'; import type { Pr } from '../types'; @@ -90,9 +91,24 @@ export function mapGerritChangeStateToPrState( return 'all'; } export function extractSourceBranch(change: GerritChange): string | undefined { - return change.hashtags - ?.find((tag) => tag.startsWith('sourceBranch-')) - ?.replace('sourceBranch-', ''); + let sourceBranch: string | undefined = undefined; + + if (change.current_revision) { + const re = regEx(/^Renovate-Branch: (.+)$/m); + const message = change.revisions[change.current_revision]?.commit?.message; + if (message) { + sourceBranch = re.exec(message)?.[1]; + } + } + + // for backwards compatibility + if (!sourceBranch) { + sourceBranch = change.hashtags + ?.find((tag) => tag.startsWith('sourceBranch-')) + ?.replace('sourceBranch-', ''); + } + + return sourceBranch ?? undefined; } export function findPullRequestBody(change: GerritChange): string | undefined {