blob: dc6ab86a2464e6a4192a9de02231b73b0a6365ad [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 Wud1d45b42018-07-05 00:46:45 +080025def get_manifest_url(repo_dir):
26 """Get manifest URL of repo project.
27
28 Args:
29 repo_dir: root directory of repo
30
31 Returns:
32 manifest URL.
33 """
34 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests.git')
35 url = util.check_output(
36 'git', 'config', 'remote.origin.url', cwd=manifest_dir)
37 url = re.sub(r'^persistent-(https?://)', r'\1', url)
38 return url
39
40
Kuang-che Wubfc4a642018-04-19 11:54:08 +080041def init(repo_dir,
42 manifest_url,
43 manifest_branch=None,
44 manifest_name=None,
45 repo_url=None):
46 """Repo init.
47
48 Args:
49 repo_dir: root directory of repo
50 manifest_url: manifest repository location
51 manifest_branch: manifest branch or revision
52 manifest_name: initial manifest file name
53 repo_url: repo repository location
54 """
55 cmd = ['repo', 'init', '--manifest-url', manifest_url]
56 if manifest_name:
57 cmd += ['--manifest-name', manifest_name]
58 if manifest_branch:
59 cmd += ['--manifest-branch', manifest_branch]
60 if repo_url:
61 cmd += ['--repo-url', repo_url]
62 util.check_call(*cmd, cwd=repo_dir)
63
64
65def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
66 """Repo sync.
67
68 Args:
69 repo_dir: root directory of repo
70 jobs: projects to fetch simultaneously
71 manifest_name: filename of manifest
72 current_branch: fetch only current branch
73 """
74 cmd = ['repo', 'sync', '-q', '--force-sync']
75 if jobs:
76 cmd += ['-j', str(jobs)]
77 if manifest_name:
78 cmd += ['--manifest-name', manifest_name]
79 if current_branch:
80 cmd += ['--current-branch']
81 util.check_call(*cmd, cwd=repo_dir)
82
83
84def abandon(repo_dir, branch_name):
85 """Repo abandon.
86
87 Args:
88 repo_dir: root directory of repo
89 branch_name: branch name to abandon
90 """
91 # Ignore errors if failed, which means the branch didn't exist beforehand.
92 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
93
94
95def info(repo_dir, query):
96 """Repo info.
97
98 Args:
99 repo_dir: root directory of repo
100 query: key to query
101 """
102 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
103 key, value = map(str.strip, line.split(':'))
104 if key == query:
105 return value
106 assert 0
107
108
109def get_current_branch(repo_dir):
110 """Get manifest branch of existing repo directory."""
111 return info(repo_dir, 'Manifest branch')
112
113
114def get_manifest_groups(repo_dir):
115 """Get manifest group of existing repo directory."""
116 return info(repo_dir, 'Manifest groups')
117
118
119class ManifestManager(object):
120 """Abstract class for manifest operations."""
121
122 def sync_disk_state(self, rev):
123 """Switches disk state to given version.
124
125 Args:
126 rev: revision
127 """
128 raise NotImplementedError
129
130 def enumerate_manifest(self, old, new):
131 """Enumerates major versions with manifest.
132
133 Args:
134 old: start version (inclusive)
135 new: last version (inclusive)
136
137 Returns:
138 list of major versions
139 """
140 raise NotImplementedError
141
142 def fetch_manifest(self, rev):
143 raise NotImplementedError
144
145
146def parse_repo_set(repo_dir, manifest_name):
147 """Parse repo manifest.
148
149 This will ignore 'notdefault' groups in the manifest.
150
151 Args:
152 repo_dir: root directory of repo
153 manifest_name: name of manifest, which also means a filename relative to
154 manifest directory
155
156 Returns:
157 (dict) parsed manifest, which is mapping from path to revisions
158 """
159 manifest_path = os.path.join(repo_dir, '.repo', 'manifests', manifest_name)
160 root = xml.etree.ElementTree.parse(manifest_path)
161 result = {}
162 for project in root.findall('.//project'):
163 if 'notdefault' in project.get('groups', ''):
164 continue
165 path = project.get('path', project.get('name')) # default path is its name
166 rev = project.get('revision')
167 result[path] = rev
168 return result
169
170
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800171def parse_manifest(content, manifest_url):
172 root = xml.etree.ElementTree.fromstring(content)
173 result = {}
174 default = root.find('default')
175 if default is None:
176 default = {}
177
178 remote_fetch_map = {}
179 for remote in root.findall('.//remote'):
180 name = remote.get('name')
181 fetch_url = urlparse.urljoin(manifest_url, remote.get('fetch'))
182 if urlparse.urlparse(fetch_url).path not in ('', '/'):
183 # TODO(kcwu): support remote url with sub folders
184 raise ValueError(
185 'only support git repo at root path of remote server: %s' % fetch_url)
186 remote_fetch_map[name] = fetch_url
187
188 include = root.find('include')
189 if include is not None:
190 # TODO(kcwu): support <include> for chromeos
191 raise ValueError('<include> is not supported')
192
193 for project in root.findall('.//project'):
194 if 'notdefault' in project.get('groups', ''):
195 continue
196 for subproject in project.findall('.//project'):
197 logger.warning('nested project %s.%s is not supported and ignored',
198 project.get('name'), subproject.get('name'))
199
200 path = project.get('path', project.get('name')) # default path is its name
201 revision = project.get('revision', default.get('revision'))
202
203 remote_name = project.get('remote', default.get('remote'))
204 if remote_name not in remote_fetch_map:
205 raise ValueError('unknown remote name=%s' % remote_name)
206 fetch_url = remote_fetch_map[remote_name]
207 repo_url = urlparse.urljoin(fetch_url, project.get('name'))
208
209 result[path] = codechange.PathSpec(path, repo_url, revision)
210 return result
211
212
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800213class DependencyManager(object):
214 """Dependency manager."""
215
216 def __init__(self, repo_dir, manifest_manager):
217 self.repo_dir = repo_dir
218 self.manifest_manager = manifest_manager
219
220 @staticmethod
221 def _parse_rev(rev):
222 m = re.match(r'^([^,]+),([^,]+)\+(\d+)$', rev)
223 assert m
224 return m.group(1), m.group(2), int(m.group(3))
225
226 def get_revlist(self, old, new):
227 logger.info('get_revlist')
228 revlist = []
229 major_revs = self.manifest_manager.enumerate_manifest(old, new)
230 for i, major_rev in enumerate(major_revs):
231 revlist.append(major_rev)
232 if i + 1 == len(major_revs):
233 continue
234
235 old_mf = self.manifest_manager.fetch_manifest(major_revs[i])
236 new_mf = self.manifest_manager.fetch_manifest(major_revs[i + 1])
237 old_set = parse_repo_set(self.repo_dir, old_mf)
238 new_set = parse_repo_set(self.repo_dir, new_mf)
239
240 self.manifest_manager.fetch_git_repos(major_revs[i + 1])
241 difflist = git_util.get_difflist_between_two_set(self.repo_dir, old_set,
242 new_set)
243 # Theoretically, "old,new+N" equals to "new" if N=len(old diff). But it's
244 # not always true in practice because of racing and incorrect diff
245 # replay. So still add it.
246 for j, _diff in enumerate(difflist, 1):
247 rev = '%s,%s+%d' % (major_revs[i], major_revs[i + 1], j)
248 revlist.append(rev)
249 return revlist
250
251 def switch(self, rev):
252 if '+' in rev:
253 rev_old, rev_new, idx = self._parse_rev(rev)
254
255 old_mf = self.manifest_manager.fetch_manifest(rev_old)
256 new_mf = self.manifest_manager.fetch_manifest(rev_new)
257 old_set = parse_repo_set(self.repo_dir, old_mf)
258 new_set = parse_repo_set(self.repo_dir, new_mf)
259 difflist = git_util.get_difflist_between_two_set(self.repo_dir, old_set,
260 new_set)
261 assert 1 <= idx <= len(difflist)
262 difflist = difflist[:idx]
263 else:
264 rev_old = rev
265 old_mf = self.manifest_manager.fetch_manifest(rev_old)
266 difflist = []
267
268 self.manifest_manager.sync_disk_state(rev_old)
269
270 for i, diff in enumerate(difflist, 1):
271 logger.debug('[%d] applying "%s"', i, diff.summary(self.repo_dir))
272 diff.apply(self.repo_dir)
273 # TODO support <copyfile> and <linkfile> tags
274
275 def view_rev_diff(self, old, new):
276 if '+' in old:
277 old_base, old_next, old_idx = self._parse_rev(old)
278 else:
279 old_base, old_next, old_idx = old, None, 0
280
281 if '+' in new:
282 new_base, new_next, new_idx = self._parse_rev(new)
283 else:
284 new_base, new_next, new_idx = new, None, 0
285
286 major_revs = self.manifest_manager.enumerate_manifest(old_base, new_base)
287 if major_revs.index(old_base) + 1 == major_revs.index(new_base):
288 old_next = new_base
289 if new_idx == 0:
290 new_base = old_base
291 new_idx = -1
292 elif major_revs.index(old_base) == major_revs.index(new_base):
293 old_next = new_next
294
295 if old_base == new_base and old_next:
296 old_mf = self.manifest_manager.fetch_manifest(old_base)
297 new_mf = self.manifest_manager.fetch_manifest(old_next)
298 old_set = parse_repo_set(self.repo_dir, old_mf)
299 new_set = parse_repo_set(self.repo_dir, new_mf)
300 difflist = git_util.get_difflist_between_two_set(self.repo_dir, old_set,
301 new_set)
302 if new_idx == -1:
303 new_idx = len(difflist)
304
305 for i in range(old_idx, new_idx + 1):
306 if i == 0:
307 print('{base}'.format(base=old_base))
308 continue
309 diff = difflist[i - 1]
310 print('{base},{next}+{idx} {timestamp} {summary}'.format(
311 base=old_base,
312 next=old_next,
313 idx=i,
314 timestamp=diff.timestamp,
315 summary=diff.summary(self.repo_dir)))
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800316
317
318class RepoMirror(codechange.CodeStorage):
319 """Repo git mirror."""
320
321 def __init__(self, mirror_dir):
322 self.mirror_dir = mirror_dir
323
324 def _url_to_cache_dir(self, url):
325 # Here we assume remote fetch url is always at root of server url, so we can
326 # simply treat whole path as repo project name.
327 path = urlparse.urlparse(url).path
328 assert path[0] == '/'
329 return '%s.git' % path[1:]
330
331 def cached_git_root(self, repo_url):
332 cache_path = self._url_to_cache_dir(repo_url)
333 return os.path.join(self.mirror_dir, cache_path)