blob: 2127500c196fd5a66fa531d7fadcec3b25b529c7 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wubfc4a642018-04-19 11:54:08 +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"""Repo utility.
6
7This module provides wrapper for "repo" (a Google-built repository management
8tool that runs on top of git) and related utility functions.
9"""
10
11from __future__ import print_function
12import logging
13import os
14import re
Kuang-che Wu34ab7b42019-10-28 19:40:05 +080015import subprocess
Kuang-che Wubfc4a642018-04-19 11:54:08 +080016import xml.etree.ElementTree
17
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +080018from six.moves import urllib
19
Kuang-che Wud1d45b42018-07-05 00:46:45 +080020from bisect_kit import codechange
Kuang-che Wue121fae2018-11-09 16:18:39 +080021from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080022from bisect_kit import git_util
23from bisect_kit import util
24
25logger = logging.getLogger(__name__)
26
27
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080028def get_manifest_url(manifest_dir):
Kuang-che Wud1d45b42018-07-05 00:46:45 +080029 """Get manifest URL of repo project.
30
31 Args:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080032 manifest_dir: path of manifest directory
Kuang-che Wud1d45b42018-07-05 00:46:45 +080033
34 Returns:
35 manifest URL.
36 """
Kuang-che Wud1d45b42018-07-05 00:46:45 +080037 url = util.check_output(
38 'git', 'config', 'remote.origin.url', cwd=manifest_dir)
Kuang-che Wud1d45b42018-07-05 00:46:45 +080039 return url
40
41
Kuang-che Wu41e8b592018-09-25 17:01:30 +080042def find_repo_root(path):
43 """Find the root path of a repo project
44
45 Args:
46 path: path
47
48 Returns:
49 project root if path is inside a repo project; otherwise None
50 """
51 path = os.path.abspath(path)
52 while not os.path.exists(os.path.join(path, '.repo')):
53 if path == '/':
54 return None
55 path = os.path.dirname(path)
56 return path
57
58
Kuang-che Wubfc4a642018-04-19 11:54:08 +080059def init(repo_dir,
60 manifest_url,
61 manifest_branch=None,
62 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080063 repo_url=None,
Kuang-che Wu41e8b592018-09-25 17:01:30 +080064 reference=None,
65 mirror=False):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080066 """Repo init.
67
68 Args:
69 repo_dir: root directory of repo
70 manifest_url: manifest repository location
71 manifest_branch: manifest branch or revision
72 manifest_name: initial manifest file name
73 repo_url: repo repository location
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080074 reference: location of mirror directory
Kuang-che Wu41e8b592018-09-25 17:01:30 +080075 mirror: indicates repo mirror
Kuang-che Wubfc4a642018-04-19 11:54:08 +080076 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080077 root = find_repo_root(repo_dir)
78 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080079 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080080 '%s should not be inside another repo project at %s' % (repo_dir, root))
81
Kuang-che Wubfc4a642018-04-19 11:54:08 +080082 cmd = ['repo', 'init', '--manifest-url', manifest_url]
83 if manifest_name:
84 cmd += ['--manifest-name', manifest_name]
85 if manifest_branch:
86 cmd += ['--manifest-branch', manifest_branch]
87 if repo_url:
88 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080089 if reference:
90 cmd += ['--reference', reference]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080091 if mirror:
92 cmd.append('--mirror')
Kuang-che Wubfc4a642018-04-19 11:54:08 +080093 util.check_call(*cmd, cwd=repo_dir)
94
95
Kuang-che Wuea3abce2018-10-04 17:50:42 +080096def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
97 """Cleanup files generated by <copyfile> <linkfile> tags.
98
99 Args:
100 repo_dir: root directory of repo
101 manifest_name: filename of manifest
102 """
103 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800104 manifest_path = os.path.join(manifest_dir, manifest_name)
105 if os.path.islink(manifest_path):
106 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800107 parser = ManifestParser(manifest_dir)
108 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
109
110 for copyfile in manifest.findall('.//copyfile'):
111 dest = copyfile.get('dest')
112 if not dest:
113 continue
114 # `dest` is relative to the top of the tree
115 dest_path = os.path.join(repo_dir, dest)
116 if not os.path.isfile(dest_path):
117 continue
118 logger.debug('delete file %r', dest_path)
119 os.unlink(dest_path)
120
121 for linkfile in manifest.findall('.//linkfile'):
122 dest = linkfile.get('dest')
123 if not dest:
124 continue
125 # `dest` is relative to the top of the tree
126 dest_path = os.path.join(repo_dir, dest)
127 if not os.path.islink(dest_path):
128 continue
129 logger.debug('delete link %r', dest_path)
130 os.unlink(dest_path)
131
132
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800133def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
134 """Repo sync.
135
136 Args:
137 repo_dir: root directory of repo
138 jobs: projects to fetch simultaneously
139 manifest_name: filename of manifest
140 current_branch: fetch only current branch
141 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800142 # Workaround to prevent garbage files left between repo syncs
143 # (http://crbug.com/881783).
144 cleanup_repo_generated_files(repo_dir)
145
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800146 cmd = ['repo', 'sync', '-q', '--force-sync']
147 if jobs:
148 cmd += ['-j', str(jobs)]
149 if manifest_name:
150 cmd += ['--manifest-name', manifest_name]
151 if current_branch:
152 cmd += ['--current-branch']
153 util.check_call(*cmd, cwd=repo_dir)
154
155
156def abandon(repo_dir, branch_name):
157 """Repo abandon.
158
159 Args:
160 repo_dir: root directory of repo
161 branch_name: branch name to abandon
162 """
163 # Ignore errors if failed, which means the branch didn't exist beforehand.
164 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
165
166
167def info(repo_dir, query):
168 """Repo info.
169
170 Args:
171 repo_dir: root directory of repo
172 query: key to query
173 """
Kuang-che Wu34ab7b42019-10-28 19:40:05 +0800174 try:
175 output = util.check_output('repo', 'info', '.', cwd=repo_dir)
176 except subprocess.CalledProcessError as e:
177 if 'Manifest branch:' not in e.output:
178 raise
179 # "repo info" may exit with error while the data we want is already
180 # printed. Ignore errors for such case.
181 output = e.output
182 for line in output.splitlines():
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800183 key, value = line.split(':', 1)
184 key, value = key.strip(), value.strip()
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800185 if key == query:
186 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800187
188 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800189
190
191def get_current_branch(repo_dir):
192 """Get manifest branch of existing repo directory."""
193 return info(repo_dir, 'Manifest branch')
194
195
196def get_manifest_groups(repo_dir):
197 """Get manifest group of existing repo directory."""
198 return info(repo_dir, 'Manifest groups')
199
200
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800201def list_projects(repo_dir):
202 """Repo list.
203
204 Args:
205 repo_dir: root directory of repo
206
207 Returns:
208 list of paths, relative to repo_dir
209 """
210 result = []
211 for line in util.check_output(
212 'repo', 'list', '--path-only', cwd=repo_dir).splitlines():
213 result.append(line)
214 return result
215
216
217def cleanup_unexpected_files(repo_dir):
218 """Clean up unexpected files in repo tree.
219
220 Note this is not fully equivalent to 'repo sync' from scratch because:
221 - This only handle git repo folders. In other words, directories under
222 repo_dir not inside any git repo will not be touched.
223 - It ignores files if matching gitignore pattern.
224 So we can keep cache files to speed up incremental build next time.
225
226 If you want truly clean tree, delete entire tree and repo sync directly
227 instead.
228
229 Args:
230 repo_dir: root directory of repo
231 """
232 projects = list_projects(repo_dir)
233
234 # When we clean up project X, we don't want to touch files under X's
235 # subprojects. Collect the nested project relationship here.
236 nested = {}
237 # By sorting, parent directory will loop before subdirectories.
238 for project_path in sorted(projects):
239 components = project_path.split(os.sep)
240 for i in range(len(components) - 1, 0, -1):
241 head = os.sep.join(components[:i])
242 tail = os.sep.join(components[i:])
243 if head in nested:
244 nested[head].append(tail)
245 break
246 nested[project_path] = []
247
248 for project_path in projects:
249 git_repo = os.path.join(repo_dir, project_path)
250 if not os.path.exists(git_repo):
251 # It should be harmless to ignore git repo nonexistence because 'repo
252 # sync' will restore them.
253 logger.warning('git repo not found: %s', git_repo)
254 continue
255 git_util.distclean(git_repo, nested[project_path])
256
257
Kuang-che Wubfa64482018-10-16 11:49:49 +0800258def _urljoin(base, url):
259 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
260 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
261 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
262 dummy_scheme = 'gopher://'
263 new_scheme = 'persistent-https://'
264 assert not base.startswith(dummy_scheme)
265 assert not url.startswith(dummy_scheme)
266 base = re.sub('^' + new_scheme, dummy_scheme, base)
267 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800268 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800269 result = re.sub('^' + dummy_scheme, new_scheme, result)
270 return result
271
272
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800273class ManifestParser(object):
274 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800275
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800276 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800277 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800278 if load_remote:
279 self.manifest_url = get_manifest_url(self.manifest_dir)
280 else:
281 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800282
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800283 def parse_single_xml(self, content, allow_include=False):
284 root = xml.etree.ElementTree.fromstring(content)
285 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800286 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800287 'Expects self-contained manifest. <include> is not allowed')
288 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800289
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800290 def parse_xml_recursive(self, git_rev, path):
291 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800292 root = xml.etree.ElementTree.fromstring(content)
293 default = None
294 notice = None
295 remotes = {}
296 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800297 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800298
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800299 for node in root:
300 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800301 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800302 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800303 nodes = [node]
304
305 for subnode in nodes:
306 if subnode.tag == 'default':
307 if default is not None and not self.element_equal(default, subnode):
308 raise errors.ExternalError('duplicated <default> %s and %s' %
309 (self.element_to_string(default),
310 self.element_to_string(subnode)))
311 if default is None:
312 default = subnode
313 result.append(subnode)
314 elif subnode.tag == 'remote':
315 name = subnode.get('name')
316 if name in remotes and not self.element_equal(remotes[name], subnode):
317 raise errors.ExternalError('duplicated <remote> %s and %s' %
318 (self.element_to_string(default),
319 self.element_to_string(subnode)))
320 if name not in remotes:
321 remotes[name] = subnode
322 result.append(subnode)
323 elif subnode.tag == 'notice':
324 if notice is not None and not self.element_equal(notice, subnode):
325 raise errors.ExternalError('duplicated <notice>')
326 if notice is None:
327 notice = subnode
328 result.append(subnode)
329 elif subnode.tag == 'manifest-server':
330 if manifest_server is not None:
331 raise errors.ExternalError('duplicated <manifest-server>')
332 manifest_server = subnode
333 result.append(subnode)
334 else:
335 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800336 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800337
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800338 def element_to_string(self, element):
339 return xml.etree.ElementTree.tostring(element).strip()
340
341 @classmethod
342 def get_project_path(cls, project):
343 path = project.get('path')
344 # default path is its name
345 if not path:
346 path = project.get('name')
347 return path
348
349 @classmethod
350 def get_project_revision(cls, project, default):
351 if default is None:
352 default = {}
353 return project.get('revision', default.get('revision'))
354
355 def element_equal(self, element1, element2):
356 """Return if two xml elements are same
357
358 Args:
359 element1: An xml element
360 element2: An xml element
361 """
362 if element1.tag != element2.tag:
363 return False
364 if element1.text != element2.text:
365 return False
366 if element1.attrib != element2.attrib:
367 return False
368 if len(element1) != len(element2):
369 return False
370 return all(
371 self.element_equal(node1, node2)
372 for node1, node2 in zip(element1, element2))
373
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800374 def process_parsed_result(self, root):
375 result = {}
376 default = root.find('default')
377 if default is None:
378 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800379
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800380 remote_fetch_map = {}
381 for remote in root.findall('.//remote'):
382 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800383 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800384 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800385 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800386 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800387 'only support git repo at root path of remote server: %s' %
388 fetch_url)
389 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800390
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800391 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800392
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800393 for project in root.findall('.//project'):
394 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800395 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800396 for subproject in project.findall('.//project'):
397 logger.warning('nested project %s.%s is not supported and ignored',
398 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800399
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800400 path = self.get_project_path(project)
401 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800402
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800403 remote_name = project.get('remote', default.get('remote'))
404 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800405 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800406 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800407 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800408
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800409 result[path] = codechange.PathSpec(path, repo_url, revision)
410 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800411
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800412 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800413
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800414 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800415 try:
416 root = self.parse_single_xml(content, allow_include=True)
417 except xml.etree.ElementTree.ParseError:
418 logger.warning('%s syntax error, skip', path)
419 return None
420
421 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800422 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800423 result.append(include.get('name'))
424 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800425
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800426 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
427 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800428
429
430class RepoMirror(codechange.CodeStorage):
431 """Repo git mirror."""
432
433 def __init__(self, mirror_dir):
434 self.mirror_dir = mirror_dir
435
436 def _url_to_cache_dir(self, url):
437 # Here we assume remote fetch url is always at root of server url, so we can
438 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800439 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800440 assert path[0] == '/'
441 return '%s.git' % path[1:]
442
443 def cached_git_root(self, repo_url):
444 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800445
446 # The location of chromeos manifest-internal repo mirror is irregular
447 # (http://crbug.com/895957). This is a workaround.
448 if cache_path == 'chromeos/manifest-internal.git':
449 cache_path = 'manifest-internal.git'
450
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800451 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800452
453 def _load_project_list(self, project_root):
454 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800455 with open(repo_project_list) as f:
456 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800457
458 def _save_project_list(self, project_root, lines):
459 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
460 with open(repo_project_list, 'w') as f:
461 f.write(''.join(sorted(lines)))
462
463 def add_to_project_list(self, project_root, path, repo_url):
464 lines = self._load_project_list(project_root)
465
466 line = path + '\n'
467 if line not in lines:
468 lines.append(line)
469
470 self._save_project_list(project_root, lines)
471
472 def remove_from_project_list(self, project_root, path):
473 lines = self._load_project_list(project_root)
474
475 line = path + '\n'
476 if line in lines:
477 lines.remove(line)
478
479 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800480
481
482class Manifest(object):
483 """This class handles a manifest and is able to patch projects."""
484
485 def __init__(self, manifest_internal_dir):
486 self.xml = None
487 self.manifest_internal_dir = manifest_internal_dir
488 self.modified = set()
489 self.parser = ManifestParser(manifest_internal_dir)
490
491 def load_from_string(self, xml_string):
492 """Load manifest xml from a string.
493
494 Args:
495 xml_string: An xml string.
496 """
497 self.xml = xml.etree.ElementTree.fromstring(xml_string)
498
499 def load_from_timestamp(self, timestamp):
500 """Load manifest xml snapshot by a timestamp.
501
502 The function will load a latest manifest before or equal to the timestamp.
503
504 Args:
505 timestamp: A unix timestamp.
506 """
507 commits = git_util.get_history(
508 self.manifest_internal_dir, before=timestamp + 1)
509 commit = commits[-1][1]
510 self.xml = self.parser.parse_xml_recursive(commit, 'full.xml')
511
512 def to_string(self):
513 """Dump current xml to a string.
514
515 Returns:
516 A string of xml.
517 """
518 return xml.etree.ElementTree.tostring(self.xml)
519
520 def is_static_manifest(self):
521 """Return true if there is any project without revision in the xml.
522
523 Returns:
524 A boolean, True if every project has a revision.
525 """
526 count = 0
527 for project in self.xml.findall('.//project'):
528 # check argument directly instead of getting value from default tag
529 if not project.get('revision'):
530 count += 1
531 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800532 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800533 return count == 0
534
535 def remove_project_revision(self):
536 """Remove revision argument from all projects"""
537 for project in self.xml.findall('.//project'):
538 if 'revision' in project:
539 del project['revision']
540
541 def count_path(self, path):
542 """Count projects that path is given path.
543
544 Returns:
545 An integer, indicates the number of projects.
546 """
547 result = 0
548 for project in self.xml.findall('.//project'):
549 if project.get('path') == path:
550 result += 1
551 return result
552
553 def apply_commit(self, path, revision, overwrite=True):
554 """Set revision to a project by path.
555
556 Args:
557 path: A project's path.
558 revision: A git commit id.
559 overwrite: Overwrite flag, the project won't change if overwrite=False
560 and it was modified before.
561 """
562 if path in self.modified and not overwrite:
563 return
564 self.modified.add(path)
565
566 count = 0
567 for project in self.xml.findall('.//project'):
568 if self.parser.get_project_path(project) == path:
569 count += 1
570 project.set('revision', revision)
571
572 if count != 1:
573 logger.warning('found %d path: %s in manifest', count, path)
574
575 def apply_upstream(self, path, upstream):
576 """Set upstream to a project by path.
577
578 Args:
579 path: A project's path.
580 upstream: A git upstream.
581 """
582 for project in self.xml.findall('.//project'):
583 if self.parser.get_project_path(project) == path:
584 project.set('upstream', upstream)
585
586 def apply_action_groups(self, action_groups):
587 """Apply multiple action groups to xml.
588
589 If there are multiple actions in one repo, only last one is applied.
590
591 Args:
592 action_groups: A list of action groups.
593 """
594 # Apply in reversed order with overwrite=False,
595 # so each repo is on the state of last action.
596 for action_group in reversed(action_groups):
597 for action in reversed(action_group.actions):
598 if isinstance(action, codechange.GitCheckoutCommit):
599 self.apply_commit(action.path, action.rev, overwrite=False)
600 if isinstance(action, codechange.GitAddRepo):
601 self.apply_commit(action.path, action.rev, overwrite=False)
602 if isinstance(action, codechange.GitRemoveRepo):
603 assert self.count_path(action.path) == 0
604 self.modified.add(action.path)
605 return
606
607 def apply_manifest(self, manifest):
608 """Apply another manifest to current xml.
609
610 By default, all the projects in manifest will be applied and won't
611 overwrite modified projects.
612
613 Args:
614 manifest: A Manifest object.
615 """
616 default = manifest.xml.get('default')
617 for project in manifest.xml.findall('.//project'):
618 path = self.parser.get_project_path(project)
619 revision = self.parser.get_project_revision(project, default)
620 if path and revision:
621 self.apply_commit(path, revision, overwrite=False)
622 upstream = project.get('upstream')
623 if upstream:
624 self.apply_upstream(path, upstream)