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

Side by Side Diff: infra/services/gnumbd/gnumbd.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
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
5 """Gnumd (Git NUMber Daemon): Adds metadata to git commits as they land in 5 """Gnumd (Git NUMber Daemon): Adds metadata to git commits as they land in
6 a primary repo. 6 a primary repo.
7 7
8 This is a simple daemon which takes commits pushed to a pending ref, alters 8 This is a simple daemon which takes commits pushed to a pending ref, alters
9 their message with metadata, and then pushes the altered commits to a parallel 9 their message with metadata, and then pushes the altered commits to a parallel
10 ref. 10 ref.
11 """ 11 """
12 12
13 import collections 13 import collections
14 import logging 14 import logging
15 import re 15 import re
16 import sys 16 import sys
17 import time 17 import time
18 18
19 LOGGER = logging.getLogger(__name__) 19 LOGGER = logging.getLogger(__name__)
20 20
21 from infra.services.gnumbd.support import git, data, util 21 from infra.libs import git2
22 from infra.libs import types
23 from infra.libs.git2 import config_ref
22 24
23 25
24 DEFAULT_CONFIG_REF = 'refs/pending-config/main'
25 DEFAULT_REPO_DIR = 'gnumbd_repos'
26 FOOTER_PREFIX = 'Cr-' 26 FOOTER_PREFIX = 'Cr-'
27 COMMIT_POSITION = FOOTER_PREFIX + 'Commit-Position' 27 COMMIT_POSITION = FOOTER_PREFIX + 'Commit-Position'
28 # takes a Ref and a number 28 # takes a Ref and a number
29 FMT_COMMIT_POSITION = '{.ref}@{{#{:d}}}'.format 29 FMT_COMMIT_POSITION = '{.ref}@{{#{:d}}}'.format
30 BRANCHED_FROM = FOOTER_PREFIX + 'Branched-From' 30 BRANCHED_FROM = FOOTER_PREFIX + 'Branched-From'
31 GIT_SVN_ID = 'git-svn-id' 31 GIT_SVN_ID = 'git-svn-id'
32 32
33 33
34 ################################################################################ 34 ################################################################################
35 # ConfigRef
36 ################################################################################
37
38 class GnumbdConfigRef(config_ref.ConfigRef):
39 CONVERT = {
40 'interval': lambda self, val: float(val),
41 'pending_tag_prefix': lambda self, val: str(val),
42 'pending_ref_prefix': lambda self, val: str(val),
43 'enabled_refglobs': lambda self, val: map(str, list(val)),
44 }
45 DEFAULTS = {
46 'interval': 5.0,
47 'pending_tag_prefix': 'refs/pending-tags',
48 'pending_ref_prefix': 'refs/pending',
49 'enabled_refglobs': [],
50 }
51 REF = 'refs/gnumbd-config/main'
52
53
54 ################################################################################
35 # Exceptions 55 # Exceptions
36 ################################################################################ 56 ################################################################################
37 57
38 class MalformedPositionFooter(Exception): 58 class MalformedPositionFooter(Exception):
39 def __init__(self, commit, header, value): 59 def __init__(self, commit, header, value):
40 super(MalformedPositionFooter, self).__init__( 60 super(MalformedPositionFooter, self).__init__(
41 'in {!r}: "{}: {}"'.format(commit, header, value)) 61 'in {!r}: "{}: {}"'.format(commit, header, value))
42 62
43 63
44 class NoPositionData(Exception): 64 class NoPositionData(Exception):
45 def __init__(self, commit): 65 def __init__(self, commit):
46 super(NoPositionData, self).__init__( 66 super(NoPositionData, self).__init__(
47 'No {!r} or git-svn-id found for {!r}'.format(COMMIT_POSITION, commit)) 67 'No {!r} or git-svn-id found for {!r}'.format(COMMIT_POSITION, commit))
48 68
49 69
50 ################################################################################ 70 ################################################################################
51 # Commit Manipulation 71 # Commit Manipulation
52 ################################################################################ 72 ################################################################################
53 73
54 def content_of(commit): 74 def content_of(commit):
55 """Calculates the content of |commit| such that a gnumbd-landed commit and 75 """Calculates the content of |commit| such that a gnumbd-landed commit and
56 the original commit will compare as equals. Returns the content as a 76 the original commit will compare as equals. Returns the content as a
57 data.CommitData object. 77 git2.CommitData object.
58 78
59 This strips out: 79 This strips out:
60 * The parent(s) 80 * The parent(s)
61 * The committer date 81 * The committer date
62 * footers beginning with 'Cr-' 82 * footers beginning with 'Cr-'
63 * the 'git-svn-id' footer. 83 * the 'git-svn-id' footer.
64 84
65 Stores a cached copy of the result data on the |commit| instance itself. 85 Stores a cached copy of the result data on the |commit| instance itself.
66 """ 86 """
67 if commit is None: 87 if commit is None:
68 return git.INVALID 88 return git2.INVALID
69 89
70 if not hasattr(commit, '_cr_content'): 90 if not hasattr(commit, '_cr_content'):
71 d = commit.data 91 d = commit.data
72 footers = util.thaw(d.footers) 92 footers = types.thaw(d.footers)
73 footers[GIT_SVN_ID] = None 93 footers[GIT_SVN_ID] = None
74 for k in footers.keys(): 94 for k in footers.keys():
75 if k.startswith(FOOTER_PREFIX): 95 if k.startswith(FOOTER_PREFIX):
76 footers[k] = None 96 footers[k] = None
77 commit._cr_content = d.alter( 97 commit._cr_content = d.alter(
78 parents=(), 98 parents=(),
79 committer=d.committer.alter(timestamp=data.NULL_TIMESTAMP), 99 committer=d.committer.alter(timestamp=git2.data.NULL_TIMESTAMP),
80 footers=footers) 100 footers=footers)
81 return commit._cr_content # pylint: disable=W0212 101 return commit._cr_content # pylint: disable=W0212
82 102
83 103
84 def get_position(commit, _position_re=re.compile('^(.*)@{#(\d*)}$')): 104 def get_position(commit, _position_re=re.compile('^(.*)@{#(\d*)}$')):
85 """Returns (ref, position number) for the given |commit|. 105 """Returns (ref, position number) for the given |commit|.
86 106
87 Looks for the Cr-Commit-Position footer. If that's unavailable, it falls back 107 Looks for the Cr-Commit-Position footer. If that's unavailable, it falls back
88 to the git-svn-id footer, passing back ref as None. 108 to the git-svn-id footer, passing back ref as None.
89 109
90 May raise the MalformedPositionFooter or NoPositionData exceptions. 110 May raise the MalformedPositionFooter or NoPositionData exceptions.
91 """ 111 """
92 f = commit.data.footers 112 f = commit.data.footers
93 current_pos = f.get(COMMIT_POSITION) 113 current_pos = f.get(COMMIT_POSITION)
94 if current_pos: 114 if current_pos:
95 assert len(current_pos) == 1 115 assert len(current_pos) == 1
96 current_pos = current_pos[0] 116 current_pos = current_pos[0]
97 117
98 m = _position_re.match(current_pos) 118 m = _position_re.match(current_pos)
99 if not m: 119 if not m:
100 raise MalformedPositionFooter(commit, COMMIT_POSITION, current_pos) 120 raise MalformedPositionFooter(commit, COMMIT_POSITION, current_pos)
101 parent_ref = git.Ref(commit.repo, m.group(1)) 121 parent_ref = commit.repo[m.group(1)]
102 parent_num = int(m.group(2)) 122 parent_num = int(m.group(2))
103 else: 123 else:
104 # TODO(iannucci): Remove this and rely on a manual initial commit? 124 # TODO(iannucci): Remove this and rely on a manual initial commit?
105 svn_pos = f.get(GIT_SVN_ID) 125 svn_pos = f.get(GIT_SVN_ID)
106 if not svn_pos: 126 if not svn_pos:
107 raise NoPositionData(commit) 127 raise NoPositionData(commit)
108 128
109 assert len(svn_pos) == 1 129 assert len(svn_pos) == 1
110 svn_pos = svn_pos[0] 130 svn_pos = svn_pos[0]
111 parent_ref = None 131 parent_ref = None
112 try: 132 try:
113 parent_num = int(svn_pos.split()[0].split('@')[1]) 133 parent_num = int(svn_pos.split()[0].split('@')[1])
114 except (IndexError, ValueError): 134 except (IndexError, ValueError):
115 raise MalformedPositionFooter(commit, GIT_SVN_ID, svn_pos) 135 raise MalformedPositionFooter(commit, GIT_SVN_ID, svn_pos)
116 136
117 return parent_ref, parent_num 137 return parent_ref, parent_num
118 138
119 139
120 def synthesize_commit(commit, new_parent, ref, clock=time): 140 def synthesize_commit(commit, new_parent, ref, clock=time):
121 """Synthesizes a new Commit given |new_parent| and ref. 141 """Synthesizes a new Commit given |new_parent| and ref.
122 142
123 The new commit will contain a Cr-Commit-Position footer, and possibly 143 The new commit will contain a Cr-Commit-Position footer, and possibly
124 Cr-Branched-From footers (if commit is on a branch). 144 Cr-Branched-From footers (if commit is on a branch).
125 145
126 The new commit's committer date will also be updated to 'time.time()', or 146 The new commit's committer date will also be updated to 'time.time()', or
127 the new parent's date + 1, whichever is higher. This means that within a branc h, 147 the new parent's date + 1, whichever is higher. This means that within a branc h,
128 commit timestamps will always increase (at least from the point where this 148 commit timestamps will always increase (at least from the point where this
129 daemon went into service). 149 daemon went into service).
130 150
131 @type commit: git.Commit 151 @type commit: git2.Commit
132 @type new_parent: git.Commit 152 @type new_parent: git2.Commit
133 @type ref: git.Ref 153 @type ref: git2.Ref
134 @kind clock: implements .time(), used for testing determinisim. 154 @kind clock: implements .time(), used for testing determinisim.
135 """ 155 """
136 # TODO(iannucci): See if there are any other footers we want to carry over 156 # TODO(iannucci): See if there are any other footers we want to carry over
137 # between new_parent and commit 157 # between new_parent and commit
138 footers = collections.OrderedDict() 158 footers = collections.OrderedDict()
139 parent_ref, parent_num = get_position(new_parent) 159 parent_ref, parent_num = get_position(new_parent)
140 # if parent_ref wasn't encoded, assume that the parent is on the same ref. 160 # if parent_ref wasn't encoded, assume that the parent is on the same ref.
141 if parent_ref is None: 161 if parent_ref is None:
142 parent_ref = ref 162 parent_ref = ref
143 163
(...skipping 11 matching lines...) Expand all
155 # Gerrit-landed commits. 175 # Gerrit-landed commits.
156 for key, value in commit.data.footers.iteritems(): 176 for key, value in commit.data.footers.iteritems():
157 if key.startswith(FOOTER_PREFIX) or key == GIT_SVN_ID: 177 if key.startswith(FOOTER_PREFIX) or key == GIT_SVN_ID:
158 LOGGER.warn('Dropping key on user commit %s: %r -> %r', 178 LOGGER.warn('Dropping key on user commit %s: %r -> %r',
159 commit.hsh, key, value) 179 commit.hsh, key, value)
160 footers[key] = None 180 footers[key] = None
161 181
162 # Ensure that every commit has a time which is at least 1 second after its 182 # Ensure that every commit has a time which is at least 1 second after its
163 # parent, and reset the tz to UTC. 183 # parent, and reset the tz to UTC.
164 parent_time = new_parent.data.committer.timestamp.secs 184 parent_time = new_parent.data.committer.timestamp.secs
165 new_parents = [] if new_parent is git.INVALID else [new_parent.hsh] 185 new_parents = [] if new_parent is git2.INVALID else [new_parent.hsh]
166 new_committer = commit.data.committer.alter( 186 new_committer = commit.data.committer.alter(
167 timestamp=data.NULL_TIMESTAMP.alter( 187 timestamp=git2.data.NULL_TIMESTAMP.alter(
168 secs=max(int(clock.time()), parent_time + 1))) 188 secs=max(int(clock.time()), parent_time + 1)))
169 189
170 return commit.alter( 190 return commit.alter(
171 parents=new_parents, 191 parents=new_parents,
172 committer=new_committer, 192 committer=new_committer,
173 footers=footers, 193 footers=footers,
174 ) 194 )
175 195
176 196
177 ################################################################################ 197 ################################################################################
(...skipping 22 matching lines...) Expand all
200 220
201 v pending_tag 221 v pending_tag
202 A B C D E F <- pending_tip 222 A B C D E F <- pending_tip
203 A' B' C' D' E' F' <- master 223 A' B' C' D' E' F' <- master
204 224
205 In either case, pending_tag would be advanced, and the method would return 225 In either case, pending_tag would be advanced, and the method would return
206 the commits beteween the tag's proper position and the tip. 226 the commits beteween the tag's proper position and the tip.
207 227
208 Other discrepancies are errors and this method will return an empty list. 228 Other discrepancies are errors and this method will return an empty list.
209 229
210 @type pending_tag: git.Ref 230 @type pending_tag: git2.Ref
211 @type pending_tip: git.Ref 231 @type pending_tip: git2.Ref
212 @type real_ref: git.Ref 232 @type real_ref: git2.Ref
213 @returns [git.Commit] 233 @returns [git2.Commit]
214 """ 234 """
215 assert pending_tag.commit != pending_tip.commit 235 assert pending_tag.commit != pending_tip.commit
216 i = 0 236 i = 0
217 new_commits = list(pending_tag.to(pending_tip)) 237 new_commits = list(pending_tag.to(pending_tip))
218 if not new_commits: 238 if not new_commits:
219 LOGGER.error('%r doesn\'t match %r, but there are no new_commits?', 239 LOGGER.error('%r doesn\'t match %r, but there are no new_commits?',
220 pending_tag.ref, pending_tip.ref) 240 pending_tag.ref, pending_tip.ref)
221 return [] 241 return []
222 242
223 for commit in new_commits: 243 for commit in new_commits:
224 parent = commit.parent 244 parent = commit.parent
225 if parent is git.INVALID: 245 if parent is git2.INVALID:
226 LOGGER.error('Cannot process pending merge commit %r', commit) 246 LOGGER.error('Cannot process pending merge commit %r', commit)
227 return [] 247 return []
228 248
229 if content_of(parent) == content_of(real_ref.commit): 249 if content_of(parent) == content_of(real_ref.commit):
230 break 250 break
231 251
232 LOGGER.warn('Skipping already-processed commit on real_ref %r: %r', 252 LOGGER.warn('Skipping already-processed commit on real_ref %r: %r',
233 real_ref, commit.hsh) 253 real_ref, commit.hsh)
234 i += 1 254 i += 1
235 255
(...skipping 28 matching lines...) Expand all
264 v pending_tag 284 v pending_tag
265 A B C D E F <- pending_tip 285 A B C D E F <- pending_tip
266 A' B' C' <- master 286 A' B' C' <- master
267 287
268 This function will produce: 288 This function will produce:
269 289
270 v pending_tag 290 v pending_tag
271 A B C D E F <- pending_tip 291 A B C D E F <- pending_tip
272 A' B' C' D' E' F' <- master 292 A' B' C' D' E' F' <- master
273 293
274 @type real_ref: git.Ref 294 @type real_ref: git2.Ref
275 @type pending_tag: git.Ref 295 @type pending_tag: git2.Ref
276 @type new_commits: [git.Commit] 296 @type new_commits: [git2.Commit]
277 @kind clock: implements .time(), used for testing determinisim. 297 @kind clock: implements .time(), used for testing determinisim.
278 """ 298 """
279 # TODO(iannucci): use push --force-with-lease to reset pending to the real 299 # TODO(iannucci): use push --force-with-lease to reset pending to the real
280 # ref? 300 # ref?
281 # TODO(iannucci): The ACL rejection message for the real ref should point 301 # TODO(iannucci): The ACL rejection message for the real ref should point
282 # users to the pending ref. 302 # users to the pending ref.
283 assert content_of(pending_tag.commit) == content_of(real_ref.commit) 303 assert content_of(pending_tag.commit) == content_of(real_ref.commit)
284 real_parent = real_ref.commit 304 real_parent = real_ref.commit
285 for commit in new_commits: 305 for commit in new_commits:
286 assert content_of(commit.parent) == content_of(real_parent) 306 assert content_of(commit.parent) == content_of(real_parent)
(...skipping 13 matching lines...) Expand all
300 """Execute a single pass over a fetched Repo. 320 """Execute a single pass over a fetched Repo.
301 321
302 Will call |process_ref| for every branch indicated by the enabled_refglobs 322 Will call |process_ref| for every branch indicated by the enabled_refglobs
303 config option. 323 config option.
304 """ 324 """
305 pending_tag_prefix = cref['pending_tag_prefix'] 325 pending_tag_prefix = cref['pending_tag_prefix']
306 pending_ref_prefix = cref['pending_ref_prefix'] 326 pending_ref_prefix = cref['pending_ref_prefix']
307 enabled_refglobs = cref['enabled_refglobs'] 327 enabled_refglobs = cref['enabled_refglobs']
308 328
309 def join(prefix, ref): 329 def join(prefix, ref):
310 return git.Ref(repo, '/'.join((prefix, ref.ref[len('refs/'):]))) 330 return repo['/'.join((prefix, ref.ref[len('refs/'):]))]
311 331
312 for refglob in enabled_refglobs: 332 for refglob in enabled_refglobs:
313 glob = join(pending_ref_prefix, git.Ref(repo, refglob)) 333 glob = join(pending_ref_prefix, repo[refglob])
314 for pending_tip in repo.refglob(glob.ref): 334 for pending_tip in repo.refglob(glob.ref):
315 # TODO(iannucci): each real_ref could have its own thread. 335 # TODO(iannucci): each real_ref could have its own thread.
316 try: 336 try:
317 real_ref = git.Ref(repo, pending_tip.ref.replace( 337 real_ref = git2.Ref(repo, pending_tip.ref.replace(
318 pending_ref_prefix, 'refs')) 338 pending_ref_prefix, 'refs'))
319 339
320 if real_ref.commit is git.INVALID: 340 if real_ref.commit is git2.INVALID:
321 LOGGER.error('Missing real ref %r', real_ref) 341 LOGGER.error('Missing real ref %r', real_ref)
322 continue 342 continue
323 343
324 LOGGER.info('Processing %r', real_ref) 344 LOGGER.info('Processing %r', real_ref)
325 pending_tag = join(pending_tag_prefix, real_ref) 345 pending_tag = join(pending_tag_prefix, real_ref)
326 346
327 if pending_tag.commit is git.INVALID: 347 if pending_tag.commit is git2.INVALID:
328 LOGGER.error('Missing pending tag %r for %r', pending_tag, real_ref) 348 LOGGER.error('Missing pending tag %r for %r', pending_tag, real_ref)
329 continue 349 continue
330 350
331 if pending_tag.commit != pending_tip.commit: 351 if pending_tag.commit != pending_tip.commit:
332 new_commits = get_new_commits(real_ref, pending_tag, pending_tip) 352 new_commits = get_new_commits(real_ref, pending_tag, pending_tip)
333 if new_commits: 353 if new_commits:
334 process_ref(real_ref, pending_tag, new_commits, clock) 354 process_ref(real_ref, pending_tag, new_commits, clock)
335 else: 355 else:
336 if content_of(pending_tag.commit) != content_of(real_ref.commit): 356 if content_of(pending_tag.commit) != content_of(real_ref.commit):
337 LOGGER.error('%r and %r match, but %r\'s content doesn\'t match!', 357 LOGGER.error('%r and %r match, but %r\'s content doesn\'t match!',
338 pending_tag, pending_tip, real_ref) 358 pending_tag, pending_tip, real_ref)
339 else: 359 else:
340 LOGGER.info('%r is up to date', real_ref) 360 LOGGER.info('%r is up to date', real_ref)
341 except (NoPositionData, MalformedPositionFooter) as e: 361 except (NoPositionData, MalformedPositionFooter) as e:
342 LOGGER.error('%s %s', e.__class__.__name__, e) 362 LOGGER.error('%s %s', e.__class__.__name__, e)
343 except Exception: # pragma: no cover 363 except Exception: # pragma: no cover
344 LOGGER.exception('Uncaught exception while processing %r', real_ref) 364 LOGGER.exception('Uncaught exception while processing %r', real_ref)
345 365
346 366
347 def inner_loop(repo, cref, clock=time): 367 def inner_loop(repo, cref, clock=time):
348 LOGGER.debug('fetching %r', repo) 368 LOGGER.debug('fetching %r', repo)
349 repo.run('fetch', stdout=sys.stdout, stderr=sys.stderr) 369 repo.run('fetch', stdout=sys.stdout, stderr=sys.stderr)
350 cref.evaluate() 370 cref.evaluate()
351 process_repo(repo, cref, clock) 371 process_repo(repo, cref, clock)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698