blob: 1d18c01ea34e35a8aa47dca896606e34ade5b888 [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
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800244 # Currently, we only support chromeos as target_os.
245 # TODO(kcwu): make it configurable if we need to bisect for other os.
246 target_os = ['chromeos']
247
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800248 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800249 'checkout_android': 'android' in target_os,
250 'checkout_chromeos': 'chromeos' in target_os,
251 'checkout_fuchsia': 'fuchsia' in target_os,
252 'checkout_ios': 'ios' in target_os,
253 'checkout_linux': 'unix' in target_os,
254 'checkout_mac': 'mac' in target_os,
255 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800256 # default cpu: x64
257 'checkout_arm64': False,
258 'checkout_arm': False,
259 'checkout_mips': False,
260 'checkout_ppc': False,
261 'checkout_s390': False,
262 'checkout_x64': True,
263 'checkout_x86': False,
264 'host_os': _detect_host_os(),
265 'False': False,
266 'None': None,
267 'True': True,
268 }
269 vars_dict.update(self.variables)
270 # pylint: disable=eval-used
271 return eval(self.condition, vars_dict)
272
273
274class Deps(object):
275 """DEPS parsed result.
276
277 Attributes:
278 variables: 'vars' dict in DEPS file; these variables will be applied
279 recursively to children.
280 entries: dict of Dep objects
281 recursedeps: list of recursive projects
282 """
283
284 def __init__(self):
285 self.variables = {}
286 self.entries = {}
287 self.recursedeps = []
288
289
290class TimeSeriesTree(object):
291 """Data structure for generating snapshots of historical dependency tree.
292
293 This is a tree structure with time information. Each tree node represents not
294 only typical tree data and tree children information, but also historical
295 value of those tree data and tree children.
296
297 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
298 represent a DEPS file. The caller will add_snapshot() to add parsed result of
299 historical DEPS instances. After that, the tree root of this class can
300 reconstruct the every historical moment of the project dependency state.
301
302 This class is slight abstraction of git_util.get_history_recursively() to
303 support more than single git repo and be version control system independent.
304 """
305
306 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
307
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800308 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800309 """TimeSeriesTree constructor.
310
311 Args:
312 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800313 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800314 start_time: start time
315 end_time: end time
316 """
317 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800318 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800319 self.snapshots = {}
320 self.start_time = start_time
321 self.end_time = end_time
322
323 # Intermediate dict to keep track alive children for the time being.
324 # Maintained by add_snapshot() and no_more_snapshot().
325 self.alive_children = {}
326
327 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800328 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800329 # once in this list because they are removed and added back to the DEPS
330 # file.
331 self.subtrees = []
332
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800333 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800334 """Compares subtree of two Deps.
335
336 Args:
337 deps_a: Deps object
338 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800339 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800340
341 Returns:
342 True if the said subtree of these two Deps equal
343 """
344 # Need to compare variables because they may influence subtree parsing
345 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800346 path = child_entry[0]
347 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800348 deps_a.variables == deps_b.variables)
349
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800350 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800351 """Adds parsed DEPS result and children.
352
353 For example, if a given DEPS file has N revisions between start_time and
354 end_time, the caller should call this method N times to feed all parsed
355 results in order (timestamp increasing).
356
357 Args:
358 timestamp: timestamp of `deps`
359 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800360 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800361 """
362 assert timestamp not in self.snapshots
363 self.snapshots[timestamp] = deps
364
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800365 for child_entry in set(self.alive_children.keys() + children_entries):
366 # `child_entry` is added at `timestamp`
367 if child_entry not in self.alive_children:
368 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800369
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800370 # `child_entry` is removed at `timestamp`
371 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800372 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800373 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
374 self.alive_children[child_entry][0], timestamp))
375 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800376
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800377 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800378 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800379 last_deps = self.alive_children[child_entry][1]
380 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800381 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800382 TimeSeriesTree(last_deps, child_entry,
383 self.alive_children[child_entry][0], timestamp))
384 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800385
386 def no_more_snapshot(self, deps):
387 """Indicates all snapshots are added.
388
389 add_snapshot() should not be invoked after no_more_snapshot().
390 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800391 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800392 if timestamp == self.end_time:
393 continue
394 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800395 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800396 self.alive_children = None
397
398 def events(self):
399 """Gets children added/removed events of this subtree.
400
401 Returns:
402 list of (timestamp, deps_name, deps, end_flag):
403 timestamp: timestamp of event
404 deps_name: name of this subtree
405 deps: Deps object of given project
406 end_flag: True indicates this is the last event of this deps tree
407 """
408 assert self.snapshots
409 assert self.alive_children is None, ('events() is valid only after '
410 'no_more_snapshot() is invoked')
411
412 result = []
413
414 last_deps = None
415 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800416 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800417 last_deps = deps
418
419 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800420 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800421
422 for subtree in self.subtrees:
423 for event in subtree.events():
424 result.append(event)
425
426 result.sort()
427
428 return result
429
430 def iter_path_specs(self):
431 """Iterates snapshots of project dependency state.
432
433 Yields:
434 (timestamp, path_specs):
435 timestamp: time of snapshot
436 path_specs: dict of path_spec entries
437 """
438 forest = {}
439 # Group by timestamp
440 for timestamp, events in itertools.groupby(self.events(),
441 operator.itemgetter(0)):
442 # It's possible that one deps is removed and added at the same timestamp,
443 # i.e. modification, so use counter to track.
444 end_counter = collections.Counter()
445
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800446 for timestamp, entry, deps, end in events:
447 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800448 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800449 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800450 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800451 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800452
453 # Merge Deps at time `timestamp` into single path_specs.
454 path_specs = {}
455 for deps in forest.values():
456 for path, dep in deps.entries.items():
457 path_specs[path] = dep.as_path_spec()
458
459 yield timestamp, path_specs
460
461 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800462 for entry, count in end_counter.items():
463 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800464 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800465 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800466
467
468class DepsParser(object):
469 """Gclient DEPS file parser."""
470
471 def __init__(self, project_root, code_storage):
472 self.project_root = project_root
473 self.code_storage = code_storage
474
Kuang-che Wu067ff292019-02-14 18:16:23 +0800475 def load_single_deps(self, content):
476
477 def var_function(name):
478 return '{%s}' % name
479
480 global_scope = dict(Var=var_function)
481 local_scope = {}
482 try:
483 exec (content, global_scope, local_scope) # pylint: disable=exec-used
484 except SyntaxError:
485 raise
486
487 return local_scope
488
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800489 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
490 """Parses DEPS file without recursion.
491
492 Args:
493 content: file content of DEPS file
494 parent_vars: variables inherent from parent DEPS
495 parent_path: project path of parent DEPS file
496
497 Returns:
498 Deps object
499 """
500
Kuang-che Wu067ff292019-02-14 18:16:23 +0800501 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800502 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800503
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800504 local_scope.setdefault('vars', {})
505 if parent_vars:
506 local_scope['vars'].update(parent_vars)
507 deps.variables = local_scope['vars']
508
509 # Warnings for old usages which we don't support.
510 for name in deps.variables:
511 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
512 logger.warning('%s is deprecated and not supported recursion syntax',
513 name)
514 if 'deps_os' in local_scope:
515 logger.warning('deps_os is no longer supported')
516
517 for path, dep_entry in local_scope['deps'].items():
518 if local_scope.get('use_relative_paths', False):
519 path = os.path.join(parent_path, path)
520 path = path.format(**deps.variables)
521 dep = Dep(path, deps.variables, dep_entry)
522 if not dep.eval_condition():
523 continue
524
525 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
526 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800527 warning_key = ('dep_type', dep.dep_type, path)
528 if warning_key not in emitted_warnings:
529 emitted_warnings.add(warning_key)
530 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
531 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800532 continue
533
534 deps.entries[path] = dep
535
536 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800537 for recurse_entry in local_scope.get('recursedeps', []):
538 # Normalize entries.
539 if isinstance(recurse_entry, tuple):
540 path, deps_file = recurse_entry
541 else:
542 assert isinstance(path, str)
543 path, deps_file = recurse_entry, 'DEPS'
544
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800545 if local_scope.get('use_relative_paths', False):
546 path = os.path.join(parent_path, path)
547 path = path.format(**deps.variables)
548 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800549 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800550 deps.recursedeps = recursedeps
551
552 return deps
553
554 def construct_deps_tree(self,
555 tstree,
556 repo_url,
557 at,
558 after,
559 before,
560 parent_vars=None,
561 parent_path='',
562 deps_file='DEPS'):
563 """Processes DEPS recursively of given time period.
564
565 This method parses all commits of DEPS between time `after` and `before`,
566 segments recursive dependencies into subtrees if they are changed, and
567 processes subtrees recursively.
568
569 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
570
571 Args:
572 tstree: TimeSeriesTree object
573 repo_url: remote repo url
574 at: branch or git commit id
575 after: begin of period
576 before: end of period
577 parent_vars: DEPS variables inherit from parent DEPS (including
578 custom_vars)
579 parent_path: the path of parent project of current DEPS file
580 deps_file: filename of DEPS file, relative to the git repo, repo_rul
581 """
582 if '://' in repo_url:
583 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800584 if not os.path.exists(git_repo):
585 with locking.lock_file(
586 os.path.join(self.code_storage.cache_dir,
587 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
588 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800589 else:
590 git_repo = repo_url
591
592 if git_util.is_git_rev(at):
593 history = [
594 (after, at),
595 (before, at),
596 ]
597 else:
598 history = git_util.get_history(
599 git_repo,
600 deps_file,
601 branch=at,
602 after=after,
603 before=before,
604 padding=True)
605 assert history
606
607 # If not equal, it means the file was deleted but is still referenced by
608 # its parent.
609 assert history[-1][0] == before
610
611 # TODO(kcwu): optimization: history[-1] is unused
612 for timestamp, git_rev in history[:-1]:
613 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
614
615 deps = self.parse_single_deps(
616 content, parent_vars=parent_vars, parent_path=parent_path)
617 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
618
619 tstree.no_more_snapshot(deps)
620
621 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800622 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800623 path_spec = subtree.parent_deps.entries[path].as_path_spec()
624 self.construct_deps_tree(
625 subtree,
626 path_spec.repo_url,
627 path_spec.at,
628 subtree.start_time,
629 subtree.end_time,
630 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800631 parent_path=path,
632 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800633
634 def enumerate_path_specs(self, start_time, end_time, path):
635 tstree = TimeSeriesTree(None, path, start_time, end_time)
636 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
637 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800638
639
640class GclientCache(codechange.CodeStorage):
641 """Gclient git cache."""
642
643 def __init__(self, cache_dir):
644 self.cache_dir = cache_dir
645
646 def _url_to_cache_dir(self, url):
647 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
648 parsed = urlparse.urlparse(url)
649 norm_url = parsed.netloc + parsed.path
650 if norm_url.endswith('.git'):
651 norm_url = norm_url[:-len('.git')]
652 return norm_url.replace('-', '--').replace('/', '-').lower()
653
654 def cached_git_root(self, repo_url):
655 cache_path = self._url_to_cache_dir(repo_url)
656 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800657
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800658 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800659 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800660
661 projects[path] = repo_url
662
Kuang-che Wu067ff292019-02-14 18:16:23 +0800663 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800664
665 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800666 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800667
668 if path in projects:
669 del projects[path]
670
Kuang-che Wu067ff292019-02-14 18:16:23 +0800671 write_gclient_entries(project_root, projects)