OLD | NEW |
(Empty) | |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import atexit |
| 6 import collections |
| 7 import datetime |
| 8 import os |
| 9 import shutil |
| 10 import subprocess |
| 11 import sys |
| 12 import tempfile |
| 13 |
| 14 |
| 15 from testing_support.git.schema import GitRepoSchema |
| 16 |
| 17 |
| 18 class GitRepo(object): |
| 19 """Creates a real git repo for a GitRepoSchema. |
| 20 |
| 21 Obtains schema and content information from the GitRepoSchema. |
| 22 |
| 23 The format for the commit data supplied by GitRepoSchema.data_for is: |
| 24 { |
| 25 SPECIAL_KEY: special_value, |
| 26 ... |
| 27 "path/to/some/file": { 'data': "some data content for this file", |
| 28 'mode': 0755 }, |
| 29 ... |
| 30 } |
| 31 |
| 32 The SPECIAL_KEYs are the following attribues of the GitRepo class: |
| 33 * AUTHOR_NAME |
| 34 * AUTHOR_EMAIL |
| 35 * AUTHOR_DATE - must be a datetime.datetime instance |
| 36 * COMMITTER_NAME |
| 37 * COMMITTER_EMAIL |
| 38 * COMMITTER_DATE - must be a datetime.datetime instance |
| 39 |
| 40 For file content, if 'data' is None, then this commit will `git rm` that file. |
| 41 """ |
| 42 BASE_TEMP_DIR = tempfile.mkdtemp(suffix='base', prefix='git_repo') |
| 43 atexit.register(shutil.rmtree, BASE_TEMP_DIR) |
| 44 |
| 45 # Singleton objects to specify specific data in a commit dictionary. |
| 46 AUTHOR_NAME = object() |
| 47 AUTHOR_EMAIL = object() |
| 48 AUTHOR_DATE = object() |
| 49 COMMITTER_NAME = object() |
| 50 COMMITTER_EMAIL = object() |
| 51 COMMITTER_DATE = object() |
| 52 |
| 53 DEFAULT_AUTHOR_NAME = 'Author McAuthorly' |
| 54 DEFAULT_AUTHOR_EMAIL = 'author@example.com' |
| 55 DEFAULT_COMMITTER_NAME = 'Charles Committish' |
| 56 DEFAULT_COMMITTER_EMAIL = 'commitish@example.com' |
| 57 |
| 58 COMMAND_OUTPUT = collections.namedtuple('COMMAND_OUTPUT', 'retcode stdout') |
| 59 |
| 60 def __init__(self, schema): |
| 61 """Makes new GitRepo. |
| 62 |
| 63 Automatically creates a temp folder under GitRepo.BASE_TEMP_DIR. It's |
| 64 recommended that you clean this repo up by calling nuke() on it, but if not, |
| 65 GitRepo will automatically clean up all allocated repos at the exit of the |
| 66 program (assuming a normal exit like with sys.exit) |
| 67 |
| 68 Args: |
| 69 schema - An instance of GitRepoSchema |
| 70 """ |
| 71 self.repo_path = tempfile.mkdtemp(dir=self.BASE_TEMP_DIR) |
| 72 self.commit_map = {} |
| 73 self._date = datetime.datetime(1970, 1, 1) |
| 74 |
| 75 self.to_schema_refs = ['--branches'] |
| 76 |
| 77 self.git('init') |
| 78 self.git('config', 'user.name', 'testcase') |
| 79 self.git('config', 'user.email', 'testcase@example.com') |
| 80 for commit in schema.walk(): |
| 81 self._add_schema_commit(commit, schema.data_for(commit.name)) |
| 82 self.last_commit = self[commit.name] |
| 83 if schema.master: |
| 84 self.git('update-ref', 'refs/heads/master', self[schema.master]) |
| 85 |
| 86 def __getitem__(self, commit_name): |
| 87 """Gets the hash of a commit by its schema name. |
| 88 |
| 89 >>> r = GitRepo(GitRepoSchema('A B C')) |
| 90 >>> r['B'] |
| 91 '7381febe1da03b09da47f009963ab7998a974935' |
| 92 """ |
| 93 return self.commit_map[commit_name] |
| 94 |
| 95 def _add_schema_commit(self, commit, commit_data): |
| 96 commit_data = commit_data or {} |
| 97 |
| 98 if commit.parents: |
| 99 parents = list(commit.parents) |
| 100 self.git('checkout', '--detach', '-q', self[parents[0]]) |
| 101 if len(parents) > 1: |
| 102 self.git('merge', '--no-commit', '-q', *[self[x] for x in parents[1:]]) |
| 103 else: |
| 104 self.git('checkout', '--orphan', 'root_%s' % commit.name) |
| 105 self.git('rm', '-rf', '.') |
| 106 |
| 107 env = self.get_git_commit_env(commit_data) |
| 108 |
| 109 for fname, file_data in commit_data.iteritems(): |
| 110 deleted = False |
| 111 if 'data' in file_data: |
| 112 data = file_data.get('data') |
| 113 if data is None: |
| 114 deleted = True |
| 115 self.git('rm', fname) |
| 116 else: |
| 117 path = os.path.join(self.repo_path, fname) |
| 118 pardir = os.path.dirname(path) |
| 119 if not os.path.exists(pardir): |
| 120 os.makedirs(pardir) |
| 121 with open(path, 'wb') as f: |
| 122 f.write(data) |
| 123 |
| 124 mode = file_data.get('mode') |
| 125 if mode and not deleted: |
| 126 os.chmod(path, mode) |
| 127 |
| 128 self.git('add', fname) |
| 129 |
| 130 rslt = self.git('commit', '--allow-empty', '-m', commit.name, env=env) |
| 131 assert rslt.retcode == 0, 'Failed to commit %s' % str(commit) |
| 132 self.commit_map[commit.name] = self.git('rev-parse', 'HEAD').stdout.strip() |
| 133 self.git('tag', 'tag_%s' % commit.name, self[commit.name]) |
| 134 if commit.is_branch: |
| 135 self.git('branch', '-f', 'branch_%s' % commit.name, self[commit.name]) |
| 136 |
| 137 def get_git_commit_env(self, commit_data=None): |
| 138 commit_data = commit_data or {} |
| 139 env = {} |
| 140 for prefix in ('AUTHOR', 'COMMITTER'): |
| 141 for suffix in ('NAME', 'EMAIL', 'DATE'): |
| 142 singleton = '%s_%s' % (prefix, suffix) |
| 143 key = getattr(self, singleton) |
| 144 if key in commit_data: |
| 145 val = commit_data[key] |
| 146 else: |
| 147 if suffix == 'DATE': |
| 148 val = self._date |
| 149 self._date += datetime.timedelta(days=1) |
| 150 else: |
| 151 val = getattr(self, 'DEFAULT_%s' % singleton) |
| 152 env['GIT_%s' % singleton] = str(val) |
| 153 return env |
| 154 |
| 155 |
| 156 def git(self, *args, **kwargs): |
| 157 """Runs a git command specified by |args| in this repo.""" |
| 158 assert self.repo_path is not None |
| 159 try: |
| 160 with open(os.devnull, 'wb') as devnull: |
| 161 output = subprocess.check_output( |
| 162 ('git',) + args, cwd=self.repo_path, stderr=devnull, **kwargs) |
| 163 return self.COMMAND_OUTPUT(0, output) |
| 164 except subprocess.CalledProcessError as e: |
| 165 return self.COMMAND_OUTPUT(e.returncode, e.output) |
| 166 |
| 167 def git_commit(self, message): |
| 168 return self.git('commit', '-am', message, env=self.get_git_commit_env()) |
| 169 |
| 170 def nuke(self): |
| 171 """Obliterates the git repo on disk. |
| 172 |
| 173 Causes this GitRepo to be unusable. |
| 174 """ |
| 175 shutil.rmtree(self.repo_path) |
| 176 self.repo_path = None |
| 177 |
| 178 def run(self, fn, *args, **kwargs): |
| 179 """Run a python function with the given args and kwargs with the cwd set to |
| 180 the git repo.""" |
| 181 assert self.repo_path is not None |
| 182 curdir = os.getcwd() |
| 183 try: |
| 184 os.chdir(self.repo_path) |
| 185 return fn(*args, **kwargs) |
| 186 finally: |
| 187 os.chdir(curdir) |
| 188 |
| 189 def capture_stdio(self, fn, *args, **kwargs): |
| 190 """Run a python function with the given args and kwargs with the cwd set to |
| 191 the git repo. |
| 192 |
| 193 Returns the (stdout, stderr) of whatever ran, instead of the what |fn| |
| 194 returned. |
| 195 """ |
| 196 stdout = sys.stdout |
| 197 stderr = sys.stderr |
| 198 try: |
| 199 # "multiple statements on a line" pylint: disable=C0321 |
| 200 with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err: |
| 201 sys.stdout = out |
| 202 sys.stderr = err |
| 203 try: |
| 204 self.run(fn, *args, **kwargs) |
| 205 except SystemExit: |
| 206 pass |
| 207 out.seek(0) |
| 208 err.seek(0) |
| 209 return out.read(), err.read() |
| 210 finally: |
| 211 sys.stdout = stdout |
| 212 sys.stderr = stderr |
| 213 |
| 214 def open(self, path, mode='rb'): |
| 215 return open(os.path.join(self.repo_path, path), mode) |
| 216 |
| 217 def to_schema(self): |
| 218 lines = self.git('rev-list', '--parents', '--reverse', '--topo-order', |
| 219 '--format=%s', *self.to_schema_refs).stdout.splitlines() |
| 220 hash_to_msg = {} |
| 221 ret = GitRepoSchema() |
| 222 current = None |
| 223 parents = [] |
| 224 for line in lines: |
| 225 if line.startswith('commit'): |
| 226 assert current is None |
| 227 tokens = line.split() |
| 228 current, parents = tokens[1], tokens[2:] |
| 229 assert all(p in hash_to_msg for p in parents) |
| 230 else: |
| 231 assert current is not None |
| 232 hash_to_msg[current] = line |
| 233 ret.add_partial(line) |
| 234 for parent in parents: |
| 235 ret.add_partial(line, hash_to_msg[parent]) |
| 236 current = None |
| 237 parents = [] |
| 238 assert current is None |
| 239 return ret |
OLD | NEW |