blob: 0ec6248bbf462578e582f668964cb8b26a968f74 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu3eb6b502018-06-06 16:15:18 +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"""Model of source code organization and changes.
6
7This module modeled complex source code organization, i.e. nested git repos,
8and their version relationship, i.e. pinned or floating git repo. In other
9words, it's abstraction of chrome's gclient DEPS, and chromeos and Android's
10repo manifest.
11"""
12
13from __future__ import print_function
Kuang-che Wu13acc7b2020-06-15 10:45:35 +080014import collections
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080015import copy
16import json
17import logging
18import os
19import re
20import shutil
21
22from bisect_kit import cli
Kuang-che Wue121fae2018-11-09 16:18:39 +080023from bisect_kit import errors
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080024from bisect_kit import git_util
25
26logger = logging.getLogger(__name__)
27
28_re_intra_rev = r'^([^,]+)~([^,]+)/(\d+)$'
29
30SPEC_FIXED = 'fixed'
31SPEC_FLOAT = 'float'
32_DIFF_CACHE_DIR = 'bisectkit-cache'
33
34
35def make_intra_rev(a, b, index):
36 """Makes intra-rev version string.
37
38 Between two major "named" versions a and b, there are many small changes
39 (commits) in-between. bisect-kit will identify all those instances and bisect
40 them. We give names to those instances and call these names as "intra-rev"
41 which stands for minor version numbers within two major version.
42
43 Note, a+index (without b) is not enough to identify an unique change due to
44 branches. Take chromeos as example, both 9900.1.0 and 9901.0.0 are derived
45 from 9900.0.0, so "9900.0.0 plus 100 changes" may ambiguously refer to states
46 in 9900.1.0 and 9901.0.0.
47
48 Args:
49 a: the start version
50 b: the end version
51 index: the index number of changes between a and b
52
53 Returns:
54 the intra-rev version string
55 """
56 return '%s~%s/%d' % (a, b, index)
57
58
59def parse_intra_rev(rev):
60 """Decomposes intra-rev string.
61
62 See comments of make_intra_rev for what is intra-rev.
63
64 Args:
65 rev: intra-rev string or normal version number
66
67 Returns:
68 (start, end, index). If rev is not intra-rev, it must be normal version
69 number and returns (rev, rev, 0).
70 """
71 m = re.match(_re_intra_rev, rev)
Kuang-che Wu89ac2e72018-07-25 17:39:07 +080072 if not m:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080073 return rev, rev, 0
74
Kuang-che Wu89ac2e72018-07-25 17:39:07 +080075 return m.group(1), m.group(2), int(m.group(3))
76
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080077
78def argtype_intra_rev(argtype):
79 """Validates argument is intra-rev.
80
81 Args:
82 argtype: argtype function which validates major version number
83
84 Returns:
85 A new argtype function which matches intra-rev
86 """
87
88 def argtype_function(s):
Kuang-che Wucab92452019-01-19 18:24:29 +080089 examples = []
90 try:
91 return argtype(s)
92 except cli.ArgTypeError as e:
93 examples += e.example
94
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080095 m = re.match(_re_intra_rev, s)
96 if m:
97 try:
98 argtype(m.group(1))
99 argtype(m.group(2))
100 return s
101 except cli.ArgTypeError as e:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800102 for example in e.example:
103 examples.append(make_intra_rev(example, example, 10))
104 raise cli.ArgTypeError('Invalid intra rev', examples)
Kuang-che Wucab92452019-01-19 18:24:29 +0800105
106 examples.append(make_intra_rev('<rev1>', '<rev2>', 10))
107 raise cli.ArgTypeError('Invalid rev', examples)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800108
109 return argtype_function
110
111
112def _normalize_repo_url(repo_url):
113 repo_url = re.sub(r'https://chrome-internal.googlesource.com/a/',
114 r'https://chrome-internal.googlesource.com/', repo_url)
115 repo_url = re.sub(r'\.git$', '', repo_url)
116 return repo_url
117
118
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800119class PathSpec:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800120 """Specified code version of one path.
121
122 Attributes:
123 path: local path, relative to project base dir
124 repo_url: code repository location
125 at: code version, could be git hash or branch name
126 """
127
128 def __init__(self, path, repo_url, at):
129 self.path = path
130 self.repo_url = repo_url
131 self.at = at
132
133 def is_static(self):
134 return git_util.is_git_rev(self.at)
135
136 def __eq__(self, rhs):
137 if self.path != rhs.path:
138 return False
139 if self.at != rhs.at:
140 return False
141 if _normalize_repo_url(self.repo_url) != _normalize_repo_url(rhs.repo_url):
142 return False
143 return True
144
145 def __ne__(self, rhs):
146 return not self == rhs
147
148
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800149class Spec:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800150 """Collection of PathSpec.
151
152 Spec is analogy to gclient's DEPS and repo's manifest.
153
154 Attributes:
155 spec_type: type of spec, SPEC_FIXED or SPEC_FLOAT. SPEC_FIXED means code
156 version is pinned and fixed. On the other hand, SPEC_FLOAT is not
157 pinned and the actual version (git commit) may change over time.
158 name: name of this spec, for debugging purpose. usually version number
159 or git hash
160 timestamp: timestamp of this spec
161 path: path of spec
162 entries: paths to PathSpec dict
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800163 revision: a commit id of manifest-internal indicates the manifest revision,
164 this argument is not used in DEPS.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800165 """
166
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800167 def __init__(self,
168 spec_type,
169 name,
170 timestamp,
171 path,
172 entries=None,
173 revision=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800174 self.spec_type = spec_type
175 self.name = name
176 self.timestamp = timestamp
177 self.path = path
178 self.entries = entries
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800179 self.revision = revision
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800180
181 def copy(self):
182 return copy.deepcopy(self)
183
184 def similar_score(self, rhs):
185 """Calculates similar score to another Spec.
186
187 Returns:
188 score of similarity. Smaller value is more similar.
189 """
190 score = 0
191 for path in set(self.entries) & set(rhs.entries):
192 if rhs[path] == self[path]:
193 continue
194 if rhs[path].at == self[path].at:
195 # it's often that remote repo moved around but should be treated as the
196 # same one
197 score += 0.1
198 else:
199 score += 1
200 score += len(set(self.entries) ^ set(rhs.entries))
201 return score
202
203 def is_static(self):
204 return all(path_spec.is_static() for path_spec in self.entries.values())
205
206 def is_subset(self, rhs):
207 return set(self.entries.keys()) <= set(rhs.entries.keys())
208
209 def __getitem__(self, path):
210 return self.entries[path]
211
212 def __contains__(self, path):
213 return path in self.entries
214
215 def apply(self, action_group):
216 self.timestamp = action_group.timestamp
217 self.name = '(%s)' % self.timestamp
218 for action in action_group.actions:
219 if isinstance(action, GitAddRepo):
220 self.entries[action.path] = PathSpec(action.path, action.repo_url,
221 action.rev)
222 elif isinstance(action, GitCheckoutCommit):
223 self.entries[action.path].at = action.rev
224 elif isinstance(action, GitRemoveRepo):
225 del self.entries[action.path]
226 else:
227 assert 0, 'unknown action: %s' % action.__class__.__name__
228
229 def dump(self):
230 # for debugging
231 print(self.name, self.path, self.timestamp)
232 print('size', len(self.entries))
233 for path, path_spec in sorted(self.entries.items()):
234 print(path, path_spec.at)
235
236 def diff(self, rhs):
237 logger.info('diff between %s and %s', self.name, rhs.name)
238 expect = set(self.entries)
239 actual = set(rhs.entries)
Kuang-che Wu4997bfd2019-03-18 13:09:26 +0800240 common_count = 0
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800241 for path in sorted(expect - actual):
242 logger.info('-%s', path)
243 for path in sorted(actual - expect):
244 logger.info('+%s', path)
245 for path in sorted(expect & actual):
246 if self[path] == rhs[path]:
Kuang-che Wu4997bfd2019-03-18 13:09:26 +0800247 common_count += 1
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800248 continue
249 if self[path].at != rhs[path].at:
250 logger.info(' %s: at %s vs %s', path, self[path].at, rhs[path].at)
251 if self[path].repo_url != rhs[path].repo_url:
252 logger.info(' %s: repo_url %s vs %s', path, self[path].repo_url,
253 rhs[path].repo_url)
Kuang-che Wu4997bfd2019-03-18 13:09:26 +0800254 logger.info('and common=%s', common_count)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800255
256
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800257class Action:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800258 """Actions describe changes from one Spec to another.
259
260 Attributes:
261 timestamp: action time
262 path: action path, which is relative to project root
263 """
264
265 def __init__(self, timestamp, path):
266 self.timestamp = timestamp
267 self.path = path
268
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800269 def apply(self, _code_storage, _root_dir):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800270 raise NotImplementedError
271
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800272 def summary(self):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800273 raise NotImplementedError
274
275 def __eq__(self, rhs):
276 return self.__dict__ == rhs.__dict__
277
278 def serialize(self):
279 return self.__class__.__name__, self.__dict__
280
281
282def unserialize_action(data):
283 classes = [GitCheckoutCommit, GitAddRepo, GitRemoveRepo]
284 class_name, values = data
285 assert class_name in [cls.__name__ for cls in classes
286 ], 'unknown action class: %s' % class_name
287 for cls in classes:
288 if class_name == cls.__name__:
Kuang-che Wu89ac2e72018-07-25 17:39:07 +0800289 action = cls(**values)
290 break
291 return action
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800292
293
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800294class ActionGroup:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800295 """Atomic group of Action objects
296
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800297 This models atomic actions, ex:
298 - repo added/removed in the same manifest commit
299 - commits appears at the same time due to repo add
300 - gerrit topic
301 - circular CQ-DEPEND (Cq-Depend)
302 Otherwise, one ActionGroup usually consists only one Action object.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800303 """
304
305 def __init__(self, timestamp, comment=None):
306 self.timestamp = timestamp
307 self.name = None
308 self.actions = []
309 self.comment = comment
310
311 def add(self, action):
312 self.actions.append(action)
313
314 def serialize(self):
Kuang-che Wu22455262018-08-03 15:38:29 +0800315 return dict(
316 timestamp=self.timestamp,
317 name=self.name,
318 comment=self.comment,
319 actions=[a.serialize() for a in self.actions])
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800320
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800321 def summary(self):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800322 result = {}
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800323 if self.comment:
Kuang-che Wue80bb872018-11-15 19:45:25 +0800324 result['comment'] = self.comment
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800325 result['actions'] = [action.summary() for action in self.actions]
Kuang-che Wue80bb872018-11-15 19:45:25 +0800326 return result
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800327
328 @staticmethod
329 def unserialize(data):
Kuang-che Wu22455262018-08-03 15:38:29 +0800330 ag = ActionGroup(data['timestamp'])
331 ag.name = data['name']
332 ag.comment = data['comment']
333 for x in data['actions']:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800334 ag.add(unserialize_action(x))
335 return ag
336
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800337 def apply(self, code_storage, root_dir):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800338 for action in self.actions:
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800339 action.apply(code_storage, root_dir)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800340
341
342class GitCheckoutCommit(Action):
343 """Describes a git commit action.
344
345 Attributes:
346 repo_url: the corresponding url of git repo
347 rev: git commit to checkout
348 """
349
350 def __init__(self, timestamp, path, repo_url, rev):
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800351 super().__init__(timestamp, path)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800352 self.repo_url = repo_url
353 self.rev = rev
354
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800355 def apply(self, code_storage, root_dir):
356 del code_storage # unused
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800357 git_repo = os.path.join(root_dir, self.path)
358 assert git_util.is_git_root(git_repo)
359 git_util.checkout_version(git_repo, self.rev)
360
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800361 def summary(self):
362 text = 'commit %s %s' % (self.rev[:10], self.path)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800363 return dict(
364 timestamp=self.timestamp,
365 action_type='commit',
366 path=self.path,
Kuang-che Wue80bb872018-11-15 19:45:25 +0800367 repo_url=self.repo_url,
368 rev=self.rev,
369 text=text,
370 )
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800371
372
373class GitAddRepo(Action):
374 """Describes a git repo add action.
375
376 Attributes:
377 repo_url: the corresponding url of git repo to add
378 rev: git commit to checkout
379 """
380
381 def __init__(self, timestamp, path, repo_url, rev):
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800382 super().__init__(timestamp, path)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800383 self.repo_url = repo_url
384 self.rev = rev
385
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800386 def apply(self, code_storage, root_dir):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800387 git_repo = os.path.join(root_dir, self.path)
Kuang-che Wudf11c8a2019-03-18 13:21:24 +0800388 if os.path.exists(git_repo):
389 if os.path.isdir(git_repo) and not os.listdir(git_repo):
390 # mimic gclient's behavior; don't panic
391 logger.warning(
392 'adding repo %s; there is already an empty directory; '
393 'assume it is okay', git_repo)
394 else:
395 assert not os.path.exists(git_repo), '%s already exists' % git_repo
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800396
397 reference = code_storage.cached_git_root(self.repo_url)
398 git_util.clone(git_repo, self.repo_url, reference=reference)
399 git_util.checkout_version(git_repo, self.rev)
400
401 code_storage.add_to_project_list(root_dir, self.path, self.repo_url)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800402
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800403 def summary(self):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800404 text = 'add repo %s from %s@%s' % (self.path, self.repo_url, self.rev[:10])
405 return dict(
406 timestamp=self.timestamp,
407 action_type='add_repo',
408 path=self.path,
Kuang-che Wu356ecb92019-04-02 16:30:25 +0800409 repo_url=self.repo_url,
410 rev=self.rev,
Kuang-che Wue80bb872018-11-15 19:45:25 +0800411 text=text,
412 )
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800413
414
415class GitRemoveRepo(Action):
416 """Describes a git repo remove action."""
417
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800418 def apply(self, code_storage, root_dir):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800419 assert self.path
420 git_repo = os.path.join(root_dir, self.path)
Kuang-che Wu95e77ff2020-12-25 21:34:09 +0800421 assert git_util.is_git_root(git_repo), '%r should be a git repo' % git_repo
Kuang-che Wu067ff292019-02-14 18:16:23 +0800422 # TODO(kcwu): other projects may be sub-tree of `git_repo`.
423 # They should not be deleted. (crbug/930047)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800424 shutil.rmtree(git_repo)
425
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800426 code_storage.remove_from_project_list(root_dir, self.path)
427
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800428 def summary(self):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800429 return dict(
430 timestamp=self.timestamp,
431 action_type='remove_repo',
432 path=self.path,
433 text='remove repo %s' % self.path,
434 )
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800435
436
437def apply_actions(code_storage, action_groups, root_dir):
438 # Speed optimization: only apply the last one of consecutive commits per
439 # repo. It is possible to optimize further, but need to take care git repo
440 # add/remove within another repo.
441 commits = {}
442
443 def batch_apply(commits):
Kuang-che Wu261174e2020-01-09 17:51:31 +0800444 for i, _, commit_action in sorted(commits.values(), key=lambda x: x[:2]):
Zheng-Jie Chang011bf952020-06-18 07:45:30 +0800445 logger.debug('[%d] applying "%r"', i, commit_action.summary())
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800446 commit_action.apply(code_storage, root_dir)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800447
448 for i, action_group in enumerate(action_groups, 1):
Kuang-che Wud1d45b42018-07-05 00:46:45 +0800449 for action in action_group.actions:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800450 if not isinstance(action, GitCheckoutCommit):
451 break
452 else:
453 # If all actions are commits, defer them for batch processing.
Kuang-che Wu261174e2020-01-09 17:51:31 +0800454 for j, action in enumerate(action_group.actions):
455 commits[action.path] = (i, j, action)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800456 continue
457
458 batch_apply(commits)
459 commits = {}
Kuang-che Wu95e77ff2020-12-25 21:34:09 +0800460 logger.debug('[%d] applying "%r"', i, action_group.summary())
461 action_group.apply(code_storage, root_dir)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800462
463 batch_apply(commits)
464
465
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800466class SpecManager:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800467 """Spec related abstract operations.
468
469 This class enumerates Spec instances and switch disk state to Spec.
470
471 In other words, this class abstracts:
472 - discovery of gclient's DEPS and repo's manifest
473 - gclient sync and repo sync
474 """
475
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800476 def collect_float_spec(self, old, new, fixed_specs=None):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800477 """Collects float Spec between two versions.
478
479 This method may fetch spec from network. However, it should not switch tree
480 version state.
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800481
482 Args:
483 old: old version
484 new: new version
485 fixed_specs: fixed specs from collect_fixed_spec(old, new) for Chrome OS
486 or None for others
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800487 """
488 raise NotImplementedError
489
490 def collect_fixed_spec(self, old, new):
491 """Collects fixed Spec between two versions.
492
493 This method may fetch spec from network. However, it should not switch tree
494 version state.
495 """
496 raise NotImplementedError
497
498 def parse_spec(self, spec):
499 """Parses information for Spec object.
500
501 Args:
502 spec: Spec object. It specifies what to parse and the parsed information
503 is stored inside.
504 """
505 raise NotImplementedError
506
507 def sync_disk_state(self, rev):
508 """Switch source tree state to given version."""
509 raise NotImplementedError
510
511
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800512class CodeStorage:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800513 """Query code history and commit relationship without checkout.
514
515 Because paths inside source tree may be deleted or map to different remote
516 repo in different versions, we cannot query git information of one version
517 but the tree state is at another version. In order to query information
518 without changing tree state and fast, we need out of tree source code
519 storage.
520
521 This class assumes all git repos are mirrored somewhere on local disk.
522 Subclasses just need to implement cached_git_root() which returns the
523 location.
524
525 In other words, this class abstracts operations upon gclient's cache-dir
526 repo's mirror.
527 """
528
529 def cached_git_root(self, repo_url):
530 """The cached path of given remote git repo.
531
532 Args:
533 repo_url: URL of git remote repo
534
535 Returns:
536 path of cache folder
537 """
538 raise NotImplementedError
539
Kuang-che Wu6948ecc2018-09-11 17:43:49 +0800540 def add_to_project_list(self, project_root, path, repo_url):
541 raise NotImplementedError
542
543 def remove_from_project_list(self, project_root, path):
544 raise NotImplementedError
545
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800546 def is_ancestor_commit(self, spec, path, old, new):
547 """Determine one commit is ancestor of another.
548
549 Args:
550 spec: Spec object
551 path: local path relative to project root
552 old: commit id
553 new: commit id
554
555 Returns:
556 True if `old` is ancestor of `new`
557 """
558 git_root = self.cached_git_root(spec[path].repo_url)
559 return git_util.is_ancestor_commit(git_root, old, new)
560
561 def get_rev_by_time(self, spec, path, timestamp):
562 """Get commit hash of given spec by time.
563
564 Args:
565 spec: Spec object
566 path: local path relative to project root
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800567 timestamp: timestamp
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800568
569 Returns:
570 The commit hash of given time. If there are commits with the given
571 timestamp, returns the last commit.
572 """
573 git_root = self.cached_git_root(spec[path].repo_url)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800574 # spec[path].at is remote reference name. Since git_root is a mirror (not
575 # a local checkout), there is no need to convert the name.
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800576 return git_util.get_rev_by_time(git_root, timestamp, spec[path].at)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800577
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800578 def get_actions_between_two_commit(self,
579 spec,
580 path,
581 old,
582 new,
583 ignore_not_ancestor=False):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800584 git_root = self.cached_git_root(spec[path].repo_url)
585 result = []
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800586 # not in the same branch, regard as an atomic operation
587 # this situation happens when
588 # 1. new is branched from old and
589 # 2. commit timestamp is not reliable(i.e. commit time != merged time)
590 # old and new might not have ancestor relation
591 if ignore_not_ancestor and old != new and not git_util.is_ancestor_commit(
592 git_root, old, new):
593 timestamp = git_util.get_commit_time(git_root, new)
594 result.append(
595 GitCheckoutCommit(timestamp, path, spec[path].repo_url, new))
596 return result
597
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800598 for timestamp, git_rev in git_util.list_commits_between_commits(
599 git_root, old, new):
600 result.append(
601 GitCheckoutCommit(timestamp, path, spec[path].repo_url, git_rev))
602 return result
603
604 def is_containing_commit(self, spec, path, rev):
605 git_root = self.cached_git_root(spec[path].repo_url)
606 return git_util.is_containing_commit(git_root, rev)
607
608 def are_spec_commits_available(self, spec):
609 for path, path_spec in spec.entries.items():
610 if not path_spec.is_static():
611 continue
612 if not self.is_containing_commit(spec, path, path_spec.at):
613 return False
614 return True
615
616
Kuang-che Wu23192ad2020-03-11 18:12:46 +0800617class CodeManager:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800618 """Class to reconstruct historical source tree state.
619
620 This class can reconstruct all moments of source tree state and diffs between
621 them.
622
623 Attributes:
624 root_dir: root path of project source tree
625 spec_manager: SpecManager object
626 code_storage: CodeStorage object
627 """
628
629 def __init__(self, root_dir, spec_manager, code_storage):
630 self.root_dir = root_dir
631 self.spec_manager = spec_manager
632 self.code_storage = code_storage
633
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800634 def generate_action_groups_between_specs(self, prev_float, next_float):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800635 """Generates actions between two float specs.
636
637 Args:
638 prev_float: start of spec object (exclusive)
639 next_float: end of spec object (inclusive)
640
641 Returns:
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800642 list of ActionGroup object (ordered)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800643 """
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800644 groups = []
645 last_group = ActionGroup(next_float.timestamp)
Zheng-Jie Changeb5aaf32020-01-10 16:36:58 +0800646 is_removed = set()
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800647
648 # `branch_between_float_specs` is currently a chromeos-only logic,
649 # and branch behavior is not verified for android and chrome now.
650 is_chromeos_branched = False
651 if hasattr(self.spec_manager, 'branch_between_float_specs'
652 ) and self.spec_manager.branch_between_float_specs(
653 prev_float, next_float):
654 is_chromeos_branched = True
655
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800656 # Sort alphabetically, so parent directories are handled before children
657 # directories.
Zheng-Jie Changeb5aaf32020-01-10 16:36:58 +0800658 for path in sorted(set(prev_float.entries) | set(next_float.entries)):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800659 # Add repo
660 if path not in prev_float:
661 if next_float[path].is_static():
662 next_at = next_float[path].at
663 else:
664 next_at = self.code_storage.get_rev_by_time(next_float, path,
665 next_float.timestamp)
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800666 last_group.add(
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800667 GitAddRepo(next_float.timestamp, path, next_float[path].repo_url,
668 next_at))
669 continue
670
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800671 # Existing path is floating.
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800672 if not prev_float[path].is_static():
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800673 # Enumerates commits until next spec. Get `prev_at` and `till_at`
674 # by prev_float and next_float's timestamp.
675 #
676 # 1. Non-branched case:
677 #
678 # prev_at till_at
679 # prev branch ---> o --------> o --------> o --------> o --------> ...
680 # ^ ^
681 # prev_float.timestamp next_float.timestamp
682 #
683 # building an image between prev_at and till_at should follow
684 # prev_float's spec.
685 #
686 # 2. Branched case:
687 #
688 # till_at
689 # /------->o---------->
690 # / ^ next_float.timestamp
691 # / prev_at
692 # ---------->o---------------------->
693 # ^prev_float.timestamp
694 #
695 # building an image between prev_at and till_at should follow
696 # next_float's spec.
697 #
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800698 prev_at = self.code_storage.get_rev_by_time(prev_float, path,
699 prev_float.timestamp)
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800700 if is_chromeos_branched:
701 till_at = self.code_storage.get_rev_by_time(next_float, path,
702 next_float.timestamp)
703 else:
704 till_at = self.code_storage.get_rev_by_time(prev_float, path,
705 next_float.timestamp)
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800706 actions = self.code_storage.get_actions_between_two_commit(
Zheng-Jie Chang868c1752020-01-21 14:42:41 +0800707 prev_float, path, prev_at, till_at, ignore_not_ancestor=True)
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800708
709 # Assume commits with the same timestamp as manifest/DEPS change are
710 # atomic.
711 if actions and actions[-1].timestamp == next_float.timestamp:
712 last_group.add(actions.pop())
713
714 for action in actions:
715 group = ActionGroup(action.timestamp)
716 group.add(action)
717 groups.append(group)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800718 else:
719 prev_at = till_at = prev_float[path].at
720
721 # At next_float.timestamp.
722 if path not in next_float:
Zheng-Jie Changeb5aaf32020-01-10 16:36:58 +0800723 if path in is_removed:
724 continue
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800725 # remove repo
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800726 next_at = None
Kuang-che Wucbe12432019-03-18 19:35:03 +0800727 sub_repos = [p for p in prev_float.entries if p.startswith(path + '/')]
Kuang-che Wucbe12432019-03-18 19:35:03 +0800728 # Remove deeper repo first
729 for path2 in sorted(sub_repos, reverse=True):
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800730 last_group.add(GitRemoveRepo(next_float.timestamp, path2))
Zheng-Jie Changeb5aaf32020-01-10 16:36:58 +0800731 is_removed.add(path2)
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800732 last_group.add(GitRemoveRepo(next_float.timestamp, path))
Zheng-Jie Changeb5aaf32020-01-10 16:36:58 +0800733 is_removed.add(path)
Kuang-che Wucbe12432019-03-18 19:35:03 +0800734 for path2 in sorted(set(sub_repos) & set(next_float.entries)):
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800735 last_group.add(
Kuang-che Wucbe12432019-03-18 19:35:03 +0800736 GitAddRepo(next_float.timestamp, path2,
737 next_float[path2].repo_url, prev_float[path2].at))
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800738
739 elif next_float[path].is_static():
740 # pinned to certain commit on different branch
741 next_at = next_float[path].at
742
743 elif next_float[path].at == prev_float[path].at:
744 # keep floating on the same branch
745 next_at = till_at
746
747 else:
748 # switch to another branch
749 # prev_at till_at
750 # prev branch ---> o --------> o --------> o --------> o --------> ...
751 #
752 # next_at
753 # next branch ...... o ------> o --------> o -----> ...
754 # ^ ^
755 # prev_float.timestamp next_float.timestamp
756 next_at = self.code_storage.get_rev_by_time(next_float, path,
757 next_float.timestamp)
758
759 if next_at and next_at != till_at:
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800760 last_group.add(
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800761 GitCheckoutCommit(next_float.timestamp, path,
762 next_float[path].repo_url, next_at))
763
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800764 groups.sort(key=lambda x: x.timestamp)
765 if last_group.actions:
766 groups.append(last_group)
767 return groups
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800768
769 def synthesize_fixed_spec(self, float_spec, timestamp):
770 """Synthesizes fixed spec from float spec of given time.
771
772 Args:
773 float_spec: the float spec
774 timestamp: snapshot time
775
776 Returns:
777 Spec object
778 """
779 result = {}
780 for path, path_spec in float_spec.entries.items():
781 if not path_spec.is_static():
782 at = self.code_storage.get_rev_by_time(float_spec, path, timestamp)
783 path_spec = PathSpec(path_spec.path, path_spec.repo_url, at)
784
785 result[path] = copy.deepcopy(path_spec)
786
787 name = '%s@%s' % (float_spec.path, timestamp)
788 return Spec(SPEC_FIXED, name, timestamp, float_spec.path, result)
789
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800790 def match_spec(self, target, specs, start_index=0):
791 threshold = 3600
792 # ideal_index is the index of last spec before target
793 # begin and end are the range of indexes within threshold (inclusive)
794 ideal_index = None
795 begin, end = None, None
796 for i, spec in enumerate(specs[start_index:], start_index):
797 if spec.timestamp <= target.timestamp:
798 ideal_index = i
799 if abs(spec.timestamp - target.timestamp) < threshold:
800 if begin is None:
801 begin = i
802 end = i
803
804 candidates = []
805 if ideal_index is not None:
806 candidates.append(ideal_index)
807 if begin is not None:
Kuang-che Wuae6824b2019-08-27 22:20:01 +0800808 candidates.extend(list(range(begin, end + 1)))
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800809 if not candidates:
810 logger.error('unable to match %s: all specs are after it', target.name)
811 return None
812
813 compatible_candidates = [
814 i for i in candidates if specs[i].is_subset(target)
815 ]
816 if not compatible_candidates:
817 logger.error('unable to match %s: no compatible specs', target.name)
818 spec = specs[candidates[0]]
819 target.diff(spec)
820 return None
821
822 scores = []
823 for i in compatible_candidates:
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800824 # Tie-break: prefer earlier timestamp and smaller difference.
825 if specs[i].timestamp <= target.timestamp:
826 timediff = 0, target.timestamp - specs[i].timestamp
827 else:
828 timediff = 1, specs[i].timestamp - target.timestamp
829 scores.append((specs[i].similar_score(target), timediff, i))
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800830 scores.sort()
831
Kuang-che Wu8a28a9d2018-09-11 17:43:36 +0800832 score, _, index = scores[0]
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800833 if score != 0:
834 logger.warning('not exactly match (score=%s): %s', score, target.name)
835 target.diff(specs[index])
836
837 if index < ideal_index:
838 logger.warning(
839 '%s (%s) matched earlier spec at %s instead of %s, racing? offset %d',
840 target.name, target.timestamp, specs[index].timestamp,
841 specs[ideal_index].timestamp,
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800842 specs[index].timestamp - target.timestamp)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800843 if index > ideal_index:
844 logger.warning(
845 'spec committed at %d matched later commit at %d. bad server clock?',
846 target.timestamp, specs[index].timestamp)
847
848 return index
849
850 def associate_fixed_and_synthesized_specs(self, fixed_specs,
851 synthesized_specs):
852 # All fixed specs are snapshot of float specs. Theoretically, they
853 # should be identical to one of the synthesized specs.
854 # However, it's not always true for some reasons --- maybe due to race
855 # condition, maybe due to bugs of this bisect-kit.
856 # To overcome this glitch, we try to match them by similarity instead of
857 # exact match.
858 result = []
859 last_index = 0
860 for i, fixed_spec in enumerate(fixed_specs):
861 matched_index = self.match_spec(fixed_spec, synthesized_specs, last_index)
862 if matched_index is None:
863 if i in (0, len(fixed_specs) - 1):
864 logger.error('essential spec mismatch, unable to continue')
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800865 raise ValueError('Commit history analyze failed. '
866 'Bisector cannot deal with this version range.')
Kuang-che Wuc0baf752020-06-29 11:32:26 +0800867 logger.warning('%s do not match, skip', fixed_spec.name)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800868 continue
869 result.append((i, matched_index))
870 last_index = matched_index
871
872 return result
873
874 def _create_make_up_actions(self, fixed_spec, synthesized):
875 timestamp = synthesized.timestamp
876 make_up = ActionGroup(
877 timestamp, comment='make up glitch for %s' % fixed_spec.name)
878 for path in set(fixed_spec.entries) & set(synthesized.entries):
879 if fixed_spec[path].at == synthesized[path].at:
880 continue
881 action = GitCheckoutCommit(timestamp, path, synthesized[path].repo_url,
882 synthesized[path].at)
883 make_up.add(action)
884
885 if not make_up.actions:
886 return None
887 return make_up
888
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800889 def _batch_fill_action_commit_log(self, details):
890 group_by_repo = collections.defaultdict(list)
891 for detail in details.values():
892 for action in detail.get('actions', []):
893 if action['action_type'] == 'commit':
894 group_by_repo[action['repo_url']].append(action)
895
896 for repo_url, actions in group_by_repo.items():
897 git_root = self.code_storage.cached_git_root(repo_url)
898 revs = set(a['rev'] for a in actions)
899 metas = git_util.get_batch_commit_metadata(git_root, revs)
900 for action in actions:
901 meta = metas[action['rev']]
902 if meta is None:
903 commit_summary = '(unknown)'
904 else:
905 commit_summary = meta['message'].splitlines()[0]
906 action['commit_summary'] = commit_summary
907
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800908 def build_revlist(self, old, new):
909 """Build revlist.
910
911 Returns:
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800912 (revlist, details):
913 revlist: list of rev string
914 details: dict of rev to rev detail
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800915 """
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800916 logger.info('build_revlist: old=%s, new=%s', old, new)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800917 revlist = []
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800918 details = {}
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800919
Kuang-che Wu020a1182020-09-08 17:17:22 +0800920 # Enable cache for repetitive git operations. The space complexity is
Kuang-che Wufcbcc502020-06-01 11:48:20 +0800921 # O(number of candidates).
922 git_util.get_commit_metadata.enable_cache()
923 git_util.get_file_from_revision.enable_cache()
Kuang-che Wu98d98462020-06-19 17:07:22 +0800924 git_util.is_containing_commit.enable_cache()
Zheng-Jie Changad174a42020-06-20 15:28:10 +0800925 git_util.is_ancestor_commit.enable_cache()
Kuang-che Wufcbcc502020-06-01 11:48:20 +0800926
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800927 # step 1, find all float and fixed specs in the given range.
928 fixed_specs = self.spec_manager.collect_fixed_spec(old, new)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800929 assert fixed_specs
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800930 for spec in fixed_specs:
931 self.spec_manager.parse_spec(spec)
932
933 float_specs = self.spec_manager.collect_float_spec(old, new, fixed_specs)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +0800934 assert float_specs
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800935 while float_specs[-1].timestamp > fixed_specs[-1].timestamp:
936 float_specs.pop()
937 assert float_specs
Zheng-Jie Changd968f552020-01-16 13:31:57 +0800938 for spec in float_specs:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800939 self.spec_manager.parse_spec(spec)
940
Kuang-che Wued1bb622020-05-30 23:06:23 +0800941 git_util.fast_lookup.optimize(
942 (float_specs[0].timestamp, float_specs[-1].timestamp))
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800943 # step 2, synthesize all fixed specs in the range from float specs.
944 specs = float_specs + [fixed_specs[-1]]
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800945 action_groups = []
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800946 logger.debug('len(specs)=%d', len(specs))
947 for i in range(len(specs) - 1):
948 prev_float = specs[i]
949 next_float = specs[i + 1]
950 logger.debug('[%d], between %s (%s) and %s (%s)', i, prev_float.name,
951 prev_float.timestamp, next_float.name, next_float.timestamp)
Kuang-che Wuae6847c2020-01-13 16:06:08 +0800952 action_groups += self.generate_action_groups_between_specs(
953 prev_float, next_float)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800954
955 spec = self.synthesize_fixed_spec(float_specs[0], fixed_specs[0].timestamp)
956 synthesized = [spec.copy()]
957 for action_group in action_groups:
958 spec.apply(action_group)
959 synthesized.append(spec.copy())
960
961 # step 3, associate fixed specs with synthesized specs.
962 associated_pairs = self.associate_fixed_and_synthesized_specs(
963 fixed_specs, synthesized)
964
965 # step 4, group actions and cache them
966 for i, (fixed_index, synthesized_index) in enumerate(associated_pairs[:-1]):
967 next_fixed_index, next_synthesized_index = associated_pairs[i + 1]
968 revlist.append(fixed_specs[fixed_index].name)
969 this_action_groups = []
970
971 # handle glitch
972 if fixed_specs[fixed_index].similar_score(
973 synthesized[synthesized_index]) != 0:
974 assert synthesized[synthesized_index].is_subset(
975 fixed_specs[fixed_index])
976 skipped = set(fixed_specs[fixed_index].entries) - set(
977 synthesized[synthesized_index].entries)
978 if skipped:
979 logger.warning(
980 'between %s and %s, '
981 'bisect-kit cannot analyze commit history of following paths:',
982 fixed_specs[fixed_index].name, fixed_specs[next_fixed_index].name)
983 for path in sorted(skipped):
984 logger.warning(' %s', path)
985
986 make_up = self._create_make_up_actions(fixed_specs[fixed_index],
987 synthesized[synthesized_index])
988 if make_up:
989 this_action_groups.append(make_up)
990
991 this_action_groups.extend(
992 action_groups[synthesized_index:next_synthesized_index])
993 for idx, ag in enumerate(this_action_groups, 1):
994 rev = make_intra_rev(fixed_specs[fixed_index].name,
995 fixed_specs[next_fixed_index].name, idx)
996 ag.name = rev
997 revlist.append(rev)
Kuang-che Wu13acc7b2020-06-15 10:45:35 +0800998 details[rev] = ag.summary()
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800999
1000 self.save_action_groups_between_releases(
1001 fixed_specs[fixed_index].name, fixed_specs[next_fixed_index].name,
1002 this_action_groups)
1003 revlist.append(fixed_specs[associated_pairs[-1][0]].name)
1004
Kuang-che Wu13acc7b2020-06-15 10:45:35 +08001005 self._batch_fill_action_commit_log(details)
1006
Kuang-che Wu98d98462020-06-19 17:07:22 +08001007 # Verify all repos in between are cached.
1008 for spec in reversed(float_specs):
1009 if self.code_storage.are_spec_commits_available(spec):
1010 continue
1011 raise errors.InternalError('Some commits in %s (%s) are unavailable' %
1012 (spec.name, spec.path))
1013
Kuang-che Wued1bb622020-05-30 23:06:23 +08001014 # Disable cache because there might be write or even destructive git
1015 # operations when switch git versions. Be conservative now. We can cache
1016 # more if we observed more slow git operations later.
1017 git_util.fast_lookup.disable()
Kuang-che Wufcbcc502020-06-01 11:48:20 +08001018 git_util.get_commit_metadata.disable_cache()
1019 git_util.get_file_from_revision.disable_cache()
Kuang-che Wu98d98462020-06-19 17:07:22 +08001020 git_util.is_containing_commit.disable_cache()
Zheng-Jie Changad174a42020-06-20 15:28:10 +08001021 git_util.is_ancestor_commit.disable_cache()
Kuang-che Wu13acc7b2020-06-15 10:45:35 +08001022
1023 return revlist, details
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001024
1025 def save_action_groups_between_releases(self, old, new, action_groups):
1026 data = [ag.serialize() for ag in action_groups]
1027
1028 cache_dir = os.path.join(self.root_dir, _DIFF_CACHE_DIR)
1029 if not os.path.exists(cache_dir):
1030 os.makedirs(cache_dir)
1031 cache_filename = os.path.join(cache_dir, '%s,%s.json' % (old, new))
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001032 with open(cache_filename, 'w') as fp:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001033 json.dump(data, fp, indent=4, sort_keys=True)
1034
1035 def load_action_groups_between_releases(self, old, new):
1036 cache_dir = os.path.join(self.root_dir, _DIFF_CACHE_DIR)
1037 cache_filename = os.path.join(cache_dir, '%s,%s.json' % (old, new))
1038 if not os.path.exists(cache_filename):
Kuang-che Wud1b74152020-05-20 08:46:46 +08001039 raise errors.InternalError('cached revlist not found: %s' %
1040 cache_filename)
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001041
1042 result = []
Kuang-che Wu74bcb642020-02-20 18:45:53 +08001043 with open(cache_filename) as f:
1044 for data in json.load(f):
1045 result.append(ActionGroup.unserialize(data))
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001046
1047 return result
1048
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001049 def switch(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001050 rev_old, action_groups = self.get_intra_and_diff(rev)
1051 self.spec_manager.sync_disk_state(rev_old)
1052 apply_actions(self.code_storage, action_groups, self.root_dir)
1053
1054 def get_intra_and_diff(self, rev):
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001055 # easy case
1056 if not re.match(_re_intra_rev, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001057 return rev, []
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001058
1059 rev_old, rev_new, idx = parse_intra_rev(rev)
1060 action_groups = self.load_action_groups_between_releases(rev_old, rev_new)
1061 assert 0 <= idx <= len(action_groups)
1062 action_groups = action_groups[:idx]
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08001063 return rev_old, action_groups