blob: 9b736260579591bdc2550ebbc5eb84608ea5eba4 [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
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800412 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
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
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800426 return git_util.get_history_recursively(
427 self.manifest_dir,
428 path,
429 start_time,
430 end_time,
431 parse_dependencies,
432 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800433
434
435class RepoMirror(codechange.CodeStorage):
436 """Repo git mirror."""
437
438 def __init__(self, mirror_dir):
439 self.mirror_dir = mirror_dir
440
441 def _url_to_cache_dir(self, url):
442 # Here we assume remote fetch url is always at root of server url, so we can
443 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800444 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800445 assert path[0] == '/'
446 return '%s.git' % path[1:]
447
448 def cached_git_root(self, repo_url):
449 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800450
451 # The location of chromeos manifest-internal repo mirror is irregular
452 # (http://crbug.com/895957). This is a workaround.
453 if cache_path == 'chromeos/manifest-internal.git':
454 cache_path = 'manifest-internal.git'
455
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800456 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800457
458 def _load_project_list(self, project_root):
459 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800460 with open(repo_project_list) as f:
461 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800462
463 def _save_project_list(self, project_root, lines):
464 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
465 with open(repo_project_list, 'w') as f:
466 f.write(''.join(sorted(lines)))
467
468 def add_to_project_list(self, project_root, path, repo_url):
469 lines = self._load_project_list(project_root)
470
471 line = path + '\n'
472 if line not in lines:
473 lines.append(line)
474
475 self._save_project_list(project_root, lines)
476
477 def remove_from_project_list(self, project_root, path):
478 lines = self._load_project_list(project_root)
479
480 line = path + '\n'
481 if line in lines:
482 lines.remove(line)
483
484 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800485
486
487class Manifest(object):
488 """This class handles a manifest and is able to patch projects."""
489
490 def __init__(self, manifest_internal_dir):
491 self.xml = None
492 self.manifest_internal_dir = manifest_internal_dir
493 self.modified = set()
494 self.parser = ManifestParser(manifest_internal_dir)
495
496 def load_from_string(self, xml_string):
497 """Load manifest xml from a string.
498
499 Args:
500 xml_string: An xml string.
501 """
502 self.xml = xml.etree.ElementTree.fromstring(xml_string)
503
504 def load_from_timestamp(self, timestamp):
505 """Load manifest xml snapshot by a timestamp.
506
507 The function will load a latest manifest before or equal to the timestamp.
508
509 Args:
510 timestamp: A unix timestamp.
511 """
512 commits = git_util.get_history(
513 self.manifest_internal_dir, before=timestamp + 1)
514 commit = commits[-1][1]
515 self.xml = self.parser.parse_xml_recursive(commit, 'full.xml')
516
517 def to_string(self):
518 """Dump current xml to a string.
519
520 Returns:
521 A string of xml.
522 """
523 return xml.etree.ElementTree.tostring(self.xml)
524
525 def is_static_manifest(self):
526 """Return true if there is any project without revision in the xml.
527
528 Returns:
529 A boolean, True if every project has a revision.
530 """
531 count = 0
532 for project in self.xml.findall('.//project'):
533 # check argument directly instead of getting value from default tag
534 if not project.get('revision'):
535 count += 1
536 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800537 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800538 return count == 0
539
540 def remove_project_revision(self):
541 """Remove revision argument from all projects"""
542 for project in self.xml.findall('.//project'):
543 if 'revision' in project:
544 del project['revision']
545
546 def count_path(self, path):
547 """Count projects that path is given path.
548
549 Returns:
550 An integer, indicates the number of projects.
551 """
552 result = 0
553 for project in self.xml.findall('.//project'):
554 if project.get('path') == path:
555 result += 1
556 return result
557
558 def apply_commit(self, path, revision, overwrite=True):
559 """Set revision to a project by path.
560
561 Args:
562 path: A project's path.
563 revision: A git commit id.
564 overwrite: Overwrite flag, the project won't change if overwrite=False
565 and it was modified before.
566 """
567 if path in self.modified and not overwrite:
568 return
569 self.modified.add(path)
570
571 count = 0
572 for project in self.xml.findall('.//project'):
573 if self.parser.get_project_path(project) == path:
574 count += 1
575 project.set('revision', revision)
576
577 if count != 1:
578 logger.warning('found %d path: %s in manifest', count, path)
579
580 def apply_upstream(self, path, upstream):
581 """Set upstream to a project by path.
582
583 Args:
584 path: A project's path.
585 upstream: A git upstream.
586 """
587 for project in self.xml.findall('.//project'):
588 if self.parser.get_project_path(project) == path:
589 project.set('upstream', upstream)
590
591 def apply_action_groups(self, action_groups):
592 """Apply multiple action groups to xml.
593
594 If there are multiple actions in one repo, only last one is applied.
595
596 Args:
597 action_groups: A list of action groups.
598 """
599 # Apply in reversed order with overwrite=False,
600 # so each repo is on the state of last action.
601 for action_group in reversed(action_groups):
602 for action in reversed(action_group.actions):
603 if isinstance(action, codechange.GitCheckoutCommit):
604 self.apply_commit(action.path, action.rev, overwrite=False)
605 if isinstance(action, codechange.GitAddRepo):
606 self.apply_commit(action.path, action.rev, overwrite=False)
607 if isinstance(action, codechange.GitRemoveRepo):
608 assert self.count_path(action.path) == 0
609 self.modified.add(action.path)
610 return
611
612 def apply_manifest(self, manifest):
613 """Apply another manifest to current xml.
614
615 By default, all the projects in manifest will be applied and won't
616 overwrite modified projects.
617
618 Args:
619 manifest: A Manifest object.
620 """
621 default = manifest.xml.get('default')
622 for project in manifest.xml.findall('.//project'):
623 path = self.parser.get_project_path(project)
624 revision = self.parser.get_project_revision(project, default)
625 if path and revision:
626 self.apply_commit(path, revision, overwrite=False)
627 upstream = project.get('upstream')
628 if upstream:
629 self.apply_upstream(path, upstream)