blob: 8877a48d3a9f76fa908d2f860f0f18f22efe521f [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
18from six.moves import urllib
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080019
20from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080021from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080022from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080023from bisect_kit import util
24
25logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080026emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080027
28
Kuang-che Wu41e8b592018-09-25 17:01:30 +080029def config(gclient_dir,
30 url=None,
31 cache_dir=None,
32 deps_file=None,
33 custom_var=None,
34 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080035 """Simply wrapper of `gclient config`.
36
37 Args:
38 gclient_dir: root directory of gclient project
39 url: URL of gclient configuration files
40 cache_dir: gclient's git cache folder
41 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080042 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080043 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080044 """
45 cmd = ['gclient', 'config']
46 if deps_file:
47 cmd += ['--deps-file', deps_file]
48 if cache_dir:
49 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080050 if custom_var:
51 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080052 if spec:
53 cmd += ['--spec', spec]
54 if url:
55 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080056
57 util.check_call(*cmd, cwd=gclient_dir)
58
59
Kuang-che Wudc714412018-10-17 16:06:39 +080060def sync(gclient_dir,
61 with_branch_heads=False,
62 with_tags=False,
63 ignore_locks=False,
64 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080065 """Simply wrapper of `gclient sync`.
66
67 Args:
68 gclient_dir: root directory of gclient project
69 with_branch_heads: whether to clone git `branch_heads` refspecs
70 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080071 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080072 jobs: how many workers running in parallel
73 """
Kuang-che Wua9639eb2019-03-19 17:15:08 +080074 # Work around gclient issue crbug/943430
75 # gclient rejected to sync if there are untracked symlink even with --force
76 for path in [
77 'src/chromeos/assistant/libassistant/src/deps',
78 'src/chromeos/assistant/libassistant/src/libassistant',
79 ]:
80 if os.path.islink(os.path.join(gclient_dir, path)):
81 os.unlink(os.path.join(gclient_dir, path))
82
83 cmd = [
84 'gclient',
85 'sync',
86 '--jobs=%d' % jobs,
87 '--delete_unversioned_trees',
88 # --force is necessary because runhook may generate some untracked files.
89 '--force',
90 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080091 if with_branch_heads:
92 cmd.append('--with_branch_heads')
93 if with_tags:
94 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080095
96 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
97 # reasons, it will leave annoying lock files on disk and thus unfriendly to
98 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
99 # this function) and bypass gclient's.
100 if ignore_locks:
101 cmd.append('--ignore_locks')
102
Kuang-che Wu067ff292019-02-14 18:16:23 +0800103 try:
104 old_projects = load_gclient_entries(gclient_dir)
105 except IOError:
106 old_projects = {}
107
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800108 util.check_call(*cmd, cwd=gclient_dir)
109
Kuang-che Wu067ff292019-02-14 18:16:23 +0800110 # Remove dead .git folder after sync.
111 # Ideally, this should be handled by gclient but sometimes gclient didn't
112 # (crbug/930047).
113 new_projects = load_gclient_entries(gclient_dir)
114 for path in old_projects:
115 if path in new_projects:
116 continue
117 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800118 if not os.path.exists(old_git_dir):
119 continue
120
121 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800122 logger.warning(
123 '%s was removed from .gclient_entries but %s still exists; remove it',
124 path, old_git_dir)
125 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800126 else:
127 logger.warning(
128 '%s was removed from .gclient_entries but %s still exists;'
129 ' keep it because it is not git root', path, old_git_dir)
130
131
132def runhook(gclient_dir, jobs=8):
133 """Simply wrapper of `gclient runhook`.
134
135 Args:
136 gclient_dir: root directory of gclient project
137 jobs: how many workers running in parallel
138 """
139 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800140
141
142def load_gclient_entries(gclient_dir):
143 """Loads .gclient_entries."""
144 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
145 scope = {}
146 exec open(repo_project_list) in scope # pylint: disable=exec-used
Kuang-che Wu058ac042019-03-18 14:14:53 +0800147 entries = scope.get('entries', {})
148
149 # normalize path: remove trailing slash
150 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
151
152 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800153
154
155def write_gclient_entries(gclient_dir, projects):
156 """Writes .gclient_entries."""
157 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
158 content = 'entries = {\n'
159 for item in sorted(projects.items()):
160 path, repo_url = map(pprint.pformat, item)
161 content += ' %s: %s,\n' % (path, repo_url)
162 content += '}\n'
163 with open(repo_project_list, 'w') as f:
164 f.write(content)
165
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800166
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800167def mirror(code_storage, repo_url):
168 """Mirror git repo.
169
170 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
171
172 Args:
173 code_storage: CodeStorage object
174 repo_url: remote repo url
175 """
176 logger.info('mirror %s', repo_url)
177 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
178 git_root = code_storage.cached_git_root(repo_url)
179 assert not os.path.exists(git_root)
180
181 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
182
183 # These config parameters are copied from gclient.
184 git_util.config(tmp_dir, 'gc.autodetach', '0')
185 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
186 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
187 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
188 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
189 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
190 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
191 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
192 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
193 '+refs/branch-heads/*:refs/branch-heads/*',
194 r'\+refs/branch-heads/\*:.*')
195
196 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
197 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
198 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
199
200 # Rename to correct name atomically.
201 os.rename(tmp_dir, git_root)
202
203
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800204# Copied from depot_tools' gclient.py
205_PLATFORM_MAPPING = {
206 'cygwin': 'win',
207 'darwin': 'mac',
208 'linux2': 'linux',
209 'win32': 'win',
210 'aix6': 'aix',
211}
212
213
214def _detect_host_os():
215 return _PLATFORM_MAPPING[sys.platform]
216
217
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800218class Dep(object):
219 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800220
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800221 One Dep object means one subproject inside DEPS file. It recorded what to
222 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800223
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800224 Attributes:
225 path: subproject path, relative to project root
226 variables: the variables of the containing DEPS file; these variables will
227 be applied to fields of this object (like 'url' and 'condition') and
228 children projects.
229 condition: whether to checkout this subproject
230 dep_type: 'git' or 'cipd'
231 url: if dep_type='git', the url of remote repo and associated branch/commit
232 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800233 """
234
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800235 def __init__(self, path, variables, entry):
236 self.path = path
237 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800238
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800239 self.url = None # only valid for dep_type='git'
240 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800241
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800242 if isinstance(entry, str):
243 self.dep_type = 'git'
244 self.url = entry
245 self.condition = None
246 else:
247 self.dep_type = entry.get('dep_type', 'git')
248 self.condition = entry.get('condition')
249 if self.dep_type == 'git':
250 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800251 else:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800252 assert self.dep_type == 'cipd', 'unknown dep_type:' + self.dep_type
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800253 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800254
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800255 if self.dep_type == 'git':
256 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800257
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800258 def __eq__(self, rhs):
259 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800260
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800261 def __ne__(self, rhs):
262 return not self.__eq__(rhs)
263
264 def as_path_spec(self):
265 assert self.dep_type == 'git'
266
267 if '@' in self.url:
268 repo_url, at = self.url.split('@')
269 else:
270 # If the dependency is not pinned, the default is master branch.
271 repo_url, at = self.url, 'master'
272 return codechange.PathSpec(self.path, repo_url, at)
273
274 def eval_condition(self):
275 """Evaluate condition for DEPS parsing.
276
277 Returns:
278 eval result
279 """
280 if not self.condition:
281 return True
282
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800283 # Currently, we only support chromeos as target_os.
284 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800285 # We don't specify `target_os_only`, so `unix` will be considered by
286 # gclient as well.
287 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800288
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800289 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800290 'checkout_android': 'android' in target_os,
291 'checkout_chromeos': 'chromeos' in target_os,
292 'checkout_fuchsia': 'fuchsia' in target_os,
293 'checkout_ios': 'ios' in target_os,
294 'checkout_linux': 'unix' in target_os,
295 'checkout_mac': 'mac' in target_os,
296 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800297 # default cpu: x64
298 'checkout_arm64': False,
299 'checkout_arm': False,
300 'checkout_mips': False,
301 'checkout_ppc': False,
302 'checkout_s390': False,
303 'checkout_x64': True,
304 'checkout_x86': False,
305 'host_os': _detect_host_os(),
306 'False': False,
307 'None': None,
308 'True': True,
309 }
310 vars_dict.update(self.variables)
311 # pylint: disable=eval-used
312 return eval(self.condition, vars_dict)
313
314
315class Deps(object):
316 """DEPS parsed result.
317
318 Attributes:
319 variables: 'vars' dict in DEPS file; these variables will be applied
320 recursively to children.
321 entries: dict of Dep objects
322 recursedeps: list of recursive projects
323 """
324
325 def __init__(self):
326 self.variables = {}
327 self.entries = {}
328 self.recursedeps = []
329
330
331class TimeSeriesTree(object):
332 """Data structure for generating snapshots of historical dependency tree.
333
334 This is a tree structure with time information. Each tree node represents not
335 only typical tree data and tree children information, but also historical
336 value of those tree data and tree children.
337
338 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
339 represent a DEPS file. The caller will add_snapshot() to add parsed result of
340 historical DEPS instances. After that, the tree root of this class can
341 reconstruct the every historical moment of the project dependency state.
342
343 This class is slight abstraction of git_util.get_history_recursively() to
344 support more than single git repo and be version control system independent.
345 """
346
347 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
348
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800349 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800350 """TimeSeriesTree constructor.
351
352 Args:
353 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800354 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800355 start_time: start time
356 end_time: end time
357 """
358 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800359 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800360 self.snapshots = {}
361 self.start_time = start_time
362 self.end_time = end_time
363
364 # Intermediate dict to keep track alive children for the time being.
365 # Maintained by add_snapshot() and no_more_snapshot().
366 self.alive_children = {}
367
368 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800369 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800370 # once in this list because they are removed and added back to the DEPS
371 # file.
372 self.subtrees = []
373
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800374 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800375 """Compares subtree of two Deps.
376
377 Args:
378 deps_a: Deps object
379 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800380 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800381
382 Returns:
383 True if the said subtree of these two Deps equal
384 """
385 # Need to compare variables because they may influence subtree parsing
386 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800387 path = child_entry[0]
388 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800389 deps_a.variables == deps_b.variables)
390
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800391 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800392 """Adds parsed DEPS result and children.
393
394 For example, if a given DEPS file has N revisions between start_time and
395 end_time, the caller should call this method N times to feed all parsed
396 results in order (timestamp increasing).
397
398 Args:
399 timestamp: timestamp of `deps`
400 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800401 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800402 """
403 assert timestamp not in self.snapshots
404 self.snapshots[timestamp] = deps
405
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800406 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800407 # `child_entry` is added at `timestamp`
408 if child_entry not in self.alive_children:
409 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800410
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800411 # `child_entry` is removed at `timestamp`
412 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800413 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800414 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
415 self.alive_children[child_entry][0], timestamp))
416 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800417
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800418 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800419 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800420 last_deps = self.alive_children[child_entry][1]
421 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800422 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800423 TimeSeriesTree(last_deps, child_entry,
424 self.alive_children[child_entry][0], timestamp))
425 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800426
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800427 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800428 """Indicates all snapshots are added.
429
430 add_snapshot() should not be invoked after no_more_snapshot().
431 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800432 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800433 if timestamp == self.end_time:
434 continue
435 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800436 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800437 self.alive_children = None
438
439 def events(self):
440 """Gets children added/removed events of this subtree.
441
442 Returns:
443 list of (timestamp, deps_name, deps, end_flag):
444 timestamp: timestamp of event
445 deps_name: name of this subtree
446 deps: Deps object of given project
447 end_flag: True indicates this is the last event of this deps tree
448 """
449 assert self.snapshots
450 assert self.alive_children is None, ('events() is valid only after '
451 'no_more_snapshot() is invoked')
452
453 result = []
454
455 last_deps = None
456 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800457 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800458 last_deps = deps
459
460 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800461 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800462
463 for subtree in self.subtrees:
464 for event in subtree.events():
465 result.append(event)
466
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800467 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800468
469 return result
470
471 def iter_path_specs(self):
472 """Iterates snapshots of project dependency state.
473
474 Yields:
475 (timestamp, path_specs):
476 timestamp: time of snapshot
477 path_specs: dict of path_spec entries
478 """
479 forest = {}
480 # Group by timestamp
481 for timestamp, events in itertools.groupby(self.events(),
482 operator.itemgetter(0)):
483 # It's possible that one deps is removed and added at the same timestamp,
484 # i.e. modification, so use counter to track.
485 end_counter = collections.Counter()
486
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800487 for timestamp, entry, deps, end in events:
488 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800489 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800490 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800491 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800492 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800493
494 # Merge Deps at time `timestamp` into single path_specs.
495 path_specs = {}
496 for deps in forest.values():
497 for path, dep in deps.entries.items():
498 path_specs[path] = dep.as_path_spec()
499
500 yield timestamp, path_specs
501
502 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800503 for entry, count in end_counter.items():
504 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800505 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800506 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800507
508
509class DepsParser(object):
510 """Gclient DEPS file parser."""
511
512 def __init__(self, project_root, code_storage):
513 self.project_root = project_root
514 self.code_storage = code_storage
515
Kuang-che Wu067ff292019-02-14 18:16:23 +0800516 def load_single_deps(self, content):
517
518 def var_function(name):
519 return '{%s}' % name
520
521 global_scope = dict(Var=var_function)
522 local_scope = {}
523 try:
524 exec (content, global_scope, local_scope) # pylint: disable=exec-used
525 except SyntaxError:
526 raise
527
528 return local_scope
529
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800530 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
531 """Parses DEPS file without recursion.
532
533 Args:
534 content: file content of DEPS file
535 parent_vars: variables inherent from parent DEPS
536 parent_path: project path of parent DEPS file
537
538 Returns:
539 Deps object
540 """
541
Kuang-che Wu067ff292019-02-14 18:16:23 +0800542 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800543 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800544
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800545 local_scope.setdefault('vars', {})
546 if parent_vars:
547 local_scope['vars'].update(parent_vars)
548 deps.variables = local_scope['vars']
549
550 # Warnings for old usages which we don't support.
551 for name in deps.variables:
552 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
553 logger.warning('%s is deprecated and not supported recursion syntax',
554 name)
555 if 'deps_os' in local_scope:
556 logger.warning('deps_os is no longer supported')
557
558 for path, dep_entry in local_scope['deps'].items():
Kuang-che Wu058ac042019-03-18 14:14:53 +0800559 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800560 if local_scope.get('use_relative_paths', False):
561 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800562 path = os.path.normpath(path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800563 dep = Dep(path, deps.variables, dep_entry)
564 if not dep.eval_condition():
565 continue
566
567 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
568 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800569 warning_key = ('dep_type', dep.dep_type, path)
570 if warning_key not in emitted_warnings:
571 emitted_warnings.add(warning_key)
572 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
573 path)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800574 continue
575
576 deps.entries[path] = dep
577
578 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800579 for recurse_entry in local_scope.get('recursedeps', []):
580 # Normalize entries.
581 if isinstance(recurse_entry, tuple):
582 path, deps_file = recurse_entry
583 else:
584 assert isinstance(path, str)
585 path, deps_file = recurse_entry, 'DEPS'
586
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800587 if local_scope.get('use_relative_paths', False):
588 path = os.path.join(parent_path, path)
589 path = path.format(**deps.variables)
590 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800591 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800592 deps.recursedeps = recursedeps
593
594 return deps
595
596 def construct_deps_tree(self,
597 tstree,
598 repo_url,
599 at,
600 after,
601 before,
602 parent_vars=None,
603 parent_path='',
604 deps_file='DEPS'):
605 """Processes DEPS recursively of given time period.
606
607 This method parses all commits of DEPS between time `after` and `before`,
608 segments recursive dependencies into subtrees if they are changed, and
609 processes subtrees recursively.
610
611 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
612
613 Args:
614 tstree: TimeSeriesTree object
615 repo_url: remote repo url
616 at: branch or git commit id
617 after: begin of period
618 before: end of period
619 parent_vars: DEPS variables inherit from parent DEPS (including
620 custom_vars)
621 parent_path: the path of parent project of current DEPS file
622 deps_file: filename of DEPS file, relative to the git repo, repo_rul
623 """
624 if '://' in repo_url:
625 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800626 if not os.path.exists(git_repo):
627 with locking.lock_file(
628 os.path.join(self.code_storage.cache_dir,
629 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
630 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800631 else:
632 git_repo = repo_url
633
634 if git_util.is_git_rev(at):
635 history = [
636 (after, at),
637 (before, at),
638 ]
639 else:
640 history = git_util.get_history(
641 git_repo,
642 deps_file,
643 branch=at,
644 after=after,
645 before=before,
646 padding=True)
647 assert history
648
649 # If not equal, it means the file was deleted but is still referenced by
650 # its parent.
651 assert history[-1][0] == before
652
653 # TODO(kcwu): optimization: history[-1] is unused
654 for timestamp, git_rev in history[:-1]:
655 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
656
657 deps = self.parse_single_deps(
658 content, parent_vars=parent_vars, parent_path=parent_path)
659 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
660
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800661 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800662
663 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800664 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800665 path_spec = subtree.parent_deps.entries[path].as_path_spec()
666 self.construct_deps_tree(
667 subtree,
668 path_spec.repo_url,
669 path_spec.at,
670 subtree.start_time,
671 subtree.end_time,
672 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800673 parent_path=path,
674 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800675
676 def enumerate_path_specs(self, start_time, end_time, path):
677 tstree = TimeSeriesTree(None, path, start_time, end_time)
678 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
679 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800680
681
682class GclientCache(codechange.CodeStorage):
683 """Gclient git cache."""
684
685 def __init__(self, cache_dir):
686 self.cache_dir = cache_dir
687
688 def _url_to_cache_dir(self, url):
689 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800690 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800691 norm_url = parsed.netloc + parsed.path
692 if norm_url.endswith('.git'):
693 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800694 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800695 return norm_url.replace('-', '--').replace('/', '-').lower()
696
697 def cached_git_root(self, repo_url):
698 cache_path = self._url_to_cache_dir(repo_url)
699 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800700
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800701 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800702 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800703
704 projects[path] = repo_url
705
Kuang-che Wu067ff292019-02-14 18:16:23 +0800706 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800707
708 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800709 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800710
711 if path in projects:
712 del projects[path]
713
Kuang-che Wu067ff292019-02-14 18:16:23 +0800714 write_gclient_entries(project_root, projects)