blob: 0c6d9ca0d684b4dc6188210f4a95c283aa82f2d9 [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 Wudedc5922020-12-17 17:19:23 +0800389 def process_parsed_result(self, root, group_constraint='default'):
390 if group_constraint not in ('default', 'all'):
391 raise ValueError('only "default" and "all" are supported')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800392 result = {}
393 default = root.find('default')
394 if default is None:
395 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800396
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800397 remote_fetch_map = {}
398 for remote in root.findall('.//remote'):
399 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800400 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800401 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800402 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800403 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800404 'only support git repo at root path of remote server: %s' %
405 fetch_url)
406 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800407
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800408 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800409
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800410 for project in root.findall('.//project'):
Kuang-che Wudedc5922020-12-17 17:19:23 +0800411 if group_constraint == 'default':
412 if 'notdefault' in project.get('groups', ''):
413 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800414 for subproject in project.findall('.//project'):
415 logger.warning('nested project %s.%s is not supported and ignored',
416 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800417
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800418 path = self.get_project_path(project)
419 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800420
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800421 remote_name = project.get('remote', default.get('remote'))
422 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800423 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800424 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wu4521ad62020-10-26 10:50:18 +0800425 # Follow repo's behavior to strip trailing slash (crbug/1086043).
426 name = project.get('name').rstrip('/')
427 repo_url = _urljoin(fetch_url, name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800428
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800429 result[path] = codechange.PathSpec(path, repo_url, revision)
430 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800431
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800432 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800433
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800434 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800435 try:
436 root = self.parse_single_xml(content, allow_include=True)
437 except xml.etree.ElementTree.ParseError:
438 logger.warning('%s syntax error, skip', path)
439 return None
440
441 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800442 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800443 result.append(include.get('name'))
444 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800445
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800446 return git_util.get_history_recursively(
447 self.manifest_dir,
448 path,
449 start_time,
450 end_time,
451 parse_dependencies,
452 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800453
454
455class RepoMirror(codechange.CodeStorage):
456 """Repo git mirror."""
457
458 def __init__(self, mirror_dir):
459 self.mirror_dir = mirror_dir
460
461 def _url_to_cache_dir(self, url):
462 # Here we assume remote fetch url is always at root of server url, so we can
463 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800464 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800465 assert path[0] == '/'
466 return '%s.git' % path[1:]
467
468 def cached_git_root(self, repo_url):
469 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800470
471 # The location of chromeos manifest-internal repo mirror is irregular
472 # (http://crbug.com/895957). This is a workaround.
473 if cache_path == 'chromeos/manifest-internal.git':
474 cache_path = 'manifest-internal.git'
475
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800476 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800477
478 def _load_project_list(self, project_root):
479 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800480 with open(repo_project_list) as f:
481 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800482
483 def _save_project_list(self, project_root, lines):
484 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
485 with open(repo_project_list, 'w') as f:
486 f.write(''.join(sorted(lines)))
487
488 def add_to_project_list(self, project_root, path, repo_url):
489 lines = self._load_project_list(project_root)
490
491 line = path + '\n'
492 if line not in lines:
493 lines.append(line)
494
495 self._save_project_list(project_root, lines)
496
497 def remove_from_project_list(self, project_root, path):
498 lines = self._load_project_list(project_root)
499
500 line = path + '\n'
501 if line in lines:
502 lines.remove(line)
503
504 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800505
506
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800507class Manifest:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800508 """This class handles a manifest and is able to patch projects."""
509
510 def __init__(self, manifest_internal_dir):
511 self.xml = None
512 self.manifest_internal_dir = manifest_internal_dir
513 self.modified = set()
514 self.parser = ManifestParser(manifest_internal_dir)
515
516 def load_from_string(self, xml_string):
517 """Load manifest xml from a string.
518
519 Args:
520 xml_string: An xml string.
521 """
522 self.xml = xml.etree.ElementTree.fromstring(xml_string)
523
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800524 def load_from_commit(self, commit):
525 """Load manifest xml snapshot by a commit hash.
526
527 Args:
528 commit: A manifest-internal commit hash.
529 """
530 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
531
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800532 def load_from_timestamp(self, timestamp):
533 """Load manifest xml snapshot by a timestamp.
534
535 The function will load a latest manifest before or equal to the timestamp.
536
537 Args:
538 timestamp: A unix timestamp.
539 """
540 commits = git_util.get_history(
541 self.manifest_internal_dir, before=timestamp + 1)
542 commit = commits[-1][1]
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800543 self.load_from_commit(commit)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800544
545 def to_string(self):
546 """Dump current xml to a string.
547
548 Returns:
549 A string of xml.
550 """
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800551 return ManifestParser.element_to_string(self.xml)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800552
553 def is_static_manifest(self):
554 """Return true if there is any project without revision in the xml.
555
556 Returns:
557 A boolean, True if every project has a revision.
558 """
559 count = 0
560 for project in self.xml.findall('.//project'):
561 # check argument directly instead of getting value from default tag
562 if not project.get('revision'):
563 count += 1
564 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800565 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800566 return count == 0
567
568 def remove_project_revision(self):
569 """Remove revision argument from all projects"""
570 for project in self.xml.findall('.//project'):
571 if 'revision' in project:
572 del project['revision']
573
574 def count_path(self, path):
575 """Count projects that path is given path.
576
577 Returns:
578 An integer, indicates the number of projects.
579 """
580 result = 0
581 for project in self.xml.findall('.//project'):
582 if project.get('path') == path:
583 result += 1
584 return result
585
586 def apply_commit(self, path, revision, overwrite=True):
587 """Set revision to a project by path.
588
589 Args:
590 path: A project's path.
591 revision: A git commit id.
592 overwrite: Overwrite flag, the project won't change if overwrite=False
593 and it was modified before.
594 """
595 if path in self.modified and not overwrite:
596 return
597 self.modified.add(path)
598
599 count = 0
600 for project in self.xml.findall('.//project'):
601 if self.parser.get_project_path(project) == path:
602 count += 1
603 project.set('revision', revision)
604
605 if count != 1:
606 logger.warning('found %d path: %s in manifest', count, path)
607
608 def apply_upstream(self, path, upstream):
609 """Set upstream to a project by path.
610
611 Args:
612 path: A project's path.
613 upstream: A git upstream.
614 """
615 for project in self.xml.findall('.//project'):
616 if self.parser.get_project_path(project) == path:
617 project.set('upstream', upstream)
618
619 def apply_action_groups(self, action_groups):
620 """Apply multiple action groups to xml.
621
622 If there are multiple actions in one repo, only last one is applied.
623
624 Args:
625 action_groups: A list of action groups.
626 """
627 # Apply in reversed order with overwrite=False,
628 # so each repo is on the state of last action.
629 for action_group in reversed(action_groups):
630 for action in reversed(action_group.actions):
631 if isinstance(action, codechange.GitCheckoutCommit):
632 self.apply_commit(action.path, action.rev, overwrite=False)
633 if isinstance(action, codechange.GitAddRepo):
634 self.apply_commit(action.path, action.rev, overwrite=False)
635 if isinstance(action, codechange.GitRemoveRepo):
636 assert self.count_path(action.path) == 0
637 self.modified.add(action.path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800638
639 def apply_manifest(self, manifest):
640 """Apply another manifest to current xml.
641
642 By default, all the projects in manifest will be applied and won't
643 overwrite modified projects.
644
645 Args:
646 manifest: A Manifest object.
647 """
648 default = manifest.xml.get('default')
649 for project in manifest.xml.findall('.//project'):
650 path = self.parser.get_project_path(project)
651 revision = self.parser.get_project_revision(project, default)
652 if path and revision:
653 self.apply_commit(path, revision, overwrite=False)
654 upstream = project.get('upstream')
655 if upstream:
656 self.apply_upstream(path, upstream)