blob: bfdff530f1e82aa7c965f7fd1726b5f757f5621e [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
33
Kuang-che Wu41e8b592018-09-25 17:01:30 +080034def config(gclient_dir,
35 url=None,
36 cache_dir=None,
37 deps_file=None,
38 custom_var=None,
39 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080040 """Simply wrapper of `gclient config`.
41
42 Args:
43 gclient_dir: root directory of gclient project
44 url: URL of gclient configuration files
45 cache_dir: gclient's git cache folder
46 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080047 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080048 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080049 """
50 cmd = ['gclient', 'config']
51 if deps_file:
52 cmd += ['--deps-file', deps_file]
53 if cache_dir:
54 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080055 if custom_var:
56 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080057 if spec:
58 cmd += ['--spec', spec]
59 if url:
60 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080061
62 util.check_call(*cmd, cwd=gclient_dir)
63
64
Kuang-che Wudc714412018-10-17 16:06:39 +080065def sync(gclient_dir,
66 with_branch_heads=False,
67 with_tags=False,
68 ignore_locks=False,
69 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080070 """Simply wrapper of `gclient sync`.
71
72 Args:
73 gclient_dir: root directory of gclient project
74 with_branch_heads: whether to clone git `branch_heads` refspecs
75 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080076 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080077 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
101 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
102 # reasons, it will leave annoying lock files on disk and thus unfriendly to
103 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
104 # this function) and bypass gclient's.
105 if ignore_locks:
106 cmd.append('--ignore_locks')
107
Kuang-che Wu067ff292019-02-14 18:16:23 +0800108 try:
109 old_projects = load_gclient_entries(gclient_dir)
110 except IOError:
111 old_projects = {}
112
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800113 util.check_call(*cmd, cwd=gclient_dir)
114
Kuang-che Wu067ff292019-02-14 18:16:23 +0800115 # Remove dead .git folder after sync.
116 # Ideally, this should be handled by gclient but sometimes gclient didn't
117 # (crbug/930047).
118 new_projects = load_gclient_entries(gclient_dir)
119 for path in old_projects:
120 if path in new_projects:
121 continue
122 old_git_dir = os.path.join(gclient_dir, path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800123 if not os.path.exists(old_git_dir):
124 continue
125
126 if git_util.is_git_root(old_git_dir):
Kuang-che Wu067ff292019-02-14 18:16:23 +0800127 logger.warning(
128 '%s was removed from .gclient_entries but %s still exists; remove it',
129 path, old_git_dir)
130 shutil.rmtree(old_git_dir)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800131 else:
132 logger.warning(
133 '%s was removed from .gclient_entries but %s still exists;'
134 ' keep it because it is not git root', path, old_git_dir)
135
136
137def runhook(gclient_dir, jobs=8):
138 """Simply wrapper of `gclient runhook`.
139
140 Args:
141 gclient_dir: root directory of gclient project
142 jobs: how many workers running in parallel
143 """
144 util.check_call('gclient', 'runhook', '--jobs', str(jobs), cwd=gclient_dir)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800145
146
147def load_gclient_entries(gclient_dir):
148 """Loads .gclient_entries."""
149 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
150 scope = {}
Kuang-che Wud2646f42019-11-27 18:31:08 +0800151 with open(repo_project_list) as f:
152 code = compile(f.read(), repo_project_list, 'exec')
153 six.exec_(code, scope)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800154 entries = scope.get('entries', {})
155
156 # normalize path: remove trailing slash
157 entries = dict((os.path.normpath(path), url) for path, url in entries.items())
158
159 return entries
Kuang-che Wu067ff292019-02-14 18:16:23 +0800160
161
162def write_gclient_entries(gclient_dir, projects):
163 """Writes .gclient_entries."""
164 repo_project_list = os.path.join(gclient_dir, '.gclient_entries')
165 content = 'entries = {\n'
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800166 for path, repo_url in sorted(projects.items()):
167 content += ' %s: %s,\n' % (pprint.pformat(path), pprint.pformat(repo_url))
Kuang-che Wu067ff292019-02-14 18:16:23 +0800168 content += '}\n'
169 with open(repo_project_list, 'w') as f:
170 f.write(content)
171
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800172
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800173def mirror(code_storage, repo_url):
174 """Mirror git repo.
175
176 This function mimics the caching behavior of 'gclient sync' with 'cache_dir'.
177
178 Args:
179 code_storage: CodeStorage object
180 repo_url: remote repo url
181 """
182 logger.info('mirror %s', repo_url)
183 tmp_dir = tempfile.mkdtemp(dir=code_storage.cache_dir)
184 git_root = code_storage.cached_git_root(repo_url)
185 assert not os.path.exists(git_root)
186
187 util.check_call('git', 'init', '--bare', cwd=tmp_dir)
188
189 # These config parameters are copied from gclient.
190 git_util.config(tmp_dir, 'gc.autodetach', '0')
191 git_util.config(tmp_dir, 'gc.autopacklimit', '0')
192 git_util.config(tmp_dir, 'core.deltaBaseCacheLimit', '2g')
193 git_util.config(tmp_dir, 'remote.origin.url', repo_url)
194 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
195 '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*')
196 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
197 '+refs/tags/*:refs/tags/*', r'\+refs/tags/\*:.*')
198 git_util.config(tmp_dir, '--replace-all', 'remote.origin.fetch',
199 '+refs/branch-heads/*:refs/branch-heads/*',
200 r'\+refs/branch-heads/\*:.*')
201
202 git_util.fetch(tmp_dir, 'origin', '+refs/heads/*:refs/heads/*')
203 git_util.fetch(tmp_dir, 'origin', '+refs/tags/*:refs/tags/*')
204 git_util.fetch(tmp_dir, 'origin', '+refs/branch-heads/*:refs/branch-heads/*')
205
206 # Rename to correct name atomically.
207 os.rename(tmp_dir, git_root)
208
209
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800210# Copied from depot_tools' gclient.py
211_PLATFORM_MAPPING = {
212 'cygwin': 'win',
213 'darwin': 'mac',
214 'linux2': 'linux',
Kuang-che Wud2646f42019-11-27 18:31:08 +0800215 'linux': 'linux',
Kuang-che Wub17b3b92018-09-04 18:12:11 +0800216 'win32': 'win',
217 'aix6': 'aix',
218}
219
220
221def _detect_host_os():
222 return _PLATFORM_MAPPING[sys.platform]
223
224
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800225class Dep:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800226 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800227
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800228 One Dep object means one subproject inside DEPS file. It recorded what to
229 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800230
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800231 Attributes:
232 path: subproject path, relative to project root
233 variables: the variables of the containing DEPS file; these variables will
234 be applied to fields of this object (like 'url' and 'condition') and
235 children projects.
236 condition: whether to checkout this subproject
237 dep_type: 'git' or 'cipd'
238 url: if dep_type='git', the url of remote repo and associated branch/commit
239 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800240 """
241
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800242 def __init__(self, path, variables, entry):
243 self.path = path
244 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800245
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800246 self.url = None # only valid for dep_type='git'
247 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800248
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800249 if isinstance(entry, str):
250 self.dep_type = 'git'
251 self.url = entry
252 self.condition = None
253 else:
254 self.dep_type = entry.get('dep_type', 'git')
255 self.condition = entry.get('condition')
256 if self.dep_type == 'git':
257 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800258 else:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800259 assert self.dep_type == 'cipd', 'unknown dep_type:' + self.dep_type
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800260 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800261
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800262 if self.dep_type == 'git':
263 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800264
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800265 def __eq__(self, rhs):
266 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800267
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800268 def __ne__(self, rhs):
269 return not self.__eq__(rhs)
270
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800271 def set_url(self, repo_url, at):
272 assert self.dep_type == 'git'
273 self.url = '%s@%s' % (repo_url, at)
274
275 def set_revision(self, at):
276 assert self.dep_type == 'git'
277 repo_url, _ = self.parse_url()
278 self.set_url(repo_url, at)
279
280 def parse_url(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800281 assert self.dep_type == 'git'
282
283 if '@' in self.url:
284 repo_url, at = self.url.split('@')
285 else:
286 # If the dependency is not pinned, the default is master branch.
287 repo_url, at = self.url, 'master'
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800288 return repo_url, at
289
290 def as_path_spec(self):
291 repo_url, at = self.parse_url()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800292 return codechange.PathSpec(self.path, repo_url, at)
293
294 def eval_condition(self):
295 """Evaluate condition for DEPS parsing.
296
297 Returns:
298 eval result
299 """
300 if not self.condition:
301 return True
302
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800303 # Currently, we only support chromeos as target_os.
304 # TODO(kcwu): make it configurable if we need to bisect for other os.
Kuang-che Wu0d7409c2019-03-18 12:29:03 +0800305 # We don't specify `target_os_only`, so `unix` will be considered by
306 # gclient as well.
307 target_os = ['chromeos', 'unix']
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800308
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800309 vars_dict = {
Kuang-che Wu3e182ec2019-02-21 15:04:30 +0800310 'checkout_android': 'android' in target_os,
311 'checkout_chromeos': 'chromeos' in target_os,
312 'checkout_fuchsia': 'fuchsia' in target_os,
313 'checkout_ios': 'ios' in target_os,
314 'checkout_linux': 'unix' in target_os,
315 'checkout_mac': 'mac' in target_os,
316 'checkout_win': 'win' in target_os,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800317 # default cpu: x64
318 'checkout_arm64': False,
319 'checkout_arm': False,
320 'checkout_mips': False,
321 'checkout_ppc': False,
322 'checkout_s390': False,
323 'checkout_x64': True,
324 'checkout_x86': False,
325 'host_os': _detect_host_os(),
326 'False': False,
327 'None': None,
328 'True': True,
329 }
330 vars_dict.update(self.variables)
331 # pylint: disable=eval-used
332 return eval(self.condition, vars_dict)
333
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800334 def to_lines(self):
335 s = []
336 condition_part = ([' "condition": %r,' %
337 self.condition] if self.condition else [])
338 if self.dep_type == 'cipd':
339 s.extend([
340 ' "%s": {' % (self.path.split(':')[0],),
341 ' "packages": [',
342 ])
343 for p in sorted(self.packages, key=lambda x: x['package']):
344 s.extend([
345 ' {',
346 ' "package": "%s",' % p['package'],
347 ' "version": "%s",' % p['version'],
348 ' },',
349 ])
350 s.extend([
351 ' ],',
352 ' "dep_type": "cipd",',
353 ] + condition_part + [
354 ' },',
355 '',
356 ])
357 else:
358 s.extend([
359 ' "%s": {' % (self.path,),
360 ' "url": "%s",' % (self.url,),
361 ] + condition_part + [
362 ' },',
363 '',
364 ])
365 return s
366
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800367
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800368class Deps:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800369 """DEPS parsed result.
370
371 Attributes:
372 variables: 'vars' dict in DEPS file; these variables will be applied
373 recursively to children.
374 entries: dict of Dep objects
375 recursedeps: list of recursive projects
376 """
377
378 def __init__(self):
379 self.variables = {}
380 self.entries = {}
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800381 self.ignored_entries = {}
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800382 self.recursedeps = []
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800383 self.allowed_hosts = set()
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800384 self.gn_args_from = None
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800385 self.gn_args_file = None
386 self.gn_args = []
387 self.hooks = []
388 self.pre_deps_hooks = []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800389 self.modified = set()
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800390
391 def _gn_settings_to_lines(self):
392 s = []
393 if self.gn_args_file:
394 s.extend([
395 'gclient_gn_args_file = "%s"' % self.gn_args_file,
396 'gclient_gn_args = %r' % self.gn_args,
397 ])
398 return s
399
400 def _allowed_hosts_to_lines(self):
401 """Converts |allowed_hosts| set to list of lines for output."""
402 if not self.allowed_hosts:
403 return []
404 s = ['allowed_hosts = [']
405 for h in sorted(self.allowed_hosts):
406 s.append(' "%s",' % h)
407 s.extend([']', ''])
408 return s
409
410 def _entries_to_lines(self):
411 """Converts |entries| dict to list of lines for output."""
412 entries = self.ignored_entries
413 entries.update(self.entries)
414 if not entries:
415 return []
416 s = ['deps = {']
417 for _, dep in sorted(entries.items()):
418 s.extend(dep.to_lines())
419 s.extend(['}', ''])
420 return s
421
422 def _vars_to_lines(self):
423 """Converts |variables| dict to list of lines for output."""
424 if not self.variables:
425 return []
426 s = ['vars = {']
427 for key, value in sorted(self.variables.items()):
428 s.extend([
429 ' "%s": %r,' % (key, value),
430 '',
431 ])
432 s.extend(['}', ''])
433 return s
434
435 def _hooks_to_lines(self, name, hooks):
436 """Converts |hooks| list to list of lines for output."""
437 if not hooks:
438 return []
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800439 hooks.sort(key=lambda x: x.get('name', ''))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800440 s = ['%s = [' % name]
441 for hook in hooks:
442 s.extend([
443 ' {',
444 ])
445 if hook.get('name') is not None:
446 s.append(' "name": "%s",' % hook.get('name'))
447 if hook.get('pattern') is not None:
448 s.append(' "pattern": "%s",' % hook.get('pattern'))
449 if hook.get('condition') is not None:
450 s.append(' "condition": %r,' % hook.get('condition'))
451 # Flattened hooks need to be written relative to the root gclient dir
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800452 cwd = os.path.relpath(os.path.normpath(hook.get('cwd', '.')))
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800453 s.extend([' "cwd": "%s",' % cwd] + [' "action": ['] +
454 [' "%s",' % arg for arg in hook.get('action', [])] +
455 [' ]', ' },', ''])
456 s.extend([']', ''])
457 return s
458
459 def to_string(self):
460 """Return flatten DEPS string."""
461 return '\n'.join(
462 self._gn_settings_to_lines() + self._allowed_hosts_to_lines() +
463 self._entries_to_lines() + self._hooks_to_lines('hooks', self.hooks) +
464 self._hooks_to_lines('pre_deps_hooks', self.pre_deps_hooks) +
465 self._vars_to_lines() + ['']) # Ensure newline at end of file.
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800466
Zheng-Jie Changc0d3cd72020-06-03 02:46:43 +0800467 def remove_src(self):
468 """Return src_revision for buildbucket use."""
469 assert 'src' in self.entries
470 _, src_rev = self.entries['src'].parse_url()
471 del self.entries['src']
472 return src_rev
473
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800474 def apply_commit(self, path, revision, overwrite=True):
475 """Set revision to a project by path.
476
477 Args:
478 path: A project's path.
479 revision: A git commit id.
480 overwrite: Overwrite flag, the project won't change if overwrite=False
481 and it was modified before.
482 """
483 if path in self.modified and not overwrite:
484 return
485 self.modified.add(path)
486
487 if path not in self.entries:
488 logger.warning('path: %s not found in DEPS', path)
489 return
490 self.entries[path].set_revision(revision)
491
492 def apply_action_groups(self, action_groups):
493 """Apply multiple action groups to DEPS.
494
495 If there are multiple actions in one repo, only last one is applied.
496
497 Args:
498 action_groups: A list of action groups.
499 """
500 # Apply in reversed order with overwrite=False,
501 # so each repo is on the state of last action.
502 for action_group in reversed(action_groups):
503 for action in reversed(action_group.actions):
504 if isinstance(action, codechange.GitCheckoutCommit):
505 self.apply_commit(action.path, action.rev, overwrite=False)
506 if isinstance(action, codechange.GitAddRepo):
507 self.apply_commit(action.path, action.rev, overwrite=False)
508 if isinstance(action, codechange.GitRemoveRepo):
509 assert action.path not in self.entries
510 self.modified.add(action.path)
511
512 def apply_deps(self, deps):
513 for path, dep in deps.entries.items():
514 if path in self.entries:
515 _, rev = dep.parse_url()
516 self.apply_commit(path, rev, overwrite=False)
517
518 # hooks, vars, ignored_entries are ignored and should be set by float_spec
519
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800520
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800521class TimeSeriesTree:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800522 """Data structure for generating snapshots of historical dependency tree.
523
524 This is a tree structure with time information. Each tree node represents not
525 only typical tree data and tree children information, but also historical
526 value of those tree data and tree children.
527
528 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
529 represent a DEPS file. The caller will add_snapshot() to add parsed result of
530 historical DEPS instances. After that, the tree root of this class can
531 reconstruct the every historical moment of the project dependency state.
532
533 This class is slight abstraction of git_util.get_history_recursively() to
534 support more than single git repo and be version control system independent.
535 """
536
537 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
538
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800539 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800540 """TimeSeriesTree constructor.
541
542 Args:
543 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800544 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800545 start_time: start time
546 end_time: end time
547 """
548 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800549 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800550 self.snapshots = {}
551 self.start_time = start_time
552 self.end_time = end_time
553
554 # Intermediate dict to keep track alive children for the time being.
555 # Maintained by add_snapshot() and no_more_snapshot().
556 self.alive_children = {}
557
558 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800559 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800560 # once in this list because they are removed and added back to the DEPS
561 # file.
562 self.subtrees = []
563
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800564 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800565 """Compares subtree of two Deps.
566
567 Args:
568 deps_a: Deps object
569 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800570 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800571
572 Returns:
573 True if the said subtree of these two Deps equal
574 """
575 # Need to compare variables because they may influence subtree parsing
576 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800577 path = child_entry[0]
578 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800579 deps_a.variables == deps_b.variables)
580
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800581 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800582 """Adds parsed DEPS result and children.
583
584 For example, if a given DEPS file has N revisions between start_time and
585 end_time, the caller should call this method N times to feed all parsed
586 results in order (timestamp increasing).
587
588 Args:
589 timestamp: timestamp of `deps`
590 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800591 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800592 """
593 assert timestamp not in self.snapshots
594 self.snapshots[timestamp] = deps
595
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800596 for child_entry in set(list(self.alive_children.keys()) + children_entries):
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800597 # `child_entry` is added at `timestamp`
598 if child_entry not in self.alive_children:
599 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800600
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800601 # `child_entry` is removed at `timestamp`
602 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800603 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800604 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
605 self.alive_children[child_entry][0], timestamp))
606 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800607
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800608 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800609 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800610 last_deps = self.alive_children[child_entry][1]
611 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800612 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800613 TimeSeriesTree(last_deps, child_entry,
614 self.alive_children[child_entry][0], timestamp))
615 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800616
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800617 def no_more_snapshot(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800618 """Indicates all snapshots are added.
619
620 add_snapshot() should not be invoked after no_more_snapshot().
621 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800622 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800623 if timestamp == self.end_time:
624 continue
625 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800626 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800627 self.alive_children = None
628
629 def events(self):
630 """Gets children added/removed events of this subtree.
631
632 Returns:
633 list of (timestamp, deps_name, deps, end_flag):
634 timestamp: timestamp of event
635 deps_name: name of this subtree
636 deps: Deps object of given project
637 end_flag: True indicates this is the last event of this deps tree
638 """
639 assert self.snapshots
640 assert self.alive_children is None, ('events() is valid only after '
641 'no_more_snapshot() is invoked')
642
643 result = []
644
645 last_deps = None
646 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800647 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800648 last_deps = deps
649
650 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800651 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800652
653 for subtree in self.subtrees:
654 for event in subtree.events():
655 result.append(event)
656
Kuang-che Wu1ad2c0e2019-02-26 00:41:10 +0800657 result.sort(key=lambda x: x[0])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800658
659 return result
660
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800661 def iter_forest(self):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800662 """Iterates snapshots of project dependency state.
663
664 Yields:
665 (timestamp, path_specs):
666 timestamp: time of snapshot
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800667 forest: A dict indicates path => deps mapping
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800668 """
669 forest = {}
670 # Group by timestamp
671 for timestamp, events in itertools.groupby(self.events(),
672 operator.itemgetter(0)):
673 # It's possible that one deps is removed and added at the same timestamp,
674 # i.e. modification, so use counter to track.
675 end_counter = collections.Counter()
676
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800677 for timestamp, entry, deps, end in events:
678 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800679 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800680 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800681 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800682 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800683
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800684 yield timestamp, forest
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800685
686 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800687 for entry, count in end_counter.items():
688 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800689 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800690 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800691
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800692 def iter_path_specs(self):
693 """Iterates snapshots of project dependency state.
694
695 Yields:
696 (timestamp, path_specs):
697 timestamp: time of snapshot
698 path_specs: dict of path_spec entries
699 """
700 for timestamp, forest in self.iter_forest():
701 path_specs = {}
702 # Merge Deps at time `timestamp` into single path_specs.
703 for deps in forest.values():
704 for path, dep in deps.entries.items():
705 path_specs[path] = dep.as_path_spec()
706 yield timestamp, path_specs
707
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800708
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800709class DepsParser:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800710 """Gclient DEPS file parser."""
711
712 def __init__(self, project_root, code_storage):
713 self.project_root = project_root
714 self.code_storage = code_storage
715
Kuang-che Wu067ff292019-02-14 18:16:23 +0800716 def load_single_deps(self, content):
717
718 def var_function(name):
719 return '{%s}' % name
720
721 global_scope = dict(Var=var_function)
722 local_scope = {}
Kuang-che Wuc0baf752020-06-29 11:32:26 +0800723 six.exec_(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800724 return local_scope
725
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800726 def parse_single_deps(self,
727 content,
728 parent_vars=None,
729 parent_path='',
730 parent_dep=None):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800731 """Parses DEPS file without recursion.
732
733 Args:
734 content: file content of DEPS file
735 parent_vars: variables inherent from parent DEPS
736 parent_path: project path of parent DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800737 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800738
739 Returns:
740 Deps object
741 """
742
Kuang-che Wu067ff292019-02-14 18:16:23 +0800743 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800744 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800745
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800746 local_scope.setdefault('vars', {})
747 if parent_vars:
748 local_scope['vars'].update(parent_vars)
749 deps.variables = local_scope['vars']
750
751 # Warnings for old usages which we don't support.
752 for name in deps.variables:
753 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
754 logger.warning('%s is deprecated and not supported recursion syntax',
755 name)
756 if 'deps_os' in local_scope:
757 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800758
759 if 'allowed_hosts' in local_scope:
760 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
761 deps.hooks = local_scope.get('hooks', [])
762 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800763 deps.gn_args_from = local_scope.get('gclient_gn_args_from')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800764 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
765 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800766
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800767 # recalculate hook path
768 use_relative_hooks = local_scope.get('use_relative_hooks', False)
769 if use_relative_hooks:
770 assert local_scope.get('use_relative_paths', False)
771 for hook in deps.hooks:
772 hook['cwd'] = os.path.join(parent_path, hook.get('cwd', ''))
773 for pre_deps_hook in deps.pre_deps_hooks:
774 pre_deps_hook['cwd'] = os.path.join(parent_path,
775 pre_deps_hook.get('cwd', ''))
776
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800777 for path, dep_entry in local_scope['deps'].items():
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800778 # recalculate path
Kuang-che Wu058ac042019-03-18 14:14:53 +0800779 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800780 if local_scope.get('use_relative_paths', False):
781 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800782 path = os.path.normpath(path)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800783
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800784 dep = Dep(path, deps.variables, dep_entry)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800785 eval_condition = dep.eval_condition()
786
787 # update condition
788 if parent_dep and parent_dep.condition:
789 tmp_dict = {'condition': dep.condition}
790 gclient_eval.UpdateCondition(tmp_dict, 'and', parent_dep.condition)
791 dep.condition = tmp_dict['condition']
792
793 if not eval_condition:
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800794 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800795 continue
796
797 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
798 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800799 warning_key = ('dep_type', dep.dep_type, path)
800 if warning_key not in emitted_warnings:
801 emitted_warnings.add(warning_key)
802 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
803 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800804 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800805 continue
806
807 deps.entries[path] = dep
808
809 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800810 for recurse_entry in local_scope.get('recursedeps', []):
811 # Normalize entries.
812 if isinstance(recurse_entry, tuple):
813 path, deps_file = recurse_entry
814 else:
815 assert isinstance(path, str)
816 path, deps_file = recurse_entry, 'DEPS'
817
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800818 if local_scope.get('use_relative_paths', False):
819 path = os.path.join(parent_path, path)
820 path = path.format(**deps.variables)
821 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800822 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800823 deps.recursedeps = recursedeps
824
825 return deps
826
827 def construct_deps_tree(self,
828 tstree,
829 repo_url,
830 at,
831 after,
832 before,
833 parent_vars=None,
834 parent_path='',
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800835 parent_dep=None,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800836 deps_file='DEPS'):
837 """Processes DEPS recursively of given time period.
838
839 This method parses all commits of DEPS between time `after` and `before`,
840 segments recursive dependencies into subtrees if they are changed, and
841 processes subtrees recursively.
842
843 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
844
845 Args:
846 tstree: TimeSeriesTree object
847 repo_url: remote repo url
848 at: branch or git commit id
849 after: begin of period
850 before: end of period
851 parent_vars: DEPS variables inherit from parent DEPS (including
852 custom_vars)
853 parent_path: the path of parent project of current DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800854 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800855 deps_file: filename of DEPS file, relative to the git repo, repo_rul
856 """
857 if '://' in repo_url:
858 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800859 if not os.path.exists(git_repo):
860 with locking.lock_file(
861 os.path.join(self.code_storage.cache_dir,
862 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
863 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800864 else:
865 git_repo = repo_url
866
867 if git_util.is_git_rev(at):
868 history = [
869 (after, at),
870 (before, at),
871 ]
872 else:
873 history = git_util.get_history(
874 git_repo,
875 deps_file,
876 branch=at,
877 after=after,
878 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800879 padding_begin=True,
880 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800881 assert history
882
883 # If not equal, it means the file was deleted but is still referenced by
884 # its parent.
885 assert history[-1][0] == before
886
887 # TODO(kcwu): optimization: history[-1] is unused
888 for timestamp, git_rev in history[:-1]:
889 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
890
891 deps = self.parse_single_deps(
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800892 content,
893 parent_vars=parent_vars,
894 parent_path=parent_path,
895 parent_dep=parent_dep)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800896 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
897
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800898 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800899
900 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800901 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800902 path_spec = subtree.parent_deps.entries[path].as_path_spec()
903 self.construct_deps_tree(
904 subtree,
905 path_spec.repo_url,
906 path_spec.at,
907 subtree.start_time,
908 subtree.end_time,
909 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800910 parent_path=path,
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800911 parent_dep=subtree.parent_deps.entries[path],
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800912 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800913
914 def enumerate_path_specs(self, start_time, end_time, path):
915 tstree = TimeSeriesTree(None, path, start_time, end_time)
916 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
917 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800918
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800919 def enumerate_gclient_solutions(self, start_time, end_time, path):
920 tstree = TimeSeriesTree(None, path, start_time, end_time)
921 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
922 return tstree.iter_forest()
923
924 def flatten(self, solutions, entry_point):
925 """Flatten all given Deps
926
927 Args:
928 solutions: A name => Deps dict, name can be either a str or a tuple.
929 entry_point (str): An entry_point name of solutions.
930
931 Returns:
932 Deps: A flatten Deps.
933 """
934
935 def _add_unvisited_recursedeps(deps_queue, visited, deps):
936 for name in deps.recursedeps:
937 if name not in visited:
938 visited.add(name)
939 deps_queue.put(name)
940
941 result = solutions[entry_point]
942 deps_queue = queue.SimpleQueue()
943 visited = set()
944 visited.add(entry_point)
945 _add_unvisited_recursedeps(deps_queue, visited, solutions[entry_point])
946
947 # BFS to merge `deps` into `result`
948 while not deps_queue.empty():
949 deps_name = deps_queue.get()
950 deps = solutions[deps_name]
951
952 result.allowed_hosts.update(deps.allowed_hosts)
953 for key, value in deps.variables.items():
954 assert key not in result.variables or deps.variables[key] == value
955 result.variables[key] = value
956 result.pre_deps_hooks += deps.pre_deps_hooks
957 result.hooks += deps.hooks
958
959 for dep in deps.entries.values():
960 assert dep.path not in result.entries or result.entries.get(
961 dep.path) == dep
962 result.entries[dep.path] = dep
963
964 for dep in deps.ignored_entries.values():
965 assert (dep.path not in result.ignored_entries or
966 result.ignored_entries.get(dep.path) == dep)
967 result.ignored_entries[dep.path] = dep
968
969 _add_unvisited_recursedeps(deps_queue, visited, deps)
970
971 # If gn_args_from is set in root DEPS, overwrite gn arguments
972 if solutions[entry_point].gn_args_from:
973 gn_args_dep = solutions[(solutions[entry_point].gn_args_from, 'DEPS')]
974 result.gn_args = gn_args_dep.gn_args
975 result.gn_args_file = gn_args_dep.gn_args_file
976
977 return result
978
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800979
980class GclientCache(codechange.CodeStorage):
981 """Gclient git cache."""
982
983 def __init__(self, cache_dir):
984 self.cache_dir = cache_dir
985
986 def _url_to_cache_dir(self, url):
987 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800988 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800989 norm_url = parsed.netloc + parsed.path
990 if norm_url.endswith('.git'):
991 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800992 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800993 return norm_url.replace('-', '--').replace('/', '-').lower()
994
995 def cached_git_root(self, repo_url):
996 cache_path = self._url_to_cache_dir(repo_url)
997 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800998
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800999 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001000 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001001
1002 projects[path] = repo_url
1003
Kuang-che Wu067ff292019-02-14 18:16:23 +08001004 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001005
1006 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001007 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001008
1009 if path in projects:
1010 del projects[path]
1011
Kuang-che Wu067ff292019-02-14 18:16:23 +08001012 write_gclient_entries(project_root, projects)