blob: d580e4cce72545385762fe38267b97498a273e6d [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 Wua9639eb2019-03-19 17:15:08 +080073 # Work around gclient issue crbug/943430
74 # gclient rejected to sync if there are untracked symlink even with --force
75 for path in [
76 'src/chromeos/assistant/libassistant/src/deps',
77 'src/chromeos/assistant/libassistant/src/libassistant',
78 ]:
79 if os.path.islink(os.path.join(gclient_dir, path)):
80 os.unlink(os.path.join(gclient_dir, path))
81
82 cmd = [
83 'gclient',
84 'sync',
85 '--jobs=%d' % jobs,
86 '--delete_unversioned_trees',
87 # --force is necessary because runhook may generate some untracked files.
88 '--force',
89 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080090 if with_branch_heads:
91 cmd.append('--with_branch_heads')
92 if with_tags:
93 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080094
95 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
96 # reasons, it will leave annoying lock files on disk and thus unfriendly to
97 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
98 # this function) and bypass gclient's.
99 if ignore_locks:
100 cmd.append('--ignore_locks')
101
Kuang-che Wu067ff292019-02-14 18:16:23 +0800102 try:
103 old_projects = load_gclient_entries(gclient_dir)
104 except IOError:
105 old_projects = {}
106
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800107 util.check_call(*cmd, cwd=gclient_dir)
108
Kuang-che Wu067ff292019-02-14 18:16:23 +0800109 # Remove dead .git folder after sync.
110 # Ideally, this should be handled by gclient but sometimes gclient didn't
111 # (crbug/930047).
112 new_projects = load_gclient_entries(gclient_dir)
113 for path in old_projects:
114 if path in new_projects:
115 continue
116 old_git_dir = os.path.join(gclient_dir, path)
117 if os.path.exists(old_git_dir):
118 logger.warning(
119 '%s was removed from .gclient_entries but %s still exists; remove it',
120 path, old_git_dir)
121 shutil.rmtree(old_git_dir)
122
123
124def load_gclient_entries(gclient_dir):
125 """Loads .gclient_entries."""
126 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
127 scope = {}
128 exec open(repo_project_list) in scope # pylint: disable=exec-used
Kuang-che Wu058ac042019-03-18 14:14:53 +0800129 entries = scope.get('entries', {})
130
131 # normalize path: remove trailing slash
132 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
133
134 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800135
136
137def write_gclient_entries(gclient_dir, projects):
138 """Writes .gclient_entries."""
139 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
140 content = 'entries = {\n'
141 for item in sorted(projects.items()):
142 path, repo_url = map(pprint.pformat, item)
143 content += ' %s: %s,\n' % (path, repo_url)
144 content += '}\n'
145 with open(repo_project_list, 'w') as f:
146 f.write(content)
147
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800148
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800149def mirror(code_storage, repo_url):
150 """Mirror git repo.
151
152 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
153
154 Args:
155 code_storage: CodeStorage object
156 repo_url: remote repo url
157 """
158 logger.info('mirror %s', repo_url)
159 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
160 git_root = code_storage.cached_git_root(repo_url)
161 assert not os.path.exists(git_root)
162
163 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
164
165 # These config parameters are copied from gclient.
166 git_util.config(tmp_dir, 'gc.autodetach', '0')
167 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
168 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
169 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
170 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
171 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
172 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
173 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
174 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
175 '+refs/branch-heads/*:refs/branch-heads/*',
176 r'\+refs/branch-heads/\*:.*')
177
178 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
179 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
180 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
181
182 # Rename to correct name atomically.
183 os.rename(tmp_dir, git_root)
184
185
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800186# Copied from depot_tools' gclient.py
187_PLATFORM_MAPPING = {
188 'cygwin': 'win',
189 'darwin': 'mac',
190 'linux2': 'linux',
191 'win32': 'win',
192 'aix6': 'aix',
193}
194
195
196def _detect_host_os():
197 return _PLATFORM_MAPPING[sys.platform]
198
199
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800200class Dep(object):
201 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800202
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800203 One Dep object means one subproject inside DEPS file. It recorded what to
204 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800205
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800206 Attributes:
207 path: subproject path, relative to project root
208 variables: the variables of the containing DEPS file; these variables will
209 be applied to fields of this object (like 'url' and 'condition') and
210 children projects.
211 condition: whether to checkout this subproject
212 dep_type: 'git' or 'cipd'
213 url: if dep_type='git', the url of remote repo and associated branch/commit
214 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800215 """
216
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800217 def __init__(self, path, variables, entry):
218 self.path = path
219 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800220
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800221 self.url = None # only valid for dep_type='git'
222 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800223
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800224 if isinstance(entry, str):
225 self.dep_type = 'git'
226 self.url = entry
227 self.condition = None
228 else:
229 self.dep_type = entry.get('dep_type', 'git')
230 self.condition = entry.get('condition')
231 if self.dep_type == 'git':
232 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800233 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800234 assert self.dep_type == 'cipd'
235 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800236
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800237 if self.dep_type == 'git':
238 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800239
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800240 def __eq__(self, rhs):
241 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800242
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800243 def __ne__(self, rhs):
244 return not self.__eq__(rhs)
245
246 def as_path_spec(self):
247 assert self.dep_type == 'git'
248
249 if '@' in self.url:
250 repo_url, at = self.url.split('@')
251 else:
252 # If the dependency is not pinned, the default is master branch.
253 repo_url, at = self.url, 'master'
254 return codechange.PathSpec(self.path, repo_url, at)
255
256 def eval_condition(self):
257 """Evaluate condition for DEPS parsing.
258
259 Returns:
260 eval result
261 """
262 if not self.condition:
263 return True
264
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800265 # Currently, we only support chromeos as target_os.
266 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800267 # We don't specify `target_os_only`, so `unix` will be considered by
268 # gclient as well.
269 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800270
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800271 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800272 'checkout_android': 'android' in target_os,
273 'checkout_chromeos': 'chromeos' in target_os,
274 'checkout_fuchsia': 'fuchsia' in target_os,
275 'checkout_ios': 'ios' in target_os,
276 'checkout_linux': 'unix' in target_os,
277 'checkout_mac': 'mac' in target_os,
278 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800279 # default cpu: x64
280 'checkout_arm64': False,
281 'checkout_arm': False,
282 'checkout_mips': False,
283 'checkout_ppc': False,
284 'checkout_s390': False,
285 'checkout_x64': True,
286 'checkout_x86': False,
287 'host_os': _detect_host_os(),
288 'False': False,
289 'None': None,
290 'True': True,
291 }
292 vars_dict.update(self.variables)
293 # pylint: disable=eval-used
294 return eval(self.condition, vars_dict)
295
296
297class Deps(object):
298 """DEPS parsed result.
299
300 Attributes:
301 variables: 'vars' dict in DEPS file; these variables will be applied
302 recursively to children.
303 entries: dict of Dep objects
304 recursedeps: list of recursive projects
305 """
306
307 def __init__(self):
308 self.variables = {}
309 self.entries = {}
310 self.recursedeps = []
311
312
313class TimeSeriesTree(object):
314 """Data structure for generating snapshots of historical dependency tree.
315
316 This is a tree structure with time information. Each tree node represents not
317 only typical tree data and tree children information, but also historical
318 value of those tree data and tree children.
319
320 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
321 represent a DEPS file. The caller will add_snapshot() to add parsed result of
322 historical DEPS instances. After that, the tree root of this class can
323 reconstruct the every historical moment of the project dependency state.
324
325 This class is slight abstraction of git_util.get_history_recursively() to
326 support more than single git repo and be version control system independent.
327 """
328
329 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
330
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800331 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800332 """TimeSeriesTree constructor.
333
334 Args:
335 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800336 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800337 start_time: start time
338 end_time: end time
339 """
340 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800341 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800342 self.snapshots = {}
343 self.start_time = start_time
344 self.end_time = end_time
345
346 # Intermediate dict to keep track alive children for the time being.
347 # Maintained by add_snapshot() and no_more_snapshot().
348 self.alive_children = {}
349
350 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800351 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800352 # once in this list because they are removed and added back to the DEPS
353 # file.
354 self.subtrees = []
355
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800356 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800357 """Compares subtree of two Deps.
358
359 Args:
360 deps_a: Deps object
361 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800362 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800363
364 Returns:
365 True if the said subtree of these two Deps equal
366 """
367 # Need to compare variables because they may influence subtree parsing
368 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800369 path = child_entry[0]
370 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800371 deps_a.variables == deps_b.variables)
372
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800373 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800374 """Adds parsed DEPS result and children.
375
376 For example, if a given DEPS file has N revisions between start_time and
377 end_time, the caller should call this method N times to feed all parsed
378 results in order (timestamp increasing).
379
380 Args:
381 timestamp: timestamp of `deps`
382 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800383 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800384 """
385 assert timestamp not in self.snapshots
386 self.snapshots[timestamp] = deps
387
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800388 for child_entry in set(self.alive_children.keys() + children_entries):
389 # `child_entry` is added at `timestamp`
390 if child_entry not in self.alive_children:
391 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800392
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800393 # `child_entry` is removed at `timestamp`
394 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800395 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800396 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
397 self.alive_children[child_entry][0], timestamp))
398 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800399
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800400 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800401 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800402 last_deps = self.alive_children[child_entry][1]
403 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800404 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800405 TimeSeriesTree(last_deps, child_entry,
406 self.alive_children[child_entry][0], timestamp))
407 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800408
409 def no_more_snapshot(self, deps):
410 """Indicates all snapshots are added.
411
412 add_snapshot() should not be invoked after no_more_snapshot().
413 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800414 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800415 if timestamp == self.end_time:
416 continue
417 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800418 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800419 self.alive_children = None
420
421 def events(self):
422 """Gets children added/removed events of this subtree.
423
424 Returns:
425 list of (timestamp, deps_name, deps, end_flag):
426 timestamp: timestamp of event
427 deps_name: name of this subtree
428 deps: Deps object of given project
429 end_flag: True indicates this is the last event of this deps tree
430 """
431 assert self.snapshots
432 assert self.alive_children is None, ('events() is valid only after '
433 'no_more_snapshot() is invoked')
434
435 result = []
436
437 last_deps = None
438 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800439 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800440 last_deps = deps
441
442 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800443 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800444
445 for subtree in self.subtrees:
446 for event in subtree.events():
447 result.append(event)
448
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800449 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800450
451 return result
452
453 def iter_path_specs(self):
454 """Iterates snapshots of project dependency state.
455
456 Yields:
457 (timestamp, path_specs):
458 timestamp: time of snapshot
459 path_specs: dict of path_spec entries
460 """
461 forest = {}
462 # Group by timestamp
463 for timestamp, events in itertools.groupby(self.events(),
464 operator.itemgetter(0)):
465 # It's possible that one deps is removed and added at the same timestamp,
466 # i.e. modification, so use counter to track.
467 end_counter = collections.Counter()
468
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800469 for timestamp, entry, deps, end in events:
470 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800471 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800472 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800473 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800474 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800475
476 # Merge Deps at time `timestamp` into single path_specs.
477 path_specs = {}
478 for deps in forest.values():
479 for path, dep in deps.entries.items():
480 path_specs[path] = dep.as_path_spec()
481
482 yield timestamp, path_specs
483
484 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800485 for entry, count in end_counter.items():
486 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800487 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800488 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800489
490
491class DepsParser(object):
492 """Gclient DEPS file parser."""
493
494 def __init__(self, project_root, code_storage):
495 self.project_root = project_root
496 self.code_storage = code_storage
497
Kuang-che Wu067ff292019-02-14 18:16:23 +0800498 def load_single_deps(self, content):
499
500 def var_function(name):
501 return '{%s}' % name
502
503 global_scope = dict(Var=var_function)
504 local_scope = {}
505 try:
506 exec (content, global_scope, local_scope) # pylint: disable=exec-used
507 except SyntaxError:
508 raise
509
510 return local_scope
511
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800512 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
513 """Parses DEPS file without recursion.
514
515 Args:
516 content: file content of DEPS file
517 parent_vars: variables inherent from parent DEPS
518 parent_path: project path of parent DEPS file
519
520 Returns:
521 Deps object
522 """
523
Kuang-che Wu067ff292019-02-14 18:16:23 +0800524 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800525 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800526
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800527 local_scope.setdefault('vars', {})
528 if parent_vars:
529 local_scope['vars'].update(parent_vars)
530 deps.variables = local_scope['vars']
531
532 # Warnings for old usages which we don't support.
533 for name in deps.variables:
534 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
535 logger.warning('%s is deprecated and not supported recursion syntax',
536 name)
537 if 'deps_os' in local_scope:
538 logger.warning('deps_os is no longer supported')
539
540 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800541 path = path.format(**deps.variables)
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)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800544 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800545 dep = Dep(path, deps.variables, dep_entry)
546 if not dep.eval_condition():
547 continue
548
549 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
550 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800551 warning_key = ('dep_type', dep.dep_type, path)
552 if warning_key not in emitted_warnings:
553 emitted_warnings.add(warning_key)
554 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
555 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800556 continue
557
558 deps.entries[path] = dep
559
560 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800561 for recurse_entry in local_scope.get('recursedeps', []):
562 # Normalize entries.
563 if isinstance(recurse_entry, tuple):
564 path, deps_file = recurse_entry
565 else:
566 assert isinstance(path, str)
567 path, deps_file = recurse_entry, 'DEPS'
568
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800569 if local_scope.get('use_relative_paths', False):
570 path = os.path.join(parent_path, path)
571 path = path.format(**deps.variables)
572 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800573 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800574 deps.recursedeps = recursedeps
575
576 return deps
577
578 def construct_deps_tree(self,
579 tstree,
580 repo_url,
581 at,
582 after,
583 before,
584 parent_vars=None,
585 parent_path='',
586 deps_file='DEPS'):
587 """Processes DEPS recursively of given time period.
588
589 This method parses all commits of DEPS between time `after` and `before`,
590 segments recursive dependencies into subtrees if they are changed, and
591 processes subtrees recursively.
592
593 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
594
595 Args:
596 tstree: TimeSeriesTree object
597 repo_url: remote repo url
598 at: branch or git commit id
599 after: begin of period
600 before: end of period
601 parent_vars: DEPS variables inherit from parent DEPS (including
602 custom_vars)
603 parent_path: the path of parent project of current DEPS file
604 deps_file: filename of DEPS file, relative to the git repo, repo_rul
605 """
606 if '://' in repo_url:
607 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800608 if not os.path.exists(git_repo):
609 with locking.lock_file(
610 os.path.join(self.code_storage.cache_dir,
611 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
612 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800613 else:
614 git_repo = repo_url
615
616 if git_util.is_git_rev(at):
617 history = [
618 (after, at),
619 (before, at),
620 ]
621 else:
622 history = git_util.get_history(
623 git_repo,
624 deps_file,
625 branch=at,
626 after=after,
627 before=before,
628 padding=True)
629 assert history
630
631 # If not equal, it means the file was deleted but is still referenced by
632 # its parent.
633 assert history[-1][0] == before
634
635 # TODO(kcwu): optimization: history[-1] is unused
636 for timestamp, git_rev in history[:-1]:
637 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
638
639 deps = self.parse_single_deps(
640 content, parent_vars=parent_vars, parent_path=parent_path)
641 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
642
643 tstree.no_more_snapshot(deps)
644
645 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800646 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800647 path_spec = subtree.parent_deps.entries[path].as_path_spec()
648 self.construct_deps_tree(
649 subtree,
650 path_spec.repo_url,
651 path_spec.at,
652 subtree.start_time,
653 subtree.end_time,
654 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800655 parent_path=path,
656 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800657
658 def enumerate_path_specs(self, start_time, end_time, path):
659 tstree = TimeSeriesTree(None, path, start_time, end_time)
660 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
661 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800662
663
664class GclientCache(codechange.CodeStorage):
665 """Gclient git cache."""
666
667 def __init__(self, cache_dir):
668 self.cache_dir = cache_dir
669
670 def _url_to_cache_dir(self, url):
671 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
672 parsed = urlparse.urlparse(url)
673 norm_url = parsed.netloc + parsed.path
674 if norm_url.endswith('.git'):
675 norm_url = norm_url[:-len('.git')]
676 return norm_url.replace('-', '--').replace('/', '-').lower()
677
678 def cached_git_root(self, repo_url):
679 cache_path = self._url_to_cache_dir(repo_url)
680 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800681
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800682 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800683 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800684
685 projects[path] = repo_url
686
Kuang-che Wu067ff292019-02-14 18:16:23 +0800687 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800688
689 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800690 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800691
692 if path in projects:
693 del projects[path]
694
Kuang-che Wu067ff292019-02-14 18:16:23 +0800695 write_gclient_entries(project_root, projects)