blob: 88c49528559987824a40a036a2f24b2c1a0fe3e8 [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
Kuang-che Wu058ac042019-03-18 14:14:53 +0800113 entries = scope.get('entries', {})
114
115 # normalize path: remove trailing slash
116 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
117
118 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800119
120
121def write_gclient_entries(gclient_dir, projects):
122 """Writes .gclient_entries."""
123 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
124 content = 'entries = {\n'
125 for item in sorted(projects.items()):
126 path, repo_url = map(pprint.pformat, item)
127 content += ' %s: %s,\n' % (path, repo_url)
128 content += '}\n'
129 with open(repo_project_list, 'w') as f:
130 f.write(content)
131
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800132
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800133def mirror(code_storage, repo_url):
134 """Mirror git repo.
135
136 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
137
138 Args:
139 code_storage: CodeStorage object
140 repo_url: remote repo url
141 """
142 logger.info('mirror %s', repo_url)
143 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
144 git_root = code_storage.cached_git_root(repo_url)
145 assert not os.path.exists(git_root)
146
147 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
148
149 # These config parameters are copied from gclient.
150 git_util.config(tmp_dir, 'gc.autodetach', '0')
151 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
152 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
153 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
154 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
155 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
156 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
157 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
158 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
159 '+refs/branch-heads/*:refs/branch-heads/*',
160 r'\+refs/branch-heads/\*:.*')
161
162 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
163 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
164 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
165
166 # Rename to correct name atomically.
167 os.rename(tmp_dir, git_root)
168
169
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800170# Copied from depot_tools' gclient.py
171_PLATFORM_MAPPING = {
172 'cygwin': 'win',
173 'darwin': 'mac',
174 'linux2': 'linux',
175 'win32': 'win',
176 'aix6': 'aix',
177}
178
179
180def _detect_host_os():
181 return _PLATFORM_MAPPING[sys.platform]
182
183
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800184class Dep(object):
185 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800186
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800187 One Dep object means one subproject inside DEPS file. It recorded what to
188 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800189
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800190 Attributes:
191 path: subproject path, relative to project root
192 variables: the variables of the containing DEPS file; these variables will
193 be applied to fields of this object (like 'url' and 'condition') and
194 children projects.
195 condition: whether to checkout this subproject
196 dep_type: 'git' or 'cipd'
197 url: if dep_type='git', the url of remote repo and associated branch/commit
198 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800199 """
200
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800201 def __init__(self, path, variables, entry):
202 self.path = path
203 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800204
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800205 self.url = None # only valid for dep_type='git'
206 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800207
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800208 if isinstance(entry, str):
209 self.dep_type = 'git'
210 self.url = entry
211 self.condition = None
212 else:
213 self.dep_type = entry.get('dep_type', 'git')
214 self.condition = entry.get('condition')
215 if self.dep_type == 'git':
216 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800217 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800218 assert self.dep_type == 'cipd'
219 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800220
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800221 if self.dep_type == 'git':
222 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800223
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800224 def __eq__(self, rhs):
225 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800226
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800227 def __ne__(self, rhs):
228 return not self.__eq__(rhs)
229
230 def as_path_spec(self):
231 assert self.dep_type == 'git'
232
233 if '@' in self.url:
234 repo_url, at = self.url.split('@')
235 else:
236 # If the dependency is not pinned, the default is master branch.
237 repo_url, at = self.url, 'master'
238 return codechange.PathSpec(self.path, repo_url, at)
239
240 def eval_condition(self):
241 """Evaluate condition for DEPS parsing.
242
243 Returns:
244 eval result
245 """
246 if not self.condition:
247 return True
248
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800249 # Currently, we only support chromeos as target_os.
250 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800251 # We don't specify `target_os_only`, so `unix` will be considered by
252 # gclient as well.
253 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800254
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800255 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800256 'checkout_android': 'android' in target_os,
257 'checkout_chromeos': 'chromeos' in target_os,
258 'checkout_fuchsia': 'fuchsia' in target_os,
259 'checkout_ios': 'ios' in target_os,
260 'checkout_linux': 'unix' in target_os,
261 'checkout_mac': 'mac' in target_os,
262 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800263 # default cpu: x64
264 'checkout_arm64': False,
265 'checkout_arm': False,
266 'checkout_mips': False,
267 'checkout_ppc': False,
268 'checkout_s390': False,
269 'checkout_x64': True,
270 'checkout_x86': False,
271 'host_os': _detect_host_os(),
272 'False': False,
273 'None': None,
274 'True': True,
275 }
276 vars_dict.update(self.variables)
277 # pylint: disable=eval-used
278 return eval(self.condition, vars_dict)
279
280
281class Deps(object):
282 """DEPS parsed result.
283
284 Attributes:
285 variables: 'vars' dict in DEPS file; these variables will be applied
286 recursively to children.
287 entries: dict of Dep objects
288 recursedeps: list of recursive projects
289 """
290
291 def __init__(self):
292 self.variables = {}
293 self.entries = {}
294 self.recursedeps = []
295
296
297class TimeSeriesTree(object):
298 """Data structure for generating snapshots of historical dependency tree.
299
300 This is a tree structure with time information. Each tree node represents not
301 only typical tree data and tree children information, but also historical
302 value of those tree data and tree children.
303
304 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
305 represent a DEPS file. The caller will add_snapshot() to add parsed result of
306 historical DEPS instances. After that, the tree root of this class can
307 reconstruct the every historical moment of the project dependency state.
308
309 This class is slight abstraction of git_util.get_history_recursively() to
310 support more than single git repo and be version control system independent.
311 """
312
313 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
314
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800315 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800316 """TimeSeriesTree constructor.
317
318 Args:
319 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800320 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800321 start_time: start time
322 end_time: end time
323 """
324 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800325 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800326 self.snapshots = {}
327 self.start_time = start_time
328 self.end_time = end_time
329
330 # Intermediate dict to keep track alive children for the time being.
331 # Maintained by add_snapshot() and no_more_snapshot().
332 self.alive_children = {}
333
334 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800335 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800336 # once in this list because they are removed and added back to the DEPS
337 # file.
338 self.subtrees = []
339
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800340 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800341 """Compares subtree of two Deps.
342
343 Args:
344 deps_a: Deps object
345 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800346 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800347
348 Returns:
349 True if the said subtree of these two Deps equal
350 """
351 # Need to compare variables because they may influence subtree parsing
352 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800353 path = child_entry[0]
354 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800355 deps_a.variables == deps_b.variables)
356
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800357 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800358 """Adds parsed DEPS result and children.
359
360 For example, if a given DEPS file has N revisions between start_time and
361 end_time, the caller should call this method N times to feed all parsed
362 results in order (timestamp increasing).
363
364 Args:
365 timestamp: timestamp of `deps`
366 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800367 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800368 """
369 assert timestamp not in self.snapshots
370 self.snapshots[timestamp] = deps
371
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800372 for child_entry in set(self.alive_children.keys() + children_entries):
373 # `child_entry` is added at `timestamp`
374 if child_entry not in self.alive_children:
375 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800376
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800377 # `child_entry` is removed at `timestamp`
378 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800379 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800380 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
381 self.alive_children[child_entry][0], timestamp))
382 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800383
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800384 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800385 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800386 last_deps = self.alive_children[child_entry][1]
387 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800388 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800389 TimeSeriesTree(last_deps, child_entry,
390 self.alive_children[child_entry][0], timestamp))
391 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800392
393 def no_more_snapshot(self, deps):
394 """Indicates all snapshots are added.
395
396 add_snapshot() should not be invoked after no_more_snapshot().
397 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800398 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800399 if timestamp == self.end_time:
400 continue
401 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800402 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800403 self.alive_children = None
404
405 def events(self):
406 """Gets children added/removed events of this subtree.
407
408 Returns:
409 list of (timestamp, deps_name, deps, end_flag):
410 timestamp: timestamp of event
411 deps_name: name of this subtree
412 deps: Deps object of given project
413 end_flag: True indicates this is the last event of this deps tree
414 """
415 assert self.snapshots
416 assert self.alive_children is None, ('events() is valid only after '
417 'no_more_snapshot() is invoked')
418
419 result = []
420
421 last_deps = None
422 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800423 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800424 last_deps = deps
425
426 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800427 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800428
429 for subtree in self.subtrees:
430 for event in subtree.events():
431 result.append(event)
432
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800433 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800434
435 return result
436
437 def iter_path_specs(self):
438 """Iterates snapshots of project dependency state.
439
440 Yields:
441 (timestamp, path_specs):
442 timestamp: time of snapshot
443 path_specs: dict of path_spec entries
444 """
445 forest = {}
446 # Group by timestamp
447 for timestamp, events in itertools.groupby(self.events(),
448 operator.itemgetter(0)):
449 # It's possible that one deps is removed and added at the same timestamp,
450 # i.e. modification, so use counter to track.
451 end_counter = collections.Counter()
452
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800453 for timestamp, entry, deps, end in events:
454 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800455 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800456 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800457 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800458 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800459
460 # Merge Deps at time `timestamp` into single path_specs.
461 path_specs = {}
462 for deps in forest.values():
463 for path, dep in deps.entries.items():
464 path_specs[path] = dep.as_path_spec()
465
466 yield timestamp, path_specs
467
468 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800469 for entry, count in end_counter.items():
470 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800471 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800472 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800473
474
475class DepsParser(object):
476 """Gclient DEPS file parser."""
477
478 def __init__(self, project_root, code_storage):
479 self.project_root = project_root
480 self.code_storage = code_storage
481
Kuang-che Wu067ff292019-02-14 18:16:23 +0800482 def load_single_deps(self, content):
483
484 def var_function(name):
485 return '{%s}' % name
486
487 global_scope = dict(Var=var_function)
488 local_scope = {}
489 try:
490 exec (content, global_scope, local_scope) # pylint: disable=exec-used
491 except SyntaxError:
492 raise
493
494 return local_scope
495
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800496 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
497 """Parses DEPS file without recursion.
498
499 Args:
500 content: file content of DEPS file
501 parent_vars: variables inherent from parent DEPS
502 parent_path: project path of parent DEPS file
503
504 Returns:
505 Deps object
506 """
507
Kuang-che Wu067ff292019-02-14 18:16:23 +0800508 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800509 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800510
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800511 local_scope.setdefault('vars', {})
512 if parent_vars:
513 local_scope['vars'].update(parent_vars)
514 deps.variables = local_scope['vars']
515
516 # Warnings for old usages which we don't support.
517 for name in deps.variables:
518 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
519 logger.warning('%s is deprecated and not supported recursion syntax',
520 name)
521 if 'deps_os' in local_scope:
522 logger.warning('deps_os is no longer supported')
523
524 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800525 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800526 if local_scope.get('use_relative_paths', False):
527 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800528 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800529 dep = Dep(path, deps.variables, dep_entry)
530 if not dep.eval_condition():
531 continue
532
533 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
534 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800535 warning_key = ('dep_type', dep.dep_type, path)
536 if warning_key not in emitted_warnings:
537 emitted_warnings.add(warning_key)
538 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
539 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800540 continue
541
542 deps.entries[path] = dep
543
544 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800545 for recurse_entry in local_scope.get('recursedeps', []):
546 # Normalize entries.
547 if isinstance(recurse_entry, tuple):
548 path, deps_file = recurse_entry
549 else:
550 assert isinstance(path, str)
551 path, deps_file = recurse_entry, 'DEPS'
552
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800553 if local_scope.get('use_relative_paths', False):
554 path = os.path.join(parent_path, path)
555 path = path.format(**deps.variables)
556 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800557 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800558 deps.recursedeps = recursedeps
559
560 return deps
561
562 def construct_deps_tree(self,
563 tstree,
564 repo_url,
565 at,
566 after,
567 before,
568 parent_vars=None,
569 parent_path='',
570 deps_file='DEPS'):
571 """Processes DEPS recursively of given time period.
572
573 This method parses all commits of DEPS between time `after` and `before`,
574 segments recursive dependencies into subtrees if they are changed, and
575 processes subtrees recursively.
576
577 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
578
579 Args:
580 tstree: TimeSeriesTree object
581 repo_url: remote repo url
582 at: branch or git commit id
583 after: begin of period
584 before: end of period
585 parent_vars: DEPS variables inherit from parent DEPS (including
586 custom_vars)
587 parent_path: the path of parent project of current DEPS file
588 deps_file: filename of DEPS file, relative to the git repo, repo_rul
589 """
590 if '://' in repo_url:
591 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800592 if not os.path.exists(git_repo):
593 with locking.lock_file(
594 os.path.join(self.code_storage.cache_dir,
595 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
596 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800597 else:
598 git_repo = repo_url
599
600 if git_util.is_git_rev(at):
601 history = [
602 (after, at),
603 (before, at),
604 ]
605 else:
606 history = git_util.get_history(
607 git_repo,
608 deps_file,
609 branch=at,
610 after=after,
611 before=before,
612 padding=True)
613 assert history
614
615 # If not equal, it means the file was deleted but is still referenced by
616 # its parent.
617 assert history[-1][0] == before
618
619 # TODO(kcwu): optimization: history[-1] is unused
620 for timestamp, git_rev in history[:-1]:
621 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
622
623 deps = self.parse_single_deps(
624 content, parent_vars=parent_vars, parent_path=parent_path)
625 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
626
627 tstree.no_more_snapshot(deps)
628
629 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800630 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800631 path_spec = subtree.parent_deps.entries[path].as_path_spec()
632 self.construct_deps_tree(
633 subtree,
634 path_spec.repo_url,
635 path_spec.at,
636 subtree.start_time,
637 subtree.end_time,
638 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800639 parent_path=path,
640 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800641
642 def enumerate_path_specs(self, start_time, end_time, path):
643 tstree = TimeSeriesTree(None, path, start_time, end_time)
644 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
645 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800646
647
648class GclientCache(codechange.CodeStorage):
649 """Gclient git cache."""
650
651 def __init__(self, cache_dir):
652 self.cache_dir = cache_dir
653
654 def _url_to_cache_dir(self, url):
655 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
656 parsed = urlparse.urlparse(url)
657 norm_url = parsed.netloc + parsed.path
658 if norm_url.endswith('.git'):
659 norm_url = norm_url[:-len('.git')]
660 return norm_url.replace('-', '--').replace('/', '-').lower()
661
662 def cached_git_root(self, repo_url):
663 cache_path = self._url_to_cache_dir(repo_url)
664 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800665
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800666 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800667 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800668
669 projects[path] = repo_url
670
Kuang-che Wu067ff292019-02-14 18:16:23 +0800671 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800672
673 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800674 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800675
676 if path in projects:
677 del projects[path]
678
Kuang-che Wu067ff292019-02-14 18:16:23 +0800679 write_gclient_entries(project_root, projects)