Repository: iterative/dvc

Issue:
Add 'dvc merge' command
'dvc merge' should act as a 'git merge' when there are no source data files conflicts detected and give priority to source data files from current branch otherwise.


Discussion:
"source data file" == "imported data file"

Resolving patch:
diff --git a/dvc/cli.py b/dvc/cli.py
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -16,6 +16,7 @@
 from dvc.command.instance_create import CmdInstanceCreate
 from dvc.command.config import CmdConfig
 from dvc.command.show_pipeline import CmdShowPipeline
+from dvc.command.merge import CmdMerge
 from dvc import VERSION
 
 
@@ -407,6 +408,13 @@ def parse_args(argv=None):
 
     workflow_parser.set_defaults(func=CmdShowWorkflow)
 
+    # Merge
+    merge_parser = subparsers.add_parser(
+                        'merge',
+                        parents=[parent_parser],
+                        help='Merge')
+    merge_parser.set_defaults(func=CmdMerge)
+
     if isinstance(argv, str):
         argv = argv.split()
 
diff --git a/dvc/command/merge.py b/dvc/command/merge.py
new file mode 100644
--- /dev/null
+++ b/dvc/command/merge.py
@@ -0,0 +1,55 @@
+import json
+
+from dvc.command.base import CmdBase, DvcLock
+from dvc.logger import Logger
+from dvc.state_file import StateFile
+from dvc.path.data_item import NotInDataDirError
+
+class CmdMerge(CmdBase):
+    def __init__(self, settings):
+        super(CmdMerge, self).__init__(settings)
+
+    def print_info(self, targets):
+        for fname in targets:
+            Logger.info('Restored original data after merge:')
+            Logger.info(' {}'.format(fname))
+
+    def collect_targets(self):
+        targets = []
+        flist = self.git.get_last_merge_changed_files()
+        for fname in flist:
+            try:
+                item = self.settings.path_factory.data_item(fname)
+            except NotInDataDirError:
+                continue
+
+            try:
+                state = StateFile.load(item, self.git)
+            except Exception as ex:
+                Logger.error('Failed to load state file for {}'.format(fname), exc_info=True)
+                return None
+
+            if not state.command == StateFile.COMMAND_IMPORT_FILE:
+                continue
+
+            targets.append(fname)
+
+        return targets
+
+    def checkout_targets(self, targets):
+        for fname in targets:
+            self.git.checkout_file_before_last_merge(fname)
+
+        msg = 'DVC merge files: {}'.format(' '.join(targets))
+        self.commit_if_needed(msg)
+
+    def run(self):
+        with DvcLock(self.is_locker, self.git):
+            targets = self.collect_targets()
+            if not targets:
+                return 1
+
+            self.checkout_targets(targets)
+            self.print_info(targets)
+
+            return 0
diff --git a/dvc/git_wrapper.py b/dvc/git_wrapper.py
--- a/dvc/git_wrapper.py
+++ b/dvc/git_wrapper.py
@@ -253,6 +253,22 @@ def get_target_commit(fname):
         except ExecutorError:
             return None
 
+    @staticmethod
+    def get_last_merge_changed_files():
+        cmd = 'git diff --name-only @{1}'
+        Logger.debug('[dvc-git] Get last merge changed files. Command: {}'.format(cmd))
+        try:
+            fstr = Executor.exec_cmd_only_success(cmd.split())
+            return fstr.split('\n')
+        except ExecutorError:
+            return None
+
+    @staticmethod
+    def checkout_file_before_last_merge(fname):
+        cmd = 'git checkout @{1} -- ' + fname
+        Logger.debug('[dvc-git] Checkout file {} before last merge. Command: {}'.format(fname, cmd))
+        Executor.exec_cmd_only_success(cmd.split())
+
     def separate_dependency_files_and_dirs(self, code_dependencies):
         code_files = []
         code_dirs = []


Test patch: