OLD | NEW |
1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
4 | 4 |
5 import calendar | 5 import calendar |
6 import httplib | 6 import httplib |
7 import json | 7 import json |
8 import logging | 8 import logging |
9 import os | 9 import os |
10 import re | 10 import re |
(...skipping 13 matching lines...) Expand all Loading... |
24 from . import env | 24 from . import env |
25 from . import requests_ssl | 25 from . import requests_ssl |
26 from .requests_ssl import requests | 26 from .requests_ssl import requests |
27 | 27 |
28 import subprocess42 | 28 import subprocess42 |
29 from google.protobuf import json_format | 29 from google.protobuf import json_format |
30 | 30 |
31 LOGGER = logging.getLogger(__name__) | 31 LOGGER = logging.getLogger(__name__) |
32 | 32 |
33 | 33 |
| 34 def has_interesting_changes(spec, changed_files): |
| 35 # TODO(iannucci): analyze bundle_extra_paths.txt too. |
| 36 return ( |
| 37 'infra/config/recipes.cfg' in changed_files or |
| 38 any(f.startswith(spec.recipes_path) for f in changed_files) |
| 39 ) |
| 40 |
| 41 |
34 class FetchError(Exception): | 42 class FetchError(Exception): |
35 pass | 43 pass |
36 | 44 |
37 | 45 |
38 class FetchNotAllowedError(FetchError): | 46 class FetchNotAllowedError(FetchError): |
39 pass | 47 pass |
40 | 48 |
41 | 49 |
42 class UnresolvedRefspec(Exception): | 50 class UnresolvedRefspec(Exception): |
43 pass | 51 pass |
44 | 52 |
45 | 53 |
46 # revision (str): the revision of this commit (i.e. hash) | 54 # revision (str): the revision of this commit (i.e. hash) |
47 # author_email (str|None): the email of the author of this commit | 55 # author_email (str|None): the email of the author of this commit |
48 # commit_timestamp (int): the unix commit timestamp for this commit | 56 # commit_timestamp (int): the unix commit timestamp for this commit |
49 # message_lines (tuple(str)): the message of this commit | 57 # message_lines (tuple(str)): the message of this commit |
50 # spec (package_pb2.Package): the parsed infra/config/recipes.cfg file or None. | 58 # spec (package_pb2.Package): the parsed infra/config/recipes.cfg file or None. |
| 59 # roll_candidate (bool): if this commit contains changes which are known to |
| 60 # affect the behavior of the recipes (i.e. modifications within recipe_path |
| 61 # and/or modifications to recipes.cfg) |
51 CommitMetadata = namedtuple( | 62 CommitMetadata = namedtuple( |
52 '_CommitMetadata', | 63 '_CommitMetadata', |
53 'revision author_email commit_timestamp message_lines spec') | 64 'revision author_email commit_timestamp message_lines spec roll_candidate') |
54 | 65 |
55 | 66 |
56 class Backend(object): | 67 class Backend(object): |
57 @staticmethod | 68 @staticmethod |
58 def class_for_type(repo_type): | 69 def class_for_type(repo_type): |
59 """ | 70 """ |
60 Args: | 71 Args: |
61 repo_type (package_pb2.DepSpec.RepoType) | 72 repo_type (package_pb2.DepSpec.RepoType) |
62 | 73 |
63 Returns Backend (class): Returns the Backend appropriate for the | 74 Returns Backend (class): Returns the Backend appropriate for the |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
107 _GIT_METADATA_CACHE = {} | 118 _GIT_METADATA_CACHE = {} |
108 | 119 |
109 # This matches git commit hashes. | 120 # This matches git commit hashes. |
110 _COMMIT_RE = re.compile(r'^[a-fA-F0-9]{40}$') | 121 _COMMIT_RE = re.compile(r'^[a-fA-F0-9]{40}$') |
111 | 122 |
112 def commit_metadata(self, refspec): | 123 def commit_metadata(self, refspec): |
113 """Cached version of _commit_metadata_impl. | 124 """Cached version of _commit_metadata_impl. |
114 | 125 |
115 The refspec will be resolved if it's not absolute. | 126 The refspec will be resolved if it's not absolute. |
116 | 127 |
117 Returns { | 128 Returns (CommitMetadata). |
118 'author': '<author name>', | |
119 'message': '<commit message>', | |
120 'spec': package_pb2.Package or None, # the parsed recipes.cfg file. | |
121 } | |
122 """ | 129 """ |
123 revision = self.resolve_refspec(refspec) | 130 revision = self.resolve_refspec(refspec) |
124 cache = self._GIT_METADATA_CACHE.setdefault(self.repo_url, {}) | 131 cache = self._GIT_METADATA_CACHE.setdefault(self.repo_url, {}) |
125 if revision not in cache: | 132 if revision not in cache: |
126 cache[revision] = self._commit_metadata_impl(revision) | 133 cache[revision] = self._commit_metadata_impl(revision) |
127 return cache[revision] | 134 return cache[revision] |
128 | 135 |
129 @classmethod | 136 @classmethod |
130 def is_resolved_revision(cls, revision): | 137 def is_resolved_revision(cls, revision): |
131 return cls._COMMIT_RE.match(revision) | 138 return cls._COMMIT_RE.match(revision) |
132 | 139 |
133 @classmethod | 140 @classmethod |
134 def assert_resolved(cls, revision): | 141 def assert_resolved(cls, revision): |
135 if not cls.is_resolved_revision(revision): | 142 if not cls.is_resolved_revision(revision): |
136 raise UnresolvedRefspec('unresolved refspec %r' % revision) | 143 raise UnresolvedRefspec('unresolved refspec %r' % revision) |
137 | 144 |
138 def resolve_refspec(self, refspec): | 145 def resolve_refspec(self, refspec): |
139 if self.is_resolved_revision(refspec): | 146 if self.is_resolved_revision(refspec): |
140 return refspec | 147 return refspec |
141 return self._resolve_refspec_impl(refspec) | 148 return self._resolve_refspec_impl(refspec) |
142 | 149 |
143 def updates(self, revision, other_revision, paths): | 150 def updates(self, revision, other_revision): |
144 """Returns a list of revisions |revision| through |other_revision| | 151 """Returns a list of revisions |revision| through |other_revision| |
145 (inclusive). | 152 (inclusive). |
146 | 153 |
147 If |paths| is a non-empty list, the history is scoped just to these paths. | |
148 | |
149 Returns list(CommitMetadata) - The commit metadata in the range | 154 Returns list(CommitMetadata) - The commit metadata in the range |
150 (revision,other_revision]. | 155 (revision,other_revision]. |
151 """ | 156 """ |
152 self.assert_resolved(revision) | 157 self.assert_resolved(revision) |
153 self.assert_resolved(other_revision) | 158 self.assert_resolved(other_revision) |
154 return self._updates_impl(revision, other_revision, paths) | 159 return self._updates_impl(revision, other_revision) |
155 | 160 |
156 ### direct overrides. These are public methods which must be overridden. | 161 ### direct overrides. These are public methods which must be overridden. |
157 | 162 |
158 @property | 163 @property |
159 def repo_type(self): | 164 def repo_type(self): |
160 """Returns package_pb2.DepSpec.RepoType.""" | 165 """Returns package_pb2.DepSpec.RepoType.""" |
161 raise NotImplementedError() | 166 raise NotImplementedError() |
162 | 167 |
163 def fetch(self, refspec): | 168 def fetch(self, refspec): |
164 """Does a fetch for the provided refspec (e.g. get all data from remote), if | 169 """Does a fetch for the provided refspec (e.g. get all data from remote), if |
(...skipping 13 matching lines...) Expand all Loading... |
178 remote git repo, e.g. 'refs/heads/master', 'deadbeef...face', etc. | 183 remote git repo, e.g. 'refs/heads/master', 'deadbeef...face', etc. |
179 """ | 184 """ |
180 # TODO(iannucci): Alter the contract for this method so that it only checks | 185 # TODO(iannucci): Alter the contract for this method so that it only checks |
181 # out the files referred to according to the rules that the bundle | 186 # out the files referred to according to the rules that the bundle |
182 # subcommand uses. | 187 # subcommand uses. |
183 raise NotImplementedError() | 188 raise NotImplementedError() |
184 | 189 |
185 ### private overrides. Override these in the implementations, but don't call | 190 ### private overrides. Override these in the implementations, but don't call |
186 ### externally. | 191 ### externally. |
187 | 192 |
188 def _updates_impl(self, revision, other_revision, paths): | 193 def _updates_impl(self, revision, other_revision): |
189 """Returns a list of revisions |revision| through |other_revision|. This | 194 """Returns a list of revisions |revision| through |other_revision|. This |
190 includes |revision| and |other_revision|. | 195 includes |revision| and |other_revision|. |
191 | 196 |
192 If |paths| is a non-empty list, the history is scoped just to these paths. | |
193 | |
194 Args: | 197 Args: |
195 revision (str) - the first git commit | 198 revision (str) - the first git commit |
196 other_revision (str) - the second git commit | 199 other_revision (str) - the second git commit |
197 | 200 |
198 Returns list(CommitMetadata) - The commit metadata in the range | 201 Returns list(CommitMetadata) - The commit metadata in the range |
199 [revision,other_revision]. | 202 [revision,other_revision]. |
200 """ | 203 """ |
201 raise NotImplementedError() | 204 raise NotImplementedError() |
202 | 205 |
203 def _resolve_refspec_impl(self, refspec): | 206 def _resolve_refspec_impl(self, refspec): |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
238 def _git(self, *args): | 241 def _git(self, *args): |
239 """Runs a git command. | 242 """Runs a git command. |
240 | 243 |
241 Will automatically set low speed limit/time, and cd into the checkout_dir. | 244 Will automatically set low speed limit/time, and cd into the checkout_dir. |
242 | 245 |
243 Args: | 246 Args: |
244 *args (str) - The list of command arguments to pass to git. | 247 *args (str) - The list of command arguments to pass to git. |
245 | 248 |
246 Raises GitError on failure. | 249 Raises GitError on failure. |
247 """ | 250 """ |
| 251 if self._GIT_BINARY.endswith('.bat'): |
| 252 # On the esteemed Windows Operating System, '^' is an escape character. |
| 253 # Since .bat files are running cmd.exe under the hood, they interpret this |
| 254 # escape character. We need to ultimately get a single ^, so we need two |
| 255 # ^'s for when we invoke the .bat, and each of those needs to be escaped |
| 256 # when the bat ultimately invokes the git.exe binary. This leaves us with |
| 257 # a total of 4x the ^'s that we originally wanted. Hooray. |
| 258 args = [a.replace('^', '^^^^') for a in args] |
| 259 |
248 cmd = [ | 260 cmd = [ |
249 self._GIT_BINARY, | 261 self._GIT_BINARY, |
250 '-C', self.checkout_dir, | 262 '-C', self.checkout_dir, |
251 ] + list(args) | 263 ] + list(args) |
252 | 264 |
253 try: | 265 try: |
254 return self._execute(*cmd) | 266 return self._execute(*cmd) |
255 except subprocess42.CalledProcessError as e: | 267 except subprocess42.CalledProcessError as e: |
256 subcommand = (args[0]) if args else ('') | 268 subcommand = (args[0]) if args else ('') |
257 raise GitError('Git "%s" failed: %s' % (subcommand, e.message,)) | 269 raise GitError('Git "%s" failed: %s' % (subcommand, e.message,)) |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
320 self.fetch(refspec) | 332 self.fetch(refspec) |
321 | 333 |
322 # reset touches index.lock which is problematic when multiple processes are | 334 # reset touches index.lock which is problematic when multiple processes are |
323 # accessing the recipes at the same time. To allieviate this, we do a quick | 335 # accessing the recipes at the same time. To allieviate this, we do a quick |
324 # diff, which will exit if `revision` is not already checked out. | 336 # diff, which will exit if `revision` is not already checked out. |
325 try: | 337 try: |
326 self._git('diff', '--quiet', revision) | 338 self._git('diff', '--quiet', revision) |
327 except GitError: | 339 except GitError: |
328 self._git('reset', '-q', '--hard', revision) | 340 self._git('reset', '-q', '--hard', revision) |
329 | 341 |
330 def _updates_impl(self, revision, other_revision, paths): | 342 def _updates_impl(self, revision, other_revision): |
331 args = [ | 343 args = [ |
332 'rev-list', | 344 'rev-list', |
333 '--reverse', | 345 '--reverse', |
334 '--topo-order', | 346 '--topo-order', |
335 '%s..%s' % (revision, other_revision), | 347 '%s..%s' % (revision, other_revision), |
336 ] | 348 ] |
337 if paths: | |
338 args.extend(['--'] + paths) | |
339 return [ | 349 return [ |
340 self.commit_metadata(rev) | 350 self.commit_metadata(rev) |
341 for rev in self._git(*args).strip().split('\n') | 351 for rev in self._git(*args).strip().split('\n') |
342 if bool(rev) | 352 if bool(rev) |
343 ] | 353 ] |
344 | 354 |
345 def _resolve_refspec_impl(self, revision): | 355 def _resolve_refspec_impl(self, revision): |
346 self._ensure_local_repo_exists() | 356 self._ensure_local_repo_exists() |
347 self.assert_remote('resolve refspec %r' % revision) | 357 self.assert_remote('resolve refspec %r' % revision) |
348 rslt = self._git('ls-remote', self.repo_url, revision).split()[0] | 358 rslt = self._git('ls-remote', self.repo_url, revision).split()[0] |
(...skipping 11 matching lines...) Expand all Loading... |
360 # %`Body` | 370 # %`Body` |
361 meta = self._git( | 371 meta = self._git( |
362 'show', '-s', '--format=%aE%n%ct%n%B', revision).rstrip('\n').splitlines() | 372 'show', '-s', '--format=%aE%n%ct%n%B', revision).rstrip('\n').splitlines() |
363 | 373 |
364 try: | 374 try: |
365 spec = package_io.parse(self._git( | 375 spec = package_io.parse(self._git( |
366 'cat-file', 'blob', '%s:infra/config/recipes.cfg' % revision)) | 376 'cat-file', 'blob', '%s:infra/config/recipes.cfg' % revision)) |
367 except GitError: | 377 except GitError: |
368 spec = None | 378 spec = None |
369 | 379 |
| 380 # check diff to see if it touches anything interesting. |
| 381 changed_files = set(self._git( |
| 382 'diff-tree', '-r', '--no-commit-id', '--name-only', '%s^!' % revision) |
| 383 .splitlines()) |
| 384 |
370 return CommitMetadata(revision, meta[0], | 385 return CommitMetadata(revision, meta[0], |
371 int(meta[1]), tuple(meta[2:]), | 386 int(meta[1]), tuple(meta[2:]), |
372 spec) | 387 spec, has_interesting_changes(spec, changed_files)) |
373 | 388 |
374 class GitilesFetchError(FetchError): | 389 class GitilesFetchError(FetchError): |
375 """An HTTP error that occurred during Gitiles fetching.""" | 390 """An HTTP error that occurred during Gitiles fetching.""" |
376 | 391 |
377 def __init__(self, status, message): | 392 def __init__(self, status, message): |
378 super(GitilesFetchError, self).__init__( | 393 super(GitilesFetchError, self).__init__( |
379 'Gitiles error code (%d): %s' % (status, message)) | 394 'Gitiles error code (%d): %s' % (status, message)) |
380 self.status = status | 395 self.status = status |
381 self.message = message | 396 self.message = message |
382 | 397 |
383 @staticmethod | 398 @staticmethod |
384 def transient(e): | 399 def transient(e): |
385 """ | 400 """ |
386 Returns (bool): True if "e" is a GitilesFetchError with transient HTTP code. | 401 Returns (bool): True if "e" is a GitilesFetchError with transient HTTP code. |
387 """ | 402 """ |
388 return (isinstance(e, GitilesFetchError) and | 403 return (isinstance(e, GitilesFetchError) and |
389 e.status >= httplib.INTERNAL_SERVER_ERROR) | 404 e.status >= httplib.INTERNAL_SERVER_ERROR) |
390 | 405 |
391 | 406 |
392 # Internal cache object for GitilesBackend. | 407 # Internal cache object for GitilesBackend. |
393 # commit (str) - the git commit hash | 408 # commit (str) - the git commit hash |
394 # author_email (str) - the author email for this commit | 409 # author_email (str) - the author email for this commit |
395 # message_lines (tuple) - the lines of the commit message | 410 # message_lines (tuple) - the lines of the commit message |
| 411 # changed_files (frozenset) - all paths touched by this commit |
396 class _GitilesCommitJson(namedtuple( | 412 class _GitilesCommitJson(namedtuple( |
397 '_GitilesCommitJson', | 413 '_GitilesCommitJson', |
398 'commit author_email commit_timestamp message_lines')): | 414 'commit author_email commit_timestamp message_lines changed_files')): |
399 @classmethod | 415 @classmethod |
400 def from_raw_json(cls, raw): | 416 def from_raw_json(cls, raw): |
| 417 mod_files = set() |
| 418 for entry in raw['tree_diff']: |
| 419 mod_files.add(entry['old_path']) |
| 420 mod_files.add(entry['new_path']) |
401 return cls( | 421 return cls( |
402 raw['commit'], | 422 raw['commit'], |
403 raw['author']['email'], | 423 raw['author']['email'], |
404 calendar.timegm(time.strptime(raw['committer']['time'])), | 424 calendar.timegm(time.strptime(raw['committer']['time'])), |
405 tuple(raw['message'].splitlines()), | 425 tuple(raw['message'].splitlines()), |
| 426 frozenset(mod_files), |
406 ) | 427 ) |
407 | 428 |
408 | 429 |
409 class GitilesBackend(Backend): | 430 class GitilesBackend(Backend): |
410 """GitilesBackend uses a repo served by Gitiles.""" | 431 """GitilesBackend uses a repo served by Gitiles.""" |
411 | 432 |
412 # Prefix at the beginning of Gerrit/Gitiles JSON API responses. | 433 # Prefix at the beginning of Gerrit/Gitiles JSON API responses. |
413 _GERRIT_XSRF_HEADER = ')]}\'\n' | 434 _GERRIT_XSRF_HEADER = ')]}\'\n' |
414 | 435 |
415 @util.exponential_retry(condition=GitilesFetchError.transient) | 436 @util.exponential_retry(condition=GitilesFetchError.transient) |
416 def _fetch_gitiles(self, url_fmt, *args): | 437 def _fetch_gitiles(self, url_fmt, *args): |
417 """Fetches a remote URL path and returns the response object on success. | 438 """Fetches a remote URL path and returns the response object on success. |
418 | 439 |
419 Args: | 440 Args: |
420 url_fmt (str) - the url path fragment as a python %format string, like | 441 url_fmt (str) - the url path fragment as a python %format string, like |
421 '%s/foo/bar?something=value' | 442 '%s/foo/bar?something=value' |
422 *args (str) - the arguments to format url_fmt with. They will be URL | 443 *args (str) - the arguments to format url_fmt with. They will be URL |
423 escaped. | 444 escaped. |
424 | 445 |
425 Returns requests.Response. | 446 Returns requests.Response. |
426 """ | 447 """ |
427 url = '%s/%s' % (self.repo_url, | 448 url = '%s/%s' % (self.repo_url, |
428 url_fmt % tuple(map(requests.utils.quote, args))) | 449 url_fmt % tuple(map(requests.utils.quote, args))) |
429 LOGGER.info('fetching %s' % url) | 450 LOGGER.info('fetching %s' % url) |
430 resp = requests.get(url) | 451 resp = requests.get(url) |
431 if resp.status_code != httplib.OK: | 452 if resp.status_code != httplib.OK: |
432 raise GitilesFetchError(resp.status_code, resp.text) | 453 raise GitilesFetchError(resp.status_code, resp.text) |
433 return resp | 454 return resp |
434 | 455 |
435 def _fetch_gitiles_json(self, url_fmt, *args): | 456 def _fetch_gitiles_committish_json(self, url_fmt, *args): |
436 """Fetches a remote URL path and expects a JSON object on success. | 457 """Fetches a remote URL path and expects a JSON object on success. |
437 | 458 |
| 459 This appends two GET params to url_fmt: |
| 460 format=JSON - Does what you expect |
| 461 name-status=1 - Ensures that commit objects returned have a 'tree_diff' |
| 462 member which shows the diff for that commit. |
| 463 |
438 Args: | 464 Args: |
439 url_fmt (str) - the url path fragment as a python %format string, like | 465 url_fmt (str) - the url path fragment as a python %format string, like |
440 '%s/foo/bar?something=value' | 466 '%s/foo/bar?something=value' |
441 *args (str) - the arguments to format url_fmt with. They will be URL | 467 *args (str) - the arguments to format url_fmt with. They will be URL |
442 escaped. | 468 escaped. |
443 | 469 |
444 Returns the decoded JSON object | 470 Returns the decoded JSON object |
445 """ | 471 """ |
446 resp = self._fetch_gitiles(url_fmt, *args) | 472 resp = self._fetch_gitiles(url_fmt+'?name-status=1&format=JSON', *args) |
447 if not resp.text.startswith(self._GERRIT_XSRF_HEADER): | 473 if not resp.text.startswith(self._GERRIT_XSRF_HEADER): |
448 raise GitilesFetchError(resp.status_code, 'Missing XSRF prefix') | 474 raise GitilesFetchError(resp.status_code, 'Missing XSRF prefix') |
449 return json.loads(resp.text[len(self._GERRIT_XSRF_HEADER):]) | 475 return json.loads(resp.text[len(self._GERRIT_XSRF_HEADER):]) |
450 | 476 |
451 # This caches entries from _fetch_commit_json. It's populated by | 477 # This caches entries from _fetch_commit_json. It's populated by |
452 # _fetch_commit_json as well as _updates_impl. | 478 # _fetch_commit_json as well as _updates_impl. |
453 # | 479 # |
454 # Mapping of: | 480 # Mapping of: |
455 # repo_url -> git_revision -> _GitilesCommitJson | 481 # repo_url -> git_revision -> _GitilesCommitJson |
456 # | 482 # |
457 # Only populated if _fetch_commit_json is passed a resolved commit. | 483 # Only populated if _fetch_commit_json is passed a resolved commit. |
458 _COMMIT_JSON_CACHE = {} | 484 _COMMIT_JSON_CACHE = {} |
459 | 485 |
460 def _fetch_commit_json(self, refspec): | 486 def _fetch_commit_json(self, refspec): |
461 """Returns _GitilesCommitJson for the refspec. | 487 """Returns _GitilesCommitJson for the refspec. |
462 | 488 |
463 If refspec is resolved then this value is cached. | 489 If refspec is resolved then this value is cached. |
464 """ | 490 """ |
465 c = self._COMMIT_JSON_CACHE.setdefault(self.repo_url, {}) | 491 c = self._COMMIT_JSON_CACHE.setdefault(self.repo_url, {}) |
466 if refspec in c: | 492 if refspec in c: |
467 return c[refspec] | 493 return c[refspec] |
468 | 494 |
469 raw = self._fetch_gitiles_json('+/%s?format=JSON', refspec) | 495 raw = self._fetch_gitiles_committish_json('+/%s', refspec) |
470 ret = _GitilesCommitJson.from_raw_json(raw) | 496 ret = _GitilesCommitJson.from_raw_json(raw) |
471 if self.is_resolved_revision(refspec): | 497 if self.is_resolved_revision(refspec): |
472 c[refspec] = ret | 498 c[refspec] = ret |
473 | 499 |
474 return ret | 500 return ret |
475 | 501 |
476 | 502 |
477 ### Backend implementations | 503 ### Backend implementations |
478 | 504 |
479 | 505 |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
518 | 544 |
519 # TODO(iannucci): This implementation may be slow if we need to retieve | 545 # TODO(iannucci): This implementation may be slow if we need to retieve |
520 # multiple files/archives from the remote server. Should possibly consider | 546 # multiple files/archives from the remote server. Should possibly consider |
521 # using a thread pool here. | 547 # using a thread pool here. |
522 | 548 |
523 archive_response = self._fetch_gitiles( | 549 archive_response = self._fetch_gitiles( |
524 '+archive/%s/%s.tar.gz', revision, recipes_path_rel) | 550 '+archive/%s/%s.tar.gz', revision, recipes_path_rel) |
525 with tarfile.open(fileobj=StringIO(archive_response.content)) as tf: | 551 with tarfile.open(fileobj=StringIO(archive_response.content)) as tf: |
526 tf.extractall(recipes_path) | 552 tf.extractall(recipes_path) |
527 | 553 |
528 def _updates_impl(self, revision, other_revision, paths): | 554 def _updates_impl(self, revision, other_revision): |
529 self.assert_remote('_updates_impl') | 555 self.assert_remote('_updates_impl') |
530 | 556 |
531 # TODO(iannucci): implement paging | 557 # TODO(iannucci): implement paging |
532 | 558 |
533 # To include info about touched paths (tree_diff), pass name-status=1 below. | 559 log_json = self._fetch_gitiles_committish_json( |
534 log_json = self._fetch_gitiles_json( | 560 '+log/%s..%s', revision, other_revision) |
535 '+log/%s..%s?name-status=1&format=JSON', revision, other_revision) | |
536 | 561 |
537 c = self._COMMIT_JSON_CACHE.setdefault(self.repo_url, {}) | 562 c = self._COMMIT_JSON_CACHE.setdefault(self.repo_url, {}) |
538 | 563 |
539 results = [] | 564 results = [] |
540 for entry in log_json['log']: | 565 for entry in log_json['log']: |
541 commit = entry['commit'] | 566 commit = entry['commit'] |
542 c[commit] = _GitilesCommitJson.from_raw_json(entry) | 567 c[commit] = _GitilesCommitJson.from_raw_json(entry) |
543 | 568 results.append(commit) |
544 matched = False | |
545 for path in paths: | |
546 for diff_entry in entry['tree_diff']: | |
547 if (diff_entry['old_path'].startswith(path) or | |
548 diff_entry['new_path'].startswith(path)): | |
549 matched = True | |
550 break | |
551 if matched: | |
552 break | |
553 if matched or not paths: | |
554 results.append(commit) | |
555 | 569 |
556 results.reverse() | 570 results.reverse() |
557 return map(self.commit_metadata, results) | 571 return map(self.commit_metadata, results) |
558 | 572 |
559 def _resolve_refspec_impl(self, refspec): | 573 def _resolve_refspec_impl(self, refspec): |
560 if self.is_resolved_revision(refspec): | 574 if self.is_resolved_revision(refspec): |
561 return self.commit_metadata(refspec).commit | 575 return self.commit_metadata(refspec).commit |
562 return self._fetch_commit_json(refspec).commit | 576 return self._fetch_commit_json(refspec).commit |
563 | 577 |
564 def _commit_metadata_impl(self, revision): | 578 def _commit_metadata_impl(self, revision): |
565 self.assert_remote('_commit_metadata_impl') | 579 self.assert_remote('_commit_metadata_impl') |
566 rev_json = self._fetch_commit_json(revision) | 580 rev_json = self._fetch_commit_json(revision) |
567 | 581 |
568 recipes_cfg_text = self._fetch_gitiles( | 582 recipes_cfg_text = self._fetch_gitiles( |
569 '+/%s/infra/config/recipes.cfg?format=TEXT', revision | 583 '+/%s/infra/config/recipes.cfg?format=TEXT', revision |
570 ).text.decode('base64') | 584 ).text.decode('base64') |
571 spec = json_format.Parse( | 585 spec = json_format.Parse( |
572 recipes_cfg_text, package_pb2.Package(), ignore_unknown_fields=True) | 586 recipes_cfg_text, package_pb2.Package(), ignore_unknown_fields=True) |
573 | 587 |
574 return CommitMetadata( | 588 return CommitMetadata( |
575 revision, | 589 revision, |
576 rev_json.author_email, | 590 rev_json.author_email, |
577 rev_json.commit_timestamp, | 591 rev_json.commit_timestamp, |
578 rev_json.message_lines, | 592 rev_json.message_lines, |
579 spec) | 593 spec, |
| 594 has_interesting_changes(spec, rev_json.changed_files)) |
OLD | NEW |