blob: 51baf7594b0a956631c43cff651fd42b748c97d9 [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 Wu999893c2020-04-13 22:06:22 +080016import urllib.parse
Kuang-che Wubfc4a642018-04-19 11:54:08 +080017import xml.etree.ElementTree
18
Kuang-che Wud1d45b42018-07-05 00:46:45 +080019from bisect_kit import codechange
Kuang-che Wue121fae2018-11-09 16:18:39 +080020from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080021from bisect_kit import git_util
22from bisect_kit import util
23
24logger = logging.getLogger(__name__)
25
26
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080027def get_manifest_url(manifest_dir):
Kuang-che Wud1d45b42018-07-05 00:46:45 +080028 """Get manifest URL of repo project.
29
30 Args:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080031 manifest_dir: path of manifest directory
Kuang-che Wud1d45b42018-07-05 00:46:45 +080032
33 Returns:
34 manifest URL.
35 """
Kuang-che Wud1d45b42018-07-05 00:46:45 +080036 url = util.check_output(
37 'git', 'config', 'remote.origin.url', cwd=manifest_dir)
Kuang-che Wud1d45b42018-07-05 00:46:45 +080038 return url
39
40
Kuang-che Wu41e8b592018-09-25 17:01:30 +080041def find_repo_root(path):
42 """Find the root path of a repo project
43
44 Args:
45 path: path
46
47 Returns:
48 project root if path is inside a repo project; otherwise None
49 """
50 path = os.path.abspath(path)
51 while not os.path.exists(os.path.join(path, '.repo')):
52 if path == '/':
53 return None
54 path = os.path.dirname(path)
55 return path
56
57
Kuang-che Wu2be0b212020-01-15 19:50:11 +080058def _get_repo_sync_env():
59 # b/120757273 Even we have set git cookies, git still occasionally asks for
60 # username/password for unknown reasons. Then it hangs forever because we are
61 # a script. Here we work around the issue by setting GIT_ASKPASS and fail the
62 # auth. The failure is usually harmless because bisect-kit will retry.
63 env = os.environ.copy()
Kuang-che Wu57e8b8e2020-02-06 20:04:36 +080064 env['GIT_ASKPASS'] = '/bin/true'
Kuang-che Wu2be0b212020-01-15 19:50:11 +080065 return env
66
67
Kuang-che Wubfc4a642018-04-19 11:54:08 +080068def init(repo_dir,
69 manifest_url,
70 manifest_branch=None,
71 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080072 repo_url=None,
Kuang-che Wu41e8b592018-09-25 17:01:30 +080073 reference=None,
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +080074 mirror=False,
75 groups=None):
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 Wu020a1182020-09-08 17:17:22 +080086 groups: repo sync groups, groups should be separate by comma
Kuang-che Wubfc4a642018-04-19 11:54:08 +080087 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080088 root = find_repo_root(repo_dir)
89 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080090 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080091 '%s should not be inside another repo project at %s' % (repo_dir, root))
92
Kuang-che Wubfc4a642018-04-19 11:54:08 +080093 cmd = ['repo', 'init', '--manifest-url', manifest_url]
94 if manifest_name:
95 cmd += ['--manifest-name', manifest_name]
96 if manifest_branch:
97 cmd += ['--manifest-branch', manifest_branch]
98 if repo_url:
99 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800100 if reference:
101 cmd += ['--reference', reference]
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +0800102 if groups:
103 cmd += ['--groups', groups]
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800104 if mirror:
105 cmd.append('--mirror')
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800106 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800107
108
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800109def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
110 """Cleanup files generated by <copyfile> <linkfile> tags.
111
112 Args:
113 repo_dir: root directory of repo
114 manifest_name: filename of manifest
115 """
116 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800117 manifest_path = os.path.join(manifest_dir, manifest_name)
118 if os.path.islink(manifest_path):
119 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800120 parser = ManifestParser(manifest_dir)
121 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
122
123 for copyfile in manifest.findall('.//copyfile'):
124 dest = copyfile.get('dest')
125 if not dest:
126 continue
127 # `dest` is relative to the top of the tree
128 dest_path = os.path.join(repo_dir, dest)
129 if not os.path.isfile(dest_path):
130 continue
131 logger.debug('delete file %r', dest_path)
132 os.unlink(dest_path)
133
134 for linkfile in manifest.findall('.//linkfile'):
135 dest = linkfile.get('dest')
136 if not dest:
137 continue
138 # `dest` is relative to the top of the tree
139 dest_path = os.path.join(repo_dir, dest)
140 if not os.path.islink(dest_path):
141 continue
142 logger.debug('delete link %r', dest_path)
143 os.unlink(dest_path)
144
145
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800146def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
147 """Repo sync.
148
149 Args:
150 repo_dir: root directory of repo
151 jobs: projects to fetch simultaneously
152 manifest_name: filename of manifest
153 current_branch: fetch only current branch
154 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800155 # Workaround to prevent garbage files left between repo syncs
156 # (http://crbug.com/881783).
157 cleanup_repo_generated_files(repo_dir)
158
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800159 cmd = ['repo', 'sync', '-q', '--force-sync']
160 if jobs:
161 cmd += ['-j', str(jobs)]
162 if manifest_name:
163 cmd += ['--manifest-name', manifest_name]
164 if current_branch:
165 cmd += ['--current-branch']
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800166 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800167
168
169def abandon(repo_dir, branch_name):
170 """Repo abandon.
171
172 Args:
173 repo_dir: root directory of repo
174 branch_name: branch name to abandon
175 """
176 # Ignore errors if failed, which means the branch didn't exist beforehand.
177 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
178
179
180def info(repo_dir, query):
181 """Repo info.
182
183 Args:
184 repo_dir: root directory of repo
185 query: key to query
186 """
Kuang-che Wu34ab7b42019-10-28 19:40:05 +0800187 try:
188 output = util.check_output('repo', 'info', '.', cwd=repo_dir)
189 except subprocess.CalledProcessError as e:
190 if 'Manifest branch:' not in e.output:
191 raise
192 # "repo info" may exit with error while the data we want is already
193 # printed. Ignore errors for such case.
194 output = e.output
195 for line in output.splitlines():
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800196 key, value = line.split(':', 1)
197 key, value = key.strip(), value.strip()
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800198 if key == query:
199 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800200
201 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800202
203
204def get_current_branch(repo_dir):
205 """Get manifest branch of existing repo directory."""
206 return info(repo_dir, 'Manifest branch')
207
208
209def get_manifest_groups(repo_dir):
210 """Get manifest group of existing repo directory."""
211 return info(repo_dir, 'Manifest groups')
212
213
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800214def list_projects(repo_dir):
215 """Repo list.
216
217 Args:
218 repo_dir: root directory of repo
219
220 Returns:
221 list of paths, relative to repo_dir
222 """
223 result = []
224 for line in util.check_output(
225 'repo', 'list', '--path-only', cwd=repo_dir).splitlines():
226 result.append(line)
227 return result
228
229
230def cleanup_unexpected_files(repo_dir):
231 """Clean up unexpected files in repo tree.
232
233 Note this is not fully equivalent to 'repo sync' from scratch because:
234 - This only handle git repo folders. In other words, directories under
235 repo_dir not inside any git repo will not be touched.
236 - It ignores files if matching gitignore pattern.
237 So we can keep cache files to speed up incremental build next time.
238
239 If you want truly clean tree, delete entire tree and repo sync directly
240 instead.
241
242 Args:
243 repo_dir: root directory of repo
244 """
245 projects = list_projects(repo_dir)
246
247 # When we clean up project X, we don't want to touch files under X's
248 # subprojects. Collect the nested project relationship here.
249 nested = {}
250 # By sorting, parent directory will loop before subdirectories.
251 for project_path in sorted(projects):
252 components = project_path.split(os.sep)
253 for i in range(len(components) - 1, 0, -1):
254 head = os.sep.join(components[:i])
255 tail = os.sep.join(components[i:])
256 if head in nested:
257 nested[head].append(tail)
258 break
259 nested[project_path] = []
260
261 for project_path in projects:
262 git_repo = os.path.join(repo_dir, project_path)
263 if not os.path.exists(git_repo):
264 # It should be harmless to ignore git repo nonexistence because 'repo
265 # sync' will restore them.
266 logger.warning('git repo not found: %s', git_repo)
267 continue
268 git_util.distclean(git_repo, nested[project_path])
269
270
Kuang-che Wubfa64482018-10-16 11:49:49 +0800271def _urljoin(base, url):
272 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
273 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
274 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
275 dummy_scheme = 'gopher://'
276 new_scheme = 'persistent-https://'
277 assert not base.startswith(dummy_scheme)
278 assert not url.startswith(dummy_scheme)
279 base = re.sub('^' + new_scheme, dummy_scheme, base)
280 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800281 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800282 result = re.sub('^' + dummy_scheme, new_scheme, result)
283 return result
284
285
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800286class ManifestParser:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800287 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800288
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800289 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800290 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800291 if load_remote:
292 self.manifest_url = get_manifest_url(self.manifest_dir)
293 else:
294 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800295
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800296 def parse_single_xml(self, content, allow_include=False):
297 root = xml.etree.ElementTree.fromstring(content)
298 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800299 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800300 'Expects self-contained manifest. <include> is not allowed')
301 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800302
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800303 def parse_xml_recursive(self, git_rev, path):
304 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800305 root = xml.etree.ElementTree.fromstring(content)
306 default = None
307 notice = None
308 remotes = {}
309 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800310 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800311
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800312 for node in root:
313 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800314 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800315 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800316 nodes = [node]
317
318 for subnode in nodes:
319 if subnode.tag == 'default':
320 if default is not None and not self.element_equal(default, subnode):
321 raise errors.ExternalError('duplicated <default> %s and %s' %
322 (self.element_to_string(default),
323 self.element_to_string(subnode)))
324 if default is None:
325 default = subnode
326 result.append(subnode)
327 elif subnode.tag == 'remote':
328 name = subnode.get('name')
329 if name in remotes and not self.element_equal(remotes[name], subnode):
330 raise errors.ExternalError('duplicated <remote> %s and %s' %
331 (self.element_to_string(default),
332 self.element_to_string(subnode)))
333 if name not in remotes:
334 remotes[name] = subnode
335 result.append(subnode)
336 elif subnode.tag == 'notice':
337 if notice is not None and not self.element_equal(notice, subnode):
338 raise errors.ExternalError('duplicated <notice>')
339 if notice is None:
340 notice = subnode
341 result.append(subnode)
342 elif subnode.tag == 'manifest-server':
343 if manifest_server is not None:
344 raise errors.ExternalError('duplicated <manifest-server>')
345 manifest_server = subnode
346 result.append(subnode)
347 else:
348 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800349 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800350
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800351 @classmethod
352 def element_to_string(cls, element):
353 return xml.etree.ElementTree.tostring(element, encoding='unicode').strip()
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800354
355 @classmethod
356 def get_project_path(cls, project):
357 path = project.get('path')
358 # default path is its name
359 if not path:
360 path = project.get('name')
361 return path
362
363 @classmethod
364 def get_project_revision(cls, project, default):
365 if default is None:
366 default = {}
367 return project.get('revision', default.get('revision'))
368
369 def element_equal(self, element1, element2):
370 """Return if two xml elements are same
371
372 Args:
373 element1: An xml element
374 element2: An xml element
375 """
376 if element1.tag != element2.tag:
377 return False
378 if element1.text != element2.text:
379 return False
380 if element1.attrib != element2.attrib:
381 return False
382 if len(element1) != len(element2):
383 return False
384 return all(
385 self.element_equal(node1, node2)
386 for node1, node2 in zip(element1, element2))
387
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800388 def process_parsed_result(self, root):
389 result = {}
390 default = root.find('default')
391 if default is None:
392 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800393
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800394 remote_fetch_map = {}
395 for remote in root.findall('.//remote'):
396 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800397 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800398 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800399 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800400 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800401 'only support git repo at root path of remote server: %s' %
402 fetch_url)
403 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800404
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800405 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800406
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800407 for project in root.findall('.//project'):
408 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800409 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800410 for subproject in project.findall('.//project'):
411 logger.warning('nested project %s.%s is not supported and ignored',
412 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800413
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800414 path = self.get_project_path(project)
415 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800416
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800417 remote_name = project.get('remote', default.get('remote'))
418 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800419 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800420 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800421 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800422
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800423 result[path] = codechange.PathSpec(path, repo_url, revision)
424 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800425
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800426 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800427
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800428 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800429 try:
430 root = self.parse_single_xml(content, allow_include=True)
431 except xml.etree.ElementTree.ParseError:
432 logger.warning('%s syntax error, skip', path)
433 return None
434
435 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800436 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800437 result.append(include.get('name'))
438 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800439
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800440 return git_util.get_history_recursively(
441 self.manifest_dir,
442 path,
443 start_time,
444 end_time,
445 parse_dependencies,
446 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800447
448
449class RepoMirror(codechange.CodeStorage):
450 """Repo git mirror."""
451
452 def __init__(self, mirror_dir):
453 self.mirror_dir = mirror_dir
454
455 def _url_to_cache_dir(self, url):
456 # Here we assume remote fetch url is always at root of server url, so we can
457 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800458 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800459 assert path[0] == '/'
460 return '%s.git' % path[1:]
461
462 def cached_git_root(self, repo_url):
463 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800464
465 # The location of chromeos manifest-internal repo mirror is irregular
466 # (http://crbug.com/895957). This is a workaround.
467 if cache_path == 'chromeos/manifest-internal.git':
468 cache_path = 'manifest-internal.git'
469
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800470 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800471
472 def _load_project_list(self, project_root):
473 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800474 with open(repo_project_list) as f:
475 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800476
477 def _save_project_list(self, project_root, lines):
478 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
479 with open(repo_project_list, 'w') as f:
480 f.write(''.join(sorted(lines)))
481
482 def add_to_project_list(self, project_root, path, repo_url):
483 lines = self._load_project_list(project_root)
484
485 line = path + '\n'
486 if line not in lines:
487 lines.append(line)
488
489 self._save_project_list(project_root, lines)
490
491 def remove_from_project_list(self, project_root, path):
492 lines = self._load_project_list(project_root)
493
494 line = path + '\n'
495 if line in lines:
496 lines.remove(line)
497
498 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800499
500
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800501class Manifest:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800502 """This class handles a manifest and is able to patch projects."""
503
504 def __init__(self, manifest_internal_dir):
505 self.xml = None
506 self.manifest_internal_dir = manifest_internal_dir
507 self.modified = set()
508 self.parser = ManifestParser(manifest_internal_dir)
509
510 def load_from_string(self, xml_string):
511 """Load manifest xml from a string.
512
513 Args:
514 xml_string: An xml string.
515 """
516 self.xml = xml.etree.ElementTree.fromstring(xml_string)
517
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800518 def load_from_commit(self, commit):
519 """Load manifest xml snapshot by a commit hash.
520
521 Args:
522 commit: A manifest-internal commit hash.
523 """
524 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
525
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800526 def load_from_timestamp(self, timestamp):
527 """Load manifest xml snapshot by a timestamp.
528
529 The function will load a latest manifest before or equal to the timestamp.
530
531 Args:
532 timestamp: A unix timestamp.
533 """
534 commits = git_util.get_history(
535 self.manifest_internal_dir, before=timestamp + 1)
536 commit = commits[-1][1]
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800537 self.load_from_commit(commit)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800538
539 def to_string(self):
540 """Dump current xml to a string.
541
542 Returns:
543 A string of xml.
544 """
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800545 return ManifestParser.element_to_string(self.xml)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800546
547 def is_static_manifest(self):
548 """Return true if there is any project without revision in the xml.
549
550 Returns:
551 A boolean, True if every project has a revision.
552 """
553 count = 0
554 for project in self.xml.findall('.//project'):
555 # check argument directly instead of getting value from default tag
556 if not project.get('revision'):
557 count += 1
558 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800559 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800560 return count == 0
561
562 def remove_project_revision(self):
563 """Remove revision argument from all projects"""
564 for project in self.xml.findall('.//project'):
565 if 'revision' in project:
566 del project['revision']
567
568 def count_path(self, path):
569 """Count projects that path is given path.
570
571 Returns:
572 An integer, indicates the number of projects.
573 """
574 result = 0
575 for project in self.xml.findall('.//project'):
576 if project.get('path') == path:
577 result += 1
578 return result
579
580 def apply_commit(self, path, revision, overwrite=True):
581 """Set revision to a project by path.
582
583 Args:
584 path: A project's path.
585 revision: A git commit id.
586 overwrite: Overwrite flag, the project won't change if overwrite=False
587 and it was modified before.
588 """
589 if path in self.modified and not overwrite:
590 return
591 self.modified.add(path)
592
593 count = 0
594 for project in self.xml.findall('.//project'):
595 if self.parser.get_project_path(project) == path:
596 count += 1
597 project.set('revision', revision)
598
599 if count != 1:
600 logger.warning('found %d path: %s in manifest', count, path)
601
602 def apply_upstream(self, path, upstream):
603 """Set upstream to a project by path.
604
605 Args:
606 path: A project's path.
607 upstream: A git upstream.
608 """
609 for project in self.xml.findall('.//project'):
610 if self.parser.get_project_path(project) == path:
611 project.set('upstream', upstream)
612
613 def apply_action_groups(self, action_groups):
614 """Apply multiple action groups to xml.
615
616 If there are multiple actions in one repo, only last one is applied.
617
618 Args:
619 action_groups: A list of action groups.
620 """
621 # Apply in reversed order with overwrite=False,
622 # so each repo is on the state of last action.
623 for action_group in reversed(action_groups):
624 for action in reversed(action_group.actions):
625 if isinstance(action, codechange.GitCheckoutCommit):
626 self.apply_commit(action.path, action.rev, overwrite=False)
627 if isinstance(action, codechange.GitAddRepo):
628 self.apply_commit(action.path, action.rev, overwrite=False)
629 if isinstance(action, codechange.GitRemoveRepo):
630 assert self.count_path(action.path) == 0
631 self.modified.add(action.path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800632
633 def apply_manifest(self, manifest):
634 """Apply another manifest to current xml.
635
636 By default, all the projects in manifest will be applied and won't
637 overwrite modified projects.
638
639 Args:
640 manifest: A Manifest object.
641 """
642 default = manifest.xml.get('default')
643 for project in manifest.xml.findall('.//project'):
644 path = self.parser.get_project_path(project)
645 revision = self.parser.get_project_revision(project, default)
646 if path and revision:
647 self.apply_commit(path, revision, overwrite=False)
648 upstream = project.get('upstream')
649 if upstream:
650 self.apply_upstream(path, upstream)