blob: 81a11e9bb2af900ec1c9b916ee8641fd40439285 [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')
102 parser = ManifestParser(manifest_dir)
103 manifest = parser.parse_xml_recursive('HEAD', manifest_name)
104
105 for copyfile in manifest.findall('.//copyfile'):
106 dest = copyfile.get('dest')
107 if not dest:
108 continue
109 # `dest` is relative to the top of the tree
110 dest_path = os.path.join(repo_dir, dest)
111 if not os.path.isfile(dest_path):
112 continue
113 logger.debug('delete file %r', dest_path)
114 os.unlink(dest_path)
115
116 for linkfile in manifest.findall('.//linkfile'):
117 dest = linkfile.get('dest')
118 if not dest:
119 continue
120 # `dest` is relative to the top of the tree
121 dest_path = os.path.join(repo_dir, dest)
122 if not os.path.islink(dest_path):
123 continue
124 logger.debug('delete link %r', dest_path)
125 os.unlink(dest_path)
126
127
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800128def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
129 """Repo sync.
130
131 Args:
132 repo_dir: root directory of repo
133 jobs: projects to fetch simultaneously
134 manifest_name: filename of manifest
135 current_branch: fetch only current branch
136 """
Kuang-che Wuea3abce2018-10-04 17:50:42 +0800137 # Workaround to prevent garbage files left between repo syncs
138 # (http://crbug.com/881783).
139 cleanup_repo_generated_files(repo_dir)
140
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800141 cmd = ['repo', 'sync', '-q', '--force-sync']
142 if jobs:
143 cmd += ['-j', str(jobs)]
144 if manifest_name:
145 cmd += ['--manifest-name', manifest_name]
146 if current_branch:
147 cmd += ['--current-branch']
148 util.check_call(*cmd, cwd=repo_dir)
149
150
151def abandon(repo_dir, branch_name):
152 """Repo abandon.
153
154 Args:
155 repo_dir: root directory of repo
156 branch_name: branch name to abandon
157 """
158 # Ignore errors if failed, which means the branch didn't exist beforehand.
159 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
160
161
162def info(repo_dir, query):
163 """Repo info.
164
165 Args:
166 repo_dir: root directory of repo
167 query: key to query
168 """
169 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
170 key, value = map(str.strip, line.split(':'))
171 if key == query:
172 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800173
174 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800175
176
177def get_current_branch(repo_dir):
178 """Get manifest branch of existing repo directory."""
179 return info(repo_dir, 'Manifest branch')
180
181
182def get_manifest_groups(repo_dir):
183 """Get manifest group of existing repo directory."""
184 return info(repo_dir, 'Manifest groups')
185
186
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800187class ManifestParser(object):
188 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800189
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800190 def __init__(self, manifest_dir):
191 self.manifest_dir = manifest_dir
192 self.manifest_url = get_manifest_url(self.manifest_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800193
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800194 def parse_single_xml(self, content, allow_include=False):
195 root = xml.etree.ElementTree.fromstring(content)
196 if not allow_include and root.find('include') is not None:
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800197 raise ValueError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800198 'Expects self-contained manifest. <include> is not allowed')
199 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800200
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800201 def parse_xml_recursive(self, git_rev, path):
202 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
203 root = self.parse_single_xml(content, allow_include=True)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800204
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800205 result = xml.etree.ElementTree.Element('manifest')
206 for node in root:
207 if node.tag == 'include':
208 for subnode in self.parse_xml_recursive(git_rev, node.get('name')):
209 result.append(subnode)
210 else:
211 result.append(node)
212 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800213
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800214 def process_parsed_result(self, root):
215 result = {}
216 default = root.find('default')
217 if default is None:
218 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800219
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800220 remote_fetch_map = {}
221 for remote in root.findall('.//remote'):
222 name = remote.get('name')
223 fetch_url = urlparse.urljoin(self.manifest_url, remote.get('fetch'))
224 if urlparse.urlparse(fetch_url).path not in ('', '/'):
225 # TODO(kcwu): support remote url with sub folders
226 raise ValueError(
227 'only support git repo at root path of remote server: %s' %
228 fetch_url)
229 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800230
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800231 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800232
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800233 for project in root.findall('.//project'):
234 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800235 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800236 for subproject in project.findall('.//project'):
237 logger.warning('nested project %s.%s is not supported and ignored',
238 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800239
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800240 # default path is its name
241 path = project.get('path', project.get('name'))
242 revision = project.get('revision', default.get('revision'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800243
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800244 remote_name = project.get('remote', default.get('remote'))
245 if remote_name not in remote_fetch_map:
246 raise ValueError('unknown remote name=%s' % remote_name)
247 fetch_url = remote_fetch_map.get(remote_name)
248 repo_url = urlparse.urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800249
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800250 result[path] = codechange.PathSpec(path, repo_url, revision)
251 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800252
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800253 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800254
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800255 def parse_dependencies(path, content):
256 del path # unused
257 root = self.parse_single_xml(content, allow_include=True)
258 for include in root.findall('.//include'):
259 yield include.get('name')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800260
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800261 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
262 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800263
264
265class RepoMirror(codechange.CodeStorage):
266 """Repo git mirror."""
267
268 def __init__(self, mirror_dir):
269 self.mirror_dir = mirror_dir
270
271 def _url_to_cache_dir(self, url):
272 # Here we assume remote fetch url is always at root of server url, so we can
273 # simply treat whole path as repo project name.
274 path = urlparse.urlparse(url).path
275 assert path[0] == '/'
276 return '%s.git' % path[1:]
277
278 def cached_git_root(self, repo_url):
279 cache_path = self._url_to_cache_dir(repo_url)
280 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800281
282 def _load_project_list(self, project_root):
283 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
284 return open(repo_project_list).readlines()
285
286 def _save_project_list(self, project_root, lines):
287 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
288 with open(repo_project_list, 'w') as f:
289 f.write(''.join(sorted(lines)))
290
291 def add_to_project_list(self, project_root, path, repo_url):
292 lines = self._load_project_list(project_root)
293
294 line = path + '\n'
295 if line not in lines:
296 lines.append(line)
297
298 self._save_project_list(project_root, lines)
299
300 def remove_from_project_list(self, project_root, path):
301 lines = self._load_project_list(project_root)
302
303 line = path + '\n'
304 if line in lines:
305 lines.remove(line)
306
307 self._save_project_list(project_root, lines)