Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(99)

Side by Side Diff: infra/services/gnumbd/support/git.py

Issue 355153002: Refactor infra git libs and testing. (Closed) Base URL: https://chromium.googlesource.com/infra/infra@fake_testing_support
Patch Set: Change config ref to have a sandard naming scheme Created 6 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 import collections
5 import fnmatch
6 import logging
7 import os
8 import subprocess
9 import sys
10 import tempfile
11 import urlparse
12
13 from infra.services.gnumbd.support.util import (
14 cached_property, CalledProcessError)
15
16 from infra.services.gnumbd.support.data import CommitData
17
18 LOGGER = logging.getLogger(__name__)
19
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):
38 """Represents a remote git repo.
39
40 Manages the (bare) on-disk mirror of the remote repo.
41 """
42 MAX_CACHE_SIZE = 1024
43
44 def __init__(self, url):
45 self.dry_run = False
46 self.repos_dir = None
47
48 self._url = url
49 self._repo_path = None
50 self._commit_cache = collections.OrderedDict()
51 self._log = LOGGER.getChild('Repo')
52
53 def reify(self):
54 """Ensures the local mirror of this Repo exists."""
55 assert self.repos_dir is not None
56 parsed = urlparse.urlparse(self._url)
57 norm_url = parsed.netloc + parsed.path
58 if norm_url.endswith('.git'):
59 norm_url = norm_url[:-len('.git')]
60 folder = norm_url.replace('-', '--').replace('/', '-').lower()
61
62 rpath = os.path.abspath(os.path.join(self.repos_dir, folder))
63 if not os.path.isdir(rpath):
64 self._log.debug('initializing %r -> %r', self, rpath)
65 name = tempfile.mkdtemp(dir=self.repos_dir)
66 self.run('clone', '--mirror', self._url, os.path.basename(name),
67 stdout=sys.stdout, stderr=sys.stderr, cwd=self.repos_dir)
68 os.rename(os.path.join(self.repos_dir, name),
69 os.path.join(self.repos_dir, folder))
70 else:
71 self._log.debug('%r already initialized', self)
72 self._repo_path = rpath
73
74 # This causes pushes to fail, so unset it.
75 self.run('config', '--unset', 'remote.origin.mirror', ok_ret={0, 5})
76
77 # Representation
78 def __repr__(self):
79 return 'Repo({_url!r})'.format(**self.__dict__)
80
81 # Methods
82 def get_commit(self, hsh):
83 """Creates a new |Commit| object for this |Repo|.
84
85 Uses a very basic LRU cache for commit objects, keeping up to
86 |MAX_CACHE_SIZE| before eviction. This cuts down on the number of redundant
87 git commands by > 50%, and allows expensive cached_property's to remain
88 for the life of the process.
89 """
90 if hsh in self._commit_cache:
91 self._log.debug('Hit %s', hsh)
92 r = self._commit_cache.pop(hsh)
93 else:
94 self._log.debug('Miss %s', hsh)
95 if len(self._commit_cache) >= self.MAX_CACHE_SIZE:
96 self._commit_cache.popitem(last=False)
97 r = Commit(self, hsh)
98
99 self._commit_cache[hsh] = r
100 return r
101
102 def refglob(self, globstring):
103 """Yield every Ref in this repo which matches |globstring|."""
104 for _, ref in (l.split() for l in self.run('show-ref').splitlines()):
105 if fnmatch.fnmatch(ref, globstring):
106 yield Ref(self, ref)
107
108 def run(self, *args, **kwargs):
109 """Yet-another-git-subprocess-wrapper.
110
111 Args: argv tokens. 'git' is always argv[0]
112
113 Kwargs:
114 indata - String data to feed to communicate()
115 ok_ret - A set() of valid return codes. Defaults to {0}.
116 ... - passes through to subprocess.Popen()
117 """
118 if args[0] == 'push' and self.dry_run:
119 self._log.warn('DRY-RUN: Would have pushed %r', args[1:])
120 return
121
122 if not 'cwd' in kwargs:
123 assert self._repo_path is not None
124 kwargs.setdefault('cwd', self._repo_path)
125
126 kwargs.setdefault('stderr', subprocess.PIPE)
127 kwargs.setdefault('stdout', subprocess.PIPE)
128 indata = kwargs.pop('indata', None)
129 if indata:
130 assert 'stdin' not in kwargs
131 kwargs['stdin'] = subprocess.PIPE
132 ok_ret = kwargs.pop('ok_ret', {0})
133 cmd = ('git',) + args
134
135 self._log.debug('Running %r', cmd)
136 process = subprocess.Popen(cmd, **kwargs)
137 output, errout = process.communicate(indata)
138 retcode = process.poll()
139 if retcode not in ok_ret:
140 raise CalledProcessError(retcode, cmd, output, errout)
141
142 if errout:
143 sys.stderr.write(errout)
144 return output
145
146 def intern(self, data, typ='blob'):
147 return self.run(
148 '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)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698