blob: b04f7bbf7095f4cf1b0160652c0ced6f07031a98 [file] [log] [blame]
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001# Copyright 2018 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Repo utility.
5
6This module provides wrapper for "repo" (a Google-built repository management
7tool that runs on top of git) and related utility functions.
8"""
9
10from __future__ import print_function
11import logging
12import os
13import re
14import xml.etree.ElementTree
15
16from bisect_kit import git_util
17from bisect_kit import util
18
19logger = logging.getLogger(__name__)
20
21
22def init(repo_dir,
23 manifest_url,
24 manifest_branch=None,
25 manifest_name=None,
26 repo_url=None):
27 """Repo init.
28
29 Args:
30 repo_dir: root directory of repo
31 manifest_url: manifest repository location
32 manifest_branch: manifest branch or revision
33 manifest_name: initial manifest file name
34 repo_url: repo repository location
35 """
36 cmd = ['repo', 'init', '--manifest-url', manifest_url]
37 if manifest_name:
38 cmd += ['--manifest-name', manifest_name]
39 if manifest_branch:
40 cmd += ['--manifest-branch', manifest_branch]
41 if repo_url:
42 cmd += ['--repo-url', repo_url]
43 util.check_call(*cmd, cwd=repo_dir)
44
45
46def sync(repo_dir, jobs=16, manifest_name=None, current_branch=False):
47 """Repo sync.
48
49 Args:
50 repo_dir: root directory of repo
51 jobs: projects to fetch simultaneously
52 manifest_name: filename of manifest
53 current_branch: fetch only current branch
54 """
55 cmd = ['repo', 'sync', '-q', '--force-sync']
56 if jobs:
57 cmd += ['-j', str(jobs)]
58 if manifest_name:
59 cmd += ['--manifest-name', manifest_name]
60 if current_branch:
61 cmd += ['--current-branch']
62 util.check_call(*cmd, cwd=repo_dir)
63
64
65def abandon(repo_dir, branch_name):
66 """Repo abandon.
67
68 Args:
69 repo_dir: root directory of repo
70 branch_name: branch name to abandon
71 """
72 # Ignore errors if failed, which means the branch didn't exist beforehand.
73 util.call('repo', 'abandon', branch_name, cwd=repo_dir)
74
75
76def info(repo_dir, query):
77 """Repo info.
78
79 Args:
80 repo_dir: root directory of repo
81 query: key to query
82 """
83 for line in util.check_output('repo', 'info', '.', cwd=repo_dir).splitlines():
84 key, value = map(str.strip, line.split(':'))
85 if key == query:
86 return value
87 assert 0
88
89
90def get_current_branch(repo_dir):
91 """Get manifest branch of existing repo directory."""
92 return info(repo_dir, 'Manifest branch')
93
94
95def get_manifest_groups(repo_dir):
96 """Get manifest group of existing repo directory."""
97 return info(repo_dir, 'Manifest groups')
98
99
100class ManifestManager(object):
101 """Abstract class for manifest operations."""
102
103 def sync_disk_state(self, rev):
104 """Switches disk state to given version.
105
106 Args:
107 rev: revision
108 """
109 raise NotImplementedError
110
111 def enumerate_manifest(self, old, new):
112 """Enumerates major versions with manifest.
113
114 Args:
115 old: start version (inclusive)
116 new: last version (inclusive)
117
118 Returns:
119 list of major versions
120 """
121 raise NotImplementedError
122
123 def fetch_manifest(self, rev):
124 raise NotImplementedError
125
126
127def parse_repo_set(repo_dir, manifest_name):
128 """Parse repo manifest.
129
130 This will ignore 'notdefault' groups in the manifest.
131
132 Args:
133 repo_dir: root directory of repo
134 manifest_name: name of manifest, which also means a filename relative to
135 manifest directory
136
137 Returns:
138 (dict) parsed manifest, which is mapping from path to revisions
139 """
140 manifest_path = os.path.join(repo_dir, '.repo', 'manifests', manifest_name)
141 root = xml.etree.ElementTree.parse(manifest_path)
142 result = {}
143 for project in root.findall('.//project'):
144 if 'notdefault' in project.get('groups', ''):
145 continue
146 path = project.get('path', project.get('name')) # default path is its name
147 rev = project.get('revision')
148 result[path] = rev
149 return result
150
151
152class DependencyManager(object):
153 """Dependency manager."""
154
155 def __init__(self, repo_dir, manifest_manager):
156 self.repo_dir = repo_dir
157 self.manifest_manager = manifest_manager
158
159 @staticmethod
160 def _parse_rev(rev):
161 m = re.match(r'^([^,]+),([^,]+)\+(\d+)$', rev)
162 assert m
163 return m.group(1), m.group(2), int(m.group(3))
164
165 def get_revlist(self, old, new):
166 logger.info('get_revlist')
167 revlist = []
168 major_revs = self.manifest_manager.enumerate_manifest(old, new)
169 for i, major_rev in enumerate(major_revs):
170 revlist.append(major_rev)
171 if i + 1 == len(major_revs):
172 continue
173
174 old_mf = self.manifest_manager.fetch_manifest(major_revs[i])
175 new_mf = self.manifest_manager.fetch_manifest(major_revs[i + 1])
176 old_set = parse_repo_set(self.repo_dir, old_mf)
177 new_set = parse_repo_set(self.repo_dir, new_mf)
178
179 self.manifest_manager.fetch_git_repos(major_revs[i + 1])
180 difflist = git_util.get_difflist_between_two_set(self.repo_dir, old_set,
181 new_set)
182 # Theoretically, "old,new+N" equals to "new" if N=len(old diff). But it's
183 # not always true in practice because of racing and incorrect diff
184 # replay. So still add it.
185 for j, _diff in enumerate(difflist, 1):
186 rev = '%s,%s+%d' % (major_revs[i], major_revs[i + 1], j)
187 revlist.append(rev)
188 return revlist
189
190 def switch(self, rev):
191 if '+' in rev:
192 rev_old, rev_new, idx = self._parse_rev(rev)
193
194 old_mf = self.manifest_manager.fetch_manifest(rev_old)
195 new_mf = self.manifest_manager.fetch_manifest(rev_new)
196 old_set = parse_repo_set(self.repo_dir, old_mf)
197 new_set = parse_repo_set(self.repo_dir, new_mf)
198 difflist = git_util.get_difflist_between_two_set(self.repo_dir, old_set,
199 new_set)
200 assert 1 <= idx <= len(difflist)
201 difflist = difflist[:idx]
202 else:
203 rev_old = rev
204 old_mf = self.manifest_manager.fetch_manifest(rev_old)
205 difflist = []
206
207 self.manifest_manager.sync_disk_state(rev_old)
208
209 for i, diff in enumerate(difflist, 1):
210 logger.debug('[%d] applying "%s"', i, diff.summary(self.repo_dir))
211 diff.apply(self.repo_dir)
212 # TODO support <copyfile> and <linkfile> tags
213
214 def view_rev_diff(self, old, new):
215 if '+' in old:
216 old_base, old_next, old_idx = self._parse_rev(old)
217 else:
218 old_base, old_next, old_idx = old, None, 0
219
220 if '+' in new:
221 new_base, new_next, new_idx = self._parse_rev(new)
222 else:
223 new_base, new_next, new_idx = new, None, 0
224
225 major_revs = self.manifest_manager.enumerate_manifest(old_base, new_base)
226 if major_revs.index(old_base) + 1 == major_revs.index(new_base):
227 old_next = new_base
228 if new_idx == 0:
229 new_base = old_base
230 new_idx = -1
231 elif major_revs.index(old_base) == major_revs.index(new_base):
232 old_next = new_next
233
234 if old_base == new_base and old_next:
235 old_mf = self.manifest_manager.fetch_manifest(old_base)
236 new_mf = self.manifest_manager.fetch_manifest(old_next)
237 old_set = parse_repo_set(self.repo_dir, old_mf)
238 new_set = parse_repo_set(self.repo_dir, new_mf)
239 difflist = git_util.get_difflist_between_two_set(self.repo_dir, old_set,
240 new_set)
241 if new_idx == -1:
242 new_idx = len(difflist)
243
244 for i in range(old_idx, new_idx + 1):
245 if i == 0:
246 print('{base}'.format(base=old_base))
247 continue
248 diff = difflist[i - 1]
249 print('{base},{next}+{idx} {timestamp} {summary}'.format(
250 base=old_base,
251 next=old_next,
252 idx=i,
253 timestamp=diff.timestamp,
254 summary=diff.summary(self.repo_dir)))