blob: c923d784a891c6cb22be8a189637dde0eb000bad [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 Wud1d45b42018-07-05 00:46:45 +080015import urlparse
Kuang-che Wubfc4a642018-04-19 11:54:08 +080016import xml.etree.ElementTree
17
Kuang-che Wud1d45b42018-07-05 00:46:45 +080018from bisect_kit import codechange
Kuang-che Wue121fae2018-11-09 16:18:39 +080019from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080020from bisect_kit import git_util
21from bisect_kit import util
22
23logger = logging.getLogger(__name__)
24
25
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080026def get_manifest_url(manifest_dir):
Kuang-che Wud1d45b42018-07-05 00:46:45 +080027 """Get manifest URL of repo project.
28
29 Args:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080030 manifest_dir: path of manifest directory
Kuang-che Wud1d45b42018-07-05 00:46:45 +080031
32 Returns:
33 manifest URL.
34 """
Kuang-che Wud1d45b42018-07-05 00:46:45 +080035 url = util.check_output(
36 'git', 'config', 'remote.origin.url', cwd=manifest_dir)
Kuang-che Wud1d45b42018-07-05 00:46:45 +080037 return url
38
39
Kuang-che Wu41e8b592018-09-25 17:01:30 +080040def find_repo_root(path):
41 """Find the root path of a repo project
42
43 Args:
44 path: path
45
46 Returns:
47 project root if path is inside a repo project; otherwise None
48 """
49 path = os.path.abspath(path)
50 while not os.path.exists(os.path.join(path, '.repo')):
51 if path == '/':
52 return None
53 path = os.path.dirname(path)
54 return path
55
56
Kuang-che Wubfc4a642018-04-19 11:54:08 +080057def init(repo_dir,
58 manifest_url,
59 manifest_branch=None,
60 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080061 repo_url=None,
Kuang-che Wu41e8b592018-09-25 17:01:30 +080062 reference=None,
63 mirror=False):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080064 """Repo init.
65
66 Args:
67 repo_dir: root directory of repo
68 manifest_url: manifest repository location
69 manifest_branch: manifest branch or revision
70 manifest_name: initial manifest file name
71 repo_url: repo repository location
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080072 reference: location of mirror directory
Kuang-che Wu41e8b592018-09-25 17:01:30 +080073 mirror: indicates repo mirror
Kuang-che Wubfc4a642018-04-19 11:54:08 +080074 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080075 root = find_repo_root(repo_dir)
76 if root and root != repo_dir:
Kuang-che Wue121fae2018-11-09 16:18:39 +080077 raise errors.ExternalError(
Kuang-che Wu41e8b592018-09-25 17:01:30 +080078 '%s should not be inside another repo project at %s' % (repo_dir, root))
79
Kuang-che Wubfc4a642018-04-19 11:54:08 +080080 cmd = ['repo', 'init', '--manifest-url', manifest_url]
81 if manifest_name:
82 cmd += ['--manifest-name', manifest_name]
83 if manifest_branch:
84 cmd += ['--manifest-branch', manifest_branch]
85 if repo_url:
86 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080087 if reference:
88 cmd += ['--reference', reference]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080089 if mirror:
90 cmd.append('--mirror')
Kuang-che Wubfc4a642018-04-19 11:54:08 +080091 util.check_call(*cmd, cwd=repo_dir)
92
93
Kuang-che Wuea3abce2018-10-04 17:50:42 +080094def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
95 """Cleanup files generated by <copyfile> <linkfile> tags.
96
97 Args:
98 repo_dir: root directory of repo
99 manifest_name: filename of manifest
100 """
101 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800102 manifest_path = os.path.join(manifest_dir, manifest_name)
103 if os.path.islink(manifest_path):
104 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800105 parser = ManifestParser(manifest_dir)
106 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
107
108 for copyfile in manifest.findall('.//copyfile'):
109 dest = copyfile.get('dest')
110 if not dest:
111 continue
112 # `dest` is relative to the top of the tree
113 dest_path = os.path.join(repo_dir, dest)
114 if not os.path.isfile(dest_path):
115 continue
116 logger.debug('delete file %r', dest_path)
117 os.unlink(dest_path)
118
119 for linkfile in manifest.findall('.//linkfile'):
120 dest = linkfile.get('dest')
121 if not dest:
122 continue
123 # `dest` is relative to the top of the tree
124 dest_path = os.path.join(repo_dir, dest)
125 if not os.path.islink(dest_path):
126 continue
127 logger.debug('delete link %r', dest_path)
128 os.unlink(dest_path)
129
130
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800131def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
132 """Repo sync.
133
134 Args:
135 repo_dir: root directory of repo
136 jobs: projects to fetch simultaneously
137 manifest_name: filename of manifest
138 current_branch: fetch only current branch
139 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800140 # Workaround to prevent garbage files left between repo syncs
141 # (http://crbug.com/881783).
142 cleanup_repo_generated_files(repo_dir)
143
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800144 cmd = ['repo', 'sync', '-q', '--force-sync']
145 if jobs:
146 cmd += ['-j', str(jobs)]
147 if manifest_name:
148 cmd += ['--manifest-name', manifest_name]
149 if current_branch:
150 cmd += ['--current-branch']
151 util.check_call(*cmd, cwd=repo_dir)
152
153
154def abandon(repo_dir, branch_name):
155 """Repo abandon.
156
157 Args:
158 repo_dir: root directory of repo
159 branch_name: branch name to abandon
160 """
161 # Ignore errors if failed, which means the branch didn't exist beforehand.
162 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
163
164
165def info(repo_dir, query):
166 """Repo info.
167
168 Args:
169 repo_dir: root directory of repo
170 query: key to query
171 """
172 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
173 key, value = map(str.strip, line.split(':'))
174 if key == query:
175 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800176
177 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800178
179
180def get_current_branch(repo_dir):
181 """Get manifest branch of existing repo directory."""
182 return info(repo_dir, 'Manifest branch')
183
184
185def get_manifest_groups(repo_dir):
186 """Get manifest group of existing repo directory."""
187 return info(repo_dir, 'Manifest groups')
188
189
Kuang-che Wubfa64482018-10-16 11:49:49 +0800190def _urljoin(base, url):
191 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
192 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
193 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
194 dummy_scheme = 'gopher://'
195 new_scheme = 'persistent-https://'
196 assert not base.startswith(dummy_scheme)
197 assert not url.startswith(dummy_scheme)
198 base = re.sub('^' + new_scheme, dummy_scheme, base)
199 url = re.sub('^' + new_scheme, dummy_scheme, url)
200 result = urlparse.urljoin(base, url)
201 result = re.sub('^' + dummy_scheme, new_scheme, result)
202 return result
203
204
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800205class ManifestParser(object):
206 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800207
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800208 def __init__(self, manifest_dir):
209 self.manifest_dir = manifest_dir
210 self.manifest_url = get_manifest_url(self.manifest_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800211
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800212 def parse_single_xml(self, content, allow_include=False):
213 root = xml.etree.ElementTree.fromstring(content)
214 if not allow_include and root.find('include') is not None:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800215 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800216 'Expects self-contained manifest. <include> is not allowed')
217 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800218
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800219 def parse_xml_recursive(self, git_rev, path):
220 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
221 root = self.parse_single_xml(content, allow_include=True)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800222
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800223 result = xml.etree.ElementTree.Element('manifest')
224 for node in root:
225 if node.tag == 'include':
226 for subnode in self.parse_xml_recursive(git_rev, node.get('name')):
227 result.append(subnode)
228 else:
229 result.append(node)
230 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800231
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800232 def process_parsed_result(self, root):
233 result = {}
234 default = root.find('default')
235 if default is None:
236 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800237
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800238 remote_fetch_map = {}
239 for remote in root.findall('.//remote'):
240 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800241 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800242 if urlparse.urlparse(fetch_url).path not in ('', '/'):
243 # TODO(kcwu): support remote url with sub folders
Kuang-che Wue121fae2018-11-09 16:18:39 +0800244 raise errors.InternalError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800245 'only support git repo at root path of remote server: %s' %
246 fetch_url)
247 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800248
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800249 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800250
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800251 for project in root.findall('.//project'):
252 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800253 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800254 for subproject in project.findall('.//project'):
255 logger.warning('nested project %s.%s is not supported and ignored',
256 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800257
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800258 # default path is its name
259 path = project.get('path', project.get('name'))
260 revision = project.get('revision', default.get('revision'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800261
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800262 remote_name = project.get('remote', default.get('remote'))
263 if remote_name not in remote_fetch_map:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800264 raise errors.InternalError('unknown remote name=%s' % remote_name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800265 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800266 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800267
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800268 result[path] = codechange.PathSpec(path, repo_url, revision)
269 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800270
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800271 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800272
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800273 def parse_dependencies(path, content):
274 del path # unused
275 root = self.parse_single_xml(content, allow_include=True)
276 for include in root.findall('.//include'):
277 yield include.get('name')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800278
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800279 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
280 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800281
282
283class RepoMirror(codechange.CodeStorage):
284 """Repo git mirror."""
285
286 def __init__(self, mirror_dir):
287 self.mirror_dir = mirror_dir
288
289 def _url_to_cache_dir(self, url):
290 # Here we assume remote fetch url is always at root of server url, so we can
291 # simply treat whole path as repo project name.
292 path = urlparse.urlparse(url).path
293 assert path[0] == '/'
294 return '%s.git' % path[1:]
295
296 def cached_git_root(self, repo_url):
297 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800298
299 # The location of chromeos manifest-internal repo mirror is irregular
300 # (http://crbug.com/895957). This is a workaround.
301 if cache_path == 'chromeos/manifest-internal.git':
302 cache_path = 'manifest-internal.git'
303
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800304 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800305
306 def _load_project_list(self, project_root):
307 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
308 return open(repo_project_list).readlines()
309
310 def _save_project_list(self, project_root, lines):
311 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
312 with open(repo_project_list, 'w') as f:
313 f.write(''.join(sorted(lines)))
314
315 def add_to_project_list(self, project_root, path, repo_url):
316 lines = self._load_project_list(project_root)
317
318 line = path + '\n'
319 if line not in lines:
320 lines.append(line)
321
322 self._save_project_list(project_root, lines)
323
324 def remove_from_project_list(self, project_root, path):
325 lines = self._load_project_list(project_root)
326
327 line = path + '\n'
328 if line in lines:
329 lines.remove(line)
330
331 self._save_project_list(project_root, lines)