diff --git a/lib/modules/manager/git-submodules/__fixtures__/.gitmodules.8 b/lib/modules/manager/git-submodules/__fixtures__/.gitmodules.8 new file mode 100644 index 00000000000000..7a5d1356a41df1 --- /dev/null +++ b/lib/modules/manager/git-submodules/__fixtures__/.gitmodules.8 @@ -0,0 +1,12 @@ +[submodule "renovate1"] + path = deps/renovate1 + url = https://github.com/renovatebot/renovate.git + branch = v0.0.1 +[submodule "renovate2"] + path = deps/renovate2 + url = https://github.com/renovatebot/renovate.git + branch = 0.0.1 +[submodule "renovate3"] + path = deps/renovate3 + url = https://github.com/renovatebot/renovate.git + branch = not-a-semver diff --git a/lib/modules/manager/git-submodules/extract.spec.ts b/lib/modules/manager/git-submodules/extract.spec.ts index b74cdebad572c3..b268c5f6859c2c 100644 --- a/lib/modules/manager/git-submodules/extract.spec.ts +++ b/lib/modules/manager/git-submodules/extract.spec.ts @@ -118,6 +118,7 @@ describe('modules/manager/git-submodules/extract', () => { it('default to master if no branch can be detected', async () => { const res = await extractPackageFile('', '.gitmodules.2', {}); expect(res?.deps).toHaveLength(1); + expect(res?.deps[0].versioning).toBeUndefined(); expect(res?.deps[0].currentValue).toBe('master'); }); @@ -336,5 +337,34 @@ describe('modules/manager/git-submodules/extract', () => { ], }); }); + + it('given semver version is extracted from branch and versioning is set to semver', async () => { + const res = await extractPackageFile('', '.gitmodules.8', {}); + expect(res).toEqual({ + datasource: 'git-refs', + deps: [ + { + currentDigest: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + currentValue: 'v0.0.1', + depName: 'deps/renovate1', + packageName: 'https://github.com/renovatebot/renovate.git', + versioning: 'semver', + }, + { + currentDigest: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + currentValue: '0.0.1', + depName: 'deps/renovate2', + packageName: 'https://github.com/renovatebot/renovate.git', + versioning: 'semver', + }, + { + currentDigest: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + currentValue: 'not-a-semver', + packageName: 'https://github.com/renovatebot/renovate.git', + depName: 'deps/renovate3', + }, + ], + }); + }); }); }); diff --git a/lib/modules/manager/git-submodules/extract.ts b/lib/modules/manager/git-submodules/extract.ts index 64cc0806cd0679..5bb6daea00460b 100644 --- a/lib/modules/manager/git-submodules/extract.ts +++ b/lib/modules/manager/git-submodules/extract.ts @@ -8,6 +8,7 @@ import { simpleGitConfig } from '../../../util/git/config'; import { getHttpUrl } from '../../../util/git/url'; import { regEx } from '../../../util/regex'; import { GitRefsDatasource } from '../../datasource/git-refs'; +import * as semVerVersioning from '../../versioning/semver'; import type { ExtractConfig, PackageFileContent } from '../types'; import type { GitModule } from './types'; @@ -138,6 +139,9 @@ export default async function extractPackageFile( packageName: httpSubModuleUrl, currentValue, currentDigest, + ...(semVerVersioning.api.isVersion(currentValue) + ? { versioning: semVerVersioning.id } + : {}), }); } catch (err) /* istanbul ignore next */ { logger.warn( diff --git a/lib/modules/manager/git-submodules/readme.md b/lib/modules/manager/git-submodules/readme.md index 3ee37476f4e9e0..4c728fa1da0731 100644 --- a/lib/modules/manager/git-submodules/readme.md +++ b/lib/modules/manager/git-submodules/readme.md @@ -11,6 +11,21 @@ You can customize the per-submodule checks of the git-submodules manager like th } ``` +### Updating to Specific Tag Values + +If you want to update your Git submodules to a specific tag, you can set the desired tag as the `branch` in your `.gitmodules` file. +Renovate will then automatically update this version to the latest Git tag. + +```ini +[submodule "renovate"] + path = deps/renovate + url = https://github.com/renovatebot/renovate.git + branch = v0.0.1 +``` + +**Note:** Using this approach will disrupt the native git submodule update experience when using `git submodule update --remote`. You may encounter an error like `fatal: Unable to find refs/remotes/origin/v0.0.1 revision in submodule path...` because Git can only update submodules when tracking a branch. +To manually update the submodule, navigate to the submodule directory and run the following commands: `git fetch && git checkout `. + ### Private Modules Authentication Before running the `git` commands to update the submodules, Renovate exports `git` [`insteadOf`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtinsteadOf) directives in environment variables. diff --git a/lib/modules/manager/git-submodules/update.spec.ts b/lib/modules/manager/git-submodules/update.spec.ts index 0271ed102a517d..dc9b026573f5f6 100644 --- a/lib/modules/manager/git-submodules/update.spec.ts +++ b/lib/modules/manager/git-submodules/update.spec.ts @@ -2,11 +2,13 @@ import { mock } from 'jest-mock-extended'; import { SimpleGit, simpleGit } from 'simple-git'; import { DirectoryResult, dir } from 'tmp-promise'; import { join } from 'upath'; +import { fs } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import * as hostRules from '../../../util/host-rules'; import type { Upgrade } from '../types'; import { updateDependency } from '.'; +jest.mock('../../../util/fs'); jest.mock('simple-git'); const simpleGitFactoryMock = simpleGit as jest.Mock>; @@ -88,6 +90,51 @@ describe('modules/manager/git-submodules/update', () => { }); }); + it('update gitmodule branch value if value changed', async () => { + gitMock.submoduleUpdate.mockResolvedValue(''); + gitMock.checkout.mockResolvedValue(''); + const updatedGitModules = `[submodule "renovate"] + path = deps/renovate + url = https://github.com/renovatebot/renovate.git + branch = v0.0.2`; + fs.readLocalFile.mockResolvedValueOnce(updatedGitModules); + + upgrade = { + depName: 'renovate', + currentValue: 'v0.0.1', + newValue: 'v0.0.2', + packageFile: '.gitmodules', + }; + const update = await updateDependency({ + fileContent: '', + upgrade, + }); + expect(update).toBe(updatedGitModules); + expect(gitMock.subModule).toHaveBeenCalledWith([ + 'set-branch', + '--branch', + 'v0.0.2', + 'renovate', + ]); + }); + + it('do not update gitmodule branch value if value not changed', async () => { + gitMock.submoduleUpdate.mockResolvedValue(''); + gitMock.checkout.mockResolvedValue(''); + upgrade = { + depName: 'renovate', + currentValue: 'main', + newValue: 'main', + packageFile: '.gitmodules', + }; + const update = await updateDependency({ + fileContent: '', + upgrade, + }); + expect(update).toBe(''); + expect(gitMock.subModule).toHaveBeenCalledTimes(0); + }); + it('returns content on update and uses git environment variables for git-tags/git-refs', async () => { gitMock.submoduleUpdate.mockResolvedValue(''); gitMock.checkout.mockResolvedValue(''); diff --git a/lib/modules/manager/git-submodules/update.ts b/lib/modules/manager/git-submodules/update.ts index 5e79e5e010b824..1b8c3c1566032d 100644 --- a/lib/modules/manager/git-submodules/update.ts +++ b/lib/modules/manager/git-submodules/update.ts @@ -2,6 +2,7 @@ import Git from 'simple-git'; import upath from 'upath'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; +import { readLocalFile } from '../../../util/fs'; import { getGitEnvironmentVariables } from '../../../util/git/auth'; import type { UpdateDependencyConfig } from '../types'; @@ -26,6 +27,19 @@ export default async function updateDependency({ try { await git.submoduleUpdate(['--init', upgrade.depName!]); await submoduleGit.checkout([upgrade.newDigest!]); + if (upgrade.newValue && upgrade.currentValue !== upgrade.newValue) { + await git.subModule([ + 'set-branch', + '--branch', + upgrade.newValue, + upgrade.depName!, + ]); + const updatedPackageContent = await readLocalFile( + upgrade.packageFile!, + 'utf8', + ); + return updatedPackageContent!; + } return fileContent; } catch (err) { logger.debug({ err }, 'submodule checkout error');