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

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

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

Powered by Google App Engine
This is Rietveld 408576698