Index: testing_support/git/repo.py |
diff --git a/testing_support/git/repo.py b/testing_support/git/repo.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..37b52a9a05d87e08c571291c3264624551049798 |
--- /dev/null |
+++ b/testing_support/git/repo.py |
@@ -0,0 +1,239 @@ |
+# 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. |
+ |
+import atexit |
+import collections |
+import datetime |
+import os |
+import shutil |
+import subprocess |
+import sys |
+import tempfile |
+ |
+ |
+from testing_support.git.schema import GitRepoSchema |
+ |
+ |
+class GitRepo(object): |
+ """Creates a real git repo for a GitRepoSchema. |
+ |
+ Obtains schema and content information from the GitRepoSchema. |
+ |
+ The format for the commit data supplied by GitRepoSchema.data_for is: |
+ { |
+ SPECIAL_KEY: special_value, |
+ ... |
+ "path/to/some/file": { 'data': "some data content for this file", |
+ 'mode': 0755 }, |
+ ... |
+ } |
+ |
+ The SPECIAL_KEYs are the following attribues of the GitRepo class: |
+ * AUTHOR_NAME |
+ * AUTHOR_EMAIL |
+ * AUTHOR_DATE - must be a datetime.datetime instance |
+ * COMMITTER_NAME |
+ * COMMITTER_EMAIL |
+ * COMMITTER_DATE - must be a datetime.datetime instance |
+ |
+ For file content, if 'data' is None, then this commit will `git rm` that file. |
+ """ |
+ BASE_TEMP_DIR = tempfile.mkdtemp(suffix='base', prefix='git_repo') |
+ atexit.register(shutil.rmtree, BASE_TEMP_DIR) |
+ |
+ # Singleton objects to specify specific data in a commit dictionary. |
+ AUTHOR_NAME = object() |
+ AUTHOR_EMAIL = object() |
+ AUTHOR_DATE = object() |
+ COMMITTER_NAME = object() |
+ COMMITTER_EMAIL = object() |
+ COMMITTER_DATE = object() |
+ |
+ DEFAULT_AUTHOR_NAME = 'Author McAuthorly' |
+ DEFAULT_AUTHOR_EMAIL = 'author@example.com' |
+ DEFAULT_COMMITTER_NAME = 'Charles Committish' |
+ DEFAULT_COMMITTER_EMAIL = 'commitish@example.com' |
+ |
+ COMMAND_OUTPUT = collections.namedtuple('COMMAND_OUTPUT', 'retcode stdout') |
+ |
+ def __init__(self, schema): |
+ """Makes new GitRepo. |
+ |
+ Automatically creates a temp folder under GitRepo.BASE_TEMP_DIR. It's |
+ recommended that you clean this repo up by calling nuke() on it, but if not, |
+ GitRepo will automatically clean up all allocated repos at the exit of the |
+ program (assuming a normal exit like with sys.exit) |
+ |
+ Args: |
+ schema - An instance of GitRepoSchema |
+ """ |
+ self.repo_path = tempfile.mkdtemp(dir=self.BASE_TEMP_DIR) |
+ self.commit_map = {} |
+ self._date = datetime.datetime(1970, 1, 1) |
+ |
+ self.to_schema_refs = ['--branches'] |
+ |
+ self.git('init') |
+ self.git('config', 'user.name', 'testcase') |
+ self.git('config', 'user.email', 'testcase@example.com') |
+ for commit in schema.walk(): |
+ self._add_schema_commit(commit, schema.data_for(commit.name)) |
+ self.last_commit = self[commit.name] |
+ if schema.master: |
+ self.git('update-ref', 'refs/heads/master', self[schema.master]) |
+ |
+ def __getitem__(self, commit_name): |
+ """Gets the hash of a commit by its schema name. |
+ |
+ >>> r = GitRepo(GitRepoSchema('A B C')) |
+ >>> r['B'] |
+ '7381febe1da03b09da47f009963ab7998a974935' |
+ """ |
+ return self.commit_map[commit_name] |
+ |
+ def _add_schema_commit(self, commit, commit_data): |
+ commit_data = commit_data or {} |
+ |
+ if commit.parents: |
+ parents = list(commit.parents) |
+ self.git('checkout', '--detach', '-q', self[parents[0]]) |
+ if len(parents) > 1: |
+ self.git('merge', '--no-commit', '-q', *[self[x] for x in parents[1:]]) |
+ else: |
+ self.git('checkout', '--orphan', 'root_%s' % commit.name) |
+ self.git('rm', '-rf', '.') |
+ |
+ env = self.get_git_commit_env(commit_data) |
+ |
+ for fname, file_data in commit_data.iteritems(): |
+ deleted = False |
+ if 'data' in file_data: |
+ data = file_data.get('data') |
+ if data is None: |
+ deleted = True |
+ self.git('rm', fname) |
+ else: |
+ path = os.path.join(self.repo_path, fname) |
+ pardir = os.path.dirname(path) |
+ if not os.path.exists(pardir): |
+ os.makedirs(pardir) |
+ with open(path, 'wb') as f: |
+ f.write(data) |
+ |
+ mode = file_data.get('mode') |
+ if mode and not deleted: |
+ os.chmod(path, mode) |
+ |
+ self.git('add', fname) |
+ |
+ rslt = self.git('commit', '--allow-empty', '-m', commit.name, env=env) |
+ assert rslt.retcode == 0, 'Failed to commit %s' % str(commit) |
+ self.commit_map[commit.name] = self.git('rev-parse', 'HEAD').stdout.strip() |
+ self.git('tag', 'tag_%s' % commit.name, self[commit.name]) |
+ if commit.is_branch: |
+ self.git('branch', '-f', 'branch_%s' % commit.name, self[commit.name]) |
+ |
+ def get_git_commit_env(self, commit_data=None): |
+ commit_data = commit_data or {} |
+ env = {} |
+ for prefix in ('AUTHOR', 'COMMITTER'): |
+ for suffix in ('NAME', 'EMAIL', 'DATE'): |
+ singleton = '%s_%s' % (prefix, suffix) |
+ key = getattr(self, singleton) |
+ if key in commit_data: |
+ val = commit_data[key] |
+ else: |
+ if suffix == 'DATE': |
+ val = self._date |
+ self._date += datetime.timedelta(days=1) |
+ else: |
+ val = getattr(self, 'DEFAULT_%s' % singleton) |
+ env['GIT_%s' % singleton] = str(val) |
+ return env |
+ |
+ |
+ def git(self, *args, **kwargs): |
+ """Runs a git command specified by |args| in this repo.""" |
+ assert self.repo_path is not None |
+ try: |
+ with open(os.devnull, 'wb') as devnull: |
+ output = subprocess.check_output( |
+ ('git',) + args, cwd=self.repo_path, stderr=devnull, **kwargs) |
+ return self.COMMAND_OUTPUT(0, output) |
+ except subprocess.CalledProcessError as e: |
+ return self.COMMAND_OUTPUT(e.returncode, e.output) |
+ |
+ def git_commit(self, message): |
+ return self.git('commit', '-am', message, env=self.get_git_commit_env()) |
+ |
+ def nuke(self): |
+ """Obliterates the git repo on disk. |
+ |
+ Causes this GitRepo to be unusable. |
+ """ |
+ shutil.rmtree(self.repo_path) |
+ self.repo_path = None |
+ |
+ def run(self, fn, *args, **kwargs): |
+ """Run a python function with the given args and kwargs with the cwd set to |
+ the git repo.""" |
+ assert self.repo_path is not None |
+ curdir = os.getcwd() |
+ try: |
+ os.chdir(self.repo_path) |
+ return fn(*args, **kwargs) |
+ finally: |
+ os.chdir(curdir) |
+ |
+ def capture_stdio(self, fn, *args, **kwargs): |
+ """Run a python function with the given args and kwargs with the cwd set to |
+ the git repo. |
+ |
+ Returns the (stdout, stderr) of whatever ran, instead of the what |fn| |
+ returned. |
+ """ |
+ stdout = sys.stdout |
+ stderr = sys.stderr |
+ try: |
+ # "multiple statements on a line" pylint: disable=C0321 |
+ with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err: |
+ sys.stdout = out |
+ sys.stderr = err |
+ try: |
+ self.run(fn, *args, **kwargs) |
+ except SystemExit: |
+ pass |
+ out.seek(0) |
+ err.seek(0) |
+ return out.read(), err.read() |
+ finally: |
+ sys.stdout = stdout |
+ sys.stderr = stderr |
+ |
+ def open(self, path, mode='rb'): |
+ return open(os.path.join(self.repo_path, path), mode) |
+ |
+ def to_schema(self): |
+ lines = self.git('rev-list', '--parents', '--reverse', '--topo-order', |
+ '--format=%s', *self.to_schema_refs).stdout.splitlines() |
+ hash_to_msg = {} |
+ ret = GitRepoSchema() |
+ current = None |
+ parents = [] |
+ for line in lines: |
+ if line.startswith('commit'): |
+ assert current is None |
+ tokens = line.split() |
+ current, parents = tokens[1], tokens[2:] |
+ assert all(p in hash_to_msg for p in parents) |
+ else: |
+ assert current is not None |
+ hash_to_msg[current] = line |
+ ret.add_partial(line) |
+ for parent in parents: |
+ ret.add_partial(line, hash_to_msg[parent]) |
+ current = None |
+ parents = [] |
+ assert current is None |
+ return ret |