blob: 434d82d9eed02b9804a2478120f335061cdfd5a2 [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 Chang0fc704b2019-12-09 18:43:38 +0800352 def element_to_string(self, element):
353 return xml.etree.ElementTree.tostring(element).strip()
354
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
518 def load_from_timestamp(self, timestamp):
519 """Load manifest xml snapshot by a timestamp.
520
521 The function will load a latest manifest before or equal to the timestamp.
522
523 Args:
524 timestamp: A unix timestamp.
525 """
526 commits = git_util.get_history(
527 self.manifest_internal_dir, before=timestamp + 1)
528 commit = commits[-1][1]
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +0800529 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800530
531 def to_string(self):
532 """Dump current xml to a string.
533
534 Returns:
535 A string of xml.
536 """
537 return xml.etree.ElementTree.tostring(self.xml)
538
539 def is_static_manifest(self):
540 """Return true if there is any project without revision in the xml.
541
542 Returns:
543 A boolean, True if every project has a revision.
544 """
545 count = 0
546 for project in self.xml.findall('.//project'):
547 # check argument directly instead of getting value from default tag
548 if not project.get('revision'):
549 count += 1
550 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800551 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800552 return count == 0
553
554 def remove_project_revision(self):
555 """Remove revision argument from all projects"""
556 for project in self.xml.findall('.//project'):
557 if 'revision' in project:
558 del project['revision']
559
560 def count_path(self, path):
561 """Count projects that path is given path.
562
563 Returns:
564 An integer, indicates the number of projects.
565 """
566 result = 0
567 for project in self.xml.findall('.//project'):
568 if project.get('path') == path:
569 result += 1
570 return result
571
572 def apply_commit(self, path, revision, overwrite=True):
573 """Set revision to a project by path.
574
575 Args:
576 path: A project's path.
577 revision: A git commit id.
578 overwrite: Overwrite flag, the project won't change if overwrite=False
579 and it was modified before.
580 """
581 if path in self.modified and not overwrite:
582 return
583 self.modified.add(path)
584
585 count = 0
586 for project in self.xml.findall('.//project'):
587 if self.parser.get_project_path(project) == path:
588 count += 1
589 project.set('revision', revision)
590
591 if count != 1:
592 logger.warning('found %d path: %s in manifest', count, path)
593
594 def apply_upstream(self, path, upstream):
595 """Set upstream to a project by path.
596
597 Args:
598 path: A project's path.
599 upstream: A git upstream.
600 """
601 for project in self.xml.findall('.//project'):
602 if self.parser.get_project_path(project) == path:
603 project.set('upstream', upstream)
604
605 def apply_action_groups(self, action_groups):
606 """Apply multiple action groups to xml.
607
608 If there are multiple actions in one repo, only last one is applied.
609
610 Args:
611 action_groups: A list of action groups.
612 """
613 # Apply in reversed order with overwrite=False,
614 # so each repo is on the state of last action.
615 for action_group in reversed(action_groups):
616 for action in reversed(action_group.actions):
617 if isinstance(action, codechange.GitCheckoutCommit):
618 self.apply_commit(action.path, action.rev, overwrite=False)
619 if isinstance(action, codechange.GitAddRepo):
620 self.apply_commit(action.path, action.rev, overwrite=False)
621 if isinstance(action, codechange.GitRemoveRepo):
622 assert self.count_path(action.path) == 0
623 self.modified.add(action.path)
624 return
625
626 def apply_manifest(self, manifest):
627 """Apply another manifest to current xml.
628
629 By default, all the projects in manifest will be applied and won't
630 overwrite modified projects.
631
632 Args:
633 manifest: A Manifest object.
634 """
635 default = manifest.xml.get('default')
636 for project in manifest.xml.findall('.//project'):
637 path = self.parser.get_project_path(project)
638 revision = self.parser.get_project_revision(project, default)
639 if path and revision:
640 self.apply_commit(path, revision, overwrite=False)
641 upstream = project.get('upstream')
642 if upstream:
643 self.apply_upstream(path, upstream)