blob: 34c0b275350ccda48ea8e76bfa2d33acb374b195 [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 Wu2be0b212020-01-15 19:50:11 +080059def _get_repo_sync_env():
60 # b/120757273 Even we have set git cookies, git still occasionally asks for
61 # username/password for unknown reasons. Then it hangs forever because we are
62 # a script. Here we work around the issue by setting GIT_ASKPASS and fail the
63 # auth. The failure is usually harmless because bisect-kit will retry.
64 env = os.environ.copy()
Kuang-che Wu57e8b8e2020-02-06 20:04:36 +080065 env['GIT_ASKPASS'] = '/bin/true'
Kuang-che Wu2be0b212020-01-15 19:50:11 +080066 return env
67
68
Kuang-che Wubfc4a642018-04-19 11:54:08 +080069def init(repo_dir,
70 manifest_url,
71 manifest_branch=None,
72 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080073 repo_url=None,
Kuang-che Wu41e8b592018-09-25 17:01:30 +080074 reference=None,
75 mirror=False):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080076 """Repo init.
77
78 Args:
79 repo_dir: root directory of repo
80 manifest_url: manifest repository location
81 manifest_branch: manifest branch or revision
82 manifest_name: initial manifest file name
83 repo_url: repo repository location
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080084 reference: location of mirror directory
Kuang-che Wu41e8b592018-09-25 17:01:30 +080085 mirror: indicates repo mirror
Kuang-che Wubfc4a642018-04-19 11:54:08 +080086 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080087 root = find_repo_root(repo_dir)
88 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080089 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080090 '%s should not be inside another repo project at %s' % (repo_dir, root))
91
Kuang-che Wubfc4a642018-04-19 11:54:08 +080092 cmd = ['repo', 'init', '--manifest-url', manifest_url]
93 if manifest_name:
94 cmd += ['--manifest-name', manifest_name]
95 if manifest_branch:
96 cmd += ['--manifest-branch', manifest_branch]
97 if repo_url:
98 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080099 if reference:
100 cmd += ['--reference', reference]
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800101 if mirror:
102 cmd.append('--mirror')
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800103 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800104
105
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800106def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
107 """Cleanup files generated by <copyfile> <linkfile> tags.
108
109 Args:
110 repo_dir: root directory of repo
111 manifest_name: filename of manifest
112 """
113 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800114 manifest_path = os.path.join(manifest_dir, manifest_name)
115 if os.path.islink(manifest_path):
116 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800117 parser = ManifestParser(manifest_dir)
118 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
119
120 for copyfile in manifest.findall('.//copyfile'):
121 dest = copyfile.get('dest')
122 if not dest:
123 continue
124 # `dest` is relative to the top of the tree
125 dest_path = os.path.join(repo_dir, dest)
126 if not os.path.isfile(dest_path):
127 continue
128 logger.debug('delete file %r', dest_path)
129 os.unlink(dest_path)
130
131 for linkfile in manifest.findall('.//linkfile'):
132 dest = linkfile.get('dest')
133 if not dest:
134 continue
135 # `dest` is relative to the top of the tree
136 dest_path = os.path.join(repo_dir, dest)
137 if not os.path.islink(dest_path):
138 continue
139 logger.debug('delete link %r', dest_path)
140 os.unlink(dest_path)
141
142
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800143def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
144 """Repo sync.
145
146 Args:
147 repo_dir: root directory of repo
148 jobs: projects to fetch simultaneously
149 manifest_name: filename of manifest
150 current_branch: fetch only current branch
151 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800152 # Workaround to prevent garbage files left between repo syncs
153 # (http://crbug.com/881783).
154 cleanup_repo_generated_files(repo_dir)
155
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800156 cmd = ['repo', 'sync', '-q', '--force-sync']
157 if jobs:
158 cmd += ['-j', str(jobs)]
159 if manifest_name:
160 cmd += ['--manifest-name', manifest_name]
161 if current_branch:
162 cmd += ['--current-branch']
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800163 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800164
165
166def abandon(repo_dir, branch_name):
167 """Repo abandon.
168
169 Args:
170 repo_dir: root directory of repo
171 branch_name: branch name to abandon
172 """
173 # Ignore errors if failed, which means the branch didn't exist beforehand.
174 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
175
176
177def info(repo_dir, query):
178 """Repo info.
179
180 Args:
181 repo_dir: root directory of repo
182 query: key to query
183 """
Kuang-che Wu34ab7b42019-10-28 19:40:05 +0800184 try:
185 output = util.check_output('repo', 'info', '.', cwd=repo_dir)
186 except subprocess.CalledProcessError as e:
187 if 'Manifest branch:' not in e.output:
188 raise
189 # "repo info" may exit with error while the data we want is already
190 # printed. Ignore errors for such case.
191 output = e.output
192 for line in output.splitlines():
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800193 key, value = line.split(':', 1)
194 key, value = key.strip(), value.strip()
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800195 if key == query:
196 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800197
198 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800199
200
201def get_current_branch(repo_dir):
202 """Get manifest branch of existing repo directory."""
203 return info(repo_dir, 'Manifest branch')
204
205
206def get_manifest_groups(repo_dir):
207 """Get manifest group of existing repo directory."""
208 return info(repo_dir, 'Manifest groups')
209
210
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800211def list_projects(repo_dir):
212 """Repo list.
213
214 Args:
215 repo_dir: root directory of repo
216
217 Returns:
218 list of paths, relative to repo_dir
219 """
220 result = []
221 for line in util.check_output(
222 'repo', 'list', '--path-only', cwd=repo_dir).splitlines():
223 result.append(line)
224 return result
225
226
227def cleanup_unexpected_files(repo_dir):
228 """Clean up unexpected files in repo tree.
229
230 Note this is not fully equivalent to 'repo sync' from scratch because:
231 - This only handle git repo folders. In other words, directories under
232 repo_dir not inside any git repo will not be touched.
233 - It ignores files if matching gitignore pattern.
234 So we can keep cache files to speed up incremental build next time.
235
236 If you want truly clean tree, delete entire tree and repo sync directly
237 instead.
238
239 Args:
240 repo_dir: root directory of repo
241 """
242 projects = list_projects(repo_dir)
243
244 # When we clean up project X, we don't want to touch files under X's
245 # subprojects. Collect the nested project relationship here.
246 nested = {}
247 # By sorting, parent directory will loop before subdirectories.
248 for project_path in sorted(projects):
249 components = project_path.split(os.sep)
250 for i in range(len(components) - 1, 0, -1):
251 head = os.sep.join(components[:i])
252 tail = os.sep.join(components[i:])
253 if head in nested:
254 nested[head].append(tail)
255 break
256 nested[project_path] = []
257
258 for project_path in projects:
259 git_repo = os.path.join(repo_dir, project_path)
260 if not os.path.exists(git_repo):
261 # It should be harmless to ignore git repo nonexistence because 'repo
262 # sync' will restore them.
263 logger.warning('git repo not found: %s', git_repo)
264 continue
265 git_util.distclean(git_repo, nested[project_path])
266
267
Kuang-che Wubfa64482018-10-16 11:49:49 +0800268def _urljoin(base, url):
269 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
270 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
271 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
272 dummy_scheme = 'gopher://'
273 new_scheme = 'persistent-https://'
274 assert not base.startswith(dummy_scheme)
275 assert not url.startswith(dummy_scheme)
276 base = re.sub('^' + new_scheme, dummy_scheme, base)
277 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800278 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800279 result = re.sub('^' + dummy_scheme, new_scheme, result)
280 return result
281
282
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800283class ManifestParser(object):
284 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800285
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800286 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800287 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800288 if load_remote:
289 self.manifest_url = get_manifest_url(self.manifest_dir)
290 else:
291 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800292
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800293 def parse_single_xml(self, content, allow_include=False):
294 root = xml.etree.ElementTree.fromstring(content)
295 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800296 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800297 'Expects self-contained manifest. <include> is not allowed')
298 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800299
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800300 def parse_xml_recursive(self, git_rev, path):
301 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800302 root = xml.etree.ElementTree.fromstring(content)
303 default = None
304 notice = None
305 remotes = {}
306 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800307 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800308
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800309 for node in root:
310 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800311 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800312 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800313 nodes = [node]
314
315 for subnode in nodes:
316 if subnode.tag == 'default':
317 if default is not None and not self.element_equal(default, subnode):
318 raise errors.ExternalError('duplicated <default> %s and %s' %
319 (self.element_to_string(default),
320 self.element_to_string(subnode)))
321 if default is None:
322 default = subnode
323 result.append(subnode)
324 elif subnode.tag == 'remote':
325 name = subnode.get('name')
326 if name in remotes and not self.element_equal(remotes[name], subnode):
327 raise errors.ExternalError('duplicated <remote> %s and %s' %
328 (self.element_to_string(default),
329 self.element_to_string(subnode)))
330 if name not in remotes:
331 remotes[name] = subnode
332 result.append(subnode)
333 elif subnode.tag == 'notice':
334 if notice is not None and not self.element_equal(notice, subnode):
335 raise errors.ExternalError('duplicated <notice>')
336 if notice is None:
337 notice = subnode
338 result.append(subnode)
339 elif subnode.tag == 'manifest-server':
340 if manifest_server is not None:
341 raise errors.ExternalError('duplicated <manifest-server>')
342 manifest_server = subnode
343 result.append(subnode)
344 else:
345 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800346 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800347
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800348 def element_to_string(self, element):
349 return xml.etree.ElementTree.tostring(element).strip()
350
351 @classmethod
352 def get_project_path(cls, project):
353 path = project.get('path')
354 # default path is its name
355 if not path:
356 path = project.get('name')
357 return path
358
359 @classmethod
360 def get_project_revision(cls, project, default):
361 if default is None:
362 default = {}
363 return project.get('revision', default.get('revision'))
364
365 def element_equal(self, element1, element2):
366 """Return if two xml elements are same
367
368 Args:
369 element1: An xml element
370 element2: An xml element
371 """
372 if element1.tag != element2.tag:
373 return False
374 if element1.text != element2.text:
375 return False
376 if element1.attrib != element2.attrib:
377 return False
378 if len(element1) != len(element2):
379 return False
380 return all(
381 self.element_equal(node1, node2)
382 for node1, node2 in zip(element1, element2))
383
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800384 def process_parsed_result(self, root):
385 result = {}
386 default = root.find('default')
387 if default is None:
388 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800389
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800390 remote_fetch_map = {}
391 for remote in root.findall('.//remote'):
392 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800393 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800394 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800395 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800396 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800397 'only support git repo at root path of remote server: %s' %
398 fetch_url)
399 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800400
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800401 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800402
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800403 for project in root.findall('.//project'):
404 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800405 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800406 for subproject in project.findall('.//project'):
407 logger.warning('nested project %s.%s is not supported and ignored',
408 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800409
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800410 path = self.get_project_path(project)
411 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800412
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800413 remote_name = project.get('remote', default.get('remote'))
414 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800415 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800416 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800417 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800418
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800419 result[path] = codechange.PathSpec(path, repo_url, revision)
420 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800421
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800422 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800423
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800424 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800425 try:
426 root = self.parse_single_xml(content, allow_include=True)
427 except xml.etree.ElementTree.ParseError:
428 logger.warning('%s syntax error, skip', path)
429 return None
430
431 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800432 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800433 result.append(include.get('name'))
434 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800435
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800436 return git_util.get_history_recursively(
437 self.manifest_dir,
438 path,
439 start_time,
440 end_time,
441 parse_dependencies,
442 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800443
444
445class RepoMirror(codechange.CodeStorage):
446 """Repo git mirror."""
447
448 def __init__(self, mirror_dir):
449 self.mirror_dir = mirror_dir
450
451 def _url_to_cache_dir(self, url):
452 # Here we assume remote fetch url is always at root of server url, so we can
453 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800454 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800455 assert path[0] == '/'
456 return '%s.git' % path[1:]
457
458 def cached_git_root(self, repo_url):
459 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800460
461 # The location of chromeos manifest-internal repo mirror is irregular
462 # (http://crbug.com/895957). This is a workaround.
463 if cache_path == 'chromeos/manifest-internal.git':
464 cache_path = 'manifest-internal.git'
465
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800466 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800467
468 def _load_project_list(self, project_root):
469 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800470 with open(repo_project_list) as f:
471 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800472
473 def _save_project_list(self, project_root, lines):
474 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
475 with open(repo_project_list, 'w') as f:
476 f.write(''.join(sorted(lines)))
477
478 def add_to_project_list(self, project_root, path, repo_url):
479 lines = self._load_project_list(project_root)
480
481 line = path + '\n'
482 if line not in lines:
483 lines.append(line)
484
485 self._save_project_list(project_root, lines)
486
487 def remove_from_project_list(self, project_root, path):
488 lines = self._load_project_list(project_root)
489
490 line = path + '\n'
491 if line in lines:
492 lines.remove(line)
493
494 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800495
496
497class Manifest(object):
498 """This class handles a manifest and is able to patch projects."""
499
500 def __init__(self, manifest_internal_dir):
501 self.xml = None
502 self.manifest_internal_dir = manifest_internal_dir
503 self.modified = set()
504 self.parser = ManifestParser(manifest_internal_dir)
505
506 def load_from_string(self, xml_string):
507 """Load manifest xml from a string.
508
509 Args:
510 xml_string: An xml string.
511 """
512 self.xml = xml.etree.ElementTree.fromstring(xml_string)
513
514 def load_from_timestamp(self, timestamp):
515 """Load manifest xml snapshot by a timestamp.
516
517 The function will load a latest manifest before or equal to the timestamp.
518
519 Args:
520 timestamp: A unix timestamp.
521 """
522 commits = git_util.get_history(
523 self.manifest_internal_dir, before=timestamp + 1)
524 commit = commits[-1][1]
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +0800525 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800526
527 def to_string(self):
528 """Dump current xml to a string.
529
530 Returns:
531 A string of xml.
532 """
533 return xml.etree.ElementTree.tostring(self.xml)
534
535 def is_static_manifest(self):
536 """Return true if there is any project without revision in the xml.
537
538 Returns:
539 A boolean, True if every project has a revision.
540 """
541 count = 0
542 for project in self.xml.findall('.//project'):
543 # check argument directly instead of getting value from default tag
544 if not project.get('revision'):
545 count += 1
546 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800547 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800548 return count == 0
549
550 def remove_project_revision(self):
551 """Remove revision argument from all projects"""
552 for project in self.xml.findall('.//project'):
553 if 'revision' in project:
554 del project['revision']
555
556 def count_path(self, path):
557 """Count projects that path is given path.
558
559 Returns:
560 An integer, indicates the number of projects.
561 """
562 result = 0
563 for project in self.xml.findall('.//project'):
564 if project.get('path') == path:
565 result += 1
566 return result
567
568 def apply_commit(self, path, revision, overwrite=True):
569 """Set revision to a project by path.
570
571 Args:
572 path: A project's path.
573 revision: A git commit id.
574 overwrite: Overwrite flag, the project won't change if overwrite=False
575 and it was modified before.
576 """
577 if path in self.modified and not overwrite:
578 return
579 self.modified.add(path)
580
581 count = 0
582 for project in self.xml.findall('.//project'):
583 if self.parser.get_project_path(project) == path:
584 count += 1
585 project.set('revision', revision)
586
587 if count != 1:
588 logger.warning('found %d path: %s in manifest', count, path)
589
590 def apply_upstream(self, path, upstream):
591 """Set upstream to a project by path.
592
593 Args:
594 path: A project's path.
595 upstream: A git upstream.
596 """
597 for project in self.xml.findall('.//project'):
598 if self.parser.get_project_path(project) == path:
599 project.set('upstream', upstream)
600
601 def apply_action_groups(self, action_groups):
602 """Apply multiple action groups to xml.
603
604 If there are multiple actions in one repo, only last one is applied.
605
606 Args:
607 action_groups: A list of action groups.
608 """
609 # Apply in reversed order with overwrite=False,
610 # so each repo is on the state of last action.
611 for action_group in reversed(action_groups):
612 for action in reversed(action_group.actions):
613 if isinstance(action, codechange.GitCheckoutCommit):
614 self.apply_commit(action.path, action.rev, overwrite=False)
615 if isinstance(action, codechange.GitAddRepo):
616 self.apply_commit(action.path, action.rev, overwrite=False)
617 if isinstance(action, codechange.GitRemoveRepo):
618 assert self.count_path(action.path) == 0
619 self.modified.add(action.path)
620 return
621
622 def apply_manifest(self, manifest):
623 """Apply another manifest to current xml.
624
625 By default, all the projects in manifest will be applied and won't
626 overwrite modified projects.
627
628 Args:
629 manifest: A Manifest object.
630 """
631 default = manifest.xml.get('default')
632 for project in manifest.xml.findall('.//project'):
633 path = self.parser.get_project_path(project)
634 revision = self.parser.get_project_revision(project, default)
635 if path and revision:
636 self.apply_commit(path, revision, overwrite=False)
637 upstream = project.get('upstream')
638 if upstream:
639 self.apply_upstream(path, upstream)