blob: 3c51c13278478d8c17745cd3772b23ad5c74542c [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 Wubfc4a642018-04-19 11:54:08 +080040def init(repo_dir,
41 manifest_url,
42 manifest_branch=None,
43 manifest_name=None,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080044 repo_url=None,
45 reference=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080046 """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
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080054 reference: location of mirror directory
Kuang-che Wubfc4a642018-04-19 11:54:08 +080055 """
56 cmd = ['repo', 'init', '--manifest-url', manifest_url]
57 if manifest_name:
58 cmd += ['--manifest-name', manifest_name]
59 if manifest_branch:
60 cmd += ['--manifest-branch', manifest_branch]
61 if repo_url:
62 cmd += ['--repo-url', repo_url]
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080063 if reference:
64 cmd += ['--reference', reference]
Kuang-che Wubfc4a642018-04-19 11:54:08 +080065 util.check_call(*cmd, cwd=repo_dir)
66
67
68def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
69 """Repo sync.
70
71 Args:
72 repo_dir: root directory of repo
73 jobs: projects to fetch simultaneously
74 manifest_name: filename of manifest
75 current_branch: fetch only current branch
76 """
77 cmd = ['repo', 'sync', '-q', '--force-sync']
78 if jobs:
79 cmd += ['-j', str(jobs)]
80 if manifest_name:
81 cmd += ['--manifest-name', manifest_name]
82 if current_branch:
83 cmd += ['--current-branch']
84 util.check_call(*cmd, cwd=repo_dir)
85
86
87def abandon(repo_dir, branch_name):
88 """Repo abandon.
89
90 Args:
91 repo_dir: root directory of repo
92 branch_name: branch name to abandon
93 """
94 # Ignore errors if failed, which means the branch didn't exist beforehand.
95 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
96
97
98def info(repo_dir, query):
99 """Repo info.
100
101 Args:
102 repo_dir: root directory of repo
103 query: key to query
104 """
105 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
106 key, value = map(str.strip, line.split(':'))
107 if key == query:
108 return value
109 assert 0
110
111
112def get_current_branch(repo_dir):
113 """Get manifest branch of existing repo directory."""
114 return info(repo_dir, 'Manifest branch')
115
116
117def get_manifest_groups(repo_dir):
118 """Get manifest group of existing repo directory."""
119 return info(repo_dir, 'Manifest groups')
120
121
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800122class ManifestParser(object):
123 """Enumerates historical manifest files and parses them."""
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800124
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800125 def __init__(self, manifest_dir):
126 self.manifest_dir = manifest_dir
127 self.manifest_url = get_manifest_url(self.manifest_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800128
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800129 def parse_single_xml(self, content, allow_include=False):
130 root = xml.etree.ElementTree.fromstring(content)
131 if not allow_include and root.find('include') is not None:
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800132 raise ValueError(
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800133 'Expects self-contained manifest. <include> is not allowed')
134 return root
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800135
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800136 def parse_xml_recursive(self, git_rev, path):
137 content = git_util.get_file_from_revision(self.manifest_dir, git_rev, path)
138 root = self.parse_single_xml(content, allow_include=True)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800139
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800140 result = xml.etree.ElementTree.Element('manifest')
141 for node in root:
142 if node.tag == 'include':
143 for subnode in self.parse_xml_recursive(git_rev, node.get('name')):
144 result.append(subnode)
145 else:
146 result.append(node)
147 return result
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800148
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800149 def process_parsed_result(self, root):
150 result = {}
151 default = root.find('default')
152 if default is None:
153 default = {}
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800154
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800155 remote_fetch_map = {}
156 for remote in root.findall('.//remote'):
157 name = remote.get('name')
158 fetch_url = urlparse.urljoin(self.manifest_url, remote.get('fetch'))
159 if urlparse.urlparse(fetch_url).path not in ('', '/'):
160 # TODO(kcwu): support remote url with sub folders
161 raise ValueError(
162 'only support git repo at root path of remote server: %s' %
163 fetch_url)
164 remote_fetch_map[name] = fetch_url
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800165
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800166 assert root.find('include') is None
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800167
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800168 for project in root.findall('.//project'):
169 if 'notdefault' in project.get('groups', ''):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800170 continue
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800171 for subproject in project.findall('.//project'):
172 logger.warning('nested project %s.%s is not supported and ignored',
173 project.get('name'), subproject.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800174
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800175 # default path is its name
176 path = project.get('path', project.get('name'))
177 revision = project.get('revision', default.get('revision'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800178
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800179 remote_name = project.get('remote', default.get('remote'))
180 if remote_name not in remote_fetch_map:
181 raise ValueError('unknown remote name=%s' % remote_name)
182 fetch_url = remote_fetch_map.get(remote_name)
183 repo_url = urlparse.urljoin(fetch_url, project.get('name'))
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800184
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800185 result[path] = codechange.PathSpec(path, repo_url, revision)
186 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800187
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800188 def enumerate_manifest_commits(self, start_time, end_time, path):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800189
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800190 def parse_dependencies(path, content):
191 del path # unused
192 root = self.parse_single_xml(content, allow_include=True)
193 for include in root.findall('.//include'):
194 yield include.get('name')
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800195
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800196 return git_util.get_history_recursively(self.manifest_dir, path, start_time,
197 end_time, parse_dependencies)
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800198
199
200class RepoMirror(codechange.CodeStorage):
201 """Repo git mirror."""
202
203 def __init__(self, mirror_dir):
204 self.mirror_dir = mirror_dir
205
206 def _url_to_cache_dir(self, url):
207 # Here we assume remote fetch url is always at root of server url, so we can
208 # simply treat whole path as repo project name.
209 path = urlparse.urlparse(url).path
210 assert path[0] == '/'
211 return '%s.git' % path[1:]
212
213 def cached_git_root(self, repo_url):
214 cache_path = self._url_to_cache_dir(repo_url)
215 return os.path.join(self.mirror_dir, cache_path)