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

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

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

Powered by Google App Engine
This is Rietveld 408576698