blob: 307f6429b7cfdf8ae2e6b5e4094112803d58ca7f [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helper to enable some svn-like operations on git repositories.
There are two operations in particular that are easy to do in SVN but hard to do
in Git:
* Get the contents of a file at a particular revision
* Get the relative ordering of a set of commits
The first is difficult because Git (as a distributed system) doesn't provide
'svn cat'-like functionality. In order to get the contents of a file, you must
have all of the repository objects cloned locally.
The second is difficult because Git (with its emphasis on branch workflows)
doesn't provide sequential monotonically increasing revision numbers. In order
to get a (weak) ordering on a set of commits, we must compute generation numbers
from the repository root and then compare those.
This file provides a lightweight way to perform both of these actions. It
provides a class that encapsulates checking out the necessary repository to a
temporary directory, gleaning the reqested information from that repo, and
cleaning up after itself.
from git_helper import GitHelper
repo = GitHelper('https://path.to/my/repo.git')
# get the contents of a readme, ten revisions ago.
file = repo.show('docs/README.md', 'master~10')
# get the generation numbers of any number of commits
commits = ['deadbeef', 'feedbead']
ordering = repo.number(*commits)
sorted_commits = [pair[1] for pair in sorted(zip(ordering, commits))]
# clean up
repo.destroy()
For super lightweight operations, GitHelper can be used as a context manager
that cleans up after itself.
with GitHelper('https://path.to/my/repo.git') as g:
g.show('VERSION')
Commit ordering is implemented via depot_tools/git-number, by iannucci@.
"""
import logging
import os
import shutil
import subprocess
import tempfile
class GitHelper(object):
"""Helper class for some common lightweight Git operations."""
def __init__(self, url):
"""Creates the GitHelper object, pointed at the repo.
Args:
url: The url at which the repository lives. If this is an
http(s):// url, the repository will be cloned into
a local temporary directory. If this is a file:// url,
the GitHelper simply assumes the on-disk repo is already
in place and does nothing.
"""
self.url = url
if self.url.startswith('file://'):
self.tmpdir = False
self.dir = os.path.abspath(self.url.replace('file://', ''))
else:
self.tmpdir = True
self.dir = tempfile.mkdtemp(prefix='git-tmp')
self._retry(3, ['clone', '--mirror', self.url, self.dir])
def __enter__(self):
"""Method called upon entering this object as a context manager."""
return self
def __exit__(self, _exc_type, _exc_value, _traceback):
"""Method called when the context is exited."""
self.destroy()
def destroy(self):
"""Deletes the tmpdir holding the git objects."""
if self.tmpdir:
shutil.rmtree(self.dir)
def _run(self, cmd, **kwargs):
"""Runs a git command and returns its output."""
kwargs.setdefault('cwd', self.dir)
cmd = ['git'] + map(str, cmd)
logging.debug('Running %s', ' '.join(repr(tok) for tok in cmd))
out = subprocess.check_output(
cmd, stderr=subprocess.STDOUT, **kwargs)
return out
def _retry(self, tries, cmd, **kwargs):
"""Retries a call to _run |tries| times.
Purposefully doesn't wrap last call in try/except,
to expose the exception to the calling code.
"""
while tries > 1:
try:
out = self._run(cmd, **kwargs)
return out
except subprocess.CalledProcessError:
logging.debug('Failed to run command, retrying.')
tries -= 1
continue
out = self._run(cmd, **kwargs)
return out
def show(self, path, ref):
cmd = ['show', '%s:%s' % (ref, path)]
return self._run(cmd)
def number(self, *refs):
# Mark this as a headless operation so 'git number' doesn't fail with a
# warning.
env = os.environ.copy()
env['CHROME_HEADLESS'] = '1'
cmd = ['number'] + list(refs)
out = self._run(cmd, env=env)
return map(int, out.splitlines())