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