blob: 41925de7a9dd254bc40b8770b0f36a85c8a03c30 [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__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080024emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080025
26
Kuang-che Wu41e8b592018-09-25 17:01:30 +080027def config(gclient_dir,
28 url=None,
29 cache_dir=None,
30 deps_file=None,
31 custom_var=None,
32 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080033 """Simply wrapper of `gclient config`.
34
35 Args:
36 gclient_dir: root directory of gclient project
37 url: URL of gclient configuration files
38 cache_dir: gclient's git cache folder
39 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080040 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080041 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080042 """
43 cmd = ['gclient', 'config']
44 if deps_file:
45 cmd += ['--deps-file', deps_file]
46 if cache_dir:
47 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080048 if custom_var:
49 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080050 if spec:
51 cmd += ['--spec', spec]
52 if url:
53 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080054
55 util.check_call(*cmd, cwd=gclient_dir)
56
57
Kuang-che Wudc714412018-10-17 16:06:39 +080058def sync(gclient_dir,
59 with_branch_heads=False,
60 with_tags=False,
61 ignore_locks=False,
62 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080063 """Simply wrapper of `gclient sync`.
64
65 Args:
66 gclient_dir: root directory of gclient project
67 with_branch_heads: whether to clone git `branch_heads` refspecs
68 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080069 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080070 jobs: how many workers running in parallel
71 """
Kuang-che Wu88b17342018-10-23 17:19:09 +080072 cmd = ['gclient', 'sync', '--jobs', str(jobs), '--delete_unversioned_trees']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080073 if with_branch_heads:
74 cmd.append('--with_branch_heads')
75 if with_tags:
76 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080077
78 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
79 # reasons, it will leave annoying lock files on disk and thus unfriendly to
80 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
81 # this function) and bypass gclient's.
82 if ignore_locks:
83 cmd.append('--ignore_locks')
84
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080085 util.check_call(*cmd, cwd=gclient_dir)
86
87
Kuang-che Wu1e49f512018-12-06 15:27:42 +080088def mirror(code_storage, repo_url):
89 """Mirror git repo.
90
91 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
92
93 Args:
94 code_storage: CodeStorage object
95 repo_url: remote repo url
96 """
97 logger.info('mirror %s', repo_url)
98 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
99 git_root = code_storage.cached_git_root(repo_url)
100 assert not os.path.exists(git_root)
101
102 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
103
104 # These config parameters are copied from gclient.
105 git_util.config(tmp_dir, 'gc.autodetach', '0')
106 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
107 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
108 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
109 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
110 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
111 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
112 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
113 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
114 '+refs/branch-heads/*:refs/branch-heads/*',
115 r'\+refs/branch-heads/\*:.*')
116
117 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
118 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
119 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
120
121 # Rename to correct name atomically.
122 os.rename(tmp_dir, git_root)
123
124
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800125# Copied from depot_tools' gclient.py
126_PLATFORM_MAPPING = {
127 'cygwin': 'win',
128 'darwin': 'mac',
129 'linux2': 'linux',
130 'win32': 'win',
131 'aix6': 'aix',
132}
133
134
135def _detect_host_os():
136 return _PLATFORM_MAPPING[sys.platform]
137
138
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800139class Dep(object):
140 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800141
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800142 One Dep object means one subproject inside DEPS file. It recorded what to
143 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800144
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800145 Attributes:
146 path: subproject path, relative to project root
147 variables: the variables of the containing DEPS file; these variables will
148 be applied to fields of this object (like 'url' and 'condition') and
149 children projects.
150 condition: whether to checkout this subproject
151 dep_type: 'git' or 'cipd'
152 url: if dep_type='git', the url of remote repo and associated branch/commit
153 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800154 """
155
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800156 def __init__(self, path, variables, entry):
157 self.path = path
158 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800159
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800160 self.url = None # only valid for dep_type='git'
161 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800162
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800163 if isinstance(entry, str):
164 self.dep_type = 'git'
165 self.url = entry
166 self.condition = None
167 else:
168 self.dep_type = entry.get('dep_type', 'git')
169 self.condition = entry.get('condition')
170 if self.dep_type == 'git':
171 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800172 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800173 assert self.dep_type == 'cipd'
174 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800175
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800176 if self.dep_type == 'git':
177 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800178
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800179 def __eq__(self, rhs):
180 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800181
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800182 def __ne__(self, rhs):
183 return not self.__eq__(rhs)
184
185 def as_path_spec(self):
186 assert self.dep_type == 'git'
187
188 if '@' in self.url:
189 repo_url, at = self.url.split('@')
190 else:
191 # If the dependency is not pinned, the default is master branch.
192 repo_url, at = self.url, 'master'
193 return codechange.PathSpec(self.path, repo_url, at)
194
195 def eval_condition(self):
196 """Evaluate condition for DEPS parsing.
197
198 Returns:
199 eval result
200 """
201 if not self.condition:
202 return True
203
204 vars_dict = {
205 # default os: linux
206 'checkout_android': False,
207 'checkout_chromeos': False,
208 'checkout_fuchsia': False,
209 'checkout_ios': False,
210 'checkout_linux': True,
211 'checkout_mac': False,
212 'checkout_win': False,
213 # default cpu: x64
214 'checkout_arm64': False,
215 'checkout_arm': False,
216 'checkout_mips': False,
217 'checkout_ppc': False,
218 'checkout_s390': False,
219 'checkout_x64': True,
220 'checkout_x86': False,
221 'host_os': _detect_host_os(),
222 'False': False,
223 'None': None,
224 'True': True,
225 }
226 vars_dict.update(self.variables)
227 # pylint: disable=eval-used
228 return eval(self.condition, vars_dict)
229
230
231class Deps(object):
232 """DEPS parsed result.
233
234 Attributes:
235 variables: 'vars' dict in DEPS file; these variables will be applied
236 recursively to children.
237 entries: dict of Dep objects
238 recursedeps: list of recursive projects
239 """
240
241 def __init__(self):
242 self.variables = {}
243 self.entries = {}
244 self.recursedeps = []
245
246
247class TimeSeriesTree(object):
248 """Data structure for generating snapshots of historical dependency tree.
249
250 This is a tree structure with time information. Each tree node represents not
251 only typical tree data and tree children information, but also historical
252 value of those tree data and tree children.
253
254 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
255 represent a DEPS file. The caller will add_snapshot() to add parsed result of
256 historical DEPS instances. After that, the tree root of this class can
257 reconstruct the every historical moment of the project dependency state.
258
259 This class is slight abstraction of git_util.get_history_recursively() to
260 support more than single git repo and be version control system independent.
261 """
262
263 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
264
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800265 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800266 """TimeSeriesTree constructor.
267
268 Args:
269 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800270 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800271 start_time: start time
272 end_time: end time
273 """
274 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800275 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800276 self.snapshots = {}
277 self.start_time = start_time
278 self.end_time = end_time
279
280 # Intermediate dict to keep track alive children for the time being.
281 # Maintained by add_snapshot() and no_more_snapshot().
282 self.alive_children = {}
283
284 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800285 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800286 # once in this list because they are removed and added back to the DEPS
287 # file.
288 self.subtrees = []
289
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800290 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800291 """Compares subtree of two Deps.
292
293 Args:
294 deps_a: Deps object
295 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800296 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800297
298 Returns:
299 True if the said subtree of these two Deps equal
300 """
301 # Need to compare variables because they may influence subtree parsing
302 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800303 path = child_entry[0]
304 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800305 deps_a.variables == deps_b.variables)
306
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800307 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800308 """Adds parsed DEPS result and children.
309
310 For example, if a given DEPS file has N revisions between start_time and
311 end_time, the caller should call this method N times to feed all parsed
312 results in order (timestamp increasing).
313
314 Args:
315 timestamp: timestamp of `deps`
316 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800317 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800318 """
319 assert timestamp not in self.snapshots
320 self.snapshots[timestamp] = deps
321
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800322 for child_entry in set(self.alive_children.keys() + children_entries):
323 # `child_entry` is added at `timestamp`
324 if child_entry not in self.alive_children:
325 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800326
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800327 # `child_entry` is removed at `timestamp`
328 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800329 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800330 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
331 self.alive_children[child_entry][0], timestamp))
332 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800333
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800334 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800335 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800336 last_deps = self.alive_children[child_entry][1]
337 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800338 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800339 TimeSeriesTree(last_deps, child_entry,
340 self.alive_children[child_entry][0], timestamp))
341 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800342
343 def no_more_snapshot(self, deps):
344 """Indicates all snapshots are added.
345
346 add_snapshot() should not be invoked after no_more_snapshot().
347 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800348 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800349 if timestamp == self.end_time:
350 continue
351 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800352 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800353 self.alive_children = None
354
355 def events(self):
356 """Gets children added/removed events of this subtree.
357
358 Returns:
359 list of (timestamp, deps_name, deps, end_flag):
360 timestamp: timestamp of event
361 deps_name: name of this subtree
362 deps: Deps object of given project
363 end_flag: True indicates this is the last event of this deps tree
364 """
365 assert self.snapshots
366 assert self.alive_children is None, ('events() is valid only after '
367 'no_more_snapshot() is invoked')
368
369 result = []
370
371 last_deps = None
372 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800373 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800374 last_deps = deps
375
376 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800377 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800378
379 for subtree in self.subtrees:
380 for event in subtree.events():
381 result.append(event)
382
383 result.sort()
384
385 return result
386
387 def iter_path_specs(self):
388 """Iterates snapshots of project dependency state.
389
390 Yields:
391 (timestamp, path_specs):
392 timestamp: time of snapshot
393 path_specs: dict of path_spec entries
394 """
395 forest = {}
396 # Group by timestamp
397 for timestamp, events in itertools.groupby(self.events(),
398 operator.itemgetter(0)):
399 # It's possible that one deps is removed and added at the same timestamp,
400 # i.e. modification, so use counter to track.
401 end_counter = collections.Counter()
402
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800403 for timestamp, entry, deps, end in events:
404 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800405 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800406 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800407 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800408 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800409
410 # Merge Deps at time `timestamp` into single path_specs.
411 path_specs = {}
412 for deps in forest.values():
413 for path, dep in deps.entries.items():
414 path_specs[path] = dep.as_path_spec()
415
416 yield timestamp, path_specs
417
418 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800419 for entry, count in end_counter.items():
420 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800421 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800422 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800423
424
425class DepsParser(object):
426 """Gclient DEPS file parser."""
427
428 def __init__(self, project_root, code_storage):
429 self.project_root = project_root
430 self.code_storage = code_storage
431
432 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
433 """Parses DEPS file without recursion.
434
435 Args:
436 content: file content of DEPS file
437 parent_vars: variables inherent from parent DEPS
438 parent_path: project path of parent DEPS file
439
440 Returns:
441 Deps object
442 """
443
444 def var_function(name):
445 return '{%s}' % name
446
447 global_scope = dict(Var=var_function)
448 local_scope = {}
449 try:
450 exec (content, global_scope, local_scope) # pylint: disable=exec-used
451 except SyntaxError:
452 raise
453
454 deps = Deps()
455 local_scope.setdefault('vars', {})
456 if parent_vars:
457 local_scope['vars'].update(parent_vars)
458 deps.variables = local_scope['vars']
459
460 # Warnings for old usages which we don't support.
461 for name in deps.variables:
462 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
463 logger.warning('%s is deprecated and not supported recursion syntax',
464 name)
465 if 'deps_os' in local_scope:
466 logger.warning('deps_os is no longer supported')
467
468 for path, dep_entry in local_scope['deps'].items():
469 if local_scope.get('use_relative_paths', False):
470 path = os.path.join(parent_path, path)
471 path = path.format(**deps.variables)
472 dep = Dep(path, deps.variables, dep_entry)
473 if not dep.eval_condition():
474 continue
475
476 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
477 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800478 warning_key = ('dep_type', dep.dep_type, path)
479 if warning_key not in emitted_warnings:
480 emitted_warnings.add(warning_key)
481 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
482 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800483 continue
484
485 deps.entries[path] = dep
486
487 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800488 for recurse_entry in local_scope.get('recursedeps', []):
489 # Normalize entries.
490 if isinstance(recurse_entry, tuple):
491 path, deps_file = recurse_entry
492 else:
493 assert isinstance(path, str)
494 path, deps_file = recurse_entry, 'DEPS'
495
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800496 if local_scope.get('use_relative_paths', False):
497 path = os.path.join(parent_path, path)
498 path = path.format(**deps.variables)
499 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800500 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800501 deps.recursedeps = recursedeps
502
503 return deps
504
505 def construct_deps_tree(self,
506 tstree,
507 repo_url,
508 at,
509 after,
510 before,
511 parent_vars=None,
512 parent_path='',
513 deps_file='DEPS'):
514 """Processes DEPS recursively of given time period.
515
516 This method parses all commits of DEPS between time `after` and `before`,
517 segments recursive dependencies into subtrees if they are changed, and
518 processes subtrees recursively.
519
520 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
521
522 Args:
523 tstree: TimeSeriesTree object
524 repo_url: remote repo url
525 at: branch or git commit id
526 after: begin of period
527 before: end of period
528 parent_vars: DEPS variables inherit from parent DEPS (including
529 custom_vars)
530 parent_path: the path of parent project of current DEPS file
531 deps_file: filename of DEPS file, relative to the git repo, repo_rul
532 """
533 if '://' in repo_url:
534 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800535 if not os.path.exists(git_repo):
536 with locking.lock_file(
537 os.path.join(self.code_storage.cache_dir,
538 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
539 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800540 else:
541 git_repo = repo_url
542
543 if git_util.is_git_rev(at):
544 history = [
545 (after, at),
546 (before, at),
547 ]
548 else:
549 history = git_util.get_history(
550 git_repo,
551 deps_file,
552 branch=at,
553 after=after,
554 before=before,
555 padding=True)
556 assert history
557
558 # If not equal, it means the file was deleted but is still referenced by
559 # its parent.
560 assert history[-1][0] == before
561
562 # TODO(kcwu): optimization: history[-1] is unused
563 for timestamp, git_rev in history[:-1]:
564 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
565
566 deps = self.parse_single_deps(
567 content, parent_vars=parent_vars, parent_path=parent_path)
568 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
569
570 tstree.no_more_snapshot(deps)
571
572 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800573 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800574 path_spec = subtree.parent_deps.entries[path].as_path_spec()
575 self.construct_deps_tree(
576 subtree,
577 path_spec.repo_url,
578 path_spec.at,
579 subtree.start_time,
580 subtree.end_time,
581 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800582 parent_path=path,
583 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800584
585 def enumerate_path_specs(self, start_time, end_time, path):
586 tstree = TimeSeriesTree(None, path, start_time, end_time)
587 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
588 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800589
590
591class GclientCache(codechange.CodeStorage):
592 """Gclient git cache."""
593
594 def __init__(self, cache_dir):
595 self.cache_dir = cache_dir
596
597 def _url_to_cache_dir(self, url):
598 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
599 parsed = urlparse.urlparse(url)
600 norm_url = parsed.netloc + parsed.path
601 if norm_url.endswith('.git'):
602 norm_url = norm_url[:-len('.git')]
603 return norm_url.replace('-', '--').replace('/', '-').lower()
604
605 def cached_git_root(self, repo_url):
606 cache_path = self._url_to_cache_dir(repo_url)
607 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800608
609 def _load_project_list(self, project_root):
610 repo_project_list = os.path.join(project_root, '.gclient_entries')
611 scope = {}
612 exec open(repo_project_list) in scope # pylint: disable=exec-used
613 return scope.get('entries', {})
614
615 def _save_project_list(self, project_root, projects):
616 repo_project_list = os.path.join(project_root, '.gclient_entries')
617 content = 'entries = {\n'
618 for item in sorted(projects.items()):
Kuang-che Wu88b17342018-10-23 17:19:09 +0800619 path, repo_url = map(pprint.pformat, item)
620 content += ' %s: %s,\n' % (path, repo_url)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800621 content += '}\n'
622 with open(repo_project_list, 'w') as f:
623 f.write(content)
624
625 def add_to_project_list(self, project_root, path, repo_url):
626 projects = self._load_project_list(project_root)
627
628 projects[path] = repo_url
629
630 self._save_project_list(project_root, projects)
631
632 def remove_from_project_list(self, project_root, path):
633 projects = self._load_project_list(project_root)
634
635 if path in projects:
636 del projects[path]
637
638 self._save_project_list(project_root, projects)