blob: 3ae07af137de15077953ccdd9d48c381d253e817 [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 Wubfc4a642018-04-19 11:54:08 +080019from bisect_kit import git_util
20from bisect_kit import util
21
22logger = logging.getLogger(__name__)
23
24
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080025def get_manifest_url(manifest_dir):
Kuang-che Wud1d45b42018-07-05 00:46:45 +080026 """Get manifest URL of repo project.
27
28 Args:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080029 manifest_dir: path of manifest directory
Kuang-che Wud1d45b42018-07-05 00:46:45 +080030
31 Returns:
32 manifest URL.
33 """
Kuang-che Wud1d45b42018-07-05 00:46:45 +080034 url = util.check_output(
35 'git', 'config', 'remote.origin.url', cwd=manifest_dir)
Kuang-che Wud1d45b42018-07-05 00:46:45 +080036 return url
37
38
Kuang-che Wu41e8b592018-09-25 17:01:30 +080039def find_repo_root(path):
40 """Find the root path of a repo project
41
42 Args:
43 path: path
44
45 Returns:
46 project root if path is inside a repo project; otherwise None
47 """
48 path = os.path.abspath(path)
49 while not os.path.exists(os.path.join(path, '.repo')):
50 if path == '/':
51 return None
52 path = os.path.dirname(path)
53 return path
54
55
Kuang-che Wubfc4a642018-04-19 11:54:08 +080056def init(repo_dir,
57 manifest_url,
58 manifest_branch=None,
59 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080060 repo_url=None,
Kuang-che Wu41e8b592018-09-25 17:01:30 +080061 reference=None,
62 mirror=False):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080063 """Repo init.
64
65 Args:
66 repo_dir: root directory of repo
67 manifest_url: manifest repository location
68 manifest_branch: manifest branch or revision
69 manifest_name: initial manifest file name
70 repo_url: repo repository location
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080071 reference: location of mirror directory
Kuang-che Wu41e8b592018-09-25 17:01:30 +080072 mirror: indicates repo mirror
Kuang-che Wubfc4a642018-04-19 11:54:08 +080073 """
Kuang-che Wu41e8b592018-09-25 17:01:30 +080074 root = find_repo_root(repo_dir)
75 if root and root != repo_dir:
76 raise Exception(
77 '%s should not be inside another repo project at %s' % (repo_dir, root))
78
Kuang-che Wubfc4a642018-04-19 11:54:08 +080079 cmd = ['repo', 'init', '--manifest-url', manifest_url]
80 if manifest_name:
81 cmd += ['--manifest-name', manifest_name]
82 if manifest_branch:
83 cmd += ['--manifest-branch', manifest_branch]
84 if repo_url:
85 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080086 if reference:
87 cmd += ['--reference', reference]
Kuang-che Wu41e8b592018-09-25 17:01:30 +080088 if mirror:
89 cmd.append('--mirror')
Kuang-che Wubfc4a642018-04-19 11:54:08 +080090 util.check_call(*cmd, cwd=repo_dir)
91
92
Kuang-che Wuea3abce2018-10-04 17:50:42 +080093def cleanup_repo_generated_files(repo_dir, manifest_name='default.xml'):
94 """Cleanup files generated by <copyfile> <linkfile> tags.
95
96 Args:
97 repo_dir: root directory of repo
98 manifest_name: filename of manifest
99 """
100 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu35080a72018-10-05 14:14:33 +0800101 manifest_path = os.path.join(manifest_dir, manifest_name)
102 if os.path.islink(manifest_path):
103 manifest_name = os.readlink(manifest_path)
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800104 parser = ManifestParser(manifest_dir)
105 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
106
107 for copyfile in manifest.findall('.//copyfile'):
108 dest = copyfile.get('dest')
109 if not dest:
110 continue
111 # `dest` is relative to the top of the tree
112 dest_path = os.path.join(repo_dir, dest)
113 if not os.path.isfile(dest_path):
114 continue
115 logger.debug('delete file %r', dest_path)
116 os.unlink(dest_path)
117
118 for linkfile in manifest.findall('.//linkfile'):
119 dest = linkfile.get('dest')
120 if not dest:
121 continue
122 # `dest` is relative to the top of the tree
123 dest_path = os.path.join(repo_dir, dest)
124 if not os.path.islink(dest_path):
125 continue
126 logger.debug('delete link %r', dest_path)
127 os.unlink(dest_path)
128
129
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800130def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
131 """Repo sync.
132
133 Args:
134 repo_dir: root directory of repo
135 jobs: projects to fetch simultaneously
136 manifest_name: filename of manifest
137 current_branch: fetch only current branch
138 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800139 # Workaround to prevent garbage files left between repo syncs
140 # (http://crbug.com/881783).
141 cleanup_repo_generated_files(repo_dir)
142
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800143 cmd = ['repo', 'sync', '-q', '--force-sync']
144 if jobs:
145 cmd += ['-j', str(jobs)]
146 if manifest_name:
147 cmd += ['--manifest-name', manifest_name]
148 if current_branch:
149 cmd += ['--current-branch']
150 util.check_call(*cmd, cwd=repo_dir)
151
152
153def abandon(repo_dir, branch_name):
154 """Repo abandon.
155
156 Args:
157 repo_dir: root directory of repo
158 branch_name: branch name to abandon
159 """
160 # Ignore errors if failed, which means the branch didn't exist beforehand.
161 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
162
163
164def info(repo_dir, query):
165 """Repo info.
166
167 Args:
168 repo_dir: root directory of repo
169 query: key to query
170 """
171 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
172 key, value = map(str.strip, line.split(':'))
173 if key == query:
174 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800175
176 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800177
178
179def get_current_branch(repo_dir):
180 """Get manifest branch of existing repo directory."""
181 return info(repo_dir, 'Manifest branch')
182
183
184def get_manifest_groups(repo_dir):
185 """Get manifest group of existing repo directory."""
186 return info(repo_dir, 'Manifest groups')
187
188
Kuang-che Wubfa64482018-10-16 11:49:49 +0800189def _urljoin(base, url):
190 # urlparse.urljoin doesn't recognize "persistent-https://" protocol.
191 # Following hack replaces "persistent-https" by obsolete protocol "gopher"
192 # before urlparse.urljoin and replaces back after urlparse.urljoin calls.
193 dummy_scheme = 'gopher://'
194 new_scheme = 'persistent-https://'
195 assert not base.startswith(dummy_scheme)
196 assert not url.startswith(dummy_scheme)
197 base = re.sub('^' + new_scheme, dummy_scheme, base)
198 url = re.sub('^' + new_scheme, dummy_scheme, url)
199 result = urlparse.urljoin(base, url)
200 result = re.sub('^' + dummy_scheme, new_scheme, result)
201 return result
202
203
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800204class ManifestParser(object):
205 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800206
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800207 def __init__(self, manifest_dir):
208 self.manifest_dir = manifest_dir
209 self.manifest_url = get_manifest_url(self.manifest_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800210
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800211 def parse_single_xml(self, content, allow_include=False):
212 root = xml.etree.ElementTree.fromstring(content)
213 if not allow_include and root.find('include') is not None:
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800214 raise ValueError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800215 'Expects self-contained manifest. <include> is not allowed')
216 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800217
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800218 def parse_xml_recursive(self, git_rev, path):
219 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
220 root = self.parse_single_xml(content, allow_include=True)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800221
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800222 result = xml.etree.ElementTree.Element('manifest')
223 for node in root:
224 if node.tag == 'include':
225 for subnode in self.parse_xml_recursive(git_rev, node.get('name')):
226 result.append(subnode)
227 else:
228 result.append(node)
229 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800230
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800231 def process_parsed_result(self, root):
232 result = {}
233 default = root.find('default')
234 if default is None:
235 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800236
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800237 remote_fetch_map = {}
238 for remote in root.findall('.//remote'):
239 name = remote.get('name')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800240 fetch_url = _urljoin(self.manifest_url, remote.get('fetch'))
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800241 if urlparse.urlparse(fetch_url).path not in ('', '/'):
242 # TODO(kcwu): support remote url with sub folders
243 raise ValueError(
244 'only support git repo at root path of remote server: %s' %
245 fetch_url)
246 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800247
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800248 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800249
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800250 for project in root.findall('.//project'):
251 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800252 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800253 for subproject in project.findall('.//project'):
254 logger.warning('nested project %s.%s is not supported and ignored',
255 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800256
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800257 # default path is its name
258 path = project.get('path', project.get('name'))
259 revision = project.get('revision', default.get('revision'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800260
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800261 remote_name = project.get('remote', default.get('remote'))
262 if remote_name not in remote_fetch_map:
263 raise ValueError('unknown remote name=%s' % remote_name)
264 fetch_url = remote_fetch_map.get(remote_name)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800265 repo_url = _urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800266
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800267 result[path] = codechange.PathSpec(path, repo_url, revision)
268 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800269
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800270 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800271
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800272 def parse_dependencies(path, content):
273 del path # unused
274 root = self.parse_single_xml(content, allow_include=True)
275 for include in root.findall('.//include'):
276 yield include.get('name')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800277
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800278 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
279 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800280
281
282class RepoMirror(codechange.CodeStorage):
283 """Repo git mirror."""
284
285 def __init__(self, mirror_dir):
286 self.mirror_dir = mirror_dir
287
288 def _url_to_cache_dir(self, url):
289 # Here we assume remote fetch url is always at root of server url, so we can
290 # simply treat whole path as repo project name.
291 path = urlparse.urlparse(url).path
292 assert path[0] == '/'
293 return '%s.git' % path[1:]
294
295 def cached_git_root(self, repo_url):
296 cache_path = self._url_to_cache_dir(repo_url)
Kuang-che Wua4f14d62018-10-15 15:59:47 +0800297
298 # The location of chromeos manifest-internal repo mirror is irregular
299 # (http://crbug.com/895957). This is a workaround.
300 if cache_path == 'chromeos/manifest-internal.git':
301 cache_path = 'manifest-internal.git'
302
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800303 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800304
305 def _load_project_list(self, project_root):
306 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
307 return open(repo_project_list).readlines()
308
309 def _save_project_list(self, project_root, lines):
310 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
311 with open(repo_project_list, 'w') as f:
312 f.write(''.join(sorted(lines)))
313
314 def add_to_project_list(self, project_root, path, repo_url):
315 lines = self._load_project_list(project_root)
316
317 line = path + '\n'
318 if line not in lines:
319 lines.append(line)
320
321 self._save_project_list(project_root, lines)
322
323 def remove_from_project_list(self, project_root, path):
324 lines = self._load_project_list(project_root)
325
326 line = path + '\n'
327 if line in lines:
328 lines.remove(line)
329
330 self._save_project_list(project_root, lines)