Index: testing_support/git/schema.py |
diff --git a/testing_support/git/schema.py b/testing_support/git/schema.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b5e171a6eb848fe31fbad69344654b01e5b72e96 |
--- /dev/null |
+++ b/testing_support/git/schema.py |
@@ -0,0 +1,124 @@ |
+# Copyright 2014 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import collections |
+import copy |
+ |
+ |
+from testing_support.git.util import OrderedSet |
+ |
+ |
+class GitRepoSchema(object): |
+ """A declarative git testing repo. |
+ |
+ Pass a schema to __init__ in the form of: |
+ A B C D |
+ B E D |
+ |
+ This is the repo |
+ |
+ A - B - C - D |
+ \ E / |
+ |
+ Whitespace doesn't matter. Each line is a declaration of which commits come |
+ before which other commits. |
+ |
+ Every commit gets a tag 'tag_%(commit)s' |
+ Every unique terminal commit gets a branch 'branch_%(commit)s' |
+ Last commit in First line is the branch 'master' |
+ Root commits get a ref 'root_%(commit)s' |
+ |
+ Timestamps are in topo order, earlier commits (as indicated by their presence |
+ in the schema) get earlier timestamps. Stamps start at the Unix Epoch, and |
+ increment by 1 day each. |
+ """ |
+ COMMIT = collections.namedtuple('COMMIT', 'name parents is_branch is_root') |
+ |
+ def __init__(self, repo_schema='', |
+ content_fn=lambda v: {v: {'data': v}}): |
+ """Builds a new GitRepoSchema. |
+ |
+ Args: |
+ repo_schema (str) - Initial schema for this repo. See class docstring for |
+ info on the schema format. |
+ content_fn ((commit_name) -> commit_data) - A function which will be |
+ lazily called to obtain data for each commit. The results of this |
+ function are cached (i.e. it will never be called twice for the same |
+ commit_name). See the docstring on the GitRepo class for the format of |
+ the data returned by this function. |
+ """ |
+ self.master = None |
+ self.par_map = {} |
+ self.data_cache = {} |
+ self.content_fn = content_fn |
+ self.add_commits(repo_schema) |
+ |
+ def walk(self): |
+ """(Generator) Walks the repo schema from roots to tips. |
+ |
+ Generates GitRepoSchema.COMMIT objects for each commit. |
+ |
+ Throws an AssertionError if it detects a cycle. |
+ """ |
+ is_root = True |
+ par_map = copy.deepcopy(self.par_map) |
+ while par_map: |
+ empty_keys = set(k for k, v in par_map.iteritems() if not v) |
+ assert empty_keys, 'Cycle detected! %s' % par_map |
+ |
+ for k in sorted(empty_keys): |
+ yield self.COMMIT(k, self.par_map[k], |
+ not any(k in v for v in self.par_map.itervalues()), |
+ is_root) |
+ del par_map[k] |
+ for v in par_map.itervalues(): |
+ v.difference_update(empty_keys) |
+ is_root = False |
+ |
+ def add_partial(self, commit, parent=None): |
+ if commit not in self.par_map: |
+ self.par_map[commit] = OrderedSet() |
+ if parent is not None: |
+ self.par_map[commit].add(parent) |
+ |
+ def add_commits(self, schema): |
+ """Adds more commits from a schema into the existing Schema. |
+ |
+ Args: |
+ schema (str) - See class docstring for info on schema format. |
+ |
+ Throws an AssertionError if it detects a cycle. |
+ """ |
+ for commits in (l.split() for l in schema.splitlines() if l.strip()): |
+ parent = None |
+ for commit in commits: |
+ self.add_partial(commit, parent) |
+ parent = commit |
+ if parent and not self.master: |
+ self.master = parent |
+ for _ in self.walk(): # This will throw if there are any cycles. |
+ pass |
+ |
+ def data_for(self, commit): |
+ """Obtains the data for |commit|. |
+ |
+ See the docstring on the GitRepo class for the format of the returned data. |
+ |
+ Caches the result on this GitRepoSchema instance. |
+ """ |
+ if commit not in self.data_cache: |
+ self.data_cache[commit] = self.content_fn(commit) |
+ return self.data_cache[commit] |
+ |
+ def simple_graph(self): |
+ """Returns a dictionary of {commit_subject: {parent commit_subjects}} |
+ |
+ This allows you to get a very simple connection graph over the whole repo |
+ for comparison purposes. Only commit subjects (not ids, not content/data) |
+ are considered |
+ """ |
+ ret = {} |
+ for commit in self.walk(): |
+ ret.setdefault(commit.name, set()).update(commit.parents) |
+ return ret |