blob: 0ce67d6296b52a904ac51617690b769007e87aa9 [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)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800117 if not os.path.exists(old_git_dir):
118 continue
119
120 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800121 logger.warning(
122 '%s was removed from .gclient_entries but %s still exists; remove it',
123 path, old_git_dir)
124 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800125 else:
126 logger.warning(
127 '%s was removed from .gclient_entries but %s still exists;'
128 ' keep it because it is not git root', path, old_git_dir)
129
130
131def runhook(gclient_dir, jobs=8):
132 """Simply wrapper of `gclient runhook`.
133
134 Args:
135 gclient_dir: root directory of gclient project
136 jobs: how many workers running in parallel
137 """
138 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800139
140
141def load_gclient_entries(gclient_dir):
142 """Loads .gclient_entries."""
143 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
144 scope = {}
145 exec open(repo_project_list) in scope # pylint: disable=exec-used
Kuang-che Wu058ac042019-03-18 14:14:53 +0800146 entries = scope.get('entries', {})
147
148 # normalize path: remove trailing slash
149 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
150
151 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800152
153
154def write_gclient_entries(gclient_dir, projects):
155 """Writes .gclient_entries."""
156 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
157 content = 'entries = {\n'
158 for item in sorted(projects.items()):
159 path, repo_url = map(pprint.pformat, item)
160 content += ' %s: %s,\n' % (path, repo_url)
161 content += '}\n'
162 with open(repo_project_list, 'w') as f:
163 f.write(content)
164
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800165
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800166def mirror(code_storage, repo_url):
167 """Mirror git repo.
168
169 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
170
171 Args:
172 code_storage: CodeStorage object
173 repo_url: remote repo url
174 """
175 logger.info('mirror %s', repo_url)
176 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
177 git_root = code_storage.cached_git_root(repo_url)
178 assert not os.path.exists(git_root)
179
180 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
181
182 # These config parameters are copied from gclient.
183 git_util.config(tmp_dir, 'gc.autodetach', '0')
184 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
185 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
186 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
187 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
188 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
189 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
190 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
191 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
192 '+refs/branch-heads/*:refs/branch-heads/*',
193 r'\+refs/branch-heads/\*:.*')
194
195 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
196 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
197 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
198
199 # Rename to correct name atomically.
200 os.rename(tmp_dir, git_root)
201
202
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800203# Copied from depot_tools' gclient.py
204_PLATFORM_MAPPING = {
205 'cygwin': 'win',
206 'darwin': 'mac',
207 'linux2': 'linux',
208 'win32': 'win',
209 'aix6': 'aix',
210}
211
212
213def _detect_host_os():
214 return _PLATFORM_MAPPING[sys.platform]
215
216
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800217class Dep(object):
218 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800219
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800220 One Dep object means one subproject inside DEPS file. It recorded what to
221 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800222
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800223 Attributes:
224 path: subproject path, relative to project root
225 variables: the variables of the containing DEPS file; these variables will
226 be applied to fields of this object (like 'url' and 'condition') and
227 children projects.
228 condition: whether to checkout this subproject
229 dep_type: 'git' or 'cipd'
230 url: if dep_type='git', the url of remote repo and associated branch/commit
231 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800232 """
233
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800234 def __init__(self, path, variables, entry):
235 self.path = path
236 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800237
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800238 self.url = None # only valid for dep_type='git'
239 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800240
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800241 if isinstance(entry, str):
242 self.dep_type = 'git'
243 self.url = entry
244 self.condition = None
245 else:
246 self.dep_type = entry.get('dep_type', 'git')
247 self.condition = entry.get('condition')
248 if self.dep_type == 'git':
249 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800250 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800251 assert self.dep_type == 'cipd'
252 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800253
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800254 if self.dep_type == 'git':
255 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800256
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800257 def __eq__(self, rhs):
258 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800259
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800260 def __ne__(self, rhs):
261 return not self.__eq__(rhs)
262
263 def as_path_spec(self):
264 assert self.dep_type == 'git'
265
266 if '@' in self.url:
267 repo_url, at = self.url.split('@')
268 else:
269 # If the dependency is not pinned, the default is master branch.
270 repo_url, at = self.url, 'master'
271 return codechange.PathSpec(self.path, repo_url, at)
272
273 def eval_condition(self):
274 """Evaluate condition for DEPS parsing.
275
276 Returns:
277 eval result
278 """
279 if not self.condition:
280 return True
281
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800282 # Currently, we only support chromeos as target_os.
283 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800284 # We don't specify `target_os_only`, so `unix` will be considered by
285 # gclient as well.
286 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800287
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800288 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800289 'checkout_android': 'android' in target_os,
290 'checkout_chromeos': 'chromeos' in target_os,
291 'checkout_fuchsia': 'fuchsia' in target_os,
292 'checkout_ios': 'ios' in target_os,
293 'checkout_linux': 'unix' in target_os,
294 'checkout_mac': 'mac' in target_os,
295 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800296 # default cpu: x64
297 'checkout_arm64': False,
298 'checkout_arm': False,
299 'checkout_mips': False,
300 'checkout_ppc': False,
301 'checkout_s390': False,
302 'checkout_x64': True,
303 'checkout_x86': False,
304 'host_os': _detect_host_os(),
305 'False': False,
306 'None': None,
307 'True': True,
308 }
309 vars_dict.update(self.variables)
310 # pylint: disable=eval-used
311 return eval(self.condition, vars_dict)
312
313
314class Deps(object):
315 """DEPS parsed result.
316
317 Attributes:
318 variables: 'vars' dict in DEPS file; these variables will be applied
319 recursively to children.
320 entries: dict of Dep objects
321 recursedeps: list of recursive projects
322 """
323
324 def __init__(self):
325 self.variables = {}
326 self.entries = {}
327 self.recursedeps = []
328
329
330class TimeSeriesTree(object):
331 """Data structure for generating snapshots of historical dependency tree.
332
333 This is a tree structure with time information. Each tree node represents not
334 only typical tree data and tree children information, but also historical
335 value of those tree data and tree children.
336
337 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
338 represent a DEPS file. The caller will add_snapshot() to add parsed result of
339 historical DEPS instances. After that, the tree root of this class can
340 reconstruct the every historical moment of the project dependency state.
341
342 This class is slight abstraction of git_util.get_history_recursively() to
343 support more than single git repo and be version control system independent.
344 """
345
346 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
347
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800348 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800349 """TimeSeriesTree constructor.
350
351 Args:
352 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800353 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800354 start_time: start time
355 end_time: end time
356 """
357 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800358 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800359 self.snapshots = {}
360 self.start_time = start_time
361 self.end_time = end_time
362
363 # Intermediate dict to keep track alive children for the time being.
364 # Maintained by add_snapshot() and no_more_snapshot().
365 self.alive_children = {}
366
367 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800368 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800369 # once in this list because they are removed and added back to the DEPS
370 # file.
371 self.subtrees = []
372
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800373 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800374 """Compares subtree of two Deps.
375
376 Args:
377 deps_a: Deps object
378 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800379 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800380
381 Returns:
382 True if the said subtree of these two Deps equal
383 """
384 # Need to compare variables because they may influence subtree parsing
385 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800386 path = child_entry[0]
387 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800388 deps_a.variables == deps_b.variables)
389
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800390 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800391 """Adds parsed DEPS result and children.
392
393 For example, if a given DEPS file has N revisions between start_time and
394 end_time, the caller should call this method N times to feed all parsed
395 results in order (timestamp increasing).
396
397 Args:
398 timestamp: timestamp of `deps`
399 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800400 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800401 """
402 assert timestamp not in self.snapshots
403 self.snapshots[timestamp] = deps
404
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800405 for child_entry in set(self.alive_children.keys() + children_entries):
406 # `child_entry` is added at `timestamp`
407 if child_entry not in self.alive_children:
408 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800409
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800410 # `child_entry` is removed at `timestamp`
411 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800412 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800413 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
414 self.alive_children[child_entry][0], timestamp))
415 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800416
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800417 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800418 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800419 last_deps = self.alive_children[child_entry][1]
420 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800421 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800422 TimeSeriesTree(last_deps, child_entry,
423 self.alive_children[child_entry][0], timestamp))
424 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800425
426 def no_more_snapshot(self, deps):
427 """Indicates all snapshots are added.
428
429 add_snapshot() should not be invoked after no_more_snapshot().
430 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800431 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800432 if timestamp == self.end_time:
433 continue
434 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800435 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800436 self.alive_children = None
437
438 def events(self):
439 """Gets children added/removed events of this subtree.
440
441 Returns:
442 list of (timestamp, deps_name, deps, end_flag):
443 timestamp: timestamp of event
444 deps_name: name of this subtree
445 deps: Deps object of given project
446 end_flag: True indicates this is the last event of this deps tree
447 """
448 assert self.snapshots
449 assert self.alive_children is None, ('events() is valid only after '
450 'no_more_snapshot() is invoked')
451
452 result = []
453
454 last_deps = None
455 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800456 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800457 last_deps = deps
458
459 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800460 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800461
462 for subtree in self.subtrees:
463 for event in subtree.events():
464 result.append(event)
465
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800466 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800467
468 return result
469
470 def iter_path_specs(self):
471 """Iterates snapshots of project dependency state.
472
473 Yields:
474 (timestamp, path_specs):
475 timestamp: time of snapshot
476 path_specs: dict of path_spec entries
477 """
478 forest = {}
479 # Group by timestamp
480 for timestamp, events in itertools.groupby(self.events(),
481 operator.itemgetter(0)):
482 # It's possible that one deps is removed and added at the same timestamp,
483 # i.e. modification, so use counter to track.
484 end_counter = collections.Counter()
485
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800486 for timestamp, entry, deps, end in events:
487 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800488 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800489 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800490 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800491 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800492
493 # Merge Deps at time `timestamp` into single path_specs.
494 path_specs = {}
495 for deps in forest.values():
496 for path, dep in deps.entries.items():
497 path_specs[path] = dep.as_path_spec()
498
499 yield timestamp, path_specs
500
501 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800502 for entry, count in end_counter.items():
503 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800504 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800505 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800506
507
508class DepsParser(object):
509 """Gclient DEPS file parser."""
510
511 def __init__(self, project_root, code_storage):
512 self.project_root = project_root
513 self.code_storage = code_storage
514
Kuang-che Wu067ff292019-02-14 18:16:23 +0800515 def load_single_deps(self, content):
516
517 def var_function(name):
518 return '{%s}' % name
519
520 global_scope = dict(Var=var_function)
521 local_scope = {}
522 try:
523 exec (content, global_scope, local_scope) # pylint: disable=exec-used
524 except SyntaxError:
525 raise
526
527 return local_scope
528
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800529 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
530 """Parses DEPS file without recursion.
531
532 Args:
533 content: file content of DEPS file
534 parent_vars: variables inherent from parent DEPS
535 parent_path: project path of parent DEPS file
536
537 Returns:
538 Deps object
539 """
540
Kuang-che Wu067ff292019-02-14 18:16:23 +0800541 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800542 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800543
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800544 local_scope.setdefault('vars', {})
545 if parent_vars:
546 local_scope['vars'].update(parent_vars)
547 deps.variables = local_scope['vars']
548
549 # Warnings for old usages which we don't support.
550 for name in deps.variables:
551 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
552 logger.warning('%s is deprecated and not supported recursion syntax',
553 name)
554 if 'deps_os' in local_scope:
555 logger.warning('deps_os is no longer supported')
556
557 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800558 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800559 if local_scope.get('use_relative_paths', False):
560 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800561 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800562 dep = Dep(path, deps.variables, dep_entry)
563 if not dep.eval_condition():
564 continue
565
566 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
567 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800568 warning_key = ('dep_type', dep.dep_type, path)
569 if warning_key not in emitted_warnings:
570 emitted_warnings.add(warning_key)
571 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
572 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800573 continue
574
575 deps.entries[path] = dep
576
577 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800578 for recurse_entry in local_scope.get('recursedeps', []):
579 # Normalize entries.
580 if isinstance(recurse_entry, tuple):
581 path, deps_file = recurse_entry
582 else:
583 assert isinstance(path, str)
584 path, deps_file = recurse_entry, 'DEPS'
585
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800586 if local_scope.get('use_relative_paths', False):
587 path = os.path.join(parent_path, path)
588 path = path.format(**deps.variables)
589 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800590 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800591 deps.recursedeps = recursedeps
592
593 return deps
594
595 def construct_deps_tree(self,
596 tstree,
597 repo_url,
598 at,
599 after,
600 before,
601 parent_vars=None,
602 parent_path='',
603 deps_file='DEPS'):
604 """Processes DEPS recursively of given time period.
605
606 This method parses all commits of DEPS between time `after` and `before`,
607 segments recursive dependencies into subtrees if they are changed, and
608 processes subtrees recursively.
609
610 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
611
612 Args:
613 tstree: TimeSeriesTree object
614 repo_url: remote repo url
615 at: branch or git commit id
616 after: begin of period
617 before: end of period
618 parent_vars: DEPS variables inherit from parent DEPS (including
619 custom_vars)
620 parent_path: the path of parent project of current DEPS file
621 deps_file: filename of DEPS file, relative to the git repo, repo_rul
622 """
623 if '://' in repo_url:
624 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800625 if not os.path.exists(git_repo):
626 with locking.lock_file(
627 os.path.join(self.code_storage.cache_dir,
628 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
629 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800630 else:
631 git_repo = repo_url
632
633 if git_util.is_git_rev(at):
634 history = [
635 (after, at),
636 (before, at),
637 ]
638 else:
639 history = git_util.get_history(
640 git_repo,
641 deps_file,
642 branch=at,
643 after=after,
644 before=before,
645 padding=True)
646 assert history
647
648 # If not equal, it means the file was deleted but is still referenced by
649 # its parent.
650 assert history[-1][0] == before
651
652 # TODO(kcwu): optimization: history[-1] is unused
653 for timestamp, git_rev in history[:-1]:
654 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
655
656 deps = self.parse_single_deps(
657 content, parent_vars=parent_vars, parent_path=parent_path)
658 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
659
660 tstree.no_more_snapshot(deps)
661
662 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800663 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800664 path_spec = subtree.parent_deps.entries[path].as_path_spec()
665 self.construct_deps_tree(
666 subtree,
667 path_spec.repo_url,
668 path_spec.at,
669 subtree.start_time,
670 subtree.end_time,
671 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800672 parent_path=path,
673 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800674
675 def enumerate_path_specs(self, start_time, end_time, path):
676 tstree = TimeSeriesTree(None, path, start_time, end_time)
677 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
678 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800679
680
681class GclientCache(codechange.CodeStorage):
682 """Gclient git cache."""
683
684 def __init__(self, cache_dir):
685 self.cache_dir = cache_dir
686
687 def _url_to_cache_dir(self, url):
688 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
689 parsed = urlparse.urlparse(url)
690 norm_url = parsed.netloc + parsed.path
691 if norm_url.endswith('.git'):
692 norm_url = norm_url[:-len('.git')]
693 return norm_url.replace('-', '--').replace('/', '-').lower()
694
695 def cached_git_root(self, repo_url):
696 cache_path = self._url_to_cache_dir(repo_url)
697 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800698
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800699 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800700 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800701
702 projects[path] = repo_url
703
Kuang-che Wu067ff292019-02-14 18:16:23 +0800704 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800705
706 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800707 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800708
709 if path in projects:
710 del projects[path]
711
Kuang-che Wu067ff292019-02-14 18:16:23 +0800712 write_gclient_entries(project_root, projects)