Skip to content
Snippets Groups Projects
Select Git revision
  • ef2a13f4f28f5b2c163991c0c5d4f53b4f45ea9f
  • main default protected
  • renovate/main-zod-3.x
  • renovate/main-ghcr.io-renovatebot-base-image-10.x
  • renovate/main-ghcr.io-containerbase-devcontainer-13.x
  • next
  • revert-31645-feat/rename-gradle-wrapper-validation-action
  • renovate/main-redis-5.x
  • fix/36615b-branch-reuse-no-cache
  • chore/punycode
  • fix/36615-branch-reuse-bug
  • refactor/pin-new-value
  • feat/36219--git-x509-signing
  • feat/structured-logger
  • hotfix/39.264.1
  • feat/skip-dangling
  • gh-readonly-queue/next/pr-36034-7a061c4ca1024a19e2c295d773d9642625d1c2be
  • hotfix/39.238.3
  • refactor/gitlab-auto-approve
  • feat/template-strings
  • gh-readonly-queue/next/pr-35654-137d934242c784e0c45d4b957362214f0eade1d7
  • 41.28.2
  • 41.28.1
  • 41.28.0
  • 41.27.1
  • 41.27.0
  • 41.26.2
  • 41.26.1
  • 41.26.0
  • 41.25.1
  • 41.25.0
  • 41.24.0
  • 41.23.5
  • 41.23.4
  • 41.23.3
  • 41.23.2
  • 41.23.1
  • 41.23.0
  • 41.22.0
  • 41.21.4
  • 41.21.3
41 results

index.ts

Blame
  • storage.js 10.09 KiB
    const fs = require('fs-extra');
    const { join } = require('path');
    const path = require('path');
    const URL = require('url');
    const Git = require('simple-git/promise');
    const convertHrtime = require('convert-hrtime');
    
    class Storage {
      constructor() {
        let config = {};
        let git = null;
        let cwd = null;
    
        Object.assign(this, {
          initRepo,
          cleanRepo,
          getRepoStatus,
          setBaseBranch,
          branchExists,
          commitFilesToBranch,
          createBranch,
          deleteBranch,
          getAllRenovateBranches,
          getBranchCommit,
          getBranchLastCommitTime,
          getCommitMessages,
          getFile,
          getFileList,
          isBranchStale,
          mergeBranch,
        });
    
        // istanbul ignore next
        async function resetToBranch(branchName) {
          await git.raw(['reset', '--hard']);
          await git.checkout(branchName);
          await git.raw(['reset', '--hard', 'origin/' + 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 function initRepo(args) {
          cleanRepo();
          config = { ...args };
          cwd = config.localDir;
          logger.info('Initialising git repository into ' + cwd);
          const gitHead = path.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', '--no-single-branch']);
              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');
            }
          }
          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 =
              Math.round(
                1 + 10 * convertHrtime(process.hrtime(cloneStart)).seconds
              ) / 10;
            logger.info({ cloneSeconds }, 'git clone completed');
          }
    
          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');
            }
          }
    
          await determineBaseBranch();
        }
    
        // istanbul ignore next
        function getRepoStatus() {
          return git.status();
        }
    
        async function createBranch(branchName, sha) {
          await git.reset('hard');
          await git.checkout(['-B', branchName, sha]);
          await git.push(['origin', branchName, '--force']);
        }
    
        // Return the commit SHA for a branch
        async function getBranchCommit(branchName) {
          const res = await git.revparse(['origin/' + branchName]);
          return res.trim();
        }
    
        async function getCommitMessages() {
          logger.debug('getCommitMessages');
          const res = await git.log({
            n: 10,
            format: { message: '%s' },
          });
          return res.all.map(commit => commit.message);
        }
    
        async function setBaseBranch(branchName) {
          if (branchName) {
            logger.debug(`Setting baseBranch to ${branchName}`);
            config.baseBranch = branchName;
            if (branchName !== 'master') {
              config.baseBranchSha = (await git.raw([
                'rev-parse',
                'origin/' + branchName,
              ])).trim();
            }
          }
        }
    
        async function getFileList(branchName) {
          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 [];
          }
          return files.split('\n').filter(Boolean);
        }
    
        async function branchExists(branchName) {
          try {
            await git.raw(['show-branch', 'origin/' + branchName]);
            return true;
          } catch (ex) {
            return false;
          }
        }
    
        async function getAllRenovateBranches(branchPrefix) {
          const branches = await git.branch(['--remotes', '--verbose']);
          return branches.all
            .map(localName)
            .filter(branchName => branchName.startsWith(branchPrefix));
        }
    
        async function isBranchStale(branchName) {
          const branches = await git.branch([
            '--remotes',
            '--verbose',
            '--contains',
            config.baseBranchSha || `origin/${config.baseBranch}`,
          ]);
          return !branches.all.map(localName).includes(branchName);
        }
    
        async function deleteLocalBranch(branchName) {
          await git.branch(['-D', branchName]);
        }
    
        async function deleteBranch(branchName) {
          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 function mergeBranch(branchName) {
          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);
        }
    
        async function getBranchLastCommitTime(branchName) {
          try {
            const time = await git.show([
              '-s',
              '--format=%ai',
              'origin/' + branchName,
            ]);
            return new Date(Date.parse(time));
          } catch (ex) {
            return new Date();
          }
        }
    
        async function getFile(filePath, branchName) {
          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 function commitFilesToBranch(
          branchName,
          files,
          message,
          parentBranch = config.baseBranch
        ) {
          try {
            await git.reset('hard');
            await git.raw(['clean', '-fd']);
            await git.checkout(['-B', branchName, 'origin/' + parentBranch]);
            for (const file of files) {
              await fs.writeFile(join(cwd, file.name), Buffer.from(file.contents));
            }
            const fileNames = files.map(f => f.name);
            // istanbul ignore if
            if (fileNames.length === 1 && fileNames[0] === 'renovate.json') {
              fileNames.shift('-f');
            }
            await git.add(fileNames);
            await git.commit(message);
            await git.push([
              'origin',
              `${branchName}:${branchName}`,
              '--force',
              '-u',
            ]);
          } 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() {}
      }
    }
    
    function localName(branchName) {
      return branchName.replace(/^origin\//, '');
    }
    
    Storage.getUrl = ({ gitFs, auth, hostname, host, repository }) => {
      let protocol = gitFs || 'https';
      // istanbul ignore if
      if (protocol.toString() === 'true') {
        protocol = 'https';
      }
      if (protocol === 'ssh') {
        return `git@${hostname}:${repository}.git`;
      }
      return URL.format({
        protocol,
        auth,
        hostname,
        host,
        pathname: repository + '.git',
      });
    };
    
    module.exports = Storage;