blob: ba161f3c6e643e1dccf1a3b0d8c0acb5b2bdb836 [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
Kuang-che Wub9bc49a2021-01-27 16:27:51 +080013import multiprocessing
Kuang-che Wubfc4a642018-04-19 11:54:08 +080014import os
15import re
Kuang-che Wu34ab7b42019-10-28 19:40:05 +080016import subprocess
Kuang-che Wu999893c2020-04-13 22:06:22 +080017import urllib.parse
Kuang-che Wubfc4a642018-04-19 11:54:08 +080018import xml.etree.ElementTree
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
Kuang-che Wu020a1182020-09-08 17:17:22 +080087 groups: repo sync groups, groups should be separate 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
Kuang-che Wub9bc49a2021-01-27 16:27:51 +0800262 with multiprocessing.Pool() as pool:
263 cleanup_jobs = []
264 for project_path in projects:
265 git_repo = os.path.join(repo_dir, project_path)
266 if not os.path.exists(git_repo):
267 # It should be harmless to ignore git repo nonexistence because 'repo
268 # sync' will restore them.
269 logger.warning('git repo not found: %s', git_repo)
270 continue
271 cleanup_jobs.append((git_repo, nested[project_path]))
272 pool.starmap(git_util.distclean, cleanup_jobs)
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800273
274
Kuang-che Wubfa64482018-10-16 11:49:49 +0800275def _urljoin(base, url):
276 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
277 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
278 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
279 dummy_scheme = 'gopher://'
280 new_scheme = 'persistent-https://'
281 assert not base.startswith(dummy_scheme)
282 assert not url.startswith(dummy_scheme)
283 base = re.sub('^' + new_scheme, dummy_scheme, base)
284 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800285 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800286 result = re.sub('^' + dummy_scheme, new_scheme, result)
287 return result
288
289
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800290class ManifestParser:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800291 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800292
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800293 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800294 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800295 if load_remote:
296 self.manifest_url = get_manifest_url(self.manifest_dir)
297 else:
298 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800299
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800300 def parse_single_xml(self, content, allow_include=False):
301 root = xml.etree.ElementTree.fromstring(content)
302 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800303 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800304 'Expects self-contained manifest. <include> is not allowed')
305 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800306
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800307 def parse_xml_recursive(self, git_rev, path):
308 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800309 root = xml.etree.ElementTree.fromstring(content)
310 default = None
311 notice = None
312 remotes = {}
313 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800314 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800315
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800316 for node in root:
317 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800318 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800319 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800320 nodes = [node]
321
322 for subnode in nodes:
323 if subnode.tag == 'default':
324 if default is not None and not self.element_equal(default, subnode):
325 raise errors.ExternalError('duplicated <default> %s and %s' %
326 (self.element_to_string(default),
327 self.element_to_string(subnode)))
328 if default is None:
329 default = subnode
330 result.append(subnode)
331 elif subnode.tag == 'remote':
332 name = subnode.get('name')
333 if name in remotes and not self.element_equal(remotes[name], subnode):
334 raise errors.ExternalError('duplicated <remote> %s and %s' %
335 (self.element_to_string(default),
336 self.element_to_string(subnode)))
337 if name not in remotes:
338 remotes[name] = subnode
339 result.append(subnode)
340 elif subnode.tag == 'notice':
341 if notice is not None and not self.element_equal(notice, subnode):
342 raise errors.ExternalError('duplicated <notice>')
343 if notice is None:
344 notice = subnode
345 result.append(subnode)
346 elif subnode.tag == 'manifest-server':
347 if manifest_server is not None:
348 raise errors.ExternalError('duplicated <manifest-server>')
349 manifest_server = subnode
350 result.append(subnode)
351 else:
352 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800353 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800354
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800355 @classmethod
356 def element_to_string(cls, element):
357 return xml.etree.ElementTree.tostring(element, encoding='unicode').strip()
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800358
359 @classmethod
360 def get_project_path(cls, project):
361 path = project.get('path')
362 # default path is its name
363 if not path:
364 path = project.get('name')
Kuang-che Wu4521ad62020-10-26 10:50:18 +0800365 # Follow repo's behavior to strip trailing slash (crbug/1086043).
366 return path.rstrip('/')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800367
368 @classmethod
369 def get_project_revision(cls, project, default):
370 if default is None:
371 default = {}
372 return project.get('revision', default.get('revision'))
373
374 def element_equal(self, element1, element2):
375 """Return if two xml elements are same
376
377 Args:
378 element1: An xml element
379 element2: An xml element
380 """
381 if element1.tag != element2.tag:
382 return False
383 if element1.text != element2.text:
384 return False
385 if element1.attrib != element2.attrib:
386 return False
387 if len(element1) != len(element2):
388 return False
389 return all(
390 self.element_equal(node1, node2)
391 for node1, node2 in zip(element1, element2))
392
Kuang-che Wudedc5922020-12-17 17:19:23 +0800393 def process_parsed_result(self, root, group_constraint='default'):
394 if group_constraint not in ('default', 'all'):
395 raise ValueError('only "default" and "all" are supported')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800396 result = {}
397 default = root.find('default')
398 if default is None:
399 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800400
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800401 remote_fetch_map = {}
402 for remote in root.findall('.//remote'):
403 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800404 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800405 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800406 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800407 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800408 'only support git repo at root path of remote server: %s' %
409 fetch_url)
410 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800411
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800412 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800413
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800414 for project in root.findall('.//project'):
Kuang-che Wudedc5922020-12-17 17:19:23 +0800415 if group_constraint == 'default':
416 if 'notdefault' in project.get('groups', ''):
417 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800418 for subproject in project.findall('.//project'):
419 logger.warning('nested project %s.%s is not supported and ignored',
420 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800421
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800422 path = self.get_project_path(project)
423 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800424
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800425 remote_name = project.get('remote', default.get('remote'))
426 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800427 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800428 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wu4521ad62020-10-26 10:50:18 +0800429 # Follow repo's behavior to strip trailing slash (crbug/1086043).
430 name = project.get('name').rstrip('/')
431 repo_url = _urljoin(fetch_url, name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800432
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800433 result[path] = codechange.PathSpec(path, repo_url, revision)
434 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800435
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800436 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800437
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800438 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800439 try:
440 root = self.parse_single_xml(content, allow_include=True)
441 except xml.etree.ElementTree.ParseError:
442 logger.warning('%s syntax error, skip', path)
443 return None
444
445 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800446 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800447 result.append(include.get('name'))
448 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800449
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800450 return git_util.get_history_recursively(
451 self.manifest_dir,
452 path,
453 start_time,
454 end_time,
455 parse_dependencies,
456 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800457
458
459class RepoMirror(codechange.CodeStorage):
460 """Repo git mirror."""
461
462 def __init__(self, mirror_dir):
463 self.mirror_dir = mirror_dir
464
465 def _url_to_cache_dir(self, url):
466 # Here we assume remote fetch url is always at root of server url, so we can
467 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800468 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800469 assert path[0] == '/'
470 return '%s.git' % path[1:]
471
472 def cached_git_root(self, repo_url):
473 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800474
475 # The location of chromeos manifest-internal repo mirror is irregular
476 # (http://crbug.com/895957). This is a workaround.
477 if cache_path == 'chromeos/manifest-internal.git':
478 cache_path = 'manifest-internal.git'
479
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800480 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800481
482 def _load_project_list(self, project_root):
483 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800484 with open(repo_project_list) as f:
485 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800486
487 def _save_project_list(self, project_root, lines):
488 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
489 with open(repo_project_list, 'w') as f:
490 f.write(''.join(sorted(lines)))
491
492 def add_to_project_list(self, project_root, path, repo_url):
493 lines = self._load_project_list(project_root)
494
495 line = path + '\n'
496 if line not in lines:
497 lines.append(line)
498
499 self._save_project_list(project_root, lines)
500
501 def remove_from_project_list(self, project_root, path):
502 lines = self._load_project_list(project_root)
503
504 line = path + '\n'
505 if line in lines:
506 lines.remove(line)
507
508 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800509
510
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800511class Manifest:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800512 """This class handles a manifest and is able to patch projects."""
513
514 def __init__(self, manifest_internal_dir):
515 self.xml = None
516 self.manifest_internal_dir = manifest_internal_dir
517 self.modified = set()
518 self.parser = ManifestParser(manifest_internal_dir)
519
520 def load_from_string(self, xml_string):
521 """Load manifest xml from a string.
522
523 Args:
524 xml_string: An xml string.
525 """
526 self.xml = xml.etree.ElementTree.fromstring(xml_string)
527
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800528 def load_from_commit(self, commit):
529 """Load manifest xml snapshot by a commit hash.
530
531 Args:
532 commit: A manifest-internal commit hash.
533 """
534 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
535
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800536 def load_from_timestamp(self, timestamp):
537 """Load manifest xml snapshot by a timestamp.
538
539 The function will load a latest manifest before or equal to the timestamp.
540
541 Args:
542 timestamp: A unix timestamp.
543 """
544 commits = git_util.get_history(
545 self.manifest_internal_dir, before=timestamp + 1)
546 commit = commits[-1][1]
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800547 self.load_from_commit(commit)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800548
549 def to_string(self):
550 """Dump current xml to a string.
551
552 Returns:
553 A string of xml.
554 """
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800555 return ManifestParser.element_to_string(self.xml)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800556
557 def is_static_manifest(self):
558 """Return true if there is any project without revision in the xml.
559
560 Returns:
561 A boolean, True if every project has a revision.
562 """
563 count = 0
564 for project in self.xml.findall('.//project'):
565 # check argument directly instead of getting value from default tag
566 if not project.get('revision'):
567 count += 1
568 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800569 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800570 return count == 0
571
572 def remove_project_revision(self):
573 """Remove revision argument from all projects"""
574 for project in self.xml.findall('.//project'):
575 if 'revision' in project:
576 del project['revision']
577
578 def count_path(self, path):
579 """Count projects that path is given path.
580
581 Returns:
582 An integer, indicates the number of projects.
583 """
584 result = 0
585 for project in self.xml.findall('.//project'):
586 if project.get('path') == path:
587 result += 1
588 return result
589
590 def apply_commit(self, path, revision, overwrite=True):
591 """Set revision to a project by path.
592
593 Args:
594 path: A project's path.
595 revision: A git commit id.
596 overwrite: Overwrite flag, the project won't change if overwrite=False
597 and it was modified before.
598 """
599 if path in self.modified and not overwrite:
600 return
601 self.modified.add(path)
602
603 count = 0
604 for project in self.xml.findall('.//project'):
605 if self.parser.get_project_path(project) == path:
606 count += 1
607 project.set('revision', revision)
608
609 if count != 1:
610 logger.warning('found %d path: %s in manifest', count, path)
611
612 def apply_upstream(self, path, upstream):
613 """Set upstream to a project by path.
614
615 Args:
616 path: A project's path.
617 upstream: A git upstream.
618 """
619 for project in self.xml.findall('.//project'):
620 if self.parser.get_project_path(project) == path:
621 project.set('upstream', upstream)
622
623 def apply_action_groups(self, action_groups):
624 """Apply multiple action groups to xml.
625
626 If there are multiple actions in one repo, only last one is applied.
627
628 Args:
629 action_groups: A list of action groups.
630 """
631 # Apply in reversed order with overwrite=False,
632 # so each repo is on the state of last action.
633 for action_group in reversed(action_groups):
634 for action in reversed(action_group.actions):
635 if isinstance(action, codechange.GitCheckoutCommit):
636 self.apply_commit(action.path, action.rev, overwrite=False)
637 if isinstance(action, codechange.GitAddRepo):
638 self.apply_commit(action.path, action.rev, overwrite=False)
639 if isinstance(action, codechange.GitRemoveRepo):
640 assert self.count_path(action.path) == 0
641 self.modified.add(action.path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800642
643 def apply_manifest(self, manifest):
644 """Apply another manifest to current xml.
645
646 By default, all the projects in manifest will be applied and won't
647 overwrite modified projects.
648
649 Args:
650 manifest: A Manifest object.
651 """
652 default = manifest.xml.get('default')
653 for project in manifest.xml.findall('.//project'):
654 path = self.parser.get_project_path(project)
655 revision = self.parser.get_project_revision(project, default)
656 if path and revision:
657 self.apply_commit(path, revision, overwrite=False)
658 upstream = project.get('upstream')
659 if upstream:
660 self.apply_upstream(path, upstream)