blob: da53b546727a9c519ea08ac2c57408cebd21de9d [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
Kuang-che Wud2646f42019-11-27 18:31:08 +080020import six
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080021
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +080022# from third_party
23from depot_tools import gclient_eval
24
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080025from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080026from bisect_kit import git_util
Kuang-che Wu1e49f512018-12-06 15:27:42 +080027from bisect_kit import locking
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080028from bisect_kit import util
29
30logger = logging.getLogger(__name__)
Kuang-che Wuced2dbf2019-01-30 23:13:24 +080031emitted_warnings = set()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080032
Kuang-che Wu88e96312020-10-20 16:21:11 +080033# If the dependency is not pinned in DEPS file, the default branch.
34# ref: gclient_scm.py GitWrapper.update default_rev
35# TODO(kcwu): follow gclient to change the default branch name
36DEFAULT_BRANCH_NAME = 'master'
37
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080038
Kuang-che Wu41e8b592018-09-25 17:01:30 +080039def config(gclient_dir,
40 url=None,
41 cache_dir=None,
42 deps_file=None,
43 custom_var=None,
44 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080045 """Simply wrapper of `gclient config`.
46
47 Args:
48 gclient_dir: root directory of gclient project
49 url: URL of gclient configuration files
50 cache_dir: gclient's git cache folder
51 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080052 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080053 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080054 """
55 cmd = ['gclient', 'config']
56 if deps_file:
57 cmd += ['--deps-file', deps_file]
58 if cache_dir:
59 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080060 if custom_var:
61 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080062 if spec:
63 cmd += ['--spec', spec]
64 if url:
65 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080066
67 util.check_call(*cmd, cwd=gclient_dir)
68
69
Kuang-che Wu6ee24b52020-11-02 11:33:49 +080070def sync(gclient_dir, with_branch_heads=False, with_tags=False, jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080071 """Simply wrapper of `gclient sync`.
72
73 Args:
74 gclient_dir: root directory of gclient project
75 with_branch_heads: whether to clone git `branch_heads` refspecs
76 with_tags: whether to clone git tags
77 jobs: how many workers running in parallel
78 """
Kuang-che Wua9639eb2019-03-19 17:15:08 +080079 # Work around gclient issue crbug/943430
80 # gclient rejected to sync if there are untracked symlink even with --force
81 for path in [
82 'src/chromeos/assistant/libassistant/src/deps',
83 'src/chromeos/assistant/libassistant/src/libassistant',
84 ]:
85 if os.path.islink(os.path.join(gclient_dir, path)):
86 os.unlink(os.path.join(gclient_dir, path))
87
88 cmd = [
89 'gclient',
90 'sync',
91 '--jobs=%d' % jobs,
92 '--delete_unversioned_trees',
93 # --force is necessary because runhook may generate some untracked files.
94 '--force',
95 ]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080096 if with_branch_heads:
97 cmd.append('--with_branch_heads')
98 if with_tags:
99 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +0800100
Kuang-che Wu067ff292019-02-14 18:16:23 +0800101 try:
102 old_projects = load_gclient_entries(gclient_dir)
103 except IOError:
104 old_projects = {}
105
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800106 util.check_call(*cmd, cwd=gclient_dir)
107
Kuang-che Wu067ff292019-02-14 18:16:23 +0800108 # Remove dead .git folder after sync.
109 # Ideally, this should be handled by gclient but sometimes gclient didn't
110 # (crbug/930047).
111 new_projects = load_gclient_entries(gclient_dir)
112 for path in old_projects:
113 if path in new_projects:
114 continue
115 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800116 if not os.path.exists(old_git_dir):
117 continue
118
119 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800120 logger.warning(
121 '%s was removed from .gclient_entries but %s still exists; remove it',
122 path, old_git_dir)
123 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800124 else:
125 logger.warning(
126 '%s was removed from .gclient_entries but %s still exists;'
127 ' keep it because it is not git root', path, old_git_dir)
128
129
130def runhook(gclient_dir, jobs=8):
131 """Simply wrapper of `gclient runhook`.
132
133 Args:
134 gclient_dir: root directory of gclient project
135 jobs: how many workers running in parallel
136 """
137 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800138
139
140def load_gclient_entries(gclient_dir):
141 """Loads .gclient_entries."""
142 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
143 scope = {}
Kuang-che Wud2646f42019-11-27 18:31:08 +0800144 with open(repo_project_list) as f:
145 code = compile(f.read(), repo_project_list, 'exec')
146 six.exec_(code, scope)
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'
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800159 for path, repo_url in sorted(projects.items()):
160 content += ' %s: %s,\n' % (pprint.pformat(path), pprint.pformat(repo_url))
Kuang-che Wu067ff292019-02-14 18:16:23 +0800161 content += '}\n'
162 with open(repo_project_list, 'w') as f:
163 f.write(content)
164
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800165
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800166def mirror(code_storage, repo_url):
167 """Mirror git repo.
168
169 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
170
171 Args:
172 code_storage: CodeStorage object
173 repo_url: remote repo url
174 """
175 logger.info('mirror %s', repo_url)
176 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
177 git_root = code_storage.cached_git_root(repo_url)
178 assert not os.path.exists(git_root)
179
180 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
181
182 # These config parameters are copied from gclient.
183 git_util.config(tmp_dir, 'gc.autodetach', '0')
184 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
185 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
186 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
187 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
188 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
189 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
190 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
191 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
192 '+refs/branch-heads/*:refs/branch-heads/*',
193 r'\+refs/branch-heads/\*:.*')
194
195 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
196 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
197 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
198
199 # Rename to correct name atomically.
200 os.rename(tmp_dir, git_root)
201
202
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800203# Copied from depot_tools' gclient.py
204_PLATFORM_MAPPING = {
205 'cygwin': 'win',
206 'darwin': 'mac',
207 'linux2': 'linux',
Kuang-che Wud2646f42019-11-27 18:31:08 +0800208 'linux': 'linux',
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800209 'win32': 'win',
210 'aix6': 'aix',
211}
212
213
214def _detect_host_os():
215 return _PLATFORM_MAPPING[sys.platform]
216
217
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800218class Dep:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800219 """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
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800264 def set_url(self, repo_url, at):
265 assert self.dep_type == 'git'
266 self.url = '%s@%s' % (repo_url, at)
267
268 def set_revision(self, at):
269 assert self.dep_type == 'git'
270 repo_url, _ = self.parse_url()
271 self.set_url(repo_url, at)
272
273 def parse_url(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800274 assert self.dep_type == 'git'
275
276 if '@' in self.url:
277 repo_url, at = self.url.split('@')
278 else:
Kuang-che Wu88e96312020-10-20 16:21:11 +0800279 # If the dependency is not pinned, the default branch.
280 repo_url, at = self.url, DEFAULT_BRANCH_NAME
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800281 return repo_url, at
282
283 def as_path_spec(self):
284 repo_url, at = self.parse_url()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800285 return codechange.PathSpec(self.path, repo_url, at)
286
287 def eval_condition(self):
288 """Evaluate condition for DEPS parsing.
289
290 Returns:
291 eval result
292 """
293 if not self.condition:
294 return True
295
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800296 # Currently, we only support chromeos as target_os.
297 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800298 # We don't specify `target_os_only`, so `unix` will be considered by
299 # gclient as well.
300 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800301
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800302 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800303 'checkout_android': 'android' in target_os,
304 'checkout_chromeos': 'chromeos' in target_os,
305 'checkout_fuchsia': 'fuchsia' in target_os,
306 'checkout_ios': 'ios' in target_os,
307 'checkout_linux': 'unix' in target_os,
308 'checkout_mac': 'mac' in target_os,
309 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800310 # default cpu: x64
311 'checkout_arm64': False,
312 'checkout_arm': False,
313 'checkout_mips': False,
314 'checkout_ppc': False,
315 'checkout_s390': False,
316 'checkout_x64': True,
317 'checkout_x86': False,
318 'host_os': _detect_host_os(),
319 'False': False,
320 'None': None,
321 'True': True,
322 }
323 vars_dict.update(self.variables)
324 # pylint: disable=eval-used
325 return eval(self.condition, vars_dict)
326
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800327 def to_lines(self):
328 s = []
329 condition_part = ([' "condition": %r,' %
330 self.condition] if self.condition else [])
331 if self.dep_type == 'cipd':
332 s.extend([
333 ' "%s": {' % (self.path.split(':')[0],),
334 ' "packages": [',
335 ])
336 for p in sorted(self.packages, key=lambda x: x['package']):
337 s.extend([
338 ' {',
339 ' "package": "%s",' % p['package'],
340 ' "version": "%s",' % p['version'],
341 ' },',
342 ])
343 s.extend([
344 ' ],',
345 ' "dep_type": "cipd",',
346 ] + condition_part + [
347 ' },',
348 '',
349 ])
350 else:
351 s.extend([
352 ' "%s": {' % (self.path,),
353 ' "url": "%s",' % (self.url,),
354 ] + condition_part + [
355 ' },',
356 '',
357 ])
358 return s
359
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800360
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800361class Deps:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800362 """DEPS parsed result.
363
364 Attributes:
365 variables: 'vars' dict in DEPS file; these variables will be applied
366 recursively to children.
367 entries: dict of Dep objects
368 recursedeps: list of recursive projects
369 """
370
371 def __init__(self):
372 self.variables = {}
373 self.entries = {}
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800374 self.ignored_entries = {}
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800375 self.recursedeps = []
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800376 self.allowed_hosts = set()
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800377 self.gn_args_from = None
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800378 self.gn_args_file = None
379 self.gn_args = []
380 self.hooks = []
381 self.pre_deps_hooks = []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800382 self.modified = set()
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800383
384 def _gn_settings_to_lines(self):
385 s = []
386 if self.gn_args_file:
387 s.extend([
388 'gclient_gn_args_file = "%s"' % self.gn_args_file,
389 'gclient_gn_args = %r' % self.gn_args,
390 ])
391 return s
392
393 def _allowed_hosts_to_lines(self):
394 """Converts |allowed_hosts| set to list of lines for output."""
395 if not self.allowed_hosts:
396 return []
397 s = ['allowed_hosts = [']
398 for h in sorted(self.allowed_hosts):
399 s.append(' "%s",' % h)
400 s.extend([']', ''])
401 return s
402
403 def _entries_to_lines(self):
404 """Converts |entries| dict to list of lines for output."""
405 entries = self.ignored_entries
406 entries.update(self.entries)
407 if not entries:
408 return []
409 s = ['deps = {']
410 for _, dep in sorted(entries.items()):
411 s.extend(dep.to_lines())
412 s.extend(['}', ''])
413 return s
414
415 def _vars_to_lines(self):
416 """Converts |variables| dict to list of lines for output."""
417 if not self.variables:
418 return []
419 s = ['vars = {']
420 for key, value in sorted(self.variables.items()):
421 s.extend([
422 ' "%s": %r,' % (key, value),
423 '',
424 ])
425 s.extend(['}', ''])
426 return s
427
428 def _hooks_to_lines(self, name, hooks):
429 """Converts |hooks| list to list of lines for output."""
430 if not hooks:
431 return []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800432 hooks.sort(key=lambda x: x.get('name', ''))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800433 s = ['%s = [' % name]
434 for hook in hooks:
435 s.extend([
436 ' {',
437 ])
438 if hook.get('name') is not None:
439 s.append(' "name": "%s",' % hook.get('name'))
440 if hook.get('pattern') is not None:
441 s.append(' "pattern": "%s",' % hook.get('pattern'))
442 if hook.get('condition') is not None:
443 s.append(' "condition": %r,' % hook.get('condition'))
444 # Flattened hooks need to be written relative to the root gclient dir
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800445 cwd = os.path.relpath(os.path.normpath(hook.get('cwd', '.')))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800446 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
447 [' "%s",' % arg for arg in hook.get('action', [])] +
448 [' ]', ' },', ''])
449 s.extend([']', ''])
450 return s
451
452 def to_string(self):
453 """Return flatten DEPS string."""
454 return '\n'.join(
455 self._gn_settings_to_lines() + self._allowed_hosts_to_lines() +
456 self._entries_to_lines() + self._hooks_to_lines('hooks', self.hooks) +
457 self._hooks_to_lines('pre_deps_hooks', self.pre_deps_hooks) +
458 self._vars_to_lines() + ['']) # Ensure newline at end of file.
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800459
Zheng-Jie Changc0d3cd72020-06-03 02:46:43 +0800460 def remove_src(self):
461 """Return src_revision for buildbucket use."""
462 assert 'src' in self.entries
463 _, src_rev = self.entries['src'].parse_url()
464 del self.entries['src']
465 return src_rev
466
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800467 def apply_commit(self, path, revision, overwrite=True):
468 """Set revision to a project by path.
469
470 Args:
471 path: A project's path.
472 revision: A git commit id.
473 overwrite: Overwrite flag, the project won't change if overwrite=False
474 and it was modified before.
475 """
476 if path in self.modified and not overwrite:
477 return
478 self.modified.add(path)
479
480 if path not in self.entries:
481 logger.warning('path: %s not found in DEPS', path)
482 return
483 self.entries[path].set_revision(revision)
484
485 def apply_action_groups(self, action_groups):
486 """Apply multiple action groups to DEPS.
487
488 If there are multiple actions in one repo, only last one is applied.
489
490 Args:
491 action_groups: A list of action groups.
492 """
493 # Apply in reversed order with overwrite=False,
494 # so each repo is on the state of last action.
495 for action_group in reversed(action_groups):
496 for action in reversed(action_group.actions):
497 if isinstance(action, codechange.GitCheckoutCommit):
498 self.apply_commit(action.path, action.rev, overwrite=False)
499 if isinstance(action, codechange.GitAddRepo):
500 self.apply_commit(action.path, action.rev, overwrite=False)
501 if isinstance(action, codechange.GitRemoveRepo):
502 assert action.path not in self.entries
503 self.modified.add(action.path)
504
505 def apply_deps(self, deps):
506 for path, dep in deps.entries.items():
507 if path in self.entries:
508 _, rev = dep.parse_url()
509 self.apply_commit(path, rev, overwrite=False)
510
511 # hooks, vars, ignored_entries are ignored and should be set by float_spec
512
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800513
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800514class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800515 """Data structure for generating snapshots of historical dependency tree.
516
517 This is a tree structure with time information. Each tree node represents not
518 only typical tree data and tree children information, but also historical
519 value of those tree data and tree children.
520
521 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
522 represent a DEPS file. The caller will add_snapshot() to add parsed result of
523 historical DEPS instances. After that, the tree root of this class can
524 reconstruct the every historical moment of the project dependency state.
525
526 This class is slight abstraction of git_util.get_history_recursively() to
527 support more than single git repo and be version control system independent.
528 """
529
530 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
531
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800532 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800533 """TimeSeriesTree constructor.
534
535 Args:
536 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800537 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800538 start_time: start time
539 end_time: end time
540 """
541 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800542 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800543 self.snapshots = {}
544 self.start_time = start_time
545 self.end_time = end_time
546
547 # Intermediate dict to keep track alive children for the time being.
548 # Maintained by add_snapshot() and no_more_snapshot().
549 self.alive_children = {}
550
551 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800552 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800553 # once in this list because they are removed and added back to the DEPS
554 # file.
555 self.subtrees = []
556
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800557 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800558 """Compares subtree of two Deps.
559
560 Args:
561 deps_a: Deps object
562 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800563 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800564
565 Returns:
566 True if the said subtree of these two Deps equal
567 """
568 # Need to compare variables because they may influence subtree parsing
569 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800570 path = child_entry[0]
571 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800572 deps_a.variables == deps_b.variables)
573
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800574 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800575 """Adds parsed DEPS result and children.
576
577 For example, if a given DEPS file has N revisions between start_time and
578 end_time, the caller should call this method N times to feed all parsed
579 results in order (timestamp increasing).
580
581 Args:
582 timestamp: timestamp of `deps`
583 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800584 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800585 """
586 assert timestamp not in self.snapshots
587 self.snapshots[timestamp] = deps
588
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800589 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800590 # `child_entry` is added at `timestamp`
591 if child_entry not in self.alive_children:
592 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800593
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800594 # `child_entry` is removed at `timestamp`
595 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800596 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800597 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
598 self.alive_children[child_entry][0], timestamp))
599 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800600
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800601 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800602 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800603 last_deps = self.alive_children[child_entry][1]
604 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800605 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800606 TimeSeriesTree(last_deps, child_entry,
607 self.alive_children[child_entry][0], timestamp))
608 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800609
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800610 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800611 """Indicates all snapshots are added.
612
613 add_snapshot() should not be invoked after no_more_snapshot().
614 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800615 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800616 if timestamp == self.end_time:
617 continue
618 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800619 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800620 self.alive_children = None
621
622 def events(self):
623 """Gets children added/removed events of this subtree.
624
625 Returns:
626 list of (timestamp, deps_name, deps, end_flag):
627 timestamp: timestamp of event
628 deps_name: name of this subtree
629 deps: Deps object of given project
630 end_flag: True indicates this is the last event of this deps tree
631 """
632 assert self.snapshots
633 assert self.alive_children is None, ('events() is valid only after '
634 'no_more_snapshot() is invoked')
635
636 result = []
637
638 last_deps = None
639 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800640 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800641 last_deps = deps
642
643 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800644 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800645
646 for subtree in self.subtrees:
647 for event in subtree.events():
648 result.append(event)
649
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800650 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800651
652 return result
653
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800654 def iter_forest(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800655 """Iterates snapshots of project dependency state.
656
657 Yields:
658 (timestamp, path_specs):
659 timestamp: time of snapshot
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800660 forest: A dict indicates path => deps mapping
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800661 """
662 forest = {}
663 # Group by timestamp
664 for timestamp, events in itertools.groupby(self.events(),
665 operator.itemgetter(0)):
666 # It's possible that one deps is removed and added at the same timestamp,
667 # i.e. modification, so use counter to track.
668 end_counter = collections.Counter()
669
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800670 for timestamp, entry, deps, end in events:
671 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800672 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800673 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800674 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800675 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800676
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800677 yield timestamp, forest
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800678
679 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800680 for entry, count in end_counter.items():
681 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800682 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800683 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800684
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800685 def iter_path_specs(self):
686 """Iterates snapshots of project dependency state.
687
688 Yields:
689 (timestamp, path_specs):
690 timestamp: time of snapshot
691 path_specs: dict of path_spec entries
692 """
693 for timestamp, forest in self.iter_forest():
694 path_specs = {}
695 # Merge Deps at time `timestamp` into single path_specs.
696 for deps in forest.values():
697 for path, dep in deps.entries.items():
698 path_specs[path] = dep.as_path_spec()
699 yield timestamp, path_specs
700
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800701
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800702class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800703 """Gclient DEPS file parser."""
704
705 def __init__(self, project_root, code_storage):
706 self.project_root = project_root
707 self.code_storage = code_storage
708
Kuang-che Wu067ff292019-02-14 18:16:23 +0800709 def load_single_deps(self, content):
710
711 def var_function(name):
712 return '{%s}' % name
713
Zheng-Jie Chang381e4162020-08-03 14:26:54 +0800714 def str_function(name):
715 return str(name)
716
717 global_scope = dict(Var=var_function, Str=str_function)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800718 local_scope = {}
Kuang-che Wuc0baf752020-06-29 11:32:26 +0800719 six.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)