blob: 2bc15555e7f5076cb822faaba67e43f5b1ceb18c [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
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +080014import queue
Kuang-che Wu067ff292019-02-14 18:16:23 +080015import shutil
Kuang-che Wub17b3b92018-09-04 18:12:11 +080016import sys
Kuang-che Wu1e49f512018-12-06 15:27:42 +080017import tempfile
Kuang-che Wu999893c2020-04-13 22:06:22 +080018import urllib.parse
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080019
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +080020# from third_party
21from depot_tools import gclient_eval
22
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080023from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080024from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080025from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080026from bisect_kit import util
27
28logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080029emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080030
Kuang-che Wu88e96312020-10-20 16:21:11 +080031# If the dependency is not pinned in DEPS file, the default branch.
32# ref: gclient_scm.py GitWrapper.update default_rev
33# TODO(kcwu): follow gclient to change the default branch name
34DEFAULT_BRANCH_NAME = 'master'
35
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080036
Kuang-che Wu41e8b592018-09-25 17:01:30 +080037def config(gclient_dir,
38 url=None,
39 cache_dir=None,
40 deps_file=None,
41 custom_var=None,
42 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080043 """Simply wrapper of `gclient config`.
44
45 Args:
46 gclient_dir: root directory of gclient project
47 url: URL of gclient configuration files
48 cache_dir: gclient's git cache folder
49 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080050 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080051 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080052 """
53 cmd = ['gclient', 'config']
54 if deps_file:
55 cmd += ['--deps-file', deps_file]
56 if cache_dir:
57 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080058 if custom_var:
59 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080060 if spec:
61 cmd += ['--spec', spec]
62 if url:
63 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080064
65 util.check_call(*cmd, cwd=gclient_dir)
66
67
Kuang-che Wu6ee24b52020-11-02 11:33:49 +080068def sync(gclient_dir, with_branch_heads=False, with_tags=False, jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080069 """Simply wrapper of `gclient sync`.
70
71 Args:
72 gclient_dir: root directory of gclient project
73 with_branch_heads: whether to clone git `branch_heads` refspecs
74 with_tags: whether to clone git tags
75 jobs: how many workers running in parallel
76 """
Kuang-che Wua9639eb2019-03-19 17:15:08 +080077 # Work around gclient issue crbug/943430
78 # gclient rejected to sync if there are untracked symlink even with --force
79 for path in [
80 'src/chromeos/assistant/libassistant/src/deps',
81 'src/chromeos/assistant/libassistant/src/libassistant',
82 ]:
83 if os.path.islink(os.path.join(gclient_dir, path)):
84 os.unlink(os.path.join(gclient_dir, path))
85
86 cmd = [
87 'gclient',
88 'sync',
89 '--jobs=%d' % jobs,
90 '--delete_unversioned_trees',
91 # --force is necessary because runhook may generate some untracked files.
92 '--force',
93 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080094 if with_branch_heads:
95 cmd.append('--with_branch_heads')
96 if with_tags:
97 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080098
Kuang-che Wu067ff292019-02-14 18:16:23 +080099 try:
100 old_projects = load_gclient_entries(gclient_dir)
101 except IOError:
102 old_projects = {}
103
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800104 util.check_call(*cmd, cwd=gclient_dir)
105
Kuang-che Wu067ff292019-02-14 18:16:23 +0800106 # Remove dead .git folder after sync.
107 # Ideally, this should be handled by gclient but sometimes gclient didn't
108 # (crbug/930047).
109 new_projects = load_gclient_entries(gclient_dir)
110 for path in old_projects:
111 if path in new_projects:
112 continue
113 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800114 if not os.path.exists(old_git_dir):
115 continue
116
117 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800118 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)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800122 else:
123 logger.warning(
124 '%s was removed from .gclient_entries but %s still exists;'
125 ' keep it because it is not git root', path, old_git_dir)
126
127
128def runhook(gclient_dir, jobs=8):
129 """Simply wrapper of `gclient runhook`.
130
131 Args:
132 gclient_dir: root directory of gclient project
133 jobs: how many workers running in parallel
134 """
135 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800136
137
138def load_gclient_entries(gclient_dir):
139 """Loads .gclient_entries."""
140 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
141 scope = {}
Kuang-che Wud2646f42019-11-27 18:31:08 +0800142 with open(repo_project_list) as f:
143 code = compile(f.read(), repo_project_list, 'exec')
Kuang-che Wub1b18ea2021-01-18 15:03:30 +0800144 # pylint: disable=exec-used
145 exec(code, scope)
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'
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800158 for path, repo_url in sorted(projects.items()):
159 content += ' %s: %s,\n' % (pprint.pformat(path), pprint.pformat(repo_url))
Kuang-che Wu067ff292019-02-14 18:16:23 +0800160 content += '}\n'
161 with open(repo_project_list, 'w') as f:
162 f.write(content)
163
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800164
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800165def mirror(code_storage, repo_url):
166 """Mirror git repo.
167
168 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
169
170 Args:
171 code_storage: CodeStorage object
172 repo_url: remote repo url
173 """
174 logger.info('mirror %s', repo_url)
175 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
176 git_root = code_storage.cached_git_root(repo_url)
177 assert not os.path.exists(git_root)
178
179 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
180
181 # These config parameters are copied from gclient.
182 git_util.config(tmp_dir, 'gc.autodetach', '0')
183 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
184 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
185 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
186 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
187 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
188 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
189 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
190 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
191 '+refs/branch-heads/*:refs/branch-heads/*',
192 r'\+refs/branch-heads/\*:.*')
193
194 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
195 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
196 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
197
198 # Rename to correct name atomically.
199 os.rename(tmp_dir, git_root)
200
201
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800202# Copied from depot_tools' gclient.py
203_PLATFORM_MAPPING = {
204 'cygwin': 'win',
205 'darwin': 'mac',
206 'linux2': 'linux',
Kuang-che Wud2646f42019-11-27 18:31:08 +0800207 'linux': 'linux',
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800208 'win32': 'win',
209 'aix6': 'aix',
210}
211
212
213def _detect_host_os():
214 return _PLATFORM_MAPPING[sys.platform]
215
216
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800217class Dep:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800218 """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 Wufe1e88a2019-09-10 21:52:25 +0800251 assert self.dep_type == 'cipd', 'unknown dep_type:' + self.dep_type
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800252 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
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800263 def set_url(self, repo_url, at):
264 assert self.dep_type == 'git'
265 self.url = '%s@%s' % (repo_url, at)
266
267 def set_revision(self, at):
268 assert self.dep_type == 'git'
269 repo_url, _ = self.parse_url()
270 self.set_url(repo_url, at)
271
272 def parse_url(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800273 assert self.dep_type == 'git'
274
275 if '@' in self.url:
276 repo_url, at = self.url.split('@')
277 else:
Kuang-che Wu88e96312020-10-20 16:21:11 +0800278 # If the dependency is not pinned, the default branch.
279 repo_url, at = self.url, DEFAULT_BRANCH_NAME
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800280 return repo_url, at
281
282 def as_path_spec(self):
283 repo_url, at = self.parse_url()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800284 return codechange.PathSpec(self.path, repo_url, at)
285
286 def eval_condition(self):
287 """Evaluate condition for DEPS parsing.
288
289 Returns:
290 eval result
291 """
292 if not self.condition:
293 return True
294
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800295 # Currently, we only support chromeos as target_os.
296 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800297 # We don't specify `target_os_only`, so `unix` will be considered by
298 # gclient as well.
299 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800300
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800301 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800302 'checkout_android': 'android' in target_os,
303 'checkout_chromeos': 'chromeos' in target_os,
304 'checkout_fuchsia': 'fuchsia' in target_os,
305 'checkout_ios': 'ios' in target_os,
306 'checkout_linux': 'unix' in target_os,
307 'checkout_mac': 'mac' in target_os,
308 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800309 # default cpu: x64
310 'checkout_arm64': False,
311 'checkout_arm': False,
312 'checkout_mips': False,
313 'checkout_ppc': False,
314 'checkout_s390': False,
315 'checkout_x64': True,
316 'checkout_x86': False,
317 'host_os': _detect_host_os(),
318 'False': False,
319 'None': None,
320 'True': True,
321 }
322 vars_dict.update(self.variables)
323 # pylint: disable=eval-used
324 return eval(self.condition, vars_dict)
325
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800326 def to_lines(self):
327 s = []
328 condition_part = ([' "condition": %r,' %
329 self.condition] if self.condition else [])
330 if self.dep_type == 'cipd':
331 s.extend([
332 ' "%s": {' % (self.path.split(':')[0],),
333 ' "packages": [',
334 ])
335 for p in sorted(self.packages, key=lambda x: x['package']):
336 s.extend([
337 ' {',
338 ' "package": "%s",' % p['package'],
339 ' "version": "%s",' % p['version'],
340 ' },',
341 ])
342 s.extend([
343 ' ],',
344 ' "dep_type": "cipd",',
345 ] + condition_part + [
346 ' },',
347 '',
348 ])
349 else:
350 s.extend([
351 ' "%s": {' % (self.path,),
352 ' "url": "%s",' % (self.url,),
353 ] + condition_part + [
354 ' },',
355 '',
356 ])
357 return s
358
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800359
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800360class Deps:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800361 """DEPS parsed result.
362
363 Attributes:
364 variables: 'vars' dict in DEPS file; these variables will be applied
365 recursively to children.
366 entries: dict of Dep objects
367 recursedeps: list of recursive projects
368 """
369
370 def __init__(self):
371 self.variables = {}
372 self.entries = {}
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800373 self.ignored_entries = {}
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800374 self.recursedeps = []
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800375 self.allowed_hosts = set()
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800376 self.gn_args_from = None
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800377 self.gn_args_file = None
378 self.gn_args = []
379 self.hooks = []
380 self.pre_deps_hooks = []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800381 self.modified = set()
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800382
383 def _gn_settings_to_lines(self):
384 s = []
385 if self.gn_args_file:
386 s.extend([
387 'gclient_gn_args_file = "%s"' % self.gn_args_file,
388 'gclient_gn_args = %r' % self.gn_args,
389 ])
390 return s
391
392 def _allowed_hosts_to_lines(self):
393 """Converts |allowed_hosts| set to list of lines for output."""
394 if not self.allowed_hosts:
395 return []
396 s = ['allowed_hosts = [']
397 for h in sorted(self.allowed_hosts):
398 s.append(' "%s",' % h)
399 s.extend([']', ''])
400 return s
401
402 def _entries_to_lines(self):
403 """Converts |entries| dict to list of lines for output."""
404 entries = self.ignored_entries
405 entries.update(self.entries)
406 if not entries:
407 return []
408 s = ['deps = {']
409 for _, dep in sorted(entries.items()):
410 s.extend(dep.to_lines())
411 s.extend(['}', ''])
412 return s
413
414 def _vars_to_lines(self):
415 """Converts |variables| dict to list of lines for output."""
416 if not self.variables:
417 return []
418 s = ['vars = {']
419 for key, value in sorted(self.variables.items()):
420 s.extend([
421 ' "%s": %r,' % (key, value),
422 '',
423 ])
424 s.extend(['}', ''])
425 return s
426
427 def _hooks_to_lines(self, name, hooks):
428 """Converts |hooks| list to list of lines for output."""
429 if not hooks:
430 return []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800431 hooks.sort(key=lambda x: x.get('name', ''))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800432 s = ['%s = [' % name]
433 for hook in hooks:
434 s.extend([
435 ' {',
436 ])
437 if hook.get('name') is not None:
438 s.append(' "name": "%s",' % hook.get('name'))
439 if hook.get('pattern') is not None:
440 s.append(' "pattern": "%s",' % hook.get('pattern'))
441 if hook.get('condition') is not None:
442 s.append(' "condition": %r,' % hook.get('condition'))
443 # Flattened hooks need to be written relative to the root gclient dir
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800444 cwd = os.path.relpath(os.path.normpath(hook.get('cwd', '.')))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800445 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
446 [' "%s",' % arg for arg in hook.get('action', [])] +
447 [' ]', ' },', ''])
448 s.extend([']', ''])
449 return s
450
451 def to_string(self):
452 """Return flatten DEPS string."""
453 return '\n'.join(
454 self._gn_settings_to_lines() + self._allowed_hosts_to_lines() +
455 self._entries_to_lines() + self._hooks_to_lines('hooks', self.hooks) +
456 self._hooks_to_lines('pre_deps_hooks', self.pre_deps_hooks) +
457 self._vars_to_lines() + ['']) # Ensure newline at end of file.
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800458
Zheng-Jie Changc0d3cd72020-06-03 02:46:43 +0800459 def remove_src(self):
460 """Return src_revision for buildbucket use."""
461 assert 'src' in self.entries
462 _, src_rev = self.entries['src'].parse_url()
463 del self.entries['src']
464 return src_rev
465
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800466 def apply_commit(self, path, revision, overwrite=True):
467 """Set revision to a project by path.
468
469 Args:
470 path: A project's path.
471 revision: A git commit id.
472 overwrite: Overwrite flag, the project won't change if overwrite=False
473 and it was modified before.
474 """
475 if path in self.modified and not overwrite:
476 return
477 self.modified.add(path)
478
479 if path not in self.entries:
480 logger.warning('path: %s not found in DEPS', path)
481 return
482 self.entries[path].set_revision(revision)
483
484 def apply_action_groups(self, action_groups):
485 """Apply multiple action groups to DEPS.
486
487 If there are multiple actions in one repo, only last one is applied.
488
489 Args:
490 action_groups: A list of action groups.
491 """
492 # Apply in reversed order with overwrite=False,
493 # so each repo is on the state of last action.
494 for action_group in reversed(action_groups):
495 for action in reversed(action_group.actions):
496 if isinstance(action, codechange.GitCheckoutCommit):
497 self.apply_commit(action.path, action.rev, overwrite=False)
498 if isinstance(action, codechange.GitAddRepo):
499 self.apply_commit(action.path, action.rev, overwrite=False)
500 if isinstance(action, codechange.GitRemoveRepo):
501 assert action.path not in self.entries
502 self.modified.add(action.path)
503
504 def apply_deps(self, deps):
505 for path, dep in deps.entries.items():
506 if path in self.entries:
507 _, rev = dep.parse_url()
508 self.apply_commit(path, rev, overwrite=False)
509
510 # hooks, vars, ignored_entries are ignored and should be set by float_spec
511
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800512
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800513class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800514 """Data structure for generating snapshots of historical dependency tree.
515
516 This is a tree structure with time information. Each tree node represents not
517 only typical tree data and tree children information, but also historical
518 value of those tree data and tree children.
519
520 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
521 represent a DEPS file. The caller will add_snapshot() to add parsed result of
522 historical DEPS instances. After that, the tree root of this class can
523 reconstruct the every historical moment of the project dependency state.
524
525 This class is slight abstraction of git_util.get_history_recursively() to
526 support more than single git repo and be version control system independent.
527 """
528
529 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
530
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800531 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800532 """TimeSeriesTree constructor.
533
534 Args:
535 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800536 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800537 start_time: start time
538 end_time: end time
539 """
540 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800541 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800542 self.snapshots = {}
543 self.start_time = start_time
544 self.end_time = end_time
545
546 # Intermediate dict to keep track alive children for the time being.
547 # Maintained by add_snapshot() and no_more_snapshot().
548 self.alive_children = {}
549
550 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800551 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800552 # once in this list because they are removed and added back to the DEPS
553 # file.
554 self.subtrees = []
555
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800556 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800557 """Compares subtree of two Deps.
558
559 Args:
560 deps_a: Deps object
561 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800562 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800563
564 Returns:
565 True if the said subtree of these two Deps equal
566 """
567 # Need to compare variables because they may influence subtree parsing
568 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800569 path = child_entry[0]
570 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800571 deps_a.variables == deps_b.variables)
572
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800573 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800574 """Adds parsed DEPS result and children.
575
576 For example, if a given DEPS file has N revisions between start_time and
577 end_time, the caller should call this method N times to feed all parsed
578 results in order (timestamp increasing).
579
580 Args:
581 timestamp: timestamp of `deps`
582 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800583 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800584 """
585 assert timestamp not in self.snapshots
586 self.snapshots[timestamp] = deps
587
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800588 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800589 # `child_entry` is added at `timestamp`
590 if child_entry not in self.alive_children:
591 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800592
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800593 # `child_entry` is removed at `timestamp`
594 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800595 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800596 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
597 self.alive_children[child_entry][0], timestamp))
598 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800599
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800600 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800601 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800602 last_deps = self.alive_children[child_entry][1]
603 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800604 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800605 TimeSeriesTree(last_deps, child_entry,
606 self.alive_children[child_entry][0], timestamp))
607 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800608
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800609 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800610 """Indicates all snapshots are added.
611
612 add_snapshot() should not be invoked after no_more_snapshot().
613 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800614 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800615 if timestamp == self.end_time:
616 continue
617 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800618 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800619 self.alive_children = None
620
621 def events(self):
622 """Gets children added/removed events of this subtree.
623
624 Returns:
625 list of (timestamp, deps_name, deps, end_flag):
626 timestamp: timestamp of event
627 deps_name: name of this subtree
628 deps: Deps object of given project
629 end_flag: True indicates this is the last event of this deps tree
630 """
631 assert self.snapshots
632 assert self.alive_children is None, ('events() is valid only after '
633 'no_more_snapshot() is invoked')
634
635 result = []
636
637 last_deps = None
638 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800639 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800640 last_deps = deps
641
642 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800643 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800644
645 for subtree in self.subtrees:
646 for event in subtree.events():
647 result.append(event)
648
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800649 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800650
651 return result
652
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800653 def iter_forest(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800654 """Iterates snapshots of project dependency state.
655
656 Yields:
657 (timestamp, path_specs):
658 timestamp: time of snapshot
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800659 forest: A dict indicates path => deps mapping
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800660 """
661 forest = {}
662 # Group by timestamp
663 for timestamp, events in itertools.groupby(self.events(),
664 operator.itemgetter(0)):
665 # It's possible that one deps is removed and added at the same timestamp,
666 # i.e. modification, so use counter to track.
667 end_counter = collections.Counter()
668
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800669 for timestamp, entry, deps, end in events:
670 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800671 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800672 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800673 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800674 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800675
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800676 yield timestamp, forest
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800677
678 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800679 for entry, count in end_counter.items():
680 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800681 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800682 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800683
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800684 def iter_path_specs(self):
685 """Iterates snapshots of project dependency state.
686
687 Yields:
688 (timestamp, path_specs):
689 timestamp: time of snapshot
690 path_specs: dict of path_spec entries
691 """
692 for timestamp, forest in self.iter_forest():
693 path_specs = {}
694 # Merge Deps at time `timestamp` into single path_specs.
695 for deps in forest.values():
696 for path, dep in deps.entries.items():
697 path_specs[path] = dep.as_path_spec()
698 yield timestamp, path_specs
699
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800700
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800701class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800702 """Gclient DEPS file parser."""
703
704 def __init__(self, project_root, code_storage):
705 self.project_root = project_root
706 self.code_storage = code_storage
707
Kuang-che Wu067ff292019-02-14 18:16:23 +0800708 def load_single_deps(self, content):
709
710 def var_function(name):
711 return '{%s}' % name
712
Zheng-Jie Chang381e4162020-08-03 14:26:54 +0800713 def str_function(name):
714 return str(name)
715
716 global_scope = dict(Var=var_function, Str=str_function)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800717 local_scope = {}
Kuang-che Wub1b18ea2021-01-18 15:03:30 +0800718 # pylint: disable=exec-used
719 exec(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800720 return local_scope
721
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800722 def parse_single_deps(self,
723 content,
724 parent_vars=None,
725 parent_path='',
726 parent_dep=None):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800727 """Parses DEPS file without recursion.
728
729 Args:
730 content: file content of DEPS file
731 parent_vars: variables inherent from parent DEPS
732 parent_path: project path of parent DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800733 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800734
735 Returns:
736 Deps object
737 """
738
Kuang-che Wu067ff292019-02-14 18:16:23 +0800739 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800740 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800741
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800742 local_scope.setdefault('vars', {})
743 if parent_vars:
744 local_scope['vars'].update(parent_vars)
745 deps.variables = local_scope['vars']
746
747 # Warnings for old usages which we don't support.
748 for name in deps.variables:
749 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
750 logger.warning('%s is deprecated and not supported recursion syntax',
751 name)
752 if 'deps_os' in local_scope:
753 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800754
755 if 'allowed_hosts' in local_scope:
756 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
757 deps.hooks = local_scope.get('hooks', [])
758 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800759 deps.gn_args_from = local_scope.get('gclient_gn_args_from')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800760 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
761 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800762
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800763 # recalculate hook path
764 use_relative_hooks = local_scope.get('use_relative_hooks', False)
765 if use_relative_hooks:
766 assert local_scope.get('use_relative_paths', False)
767 for hook in deps.hooks:
768 hook['cwd'] = os.path.join(parent_path, hook.get('cwd', ''))
769 for pre_deps_hook in deps.pre_deps_hooks:
770 pre_deps_hook['cwd'] = os.path.join(parent_path,
771 pre_deps_hook.get('cwd', ''))
772
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800773 for path, dep_entry in local_scope['deps'].items():
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800774 # recalculate path
Kuang-che Wu058ac042019-03-18 14:14:53 +0800775 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800776 if local_scope.get('use_relative_paths', False):
777 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800778 path = os.path.normpath(path)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800779
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800780 dep = Dep(path, deps.variables, dep_entry)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800781 eval_condition = dep.eval_condition()
782
783 # update condition
784 if parent_dep and parent_dep.condition:
785 tmp_dict = {'condition': dep.condition}
786 gclient_eval.UpdateCondition(tmp_dict, 'and', parent_dep.condition)
787 dep.condition = tmp_dict['condition']
788
789 if not eval_condition:
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800790 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800791 continue
792
793 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
794 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800795 warning_key = ('dep_type', dep.dep_type, path)
796 if warning_key not in emitted_warnings:
797 emitted_warnings.add(warning_key)
798 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
799 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800800 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800801 continue
802
803 deps.entries[path] = dep
804
805 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800806 for recurse_entry in local_scope.get('recursedeps', []):
807 # Normalize entries.
808 if isinstance(recurse_entry, tuple):
809 path, deps_file = recurse_entry
810 else:
811 assert isinstance(path, str)
812 path, deps_file = recurse_entry, 'DEPS'
813
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800814 if local_scope.get('use_relative_paths', False):
815 path = os.path.join(parent_path, path)
816 path = path.format(**deps.variables)
817 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800818 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800819 deps.recursedeps = recursedeps
820
821 return deps
822
823 def construct_deps_tree(self,
824 tstree,
825 repo_url,
826 at,
827 after,
828 before,
829 parent_vars=None,
830 parent_path='',
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800831 parent_dep=None,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800832 deps_file='DEPS'):
833 """Processes DEPS recursively of given time period.
834
835 This method parses all commits of DEPS between time `after` and `before`,
836 segments recursive dependencies into subtrees if they are changed, and
837 processes subtrees recursively.
838
839 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
840
841 Args:
842 tstree: TimeSeriesTree object
843 repo_url: remote repo url
844 at: branch or git commit id
845 after: begin of period
846 before: end of period
847 parent_vars: DEPS variables inherit from parent DEPS (including
848 custom_vars)
849 parent_path: the path of parent project of current DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800850 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800851 deps_file: filename of DEPS file, relative to the git repo, repo_rul
852 """
853 if '://' in repo_url:
854 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800855 if not os.path.exists(git_repo):
856 with locking.lock_file(
857 os.path.join(self.code_storage.cache_dir,
858 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
859 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800860 else:
861 git_repo = repo_url
862
863 if git_util.is_git_rev(at):
864 history = [
865 (after, at),
866 (before, at),
867 ]
868 else:
869 history = git_util.get_history(
870 git_repo,
871 deps_file,
872 branch=at,
873 after=after,
874 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800875 padding_begin=True,
876 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800877 assert history
878
879 # If not equal, it means the file was deleted but is still referenced by
880 # its parent.
881 assert history[-1][0] == before
882
883 # TODO(kcwu): optimization: history[-1] is unused
884 for timestamp, git_rev in history[:-1]:
885 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
886
887 deps = self.parse_single_deps(
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800888 content,
889 parent_vars=parent_vars,
890 parent_path=parent_path,
891 parent_dep=parent_dep)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800892 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
893
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800894 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800895
896 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800897 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800898 path_spec = subtree.parent_deps.entries[path].as_path_spec()
899 self.construct_deps_tree(
900 subtree,
901 path_spec.repo_url,
902 path_spec.at,
903 subtree.start_time,
904 subtree.end_time,
905 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800906 parent_path=path,
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800907 parent_dep=subtree.parent_deps.entries[path],
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800908 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800909
910 def enumerate_path_specs(self, start_time, end_time, path):
911 tstree = TimeSeriesTree(None, path, start_time, end_time)
Kuang-che Wu88e96312020-10-20 16:21:11 +0800912 self.construct_deps_tree(tstree, path, DEFAULT_BRANCH_NAME, start_time,
913 end_time)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800914 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800915
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800916 def enumerate_gclient_solutions(self, start_time, end_time, path):
917 tstree = TimeSeriesTree(None, path, start_time, end_time)
Kuang-che Wu88e96312020-10-20 16:21:11 +0800918 self.construct_deps_tree(tstree, path, DEFAULT_BRANCH_NAME, start_time,
919 end_time)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800920 return tstree.iter_forest()
921
922 def flatten(self, solutions, entry_point):
923 """Flatten all given Deps
924
925 Args:
926 solutions: A name => Deps dict, name can be either a str or a tuple.
927 entry_point (str): An entry_point name of solutions.
928
929 Returns:
930 Deps: A flatten Deps.
931 """
932
933 def _add_unvisited_recursedeps(deps_queue, visited, deps):
934 for name in deps.recursedeps:
935 if name not in visited:
936 visited.add(name)
937 deps_queue.put(name)
938
939 result = solutions[entry_point]
940 deps_queue = queue.SimpleQueue()
941 visited = set()
942 visited.add(entry_point)
943 _add_unvisited_recursedeps(deps_queue, visited, solutions[entry_point])
944
945 # BFS to merge `deps` into `result`
946 while not deps_queue.empty():
947 deps_name = deps_queue.get()
948 deps = solutions[deps_name]
949
950 result.allowed_hosts.update(deps.allowed_hosts)
951 for key, value in deps.variables.items():
952 assert key not in result.variables or deps.variables[key] == value
953 result.variables[key] = value
954 result.pre_deps_hooks += deps.pre_deps_hooks
955 result.hooks += deps.hooks
956
957 for dep in deps.entries.values():
958 assert dep.path not in result.entries or result.entries.get(
959 dep.path) == dep
960 result.entries[dep.path] = dep
961
962 for dep in deps.ignored_entries.values():
963 assert (dep.path not in result.ignored_entries or
964 result.ignored_entries.get(dep.path) == dep)
965 result.ignored_entries[dep.path] = dep
966
967 _add_unvisited_recursedeps(deps_queue, visited, deps)
968
969 # If gn_args_from is set in root DEPS, overwrite gn arguments
970 if solutions[entry_point].gn_args_from:
971 gn_args_dep = solutions[(solutions[entry_point].gn_args_from, 'DEPS')]
972 result.gn_args = gn_args_dep.gn_args
973 result.gn_args_file = gn_args_dep.gn_args_file
974
975 return result
976
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800977
978class GclientCache(codechange.CodeStorage):
979 """Gclient git cache."""
980
981 def __init__(self, cache_dir):
982 self.cache_dir = cache_dir
983
984 def _url_to_cache_dir(self, url):
985 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800986 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800987 norm_url = parsed.netloc + parsed.path
988 if norm_url.endswith('.git'):
989 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800990 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800991 return norm_url.replace('-', '--').replace('/', '-').lower()
992
993 def cached_git_root(self, repo_url):
994 cache_path = self._url_to_cache_dir(repo_url)
995 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800996
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800997 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800998 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800999
1000 projects[path] = repo_url
1001
Kuang-che Wu067ff292019-02-14 18:16:23 +08001002 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001003
1004 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001005 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001006
1007 if path in projects:
1008 del projects[path]
1009
Kuang-che Wu067ff292019-02-14 18:16:23 +08001010 write_gclient_entries(project_root, projects)