diff --git a/git_upstream/tests/base.py b/git_upstream/tests/base.py index 5331fd9..f7deca5 100644 --- a/git_upstream/tests/base.py +++ b/git_upstream/tests/base.py @@ -115,7 +115,7 @@ class GitRepo(fixtures.Fixture): message = message + "\n\nChange-Id: %s" % change_id self.repo.git.commit(m=message) - def add_commits(self, num=1, ref="HEAD", change_ids=None): + def add_commits(self, num=1, ref="HEAD", change_ids=[]): """Create the given number of commits using generated files""" if ref != "HEAD": self.repo.git.checkout(ref) @@ -137,3 +137,101 @@ class BaseTestCase(testtools.TestCase): repo_path = self.testrepo.path self.useFixture(DiveDir(repo_path)) self.repo = self.testrepo.repo + self.git = self.repo.git + + def _build_git_tree(self, graph_def, branches=[]): + """Helper function to build a git repository from a graph definition + of nodes and their parent nodes. A list of branches may be provided + where each element has two members corresponding to the name and the + target node it references. + + Root commits can specified by an empty list as the second member: + + ('NodeA', []) + + Merge commits are specified by multiple nodes: + + ('NodeMerge', ['Node1', 'Node2']) + + + As the import subcommand to git-upstream supports a special merge + commit that ignores all previous history from the other tree being + merged in using the 'ours' strategy. You specify this by defining + a parent node as '='. The resulting merge commit contains just + the contents of the tree from the specified parent while still + recording the parents. + + Following will result in a merge commit 'C', with parents 'P1' and + 'P2', but will have the same tree as 'P1'. + + ('C', ['=P1', 'P2']) + + + Current code requires that the graph defintion defines each node + before subsequently referencing it as a parent. + + This works: + + [('A', []), ('B', ['A']), ('C', ['B'])] + + This will not: + + [('A', []), ('C', ['B']), ('B', ['A'])] + """ + + self._graph = {} + + # first commit is special, assume root commit and repo has 1 commit + node, parents = graph_def[0] + if not parents: + assert("First commit in graph def must be a root commit") + self._graph[node] = self.repo.commit() + + # uses the fact that you can create commits in detached head mode + # and then create branches after the fact + for node, parents in graph_def[1:]: + # other root commits + if not parents: + self.git.symbolic_ref("HEAD", "refs/heads/%s" % node) + self.git.rm(".", r=True, cached=True) + self.git.clean(f=True, d=True, x=True) + self.testrepo.add_commits(1, ref="HEAD") + # only explicitly listed branches should exist afterwards + self.git.checkout(self.repo.commit()) + self.git.branch(node, D=True) + + else: + # checkout the dependent node + self.git.checkout(self._graph[parents[0]]) + + if len(parents) > 1: + # merge commits + parent_nodes = [p.strip("=") for p in parents] + commits = [str(self._graph[p]) for p in parent_nodes[1:]] + if any([True for p in parents if p.startswith("=")]): + # special merge commit using inverse of 'ours' + self.git.merge(*commits, s="ours", no_commit=True) + use = str(self._graph[ + next(p.strip("=") for p in parents + if p.startswith("="))]) + self.git.read_tree(use, u=True, reset=True) + self.git.commit(m="Merging %s into %s" % + (",".join(parent_nodes), node)) + else: + # standard merge + self.git.merge(*commits, no_edit=True) + else: + # standard commit + self.testrepo.add_commits(1, ref="HEAD") + + self._graph[node] = self.repo.commit() + + for name, node in branches: + self.git.branch(name, str(self._graph[node]), f=True) + + # return to master + self.git.checkout("master") + + def _commits_from_nodes(self, nodes=[]): + + return [self._graph[n] for n in nodes] diff --git a/git_upstream/tests/test_searchers.py b/git_upstream/tests/test_searchers.py new file mode 100644 index 0000000..455d0a7 --- /dev/null +++ b/git_upstream/tests/test_searchers.py @@ -0,0 +1,237 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from base import BaseTestCase +from git_upstream.lib.searchers import UpstreamMergeBaseSearcher + + +class TestUpstreamMergeBaseSearcher(BaseTestCase): + + def _verify_expected(self, tree, branches, expected_nodes): + self._build_git_tree(tree, branches.values()) + + searcher = UpstreamMergeBaseSearcher(pattern=branches['upstream'][0], + repo=self.repo) + + self.assertEquals(self._commits_from_nodes(reversed(expected_nodes)), + searcher.list()) + + def test_search_basic(self): + """Construct a basic repo layout and validate that locate changes + walker can find the expected changes. + + Repository layout being tested + + B master + / + A---C---D upstream/master + + """ + tree = [ + ('A', []), + ('B', ['A']), + ('C', ['A']), + ('D', ['C']) + ] + + branches = { + 'head': ('master', 'B'), + 'upstream': ('upstream/master', 'D'), + } + + expected_changes = ["B"] + self._verify_expected(tree, branches, expected_changes) + + def test_search_additional_branch(self): + """Construct a repo layout where previously an additional branch has + been included and validate that locate changes walker can find the + expected changes + + Repository layout being tested + + B example/packaging + \ + C---D---E master + / + A---F---G upstream/master + + """ + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']) + ] + + branches = { + 'head': ('master', 'E'), + 'upstream': ('upstream/master', 'G'), + } + + expected_changes = ["C", "D", "E"] + self._verify_expected(tree, branches, expected_changes) + + def test_search_additional_branch_multiple_imports(self): + """Construct a repo layout where previously an additional branch has + been included and validate that locate changes walker can find the + right changes after an additional import + + Repository layout being tested + + B-- example/packaging + \ \ + C---D---E-------I---J---K master + / \ / + / --H---D1--E1 import/next + / / + A---F---G---L---M upstream/master + + """ + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']), + ('H', ['G', 'B']), + ('D1', ['H']), + ('E1', ['D1']), + ('I', ['E', '=E1']), + ('J', ['I']), + ('K', ['J']), + ('L', ['G']), + ('M', ['L']) + ] + + branches = { + 'head': ('master', 'K'), + 'upstream': ('upstream/master', 'M'), + } + + expected_changes = ["H", "D1", "E1", "I", "J", "K"] + self._verify_expected(tree, branches, expected_changes) + + def test_search_changes_upload_prior_to_import(self): + """Construct a repo layout where using a complex layout involving + additional branches having been included, and a previous import from + upstream having been completed, test that if a change was created on + another branch before the previous import was created, and merged to + the target branch after the previous import, can we find it correctly. + i.e. will the strategy also include commit 'O' in the diagram below. + + Repository layout being tested + + B-- + \ \ + \ \ O---------------- + \ \ / \ + C---D---E-------I---J---K---P---Q master + / \ / + / --H---D1--E1 + / / + A---F---G---L---M upstream/master + + """ + + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']), + ('H', ['G', 'B']), + ('D1', ['H']), + ('E1', ['D1']), + ('I', ['E', '=E1']), + ('J', ['I']), + ('K', ['J']), + ('L', ['G']), + ('M', ['L']), + ('O', ['E']), + ('P', ['K', 'O']), + ('Q', ['P']) + ] + + branches = { + 'head': ('master', 'Q'), + 'upstream': ('upstream/master', 'M'), + } + + expected_changes = ["H", "D1", "E1", "I", "J", "K", "O", "P", "Q"] + self.expectFailure( + "Should fail to find change 'O'", + self._verify_expected, tree, branches, expected_changes) + + def test_search_multi_changes_upload_prior_to_import(self): + """Construct a repo layout where using a complex layout involving + additional branches having been included, and a previous import from + upstream having been completed, test that if a change was created on + another branch before the previous import was created, and merged to + the target branch after the previous import, can we find it correctly. + i.e. will the strategy also include commit 'O' in the diagram below. + + Repository layout being tested + + B-- L------------- + \ \ / \ + \ \ / J--------- \ + \ \ / / \ \ + C---D---E-------I---K---M---O---P master + / \ / + / --H---D1--E1 + / / + A---F---G---Q---R upstream/master + + """ + + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']), + ('H', ['G', 'B']), + ('D1', ['H']), + ('E1', ['D1']), + ('I', ['E', '=E1']), + ('J', ['E']), + ('K', ['I', 'J']), + ('L', ['D']), + ('M', ['K', 'L']), + ('O', ['M']), + ('P', ['O']), + ('Q', ['G']), + ('R', ['Q']) + ] + + branches = { + 'head': ('master', 'P'), + 'upstream': ('upstream/master', 'R'), + } + + expected_changes = ["H", "D1", "E1", "I", "J", "K", "L", "M", "O", "P"] + self.expectFailure( + "Should fail to find changes 'J' and 'L'", + self._verify_expected, tree, branches, expected_changes) diff --git a/git_upstream/tests/test_strategies.py b/git_upstream/tests/test_strategies.py new file mode 100644 index 0000000..efae8a0 --- /dev/null +++ b/git_upstream/tests/test_strategies.py @@ -0,0 +1,187 @@ +# +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from base import BaseTestCase + +import_command = __import__("git_upstream.commands.import", globals(), + locals(), ['LocateChangesWalk'], -1) +LocateChangesWalk = import_command.LocateChangesWalk + + +class TestStrategies(BaseTestCase): + + def _verify_expected(self, tree, branches, expected_nodes): + self._build_git_tree(tree, branches.values()) + + strategy = LocateChangesWalk(branch=branches['head'][0], + search_ref=branches['upstream'][0]) + + self.assertEquals(self._commits_from_nodes(expected_nodes), + [c for c in strategy.filtered_iter()]) + + def test_locate_changes_walk_basic(self): + """Construct a basic repo layout and validate that locate changes + walker can find the expected changes. + + Repository layout being tested + + B master + / + A---C---D upstream/master + + """ + tree = [ + ('A', []), + ('B', ['A']), + ('C', ['A']), + ('D', ['C']) + ] + + branches = { + 'head': ('master', 'B'), + 'upstream': ('upstream/master', 'D'), + } + + expected_changes = ["B"] + self._verify_expected(tree, branches, expected_changes) + + def test_locate_changes_walk_additional_branch(self): + """Construct a repo layout where previously an additional branch has + been included and validate that locate changes walker can find the + expected changes + + Repository layout being tested + + B example/packaging + \ + C---D---E master + / + A---F---G upstream/master + + """ + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']) + ] + + branches = { + 'head': ('master', 'E'), + 'upstream': ('upstream/master', 'G'), + } + + expected_changes = ["D", "E"] + self._verify_expected(tree, branches, expected_changes) + + def test_locate_changes_walk_additional_branch_multiple_imports(self): + """Construct a repo layout where previously an additional branch has + been included and validate that locate changes walker can find the + right changes after an additional import + + Repository layout being tested + + B-- example/packaging + \ \ + C---D---E-------I---J---K master + / \ / + / --H---D1--E1 import/next + / / + A---F---G---L---M upstream/master + + """ + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']), + ('H', ['G', 'B']), + ('D1', ['H']), + ('E1', ['D1']), + ('I', ['E', '=E1']), + ('J', ['I']), + ('K', ['J']), + ('L', ['G']), + ('M', ['L']) + ] + + branches = { + 'head': ('master', 'K'), + 'upstream': ('upstream/master', 'M'), + } + + expected_changes = ["D1", "E1", "J", "K"] + self._verify_expected(tree, branches, expected_changes) + + def test_locate_changes_walk_changes_prior_to_import(self): + """Construct a repo layout where using a complex layout involving + additional branches having been included, and a previous import from + upstream having been completed, test that if a change was created on + another branch before the previous import was created, and merged to + the target branch after the previous import, can we find it correctly. + i.e. will the strategy also include commit 'O' in the diagram below. + + Repository layout being tested + + B-- example/packaging + \ \ + \ \ O---------------- + \ \ / \ + C---D---E-------I---J---K---P---Q master + / \ / + / --H---D1--E1 import/next + / / + A---F---G---L---M upstream/master + + """ + + tree = [ + ('A', []), + ('B', []), + ('C', ['A', 'B']), + ('D', ['C']), + ('E', ['D']), + ('F', ['A']), + ('G', ['F']), + ('H', ['G', 'B']), + ('D1', ['H']), + ('E1', ['D1']), + ('I', ['E', '=E1']), + ('J', ['I']), + ('K', ['J']), + ('L', ['G']), + ('M', ['L']), + ('O', ['E']), + ('P', ['K', 'O']), + ('Q', ['P']) + ] + + branches = { + 'head': ('master', 'Q'), + 'upstream': ('upstream/master', 'M'), + } + + expected_changes = ["D1", "E1", "J", "K", "O", "Q"] + self.expectFailure( + "Should fail to find change 'O'", + self._verify_expected, tree, branches, expected_changes)