blob: e3cbece25e5b616bec653f9c84714c8ea5012b14 [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 Wua7ddf9b2019-11-25 18:59:57 +080017
Kuang-che Wud2646f42019-11-27 18:31:08 +080018import six
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080019from six.moves import urllib
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080020
21from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080022from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080023from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080024from bisect_kit import util
25
26logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080027emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080028
29
Kuang-che Wu41e8b592018-09-25 17:01:30 +080030def config(gclient_dir,
31 url=None,
32 cache_dir=None,
33 deps_file=None,
34 custom_var=None,
35 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080036 """Simply wrapper of `gclient config`.
37
38 Args:
39 gclient_dir: root directory of gclient project
40 url: URL of gclient configuration files
41 cache_dir: gclient's git cache folder
42 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080043 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080044 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080045 """
46 cmd = ['gclient', 'config']
47 if deps_file:
48 cmd += ['--deps-file', deps_file]
49 if cache_dir:
50 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080051 if custom_var:
52 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080053 if spec:
54 cmd += ['--spec', spec]
55 if url:
56 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080057
58 util.check_call(*cmd, cwd=gclient_dir)
59
60
Kuang-che Wudc714412018-10-17 16:06:39 +080061def sync(gclient_dir,
62 with_branch_heads=False,
63 with_tags=False,
64 ignore_locks=False,
65 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080066 """Simply wrapper of `gclient sync`.
67
68 Args:
69 gclient_dir: root directory of gclient project
70 with_branch_heads: whether to clone git `branch_heads` refspecs
71 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080072 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080073 jobs: how many workers running in parallel
74 """
Kuang-che Wua9639eb2019-03-19 17:15:08 +080075 # Work around gclient issue crbug/943430
76 # gclient rejected to sync if there are untracked symlink even with --force
77 for path in [
78 'src/chromeos/assistant/libassistant/src/deps',
79 'src/chromeos/assistant/libassistant/src/libassistant',
80 ]:
81 if os.path.islink(os.path.join(gclient_dir, path)):
82 os.unlink(os.path.join(gclient_dir, path))
83
84 cmd = [
85 'gclient',
86 'sync',
87 '--jobs=%d' % jobs,
88 '--delete_unversioned_trees',
89 # --force is necessary because runhook may generate some untracked files.
90 '--force',
91 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080092 if with_branch_heads:
93 cmd.append('--with_branch_heads')
94 if with_tags:
95 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080096
97 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
98 # reasons, it will leave annoying lock files on disk and thus unfriendly to
99 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
100 # this function) and bypass gclient's.
101 if ignore_locks:
102 cmd.append('--ignore_locks')
103
Kuang-che Wu067ff292019-02-14 18:16:23 +0800104 try:
105 old_projects = load_gclient_entries(gclient_dir)
106 except IOError:
107 old_projects = {}
108
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800109 util.check_call(*cmd, cwd=gclient_dir)
110
Kuang-che Wu067ff292019-02-14 18:16:23 +0800111 # Remove dead .git folder after sync.
112 # Ideally, this should be handled by gclient but sometimes gclient didn't
113 # (crbug/930047).
114 new_projects = load_gclient_entries(gclient_dir)
115 for path in old_projects:
116 if path in new_projects:
117 continue
118 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800119 if not os.path.exists(old_git_dir):
120 continue
121
122 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800123 logger.warning(
124 '%s was removed from .gclient_entries but %s still exists; remove it',
125 path, old_git_dir)
126 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800127 else:
128 logger.warning(
129 '%s was removed from .gclient_entries but %s still exists;'
130 ' keep it because it is not git root', path, old_git_dir)
131
132
133def runhook(gclient_dir, jobs=8):
134 """Simply wrapper of `gclient runhook`.
135
136 Args:
137 gclient_dir: root directory of gclient project
138 jobs: how many workers running in parallel
139 """
140 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800141
142
143def load_gclient_entries(gclient_dir):
144 """Loads .gclient_entries."""
145 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
146 scope = {}
Kuang-che Wud2646f42019-11-27 18:31:08 +0800147 with open(repo_project_list) as f:
148 code = compile(f.read(), repo_project_list, 'exec')
149 six.exec_(code, scope)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800150 entries = scope.get('entries', {})
151
152 # normalize path: remove trailing slash
153 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
154
155 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800156
157
158def write_gclient_entries(gclient_dir, projects):
159 """Writes .gclient_entries."""
160 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
161 content = 'entries = {\n'
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800162 for path, repo_url in sorted(projects.items()):
163 content += ' %s: %s,\n' % (pprint.pformat(path), pprint.pformat(repo_url))
Kuang-che Wu067ff292019-02-14 18:16:23 +0800164 content += '}\n'
165 with open(repo_project_list, 'w') as f:
166 f.write(content)
167
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800168
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800169def mirror(code_storage, repo_url):
170 """Mirror git repo.
171
172 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
173
174 Args:
175 code_storage: CodeStorage object
176 repo_url: remote repo url
177 """
178 logger.info('mirror %s', repo_url)
179 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
180 git_root = code_storage.cached_git_root(repo_url)
181 assert not os.path.exists(git_root)
182
183 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
184
185 # These config parameters are copied from gclient.
186 git_util.config(tmp_dir, 'gc.autodetach', '0')
187 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
188 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
189 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
190 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
191 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
192 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
193 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
194 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
195 '+refs/branch-heads/*:refs/branch-heads/*',
196 r'\+refs/branch-heads/\*:.*')
197
198 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
199 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
200 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
201
202 # Rename to correct name atomically.
203 os.rename(tmp_dir, git_root)
204
205
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800206# Copied from depot_tools' gclient.py
207_PLATFORM_MAPPING = {
208 'cygwin': 'win',
209 'darwin': 'mac',
210 'linux2': 'linux',
Kuang-che Wud2646f42019-11-27 18:31:08 +0800211 'linux': 'linux',
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800212 'win32': 'win',
213 'aix6': 'aix',
214}
215
216
217def _detect_host_os():
218 return _PLATFORM_MAPPING[sys.platform]
219
220
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800221class Dep:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800222 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800223
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800224 One Dep object means one subproject inside DEPS file. It recorded what to
225 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800226
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800227 Attributes:
228 path: subproject path, relative to project root
229 variables: the variables of the containing DEPS file; these variables will
230 be applied to fields of this object (like 'url' and 'condition') and
231 children projects.
232 condition: whether to checkout this subproject
233 dep_type: 'git' or 'cipd'
234 url: if dep_type='git', the url of remote repo and associated branch/commit
235 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800236 """
237
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800238 def __init__(self, path, variables, entry):
239 self.path = path
240 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800241
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800242 self.url = None # only valid for dep_type='git'
243 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800244
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800245 if isinstance(entry, str):
246 self.dep_type = 'git'
247 self.url = entry
248 self.condition = None
249 else:
250 self.dep_type = entry.get('dep_type', 'git')
251 self.condition = entry.get('condition')
252 if self.dep_type == 'git':
253 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800254 else:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800255 assert self.dep_type == 'cipd', 'unknown dep_type:' + self.dep_type
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800256 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800257
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800258 if self.dep_type == 'git':
259 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800260
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800261 def __eq__(self, rhs):
262 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800263
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800264 def __ne__(self, rhs):
265 return not self.__eq__(rhs)
266
267 def as_path_spec(self):
268 assert self.dep_type == 'git'
269
270 if '@' in self.url:
271 repo_url, at = self.url.split('@')
272 else:
273 # If the dependency is not pinned, the default is master branch.
274 repo_url, at = self.url, 'master'
275 return codechange.PathSpec(self.path, repo_url, at)
276
277 def eval_condition(self):
278 """Evaluate condition for DEPS parsing.
279
280 Returns:
281 eval result
282 """
283 if not self.condition:
284 return True
285
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800286 # Currently, we only support chromeos as target_os.
287 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800288 # We don't specify `target_os_only`, so `unix` will be considered by
289 # gclient as well.
290 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800291
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800292 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800293 'checkout_android': 'android' in target_os,
294 'checkout_chromeos': 'chromeos' in target_os,
295 'checkout_fuchsia': 'fuchsia' in target_os,
296 'checkout_ios': 'ios' in target_os,
297 'checkout_linux': 'unix' in target_os,
298 'checkout_mac': 'mac' in target_os,
299 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800300 # default cpu: x64
301 'checkout_arm64': False,
302 'checkout_arm': False,
303 'checkout_mips': False,
304 'checkout_ppc': False,
305 'checkout_s390': False,
306 'checkout_x64': True,
307 'checkout_x86': False,
308 'host_os': _detect_host_os(),
309 'False': False,
310 'None': None,
311 'True': True,
312 }
313 vars_dict.update(self.variables)
314 # pylint: disable=eval-used
315 return eval(self.condition, vars_dict)
316
317
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800318class Deps:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800319 """DEPS parsed result.
320
321 Attributes:
322 variables: 'vars' dict in DEPS file; these variables will be applied
323 recursively to children.
324 entries: dict of Dep objects
325 recursedeps: list of recursive projects
326 """
327
328 def __init__(self):
329 self.variables = {}
330 self.entries = {}
331 self.recursedeps = []
332
333
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800334class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800335 """Data structure for generating snapshots of historical dependency tree.
336
337 This is a tree structure with time information. Each tree node represents not
338 only typical tree data and tree children information, but also historical
339 value of those tree data and tree children.
340
341 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
342 represent a DEPS file. The caller will add_snapshot() to add parsed result of
343 historical DEPS instances. After that, the tree root of this class can
344 reconstruct the every historical moment of the project dependency state.
345
346 This class is slight abstraction of git_util.get_history_recursively() to
347 support more than single git repo and be version control system independent.
348 """
349
350 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
351
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800352 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800353 """TimeSeriesTree constructor.
354
355 Args:
356 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800357 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800358 start_time: start time
359 end_time: end time
360 """
361 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800362 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800363 self.snapshots = {}
364 self.start_time = start_time
365 self.end_time = end_time
366
367 # Intermediate dict to keep track alive children for the time being.
368 # Maintained by add_snapshot() and no_more_snapshot().
369 self.alive_children = {}
370
371 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800372 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800373 # once in this list because they are removed and added back to the DEPS
374 # file.
375 self.subtrees = []
376
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800377 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800378 """Compares subtree of two Deps.
379
380 Args:
381 deps_a: Deps object
382 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800383 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800384
385 Returns:
386 True if the said subtree of these two Deps equal
387 """
388 # Need to compare variables because they may influence subtree parsing
389 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800390 path = child_entry[0]
391 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800392 deps_a.variables == deps_b.variables)
393
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800394 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800395 """Adds parsed DEPS result and children.
396
397 For example, if a given DEPS file has N revisions between start_time and
398 end_time, the caller should call this method N times to feed all parsed
399 results in order (timestamp increasing).
400
401 Args:
402 timestamp: timestamp of `deps`
403 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800404 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800405 """
406 assert timestamp not in self.snapshots
407 self.snapshots[timestamp] = deps
408
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800409 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800410 # `child_entry` is added at `timestamp`
411 if child_entry not in self.alive_children:
412 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800413
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800414 # `child_entry` is removed at `timestamp`
415 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800416 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800417 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
418 self.alive_children[child_entry][0], timestamp))
419 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800420
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800421 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800422 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800423 last_deps = self.alive_children[child_entry][1]
424 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800425 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800426 TimeSeriesTree(last_deps, child_entry,
427 self.alive_children[child_entry][0], timestamp))
428 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800429
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800430 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800431 """Indicates all snapshots are added.
432
433 add_snapshot() should not be invoked after no_more_snapshot().
434 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800435 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800436 if timestamp == self.end_time:
437 continue
438 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800439 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800440 self.alive_children = None
441
442 def events(self):
443 """Gets children added/removed events of this subtree.
444
445 Returns:
446 list of (timestamp, deps_name, deps, end_flag):
447 timestamp: timestamp of event
448 deps_name: name of this subtree
449 deps: Deps object of given project
450 end_flag: True indicates this is the last event of this deps tree
451 """
452 assert self.snapshots
453 assert self.alive_children is None, ('events() is valid only after '
454 'no_more_snapshot() is invoked')
455
456 result = []
457
458 last_deps = None
459 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800460 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800461 last_deps = deps
462
463 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800464 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800465
466 for subtree in self.subtrees:
467 for event in subtree.events():
468 result.append(event)
469
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800470 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800471
472 return result
473
474 def iter_path_specs(self):
475 """Iterates snapshots of project dependency state.
476
477 Yields:
478 (timestamp, path_specs):
479 timestamp: time of snapshot
480 path_specs: dict of path_spec entries
481 """
482 forest = {}
483 # Group by timestamp
484 for timestamp, events in itertools.groupby(self.events(),
485 operator.itemgetter(0)):
486 # It's possible that one deps is removed and added at the same timestamp,
487 # i.e. modification, so use counter to track.
488 end_counter = collections.Counter()
489
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800490 for timestamp, entry, deps, end in events:
491 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800492 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800493 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800494 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800495 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800496
497 # Merge Deps at time `timestamp` into single path_specs.
498 path_specs = {}
499 for deps in forest.values():
500 for path, dep in deps.entries.items():
501 path_specs[path] = dep.as_path_spec()
502
503 yield timestamp, path_specs
504
505 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800506 for entry, count in end_counter.items():
507 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800508 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800509 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800510
511
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800512class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800513 """Gclient DEPS file parser."""
514
515 def __init__(self, project_root, code_storage):
516 self.project_root = project_root
517 self.code_storage = code_storage
518
Kuang-che Wu067ff292019-02-14 18:16:23 +0800519 def load_single_deps(self, content):
520
521 def var_function(name):
522 return '{%s}' % name
523
524 global_scope = dict(Var=var_function)
525 local_scope = {}
526 try:
Kuang-che Wud2646f42019-11-27 18:31:08 +0800527 six.exec_(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800528 except SyntaxError:
529 raise
530
531 return local_scope
532
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800533 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
534 """Parses DEPS file without recursion.
535
536 Args:
537 content: file content of DEPS file
538 parent_vars: variables inherent from parent DEPS
539 parent_path: project path of parent DEPS file
540
541 Returns:
542 Deps object
543 """
544
Kuang-che Wu067ff292019-02-14 18:16:23 +0800545 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800546 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800547
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800548 local_scope.setdefault('vars', {})
549 if parent_vars:
550 local_scope['vars'].update(parent_vars)
551 deps.variables = local_scope['vars']
552
553 # Warnings for old usages which we don't support.
554 for name in deps.variables:
555 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
556 logger.warning('%s is deprecated and not supported recursion syntax',
557 name)
558 if 'deps_os' in local_scope:
559 logger.warning('deps_os is no longer supported')
560
561 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800562 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800563 if local_scope.get('use_relative_paths', False):
564 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800565 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800566 dep = Dep(path, deps.variables, dep_entry)
567 if not dep.eval_condition():
568 continue
569
570 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
571 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800572 warning_key = ('dep_type', dep.dep_type, path)
573 if warning_key not in emitted_warnings:
574 emitted_warnings.add(warning_key)
575 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
576 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800577 continue
578
579 deps.entries[path] = dep
580
581 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800582 for recurse_entry in local_scope.get('recursedeps', []):
583 # Normalize entries.
584 if isinstance(recurse_entry, tuple):
585 path, deps_file = recurse_entry
586 else:
587 assert isinstance(path, str)
588 path, deps_file = recurse_entry, 'DEPS'
589
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800590 if local_scope.get('use_relative_paths', False):
591 path = os.path.join(parent_path, path)
592 path = path.format(**deps.variables)
593 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800594 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800595 deps.recursedeps = recursedeps
596
597 return deps
598
599 def construct_deps_tree(self,
600 tstree,
601 repo_url,
602 at,
603 after,
604 before,
605 parent_vars=None,
606 parent_path='',
607 deps_file='DEPS'):
608 """Processes DEPS recursively of given time period.
609
610 This method parses all commits of DEPS between time `after` and `before`,
611 segments recursive dependencies into subtrees if they are changed, and
612 processes subtrees recursively.
613
614 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
615
616 Args:
617 tstree: TimeSeriesTree object
618 repo_url: remote repo url
619 at: branch or git commit id
620 after: begin of period
621 before: end of period
622 parent_vars: DEPS variables inherit from parent DEPS (including
623 custom_vars)
624 parent_path: the path of parent project of current DEPS file
625 deps_file: filename of DEPS file, relative to the git repo, repo_rul
626 """
627 if '://' in repo_url:
628 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800629 if not os.path.exists(git_repo):
630 with locking.lock_file(
631 os.path.join(self.code_storage.cache_dir,
632 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
633 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800634 else:
635 git_repo = repo_url
636
637 if git_util.is_git_rev(at):
638 history = [
639 (after, at),
640 (before, at),
641 ]
642 else:
643 history = git_util.get_history(
644 git_repo,
645 deps_file,
646 branch=at,
647 after=after,
648 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800649 padding_begin=True,
650 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800651 assert history
652
653 # If not equal, it means the file was deleted but is still referenced by
654 # its parent.
655 assert history[-1][0] == before
656
657 # TODO(kcwu): optimization: history[-1] is unused
658 for timestamp, git_rev in history[:-1]:
659 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
660
661 deps = self.parse_single_deps(
662 content, parent_vars=parent_vars, parent_path=parent_path)
663 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
664
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800665 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800666
667 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800668 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800669 path_spec = subtree.parent_deps.entries[path].as_path_spec()
670 self.construct_deps_tree(
671 subtree,
672 path_spec.repo_url,
673 path_spec.at,
674 subtree.start_time,
675 subtree.end_time,
676 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800677 parent_path=path,
678 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800679
680 def enumerate_path_specs(self, start_time, end_time, path):
681 tstree = TimeSeriesTree(None, path, start_time, end_time)
682 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
683 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800684
685
686class GclientCache(codechange.CodeStorage):
687 """Gclient git cache."""
688
689 def __init__(self, cache_dir):
690 self.cache_dir = cache_dir
691
692 def _url_to_cache_dir(self, url):
693 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800694 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800695 norm_url = parsed.netloc + parsed.path
696 if norm_url.endswith('.git'):
697 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800698 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800699 return norm_url.replace('-', '--').replace('/', '-').lower()
700
701 def cached_git_root(self, repo_url):
702 cache_path = self._url_to_cache_dir(repo_url)
703 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800704
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800705 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800706 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800707
708 projects[path] = repo_url
709
Kuang-che Wu067ff292019-02-14 18:16:23 +0800710 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800711
712 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800713 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800714
715 if path in projects:
716 del projects[path]
717
Kuang-che Wu067ff292019-02-14 18:16:23 +0800718 write_gclient_entries(project_root, projects)