diff --git a/lib/platform/git/storage.ts b/lib/platform/git/storage.ts index 77f581a750df2273e9de12c053ef28db78b79510..82c039dd995fc230ea8119423efd3cf2fdeaf76f 100644 --- a/lib/platform/git/storage.ts +++ b/lib/platform/git/storage.ts @@ -22,370 +22,357 @@ interface ILocalConfig extends IStorageConfig { } class Storage { - constructor() { - let config: ILocalConfig = {} as any; - let git: Git.SimpleGit; - let cwd: string; + private _config: ILocalConfig = {} as any; + private _git: Git.SimpleGit | undefined; + private _cwd: string | undefined; - Object.assign(this, { - initRepo, - cleanRepo, - getRepoStatus, - setBaseBranch, - branchExists, - commitFilesToBranch, - createBranch, - deleteBranch, - getAllRenovateBranches, - getBranchCommit, - getBranchLastCommitTime, - getCommitMessages, - getFile, - getFileList, - isBranchStale, - mergeBranch, - }); + // istanbul ignore next + private async _resetToBranch(branchName: string) { + logger.debug(`resetToBranch(${branchName})`); + await this._git!.raw(['reset', '--hard']); + await this._git!.checkout(branchName); + await this._git!.raw(['reset', '--hard', 'origin/' + branchName]); + await this._git!.raw(['clean', '-fd']); + } - // istanbul ignore next - async function resetToBranch(branchName: string) { - logger.debug(`resetToBranch(${branchName})`); - await git.raw(['reset', '--hard']); - await git.checkout(branchName); - await git.raw(['reset', '--hard', 'origin/' + branchName]); - await git.raw(['clean', '-fd']); + // istanbul ignore next + private async _cleanLocalBranches() { + const existingBranches = (await this._git!.raw(['branch'])) + .split('\n') + .map(branch => branch.trim()) + .filter(branch => branch.length) + .filter(branch => !branch.startsWith('* ')); + logger.debug({ existingBranches }); + for (const branchName of existingBranches) { + await this._deleteLocalBranch(branchName); } + } - // istanbul ignore next - async function cleanLocalBranches() { - const existingBranches = (await git.raw(['branch'])) - .split('\n') - .map(branch => branch.trim()) - .filter(branch => branch.length) - .filter(branch => !branch.startsWith('* ')); - logger.debug({ existingBranches }); - for (const branchName of existingBranches) { - await deleteLocalBranch(branchName); - } - } + async initRepo(args: IStorageConfig) { + this.cleanRepo(); + let config: ILocalConfig = (this._config = { ...args } as any); + let cwd = (this._cwd = config.localDir); + this._config.branchExists = {}; + logger.info('Initialising git repository into ' + cwd); + const gitHead = join(cwd, '.git/HEAD'); + let clone = true; - async function initRepo(args: IStorageConfig) { - cleanRepo(); - config = { ...args } as any; - cwd = config.localDir; - config.branchExists = {}; - logger.info('Initialising git repository into ' + cwd); - const gitHead = join(cwd, '.git/HEAD'); - let clone = true; - async function determineBaseBranch() { - // see https://stackoverflow.com/a/44750379/1438522 - try { - config.baseBranch = - config.baseBranch || - (await git.raw(['symbolic-ref', 'refs/remotes/origin/HEAD'])) - .replace('refs/remotes/origin/', '') - .trim(); - } catch (err) /* istanbul ignore next */ { - if ( - err.message.startsWith( - 'fatal: ref refs/remotes/origin/HEAD is not a symbolic ref' - ) - ) { - throw new Error('empty'); - } - throw err; - } - } - // istanbul ignore if - if (process.env.NODE_ENV !== 'test' && (await fs.exists(gitHead))) { - try { - git = Git(cwd).silent(true); - await git.raw(['remote', 'set-url', 'origin', config.url]); - const fetchStart = process.hrtime(); - await git.fetch([config.url, '--depth=2']); - await determineBaseBranch(); - await resetToBranch(config.baseBranch); - await cleanLocalBranches(); - await git.raw(['remote', 'prune', 'origin']); - const fetchSeconds = - Math.round( - 1 + 10 * convertHrtime(process.hrtime(fetchStart)).seconds - ) / 10; - logger.info({ fetchSeconds }, 'git fetch completed'); - clone = false; - } catch (err) { - logger.error({ err }, 'git fetch error'); + //TODO: move to private class scope + async function determineBaseBranch(git: Git.SimpleGit) { + // see https://stackoverflow.com/a/44750379/1438522 + try { + config.baseBranch = + config.baseBranch || + (await git.raw(['symbolic-ref', 'refs/remotes/origin/HEAD'])) + .replace('refs/remotes/origin/', '') + .trim(); + } catch (err) /* istanbul ignore next */ { + if ( + err.message.startsWith( + 'fatal: ref refs/remotes/origin/HEAD is not a symbolic ref' + ) + ) { + throw new Error('empty'); } + throw err; } - if (clone) { - await fs.emptyDir(cwd); - git = Git(cwd).silent(true); - const cloneStart = process.hrtime(); - try { - await git.clone(config.url, '.', ['--depth=2', '--no-single-branch']); - } catch (err) /* istanbul ignore next */ { - logger.debug({ err }, 'git clone error'); - throw new Error('platform-failure'); - } - const cloneSeconds = + } + + // istanbul ignore if + if ( + process.env.NODE_ENV !== 'test' && + /* istanbul ignore next */ (await fs.exists(gitHead)) + ) { + try { + this._git = Git(cwd).silent(true); + await this._git.raw(['remote', 'set-url', 'origin', config.url]); + const fetchStart = process.hrtime(); + await this._git.fetch([config.url, '--depth=2']); + await determineBaseBranch(this._git); + await this._resetToBranch(config.baseBranch); + await this._cleanLocalBranches(); + await this._git.raw(['remote', 'prune', 'origin']); + const fetchSeconds = Math.round( - 1 + 10 * convertHrtime(process.hrtime(cloneStart)).seconds + 1 + 10 * convertHrtime(process.hrtime(fetchStart)).seconds ) / 10; - logger.info({ cloneSeconds }, 'git clone completed'); + logger.info({ fetchSeconds }, 'git fetch completed'); + clone = false; + } catch (err) { + logger.error({ err }, 'git fetch error'); } + } + if (clone) { + await fs.emptyDir(cwd); + this._git = Git(cwd).silent(true); + const cloneStart = process.hrtime(); try { - const latestCommitDate = (await git.log({ n: 1 })).latest.date; - logger.debug({ latestCommitDate }, 'latest commit'); + await this._git.clone(config.url, '.', [ + '--depth=2', + '--no-single-branch', + ]); } catch (err) /* istanbul ignore next */ { - if (err.message.includes('does not have any commits yet')) { - throw new Error('empty'); - } - logger.warn({ err }, 'Cannot retrieve latest commit date'); + logger.debug({ err }, 'git clone error'); + throw new Error('platform-failure'); } - // istanbul ignore if - if (config.gitPrivateKey) { - logger.debug('Git private key configured, but not being set'); - } else { - logger.debug('No git private key present - commits will be unsigned'); - await git.raw(['config', 'commit.gpgsign', 'false']); + const cloneSeconds = + Math.round(1 + 10 * convertHrtime(process.hrtime(cloneStart)).seconds) / + 10; + logger.info({ cloneSeconds }, 'git clone completed'); + } + try { + const latestCommitDate = (await this._git!.log({ n: 1 })).latest.date; + logger.debug({ latestCommitDate }, 'latest commit'); + } catch (err) /* istanbul ignore next */ { + if (err.message.includes('does not have any commits yet')) { + throw new Error('empty'); } + logger.warn({ err }, 'Cannot retrieve latest commit date'); + } + // istanbul ignore if + if (config.gitPrivateKey) { + logger.debug('Git private key configured, but not being set'); + } else { + logger.debug('No git private key present - commits will be unsigned'); + await this._git!.raw(['config', 'commit.gpgsign', 'false']); + } - if (global.gitAuthor) { - logger.info({ gitAuthor: global.gitAuthor }, 'Setting git author'); - try { - await git.raw(['config', 'user.name', global.gitAuthor.name]); - await git.raw(['config', 'user.email', global.gitAuthor.email]); - } catch (err) /* istanbul ignore next */ { - logger.debug({ err }, 'Error setting git config'); - throw new Error('temporary-error'); - } + if (global.gitAuthor) { + logger.info({ gitAuthor: global.gitAuthor }, 'Setting git author'); + try { + await this._git!.raw(['config', 'user.name', global.gitAuthor.name]); + await this._git!.raw(['config', 'user.email', global.gitAuthor.email]); + } catch (err) /* istanbul ignore next */ { + logger.debug({ err }, 'Error setting git config'); + throw new Error('temporary-error'); } - - await determineBaseBranch(); } - // istanbul ignore next - function getRepoStatus() { - return git.status(); - } + await determineBaseBranch(this._git!); + } - async function createBranch(branchName: string, sha: string) { - logger.debug(`createBranch(${branchName})`); - await git.reset('hard'); - await git.raw(['clean', '-fd']); - await git.checkout(['-B', branchName, sha]); - await git.push('origin', branchName, { '--force': true }); - config.branchExists[branchName] = true; - } + // istanbul ignore next + getRepoStatus() { + return this._git!.status(); + } - // Return the commit SHA for a branch - async function getBranchCommit(branchName: string) { - const res = await git.revparse(['origin/' + branchName]); - return res.trim(); - } + async createBranch(branchName: string, sha: string) { + logger.debug(`createBranch(${branchName})`); + await this._git!.reset('hard'); + await this._git!.raw(['clean', '-fd']); + await this._git!.checkout(['-B', branchName, sha]); + await this._git!.push('origin', branchName, { '--force': true }); + this._config.branchExists[branchName] = true; + } - async function getCommitMessages() { - logger.debug('getCommitMessages'); - const res = await git.log({ - n: 10, - format: { message: '%s' }, - }); - return res.all.map(commit => commit.message); - } + // Return the commit SHA for a branch + async getBranchCommit(branchName: string) { + const res = await this._git!.revparse(['origin/' + branchName]); + return res.trim(); + } - async function setBaseBranch(branchName: string) { - if (branchName) { - logger.debug(`Setting baseBranch to ${branchName}`); - config.baseBranch = branchName; - if (branchName !== 'master') { - config.baseBranchSha = (await git.raw([ - 'rev-parse', - 'origin/' + branchName, - ])).trim(); - } - await git.checkout([branchName, '-f']); - await git.reset('hard'); - } - } + async getCommitMessages() { + logger.debug('getCommitMessages'); + const res = await this._git!.log({ + n: 10, + format: { message: '%s' }, + }); + return res.all.map(commit => commit.message); + } - async function getFileList(branchName?: string) { - const branch = branchName || config.baseBranch; - const exists = await branchExists(branch); - if (!exists) { - return []; - } - const files = await git.raw([ - 'ls-tree', - '-r', - '--name-only', - 'origin/' + branch, - ]); - // istanbul ignore if - if (!files) { - return []; + async setBaseBranch(branchName: string) { + if (branchName) { + logger.debug(`Setting baseBranch to ${branchName}`); + this._config.baseBranch = branchName; + if (branchName !== 'master') { + this._config.baseBranchSha = (await this._git!.raw([ + 'rev-parse', + 'origin/' + branchName, + ])).trim(); } - return files.split('\n').filter(Boolean); + await this._git!.checkout([branchName, '-f']); + await this._git!.reset('hard'); } + } - async function branchExists(branchName: string) { - // First check cache - if (config.branchExists[branchName] !== undefined) { - return config.branchExists[branchName]; - } - try { - await git.raw(['show-branch', 'origin/' + branchName]); - config.branchExists[branchName] = true; - return true; - } catch (ex) { - config.branchExists[branchName] = false; - return false; - } + async getFileList(branchName?: string) { + const branch = branchName || this._config.baseBranch; + const exists = await this.branchExists(branch); + if (!exists) { + return []; } - - async function getAllRenovateBranches(branchPrefix: string) { - const branches = await git.branch(['--remotes', '--verbose']); - return branches.all - .map(localName) - .filter(branchName => branchName.startsWith(branchPrefix)); + const files = await this._git!.raw([ + 'ls-tree', + '-r', + '--name-only', + 'origin/' + branch, + ]); + // istanbul ignore if + if (!files) { + return []; } + return files.split('\n').filter(Boolean); + } - async function isBranchStale(branchName: string) { - const branches = await git.branch([ - '--remotes', - '--verbose', - '--contains', - config.baseBranchSha || `origin/${config.baseBranch}`, - ]); - return !branches.all.map(localName).includes(branchName); + async branchExists(branchName: string) { + // First check cache + if (this._config.branchExists[branchName] !== undefined) { + return this._config.branchExists[branchName]; } - - async function deleteLocalBranch(branchName: string) { - await git.branch(['-D', branchName]); + try { + await this._git!.raw(['show-branch', 'origin/' + branchName]); + this._config.branchExists[branchName] = true; + return true; + } catch (ex) { + this._config.branchExists[branchName] = false; + return false; } + } - async function deleteBranch(branchName: string) { - try { - await git.raw(['push', '--delete', 'origin', branchName]); - logger.debug({ branchName }, 'Deleted remote branch'); - } catch (err) /* istanbul ignore next */ { - logger.info({ branchName, err }, 'Error deleting remote branch'); - if (err.message.includes('.github/main.workflow')) { - logger.warn( - 'A GitHub bug prevents gitFs + GitHub Actions. Please disable gitFs' - ); - } else { - throw new Error('repository-changed'); - } - } - try { - await deleteLocalBranch(branchName); - // istanbul ignore next - logger.debug({ branchName }, 'Deleted local branch'); - } catch (err) { - logger.debug({ branchName }, 'No local branch to delete'); + async getAllRenovateBranches(branchPrefix: string) { + const branches = await this._git!.branch(['--remotes', '--verbose']); + return branches.all + .map(localName) + .filter(branchName => branchName.startsWith(branchPrefix)); + } + + async isBranchStale(branchName: string) { + const branches = await this._git!.branch([ + '--remotes', + '--verbose', + '--contains', + this._config.baseBranchSha || `origin/${this._config.baseBranch}`, + ]); + return !branches.all.map(localName).includes(branchName); + } + + private async _deleteLocalBranch(branchName: string) { + await this._git!.branch(['-D', branchName]); + } + + async deleteBranch(branchName: string) { + try { + await this._git!.raw(['push', '--delete', 'origin', branchName]); + logger.debug({ branchName }, 'Deleted remote branch'); + } catch (err) /* istanbul ignore next */ { + logger.info({ branchName, err }, 'Error deleting remote branch'); + if (err.message.includes('.github/main.workflow')) { + logger.warn( + 'A GitHub bug prevents gitFs + GitHub Actions. Please disable gitFs' + ); + } else { + throw new Error('repository-changed'); } - config.branchExists[branchName] = false; } - - async function mergeBranch(branchName: string) { - await git.reset('hard'); - await git.checkout(['-B', branchName, 'origin/' + branchName]); - await git.checkout(config.baseBranch); - await git.merge([branchName]); - await git.push('origin', config.baseBranch); + try { + await this._deleteLocalBranch(branchName); + // istanbul ignore next + logger.debug({ branchName }, 'Deleted local branch'); + } catch (err) { + logger.debug({ branchName }, 'No local branch to delete'); } + this._config.branchExists[branchName] = false; + } - async function getBranchLastCommitTime(branchName: string) { - try { - const time = await git.show([ - '-s', - '--format=%ai', - 'origin/' + branchName, - ]); - return new Date(Date.parse(time)); - } catch (ex) { - return new Date(); - } + async mergeBranch(branchName: string) { + await this._git!.reset('hard'); + await this._git!.checkout(['-B', branchName, 'origin/' + branchName]); + await this._git!.checkout(this._config.baseBranch); + await this._git!.merge([branchName]); + await this._git!.push('origin', this._config.baseBranch); + } + + async getBranchLastCommitTime(branchName: string) { + try { + const time = await this._git!.show([ + '-s', + '--format=%ai', + 'origin/' + branchName, + ]); + return new Date(Date.parse(time)); + } catch (ex) { + return new Date(); } + } - async function getFile(filePath: string, branchName?: string) { - if (branchName) { - const exists = await branchExists(branchName); - if (!exists) { - logger.info({ branchName }, 'branch no longer exists - aborting'); - throw new Error('repository-changed'); - } - } - try { - const content = await git.show([ - 'origin/' + (branchName || config.baseBranch) + ':' + filePath, - ]); - return content; - } catch (ex) { - return null; + async getFile(filePath: string, branchName?: string) { + if (branchName) { + const exists = await this.branchExists(branchName); + if (!exists) { + logger.info({ branchName }, 'branch no longer exists - aborting'); + throw new Error('repository-changed'); } } + try { + const content = await this._git!.show([ + 'origin/' + (branchName || this._config.baseBranch) + ':' + filePath, + ]); + return content; + } catch (ex) { + return null; + } + } - async function commitFilesToBranch( - branchName: string, - files: any[], - message: string, - parentBranch = config.baseBranch - ) { - logger.debug(`Committing files to branch ${branchName}`); - try { - await git.reset('hard'); - await git.raw(['clean', '-fd']); - await git.checkout(['-B', branchName, 'origin/' + parentBranch]); - const fileNames = []; - const deleted = []; - for (const file of files) { - // istanbul ignore if - if (file.name === '|delete|') { - deleted.push(file.contents); - } else { - fileNames.push(file.name); - await fs.outputFile( - join(cwd, file.name), - Buffer.from(file.contents) - ); - } - } + async commitFilesToBranch( + branchName: string, + files: any[], + message: string, + parentBranch = this._config.baseBranch + ) { + logger.debug(`Committing files to branch ${branchName}`); + try { + await this._git!.reset('hard'); + await this._git!.raw(['clean', '-fd']); + await this._git!.checkout(['-B', branchName, 'origin/' + parentBranch]); + const fileNames = []; + const deleted = []; + for (const file of files) { // istanbul ignore if - if (fileNames.length === 1 && fileNames[0] === 'renovate.json') { - fileNames.unshift('-f'); + if (file.name === '|delete|') { + deleted.push(file.contents); + } else { + fileNames.push(file.name); + await fs.outputFile( + join(this._cwd!, file.name), + Buffer.from(file.contents) + ); } - if (fileNames.length) await git.add(fileNames); - if (deleted.length) { - for (const f of deleted) { - try { - await git.rm([f]); - } catch (err) /* istanbul ignore next */ { - logger.debug({ err }, 'Cannot delete ' + f); - } + } + // istanbul ignore if + if (fileNames.length === 1 && fileNames[0] === 'renovate.json') { + fileNames.unshift('-f'); + } + if (fileNames.length) await this._git!.add(fileNames); + if (deleted.length) { + for (const f of deleted) { + try { + await this._git!.rm([f]); + } catch (err) /* istanbul ignore next */ { + logger.debug({ err }, 'Cannot delete ' + f); } } - await git.commit(message); - await git.push('origin', `${branchName}:${branchName}`, { - '--force': true, - '-u': true, - }); - } catch (err) /* istanbul ignore next */ { - logger.debug({ err }, 'Error commiting files'); - if (err.message.includes('.github/main.workflow')) { - logger.warn( - 'A GitHub bug prevents gitFs + GitHub Actions. Please disable gitFs' - ); - throw new Error('disable-gitfs'); - } else if (err.message.includes('[remote rejected]')) { - throw new Error('repository-changed'); - } - throw err; } + await this._git!.commit(message); + await this._git!.push('origin', `${branchName}:${branchName}`, { + '--force': true, + '-u': true, + }); + } catch (err) /* istanbul ignore next */ { + logger.debug({ err }, 'Error commiting files'); + if (err.message.includes('.github/main.workflow')) { + logger.warn( + 'A GitHub bug prevents gitFs + GitHub Actions. Please disable gitFs' + ); + throw new Error('disable-gitfs'); + } else if (err.message.includes('[remote rejected]')) { + throw new Error('repository-changed'); + } + throw err; } - - function cleanRepo() {} } + cleanRepo() {} + static getUrl({ gitFs, auth, @@ -393,10 +380,10 @@ class Storage { host, repository, }: { - gitFs: 'ssh' | 'http' | 'https'; - auth: string; - hostname: string; - host: string; + gitFs?: 'ssh' | 'http' | 'https'; + auth?: string; + hostname?: string; + host?: string; repository: string; }) { let protocol = gitFs || 'https'; diff --git a/package.json b/package.json index 369d7a8e3b6a435ec4d3bb78460f47cd52bc4e07..74ffa93ad1e81e48b5c26bcf599e349ae91b0366 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ "coverageDirectory": "./coverage", "collectCoverage": true, "collectCoverageFrom": [ - "lib/**/*.js", + "lib/**/*.{js,ts}", "!lib/platform/bitbucket-server/bb-got-wrapper.js", "!lib/versioning/maven/index.js", "!lib/proxy.js" diff --git a/test/platform/git/__snapshots__/storage.spec.js.snap b/test/platform/git/__snapshots__/storage.spec.js.snap index e63f3686f2d7a9e833839e79678b88d9c0755501..5837cc8a81a2262af2e9e4eb316abcc5417a0af0 100644 --- a/test/platform/git/__snapshots__/storage.spec.js.snap +++ b/test/platform/git/__snapshots__/storage.spec.js.snap @@ -18,6 +18,14 @@ Array [ ] `; +exports[`platform/git/storage getFileList() should return the correct files 2`] = ` +Array [ + "file_to_delete", + "master_file", + "past_file", +] +`; + exports[`platform/git/storage mergeBranch(branchName) should throw if branch merge throws 1`] = ` [Error: fatal: 'origin/not_found' is not a commit and a branch 'not_found' cannot be created from it ] diff --git a/test/platform/git/storage.spec.js b/test/platform/git/storage.spec.js index 0cf00ce74ba8349fb38d248d1e16f7c34747b458..ce61907925e2c7c28d7fa199157c17a1ef892f13 100644 --- a/test/platform/git/storage.spec.js +++ b/test/platform/git/storage.spec.js @@ -81,6 +81,7 @@ describe('platform/git/storage', () => { }); it('should return the correct files', async () => { expect(await git.getFileList('renovate/future_branch')).toMatchSnapshot(); + expect(await git.getFileList()).toMatchSnapshot(); }); }); describe('branchExists(branchName)', () => { @@ -236,6 +237,13 @@ describe('platform/git/storage', () => { repository: 'some/repo', }) ).toEqual('https://user:pass@host/some/repo.git'); + expect( + getUrl({ + auth: 'user:pass', + hostname: 'host', + repository: 'some/repo', + }) + ).toEqual('https://user:pass@host/some/repo.git'); }); it('returns ssh url', () => { diff --git a/test/platform/github/storage.spec.js b/test/platform/github/storage.spec.js index 4d52ca4130295d7c08c61eee24dcec88da948a2b..c12b039ca3f1602066c26a01394eff4cdfc42a1d 100644 --- a/test/platform/github/storage.spec.js +++ b/test/platform/github/storage.spec.js @@ -1,9 +1,22 @@ describe('platform/github/storage', () => { const GithubStorage = require('../../../lib/platform/github/storage'); const GitStorage = require('../../../lib/platform/git/storage'); + + function getAllPropertyNames(obj) { + let props = []; + let obj2 = obj; + + while (obj2 != null) { + props = props.concat(Object.getOwnPropertyNames(obj2)); + obj2 = Object.getPrototypeOf(obj2); + } + + return props.filter(p => !p.startsWith('_')); + } + it('has same API for git storage', () => { - const githubMethods = Object.keys(new GithubStorage()).sort(); - const gitMethods = Object.keys(new GitStorage()).sort(); + const githubMethods = getAllPropertyNames(new GithubStorage()).sort(); + const gitMethods = getAllPropertyNames(new GitStorage()).sort(); expect(githubMethods).toMatchObject(gitMethods); }); it('getRepoStatus exists', async () => {