blob: ef5ac8dcc33c554ce06db8913ca64eca66d6a722 [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
Zheng-Jie Chang381e4162020-08-03 14:26:54 +0800721 def str_function(name):
722 return str(name)
723
724 global_scope = dict(Var=var_function, Str=str_function)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800725 local_scope = {}
Kuang-che Wuc0baf752020-06-29 11:32:26 +0800726 six.exec_(content, global_scope, local_scope)
Kuang-che Wu067ff292019-02-14 18:16:23 +0800727 return local_scope
728
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800729 def parse_single_deps(self,
730 content,
731 parent_vars=None,
732 parent_path='',
733 parent_dep=None):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800734 """Parses DEPS file without recursion.
735
736 Args:
737 content: file content of DEPS file
738 parent_vars: variables inherent from parent DEPS
739 parent_path: project path of parent DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800740 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800741
742 Returns:
743 Deps object
744 """
745
Kuang-che Wu067ff292019-02-14 18:16:23 +0800746 local_scope = self.load_single_deps(content)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800747 deps = Deps()
Kuang-che Wu067ff292019-02-14 18:16:23 +0800748
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800749 local_scope.setdefault('vars', {})
750 if parent_vars:
751 local_scope['vars'].update(parent_vars)
752 deps.variables = local_scope['vars']
753
754 # Warnings for old usages which we don't support.
755 for name in deps.variables:
756 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
757 logger.warning('%s is deprecated and not supported recursion syntax',
758 name)
759 if 'deps_os' in local_scope:
760 logger.warning('deps_os is no longer supported')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800761
762 if 'allowed_hosts' in local_scope:
763 deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
764 deps.hooks = local_scope.get('hooks', [])
765 deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800766 deps.gn_args_from = local_scope.get('gclient_gn_args_from')
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800767 deps.gn_args_file = local_scope.get('gclient_gn_args_file')
768 deps.gn_args = local_scope.get('gclient_gn_args', [])
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800769
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800770 # recalculate hook path
771 use_relative_hooks = local_scope.get('use_relative_hooks', False)
772 if use_relative_hooks:
773 assert local_scope.get('use_relative_paths', False)
774 for hook in deps.hooks:
775 hook['cwd'] = os.path.join(parent_path, hook.get('cwd', ''))
776 for pre_deps_hook in deps.pre_deps_hooks:
777 pre_deps_hook['cwd'] = os.path.join(parent_path,
778 pre_deps_hook.get('cwd', ''))
779
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800780 for path, dep_entry in local_scope['deps'].items():
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800781 # recalculate path
Kuang-che Wu058ac042019-03-18 14:14:53 +0800782 path = path.format(**deps.variables)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800783 if local_scope.get('use_relative_paths', False):
784 path = os.path.join(parent_path, path)
Kuang-che Wu058ac042019-03-18 14:14:53 +0800785 path = os.path.normpath(path)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800786
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800787 dep = Dep(path, deps.variables, dep_entry)
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800788 eval_condition = dep.eval_condition()
789
790 # update condition
791 if parent_dep and parent_dep.condition:
792 tmp_dict = {'condition': dep.condition}
793 gclient_eval.UpdateCondition(tmp_dict, 'and', parent_dep.condition)
794 dep.condition = tmp_dict['condition']
795
796 if not eval_condition:
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800797 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800798 continue
799
800 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
801 if dep.dep_type != 'git':
Kuang-che Wuced2dbf2019-01-30 23:13:24 +0800802 warning_key = ('dep_type', dep.dep_type, path)
803 if warning_key not in emitted_warnings:
804 emitted_warnings.add(warning_key)
805 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
806 path)
Zheng-Jie Chang7ab92082020-05-29 19:39:59 +0800807 deps.ignored_entries[path] = dep
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800808 continue
809
810 deps.entries[path] = dep
811
812 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800813 for recurse_entry in local_scope.get('recursedeps', []):
814 # Normalize entries.
815 if isinstance(recurse_entry, tuple):
816 path, deps_file = recurse_entry
817 else:
818 assert isinstance(path, str)
819 path, deps_file = recurse_entry, 'DEPS'
820
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800821 if local_scope.get('use_relative_paths', False):
822 path = os.path.join(parent_path, path)
823 path = path.format(**deps.variables)
824 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800825 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800826 deps.recursedeps = recursedeps
827
828 return deps
829
830 def construct_deps_tree(self,
831 tstree,
832 repo_url,
833 at,
834 after,
835 before,
836 parent_vars=None,
837 parent_path='',
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800838 parent_dep=None,
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800839 deps_file='DEPS'):
840 """Processes DEPS recursively of given time period.
841
842 This method parses all commits of DEPS between time `after` and `before`,
843 segments recursive dependencies into subtrees if they are changed, and
844 processes subtrees recursively.
845
846 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
847
848 Args:
849 tstree: TimeSeriesTree object
850 repo_url: remote repo url
851 at: branch or git commit id
852 after: begin of period
853 before: end of period
854 parent_vars: DEPS variables inherit from parent DEPS (including
855 custom_vars)
856 parent_path: the path of parent project of current DEPS file
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800857 parent_dep: A corresponding Dep object in parent DEPS
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800858 deps_file: filename of DEPS file, relative to the git repo, repo_rul
859 """
860 if '://' in repo_url:
861 git_repo = self.code_storage.cached_git_root(repo_url)
Kuang-che Wu1e49f512018-12-06 15:27:42 +0800862 if not os.path.exists(git_repo):
863 with locking.lock_file(
864 os.path.join(self.code_storage.cache_dir,
865 locking.LOCK_FILE_FOR_MIRROR_SYNC)):
866 mirror(self.code_storage, repo_url)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800867 else:
868 git_repo = repo_url
869
870 if git_util.is_git_rev(at):
871 history = [
872 (after, at),
873 (before, at),
874 ]
875 else:
876 history = git_util.get_history(
877 git_repo,
878 deps_file,
879 branch=at,
880 after=after,
881 before=before,
Zheng-Jie Chang313eec32020-02-18 16:17:07 +0800882 padding_begin=True,
883 padding_end=True)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800884 assert history
885
886 # If not equal, it means the file was deleted but is still referenced by
887 # its parent.
888 assert history[-1][0] == before
889
890 # TODO(kcwu): optimization: history[-1] is unused
891 for timestamp, git_rev in history[:-1]:
892 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
893
894 deps = self.parse_single_deps(
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800895 content,
896 parent_vars=parent_vars,
897 parent_path=parent_path,
898 parent_dep=parent_dep)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800899 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
900
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800901 tstree.no_more_snapshot()
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800902
903 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800904 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800905 path_spec = subtree.parent_deps.entries[path].as_path_spec()
906 self.construct_deps_tree(
907 subtree,
908 path_spec.repo_url,
909 path_spec.at,
910 subtree.start_time,
911 subtree.end_time,
912 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800913 parent_path=path,
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800914 parent_dep=subtree.parent_deps.entries[path],
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800915 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800916
917 def enumerate_path_specs(self, start_time, end_time, path):
918 tstree = TimeSeriesTree(None, path, start_time, end_time)
919 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
920 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800921
Zheng-Jie Chang8b8a4822020-06-24 13:24:02 +0800922 def enumerate_gclient_solutions(self, start_time, end_time, path):
923 tstree = TimeSeriesTree(None, path, start_time, end_time)
924 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
925 return tstree.iter_forest()
926
927 def flatten(self, solutions, entry_point):
928 """Flatten all given Deps
929
930 Args:
931 solutions: A name => Deps dict, name can be either a str or a tuple.
932 entry_point (str): An entry_point name of solutions.
933
934 Returns:
935 Deps: A flatten Deps.
936 """
937
938 def _add_unvisited_recursedeps(deps_queue, visited, deps):
939 for name in deps.recursedeps:
940 if name not in visited:
941 visited.add(name)
942 deps_queue.put(name)
943
944 result = solutions[entry_point]
945 deps_queue = queue.SimpleQueue()
946 visited = set()
947 visited.add(entry_point)
948 _add_unvisited_recursedeps(deps_queue, visited, solutions[entry_point])
949
950 # BFS to merge `deps` into `result`
951 while not deps_queue.empty():
952 deps_name = deps_queue.get()
953 deps = solutions[deps_name]
954
955 result.allowed_hosts.update(deps.allowed_hosts)
956 for key, value in deps.variables.items():
957 assert key not in result.variables or deps.variables[key] == value
958 result.variables[key] = value
959 result.pre_deps_hooks += deps.pre_deps_hooks
960 result.hooks += deps.hooks
961
962 for dep in deps.entries.values():
963 assert dep.path not in result.entries or result.entries.get(
964 dep.path) == dep
965 result.entries[dep.path] = dep
966
967 for dep in deps.ignored_entries.values():
968 assert (dep.path not in result.ignored_entries or
969 result.ignored_entries.get(dep.path) == dep)
970 result.ignored_entries[dep.path] = dep
971
972 _add_unvisited_recursedeps(deps_queue, visited, deps)
973
974 # If gn_args_from is set in root DEPS, overwrite gn arguments
975 if solutions[entry_point].gn_args_from:
976 gn_args_dep = solutions[(solutions[entry_point].gn_args_from, 'DEPS')]
977 result.gn_args = gn_args_dep.gn_args
978 result.gn_args_file = gn_args_dep.gn_args_file
979
980 return result
981
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800982
983class GclientCache(codechange.CodeStorage):
984 """Gclient git cache."""
985
986 def __init__(self, cache_dir):
987 self.cache_dir = cache_dir
988
989 def _url_to_cache_dir(self, url):
990 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800991 parsed = urllib.parse.urlparse(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800992 norm_url = parsed.netloc + parsed.path
993 if norm_url.endswith('.git'):
994 norm_url = norm_url[:-len('.git')]
Kuang-che Wu5fef2912019-04-15 16:18:29 +0800995 norm_url = norm_url.replace('googlesource.com/a/', 'googlesource.com/')
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800996 return norm_url.replace('-', '--').replace('/', '-').lower()
997
998 def cached_git_root(self, repo_url):
999 cache_path = self._url_to_cache_dir(repo_url)
1000 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001001
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001002 def add_to_project_list(self, project_root, path, repo_url):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001003 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001004
1005 projects[path] = repo_url
1006
Kuang-che Wu067ff292019-02-14 18:16:23 +08001007 write_gclient_entries(project_root, projects)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001008
1009 def remove_from_project_list(self, project_root, path):
Kuang-che Wu067ff292019-02-14 18:16:23 +08001010 projects = load_gclient_entries(project_root)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +08001011
1012 if path in projects:
1013 del projects[path]
1014
Kuang-che Wu067ff292019-02-14 18:16:23 +08001015 write_gclient_entries(project_root, projects)