blob: 3e95a74f415a9e9a9f3b427b8fd3ccab403a4dd9 [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.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800246 # We don't specify `target_os_only`, so `unix` will be considered by
247 # gclient as well.
248 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800249
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800250 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800251 'checkout_android': 'android' in target_os,
252 'checkout_chromeos': 'chromeos' in target_os,
253 'checkout_fuchsia': 'fuchsia' in target_os,
254 'checkout_ios': 'ios' in target_os,
255 'checkout_linux': 'unix' in target_os,
256 'checkout_mac': 'mac' in target_os,
257 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800258 # default cpu: x64
259 'checkout_arm64': False,
260 'checkout_arm': False,
261 'checkout_mips': False,
262 'checkout_ppc': False,
263 'checkout_s390': False,
264 'checkout_x64': True,
265 'checkout_x86': False,
266 'host_os': _detect_host_os(),
267 'False': False,
268 'None': None,
269 'True': True,
270 }
271 vars_dict.update(self.variables)
272 # pylint: disable=eval-used
273 return eval(self.condition, vars_dict)
274
275
276class Deps(object):
277 """DEPS parsed result.
278
279 Attributes:
280 variables: 'vars' dict in DEPS file; these variables will be applied
281 recursively to children.
282 entries: dict of Dep objects
283 recursedeps: list of recursive projects
284 """
285
286 def __init__(self):
287 self.variables = {}
288 self.entries = {}
289 self.recursedeps = []
290
291
292class TimeSeriesTree(object):
293 """Data structure for generating snapshots of historical dependency tree.
294
295 This is a tree structure with time information. Each tree node represents not
296 only typical tree data and tree children information, but also historical
297 value of those tree data and tree children.
298
299 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
300 represent a DEPS file. The caller will add_snapshot() to add parsed result of
301 historical DEPS instances. After that, the tree root of this class can
302 reconstruct the every historical moment of the project dependency state.
303
304 This class is slight abstraction of git_util.get_history_recursively() to
305 support more than single git repo and be version control system independent.
306 """
307
308 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
309
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800310 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800311 """TimeSeriesTree constructor.
312
313 Args:
314 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800315 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800316 start_time: start time
317 end_time: end time
318 """
319 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800320 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800321 self.snapshots = {}
322 self.start_time = start_time
323 self.end_time = end_time
324
325 # Intermediate dict to keep track alive children for the time being.
326 # Maintained by add_snapshot() and no_more_snapshot().
327 self.alive_children = {}
328
329 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800330 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800331 # once in this list because they are removed and added back to the DEPS
332 # file.
333 self.subtrees = []
334
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800335 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800336 """Compares subtree of two Deps.
337
338 Args:
339 deps_a: Deps object
340 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800341 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800342
343 Returns:
344 True if the said subtree of these two Deps equal
345 """
346 # Need to compare variables because they may influence subtree parsing
347 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800348 path = child_entry[0]
349 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800350 deps_a.variables == deps_b.variables)
351
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800352 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800353 """Adds parsed DEPS result and children.
354
355 For example, if a given DEPS file has N revisions between start_time and
356 end_time, the caller should call this method N times to feed all parsed
357 results in order (timestamp increasing).
358
359 Args:
360 timestamp: timestamp of `deps`
361 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800362 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800363 """
364 assert timestamp not in self.snapshots
365 self.snapshots[timestamp] = deps
366
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800367 for child_entry in set(self.alive_children.keys() + children_entries):
368 # `child_entry` is added at `timestamp`
369 if child_entry not in self.alive_children:
370 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800371
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800372 # `child_entry` is removed at `timestamp`
373 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800374 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800375 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
376 self.alive_children[child_entry][0], timestamp))
377 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800378
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800379 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800380 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800381 last_deps = self.alive_children[child_entry][1]
382 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800383 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800384 TimeSeriesTree(last_deps, child_entry,
385 self.alive_children[child_entry][0], timestamp))
386 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800387
388 def no_more_snapshot(self, deps):
389 """Indicates all snapshots are added.
390
391 add_snapshot() should not be invoked after no_more_snapshot().
392 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800393 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800394 if timestamp == self.end_time:
395 continue
396 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800397 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800398 self.alive_children = None
399
400 def events(self):
401 """Gets children added/removed events of this subtree.
402
403 Returns:
404 list of (timestamp, deps_name, deps, end_flag):
405 timestamp: timestamp of event
406 deps_name: name of this subtree
407 deps: Deps object of given project
408 end_flag: True indicates this is the last event of this deps tree
409 """
410 assert self.snapshots
411 assert self.alive_children is None, ('events() is valid only after '
412 'no_more_snapshot() is invoked')
413
414 result = []
415
416 last_deps = None
417 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800418 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800419 last_deps = deps
420
421 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800422 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800423
424 for subtree in self.subtrees:
425 for event in subtree.events():
426 result.append(event)
427
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800428 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800429
430 return result
431
432 def iter_path_specs(self):
433 """Iterates snapshots of project dependency state.
434
435 Yields:
436 (timestamp, path_specs):
437 timestamp: time of snapshot
438 path_specs: dict of path_spec entries
439 """
440 forest = {}
441 # Group by timestamp
442 for timestamp, events in itertools.groupby(self.events(),
443 operator.itemgetter(0)):
444 # It's possible that one deps is removed and added at the same timestamp,
445 # i.e. modification, so use counter to track.
446 end_counter = collections.Counter()
447
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800448 for timestamp, entry, deps, end in events:
449 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800450 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800451 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800452 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800453 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800454
455 # Merge Deps at time `timestamp` into single path_specs.
456 path_specs = {}
457 for deps in forest.values():
458 for path, dep in deps.entries.items():
459 path_specs[path] = dep.as_path_spec()
460
461 yield timestamp, path_specs
462
463 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800464 for entry, count in end_counter.items():
465 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800466 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800467 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800468
469
470class DepsParser(object):
471 """Gclient DEPS file parser."""
472
473 def __init__(self, project_root, code_storage):
474 self.project_root = project_root
475 self.code_storage = code_storage
476
Kuang-che Wu067ff292019-02-14 18:16:23 +0800477 def load_single_deps(self, content):
478
479 def var_function(name):
480 return '{%s}' % name
481
482 global_scope = dict(Var=var_function)
483 local_scope = {}
484 try:
485 exec (content, global_scope, local_scope) # pylint: disable=exec-used
486 except SyntaxError:
487 raise
488
489 return local_scope
490
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800491 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
492 """Parses DEPS file without recursion.
493
494 Args:
495 content: file content of DEPS file
496 parent_vars: variables inherent from parent DEPS
497 parent_path: project path of parent DEPS file
498
499 Returns:
500 Deps object
501 """
502
Kuang-che Wu067ff292019-02-14 18:16:23 +0800503 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800504 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800505
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800506 local_scope.setdefault('vars', {})
507 if parent_vars:
508 local_scope['vars'].update(parent_vars)
509 deps.variables = local_scope['vars']
510
511 # Warnings for old usages which we don't support.
512 for name in deps.variables:
513 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
514 logger.warning('%s is deprecated and not supported recursion syntax',
515 name)
516 if 'deps_os' in local_scope:
517 logger.warning('deps_os is no longer supported')
518
519 for path, dep_entry in local_scope['deps'].items():
520 if local_scope.get('use_relative_paths', False):
521 path = os.path.join(parent_path, path)
522 path = path.format(**deps.variables)
523 dep = Dep(path, deps.variables, dep_entry)
524 if not dep.eval_condition():
525 continue
526
527 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
528 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800529 warning_key = ('dep_type', dep.dep_type, path)
530 if warning_key not in emitted_warnings:
531 emitted_warnings.add(warning_key)
532 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
533 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800534 continue
535
536 deps.entries[path] = dep
537
538 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800539 for recurse_entry in local_scope.get('recursedeps', []):
540 # Normalize entries.
541 if isinstance(recurse_entry, tuple):
542 path, deps_file = recurse_entry
543 else:
544 assert isinstance(path, str)
545 path, deps_file = recurse_entry, 'DEPS'
546
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800547 if local_scope.get('use_relative_paths', False):
548 path = os.path.join(parent_path, path)
549 path = path.format(**deps.variables)
550 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800551 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800552 deps.recursedeps = recursedeps
553
554 return deps
555
556 def construct_deps_tree(self,
557 tstree,
558 repo_url,
559 at,
560 after,
561 before,
562 parent_vars=None,
563 parent_path='',
564 deps_file='DEPS'):
565 """Processes DEPS recursively of given time period.
566
567 This method parses all commits of DEPS between time `after` and `before`,
568 segments recursive dependencies into subtrees if they are changed, and
569 processes subtrees recursively.
570
571 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
572
573 Args:
574 tstree: TimeSeriesTree object
575 repo_url: remote repo url
576 at: branch or git commit id
577 after: begin of period
578 before: end of period
579 parent_vars: DEPS variables inherit from parent DEPS (including
580 custom_vars)
581 parent_path: the path of parent project of current DEPS file
582 deps_file: filename of DEPS file, relative to the git repo, repo_rul
583 """
584 if '://' in repo_url:
585 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800586 if not os.path.exists(git_repo):
587 with locking.lock_file(
588 os.path.join(self.code_storage.cache_dir,
589 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
590 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800591 else:
592 git_repo = repo_url
593
594 if git_util.is_git_rev(at):
595 history = [
596 (after, at),
597 (before, at),
598 ]
599 else:
600 history = git_util.get_history(
601 git_repo,
602 deps_file,
603 branch=at,
604 after=after,
605 before=before,
606 padding=True)
607 assert history
608
609 # If not equal, it means the file was deleted but is still referenced by
610 # its parent.
611 assert history[-1][0] == before
612
613 # TODO(kcwu): optimization: history[-1] is unused
614 for timestamp, git_rev in history[:-1]:
615 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
616
617 deps = self.parse_single_deps(
618 content, parent_vars=parent_vars, parent_path=parent_path)
619 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
620
621 tstree.no_more_snapshot(deps)
622
623 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800624 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800625 path_spec = subtree.parent_deps.entries[path].as_path_spec()
626 self.construct_deps_tree(
627 subtree,
628 path_spec.repo_url,
629 path_spec.at,
630 subtree.start_time,
631 subtree.end_time,
632 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800633 parent_path=path,
634 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800635
636 def enumerate_path_specs(self, start_time, end_time, path):
637 tstree = TimeSeriesTree(None, path, start_time, end_time)
638 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
639 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800640
641
642class GclientCache(codechange.CodeStorage):
643 """Gclient git cache."""
644
645 def __init__(self, cache_dir):
646 self.cache_dir = cache_dir
647
648 def _url_to_cache_dir(self, url):
649 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
650 parsed = urlparse.urlparse(url)
651 norm_url = parsed.netloc + parsed.path
652 if norm_url.endswith('.git'):
653 norm_url = norm_url[:-len('.git')]
654 return norm_url.replace('-', '--').replace('/', '-').lower()
655
656 def cached_git_root(self, repo_url):
657 cache_path = self._url_to_cache_dir(repo_url)
658 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800659
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800660 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800661 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800662
663 projects[path] = repo_url
664
Kuang-che Wu067ff292019-02-14 18:16:23 +0800665 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800666
667 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800668 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800669
670 if path in projects:
671 del projects[path]
672
Kuang-che Wu067ff292019-02-14 18:16:23 +0800673 write_gclient_entries(project_root, projects)