blob: cfb16c8c51ec4e8383baca21034a8f9e88011123 [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)
36 url = re.sub(r'^persistent-(https?://)', r'\1', url)
37 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:
77 raise Exception(
78 '%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 Wue4bae0b2018-07-19 12:10:14 +0800190class ManifestParser(object):
191 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800192
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800193 def __init__(self, manifest_dir):
194 self.manifest_dir = manifest_dir
195 self.manifest_url = get_manifest_url(self.manifest_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800196
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800197 def parse_single_xml(self, content, allow_include=False):
198 root = xml.etree.ElementTree.fromstring(content)
199 if not allow_include and root.find('include') is not None:
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800200 raise ValueError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800201 'Expects self-contained manifest. <include> is not allowed')
202 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800203
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800204 def parse_xml_recursive(self, git_rev, path):
205 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
206 root = self.parse_single_xml(content, allow_include=True)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800207
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800208 result = xml.etree.ElementTree.Element('manifest')
209 for node in root:
210 if node.tag == 'include':
211 for subnode in self.parse_xml_recursive(git_rev, node.get('name')):
212 result.append(subnode)
213 else:
214 result.append(node)
215 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800216
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800217 def process_parsed_result(self, root):
218 result = {}
219 default = root.find('default')
220 if default is None:
221 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800222
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800223 remote_fetch_map = {}
224 for remote in root.findall('.//remote'):
225 name = remote.get('name')
226 fetch_url = urlparse.urljoin(self.manifest_url, remote.get('fetch'))
227 if urlparse.urlparse(fetch_url).path not in ('', '/'):
228 # TODO(kcwu): support remote url with sub folders
229 raise ValueError(
230 'only support git repo at root path of remote server: %s' %
231 fetch_url)
232 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800233
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800234 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800235
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800236 for project in root.findall('.//project'):
237 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800238 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800239 for subproject in project.findall('.//project'):
240 logger.warning('nested project %s.%s is not supported and ignored',
241 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800242
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800243 # default path is its name
244 path = project.get('path', project.get('name'))
245 revision = project.get('revision', default.get('revision'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800246
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800247 remote_name = project.get('remote', default.get('remote'))
248 if remote_name not in remote_fetch_map:
249 raise ValueError('unknown remote name=%s' % remote_name)
250 fetch_url = remote_fetch_map.get(remote_name)
251 repo_url = urlparse.urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800252
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800253 result[path] = codechange.PathSpec(path, repo_url, revision)
254 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800255
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800256 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800257
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800258 def parse_dependencies(path, content):
259 del path # unused
260 root = self.parse_single_xml(content, allow_include=True)
261 for include in root.findall('.//include'):
262 yield include.get('name')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800263
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800264 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
265 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800266
267
268class RepoMirror(codechange.CodeStorage):
269 """Repo git mirror."""
270
271 def __init__(self, mirror_dir):
272 self.mirror_dir = mirror_dir
273
274 def _url_to_cache_dir(self, url):
275 # Here we assume remote fetch url is always at root of server url, so we can
276 # simply treat whole path as repo project name.
277 path = urlparse.urlparse(url).path
278 assert path[0] == '/'
279 return '%s.git' % path[1:]
280
281 def cached_git_root(self, repo_url):
282 cache_path = self._url_to_cache_dir(repo_url)
283 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800284
285 def _load_project_list(self, project_root):
286 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
287 return open(repo_project_list).readlines()
288
289 def _save_project_list(self, project_root, lines):
290 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
291 with open(repo_project_list, 'w') as f:
292 f.write(''.join(sorted(lines)))
293
294 def add_to_project_list(self, project_root, path, repo_url):
295 lines = self._load_project_list(project_root)
296
297 line = path + '\n'
298 if line not in lines:
299 lines.append(line)
300
301 self._save_project_list(project_root, lines)
302
303 def remove_from_project_list(self, project_root, path):
304 lines = self._load_project_list(project_root)
305
306 line = path + '\n'
307 if line in lines:
308 lines.remove(line)
309
310 self._save_project_list(project_root, lines)