blob: ea6b5d81af4ec8d9dcd7ab1aea26619b76a6c0e4 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08002# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Gclient utility."""
6
7from __future__ import print_function
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +08008import collections
9import itertools
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080010import logging
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080011import operator
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080012import os
Kuang-che Wu6948ecc2018-09-11 17:43:49 +080013import pprint
Kuang-che Wub17b3b92018-09-04 18:12:11 +080014import sys
Kuang-che Wu1e49f512018-12-06 15:27:42 +080015import tempfile
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080016import urlparse
17
18from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080019from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080020from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080021from bisect_kit import util
22
23logger = logging.getLogger(__name__)
24
25
Kuang-che Wu41e8b592018-09-25 17:01:30 +080026def config(gclient_dir,
27 url=None,
28 cache_dir=None,
29 deps_file=None,
30 custom_var=None,
31 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080032 """Simply wrapper of `gclient config`.
33
34 Args:
35 gclient_dir: root directory of gclient project
36 url: URL of gclient configuration files
37 cache_dir: gclient's git cache folder
38 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080039 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080040 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080041 """
42 cmd = ['gclient', 'config']
43 if deps_file:
44 cmd += ['--deps-file', deps_file]
45 if cache_dir:
46 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080047 if custom_var:
48 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080049 if spec:
50 cmd += ['--spec', spec]
51 if url:
52 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080053
54 util.check_call(*cmd, cwd=gclient_dir)
55
56
Kuang-che Wudc714412018-10-17 16:06:39 +080057def sync(gclient_dir,
58 with_branch_heads=False,
59 with_tags=False,
60 ignore_locks=False,
61 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080062 """Simply wrapper of `gclient sync`.
63
64 Args:
65 gclient_dir: root directory of gclient project
66 with_branch_heads: whether to clone git `branch_heads` refspecs
67 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080068 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080069 jobs: how many workers running in parallel
70 """
Kuang-che Wu88b17342018-10-23 17:19:09 +080071 cmd = ['gclient', 'sync', '--jobs', str(jobs), '--delete_unversioned_trees']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080072 if with_branch_heads:
73 cmd.append('--with_branch_heads')
74 if with_tags:
75 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080076
77 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
78 # reasons, it will leave annoying lock files on disk and thus unfriendly to
79 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
80 # this function) and bypass gclient's.
81 if ignore_locks:
82 cmd.append('--ignore_locks')
83
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080084 util.check_call(*cmd, cwd=gclient_dir)
85
86
Kuang-che Wu1e49f512018-12-06 15:27:42 +080087def mirror(code_storage, repo_url):
88 """Mirror git repo.
89
90 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
91
92 Args:
93 code_storage: CodeStorage object
94 repo_url: remote repo url
95 """
96 logger.info('mirror %s', repo_url)
97 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
98 git_root = code_storage.cached_git_root(repo_url)
99 assert not os.path.exists(git_root)
100
101 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
102
103 # These config parameters are copied from gclient.
104 git_util.config(tmp_dir, 'gc.autodetach', '0')
105 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
106 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
107 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
108 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
109 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
110 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
111 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
112 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
113 '+refs/branch-heads/*:refs/branch-heads/*',
114 r'\+refs/branch-heads/\*:.*')
115
116 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
117 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
118 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
119
120 # Rename to correct name atomically.
121 os.rename(tmp_dir, git_root)
122
123
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800124# Copied from depot_tools' gclient.py
125_PLATFORM_MAPPING = {
126 'cygwin': 'win',
127 'darwin': 'mac',
128 'linux2': 'linux',
129 'win32': 'win',
130 'aix6': 'aix',
131}
132
133
134def _detect_host_os():
135 return _PLATFORM_MAPPING[sys.platform]
136
137
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800138class Dep(object):
139 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800140
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800141 One Dep object means one subproject inside DEPS file. It recorded what to
142 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800143
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800144 Attributes:
145 path: subproject path, relative to project root
146 variables: the variables of the containing DEPS file; these variables will
147 be applied to fields of this object (like 'url' and 'condition') and
148 children projects.
149 condition: whether to checkout this subproject
150 dep_type: 'git' or 'cipd'
151 url: if dep_type='git', the url of remote repo and associated branch/commit
152 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800153 """
154
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800155 def __init__(self, path, variables, entry):
156 self.path = path
157 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800158
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800159 self.url = None # only valid for dep_type='git'
160 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800161
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800162 if isinstance(entry, str):
163 self.dep_type = 'git'
164 self.url = entry
165 self.condition = None
166 else:
167 self.dep_type = entry.get('dep_type', 'git')
168 self.condition = entry.get('condition')
169 if self.dep_type == 'git':
170 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800171 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800172 assert self.dep_type == 'cipd'
173 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800174
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800175 if self.dep_type == 'git':
176 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800177
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800178 def __eq__(self, rhs):
179 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800180
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800181 def __ne__(self, rhs):
182 return not self.__eq__(rhs)
183
184 def as_path_spec(self):
185 assert self.dep_type == 'git'
186
187 if '@' in self.url:
188 repo_url, at = self.url.split('@')
189 else:
190 # If the dependency is not pinned, the default is master branch.
191 repo_url, at = self.url, 'master'
192 return codechange.PathSpec(self.path, repo_url, at)
193
194 def eval_condition(self):
195 """Evaluate condition for DEPS parsing.
196
197 Returns:
198 eval result
199 """
200 if not self.condition:
201 return True
202
203 vars_dict = {
204 # default os: linux
205 'checkout_android': False,
206 'checkout_chromeos': False,
207 'checkout_fuchsia': False,
208 'checkout_ios': False,
209 'checkout_linux': True,
210 'checkout_mac': False,
211 'checkout_win': False,
212 # default cpu: x64
213 'checkout_arm64': False,
214 'checkout_arm': False,
215 'checkout_mips': False,
216 'checkout_ppc': False,
217 'checkout_s390': False,
218 'checkout_x64': True,
219 'checkout_x86': False,
220 'host_os': _detect_host_os(),
221 'False': False,
222 'None': None,
223 'True': True,
224 }
225 vars_dict.update(self.variables)
226 # pylint: disable=eval-used
227 return eval(self.condition, vars_dict)
228
229
230class Deps(object):
231 """DEPS parsed result.
232
233 Attributes:
234 variables: 'vars' dict in DEPS file; these variables will be applied
235 recursively to children.
236 entries: dict of Dep objects
237 recursedeps: list of recursive projects
238 """
239
240 def __init__(self):
241 self.variables = {}
242 self.entries = {}
243 self.recursedeps = []
244
245
246class TimeSeriesTree(object):
247 """Data structure for generating snapshots of historical dependency tree.
248
249 This is a tree structure with time information. Each tree node represents not
250 only typical tree data and tree children information, but also historical
251 value of those tree data and tree children.
252
253 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
254 represent a DEPS file. The caller will add_snapshot() to add parsed result of
255 historical DEPS instances. After that, the tree root of this class can
256 reconstruct the every historical moment of the project dependency state.
257
258 This class is slight abstraction of git_util.get_history_recursively() to
259 support more than single git repo and be version control system independent.
260 """
261
262 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
263
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800264 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800265 """TimeSeriesTree constructor.
266
267 Args:
268 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800269 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800270 start_time: start time
271 end_time: end time
272 """
273 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800274 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800275 self.snapshots = {}
276 self.start_time = start_time
277 self.end_time = end_time
278
279 # Intermediate dict to keep track alive children for the time being.
280 # Maintained by add_snapshot() and no_more_snapshot().
281 self.alive_children = {}
282
283 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800284 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800285 # once in this list because they are removed and added back to the DEPS
286 # file.
287 self.subtrees = []
288
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800289 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800290 """Compares subtree of two Deps.
291
292 Args:
293 deps_a: Deps object
294 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800295 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800296
297 Returns:
298 True if the said subtree of these two Deps equal
299 """
300 # Need to compare variables because they may influence subtree parsing
301 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800302 path = child_entry[0]
303 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800304 deps_a.variables == deps_b.variables)
305
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800306 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800307 """Adds parsed DEPS result and children.
308
309 For example, if a given DEPS file has N revisions between start_time and
310 end_time, the caller should call this method N times to feed all parsed
311 results in order (timestamp increasing).
312
313 Args:
314 timestamp: timestamp of `deps`
315 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800316 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800317 """
318 assert timestamp not in self.snapshots
319 self.snapshots[timestamp] = deps
320
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800321 for child_entry in set(self.alive_children.keys() + children_entries):
322 # `child_entry` is added at `timestamp`
323 if child_entry not in self.alive_children:
324 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800325
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800326 # `child_entry` is removed at `timestamp`
327 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800328 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800329 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
330 self.alive_children[child_entry][0], timestamp))
331 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800332
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800333 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800334 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800335 last_deps = self.alive_children[child_entry][1]
336 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800337 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800338 TimeSeriesTree(last_deps, child_entry,
339 self.alive_children[child_entry][0], timestamp))
340 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800341
342 def no_more_snapshot(self, deps):
343 """Indicates all snapshots are added.
344
345 add_snapshot() should not be invoked after no_more_snapshot().
346 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800347 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800348 if timestamp == self.end_time:
349 continue
350 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800351 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800352 self.alive_children = None
353
354 def events(self):
355 """Gets children added/removed events of this subtree.
356
357 Returns:
358 list of (timestamp, deps_name, deps, end_flag):
359 timestamp: timestamp of event
360 deps_name: name of this subtree
361 deps: Deps object of given project
362 end_flag: True indicates this is the last event of this deps tree
363 """
364 assert self.snapshots
365 assert self.alive_children is None, ('events() is valid only after '
366 'no_more_snapshot() is invoked')
367
368 result = []
369
370 last_deps = None
371 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800372 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800373 last_deps = deps
374
375 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800376 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800377
378 for subtree in self.subtrees:
379 for event in subtree.events():
380 result.append(event)
381
382 result.sort()
383
384 return result
385
386 def iter_path_specs(self):
387 """Iterates snapshots of project dependency state.
388
389 Yields:
390 (timestamp, path_specs):
391 timestamp: time of snapshot
392 path_specs: dict of path_spec entries
393 """
394 forest = {}
395 # Group by timestamp
396 for timestamp, events in itertools.groupby(self.events(),
397 operator.itemgetter(0)):
398 # It's possible that one deps is removed and added at the same timestamp,
399 # i.e. modification, so use counter to track.
400 end_counter = collections.Counter()
401
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800402 for timestamp, entry, deps, end in events:
403 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800404 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800405 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800406 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800407 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800408
409 # Merge Deps at time `timestamp` into single path_specs.
410 path_specs = {}
411 for deps in forest.values():
412 for path, dep in deps.entries.items():
413 path_specs[path] = dep.as_path_spec()
414
415 yield timestamp, path_specs
416
417 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800418 for entry, count in end_counter.items():
419 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800420 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800421 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800422
423
424class DepsParser(object):
425 """Gclient DEPS file parser."""
426
427 def __init__(self, project_root, code_storage):
428 self.project_root = project_root
429 self.code_storage = code_storage
430
431 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
432 """Parses DEPS file without recursion.
433
434 Args:
435 content: file content of DEPS file
436 parent_vars: variables inherent from parent DEPS
437 parent_path: project path of parent DEPS file
438
439 Returns:
440 Deps object
441 """
442
443 def var_function(name):
444 return '{%s}' % name
445
446 global_scope = dict(Var=var_function)
447 local_scope = {}
448 try:
449 exec (content, global_scope, local_scope) # pylint: disable=exec-used
450 except SyntaxError:
451 raise
452
453 deps = Deps()
454 local_scope.setdefault('vars', {})
455 if parent_vars:
456 local_scope['vars'].update(parent_vars)
457 deps.variables = local_scope['vars']
458
459 # Warnings for old usages which we don't support.
460 for name in deps.variables:
461 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
462 logger.warning('%s is deprecated and not supported recursion syntax',
463 name)
464 if 'deps_os' in local_scope:
465 logger.warning('deps_os is no longer supported')
466
467 for path, dep_entry in local_scope['deps'].items():
468 if local_scope.get('use_relative_paths', False):
469 path = os.path.join(parent_path, path)
470 path = path.format(**deps.variables)
471 dep = Dep(path, deps.variables, dep_entry)
472 if not dep.eval_condition():
473 continue
474
475 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
476 if dep.dep_type != 'git':
477 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
478 path)
479 continue
480
481 deps.entries[path] = dep
482
483 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800484 for recurse_entry in local_scope.get('recursedeps', []):
485 # Normalize entries.
486 if isinstance(recurse_entry, tuple):
487 path, deps_file = recurse_entry
488 else:
489 assert isinstance(path, str)
490 path, deps_file = recurse_entry, 'DEPS'
491
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800492 if local_scope.get('use_relative_paths', False):
493 path = os.path.join(parent_path, path)
494 path = path.format(**deps.variables)
495 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800496 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800497 deps.recursedeps = recursedeps
498
499 return deps
500
501 def construct_deps_tree(self,
502 tstree,
503 repo_url,
504 at,
505 after,
506 before,
507 parent_vars=None,
508 parent_path='',
509 deps_file='DEPS'):
510 """Processes DEPS recursively of given time period.
511
512 This method parses all commits of DEPS between time `after` and `before`,
513 segments recursive dependencies into subtrees if they are changed, and
514 processes subtrees recursively.
515
516 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
517
518 Args:
519 tstree: TimeSeriesTree object
520 repo_url: remote repo url
521 at: branch or git commit id
522 after: begin of period
523 before: end of period
524 parent_vars: DEPS variables inherit from parent DEPS (including
525 custom_vars)
526 parent_path: the path of parent project of current DEPS file
527 deps_file: filename of DEPS file, relative to the git repo, repo_rul
528 """
529 if '://' in repo_url:
530 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800531 if not os.path.exists(git_repo):
532 with locking.lock_file(
533 os.path.join(self.code_storage.cache_dir,
534 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
535 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800536 else:
537 git_repo = repo_url
538
539 if git_util.is_git_rev(at):
540 history = [
541 (after, at),
542 (before, at),
543 ]
544 else:
545 history = git_util.get_history(
546 git_repo,
547 deps_file,
548 branch=at,
549 after=after,
550 before=before,
551 padding=True)
552 assert history
553
554 # If not equal, it means the file was deleted but is still referenced by
555 # its parent.
556 assert history[-1][0] == before
557
558 # TODO(kcwu): optimization: history[-1] is unused
559 for timestamp, git_rev in history[:-1]:
560 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
561
562 deps = self.parse_single_deps(
563 content, parent_vars=parent_vars, parent_path=parent_path)
564 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
565
566 tstree.no_more_snapshot(deps)
567
568 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800569 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800570 path_spec = subtree.parent_deps.entries[path].as_path_spec()
571 self.construct_deps_tree(
572 subtree,
573 path_spec.repo_url,
574 path_spec.at,
575 subtree.start_time,
576 subtree.end_time,
577 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800578 parent_path=path,
579 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800580
581 def enumerate_path_specs(self, start_time, end_time, path):
582 tstree = TimeSeriesTree(None, path, start_time, end_time)
583 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
584 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800585
586
587class GclientCache(codechange.CodeStorage):
588 """Gclient git cache."""
589
590 def __init__(self, cache_dir):
591 self.cache_dir = cache_dir
592
593 def _url_to_cache_dir(self, url):
594 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
595 parsed = urlparse.urlparse(url)
596 norm_url = parsed.netloc + parsed.path
597 if norm_url.endswith('.git'):
598 norm_url = norm_url[:-len('.git')]
599 return norm_url.replace('-', '--').replace('/', '-').lower()
600
601 def cached_git_root(self, repo_url):
602 cache_path = self._url_to_cache_dir(repo_url)
603 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800604
605 def _load_project_list(self, project_root):
606 repo_project_list = os.path.join(project_root, '.gclient_entries')
607 scope = {}
608 exec open(repo_project_list) in scope # pylint: disable=exec-used
609 return scope.get('entries', {})
610
611 def _save_project_list(self, project_root, projects):
612 repo_project_list = os.path.join(project_root, '.gclient_entries')
613 content = 'entries = {\n'
614 for item in sorted(projects.items()):
Kuang-che Wu88b17342018-10-23 17:19:09 +0800615 path, repo_url = map(pprint.pformat, item)
616 content += ' %s: %s,\n' % (path, repo_url)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800617 content += '}\n'
618 with open(repo_project_list, 'w') as f:
619 f.write(content)
620
621 def add_to_project_list(self, project_root, path, repo_url):
622 projects = self._load_project_list(project_root)
623
624 projects[path] = repo_url
625
626 self._save_project_list(project_root, projects)
627
628 def remove_from_project_list(self, project_root, path):
629 projects = self._load_project_list(project_root)
630
631 if path in projects:
632 del projects[path]
633
634 self._save_project_list(project_root, projects)