blob: cac7175d4b617ce439a18fda668a9d46319ca72e [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')
Kuang-che Wu4521ad62020-10-26 10:50:18 +0800361 # Follow repo's behavior to strip trailing slash (crbug/1086043).
362 return path.rstrip('/')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800363
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 Wu4521ad62020-10-26 10:50:18 +0800422 # Follow repo's behavior to strip trailing slash (crbug/1086043).
423 name = project.get('name').rstrip('/')
424 repo_url = _urljoin(fetch_url, name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800425
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800426 result[path] = codechange.PathSpec(path, repo_url, revision)
427 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800428
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800429 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800430
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800431 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800432 try:
433 root = self.parse_single_xml(content, allow_include=True)
434 except xml.etree.ElementTree.ParseError:
435 logger.warning('%s syntax error, skip', path)
436 return None
437
438 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800439 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800440 result.append(include.get('name'))
441 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800442
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800443 return git_util.get_history_recursively(
444 self.manifest_dir,
445 path,
446 start_time,
447 end_time,
448 parse_dependencies,
449 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800450
451
452class RepoMirror(codechange.CodeStorage):
453 """Repo git mirror."""
454
455 def __init__(self, mirror_dir):
456 self.mirror_dir = mirror_dir
457
458 def _url_to_cache_dir(self, url):
459 # Here we assume remote fetch url is always at root of server url, so we can
460 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800461 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800462 assert path[0] == '/'
463 return '%s.git' % path[1:]
464
465 def cached_git_root(self, repo_url):
466 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800467
468 # The location of chromeos manifest-internal repo mirror is irregular
469 # (http://crbug.com/895957). This is a workaround.
470 if cache_path == 'chromeos/manifest-internal.git':
471 cache_path = 'manifest-internal.git'
472
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800473 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800474
475 def _load_project_list(self, project_root):
476 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800477 with open(repo_project_list) as f:
478 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800479
480 def _save_project_list(self, project_root, lines):
481 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
482 with open(repo_project_list, 'w') as f:
483 f.write(''.join(sorted(lines)))
484
485 def add_to_project_list(self, project_root, path, repo_url):
486 lines = self._load_project_list(project_root)
487
488 line = path + '\n'
489 if line not in lines:
490 lines.append(line)
491
492 self._save_project_list(project_root, lines)
493
494 def remove_from_project_list(self, project_root, path):
495 lines = self._load_project_list(project_root)
496
497 line = path + '\n'
498 if line in lines:
499 lines.remove(line)
500
501 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800502
503
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800504class Manifest:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800505 """This class handles a manifest and is able to patch projects."""
506
507 def __init__(self, manifest_internal_dir):
508 self.xml = None
509 self.manifest_internal_dir = manifest_internal_dir
510 self.modified = set()
511 self.parser = ManifestParser(manifest_internal_dir)
512
513 def load_from_string(self, xml_string):
514 """Load manifest xml from a string.
515
516 Args:
517 xml_string: An xml string.
518 """
519 self.xml = xml.etree.ElementTree.fromstring(xml_string)
520
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800521 def load_from_commit(self, commit):
522 """Load manifest xml snapshot by a commit hash.
523
524 Args:
525 commit: A manifest-internal commit hash.
526 """
527 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
528
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800529 def load_from_timestamp(self, timestamp):
530 """Load manifest xml snapshot by a timestamp.
531
532 The function will load a latest manifest before or equal to the timestamp.
533
534 Args:
535 timestamp: A unix timestamp.
536 """
537 commits = git_util.get_history(
538 self.manifest_internal_dir, before=timestamp + 1)
539 commit = commits[-1][1]
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800540 self.load_from_commit(commit)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800541
542 def to_string(self):
543 """Dump current xml to a string.
544
545 Returns:
546 A string of xml.
547 """
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800548 return ManifestParser.element_to_string(self.xml)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800549
550 def is_static_manifest(self):
551 """Return true if there is any project without revision in the xml.
552
553 Returns:
554 A boolean, True if every project has a revision.
555 """
556 count = 0
557 for project in self.xml.findall('.//project'):
558 # check argument directly instead of getting value from default tag
559 if not project.get('revision'):
560 count += 1
561 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800562 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800563 return count == 0
564
565 def remove_project_revision(self):
566 """Remove revision argument from all projects"""
567 for project in self.xml.findall('.//project'):
568 if 'revision' in project:
569 del project['revision']
570
571 def count_path(self, path):
572 """Count projects that path is given path.
573
574 Returns:
575 An integer, indicates the number of projects.
576 """
577 result = 0
578 for project in self.xml.findall('.//project'):
579 if project.get('path') == path:
580 result += 1
581 return result
582
583 def apply_commit(self, path, revision, overwrite=True):
584 """Set revision to a project by path.
585
586 Args:
587 path: A project's path.
588 revision: A git commit id.
589 overwrite: Overwrite flag, the project won't change if overwrite=False
590 and it was modified before.
591 """
592 if path in self.modified and not overwrite:
593 return
594 self.modified.add(path)
595
596 count = 0
597 for project in self.xml.findall('.//project'):
598 if self.parser.get_project_path(project) == path:
599 count += 1
600 project.set('revision', revision)
601
602 if count != 1:
603 logger.warning('found %d path: %s in manifest', count, path)
604
605 def apply_upstream(self, path, upstream):
606 """Set upstream to a project by path.
607
608 Args:
609 path: A project's path.
610 upstream: A git upstream.
611 """
612 for project in self.xml.findall('.//project'):
613 if self.parser.get_project_path(project) == path:
614 project.set('upstream', upstream)
615
616 def apply_action_groups(self, action_groups):
617 """Apply multiple action groups to xml.
618
619 If there are multiple actions in one repo, only last one is applied.
620
621 Args:
622 action_groups: A list of action groups.
623 """
624 # Apply in reversed order with overwrite=False,
625 # so each repo is on the state of last action.
626 for action_group in reversed(action_groups):
627 for action in reversed(action_group.actions):
628 if isinstance(action, codechange.GitCheckoutCommit):
629 self.apply_commit(action.path, action.rev, overwrite=False)
630 if isinstance(action, codechange.GitAddRepo):
631 self.apply_commit(action.path, action.rev, overwrite=False)
632 if isinstance(action, codechange.GitRemoveRepo):
633 assert self.count_path(action.path) == 0
634 self.modified.add(action.path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800635
636 def apply_manifest(self, manifest):
637 """Apply another manifest to current xml.
638
639 By default, all the projects in manifest will be applied and won't
640 overwrite modified projects.
641
642 Args:
643 manifest: A Manifest object.
644 """
645 default = manifest.xml.get('default')
646 for project in manifest.xml.findall('.//project'):
647 path = self.parser.get_project_path(project)
648 revision = self.parser.get_project_revision(project, default)
649 if path and revision:
650 self.apply_commit(path, revision, overwrite=False)
651 upstream = project.get('upstream')
652 if upstream:
653 self.apply_upstream(path, upstream)