blob: 4a00b90eff59225b51c717ed08f6b2abe7746b94 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08002# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Gclient utility."""
6
7from __future__ import print_function
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +08008import collections
9import itertools
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080010import logging
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080011import operator
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080012import os
Kuang-che Wu6948ecc2018-09-11 17:43:49 +080013import pprint
Kuang-che Wub17b3b92018-09-04 18:12:11 +080014import sys
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080015import urlparse
16
17from bisect_kit import codechange
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080018from bisect_kit import git_util
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080019from bisect_kit import util
20
21logger = logging.getLogger(__name__)
22
23
Kuang-che Wu41e8b592018-09-25 17:01:30 +080024def config(gclient_dir,
25 url=None,
26 cache_dir=None,
27 deps_file=None,
28 custom_var=None,
29 spec=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080030 """Simply wrapper of `gclient config`.
31
32 Args:
33 gclient_dir: root directory of gclient project
34 url: URL of gclient configuration files
35 cache_dir: gclient's git cache folder
36 deps_file: override the default DEPS file name
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080037 custom_var: custom variables
Kuang-che Wu41e8b592018-09-25 17:01:30 +080038 spec: content of gclient file
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080039 """
40 cmd = ['gclient', 'config']
41 if deps_file:
42 cmd += ['--deps-file', deps_file]
43 if cache_dir:
44 cmd += ['--cache-dir', cache_dir]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080045 if custom_var:
46 cmd += ['--custom-var', custom_var]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080047 if spec:
48 cmd += ['--spec', spec]
49 if url:
50 cmd.append(url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080051
52 util.check_call(*cmd, cwd=gclient_dir)
53
54
Kuang-che Wudc714412018-10-17 16:06:39 +080055def sync(gclient_dir,
56 with_branch_heads=False,
57 with_tags=False,
58 ignore_locks=False,
59 jobs=8):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080060 """Simply wrapper of `gclient sync`.
61
62 Args:
63 gclient_dir: root directory of gclient project
64 with_branch_heads: whether to clone git `branch_heads` refspecs
65 with_tags: whether to clone git tags
Kuang-che Wudc714412018-10-17 16:06:39 +080066 ignore_locks: bypass gclient's lock
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080067 jobs: how many workers running in parallel
68 """
Kuang-che Wu88b17342018-10-23 17:19:09 +080069 cmd = ['gclient', 'sync', '--jobs', str(jobs), '--delete_unversioned_trees']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080070 if with_branch_heads:
71 cmd.append('--with_branch_heads')
72 if with_tags:
73 cmd.append('--with_tags')
Kuang-che Wudc714412018-10-17 16:06:39 +080074
75 # If 'gclient sync' is interrupted by ctrl-c or terminated with whatever
76 # reasons, it will leave annoying lock files on disk and thus unfriendly to
77 # bot tasks. In bisect-kit, we will use our own lock mechanism (in caller of
78 # this function) and bypass gclient's.
79 if ignore_locks:
80 cmd.append('--ignore_locks')
81
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080082 util.check_call(*cmd, cwd=gclient_dir)
83
84
Kuang-che Wub17b3b92018-09-04 18:12:11 +080085# Copied from depot_tools' gclient.py
86_PLATFORM_MAPPING = {
87 'cygwin': 'win',
88 'darwin': 'mac',
89 'linux2': 'linux',
90 'win32': 'win',
91 'aix6': 'aix',
92}
93
94
95def _detect_host_os():
96 return _PLATFORM_MAPPING[sys.platform]
97
98
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +080099class Dep(object):
100 """Represent one entry of DEPS's deps.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800101
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800102 One Dep object means one subproject inside DEPS file. It recorded what to
103 checkout (like git or cipd) content of each subproject.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800104
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800105 Attributes:
106 path: subproject path, relative to project root
107 variables: the variables of the containing DEPS file; these variables will
108 be applied to fields of this object (like 'url' and 'condition') and
109 children projects.
110 condition: whether to checkout this subproject
111 dep_type: 'git' or 'cipd'
112 url: if dep_type='git', the url of remote repo and associated branch/commit
113 packages: if dep_type='cipd', cipd package version and location
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800114 """
115
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800116 def __init__(self, path, variables, entry):
117 self.path = path
118 self.variables = variables
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800119
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800120 self.url = None # only valid for dep_type='git'
121 self.packages = None # only valid for dep_type='cipd'
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800122
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800123 if isinstance(entry, str):
124 self.dep_type = 'git'
125 self.url = entry
126 self.condition = None
127 else:
128 self.dep_type = entry.get('dep_type', 'git')
129 self.condition = entry.get('condition')
130 if self.dep_type == 'git':
131 self.url = entry['url']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800132 else:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800133 assert self.dep_type == 'cipd'
134 self.packages = entry['packages']
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800135
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800136 if self.dep_type == 'git':
137 self.url = self.url.format(**self.variables)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800138
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800139 def __eq__(self, rhs):
140 return vars(self) == vars(rhs)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800141
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800142 def __ne__(self, rhs):
143 return not self.__eq__(rhs)
144
145 def as_path_spec(self):
146 assert self.dep_type == 'git'
147
148 if '@' in self.url:
149 repo_url, at = self.url.split('@')
150 else:
151 # If the dependency is not pinned, the default is master branch.
152 repo_url, at = self.url, 'master'
153 return codechange.PathSpec(self.path, repo_url, at)
154
155 def eval_condition(self):
156 """Evaluate condition for DEPS parsing.
157
158 Returns:
159 eval result
160 """
161 if not self.condition:
162 return True
163
164 vars_dict = {
165 # default os: linux
166 'checkout_android': False,
167 'checkout_chromeos': False,
168 'checkout_fuchsia': False,
169 'checkout_ios': False,
170 'checkout_linux': True,
171 'checkout_mac': False,
172 'checkout_win': False,
173 # default cpu: x64
174 'checkout_arm64': False,
175 'checkout_arm': False,
176 'checkout_mips': False,
177 'checkout_ppc': False,
178 'checkout_s390': False,
179 'checkout_x64': True,
180 'checkout_x86': False,
181 'host_os': _detect_host_os(),
182 'False': False,
183 'None': None,
184 'True': True,
185 }
186 vars_dict.update(self.variables)
187 # pylint: disable=eval-used
188 return eval(self.condition, vars_dict)
189
190
191class Deps(object):
192 """DEPS parsed result.
193
194 Attributes:
195 variables: 'vars' dict in DEPS file; these variables will be applied
196 recursively to children.
197 entries: dict of Dep objects
198 recursedeps: list of recursive projects
199 """
200
201 def __init__(self):
202 self.variables = {}
203 self.entries = {}
204 self.recursedeps = []
205
206
207class TimeSeriesTree(object):
208 """Data structure for generating snapshots of historical dependency tree.
209
210 This is a tree structure with time information. Each tree node represents not
211 only typical tree data and tree children information, but also historical
212 value of those tree data and tree children.
213
214 To be more specific in terms of DEPS parsing, one TimeSeriesTree object
215 represent a DEPS file. The caller will add_snapshot() to add parsed result of
216 historical DEPS instances. After that, the tree root of this class can
217 reconstruct the every historical moment of the project dependency state.
218
219 This class is slight abstraction of git_util.get_history_recursively() to
220 support more than single git repo and be version control system independent.
221 """
222
223 # TODO(kcwu): refactor git_util.get_history_recursively() to reuse this class.
224
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800225 def __init__(self, parent_deps, entry, start_time, end_time):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800226 """TimeSeriesTree constructor.
227
228 Args:
229 parent_deps: parent DEPS of the given period. None if this is tree root.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800230 entry: project entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800231 start_time: start time
232 end_time: end time
233 """
234 self.parent_deps = parent_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800235 self.entry = entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800236 self.snapshots = {}
237 self.start_time = start_time
238 self.end_time = end_time
239
240 # Intermediate dict to keep track alive children for the time being.
241 # Maintained by add_snapshot() and no_more_snapshot().
242 self.alive_children = {}
243
244 # All historical children (TimeSeriesTree object) between start_time and
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800245 # end_time. It's possible that children with the same entry appear more than
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800246 # once in this list because they are removed and added back to the DEPS
247 # file.
248 self.subtrees = []
249
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800250 def subtree_eq(self, deps_a, deps_b, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800251 """Compares subtree of two Deps.
252
253 Args:
254 deps_a: Deps object
255 deps_b: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800256 child_entry: the subtree to compare
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800257
258 Returns:
259 True if the said subtree of these two Deps equal
260 """
261 # Need to compare variables because they may influence subtree parsing
262 # behavior
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800263 path = child_entry[0]
264 return (deps_a.entries[path] == deps_b.entries[path] and
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800265 deps_a.variables == deps_b.variables)
266
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800267 def add_snapshot(self, timestamp, deps, children_entries):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800268 """Adds parsed DEPS result and children.
269
270 For example, if a given DEPS file has N revisions between start_time and
271 end_time, the caller should call this method N times to feed all parsed
272 results in order (timestamp increasing).
273
274 Args:
275 timestamp: timestamp of `deps`
276 deps: Deps object
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800277 children_entries: list of names of deps' children
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800278 """
279 assert timestamp not in self.snapshots
280 self.snapshots[timestamp] = deps
281
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800282 for child_entry in set(self.alive_children.keys() + children_entries):
283 # `child_entry` is added at `timestamp`
284 if child_entry not in self.alive_children:
285 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800286
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800287 # `child_entry` is removed at `timestamp`
288 elif child_entry not in children_entries:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800289 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800290 TimeSeriesTree(self.alive_children[child_entry][1], child_entry,
291 self.alive_children[child_entry][0], timestamp))
292 del self.alive_children[child_entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800293
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800294 # `child_entry` is alive before and after `timestamp`
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800295 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800296 last_deps = self.alive_children[child_entry][1]
297 if not self.subtree_eq(last_deps, deps, child_entry):
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800298 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800299 TimeSeriesTree(last_deps, child_entry,
300 self.alive_children[child_entry][0], timestamp))
301 self.alive_children[child_entry] = timestamp, deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800302
303 def no_more_snapshot(self, deps):
304 """Indicates all snapshots are added.
305
306 add_snapshot() should not be invoked after no_more_snapshot().
307 """
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800308 for child_entry, (timestamp, deps) in self.alive_children.items():
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800309 if timestamp == self.end_time:
310 continue
311 self.subtrees.append(
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800312 TimeSeriesTree(deps, child_entry, timestamp, self.end_time))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800313 self.alive_children = None
314
315 def events(self):
316 """Gets children added/removed events of this subtree.
317
318 Returns:
319 list of (timestamp, deps_name, deps, end_flag):
320 timestamp: timestamp of event
321 deps_name: name of this subtree
322 deps: Deps object of given project
323 end_flag: True indicates this is the last event of this deps tree
324 """
325 assert self.snapshots
326 assert self.alive_children is None, ('events() is valid only after '
327 'no_more_snapshot() is invoked')
328
329 result = []
330
331 last_deps = None
332 for timestamp, deps in self.snapshots.items():
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800333 result.append((timestamp, self.entry, deps, False))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800334 last_deps = deps
335
336 assert last_deps
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800337 result.append((self.end_time, self.entry, last_deps, True))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800338
339 for subtree in self.subtrees:
340 for event in subtree.events():
341 result.append(event)
342
343 result.sort()
344
345 return result
346
347 def iter_path_specs(self):
348 """Iterates snapshots of project dependency state.
349
350 Yields:
351 (timestamp, path_specs):
352 timestamp: time of snapshot
353 path_specs: dict of path_spec entries
354 """
355 forest = {}
356 # Group by timestamp
357 for timestamp, events in itertools.groupby(self.events(),
358 operator.itemgetter(0)):
359 # It's possible that one deps is removed and added at the same timestamp,
360 # i.e. modification, so use counter to track.
361 end_counter = collections.Counter()
362
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800363 for timestamp, entry, deps, end in events:
364 forest[entry] = deps
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800365 if end:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800366 end_counter[entry] += 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800367 else:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800368 end_counter[entry] -= 1
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800369
370 # Merge Deps at time `timestamp` into single path_specs.
371 path_specs = {}
372 for deps in forest.values():
373 for path, dep in deps.entries.items():
374 path_specs[path] = dep.as_path_spec()
375
376 yield timestamp, path_specs
377
378 # Remove deps which are removed at this timestamp.
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800379 for entry, count in end_counter.items():
380 assert -1 <= count <= 1, (timestamp, entry)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800381 if count == 1:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800382 del forest[entry]
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800383
384
385class DepsParser(object):
386 """Gclient DEPS file parser."""
387
388 def __init__(self, project_root, code_storage):
389 self.project_root = project_root
390 self.code_storage = code_storage
391
392 def parse_single_deps(self, content, parent_vars=None, parent_path=''):
393 """Parses DEPS file without recursion.
394
395 Args:
396 content: file content of DEPS file
397 parent_vars: variables inherent from parent DEPS
398 parent_path: project path of parent DEPS file
399
400 Returns:
401 Deps object
402 """
403
404 def var_function(name):
405 return '{%s}' % name
406
407 global_scope = dict(Var=var_function)
408 local_scope = {}
409 try:
410 exec (content, global_scope, local_scope) # pylint: disable=exec-used
411 except SyntaxError:
412 raise
413
414 deps = Deps()
415 local_scope.setdefault('vars', {})
416 if parent_vars:
417 local_scope['vars'].update(parent_vars)
418 deps.variables = local_scope['vars']
419
420 # Warnings for old usages which we don't support.
421 for name in deps.variables:
422 if name.startswith('RECURSEDEPS_') or name.endswith('_DEPS_file'):
423 logger.warning('%s is deprecated and not supported recursion syntax',
424 name)
425 if 'deps_os' in local_scope:
426 logger.warning('deps_os is no longer supported')
427
428 for path, dep_entry in local_scope['deps'].items():
429 if local_scope.get('use_relative_paths', False):
430 path = os.path.join(parent_path, path)
431 path = path.format(**deps.variables)
432 dep = Dep(path, deps.variables, dep_entry)
433 if not dep.eval_condition():
434 continue
435
436 # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
437 if dep.dep_type != 'git':
438 logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
439 path)
440 continue
441
442 deps.entries[path] = dep
443
444 recursedeps = []
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800445 for recurse_entry in local_scope.get('recursedeps', []):
446 # Normalize entries.
447 if isinstance(recurse_entry, tuple):
448 path, deps_file = recurse_entry
449 else:
450 assert isinstance(path, str)
451 path, deps_file = recurse_entry, 'DEPS'
452
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800453 if local_scope.get('use_relative_paths', False):
454 path = os.path.join(parent_path, path)
455 path = path.format(**deps.variables)
456 if path in deps.entries:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800457 recursedeps.append((path, deps_file))
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800458 deps.recursedeps = recursedeps
459
460 return deps
461
462 def construct_deps_tree(self,
463 tstree,
464 repo_url,
465 at,
466 after,
467 before,
468 parent_vars=None,
469 parent_path='',
470 deps_file='DEPS'):
471 """Processes DEPS recursively of given time period.
472
473 This method parses all commits of DEPS between time `after` and `before`,
474 segments recursive dependencies into subtrees if they are changed, and
475 processes subtrees recursively.
476
477 The parsed results (multiple revisions of DEPS file) are stored in `tstree`.
478
479 Args:
480 tstree: TimeSeriesTree object
481 repo_url: remote repo url
482 at: branch or git commit id
483 after: begin of period
484 before: end of period
485 parent_vars: DEPS variables inherit from parent DEPS (including
486 custom_vars)
487 parent_path: the path of parent project of current DEPS file
488 deps_file: filename of DEPS file, relative to the git repo, repo_rul
489 """
490 if '://' in repo_url:
491 git_repo = self.code_storage.cached_git_root(repo_url)
492 else:
493 git_repo = repo_url
494
495 if git_util.is_git_rev(at):
496 history = [
497 (after, at),
498 (before, at),
499 ]
500 else:
501 history = git_util.get_history(
502 git_repo,
503 deps_file,
504 branch=at,
505 after=after,
506 before=before,
507 padding=True)
508 assert history
509
510 # If not equal, it means the file was deleted but is still referenced by
511 # its parent.
512 assert history[-1][0] == before
513
514 # TODO(kcwu): optimization: history[-1] is unused
515 for timestamp, git_rev in history[:-1]:
516 content = git_util.get_file_from_revision(git_repo, git_rev, deps_file)
517
518 deps = self.parse_single_deps(
519 content, parent_vars=parent_vars, parent_path=parent_path)
520 tstree.add_snapshot(timestamp, deps, deps.recursedeps)
521
522 tstree.no_more_snapshot(deps)
523
524 for subtree in tstree.subtrees:
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800525 path, deps_file = subtree.entry
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800526 path_spec = subtree.parent_deps.entries[path].as_path_spec()
527 self.construct_deps_tree(
528 subtree,
529 path_spec.repo_url,
530 path_spec.at,
531 subtree.start_time,
532 subtree.end_time,
533 parent_vars=subtree.parent_deps.variables,
Kuang-che Wu3a7352d2018-10-20 17:22:03 +0800534 parent_path=path,
535 deps_file=deps_file)
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800536
537 def enumerate_path_specs(self, start_time, end_time, path):
538 tstree = TimeSeriesTree(None, path, start_time, end_time)
539 self.construct_deps_tree(tstree, path, 'master', start_time, end_time)
540 return tstree.iter_path_specs()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800541
542
543class GclientCache(codechange.CodeStorage):
544 """Gclient git cache."""
545
546 def __init__(self, cache_dir):
547 self.cache_dir = cache_dir
548
549 def _url_to_cache_dir(self, url):
550 # ref: depot_tools' git_cache.Mirror.UrlToCacheDir
551 parsed = urlparse.urlparse(url)
552 norm_url = parsed.netloc + parsed.path
553 if norm_url.endswith('.git'):
554 norm_url = norm_url[:-len('.git')]
555 return norm_url.replace('-', '--').replace('/', '-').lower()
556
557 def cached_git_root(self, repo_url):
558 cache_path = self._url_to_cache_dir(repo_url)
559 return os.path.join(self.cache_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800560
561 def _load_project_list(self, project_root):
562 repo_project_list = os.path.join(project_root, '.gclient_entries')
563 scope = {}
564 exec open(repo_project_list) in scope # pylint: disable=exec-used
565 return scope.get('entries', {})
566
567 def _save_project_list(self, project_root, projects):
568 repo_project_list = os.path.join(project_root, '.gclient_entries')
569 content = 'entries = {\n'
570 for item in sorted(projects.items()):
Kuang-che Wu88b17342018-10-23 17:19:09 +0800571 path, repo_url = map(pprint.pformat, item)
572 content += ' %s: %s,\n' % (path, repo_url)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800573 content += '}\n'
574 with open(repo_project_list, 'w') as f:
575 f.write(content)
576
577 def add_to_project_list(self, project_root, path, repo_url):
578 projects = self._load_project_list(project_root)
579
580 projects[path] = repo_url
581
582 self._save_project_list(project_root, projects)
583
584 def remove_from_project_list(self, project_root, path):
585 projects = self._load_project_list(project_root)
586
587 if path in projects:
588 del projects[path]
589
590 self._save_project_list(project_root, projects)