blob: c52c00d83ae10b11e986f34e4d2fc8f45101c8b5 [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()
65 env['GIT_ASKPASS'] = '/usr/bin/true'
66 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,
75 mirror=False):
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 Wubfc4a642018-04-19 11:54:08 +080086 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080087 root = find_repo_root(repo_dir)
88 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080089 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080090 '%s should not be inside another repo project at %s' % (repo_dir, root))
91
Kuang-che Wubfc4a642018-04-19 11:54:08 +080092 cmd = ['repo', 'init', '--manifest-url', manifest_url]
93 if manifest_name:
94 cmd += ['--manifest-name', manifest_name]
95 if manifest_branch:
96 cmd += ['--manifest-branch', manifest_branch]
97 if repo_url:
98 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080099 if reference:
100 cmd += ['--reference', reference]
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800101 if mirror:
102 cmd.append('--mirror')
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800103 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800104
105
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800106def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
107 """Cleanup files generated by <copyfile> <linkfile> tags.
108
109 Args:
110 repo_dir: root directory of repo
111 manifest_name: filename of manifest
112 """
113 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800114 manifest_path = os.path.join(manifest_dir, manifest_name)
115 if os.path.islink(manifest_path):
116 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800117 parser = ManifestParser(manifest_dir)
118 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
119
120 for copyfile in manifest.findall('.//copyfile'):
121 dest = copyfile.get('dest')
122 if not dest:
123 continue
124 # `dest` is relative to the top of the tree
125 dest_path = os.path.join(repo_dir, dest)
126 if not os.path.isfile(dest_path):
127 continue
128 logger.debug('delete file %r', dest_path)
129 os.unlink(dest_path)
130
131 for linkfile in manifest.findall('.//linkfile'):
132 dest = linkfile.get('dest')
133 if not dest:
134 continue
135 # `dest` is relative to the top of the tree
136 dest_path = os.path.join(repo_dir, dest)
137 if not os.path.islink(dest_path):
138 continue
139 logger.debug('delete link %r', dest_path)
140 os.unlink(dest_path)
141
142
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800143def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
144 """Repo sync.
145
146 Args:
147 repo_dir: root directory of repo
148 jobs: projects to fetch simultaneously
149 manifest_name: filename of manifest
150 current_branch: fetch only current branch
151 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800152 # Workaround to prevent garbage files left between repo syncs
153 # (http://crbug.com/881783).
154 cleanup_repo_generated_files(repo_dir)
155
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800156 cmd = ['repo', 'sync', '-q', '--force-sync']
157 if jobs:
158 cmd += ['-j', str(jobs)]
159 if manifest_name:
160 cmd += ['--manifest-name', manifest_name]
161 if current_branch:
162 cmd += ['--current-branch']
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800163 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800164
165
166def abandon(repo_dir, branch_name):
167 """Repo abandon.
168
169 Args:
170 repo_dir: root directory of repo
171 branch_name: branch name to abandon
172 """
173 # Ignore errors if failed, which means the branch didn't exist beforehand.
174 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
175
176
177def info(repo_dir, query):
178 """Repo info.
179
180 Args:
181 repo_dir: root directory of repo
182 query: key to query
183 """
Kuang-che Wu34ab7b42019-10-28 19:40:05 +0800184 try:
185 output = util.check_output('repo', 'info', '.', cwd=repo_dir)
186 except subprocess.CalledProcessError as e:
187 if 'Manifest branch:' not in e.output:
188 raise
189 # "repo info" may exit with error while the data we want is already
190 # printed. Ignore errors for such case.
191 output = e.output
192 for line in output.splitlines():
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800193 key, value = line.split(':', 1)
194 key, value = key.strip(), value.strip()
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800195 if key == query:
196 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800197
198 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800199
200
201def get_current_branch(repo_dir):
202 """Get manifest branch of existing repo directory."""
203 return info(repo_dir, 'Manifest branch')
204
205
206def get_manifest_groups(repo_dir):
207 """Get manifest group of existing repo directory."""
208 return info(repo_dir, 'Manifest groups')
209
210
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800211def list_projects(repo_dir):
212 """Repo list.
213
214 Args:
215 repo_dir: root directory of repo
216
217 Returns:
218 list of paths, relative to repo_dir
219 """
220 result = []
221 for line in util.check_output(
222 'repo', 'list', '--path-only', cwd=repo_dir).splitlines():
223 result.append(line)
224 return result
225
226
227def cleanup_unexpected_files(repo_dir):
228 """Clean up unexpected files in repo tree.
229
230 Note this is not fully equivalent to 'repo sync' from scratch because:
231 - This only handle git repo folders. In other words, directories under
232 repo_dir not inside any git repo will not be touched.
233 - It ignores files if matching gitignore pattern.
234 So we can keep cache files to speed up incremental build next time.
235
236 If you want truly clean tree, delete entire tree and repo sync directly
237 instead.
238
239 Args:
240 repo_dir: root directory of repo
241 """
242 projects = list_projects(repo_dir)
243
244 # When we clean up project X, we don't want to touch files under X's
245 # subprojects. Collect the nested project relationship here.
246 nested = {}
247 # By sorting, parent directory will loop before subdirectories.
248 for project_path in sorted(projects):
249 components = project_path.split(os.sep)
250 for i in range(len(components) - 1, 0, -1):
251 head = os.sep.join(components[:i])
252 tail = os.sep.join(components[i:])
253 if head in nested:
254 nested[head].append(tail)
255 break
256 nested[project_path] = []
257
258 for project_path in projects:
259 git_repo = os.path.join(repo_dir, project_path)
260 if not os.path.exists(git_repo):
261 # It should be harmless to ignore git repo nonexistence because 'repo
262 # sync' will restore them.
263 logger.warning('git repo not found: %s', git_repo)
264 continue
265 git_util.distclean(git_repo, nested[project_path])
266
267
Kuang-che Wubfa64482018-10-16 11:49:49 +0800268def _urljoin(base, url):
269 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
270 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
271 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
272 dummy_scheme = 'gopher://'
273 new_scheme = 'persistent-https://'
274 assert not base.startswith(dummy_scheme)
275 assert not url.startswith(dummy_scheme)
276 base = re.sub('^' + new_scheme, dummy_scheme, base)
277 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800278 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800279 result = re.sub('^' + dummy_scheme, new_scheme, result)
280 return result
281
282
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800283class ManifestParser(object):
284 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800285
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800286 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800287 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800288 if load_remote:
289 self.manifest_url = get_manifest_url(self.manifest_dir)
290 else:
291 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800292
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800293 def parse_single_xml(self, content, allow_include=False):
294 root = xml.etree.ElementTree.fromstring(content)
295 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800296 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800297 'Expects self-contained manifest. <include> is not allowed')
298 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800299
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800300 def parse_xml_recursive(self, git_rev, path):
301 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800302 root = xml.etree.ElementTree.fromstring(content)
303 default = None
304 notice = None
305 remotes = {}
306 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800307 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800308
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800309 for node in root:
310 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800311 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800312 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800313 nodes = [node]
314
315 for subnode in nodes:
316 if subnode.tag == 'default':
317 if default is not None and not self.element_equal(default, subnode):
318 raise errors.ExternalError('duplicated <default> %s and %s' %
319 (self.element_to_string(default),
320 self.element_to_string(subnode)))
321 if default is None:
322 default = subnode
323 result.append(subnode)
324 elif subnode.tag == 'remote':
325 name = subnode.get('name')
326 if name in remotes and not self.element_equal(remotes[name], subnode):
327 raise errors.ExternalError('duplicated <remote> %s and %s' %
328 (self.element_to_string(default),
329 self.element_to_string(subnode)))
330 if name not in remotes:
331 remotes[name] = subnode
332 result.append(subnode)
333 elif subnode.tag == 'notice':
334 if notice is not None and not self.element_equal(notice, subnode):
335 raise errors.ExternalError('duplicated <notice>')
336 if notice is None:
337 notice = subnode
338 result.append(subnode)
339 elif subnode.tag == 'manifest-server':
340 if manifest_server is not None:
341 raise errors.ExternalError('duplicated <manifest-server>')
342 manifest_server = subnode
343 result.append(subnode)
344 else:
345 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800346 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800347
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800348 def element_to_string(self, element):
349 return xml.etree.ElementTree.tostring(element).strip()
350
351 @classmethod
352 def get_project_path(cls, project):
353 path = project.get('path')
354 # default path is its name
355 if not path:
356 path = project.get('name')
357 return path
358
359 @classmethod
360 def get_project_revision(cls, project, default):
361 if default is None:
362 default = {}
363 return project.get('revision', default.get('revision'))
364
365 def element_equal(self, element1, element2):
366 """Return if two xml elements are same
367
368 Args:
369 element1: An xml element
370 element2: An xml element
371 """
372 if element1.tag != element2.tag:
373 return False
374 if element1.text != element2.text:
375 return False
376 if element1.attrib != element2.attrib:
377 return False
378 if len(element1) != len(element2):
379 return False
380 return all(
381 self.element_equal(node1, node2)
382 for node1, node2 in zip(element1, element2))
383
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800384 def process_parsed_result(self, root):
385 result = {}
386 default = root.find('default')
387 if default is None:
388 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800389
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800390 remote_fetch_map = {}
391 for remote in root.findall('.//remote'):
392 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800393 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800394 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800395 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800396 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800397 'only support git repo at root path of remote server: %s' %
398 fetch_url)
399 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800400
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800401 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800402
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800403 for project in root.findall('.//project'):
404 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800405 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800406 for subproject in project.findall('.//project'):
407 logger.warning('nested project %s.%s is not supported and ignored',
408 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800409
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800410 path = self.get_project_path(project)
411 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800412
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800413 remote_name = project.get('remote', default.get('remote'))
414 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800415 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800416 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800417 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800418
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800419 result[path] = codechange.PathSpec(path, repo_url, revision)
420 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800421
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800422 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800423
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800424 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800425 try:
426 root = self.parse_single_xml(content, allow_include=True)
427 except xml.etree.ElementTree.ParseError:
428 logger.warning('%s syntax error, skip', path)
429 return None
430
431 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800432 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800433 result.append(include.get('name'))
434 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800435
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800436 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
437 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800438
439
440class RepoMirror(codechange.CodeStorage):
441 """Repo git mirror."""
442
443 def __init__(self, mirror_dir):
444 self.mirror_dir = mirror_dir
445
446 def _url_to_cache_dir(self, url):
447 # Here we assume remote fetch url is always at root of server url, so we can
448 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800449 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800450 assert path[0] == '/'
451 return '%s.git' % path[1:]
452
453 def cached_git_root(self, repo_url):
454 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800455
456 # The location of chromeos manifest-internal repo mirror is irregular
457 # (http://crbug.com/895957). This is a workaround.
458 if cache_path == 'chromeos/manifest-internal.git':
459 cache_path = 'manifest-internal.git'
460
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800461 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800462
463 def _load_project_list(self, project_root):
464 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800465 with open(repo_project_list) as f:
466 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800467
468 def _save_project_list(self, project_root, lines):
469 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
470 with open(repo_project_list, 'w') as f:
471 f.write(''.join(sorted(lines)))
472
473 def add_to_project_list(self, project_root, path, repo_url):
474 lines = self._load_project_list(project_root)
475
476 line = path + '\n'
477 if line not in lines:
478 lines.append(line)
479
480 self._save_project_list(project_root, lines)
481
482 def remove_from_project_list(self, project_root, path):
483 lines = self._load_project_list(project_root)
484
485 line = path + '\n'
486 if line in lines:
487 lines.remove(line)
488
489 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800490
491
492class Manifest(object):
493 """This class handles a manifest and is able to patch projects."""
494
495 def __init__(self, manifest_internal_dir):
496 self.xml = None
497 self.manifest_internal_dir = manifest_internal_dir
498 self.modified = set()
499 self.parser = ManifestParser(manifest_internal_dir)
500
501 def load_from_string(self, xml_string):
502 """Load manifest xml from a string.
503
504 Args:
505 xml_string: An xml string.
506 """
507 self.xml = xml.etree.ElementTree.fromstring(xml_string)
508
509 def load_from_timestamp(self, timestamp):
510 """Load manifest xml snapshot by a timestamp.
511
512 The function will load a latest manifest before or equal to the timestamp.
513
514 Args:
515 timestamp: A unix timestamp.
516 """
517 commits = git_util.get_history(
518 self.manifest_internal_dir, before=timestamp + 1)
519 commit = commits[-1][1]
520 self.xml = self.parser.parse_xml_recursive(commit, 'full.xml')
521
522 def to_string(self):
523 """Dump current xml to a string.
524
525 Returns:
526 A string of xml.
527 """
528 return xml.etree.ElementTree.tostring(self.xml)
529
530 def is_static_manifest(self):
531 """Return true if there is any project without revision in the xml.
532
533 Returns:
534 A boolean, True if every project has a revision.
535 """
536 count = 0
537 for project in self.xml.findall('.//project'):
538 # check argument directly instead of getting value from default tag
539 if not project.get('revision'):
540 count += 1
541 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800542 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800543 return count == 0
544
545 def remove_project_revision(self):
546 """Remove revision argument from all projects"""
547 for project in self.xml.findall('.//project'):
548 if 'revision' in project:
549 del project['revision']
550
551 def count_path(self, path):
552 """Count projects that path is given path.
553
554 Returns:
555 An integer, indicates the number of projects.
556 """
557 result = 0
558 for project in self.xml.findall('.//project'):
559 if project.get('path') == path:
560 result += 1
561 return result
562
563 def apply_commit(self, path, revision, overwrite=True):
564 """Set revision to a project by path.
565
566 Args:
567 path: A project's path.
568 revision: A git commit id.
569 overwrite: Overwrite flag, the project won't change if overwrite=False
570 and it was modified before.
571 """
572 if path in self.modified and not overwrite:
573 return
574 self.modified.add(path)
575
576 count = 0
577 for project in self.xml.findall('.//project'):
578 if self.parser.get_project_path(project) == path:
579 count += 1
580 project.set('revision', revision)
581
582 if count != 1:
583 logger.warning('found %d path: %s in manifest', count, path)
584
585 def apply_upstream(self, path, upstream):
586 """Set upstream to a project by path.
587
588 Args:
589 path: A project's path.
590 upstream: A git upstream.
591 """
592 for project in self.xml.findall('.//project'):
593 if self.parser.get_project_path(project) == path:
594 project.set('upstream', upstream)
595
596 def apply_action_groups(self, action_groups):
597 """Apply multiple action groups to xml.
598
599 If there are multiple actions in one repo, only last one is applied.
600
601 Args:
602 action_groups: A list of action groups.
603 """
604 # Apply in reversed order with overwrite=False,
605 # so each repo is on the state of last action.
606 for action_group in reversed(action_groups):
607 for action in reversed(action_group.actions):
608 if isinstance(action, codechange.GitCheckoutCommit):
609 self.apply_commit(action.path, action.rev, overwrite=False)
610 if isinstance(action, codechange.GitAddRepo):
611 self.apply_commit(action.path, action.rev, overwrite=False)
612 if isinstance(action, codechange.GitRemoveRepo):
613 assert self.count_path(action.path) == 0
614 self.modified.add(action.path)
615 return
616
617 def apply_manifest(self, manifest):
618 """Apply another manifest to current xml.
619
620 By default, all the projects in manifest will be applied and won't
621 overwrite modified projects.
622
623 Args:
624 manifest: A Manifest object.
625 """
626 default = manifest.xml.get('default')
627 for project in manifest.xml.findall('.//project'):
628 path = self.parser.get_project_path(project)
629 revision = self.parser.get_project_revision(project, default)
630 if path and revision:
631 self.apply_commit(path, revision, overwrite=False)
632 upstream = project.get('upstream')
633 if upstream:
634 self.apply_upstream(path, upstream)