blob: cc163ce92b93102c167b1304c44b6643ab14854c [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
Kuang-che Wucc2870b2021-02-18 15:36:00 +080027# Relative to repo root dir.
28REPO_META_DIR = '.repo'
29LOCAL_MANIFESTS_DIR = os.path.join(REPO_META_DIR, 'local_manifests')
30
Kuang-che Wubfc4a642018-04-19 11:54:08 +080031
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080032def get_manifest_url(manifest_dir):
Kuang-che Wud1d45b42018-07-05 00:46:45 +080033 """Get manifest URL of repo project.
34
35 Args:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080036 manifest_dir: path of manifest directory
Kuang-che Wud1d45b42018-07-05 00:46:45 +080037
38 Returns:
39 manifest URL.
40 """
Kuang-che Wud1d45b42018-07-05 00:46:45 +080041 url = util.check_output(
42 'git', 'config', 'remote.origin.url', cwd=manifest_dir)
Kuang-che Wud1d45b42018-07-05 00:46:45 +080043 return url
44
45
Kuang-che Wu41e8b592018-09-25 17:01:30 +080046def find_repo_root(path):
47 """Find the root path of a repo project
48
49 Args:
50 path: path
51
52 Returns:
53 project root if path is inside a repo project; otherwise None
54 """
55 path = os.path.abspath(path)
56 while not os.path.exists(os.path.join(path, '.repo')):
57 if path == '/':
58 return None
59 path = os.path.dirname(path)
60 return path
61
62
Kuang-che Wu2be0b212020-01-15 19:50:11 +080063def _get_repo_sync_env():
64 # b/120757273 Even we have set git cookies, git still occasionally asks for
65 # username/password for unknown reasons. Then it hangs forever because we are
66 # a script. Here we work around the issue by setting GIT_ASKPASS and fail the
67 # auth. The failure is usually harmless because bisect-kit will retry.
68 env = os.environ.copy()
Kuang-che Wu57e8b8e2020-02-06 20:04:36 +080069 env['GIT_ASKPASS'] = '/bin/true'
Kuang-che Wu2be0b212020-01-15 19:50:11 +080070 return env
71
72
Kuang-che Wubfc4a642018-04-19 11:54:08 +080073def init(repo_dir,
74 manifest_url,
75 manifest_branch=None,
76 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080077 repo_url=None,
Kuang-che Wu41e8b592018-09-25 17:01:30 +080078 reference=None,
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +080079 mirror=False,
80 groups=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080081 """Repo init.
82
83 Args:
84 repo_dir: root directory of repo
85 manifest_url: manifest repository location
86 manifest_branch: manifest branch or revision
87 manifest_name: initial manifest file name
88 repo_url: repo repository location
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080089 reference: location of mirror directory
Kuang-che Wu41e8b592018-09-25 17:01:30 +080090 mirror: indicates repo mirror
Kuang-che Wu020a1182020-09-08 17:17:22 +080091 groups: repo sync groups, groups should be separate by comma
Kuang-che Wubfc4a642018-04-19 11:54:08 +080092 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080093 root = find_repo_root(repo_dir)
94 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080095 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080096 '%s should not be inside another repo project at %s' % (repo_dir, root))
97
Kuang-che Wubfc4a642018-04-19 11:54:08 +080098 cmd = ['repo', 'init', '--manifest-url', manifest_url]
99 if manifest_name:
100 cmd += ['--manifest-name', manifest_name]
101 if manifest_branch:
102 cmd += ['--manifest-branch', manifest_branch]
103 if repo_url:
104 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800105 if reference:
106 cmd += ['--reference', reference]
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +0800107 if groups:
108 cmd += ['--groups', groups]
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800109 if mirror:
110 cmd.append('--mirror')
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800111 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800112
113
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800114def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
115 """Cleanup files generated by <copyfile> <linkfile> tags.
116
117 Args:
118 repo_dir: root directory of repo
119 manifest_name: filename of manifest
120 """
121 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800122 manifest_path = os.path.join(manifest_dir, manifest_name)
123 if os.path.islink(manifest_path):
124 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800125 parser = ManifestParser(manifest_dir)
126 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
127
128 for copyfile in manifest.findall('.//copyfile'):
129 dest = copyfile.get('dest')
130 if not dest:
131 continue
132 # `dest` is relative to the top of the tree
133 dest_path = os.path.join(repo_dir, dest)
134 if not os.path.isfile(dest_path):
135 continue
136 logger.debug('delete file %r', dest_path)
137 os.unlink(dest_path)
138
139 for linkfile in manifest.findall('.//linkfile'):
140 dest = linkfile.get('dest')
141 if not dest:
142 continue
143 # `dest` is relative to the top of the tree
144 dest_path = os.path.join(repo_dir, dest)
145 if not os.path.islink(dest_path):
146 continue
147 logger.debug('delete link %r', dest_path)
148 os.unlink(dest_path)
149
150
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800151def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
152 """Repo sync.
153
154 Args:
155 repo_dir: root directory of repo
156 jobs: projects to fetch simultaneously
157 manifest_name: filename of manifest
158 current_branch: fetch only current branch
159 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800160 # Workaround to prevent garbage files left between repo syncs
161 # (http://crbug.com/881783).
162 cleanup_repo_generated_files(repo_dir)
163
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800164 cmd = ['repo', 'sync', '-q', '--force-sync']
165 if jobs:
166 cmd += ['-j', str(jobs)]
167 if manifest_name:
168 cmd += ['--manifest-name', manifest_name]
169 if current_branch:
170 cmd += ['--current-branch']
Kuang-che Wu2be0b212020-01-15 19:50:11 +0800171 util.check_call(*cmd, cwd=repo_dir, env=_get_repo_sync_env())
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800172
173
174def abandon(repo_dir, branch_name):
175 """Repo abandon.
176
177 Args:
178 repo_dir: root directory of repo
179 branch_name: branch name to abandon
180 """
181 # Ignore errors if failed, which means the branch didn't exist beforehand.
182 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
183
184
185def info(repo_dir, query):
186 """Repo info.
187
188 Args:
189 repo_dir: root directory of repo
190 query: key to query
191 """
Kuang-che Wu34ab7b42019-10-28 19:40:05 +0800192 try:
193 output = util.check_output('repo', 'info', '.', cwd=repo_dir)
194 except subprocess.CalledProcessError as e:
195 if 'Manifest branch:' not in e.output:
196 raise
197 # "repo info" may exit with error while the data we want is already
198 # printed. Ignore errors for such case.
199 output = e.output
200 for line in output.splitlines():
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800201 key, value = line.split(':', 1)
202 key, value = key.strip(), value.strip()
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800203 if key == query:
204 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800205
206 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800207
208
209def get_current_branch(repo_dir):
210 """Get manifest branch of existing repo directory."""
211 return info(repo_dir, 'Manifest branch')
212
213
214def get_manifest_groups(repo_dir):
215 """Get manifest group of existing repo directory."""
216 return info(repo_dir, 'Manifest groups')
217
218
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800219def list_projects(repo_dir):
220 """Repo list.
221
222 Args:
223 repo_dir: root directory of repo
224
225 Returns:
226 list of paths, relative to repo_dir
227 """
228 result = []
229 for line in util.check_output(
230 'repo', 'list', '--path-only', cwd=repo_dir).splitlines():
231 result.append(line)
232 return result
233
234
235def cleanup_unexpected_files(repo_dir):
236 """Clean up unexpected files in repo tree.
237
238 Note this is not fully equivalent to 'repo sync' from scratch because:
239 - This only handle git repo folders. In other words, directories under
240 repo_dir not inside any git repo will not be touched.
241 - It ignores files if matching gitignore pattern.
242 So we can keep cache files to speed up incremental build next time.
243
244 If you want truly clean tree, delete entire tree and repo sync directly
245 instead.
246
247 Args:
248 repo_dir: root directory of repo
249 """
250 projects = list_projects(repo_dir)
251
252 # When we clean up project X, we don't want to touch files under X's
253 # subprojects. Collect the nested project relationship here.
254 nested = {}
255 # By sorting, parent directory will loop before subdirectories.
256 for project_path in sorted(projects):
257 components = project_path.split(os.sep)
258 for i in range(len(components) - 1, 0, -1):
259 head = os.sep.join(components[:i])
260 tail = os.sep.join(components[i:])
261 if head in nested:
262 nested[head].append(tail)
263 break
264 nested[project_path] = []
265
Kuang-che Wub9bc49a2021-01-27 16:27:51 +0800266 with multiprocessing.Pool() as pool:
267 cleanup_jobs = []
268 for project_path in projects:
269 git_repo = os.path.join(repo_dir, project_path)
270 if not os.path.exists(git_repo):
271 # It should be harmless to ignore git repo nonexistence because 'repo
272 # sync' will restore them.
273 logger.warning('git repo not found: %s', git_repo)
274 continue
275 cleanup_jobs.append((git_repo, nested[project_path]))
276 pool.starmap(git_util.distclean, cleanup_jobs)
Kuang-che Wu3d04eda2019-09-05 23:56:40 +0800277
278
Kuang-che Wubfa64482018-10-16 11:49:49 +0800279def _urljoin(base, url):
280 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
281 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
282 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
283 dummy_scheme = 'gopher://'
284 new_scheme = 'persistent-https://'
285 assert not base.startswith(dummy_scheme)
286 assert not url.startswith(dummy_scheme)
287 base = re.sub('^' + new_scheme, dummy_scheme, base)
288 url = re.sub('^' + new_scheme, dummy_scheme, url)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800289 result = urllib.parse.urljoin(base, url)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800290 result = re.sub('^' + dummy_scheme, new_scheme, result)
291 return result
292
293
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800294class ManifestParser:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800295 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800296
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800297 def __init__(self, manifest_dir, load_remote=True):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800298 self.manifest_dir = manifest_dir
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800299 if load_remote:
300 self.manifest_url = get_manifest_url(self.manifest_dir)
301 else:
302 self.manifest_url = None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800303
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800304 def parse_single_xml(self, content, allow_include=False):
305 root = xml.etree.ElementTree.fromstring(content)
306 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800307 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800308 'Expects self-contained manifest. <include> is not allowed')
309 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800310
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800311 def parse_xml_recursive(self, git_rev, path):
312 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800313 root = xml.etree.ElementTree.fromstring(content)
314 default = None
315 notice = None
316 remotes = {}
317 manifest_server = None
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800318 result = xml.etree.ElementTree.Element('manifest')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800319
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800320 for node in root:
321 if node.tag == 'include':
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800322 nodes = self.parse_xml_recursive(git_rev, node.get('name'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800323 else:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800324 nodes = [node]
325
326 for subnode in nodes:
327 if subnode.tag == 'default':
328 if default is not None and not self.element_equal(default, subnode):
329 raise errors.ExternalError('duplicated <default> %s and %s' %
330 (self.element_to_string(default),
331 self.element_to_string(subnode)))
332 if default is None:
333 default = subnode
334 result.append(subnode)
335 elif subnode.tag == 'remote':
336 name = subnode.get('name')
337 if name in remotes and not self.element_equal(remotes[name], subnode):
338 raise errors.ExternalError('duplicated <remote> %s and %s' %
339 (self.element_to_string(default),
340 self.element_to_string(subnode)))
341 if name not in remotes:
342 remotes[name] = subnode
343 result.append(subnode)
344 elif subnode.tag == 'notice':
345 if notice is not None and not self.element_equal(notice, subnode):
346 raise errors.ExternalError('duplicated <notice>')
347 if notice is None:
348 notice = subnode
349 result.append(subnode)
350 elif subnode.tag == 'manifest-server':
351 if manifest_server is not None:
352 raise errors.ExternalError('duplicated <manifest-server>')
353 manifest_server = subnode
354 result.append(subnode)
355 else:
356 result.append(subnode)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800357 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800358
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800359 @classmethod
360 def element_to_string(cls, element):
361 return xml.etree.ElementTree.tostring(element, encoding='unicode').strip()
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800362
363 @classmethod
364 def get_project_path(cls, project):
365 path = project.get('path')
366 # default path is its name
367 if not path:
368 path = project.get('name')
Kuang-che Wu4521ad62020-10-26 10:50:18 +0800369 # Follow repo's behavior to strip trailing slash (crbug/1086043).
370 return path.rstrip('/')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800371
372 @classmethod
373 def get_project_revision(cls, project, default):
374 if default is None:
375 default = {}
376 return project.get('revision', default.get('revision'))
377
378 def element_equal(self, element1, element2):
379 """Return if two xml elements are same
380
381 Args:
382 element1: An xml element
383 element2: An xml element
384 """
385 if element1.tag != element2.tag:
386 return False
387 if element1.text != element2.text:
388 return False
389 if element1.attrib != element2.attrib:
390 return False
391 if len(element1) != len(element2):
392 return False
393 return all(
394 self.element_equal(node1, node2)
395 for node1, node2 in zip(element1, element2))
396
Kuang-che Wudedc5922020-12-17 17:19:23 +0800397 def process_parsed_result(self, root, group_constraint='default'):
398 if group_constraint not in ('default', 'all'):
399 raise ValueError('only "default" and "all" are supported')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800400 result = {}
401 default = root.find('default')
402 if default is None:
403 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800404
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800405 remote_fetch_map = {}
406 for remote in root.findall('.//remote'):
407 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800408 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800409 if urllib.parse.urlparse(fetch_url).path not in ('', '/'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800410 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800411 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800412 'only support git repo at root path of remote server: %s' %
413 fetch_url)
414 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800415
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800416 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800417
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800418 for project in root.findall('.//project'):
Kuang-che Wudedc5922020-12-17 17:19:23 +0800419 if group_constraint == 'default':
420 if 'notdefault' in project.get('groups', ''):
421 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800422 for subproject in project.findall('.//project'):
423 logger.warning('nested project %s.%s is not supported and ignored',
424 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800425
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800426 path = self.get_project_path(project)
427 revision = self.get_project_revision(project, default)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800428
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800429 remote_name = project.get('remote', default.get('remote'))
430 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800431 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800432 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wu4521ad62020-10-26 10:50:18 +0800433 # Follow repo's behavior to strip trailing slash (crbug/1086043).
434 name = project.get('name').rstrip('/')
435 repo_url = _urljoin(fetch_url, name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800436
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800437 result[path] = codechange.PathSpec(path, repo_url, revision)
438 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800439
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800440 def enumerate_manifest_commits(self, start_time, end_time, path, branch=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800441
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800442 def parse_dependencies(path, content):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800443 try:
444 root = self.parse_single_xml(content, allow_include=True)
445 except xml.etree.ElementTree.ParseError:
446 logger.warning('%s syntax error, skip', path)
447 return None
448
449 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800450 for include in root.findall('.//include'):
Kuang-che Wu7d0c7592019-09-16 09:59:28 +0800451 result.append(include.get('name'))
452 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800453
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800454 return git_util.get_history_recursively(
455 self.manifest_dir,
456 path,
457 start_time,
458 end_time,
459 parse_dependencies,
460 branch=branch)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800461
462
463class RepoMirror(codechange.CodeStorage):
464 """Repo git mirror."""
465
466 def __init__(self, mirror_dir):
467 self.mirror_dir = mirror_dir
468
469 def _url_to_cache_dir(self, url):
470 # Here we assume remote fetch url is always at root of server url, so we can
471 # simply treat whole path as repo project name.
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800472 path = urllib.parse.urlparse(url).path
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800473 assert path[0] == '/'
474 return '%s.git' % path[1:]
475
476 def cached_git_root(self, repo_url):
477 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800478
479 # The location of chromeos manifest-internal repo mirror is irregular
480 # (http://crbug.com/895957). This is a workaround.
481 if cache_path == 'chromeos/manifest-internal.git':
482 cache_path = 'manifest-internal.git'
483
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800484 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800485
486 def _load_project_list(self, project_root):
487 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
Kuang-che Wua5723492019-11-25 20:59:34 +0800488 with open(repo_project_list) as f:
489 return f.readlines()
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800490
491 def _save_project_list(self, project_root, lines):
492 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
493 with open(repo_project_list, 'w') as f:
494 f.write(''.join(sorted(lines)))
495
496 def add_to_project_list(self, project_root, path, repo_url):
497 lines = self._load_project_list(project_root)
498
499 line = path + '\n'
500 if line not in lines:
501 lines.append(line)
502
503 self._save_project_list(project_root, lines)
504
505 def remove_from_project_list(self, project_root, path):
506 lines = self._load_project_list(project_root)
507
508 line = path + '\n'
509 if line in lines:
510 lines.remove(line)
511
512 self._save_project_list(project_root, lines)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800513
514
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800515class Manifest:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800516 """This class handles a manifest and is able to patch projects."""
517
518 def __init__(self, manifest_internal_dir):
519 self.xml = None
520 self.manifest_internal_dir = manifest_internal_dir
521 self.modified = set()
522 self.parser = ManifestParser(manifest_internal_dir)
523
524 def load_from_string(self, xml_string):
525 """Load manifest xml from a string.
526
527 Args:
528 xml_string: An xml string.
529 """
530 self.xml = xml.etree.ElementTree.fromstring(xml_string)
531
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800532 def load_from_commit(self, commit):
533 """Load manifest xml snapshot by a commit hash.
534
535 Args:
536 commit: A manifest-internal commit hash.
537 """
538 self.xml = self.parser.parse_xml_recursive(commit, 'default.xml')
539
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800540 def load_from_timestamp(self, timestamp):
541 """Load manifest xml snapshot by a timestamp.
542
543 The function will load a latest manifest before or equal to the timestamp.
544
545 Args:
546 timestamp: A unix timestamp.
547 """
548 commits = git_util.get_history(
549 self.manifest_internal_dir, before=timestamp + 1)
550 commit = commits[-1][1]
Zheng-Jie Chang34812742020-04-07 19:05:46 +0800551 self.load_from_commit(commit)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800552
553 def to_string(self):
554 """Dump current xml to a string.
555
556 Returns:
557 A string of xml.
558 """
Zheng-Jie Chang978f90e2020-03-17 16:09:01 +0800559 return ManifestParser.element_to_string(self.xml)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800560
561 def is_static_manifest(self):
562 """Return true if there is any project without revision in the xml.
563
564 Returns:
565 A boolean, True if every project has a revision.
566 """
567 count = 0
568 for project in self.xml.findall('.//project'):
569 # check argument directly instead of getting value from default tag
570 if not project.get('revision'):
571 count += 1
572 path = self.parser.get_project_path(project)
Kuang-che Wuc82c6492020-01-08 18:01:01 +0800573 logger.warning('path: %s has no revision', path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800574 return count == 0
575
576 def remove_project_revision(self):
577 """Remove revision argument from all projects"""
578 for project in self.xml.findall('.//project'):
579 if 'revision' in project:
580 del project['revision']
581
582 def count_path(self, path):
583 """Count projects that path is given path.
584
585 Returns:
586 An integer, indicates the number of projects.
587 """
588 result = 0
589 for project in self.xml.findall('.//project'):
590 if project.get('path') == path:
591 result += 1
592 return result
593
594 def apply_commit(self, path, revision, overwrite=True):
595 """Set revision to a project by path.
596
597 Args:
598 path: A project's path.
599 revision: A git commit id.
600 overwrite: Overwrite flag, the project won't change if overwrite=False
601 and it was modified before.
602 """
603 if path in self.modified and not overwrite:
604 return
605 self.modified.add(path)
606
607 count = 0
608 for project in self.xml.findall('.//project'):
609 if self.parser.get_project_path(project) == path:
610 count += 1
611 project.set('revision', revision)
612
613 if count != 1:
614 logger.warning('found %d path: %s in manifest', count, path)
615
616 def apply_upstream(self, path, upstream):
617 """Set upstream to a project by path.
618
619 Args:
620 path: A project's path.
621 upstream: A git upstream.
622 """
623 for project in self.xml.findall('.//project'):
624 if self.parser.get_project_path(project) == path:
625 project.set('upstream', upstream)
626
627 def apply_action_groups(self, action_groups):
628 """Apply multiple action groups to xml.
629
630 If there are multiple actions in one repo, only last one is applied.
631
632 Args:
633 action_groups: A list of action groups.
634 """
635 # Apply in reversed order with overwrite=False,
636 # so each repo is on the state of last action.
637 for action_group in reversed(action_groups):
638 for action in reversed(action_group.actions):
639 if isinstance(action, codechange.GitCheckoutCommit):
640 self.apply_commit(action.path, action.rev, overwrite=False)
641 if isinstance(action, codechange.GitAddRepo):
642 self.apply_commit(action.path, action.rev, overwrite=False)
643 if isinstance(action, codechange.GitRemoveRepo):
644 assert self.count_path(action.path) == 0
645 self.modified.add(action.path)
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +0800646
647 def apply_manifest(self, manifest):
648 """Apply another manifest to current xml.
649
650 By default, all the projects in manifest will be applied and won't
651 overwrite modified projects.
652
653 Args:
654 manifest: A Manifest object.
655 """
656 default = manifest.xml.get('default')
657 for project in manifest.xml.findall('.//project'):
658 path = self.parser.get_project_path(project)
659 revision = self.parser.get_project_revision(project, default)
660 if path and revision:
661 self.apply_commit(path, revision, overwrite=False)
662 upstream = project.get('upstream')
663 if upstream:
664 self.apply_upstream(path, upstream)