blob: 5a3d7f6c6ac0741dbc896860a96b7543ce9ba141 [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 Wu067ff292019-02-14 18:16:23 +080014import shutil
Kuang-che Wub17b3b92018-09-04 18:12:11 +080015import sys
Kuang-che Wu1e49f512018-12-06 15:27:42 +080016import tempfile
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080017import urlparse
18
19from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080020from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080021from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080022from bisect_kit import util
23
24logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080025emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080026
27
Kuang-che Wu41e8b592018-09-25 17:01:30 +080028def config(gclient_dir,
29 url=None,
30 cache_dir=None,
31 deps_file=None,
32 custom_var=None,
33 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080034 """Simply wrapper of `gclient config`.
35
36 Args:
37 gclient_dir: root directory of gclient project
38 url: URL of gclient configuration files
39 cache_dir: gclient's git cache folder
40 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080041 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080042 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080043 """
44 cmd = ['gclient', 'config']
45 if deps_file:
46 cmd += ['--deps-file', deps_file]
47 if cache_dir:
48 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080049 if custom_var:
50 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080051 if spec:
52 cmd += ['--spec', spec]
53 if url:
54 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080055
56 util.check_call(*cmd, cwd=gclient_dir)
57
58
Kuang-che Wudc714412018-10-17 16:06:39 +080059def sync(gclient_dir,
60 with_branch_heads=False,
61 with_tags=False,
62 ignore_locks=False,
63 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080064 """Simply wrapper of `gclient sync`.
65
66 Args:
67 gclient_dir: root directory of gclient project
68 with_branch_heads: whether to clone git `branch_heads` refspecs
69 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080070 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080071 jobs: how many workers running in parallel
72 """
Kuang-che Wu88b17342018-10-23 17:19:09 +080073 cmd = ['gclient', 'sync', '--jobs', str(jobs), '--delete_unversioned_trees']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080074 if with_branch_heads:
75 cmd.append('--with_branch_heads')
76 if with_tags:
77 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080078
79 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
80 # reasons, it will leave annoying lock files on disk and thus unfriendly to
81 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
82 # this function) and bypass gclient's.
83 if ignore_locks:
84 cmd.append('--ignore_locks')
85
Kuang-che Wu067ff292019-02-14 18:16:23 +080086 try:
87 old_projects = load_gclient_entries(gclient_dir)
88 except IOError:
89 old_projects = {}
90
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080091 util.check_call(*cmd, cwd=gclient_dir)
92
Kuang-che Wu067ff292019-02-14 18:16:23 +080093 # Remove dead .git folder after sync.
94 # Ideally, this should be handled by gclient but sometimes gclient didn't
95 # (crbug/930047).
96 new_projects = load_gclient_entries(gclient_dir)
97 for path in old_projects:
98 if path in new_projects:
99 continue
100 old_git_dir = os.path.join(gclient_dir, path)
101 if os.path.exists(old_git_dir):
102 logger.warning(
103 '%s was removed from .gclient_entries but %s still exists; remove it',
104 path, old_git_dir)
105 shutil.rmtree(old_git_dir)
106
107
108def load_gclient_entries(gclient_dir):
109 """Loads .gclient_entries."""
110 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
111 scope = {}
112 exec open(repo_project_list) in scope # pylint: disable=exec-used
113 return scope.get('entries', {})
114
115
116def write_gclient_entries(gclient_dir, projects):
117 """Writes .gclient_entries."""
118 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
119 content = 'entries = {\n'
120 for item in sorted(projects.items()):
121 path, repo_url = map(pprint.pformat, item)
122 content += ' %s: %s,\n' % (path, repo_url)
123 content += '}\n'
124 with open(repo_project_list, 'w') as f:
125 f.write(content)
126
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800127
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800128def mirror(code_storage, repo_url):
129 """Mirror git repo.
130
131 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
132
133 Args:
134 code_storage: CodeStorage object
135 repo_url: remote repo url
136 """
137 logger.info('mirror %s', repo_url)
138 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
139 git_root = code_storage.cached_git_root(repo_url)
140 assert not os.path.exists(git_root)
141
142 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
143
144 # These config parameters are copied from gclient.
145 git_util.config(tmp_dir, 'gc.autodetach', '0')
146 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
147 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
148 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
149 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
150 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
151 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
152 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
153 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
154 '+refs/branch-heads/*:refs/branch-heads/*',
155 r'\+refs/branch-heads/\*:.*')
156
157 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
158 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
159 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
160
161 # Rename to correct name atomically.
162 os.rename(tmp_dir, git_root)
163
164
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800165# Copied from depot_tools' gclient.py
166_PLATFORM_MAPPING = {
167 'cygwin': 'win',
168 'darwin': 'mac',
169 'linux2': 'linux',
170 'win32': 'win',
171 'aix6': 'aix',
172}
173
174
175def _detect_host_os():
176 return _PLATFORM_MAPPING[sys.platform]
177
178
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800179class Dep(object):
180 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800181
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800182 One Dep object means one subproject inside DEPS file. It recorded what to
183 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800184
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800185 Attributes:
186 path: subproject path, relative to project root
187 variables: the variables of the containing DEPS file; these variables will
188 be applied to fields of this object (like 'url' and 'condition') and
189 children projects.
190 condition: whether to checkout this subproject
191 dep_type: 'git' or 'cipd'
192 url: if dep_type='git', the url of remote repo and associated branch/commit
193 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800194 """
195
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800196 def __init__(self, path, variables, entry):
197 self.path = path
198 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800199
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800200 self.url = None # only valid for dep_type='git'
201 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800202
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800203 if isinstance(entry, str):
204 self.dep_type = 'git'
205 self.url = entry
206 self.condition = None
207 else:
208 self.dep_type = entry.get('dep_type', 'git')
209 self.condition = entry.get('condition')
210 if self.dep_type == 'git':
211 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800212 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800213 assert self.dep_type == 'cipd'
214 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800215
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800216 if self.dep_type == 'git':
217 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800218
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800219 def __eq__(self, rhs):
220 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800221
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800222 def __ne__(self, rhs):
223 return not self.__eq__(rhs)
224
225 def as_path_spec(self):
226 assert self.dep_type == 'git'
227
228 if '@' in self.url:
229 repo_url, at = self.url.split('@')
230 else:
231 # If the dependency is not pinned, the default is master branch.
232 repo_url, at = self.url, 'master'
233 return codechange.PathSpec(self.path, repo_url, at)
234
235 def eval_condition(self):
236 """Evaluate condition for DEPS parsing.
237
238 Returns:
239 eval result
240 """
241 if not self.condition:
242 return True
243
244 vars_dict = {
245 # default os: linux
246 'checkout_android': False,
247 'checkout_chromeos': False,
248 'checkout_fuchsia': False,
249 'checkout_ios': False,
250 'checkout_linux': True,
251 'checkout_mac': False,
252 'checkout_win': False,
253 # default cpu: x64
254 'checkout_arm64': False,
255 'checkout_arm': False,
256 'checkout_mips': False,
257 'checkout_ppc': False,
258 'checkout_s390': False,
259 'checkout_x64': True,
260 'checkout_x86': False,
261 'host_os': _detect_host_os(),
262 'False': False,
263 'None': None,
264 'True': True,
265 }
266 vars_dict.update(self.variables)
267 # pylint: disable=eval-used
268 return eval(self.condition, vars_dict)
269
270
271class Deps(object):
272 """DEPS parsed result.
273
274 Attributes:
275 variables: 'vars' dict in DEPS file; these variables will be applied
276 recursively to children.
277 entries: dict of Dep objects
278 recursedeps: list of recursive projects
279 """
280
281 def __init__(self):
282 self.variables = {}
283 self.entries = {}
284 self.recursedeps = []
285
286
287class TimeSeriesTree(object):
288 """Data structure for generating snapshots of historical dependency tree.
289
290 This is a tree structure with time information. Each tree node represents not
291 only typical tree data and tree children information, but also historical
292 value of those tree data and tree children.
293
294 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
295 represent a DEPS file. The caller will add_snapshot() to add parsed result of
296 historical DEPS instances. After that, the tree root of this class can
297 reconstruct the every historical moment of the project dependency state.
298
299 This class is slight abstraction of git_util.get_history_recursively() to
300 support more than single git repo and be version control system independent.
301 """
302
303 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
304
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800305 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800306 """TimeSeriesTree constructor.
307
308 Args:
309 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800310 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800311 start_time: start time
312 end_time: end time
313 """
314 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800315 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800316 self.snapshots = {}
317 self.start_time = start_time
318 self.end_time = end_time
319
320 # Intermediate dict to keep track alive children for the time being.
321 # Maintained by add_snapshot() and no_more_snapshot().
322 self.alive_children = {}
323
324 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800325 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800326 # once in this list because they are removed and added back to the DEPS
327 # file.
328 self.subtrees = []
329
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800330 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800331 """Compares subtree of two Deps.
332
333 Args:
334 deps_a: Deps object
335 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800336 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800337
338 Returns:
339 True if the said subtree of these two Deps equal
340 """
341 # Need to compare variables because they may influence subtree parsing
342 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800343 path = child_entry[0]
344 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800345 deps_a.variables == deps_b.variables)
346
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800347 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800348 """Adds parsed DEPS result and children.
349
350 For example, if a given DEPS file has N revisions between start_time and
351 end_time, the caller should call this method N times to feed all parsed
352 results in order (timestamp increasing).
353
354 Args:
355 timestamp: timestamp of `deps`
356 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800357 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800358 """
359 assert timestamp not in self.snapshots
360 self.snapshots[timestamp] = deps
361
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800362 for child_entry in set(self.alive_children.keys() + children_entries):
363 # `child_entry` is added at `timestamp`
364 if child_entry not in self.alive_children:
365 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800366
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800367 # `child_entry` is removed at `timestamp`
368 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800369 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800370 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
371 self.alive_children[child_entry][0], timestamp))
372 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800373
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800374 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800375 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800376 last_deps = self.alive_children[child_entry][1]
377 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800378 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800379 TimeSeriesTree(last_deps, child_entry,
380 self.alive_children[child_entry][0], timestamp))
381 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800382
383 def no_more_snapshot(self, deps):
384 """Indicates all snapshots are added.
385
386 add_snapshot() should not be invoked after no_more_snapshot().
387 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800388 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800389 if timestamp == self.end_time:
390 continue
391 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800392 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800393 self.alive_children = None
394
395 def events(self):
396 """Gets children added/removed events of this subtree.
397
398 Returns:
399 list of (timestamp, deps_name, deps, end_flag):
400 timestamp: timestamp of event
401 deps_name: name of this subtree
402 deps: Deps object of given project
403 end_flag: True indicates this is the last event of this deps tree
404 """
405 assert self.snapshots
406 assert self.alive_children is None, ('events() is valid only after '
407 'no_more_snapshot() is invoked')
408
409 result = []
410
411 last_deps = None
412 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800413 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800414 last_deps = deps
415
416 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800417 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800418
419 for subtree in self.subtrees:
420 for event in subtree.events():
421 result.append(event)
422
423 result.sort()
424
425 return result
426
427 def iter_path_specs(self):
428 """Iterates snapshots of project dependency state.
429
430 Yields:
431 (timestamp, path_specs):
432 timestamp: time of snapshot
433 path_specs: dict of path_spec entries
434 """
435 forest = {}
436 # Group by timestamp
437 for timestamp, events in itertools.groupby(self.events(),
438 operator.itemgetter(0)):
439 # It's possible that one deps is removed and added at the same timestamp,
440 # i.e. modification, so use counter to track.
441 end_counter = collections.Counter()
442
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800443 for timestamp, entry, deps, end in events:
444 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800445 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800446 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800447 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800448 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800449
450 # Merge Deps at time `timestamp` into single path_specs.
451 path_specs = {}
452 for deps in forest.values():
453 for path, dep in deps.entries.items():
454 path_specs[path] = dep.as_path_spec()
455
456 yield timestamp, path_specs
457
458 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800459 for entry, count in end_counter.items():
460 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800461 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800462 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800463
464
465class DepsParser(object):
466 """Gclient DEPS file parser."""
467
468 def __init__(self, project_root, code_storage):
469 self.project_root = project_root
470 self.code_storage = code_storage
471
Kuang-che Wu067ff292019-02-14 18:16:23 +0800472 def load_single_deps(self, content):
473
474 def var_function(name):
475 return '{%s}' % name
476
477 global_scope = dict(Var=var_function)
478 local_scope = {}
479 try:
480 exec (content, global_scope, local_scope) # pylint: disable=exec-used
481 except SyntaxError:
482 raise
483
484 return local_scope
485
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800486 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
487 """Parses DEPS file without recursion.
488
489 Args:
490 content: file content of DEPS file
491 parent_vars: variables inherent from parent DEPS
492 parent_path: project path of parent DEPS file
493
494 Returns:
495 Deps object
496 """
497
Kuang-che Wu067ff292019-02-14 18:16:23 +0800498 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800499 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800500
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800501 local_scope.setdefault('vars', {})
502 if parent_vars:
503 local_scope['vars'].update(parent_vars)
504 deps.variables = local_scope['vars']
505
506 # Warnings for old usages which we don't support.
507 for name in deps.variables:
508 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
509 logger.warning('%s is deprecated and not supported recursion syntax',
510 name)
511 if 'deps_os' in local_scope:
512 logger.warning('deps_os is no longer supported')
513
514 for path, dep_entry in local_scope['deps'].items():
515 if local_scope.get('use_relative_paths', False):
516 path = os.path.join(parent_path, path)
517 path = path.format(**deps.variables)
518 dep = Dep(path, deps.variables, dep_entry)
519 if not dep.eval_condition():
520 continue
521
522 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
523 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800524 warning_key = ('dep_type', dep.dep_type, path)
525 if warning_key not in emitted_warnings:
526 emitted_warnings.add(warning_key)
527 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
528 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800529 continue
530
531 deps.entries[path] = dep
532
533 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800534 for recurse_entry in local_scope.get('recursedeps', []):
535 # Normalize entries.
536 if isinstance(recurse_entry, tuple):
537 path, deps_file = recurse_entry
538 else:
539 assert isinstance(path, str)
540 path, deps_file = recurse_entry, 'DEPS'
541
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800542 if local_scope.get('use_relative_paths', False):
543 path = os.path.join(parent_path, path)
544 path = path.format(**deps.variables)
545 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800546 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800547 deps.recursedeps = recursedeps
548
549 return deps
550
551 def construct_deps_tree(self,
552 tstree,
553 repo_url,
554 at,
555 after,
556 before,
557 parent_vars=None,
558 parent_path='',
559 deps_file='DEPS'):
560 """Processes DEPS recursively of given time period.
561
562 This method parses all commits of DEPS between time `after` and `before`,
563 segments recursive dependencies into subtrees if they are changed, and
564 processes subtrees recursively.
565
566 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
567
568 Args:
569 tstree: TimeSeriesTree object
570 repo_url: remote repo url
571 at: branch or git commit id
572 after: begin of period
573 before: end of period
574 parent_vars: DEPS variables inherit from parent DEPS (including
575 custom_vars)
576 parent_path: the path of parent project of current DEPS file
577 deps_file: filename of DEPS file, relative to the git repo, repo_rul
578 """
579 if '://' in repo_url:
580 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800581 if not os.path.exists(git_repo):
582 with locking.lock_file(
583 os.path.join(self.code_storage.cache_dir,
584 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
585 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800586 else:
587 git_repo = repo_url
588
589 if git_util.is_git_rev(at):
590 history = [
591 (after, at),
592 (before, at),
593 ]
594 else:
595 history = git_util.get_history(
596 git_repo,
597 deps_file,
598 branch=at,
599 after=after,
600 before=before,
601 padding=True)
602 assert history
603
604 # If not equal, it means the file was deleted but is still referenced by
605 # its parent.
606 assert history[-1][0] == before
607
608 # TODO(kcwu): optimization: history[-1] is unused
609 for timestamp, git_rev in history[:-1]:
610 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
611
612 deps = self.parse_single_deps(
613 content, parent_vars=parent_vars, parent_path=parent_path)
614 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
615
616 tstree.no_more_snapshot(deps)
617
618 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800619 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800620 path_spec = subtree.parent_deps.entries[path].as_path_spec()
621 self.construct_deps_tree(
622 subtree,
623 path_spec.repo_url,
624 path_spec.at,
625 subtree.start_time,
626 subtree.end_time,
627 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800628 parent_path=path,
629 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800630
631 def enumerate_path_specs(self, start_time, end_time, path):
632 tstree = TimeSeriesTree(None, path, start_time, end_time)
633 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
634 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800635
636
637class GclientCache(codechange.CodeStorage):
638 """Gclient git cache."""
639
640 def __init__(self, cache_dir):
641 self.cache_dir = cache_dir
642
643 def _url_to_cache_dir(self, url):
644 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
645 parsed = urlparse.urlparse(url)
646 norm_url = parsed.netloc + parsed.path
647 if norm_url.endswith('.git'):
648 norm_url = norm_url[:-len('.git')]
649 return norm_url.replace('-', '--').replace('/', '-').lower()
650
651 def cached_git_root(self, repo_url):
652 cache_path = self._url_to_cache_dir(repo_url)
653 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800654
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800655 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800656 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800657
658 projects[path] = repo_url
659
Kuang-che Wu067ff292019-02-14 18:16:23 +0800660 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800661
662 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800663 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800664
665 if path in projects:
666 del projects[path]
667
Kuang-che Wu067ff292019-02-14 18:16:23 +0800668 write_gclient_entries(project_root, projects)