blob: ca18ee139c83107b84abb136676db6182baa4838 [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
94def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
95 """Repo sync.
96
97 Args:
98 repo_dir: root directory of repo
99 jobs: projects to fetch simultaneously
100 manifest_name: filename of manifest
101 current_branch: fetch only current branch
102 """
103 cmd = ['repo', 'sync', '-q', '--force-sync']
104 if jobs:
105 cmd += ['-j', str(jobs)]
106 if manifest_name:
107 cmd += ['--manifest-name', manifest_name]
108 if current_branch:
109 cmd += ['--current-branch']
110 util.check_call(*cmd, cwd=repo_dir)
111
112
113def abandon(repo_dir, branch_name):
114 """Repo abandon.
115
116 Args:
117 repo_dir: root directory of repo
118 branch_name: branch name to abandon
119 """
120 # Ignore errors if failed, which means the branch didn't exist beforehand.
121 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
122
123
124def info(repo_dir, query):
125 """Repo info.
126
127 Args:
128 repo_dir: root directory of repo
129 query: key to query
130 """
131 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
132 key, value = map(str.strip, line.split(':'))
133 if key == query:
134 return value
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800135
136 return None
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800137
138
139def get_current_branch(repo_dir):
140 """Get manifest branch of existing repo directory."""
141 return info(repo_dir, 'Manifest branch')
142
143
144def get_manifest_groups(repo_dir):
145 """Get manifest group of existing repo directory."""
146 return info(repo_dir, 'Manifest groups')
147
148
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800149class ManifestParser(object):
150 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800151
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800152 def __init__(self, manifest_dir):
153 self.manifest_dir = manifest_dir
154 self.manifest_url = get_manifest_url(self.manifest_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800155
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800156 def parse_single_xml(self, content, allow_include=False):
157 root = xml.etree.ElementTree.fromstring(content)
158 if not allow_include and root.find('include') is not None:
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800159 raise ValueError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800160 'Expects self-contained manifest. <include> is not allowed')
161 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800162
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800163 def parse_xml_recursive(self, git_rev, path):
164 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
165 root = self.parse_single_xml(content, allow_include=True)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800166
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800167 result = xml.etree.ElementTree.Element('manifest')
168 for node in root:
169 if node.tag == 'include':
170 for subnode in self.parse_xml_recursive(git_rev, node.get('name')):
171 result.append(subnode)
172 else:
173 result.append(node)
174 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800175
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800176 def process_parsed_result(self, root):
177 result = {}
178 default = root.find('default')
179 if default is None:
180 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800181
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800182 remote_fetch_map = {}
183 for remote in root.findall('.//remote'):
184 name = remote.get('name')
185 fetch_url = urlparse.urljoin(self.manifest_url, remote.get('fetch'))
186 if urlparse.urlparse(fetch_url).path not in ('', '/'):
187 # TODO(kcwu): support remote url with sub folders
188 raise ValueError(
189 'only support git repo at root path of remote server: %s' %
190 fetch_url)
191 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800192
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800193 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800194
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800195 for project in root.findall('.//project'):
196 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800197 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800198 for subproject in project.findall('.//project'):
199 logger.warning('nested project %s.%s is not supported and ignored',
200 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800201
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800202 # default path is its name
203 path = project.get('path', project.get('name'))
204 revision = project.get('revision', default.get('revision'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800205
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800206 remote_name = project.get('remote', default.get('remote'))
207 if remote_name not in remote_fetch_map:
208 raise ValueError('unknown remote name=%s' % remote_name)
209 fetch_url = remote_fetch_map.get(remote_name)
210 repo_url = urlparse.urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800211
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800212 result[path] = codechange.PathSpec(path, repo_url, revision)
213 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800214
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800215 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800216
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800217 def parse_dependencies(path, content):
218 del path # unused
219 root = self.parse_single_xml(content, allow_include=True)
220 for include in root.findall('.//include'):
221 yield include.get('name')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800222
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800223 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
224 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800225
226
227class RepoMirror(codechange.CodeStorage):
228 """Repo git mirror."""
229
230 def __init__(self, mirror_dir):
231 self.mirror_dir = mirror_dir
232
233 def _url_to_cache_dir(self, url):
234 # Here we assume remote fetch url is always at root of server url, so we can
235 # simply treat whole path as repo project name.
236 path = urlparse.urlparse(url).path
237 assert path[0] == '/'
238 return '%s.git' % path[1:]
239
240 def cached_git_root(self, repo_url):
241 cache_path = self._url_to_cache_dir(repo_url)
242 return os.path.join(self.mirror_dir, cache_path)
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800243
244 def _load_project_list(self, project_root):
245 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
246 return open(repo_project_list).readlines()
247
248 def _save_project_list(self, project_root, lines):
249 repo_project_list = os.path.join(project_root, '.repo', 'project.list')
250 with open(repo_project_list, 'w') as f:
251 f.write(''.join(sorted(lines)))
252
253 def add_to_project_list(self, project_root, path, repo_url):
254 lines = self._load_project_list(project_root)
255
256 line = path + '\n'
257 if line not in lines:
258 lines.append(line)
259
260 self._save_project_list(project_root, lines)
261
262 def remove_from_project_list(self, project_root, path):
263 lines = self._load_project_list(project_root)
264
265 line = path + '\n'
266 if line in lines:
267 lines.remove(line)
268
269 self._save_project_list(project_root, lines)