diff options
-rw-r--r-- | morphlib/plugins/branch_and_merge_plugin.py | 128 | ||||
-rw-r--r-- | tests.branching/merge-conflict-chunks.stdout | 4 | ||||
-rw-r--r-- | tests.branching/merge-conflict-stratum.stdout | 3 | ||||
-rwxr-xr-x | tests.branching/merge.script | 4 |
4 files changed, 127 insertions, 12 deletions
diff --git a/morphlib/plugins/branch_and_merge_plugin.py b/morphlib/plugins/branch_and_merge_plugin.py index 30f02fde..279a9948 100644 --- a/morphlib/plugins/branch_and_merge_plugin.py +++ b/morphlib/plugins/branch_and_merge_plugin.py @@ -29,7 +29,6 @@ import uuid import morphlib - class BranchAndMergePlugin(cliapp.Plugin): def __init__(self): @@ -58,6 +57,8 @@ 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 @@ -710,10 +711,51 @@ class BranchAndMergePlugin(cliapp.Plugin): 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): + 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): + attributes_file = os.path.join(repo_dir, ".git", "info", "attributes") + with open(attributes_file, 'r') as f: + attributes = f.read() + if attributes == self.MERGE_ATTRIBUTE: + os.unlink(attributes_file) + elif attributes.endswith(self.MERGE_ATTRIBUTE): + 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''' + '''Merge changes for a system branch in a specific repository + + This done using a standard 'git pull', with a custom merge driver to + handle the .morph files. + + 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. + + 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. + + ''' if to_repo in failed_repos: return None @@ -732,16 +774,25 @@ class BranchAndMergePlugin(cliapp.Plugin): raise cliapp.AppException('repository %s has uncommitted ' 'changes' % to_repo) - # repo must be made into a URL to avoid ':' in pathnames confusing git + 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) - status, output, error = self.app.runcmd_unchecked( ['git', 'pull', '--quiet', '--no-commit', '--no-ff', - from_url, from_ref], cwd=to_repo_dir) + 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 merging into %s in repo %s:\n%s%s\n' - % (to_ref, to_repo, error, output)) + 'Merge errors encountered in %s, branch %s:\n%s' + % (to_repo, to_ref, output)) + + if status != 0: + self.app.output.write('\n') failed_repos.add(to_repo) return None return to_repo_dir @@ -875,6 +926,69 @@ class BranchAndMergePlugin(cliapp.Plugin): 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/merge-conflict-chunks.stdout b/tests.branching/merge-conflict-chunks.stdout index cfb9949d..75d50c7f 100644 --- a/tests.branching/merge-conflict-chunks.stdout +++ b/tests.branching/merge-conflict-chunks.stdout @@ -1,9 +1,9 @@ -Merge errors encountered merging into test/stable in repo baserock:stratum2-hello: +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 merging into test/stable in repo baserock:stratum3-hello: +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. diff --git a/tests.branching/merge-conflict-stratum.stdout b/tests.branching/merge-conflict-stratum.stdout index a3bb4cc0..20064406 100644 --- a/tests.branching/merge-conflict-stratum.stdout +++ b/tests.branching/merge-conflict-stratum.stdout @@ -1,6 +1,5 @@ -Merge errors encountered merging into test/stable in repo baserock:morphs: +Merge errors encountered in baserock:morphs, branch test/stable: Auto-merging hello-system.morph -CONFLICT (content): Merge conflict in 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. diff --git a/tests.branching/merge.script b/tests.branching/merge.script index 9dbc2d84..d73e4835 100755 --- a/tests.branching/merge.script +++ b/tests.branching/merge.script @@ -31,11 +31,13 @@ cd "$DATADIR/workspace" "$SRCDIR/scripts/test-morph" branch baserock:morphs baserock/newbranch # Make a change to a chunk. +cd baserock/newbranch "$SRCDIR/scripts/test-morph" edit hello-system hello-stratum hello -cd baserock/newbranch/baserock:hello +cd baserock:hello touch newfile.txt git add newfile.txt git commit -m foo --quiet +git push --quiet origin baserock/newbranch # Commit in morphs repo cd ../baserock:morphs |