summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <daniel.silverstone@codethink.co.uk>2012-10-03 14:49:46 +0100
committerDaniel Silverstone <daniel.silverstone@codethink.co.uk>2012-10-03 14:49:46 +0100
commita4a66fce7c6086f25b4071d5c33f07d5badf8181 (patch)
treececfdd96a67fe39ad0ad36e9a1bafb3b429250b4
parent3b592b24c6e1afd6fc1fdadea76093b5e1f6231c (diff)
parent47ccbd4d3d7a2bbfa3c840f442f00528c88f5a45 (diff)
downloadmorph-a4a66fce7c6086f25b4071d5c33f07d5badf8181.tar.gz
Merge remote-tracking branch 'origin/samthursfield/ref-access-fixes'
-rwxr-xr-xmorph5
-rw-r--r--morphlib/cachedrepo.py21
-rw-r--r--morphlib/cachedrepo_tests.py14
-rw-r--r--morphlib/git.py13
-rw-r--r--morphlib/plugins/branch_and_merge_plugin.py385
-rwxr-xr-xtests.branching/ambiguous-refs.script49
-rw-r--r--tests.branching/merge-conflict-chunks.stderr7
-rw-r--r--tests.branching/merge-conflict-chunks.stdout14
-rw-r--r--tests.branching/merge-conflict-stratum.stderr2
-rw-r--r--tests.branching/merge-conflict-stratum.stdout8
-rw-r--r--tests.branching/merge-handles-unmergable-cases.stderr2
-rw-r--r--tests.branching/merge-with-stratum-renamed.stderr2
12 files changed, 257 insertions, 265 deletions
diff --git a/morph b/morph
index f55bce96..0c0789d5 100755
--- a/morph
+++ b/morph
@@ -17,8 +17,5 @@
import morphlib
-import os.path
-app = morphlib.app.Morph(version=morphlib.__version__)
-app.__file__ = os.path.realpath(__file__)
-app.run()
+morphlib.app.Morph(version=morphlib.__version__).run()
diff --git a/morphlib/cachedrepo.py b/morphlib/cachedrepo.py
index 61526cd7..f0ca627b 100644
--- a/morphlib/cachedrepo.py
+++ b/morphlib/cachedrepo.py
@@ -103,12 +103,6 @@ class CachedRepo(object):
self.url = url
self.path = path
- def is_valid_sha1(self, ref):
- '''Checks whether a string is a valid SHA1.'''
-
- valid_chars = 'abcdefABCDEF0123456789'
- return len(ref) == 40 and all([x in valid_chars for x in ref])
-
def resolve_ref(self, ref):
'''Attempts to resolve a ref into its SHA1 and tree SHA1.
@@ -117,7 +111,7 @@ class CachedRepo(object):
'''
- if not self.is_valid_sha1(ref):
+ if not morphlib.git.is_valid_sha1(ref):
try:
refs = self._show_ref(ref).split('\n')
refs = [x.split() for x in refs]
@@ -143,7 +137,7 @@ class CachedRepo(object):
'''
- if not self.is_valid_sha1(ref):
+ if not morphlib.git.is_valid_sha1(ref):
raise UnresolvedNamedReferenceError(self, ref)
try:
sha1 = self._rev_list(ref).strip()
@@ -194,6 +188,15 @@ class CachedRepo(object):
self._checkout_ref(ref, target_dir)
+ def load_morphology(self, ref, name):
+ '''Loads a morphology from a given ref'''
+
+ if not morphlib.git.is_valid_sha1(ref):
+ ref = self._rev_list(ref).strip()
+ text = self.cat(ref, '%s.morph' % name)
+ morphology = morphlib.morph2.Morphology(text)
+ return morphology
+
def ls_tree(self, ref):
'''Return file names found in root tree. Does not recurse to subtrees.
@@ -203,7 +206,7 @@ class CachedRepo(object):
'''
- if not self.is_valid_sha1(ref):
+ if not morphlib.git.is_valid_sha1(ref):
raise UnresolvedNamedReferenceError(self, ref)
try:
sha1 = self._rev_list(ref).strip()
diff --git a/morphlib/cachedrepo_tests.py b/morphlib/cachedrepo_tests.py
index 0a87b32a..0ca0882f 100644
--- a/morphlib/cachedrepo_tests.py
+++ b/morphlib/cachedrepo_tests.py
@@ -26,6 +26,11 @@ from morphlib import cachedrepo
class CachedRepoTests(unittest.TestCase):
+ EXAMPLE_MORPH = '''{
+ "name": "foo",
+ "kind": "chunk"
+ }'''
+
def show_ref(self, ref):
output = {
'master':
@@ -57,6 +62,7 @@ class CachedRepoTests(unittest.TestCase):
def rev_list(self, ref):
output = {
+ 'master': 'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9',
'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9':
'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9',
'a4da32f5a81c8bc6d660404724cedc3bc0914a75':
@@ -70,7 +76,7 @@ class CachedRepoTests(unittest.TestCase):
def cat_file(self, ref, filename):
output = {
'e28a23812eadf2fce6583b8819b9c5dbd36b9fb9:foo.morph':
- 'contents of foo.morph'
+ self.EXAMPLE_MORPH
}
try:
return output['%s:%s' % (ref, filename)]
@@ -165,7 +171,7 @@ class CachedRepoTests(unittest.TestCase):
def test_cat_existing_file_in_existing_ref(self):
data = self.repo.cat('e28a23812eadf2fce6583b8819b9c5dbd36b9fb9',
'foo.morph')
- self.assertEqual(data, 'contents of foo.morph')
+ self.assertEqual(data, self.EXAMPLE_MORPH)
def test_fail_cat_file_in_invalid_ref(self):
self.assertRaises(cachedrepo.InvalidReferenceError, self.repo.cat,
@@ -216,6 +222,10 @@ class CachedRepoTests(unittest.TestCase):
morph_filename = os.path.join(unpack_dir, 'foo.morph')
self.assertTrue(os.path.exists(morph_filename))
+ def test_load_morphology_from_existing_ref(self):
+ morph = self.repo.load_morphology('master', 'foo')
+ self.assertTrue(morph['name'] == 'foo')
+
def test_ls_tree_in_existing_ref(self):
data = self.repo.ls_tree('e28a23812eadf2fce6583b8819b9c5dbd36b9fb9')
self.assertEqual(data, ['foo.morph'])
diff --git a/morphlib/git.py b/morphlib/git.py
index 5862ef9b..b3dd2c45 100644
--- a/morphlib/git.py
+++ b/morphlib/git.py
@@ -221,7 +221,12 @@ def clone_into(runcmd, srcpath, targetpath, ref=None):
else:
runcmd(['git', 'clone', '-b', ref, srcpath, targetpath])
-def find_first_ref(runcmd, gitdir, ref):
- '''Find the *first* ref match and returns its sha1.'''
- return runcmd(['git', 'show-ref', ref],
- cwd=gitdir).split("\n")[0].split(" ")[0]
+def is_valid_sha1(ref):
+ '''Checks whether a string is a valid SHA1.'''
+
+ valid_chars = 'abcdefABCDEF0123456789'
+ return len(ref) == 40 and all([x in valid_chars for x in ref])
+
+def rev_parse(runcmd, gitdir, ref):
+ '''Find the sha1 for the given ref'''
+ return runcmd(['git', 'rev-parse', ref], cwd=gitdir)[0:40]
diff --git a/morphlib/plugins/branch_and_merge_plugin.py b/morphlib/plugins/branch_and_merge_plugin.py
index 279a9948..a3faa0c2 100644
--- a/morphlib/plugins/branch_and_merge_plugin.py
+++ b/morphlib/plugins/branch_and_merge_plugin.py
@@ -57,8 +57,6 @@ class BranchAndMergePlugin(cliapp.Plugin):
arg_synopsis='SYSTEM')
self.app.add_subcommand('foreach', self.foreach,
arg_synopsis='COMMAND')
- # This command should be hidden, once cliapp supports such a thing
- self.app.add_subcommand('merge-morphology', self.merge_morphology)
def disable(self):
pass
@@ -160,6 +158,20 @@ class BranchAndMergePlugin(cliapp.Plugin):
changes.append(paths.split()[0])
return changes
+ def get_unmerged_changes(self, repo_dir, env={}):
+ '''Identifies files which have unresolved merge conflicts'''
+
+ # The second column of the git command output is set either if the
+ # file has changes in the working tree or if it has conflicts.
+ status = self.app.runcmd(['git', 'status', '--porcelain'],
+ cwd=repo_dir, env=env)
+ changes = []
+ for change in status.strip().splitlines():
+ xy, paths = change[0:2], change[2:].strip()
+ if xy[1] != ' ' and xy != '??':
+ changes.append(paths.split()[0])
+ return changes
+
def resolve_ref(self, repodir, ref):
try:
return self.app.runcmd(['git', 'show-ref', ref],
@@ -174,6 +186,21 @@ class BranchAndMergePlugin(cliapp.Plugin):
self.app.settings['repo-alias'])
return resolver.pull_url(reponame)
+ def get_cached_repo(self, repo_name):
+ '''Return CachedRepo object from the local repository cache
+
+ Repo is cached and updated if necessary. The cache itself has a
+ mechanism in place to avoid multiple updates per Morph invocation.
+ '''
+
+ self.app.status(msg='Updating git repository %s in cache' % repo_name)
+ if not self.app.settings['no-git-update']:
+ repo = self.lrc.cache_repo(repo_name)
+ repo.update()
+ else:
+ repo = self.lrc.get_repo(repo_name)
+ return repo
+
def clone_to_directory(self, dirname, reponame, ref):
'''Clone a repository below a directory.
@@ -182,16 +209,9 @@ class BranchAndMergePlugin(cliapp.Plugin):
'''
# Setup.
- cache = morphlib.util.new_repo_caches(self.app)[0]
resolver = morphlib.repoaliasresolver.RepoAliasResolver(
self.app.settings['repo-alias'])
-
- # Get the repository into the cache; make sure it is up to date.
- self.app.status(msg='Updating git repository %(reponame)s in cache',
- reponame=reponame)
- repo = cache.cache_repo(reponame)
- if not self.app.settings['no-git-update']:
- repo.update()
+ repo = self.get_cached_repo(reponame)
# Make sure the parent directories needed for the repo dir exist.
parent_dir = os.path.dirname(dirname)
@@ -221,12 +241,22 @@ class BranchAndMergePlugin(cliapp.Plugin):
self.app.runcmd(['git', 'remote', 'update'], cwd=dirname)
def load_morphology(self, repo_dir, name, ref=None):
+ '''Loads a morphology from a repo in a system branch
+
+ If 'ref' is specified, the version is taken from there instead of the
+ working tree. Note that you shouldn't use this to fetch files on
+ branches other than the current system branch, because the remote in
+ the system branch repo may be completely out of date. Use the local
+ repository cache instead for this.
+ '''
+
if ref is None:
filename = os.path.join(repo_dir, '%s.morph' % name)
with open(filename) as f:
text = f.read()
else:
- ref = morphlib.git.find_first_ref(self.app.runcmd, repo_dir, ref)
+ if not morphlib.git.is_valid_sha1(ref):
+ ref = morphlib.git.rev_parse(self.app.runcmd, repo_dir, ref)
try:
text = self.app.runcmd(['git', 'cat-file', 'blob',
'%s:%s.morph' % (ref, name)],
@@ -389,6 +419,7 @@ class BranchAndMergePlugin(cliapp.Plugin):
workspace = self.deduce_workspace()
branch_dir = os.path.join(workspace, new_branch)
os.makedirs(branch_dir)
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
try:
# Create a .morph-system-branch directory to clearly identify
@@ -434,6 +465,7 @@ class BranchAndMergePlugin(cliapp.Plugin):
workspace = self.deduce_workspace()
branch_dir = os.path.join(workspace, system_branch)
os.makedirs(branch_dir)
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
try:
# Create a .morph-system-branch directory to clearly identify
@@ -504,30 +536,29 @@ class BranchAndMergePlugin(cliapp.Plugin):
(' '.join(command), repo, error))
return repo_dir
- def edit_stratum(self, system_branch, branch_dir, branch_root_dir,
- stratum):
- # Make the stratum repository and the ref available locally.
- stratum_repo_dir = self.checkout_repository(
- branch_dir, stratum['repo'], system_branch,
- parent_ref=stratum['ref'])
-
- # Check if we need to change anything at all.
- if stratum['ref'] != system_branch:
- # If the stratum is in the same repository as the system,
- # copy its morphology from its source ref into the system branch.
- if branch_root_dir == stratum_repo_dir:
- stratum_morphology = self.load_morphology(
- branch_root_dir, stratum['morph'], ref=stratum['ref'])
- self.save_morphology(
- branch_root_dir, stratum['morph'], stratum_morphology)
+ def edit_stratum(self, system_branch, branch_dir, branch_root,
+ branch_root_dir, stratum):
+ if stratum['repo'] == branch_root:
+ stratum_repo_dir = branch_root_dir
+ if stratum['ref'] != system_branch:
+ # We need to bring the morphology forwards from its ref to the
+ # current HEAD
+ repo = self.lrc.get_repo(branch_root)
+ stratum_morphology = repo.load_morphology(
+ stratum['ref'], stratum['morph'])
+ self.save_morphology(branch_root_dir,
+ stratum['morph'], stratum_morphology)
self.log_change(
stratum['repo'],
'"%s" copied from "%s" to "%s"' %
(stratum['morph'], stratum['ref'], system_branch))
+ else:
+ # Make sure the stratum repository is available
+ stratum_repo_dir = self.checkout_repository(
+ branch_dir, stratum['repo'], system_branch,
+ parent_ref=stratum['ref'])
- # Update the reference to the stratum in the system morphology.
- stratum['ref'] = system_branch
-
+ stratum['ref'] = system_branch
return stratum_repo_dir
def edit_chunk(self, system_branch, branch_dir, stratum_repo_dir, chunk):
@@ -564,13 +595,15 @@ class BranchAndMergePlugin(cliapp.Plugin):
stratum_name = args[1]
chunk_name = args[2] if len(args) > 2 else None
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
+
# Load the system morphology and find out which repo and ref
# we need to edit the stratum.
system_morphology = self.load_morphology(branch_root_dir, system_name)
stratum = self.get_edit_info(system_name, system_morphology,
stratum_name, collection='strata')
stratum_repo_dir = self.edit_stratum(
- system_branch, branch_dir, branch_root_dir, stratum)
+ system_branch, branch_dir, branch_root, branch_root_dir, stratum)
self.save_morphology(branch_root_dir, system_name,
system_morphology)
self.log_change(branch_root,
@@ -615,7 +648,7 @@ class BranchAndMergePlugin(cliapp.Plugin):
root_repo = self.get_branch_config(branch_path, 'branch.root')
root_repo_dir = self.find_repository(branch_path, root_repo)
- lrc, rrc = morphlib.util.new_repo_caches(self.app)
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
for f in glob.glob(os.path.join(root_repo_dir, '*.morph')):
name = os.path.basename(f)[:-len('.morph')]
@@ -625,14 +658,16 @@ class BranchAndMergePlugin(cliapp.Plugin):
for stratum_info in morphology['strata']:
repo_dir = self.edit_stratum(
- branch, branch_path, root_repo_dir, stratum_info)
+ branch, branch_path, root_repo, root_repo_dir,
+ stratum_info)
stratum = self.load_morphology(repo_dir, stratum_info['morph'])
for chunk_info in stratum['chunks']:
if 'unpetrify-ref' not in chunk_info:
commit_sha1, tree_sha1 = self.app.resolve_ref(
- lrc, rrc, chunk_info['repo'], chunk_info['ref'],
+ self.lrc, self.rrc, chunk_info['repo'],
+ chunk_info['ref'],
update=not self.app.settings['no-git-update'])
chunk_info['unpetrify-ref'] = chunk_info['ref']
chunk_info['ref'] = commit_sha1
@@ -658,6 +693,8 @@ class BranchAndMergePlugin(cliapp.Plugin):
if len(args) != 0:
raise cliapp.AppException('morph unpetrify takes no arguments')
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
+
workspace = self.deduce_workspace()
branch, branch_path = self.deduce_system_branch()
root_repo = self.get_branch_config(branch_path, 'branch.root')
@@ -671,7 +708,8 @@ class BranchAndMergePlugin(cliapp.Plugin):
for stratum_info in morphology['strata']:
repo_dir = self.edit_stratum(
- branch, branch_path, root_repo_dir, stratum_info)
+ branch, branch_path, root_repo, root_repo_dir,
+ stratum_info)
stratum = self.load_morphology(repo_dir, stratum_info['morph'])
@@ -686,46 +724,16 @@ class BranchAndMergePlugin(cliapp.Plugin):
self.print_changelog('The following changes were made but have not '
'been comitted')
- def load_morphology_pair(self, repo_dir, ref, name):
- '''Load two versions of a morphology and check for major conflicts
-
- Returns the version at 'ref' (if it exists) and the on-disk version.
+ # When 'merge' is unset, git doesn't try to resolve conflicts itself in
+ # those files.
+ MERGE_ATTRIBUTE = '*.morph\t-merge\n'
- '''
-
- new = self.load_morphology(repo_dir, name)
- try:
- old = self.load_morphology(repo_dir, name, ref=ref)
- except cliapp.AppException as e:
- return None, new
-
- if old['name'] != new['name']:
- # We should enforce this in validation during load_morphology()
- # rather than having to check it here
- raise cliapp.AppException(
- 'merge confict: "name" of morphology %s (name should '
- 'always match filename)' % name)
- if old['kind'] != new['kind']:
- raise cliapp.AppException(
- 'merge conflict: "kind" of morphology %s' % name)
-
- return old, new
-
- def configure_merge_driver(self, repo_dir):
- self.set_repo_config(repo_dir, 'merge.morph.name',
- 'Morphology merge driver')
- self.set_repo_config(repo_dir, 'merge.morph.driver',
- '%s merge-morphology $MORPH_FROM_BRANCH $MORPH_TO_BRANCH '
- '%%O %%A %%B' % self.app.__file__)
-
- MERGE_ATTRIBUTE = '*.morph\tmerge=morph\n'
-
- def enable_merge_driver(self, repo_dir):
+ def disable_morph_merging(self, repo_dir):
attributes_file = os.path.join(repo_dir, ".git", "info", "attributes")
with open(attributes_file, 'a') as f:
f.write(self.MERGE_ATTRIBUTE)
- def disable_merge_driver(self, repo_dir):
+ def enable_morph_merging(self, repo_dir):
attributes_file = os.path.join(repo_dir, ".git", "info", "attributes")
with open(attributes_file, 'r') as f:
attributes = f.read()
@@ -735,38 +743,46 @@ class BranchAndMergePlugin(cliapp.Plugin):
with morphlib.savefile.SaveFile(attributes_file, 'w') as f:
f.write(attributes[:-len(self.MERGE_ATTRIBUTE)])
- def merge_repo(self, dirty_repos, failed_repos,
- from_branch_dir, from_repo, from_ref,
- to_branch_dir, to_repo, to_ref):
- '''Merge changes for a system branch in a specific repository
+ def get_merge_files(self, repo_dir, from_sha1, to_ref, name):
+ '''Returns merge base, remote and local versions of a morphology
+
+ We already ran 'git fetch', so the remote branch is available within
+ the target repository.
+
+ '''
+
+ base_sha1 = self.app.runcmd(['git', 'merge-base', from_sha1, to_ref],
+ cwd=repo_dir).strip()
+ base_morph = self.load_morphology(repo_dir, name, ref=base_sha1)
+ from_morph = self.load_morphology(repo_dir, name, ref=from_sha1)
+ to_morph = self.load_morphology(repo_dir, name, ref=to_ref)
- This done using a standard 'git pull', with a custom merge driver to
- handle the .morph files.
+ if from_morph['name'] != to_morph['name']:
+ # We should enforce name == filename in load_morphology()
+ raise cliapp.AppException(
+ 'merge conflict: "name" of morphology %s (name should always '
+ 'match filename)' % name)
+ if from_morph['kind'] != to_morph['kind']:
+ raise cliapp.AppException(
+ 'merge conflict: "kind" of morphology %s changed from %s to %s'
+ % (name, from_morph['kind'], to_morph['kind']))
+
+ return base_morph, from_morph, to_morph
- The merge driver is only invoked when there is a three-way merge
- conflict on a file. If the file has changed in the FROM branch but not
- in the TO branch (relative to the base version) git will choose the
- FROM version without considering its contents.
+ def merge_repo(self, merged_repos, from_branch_dir, from_repo, from_ref,
+ to_branch_dir, to_repo, to_ref):
+ '''Merge changes for a system branch in a specific repository
- Since git may automatically update morphologies in TO to use the refs
- out of FROM, the merge driver does the same, and we then reset them
- back and write the morphologies out again after recursively merging
- the components. It's important that we disable the merge driver again
- after the pull, or we risk confusing later users of the same repo who
- do a manual git merge.
+ We disable merging for morphologies and do this manually later on.
'''
- if to_repo in failed_repos:
- return None
+ if to_repo in merged_repos:
+ return merged_repos[to_repo]
from_repo_dir = self.find_repository(from_branch_dir, from_repo)
to_repo_dir = self.checkout_repository(to_branch_dir, to_repo, to_ref)
- if to_repo_dir in dirty_repos:
- return to_repo_dir
- dirty_repos.add(to_repo_dir)
-
if self.get_uncommitted_changes(from_repo_dir) != []:
raise cliapp.AppException('repository %s has uncommitted '
'changes' % from_repo)
@@ -774,41 +790,42 @@ class BranchAndMergePlugin(cliapp.Plugin):
raise cliapp.AppException('repository %s has uncommitted '
'changes' % to_repo)
- self.configure_merge_driver(to_repo_dir)
- self.enable_merge_driver(to_repo_dir)
- env = dict(os.environ)
- env['MORPH_FROM_BRANCH'] = from_ref
- env['MORPH_TO_BRANCH'] = to_ref
- # ':' in pathnames confuses git, so we have to pass it a URL
- from_url = urlparse.urljoin('file://', from_repo_dir)
+ # Fetch the local FROM branch; its sha1 will be stored in FETCH_HEAD.
+ # ':' in pathnames confuses git, so we have to pass it a URL.
+ from_repo_url = urlparse.urljoin('file://', from_repo_dir)
+ self.app.runcmd(['git', 'fetch', from_repo_url, from_ref],
+ cwd=to_repo_dir)
+
+ # Merge everything but the morphologies; error output is ignored (it's
+ # not very good) and instead we report conflicts manually later on.
+ self.disable_morph_merging(to_repo_dir)
+ with open(os.path.join(to_repo_dir, '.git', 'FETCH_HEAD')) as f:
+ from_sha1 = f.read(40)
status, output, error = self.app.runcmd_unchecked(
- ['git', 'pull', '--quiet', '--no-commit', '--no-ff',
- from_url, from_ref], cwd=to_repo_dir, env=env)
- self.disable_merge_driver(to_repo_dir)
-
- if status != 0:
- self.app.output.write(
- 'Merge errors encountered in %s, branch %s:\n%s'
- % (to_repo, to_ref, output))
+ ['git', 'merge', '--no-commit', '--no-ff', from_sha1],
+ cwd=to_repo_dir)
+ self.enable_morph_merging(to_repo_dir)
- if status != 0:
- self.app.output.write('\n')
- failed_repos.add(to_repo)
- return None
- return to_repo_dir
+ merged_repos[to_repo] = (to_repo_dir, from_sha1)
+ return (to_repo_dir, from_sha1)
def merge(self, args):
- '''Pull and merge changes from a system branch into the current one.'''
+ '''Pull and merge changes from a system branch into the current one.
+
+ The remote branch is pulled from the current workspace into the target
+ repositories (so any local commits are included).
+
+ '''
if len(args) != 1:
raise cliapp.AppException('morph merge requires a system branch '
'name as its argument')
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
workspace = self.deduce_workspace()
from_branch = args[0]
from_branch_dir = self.find_system_branch(workspace, from_branch)
to_branch, to_branch_dir = self.deduce_system_branch()
-
if from_branch_dir is None:
raise cliapp.AppException('branch %s must be checked out before '
'it can be merged' % from_branch)
@@ -821,26 +838,22 @@ class BranchAndMergePlugin(cliapp.Plugin):
(root_repo, other_root_repo))
def merge_chunk(old_ci, ci):
- self.merge_repo(
- dirty_repo_dirs, failed_repos,
+ self.merge_repo(merged_repos,
from_branch_dir, old_ci['repo'], from_branch,
to_branch_dir, ci['repo'], ci['ref'])
def merge_stratum(old_si, si):
- to_repo_dir = self.merge_repo(
- dirty_repo_dirs, failed_repos,
+ to_repo_dir, from_sha1 = self.merge_repo(merged_repos,
from_branch_dir, old_si['repo'], from_branch,
to_branch_dir, si['repo'], si['ref'])
- if to_repo_dir is None:
- return
+ base_morph, from_morph, to_morph = self.get_merge_files(
+ to_repo_dir, from_sha1, si['ref'], si['morph'])
- old_stratum, stratum = self.load_morphology_pair(
- to_repo_dir, old_si['ref'], si['morph'])
changed = False
- edited_chunks = [ci for ci in stratum['chunks']
+ edited_chunks = [ci for ci in from_morph['chunks']
if ci['ref'] == from_branch]
for ci in edited_chunks:
- for old_ci in old_stratum['chunks']:
+ for old_ci in to_morph['chunks']:
if old_ci['repo'] == ci['repo']:
break
else:
@@ -852,22 +865,21 @@ class BranchAndMergePlugin(cliapp.Plugin):
ci['ref'] = old_ci['ref']
merge_chunk(old_ci, ci)
if changed:
- self.save_morphology(to_repo_dir, si['morph'], stratum)
+ self.save_morphology(to_repo_dir, si['morph'], to_morph)
self.app.runcmd(['git', 'add', si['morph'] + '.morph'],
cwd=to_repo_dir)
def merge_system(name):
- old_morphology, morphology = self.load_morphology_pair(
- to_root_dir, to_branch, name)
-
- if morphology['kind'] != 'system':
+ base_morph, from_morph, to_morph = self.get_merge_files(
+ to_root_dir, from_sha1, to_branch, name)
+ if to_morph['kind'] != 'system':
return
changed = False
- edited_strata = [si for si in morphology['strata']
+ edited_strata = [si for si in from_morph['strata']
if si['ref'] == from_branch]
for si in edited_strata:
- for old_si in old_morphology['strata']:
+ for old_si in to_morph['strata']:
# We make no attempt at rename / move detection
if old_si['morph'] == si['morph'] \
and old_si['repo'] == si['repo']:
@@ -881,114 +893,45 @@ class BranchAndMergePlugin(cliapp.Plugin):
si['ref'] = old_si['ref']
merge_stratum(old_si, si)
if changed:
- self.save_morphology(to_root_dir, name, morphology)
+ self.save_morphology(to_root_dir, name, to_morph)
self.app.runcmd(['git', 'add', f], cwd=to_root_dir)
- dirty_repo_dirs = set()
- failed_repos = set()
-
+ merged_repos = {}
try:
- to_root_dir = self.merge_repo(
- dirty_repo_dirs, failed_repos,
+ to_root_dir, from_sha1 = self.merge_repo(merged_repos,
from_branch_dir, root_repo, from_branch,
to_branch_dir, root_repo, to_branch)
- if to_root_dir is None:
- raise cliapp.AppException(
- 'Merging failed in %s: please \'git merge\' the latest %s '
- 'into %s in the \'%s\' system branch.' %
- (root_repo, to_branch, from_branch, from_branch))
for f in glob.glob(os.path.join(to_root_dir, '*.morph')):
name = os.path.basename(f)[:-len('.morph')]
merge_system(name)
- if len(failed_repos) > 0:
- raise cliapp.AppException(
- 'merge errors were encountered in the following %s:\n\n'
- '\t%s\n\nPlease manually merge the target ref into %s in '
- 'each case in the %s system branch, and then repeat the '
- 'system branch merge.' %
- ('repository' if len(failed_repos)==1 else 'repositories',
- '\n\t'.join(failed_repos), from_branch, from_branch))
-
- for repo_dir in dirty_repo_dirs:
- # Repo will often turn out to not be dirty: if the changes we
- # merged only updated refs to the system branch, we will have
- # changed them back again so that the index will now be empty.
- if morphlib.git.index_has_changes(self.app.runcmd, repo_dir):
+ success = True
+ for repo_name, repo_info in merged_repos.iteritems():
+ repo_dir = repo_info[0]
+ conflicts = self.get_unmerged_changes(repo_dir)
+ if len(conflicts) > 0:
+ self.app.output.write("Merge conflicts in %s:\n\t%s\n" %
+ (repo_name, '\n\t'.join(conflicts)))
+ success = False
+ elif morphlib.git.index_has_changes(self.app.runcmd, repo_dir):
+ # Repo may not be dirty if the changes only touched refs,
+ # because they may now match the previous state.
msg = "Merge system branch '%s'" % from_branch
self.app.runcmd(['git', 'commit', '--all', '-m%s' % msg],
cwd=repo_dir)
-
+ if not success:
+ raise cliapp.AppException(
+ "merge errors were encountered. Please manually merge the "
+ "target ref into %s in the remote system branch in each "
+ "case, and then repeat the 'morph merge' operation." %
+ from_branch)
self.app.status(msg="Merge successful")
except:
- for repo_dir in dirty_repo_dirs:
+ for repo_dir, sha1 in merged_repos.itervalues():
self.reset_work_tree_safe(repo_dir)
raise
- def merge_morphology_contents(self, from_branch, from_morph, to_morph):
- if from_morph['kind'] != to_morph['kind'] or \
- from_morph['name'] != to_morph['name']:
- raise cliapp.AppException('mismatch in name or kind')
-
- pairs = []
- if to_morph['kind'] == 'system':
- pairs = zip(from_morph['strata'], to_morph['strata'])
- elif to_morph['kind'] == 'stratum':
- pairs = zip(from_morph['chunks'], to_morph['chunks'])
-
- for from_child, to_child in pairs:
- if from_child['morph'] != to_child['morph'] or \
- from_child['repo'] != to_child['repo']:
- continue
-
- # 'morph merge' has two stages, of which this is the first. In the
- # second stage we iterate each component whose 'ref' points to
- # 'from_branch', merge inside its repository, and then update the
- # morphology again to point at the merged ref in that repository.
- # That is why we apparently merge the 'ref' field backwards here.
- if from_child['ref'] == from_branch:
- to_child['ref'] = from_branch
-
- def merge_morphology(self, args):
- '''Automatically merge changes between two conflicting morphologies.
-
- Normally executed as a git merge driver.
-
- In the future, this function can be expanded to resolve changes in
- field ordering.
-
- '''
-
- if len(args) != 5:
- raise cliapp.AppException('this command is not meant to be run '
- 'manually.')
-
- from_branch = args[0]
- to_branch = args[1]
- base_file = args[2]
- to_file = args[3] # local
- from_file = args[4] # remote
-
- with open(from_file) as f:
- from_morph = morphlib.morph2.Morphology(f.read())
- with open(to_file) as f:
- to_morph = morphlib.morph2.Morphology(f.read())
-
- self.merge_morphology_contents(from_branch, from_morph, to_morph)
-
- # git merge gives us temporary files which we can overwrite with our
- # resolution
- with morphlib.savefile.SaveFile(to_file, 'w') as f:
- to_morph.write_to_file(f)
-
- # Leave the rest of the merging to git for now.
- status, output, error = self.app.runcmd_unchecked(
- ['git', 'merge-file', '-LHEAD', '-LBASE', '-L%s' % from_branch,
- to_file, base_file, from_file])
- if status != 0:
- raise cliapp.AppException('Morph did not resolve all conflicts')
-
def build(self, args):
if len(args) != 1:
raise cliapp.AppException('morph build expects exactly one '
diff --git a/tests.branching/ambiguous-refs.script b/tests.branching/ambiguous-refs.script
new file mode 100755
index 00000000..3321a310
--- /dev/null
+++ b/tests.branching/ambiguous-refs.script
@@ -0,0 +1,49 @@
+#!/bin/sh
+# Copyright (C) 2012 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+# 'git show-ref master' sorts its output alphebetically, so we can't rely on
+# it to resolve 'master': we might get 'alpha/master' instead.
+
+set -eu
+
+. "$SRCDIR/scripts/fix-committer-info"
+
+# Disable test on versions of Python before 2.7.
+if ! python --version 2>&1 | grep '^Python 2\.[78]' > /dev/null
+then
+ exit 0
+fi
+
+cd "$DATADIR/morphs"
+git mv hello-stratum.morph goodbye-stratum.morph
+sed -e 's/"morph": "hello-stratum"/"morph": "goodbye-stratum"/' \
+ -i hello-system.morph
+
+git commit --quiet -am "Rename hello-system"
+
+cd "$DATADIR/workspace"
+"$SRCDIR/scripts/test-morph" init
+"$SRCDIR/scripts/test-morph" branch baserock:morphs release
+
+# Create an extra ref to confuse any users of git show-ref
+cd release/baserock:morphs
+git checkout --quiet -b alpha/master HEAD~1
+git checkout --quiet release
+
+# The petrify will fail if we resolved 'master' as 'alpha/master' by mistake.
+cd "$DATADIR/workspace/release/baserock:morphs"
+"$SRCDIR/scripts/test-morph" petrify
diff --git a/tests.branching/merge-conflict-chunks.stderr b/tests.branching/merge-conflict-chunks.stderr
index 9692d2ec..57aafa5a 100644
--- a/tests.branching/merge-conflict-chunks.stderr
+++ b/tests.branching/merge-conflict-chunks.stderr
@@ -1,6 +1 @@
-ERROR: merge errors were encountered in the following repositories:
-
- baserock:stratum2-hello
- baserock:stratum3-hello
-
-Please manually merge the target ref into test/feature in each case in the test/feature system branch, and then repeat the system branch merge.
+ERROR: merge errors were encountered. Please manually merge the target ref into test/feature in the remote system branch in each case, and then repeat the 'morph merge' operation.
diff --git a/tests.branching/merge-conflict-chunks.stdout b/tests.branching/merge-conflict-chunks.stdout
index 75d50c7f..bf6d3828 100644
--- a/tests.branching/merge-conflict-chunks.stdout
+++ b/tests.branching/merge-conflict-chunks.stdout
@@ -1,10 +1,4 @@
-Merge errors encountered in baserock:stratum2-hello, branch test/stable:
-Auto-merging conflict.txt
-CONFLICT (add/add): Merge conflict in conflict.txt
-Automatic merge failed; fix conflicts and then commit the result.
-
-Merge errors encountered in baserock:stratum3-hello, branch test/stable:
-Auto-merging conflict.txt
-CONFLICT (add/add): Merge conflict in conflict.txt
-Automatic merge failed; fix conflicts and then commit the result.
-
+Merge conflicts in baserock:stratum2-hello:
+ conflict.txt
+Merge conflicts in baserock:stratum3-hello:
+ conflict.txt
diff --git a/tests.branching/merge-conflict-stratum.stderr b/tests.branching/merge-conflict-stratum.stderr
index 3a7c1522..57aafa5a 100644
--- a/tests.branching/merge-conflict-stratum.stderr
+++ b/tests.branching/merge-conflict-stratum.stderr
@@ -1 +1 @@
-ERROR: Merging failed in baserock:morphs: please 'git merge' the latest test/stable into test/feature in the 'test/feature' system branch.
+ERROR: merge errors were encountered. Please manually merge the target ref into test/feature in the remote system branch in each case, and then repeat the 'morph merge' operation.
diff --git a/tests.branching/merge-conflict-stratum.stdout b/tests.branching/merge-conflict-stratum.stdout
index 20064406..cce0973c 100644
--- a/tests.branching/merge-conflict-stratum.stdout
+++ b/tests.branching/merge-conflict-stratum.stdout
@@ -1,6 +1,2 @@
-Merge errors encountered in baserock:morphs, branch test/stable:
-Auto-merging hello-system.morph
-Auto-merging hello-stratum.morph
-CONFLICT (content): Merge conflict in hello-stratum.morph
-Automatic merge failed; fix conflicts and then commit the result.
-
+Merge conflicts in baserock:morphs:
+ hello-stratum.morph
diff --git a/tests.branching/merge-handles-unmergable-cases.stderr b/tests.branching/merge-handles-unmergable-cases.stderr
index 513db2b8..64c9c34b 100644
--- a/tests.branching/merge-handles-unmergable-cases.stderr
+++ b/tests.branching/merge-handles-unmergable-cases.stderr
@@ -1 +1 @@
-ERROR: merge conflict: "kind" of morphology hello-stratum
+ERROR: merge conflict: "kind" of morphology hello-stratum changed from chunk to stratum
diff --git a/tests.branching/merge-with-stratum-renamed.stderr b/tests.branching/merge-with-stratum-renamed.stderr
index ad144f93..ff408036 100644
--- a/tests.branching/merge-with-stratum-renamed.stderr
+++ b/tests.branching/merge-with-stratum-renamed.stderr
@@ -1 +1 @@
-ERROR: stratum goodbye-stratum was added within this branch and subsequently edited. This is not yet supported: refusing to merge.
+ERROR: goodbye-stratum.morph was not found in TMP/workspace/master/baserock:morphs at ref 804a475402fb6ded64a2749c7adf5b33c7c8ce1e