OLD | NEW |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 |
4 import collections | 5 import collections |
5 import fnmatch | 6 import fnmatch |
6 import logging | 7 import logging |
7 import os | 8 import os |
8 import subprocess | 9 import subprocess |
9 import sys | 10 import sys |
10 import tempfile | 11 import tempfile |
11 import urlparse | 12 import urlparse |
12 | 13 |
13 from infra.services.gnumbd.support.util import ( | 14 from infra.libs.git2 import CalledProcessError |
14 cached_property, CalledProcessError) | 15 from infra.libs.git2 import Commit |
15 | 16 from infra.libs.git2 import Ref |
16 from infra.services.gnumbd.support.data import CommitData | |
17 | 17 |
18 LOGGER = logging.getLogger(__name__) | 18 LOGGER = logging.getLogger(__name__) |
19 | 19 |
20 | 20 |
21 class _Invalid(object): | |
22 def __call__(self, *_args, **_kwargs): | |
23 return self | |
24 | |
25 def __getattr__(self, _key): | |
26 return self | |
27 | |
28 def __eq__(self, _other): | |
29 return False | |
30 | |
31 def __ne__(self, _other): # pylint: disable=R0201 | |
32 return True | |
33 | |
34 INVALID = _Invalid() | |
35 | |
36 | |
37 class Repo(object): | 21 class Repo(object): |
38 """Represents a remote git repo. | 22 """Represents a remote git repo. |
39 | 23 |
40 Manages the (bare) on-disk mirror of the remote repo. | 24 Manages the (bare) on-disk mirror of the remote repo. |
41 """ | 25 """ |
42 MAX_CACHE_SIZE = 1024 | 26 MAX_CACHE_SIZE = 1024 |
43 | 27 |
44 def __init__(self, url): | 28 def __init__(self, url): |
45 self.dry_run = False | 29 self.dry_run = False |
46 self.repos_dir = None | 30 self.repos_dir = None |
47 | 31 |
48 self._url = url | 32 self._url = url |
49 self._repo_path = None | 33 self._repo_path = None |
50 self._commit_cache = collections.OrderedDict() | 34 self._commit_cache = collections.OrderedDict() |
51 self._log = LOGGER.getChild('Repo') | 35 self._log = LOGGER.getChild('Repo') |
52 | 36 |
| 37 def __getitem__(self, ref): |
| 38 """Get a Ref attached to this Repo.""" |
| 39 return Ref(self, ref) |
| 40 |
53 def reify(self): | 41 def reify(self): |
54 """Ensures the local mirror of this Repo exists.""" | 42 """Ensures the local mirror of this Repo exists.""" |
55 assert self.repos_dir is not None | 43 assert self.repos_dir is not None |
56 parsed = urlparse.urlparse(self._url) | 44 parsed = urlparse.urlparse(self._url) |
57 norm_url = parsed.netloc + parsed.path | 45 norm_url = parsed.netloc + parsed.path |
58 if norm_url.endswith('.git'): | 46 if norm_url.endswith('.git'): |
59 norm_url = norm_url[:-len('.git')] | 47 norm_url = norm_url[:-len('.git')] |
60 folder = norm_url.replace('-', '--').replace('/', '-').lower() | 48 folder = norm_url.replace('-', '--').replace('/', '-').lower() |
61 | 49 |
62 rpath = os.path.abspath(os.path.join(self.repos_dir, folder)) | 50 rpath = os.path.abspath(os.path.join(self.repos_dir, folder)) |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
139 if retcode not in ok_ret: | 127 if retcode not in ok_ret: |
140 raise CalledProcessError(retcode, cmd, output, errout) | 128 raise CalledProcessError(retcode, cmd, output, errout) |
141 | 129 |
142 if errout: | 130 if errout: |
143 sys.stderr.write(errout) | 131 sys.stderr.write(errout) |
144 return output | 132 return output |
145 | 133 |
146 def intern(self, data, typ='blob'): | 134 def intern(self, data, typ='blob'): |
147 return self.run( | 135 return self.run( |
148 'hash-object', '-w', '-t', typ, '--stdin', indata=str(data)).strip() | 136 'hash-object', '-w', '-t', typ, '--stdin', indata=str(data)).strip() |
149 | |
150 | |
151 class Commit(object): | |
152 """Represents the identity of a commit in a git repo.""" | |
153 | |
154 def __init__(self, repo, hsh): | |
155 """ | |
156 @type repo: Repo | |
157 """ | |
158 assert CommitData.HASH_RE.match(hsh) | |
159 self._repo = repo | |
160 self._hsh = hsh | |
161 | |
162 # Comparison & Representation | |
163 def __eq__(self, other): | |
164 return (self is other) or ( | |
165 isinstance(other, Commit) and ( | |
166 self.hsh == other.hsh | |
167 ) | |
168 ) | |
169 | |
170 def __ne__(self, other): | |
171 return not (self == other) | |
172 | |
173 def __repr__(self): | |
174 return 'Commit({_repo!r}, {_hsh!r})'.format(**self.__dict__) | |
175 | |
176 # Accessors | |
177 # pylint: disable=W0212 | |
178 repo = property(lambda self: self._repo) | |
179 hsh = property(lambda self: self._hsh) | |
180 | |
181 # Properties | |
182 @cached_property | |
183 def data(self): | |
184 """Get a structured data representation of this commit.""" | |
185 try: | |
186 raw_data = self.repo.run('cat-file', 'commit', self.hsh) | |
187 except CalledProcessError: | |
188 return INVALID | |
189 return CommitData.from_raw(raw_data) | |
190 | |
191 @cached_property | |
192 def parent(self): | |
193 """Get the corresponding parent Commit() for this Commit(), or None. | |
194 | |
195 If self has more than one parent, this raises an Exception. | |
196 """ | |
197 parents = self.data.parents | |
198 if len(parents) > 1: | |
199 LOGGER.error('Commit %r has more than one parent!', self.hsh) | |
200 return INVALID | |
201 return self.repo.get_commit(parents[0]) if parents else None | |
202 | |
203 # Methods | |
204 def alter(self, **kwargs): | |
205 """Get a new Commit which is the same as this one, except for alterations | |
206 specified by kwargs. | |
207 | |
208 This will intern the new Commit object into the Repo. | |
209 """ | |
210 return self.repo.get_commit( | |
211 self.repo.intern(self.data.alter(**kwargs), 'commit')) | |
212 | |
213 | |
214 class Ref(object): | |
215 """Represents a single simple ref in a git Repo.""" | |
216 def __init__(self, repo, ref_str): | |
217 """ | |
218 @type repo: Repo | |
219 @type ref_str: str | |
220 """ | |
221 self._repo = repo | |
222 self._ref = ref_str | |
223 | |
224 # Comparison & Representation | |
225 def __eq__(self, other): | |
226 return (self is other) or ( | |
227 isinstance(other, Ref) and ( | |
228 self.ref == other.ref and | |
229 self.repo is other.repo | |
230 ) | |
231 ) | |
232 | |
233 def __ne__(self, other): | |
234 return not (self == other) | |
235 | |
236 def __repr__(self): | |
237 return 'Ref({_repo!r}, {_ref!r})'.format(**self.__dict__) | |
238 | |
239 # Accessors | |
240 # pylint: disable=W0212 | |
241 repo = property(lambda self: self._repo) | |
242 ref = property(lambda self: self._ref) | |
243 | |
244 # Properties | |
245 @property | |
246 def commit(self): | |
247 """Get the Commit at the tip of this Ref.""" | |
248 try: | |
249 val = self._repo.run('show-ref', '--verify', self._ref) | |
250 except CalledProcessError: | |
251 return INVALID | |
252 return self._repo.get_commit(val.split()[0]) | |
253 | |
254 # Methods | |
255 def to(self, other): | |
256 """Generate Commit()'s which occur from `self..other`.""" | |
257 assert self.commit is not INVALID | |
258 arg = '%s..%s' % (self.ref, other.ref) | |
259 for hsh in self.repo.run('rev-list', '--reverse', arg).splitlines(): | |
260 yield self.repo.get_commit(hsh) | |
261 | |
262 def fast_forward_push(self, commit): | |
263 """Push |commit| to this ref on the remote, and update the local copy of the | |
264 ref to |commit|.""" | |
265 self.repo.run('push', 'origin', '%s:%s' % (commit.hsh, self.ref)) | |
266 self.update_to(commit) | |
267 | |
268 def update_to(self, commit): | |
269 """Update the local copy of the ref to |commit|.""" | |
270 self.repo.run('update-ref', self.ref, commit.hsh) | |
OLD | NEW |