blob: f81aaa35d87d9679fdfd5203daf3bf48a3ef0e60 [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,
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +080075 mirror=False,
76 groups=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080077 """Repo init.
78
79 Args:
80 repo_dir: root directory of repo
81 manifest_url: manifest repository location
82 manifest_branch: manifest branch or revision
83 manifest_name: initial manifest file name
84 repo_url: repo repository location
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080085 reference: location of mirror directory
Kuang-che Wu41e8b592018-09-25 17:01:30 +080086 mirror: indicates repo mirror
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +080087 groups: repo sync groups, groups should be seperate by comma
Kuang-che Wubfc4a642018-04-19 11:54:08 +080088 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080089 root = find_repo_root(repo_dir)
90 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080091 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080092 '%s should not be inside another repo project at %s' % (repo_dir, root))
93
Kuang-che Wubfc4a642018-04-19 11:54:08 +080094 cmd = ['repo', 'init', '--manifest-url', manifest_url]
95 if manifest_name:
96 cmd += ['--manifest-name', manifest_name]
97 if manifest_branch:
98 cmd += ['--manifest-branch', manifest_branch]
99 if repo_url:
100 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800101 if reference:
102 cmd += ['--reference', reference]
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +0800103 if groups:
104 cmd += ['--groups', groups]
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800105 if mirror:
106 cmd.append('--mirror')
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800107 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800108
109
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800110def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
111 """Cleanup files generated by <copyfile> <linkfile> tags.
112
113 Args:
114 repo_dir: root directory of repo
115 manifest_name: filename of manifest
116 """
117 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800118 manifest_path = os.path.join(manifest_dir, manifest_name)
119 if os.path.islink(manifest_path):
120 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800121 parser = ManifestParser(manifest_dir)
122 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
123
124 for copyfile in manifest.findall('.//copyfile'):
125 dest = copyfile.get('dest')
126 if not dest:
127 continue
128 # `dest` is relative to the top of the tree
129 dest_path = os.path.join(repo_dir, dest)
130 if not os.path.isfile(dest_path):
131 continue
132 logger.debug('delete file %r', dest_path)
133 os.unlink(dest_path)
134
135 for linkfile in manifest.findall('.//linkfile'):
136 dest = linkfile.get('dest')
137 if not dest:
138 continue
139 # `dest` is relative to the top of the tree
140 dest_path = os.path.join(repo_dir, dest)
141 if not os.path.islink(dest_path):
142 continue
143 logger.debug('delete link %r', dest_path)
144 os.unlink(dest_path)
145
146
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800147def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
148 """Repo sync.
149
150 Args:
151 repo_dir: root directory of repo
152 jobs: projects to fetch simultaneously
153 manifest_name: filename of manifest
154 current_branch: fetch only current branch
155 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800156 # Workaround to prevent garbage files left between repo syncs
157 # (http://crbug.com/881783).
158 cleanup_repo_generated_files(repo_dir)
159
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800160 cmd = ['repo', 'sync', '-q', '--force-sync']
161 if jobs:
162 cmd += ['-j', str(jobs)]
163 if manifest_name:
164 cmd += ['--manifest-name', manifest_name]
165 if current_branch:
166 cmd += ['--current-branch']
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800167 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800168
169
170def abandon(repo_dir, branch_name):
171 """Repo abandon.
172
173 Args:
174 repo_dir: root directory of repo
175 branch_name: branch name to abandon
176 """
177 # Ignore errors if failed, which means the branch didn't exist beforehand.
178 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
179
180
181def info(repo_dir, query):
182 """Repo info.
183
184 Args:
185 repo_dir: root directory of repo
186 query: key to query
187 """
Kuang-che Wu34ab7b42019-10-28 19:40:05 +0800188 try:
189 output = util.check_output('repo', 'info', '.', cwd=repo_dir)
190 except subprocess.CalledProcessError as e:
191 if 'Manifest branch:' not in e.output:
192 raise
193 # "repo info" may exit with error while the data we want is already
194 # printed. Ignore errors for such case.
195 output = e.output
196 for line in output.splitlines():
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800197 key, value = line.split(':', 1)
198 key, value = key.strip(), value.strip()
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800199 if key == query:
200 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800201
202 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800203
204
205def get_current_branch(repo_dir):
206 """Get manifest branch of existing repo directory."""
207 return info(repo_dir, 'Manifest branch')
208
209
210def get_manifest_groups(repo_dir):
211 """Get manifest group of existing repo directory."""
212 return info(repo_dir, 'Manifest groups')
213
214
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800215def list_projects(repo_dir):
216 """Repo list.
217
218 Args:
219 repo_dir: root directory of repo
220
221 Returns:
222 list of paths, relative to repo_dir
223 """
224 result = []
225 for line in util.check_output(
226 'repo', 'list', '--path-only', cwd=repo_dir).splitlines():
227 result.append(line)
228 return result
229
230
231def cleanup_unexpected_files(repo_dir):
232 """Clean up unexpected files in repo tree.
233
234 Note this is not fully equivalent to 'repo sync' from scratch because:
235 - This only handle git repo folders. In other words, directories under
236 repo_dir not inside any git repo will not be touched.
237 - It ignores files if matching gitignore pattern.
238 So we can keep cache files to speed up incremental build next time.
239
240 If you want truly clean tree, delete entire tree and repo sync directly
241 instead.
242
243 Args:
244 repo_dir: root directory of repo
245 """
246 projects = list_projects(repo_dir)
247
248 # When we clean up project X, we don't want to touch files under X's
249 # subprojects. Collect the nested project relationship here.
250 nested = {}
251 # By sorting, parent directory will loop before subdirectories.
252 for project_path in sorted(projects):
253 components = project_path.split(os.sep)
254 for i in range(len(components) - 1, 0, -1):
255 head = os.sep.join(components[:i])
256 tail = os.sep.join(components[i:])
257 if head in nested:
258 nested[head].append(tail)
259 break
260 nested[project_path] = []
261
262 for project_path in projects:
263 git_repo = os.path.join(repo_dir, project_path)
264 if not os.path.exists(git_repo):
265 # It should be harmless to ignore git repo nonexistence because 'repo
266 # sync' will restore them.
267 logger.warning('git repo not found: %s', git_repo)
268 continue
269 git_util.distclean(git_repo, nested[project_path])
270
271
Kuang-che Wubfa64482018-10-16 11:49:49 +0800272def _urljoin(base, url):
273 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
274 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
275 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
276 dummy_scheme = 'gopher://'
277 new_scheme = 'persistent-https://'
278 assert not base.startswith(dummy_scheme)
279 assert not url.startswith(dummy_scheme)
280 base = re.sub('^' + new_scheme, dummy_scheme, base)
281 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800282 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800283 result = re.sub('^' + dummy_scheme, new_scheme, result)
284 return result
285
286
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800287class ManifestParser:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800288 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800289
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800290 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800291 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800292 if load_remote:
293 self.manifest_url = get_manifest_url(self.manifest_dir)
294 else:
295 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800296
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800297 def parse_single_xml(self, content, allow_include=False):
298 root = xml.etree.ElementTree.fromstring(content)
299 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800300 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800301 'Expects self-contained manifest. <include> is not allowed')
302 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800303
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800304 def parse_xml_recursive(self, git_rev, path):
305 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800306 root = xml.etree.ElementTree.fromstring(content)
307 default = None
308 notice = None
309 remotes = {}
310 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800311 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800312
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800313 for node in root:
314 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800315 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800316 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800317 nodes = [node]
318
319 for subnode in nodes:
320 if subnode.tag == 'default':
321 if default is not None and not self.element_equal(default, subnode):
322 raise errors.ExternalError('duplicated <default> %s and %s' %
323 (self.element_to_string(default),
324 self.element_to_string(subnode)))
325 if default is None:
326 default = subnode
327 result.append(subnode)
328 elif subnode.tag == 'remote':
329 name = subnode.get('name')
330 if name in remotes and not self.element_equal(remotes[name], subnode):
331 raise errors.ExternalError('duplicated <remote> %s and %s' %
332 (self.element_to_string(default),
333 self.element_to_string(subnode)))
334 if name not in remotes:
335 remotes[name] = subnode
336 result.append(subnode)
337 elif subnode.tag == 'notice':
338 if notice is not None and not self.element_equal(notice, subnode):
339 raise errors.ExternalError('duplicated <notice>')
340 if notice is None:
341 notice = subnode
342 result.append(subnode)
343 elif subnode.tag == 'manifest-server':
344 if manifest_server is not None:
345 raise errors.ExternalError('duplicated <manifest-server>')
346 manifest_server = subnode
347 result.append(subnode)
348 else:
349 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800350 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800351
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800352 @classmethod
353 def element_to_string(cls, element):
354 return xml.etree.ElementTree.tostring(element, encoding='unicode').strip()
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800355
356 @classmethod
357 def get_project_path(cls, project):
358 path = project.get('path')
359 # default path is its name
360 if not path:
361 path = project.get('name')
362 return path
363
364 @classmethod
365 def get_project_revision(cls, project, default):
366 if default is None:
367 default = {}
368 return project.get('revision', default.get('revision'))
369
370 def element_equal(self, element1, element2):
371 """Return if two xml elements are same
372
373 Args:
374 element1: An xml element
375 element2: An xml element
376 """
377 if element1.tag != element2.tag:
378 return False
379 if element1.text != element2.text:
380 return False
381 if element1.attrib != element2.attrib:
382 return False
383 if len(element1) != len(element2):
384 return False
385 return all(
386 self.element_equal(node1, node2)
387 for node1, node2 in zip(element1, element2))
388
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800389 def process_parsed_result(self, root):
390 result = {}
391 default = root.find('default')
392 if default is None:
393 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800394
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800395 remote_fetch_map = {}
396 for remote in root.findall('.//remote'):
397 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800398 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800399 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800400 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800401 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800402 'only support git repo at root path of remote server: %s' %
403 fetch_url)
404 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800405
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800406 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800407
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800408 for project in root.findall('.//project'):
409 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800410 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800411 for subproject in project.findall('.//project'):
412 logger.warning('nested project %s.%s is not supported and ignored',
413 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800414
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800415 path = self.get_project_path(project)
416 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800417
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800418 remote_name = project.get('remote', default.get('remote'))
419 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800420 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800421 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800422 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800423
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800424 result[path] = codechange.PathSpec(path, repo_url, revision)
425 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800426
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800427 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800428
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800429 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800430 try:
431 root = self.parse_single_xml(content, allow_include=True)
432 except xml.etree.ElementTree.ParseError:
433 logger.warning('%s syntax error, skip', path)
434 return None
435
436 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800437 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800438 result.append(include.get('name'))
439 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800440
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800441 return git_util.get_history_recursively(
442 self.manifest_dir,
443 path,
444 start_time,
445 end_time,
446 parse_dependencies,
447 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800448
449
450class RepoMirror(codechange.CodeStorage):
451 """Repo git mirror."""
452
453 def __init__(self, mirror_dir):
454 self.mirror_dir = mirror_dir
455
456 def _url_to_cache_dir(self, url):
457 # Here we assume remote fetch url is always at root of server url, so we can
458 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800459 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800460 assert path[0] == '/'
461 return '%s.git' % path[1:]
462
463 def cached_git_root(self, repo_url):
464 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800465
466 # The location of chromeos manifest-internal repo mirror is irregular
467 # (http://crbug.com/895957). This is a workaround.
468 if cache_path == 'chromeos/manifest-internal.git':
469 cache_path = 'manifest-internal.git'
470
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800471 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800472
473 def _load_project_list(self, project_root):
474 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800475 with open(repo_project_list) as f:
476 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800477
478 def _save_project_list(self, project_root, lines):
479 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
480 with open(repo_project_list, 'w') as f:
481 f.write(''.join(sorted(lines)))
482
483 def add_to_project_list(self, project_root, path, repo_url):
484 lines = self._load_project_list(project_root)
485
486 line = path + '\n'
487 if line not in lines:
488 lines.append(line)
489
490 self._save_project_list(project_root, lines)
491
492 def remove_from_project_list(self, project_root, path):
493 lines = self._load_project_list(project_root)
494
495 line = path + '\n'
496 if line in lines:
497 lines.remove(line)
498
499 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800500
501
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800502class Manifest:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800503 """This class handles a manifest and is able to patch projects."""
504
505 def __init__(self, manifest_internal_dir):
506 self.xml = None
507 self.manifest_internal_dir = manifest_internal_dir
508 self.modified = set()
509 self.parser = ManifestParser(manifest_internal_dir)
510
511 def load_from_string(self, xml_string):
512 """Load manifest xml from a string.
513
514 Args:
515 xml_string: An xml string.
516 """
517 self.xml = xml.etree.ElementTree.fromstring(xml_string)
518
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800519 def load_from_commit(self, commit):
520 """Load manifest xml snapshot by a commit hash.
521
522 Args:
523 commit: A manifest-internal commit hash.
524 """
525 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
526
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800527 def load_from_timestamp(self, timestamp):
528 """Load manifest xml snapshot by a timestamp.
529
530 The function will load a latest manifest before or equal to the timestamp.
531
532 Args:
533 timestamp: A unix timestamp.
534 """
535 commits = git_util.get_history(
536 self.manifest_internal_dir, before=timestamp + 1)
537 commit = commits[-1][1]
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800538 self.load_from_commit(commit)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800539
540 def to_string(self):
541 """Dump current xml to a string.
542
543 Returns:
544 A string of xml.
545 """
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800546 return ManifestParser.element_to_string(self.xml)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800547
548 def is_static_manifest(self):
549 """Return true if there is any project without revision in the xml.
550
551 Returns:
552 A boolean, True if every project has a revision.
553 """
554 count = 0
555 for project in self.xml.findall('.//project'):
556 # check argument directly instead of getting value from default tag
557 if not project.get('revision'):
558 count += 1
559 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800560 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800561 return count == 0
562
563 def remove_project_revision(self):
564 """Remove revision argument from all projects"""
565 for project in self.xml.findall('.//project'):
566 if 'revision' in project:
567 del project['revision']
568
569 def count_path(self, path):
570 """Count projects that path is given path.
571
572 Returns:
573 An integer, indicates the number of projects.
574 """
575 result = 0
576 for project in self.xml.findall('.//project'):
577 if project.get('path') == path:
578 result += 1
579 return result
580
581 def apply_commit(self, path, revision, overwrite=True):
582 """Set revision to a project by path.
583
584 Args:
585 path: A project's path.
586 revision: A git commit id.
587 overwrite: Overwrite flag, the project won't change if overwrite=False
588 and it was modified before.
589 """
590 if path in self.modified and not overwrite:
591 return
592 self.modified.add(path)
593
594 count = 0
595 for project in self.xml.findall('.//project'):
596 if self.parser.get_project_path(project) == path:
597 count += 1
598 project.set('revision', revision)
599
600 if count != 1:
601 logger.warning('found %d path: %s in manifest', count, path)
602
603 def apply_upstream(self, path, upstream):
604 """Set upstream to a project by path.
605
606 Args:
607 path: A project's path.
608 upstream: A git upstream.
609 """
610 for project in self.xml.findall('.//project'):
611 if self.parser.get_project_path(project) == path:
612 project.set('upstream', upstream)
613
614 def apply_action_groups(self, action_groups):
615 """Apply multiple action groups to xml.
616
617 If there are multiple actions in one repo, only last one is applied.
618
619 Args:
620 action_groups: A list of action groups.
621 """
622 # Apply in reversed order with overwrite=False,
623 # so each repo is on the state of last action.
624 for action_group in reversed(action_groups):
625 for action in reversed(action_group.actions):
626 if isinstance(action, codechange.GitCheckoutCommit):
627 self.apply_commit(action.path, action.rev, overwrite=False)
628 if isinstance(action, codechange.GitAddRepo):
629 self.apply_commit(action.path, action.rev, overwrite=False)
630 if isinstance(action, codechange.GitRemoveRepo):
631 assert self.count_path(action.path) == 0
632 self.modified.add(action.path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800633
634 def apply_manifest(self, manifest):
635 """Apply another manifest to current xml.
636
637 By default, all the projects in manifest will be applied and won't
638 overwrite modified projects.
639
640 Args:
641 manifest: A Manifest object.
642 """
643 default = manifest.xml.get('default')
644 for project in manifest.xml.findall('.//project'):
645 path = self.parser.get_project_path(project)
646 revision = self.parser.get_project_revision(project, default)
647 if path and revision:
648 self.apply_commit(path, revision, overwrite=False)
649 upstream = project.get('upstream')
650 if upstream:
651 self.apply_upstream(path, upstream)