chiark / gitweb /
Re-add the interactive merge
authorCatalin Marinas <catalin.marinas@gmail.com>
Wed, 19 Dec 2007 18:00:15 +0000 (18:00 +0000)
committerCatalin Marinas <catalin.marinas@gmail.com>
Wed, 19 Dec 2007 23:13:31 +0000 (23:13 +0000)
This feature was dropped by previous changes to
git.merge_recursive(). This patch modifies gitmergeonefile.merge() to
only deal with interactive merges or simply check out the conflict
stages. The stgit.commands.common.resolved() function was moved to
git.resolved(). The patch also drops the git.merge() function since it
can no longer use gitmergeonefile.merge() (different API) and modifies
the 'sync' command to always use git.merge_recursive().

Signed-off-by: Catalin Marinas <catalin.marinas@gmail.com>
examples/gitconfig
stgit/commands/common.py
stgit/commands/resolved.py
stgit/commands/status.py
stgit/commands/sync.py
stgit/config.py
stgit/git.py
stgit/gitmergeonefile.py
stgit/stack.py
t/t0002-status.sh

index 52d2a696d5d0c004fa13c7327136f75f44655650..cd9b569d021d0de85ca1224fd11aec4ed0f802cc 100644 (file)
        # To support local parent branches:
        #pull-policy = rebase
 
        # To support local parent branches:
        #pull-policy = rebase
 
-       # The three-way merge tool. Note that the 'output' file contains the
-       # same data as 'branch1'. This is useful for tools that do not take an
-       # output parameter
-       #merger = diff3 -L current -L ancestor -L patched -m -E \
-       #       \"%(branch1)s\" \"%(ancestor)s\" \"%(branch2)s\" \
-       #       > \"%(output)s\"
-
        # Interactive two/three-way merge tool. It is executed by the
        # 'resolved --interactive' command
        #i3merge = xxdiff --title1 current --title2 ancestor --title3 patched \
        # Interactive two/three-way merge tool. It is executed by the
        # 'resolved --interactive' command
        #i3merge = xxdiff --title1 current --title2 ancestor --title3 patched \
index 2179684139efd34626e97bcb73d961f248a11a3a..7d9df027753e073348bb62ac37f0830e2bd58897 100644 (file)
@@ -145,18 +145,9 @@ def print_crt_patch(crt_series, branch = None):
     else:
         out.info('No patches applied')
 
     else:
         out.info('No patches applied')
 
-def resolved(filenames, reset = None):
-    if reset:
-        stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
-        Run('git', 'checkout-index', '--no-create', '--stage=%d' % stage,
-            '--stdin', '-z').input_nulterm(filenames).no_output()
-    git.add(filenames)
-    for filename in filenames:
-        os.utime(filename, None) # update the access and modificatied times
-
 def resolved_all(reset = None):
     conflicts = git.get_conflicts()
 def resolved_all(reset = None):
     conflicts = git.get_conflicts()
-    resolved(conflicts, reset)
+    git.resolved(conflicts, reset)
 
 def push_patches(crt_series, patches, check_merged = False):
     """Push multiple patches onto the stack. This function is shared
 
 def push_patches(crt_series, patches, check_merged = False):
     """Push multiple patches onto the stack. This function is shared
index adc591fc31c4ace484b6a1db42711d8932617376..4ee75b818756b6215d222bf27f05faaad2176ddf 100644 (file)
@@ -79,4 +79,6 @@ def func(parser, options, args):
     if options.interactive:
         for filename in files:
             interactive_merge(filename)
     if options.interactive:
         for filename in files:
             interactive_merge(filename)
-    resolved(files, options.reset)
+            git.resolved([filename])
+    else:
+        git.resolved(files, options.reset)
index 370e0332a6ad968e928b98c50e30f85f71fe809b..708dd161d265c2ce6bf4fdb8385fc8913286d253 100644 (file)
@@ -109,7 +109,8 @@ def func(parser, options, args):
 
     if options.reset:
         if args:
 
     if options.reset:
         if args:
-            resolved(args)
+            conflicts = git.get_conflicts()
+            git.resolved(fn for fn in args if fn in conflicts)
             git.reset(args)
         else:
             resolved_all()
             git.reset(args)
         else:
             resolved_all()
index e8e3bacd9e8203d0b7a3f9db898b8d45286c9adc..99ab7de9453ccfa7a6f69ff6ba4ecd049cb8440e 100644 (file)
@@ -57,7 +57,7 @@ def __branch_merge_patch(remote_series, pname):
     """Merge a patch from a remote branch into the current tree.
     """
     patch = remote_series.get_patch(pname)
     """Merge a patch from a remote branch into the current tree.
     """
     patch = remote_series.get_patch(pname)
-    git.merge(patch.get_bottom(), git.get_head(), patch.get_top())
+    git.merge_recursive(patch.get_bottom(), git.get_head(), patch.get_top())
 
 def __series_merge_patch(base, patchdir, pname):
     """Merge a patch file with the given StGIT patch.
 
 def __series_merge_patch(base, patchdir, pname):
     """Merge a patch file with the given StGIT patch.
index 61f91b06730051fa96dde3956d83153a4d868983..1d71cd2b787f687feb907c186fa6b62660b9ad2c 100644 (file)
@@ -34,8 +34,6 @@ class GitConfig:
         'stgit.pullcmd':       'git pull',
         'stgit.fetchcmd':      'git fetch',
         'stgit.pull-policy':   'pull',
         'stgit.pullcmd':       'git pull',
         'stgit.fetchcmd':      'git fetch',
         'stgit.pull-policy':   'pull',
-        'stgit.merger':                'diff3 -L current -L ancestor -L patched -m -E ' \
-                               '"%(branch1)s" "%(ancestor)s" "%(branch2)s" > "%(output)s"',
         'stgit.autoimerge':    'no',
         'stgit.keeporig':      'yes',
         'stgit.keepoptimized': 'no',
         'stgit.autoimerge':    'no',
         'stgit.keeporig':      'yes',
         'stgit.keepoptimized': 'no',
index 6629e395ab5de48e26e5b50c06b870de2cc255f1..0cfcd42b6606c39621cc2c1a01197f9fe620aa0a 100644 (file)
@@ -43,15 +43,6 @@ class GRun(Run):
         """
         Run.__init__(self, 'git', *cmd)
 
         """
         Run.__init__(self, 'git', *cmd)
 
-class GitConflictException(GitException):
-    def __init__(self, conflicts):
-        GitException.__init__(self)
-        self.conflicts = conflicts
-    def __str__(self):
-        return "%d conflicts" % len(self.conflicts)
-    def list(self):
-        out.info(*self.conflicts)
-
 #
 # Classes
 #
 #
 # Classes
 #
@@ -713,74 +704,21 @@ def merge_recursive(base, head1, head2):
           'GITHEAD_%s' % head1: 'current',
           'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
     output = p.output_lines()
           'GITHEAD_%s' % head1: 'current',
           'GITHEAD_%s' % head2: 'patched'}).returns([0, 1])
     output = p.output_lines()
-    if p.exitcode == 0:
-        # No problems
-        return
-    else: # exitcode == 1
+    if p.exitcode:
         # There were conflicts
         conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
         # There were conflicts
         conflicts = [l.strip() for l in output if l.startswith('CONFLICT')]
-        raise GitConflictException(conflicts)
+        out.info(*conflicts)
 
 
-def merge(base, head1, head2):
-    """Perform a 3-way merge between base, head1 and head2 into the
-    local tree
-    """
-    refresh_index()
+        # try the interactive merge or stage checkout (if enabled)
+        for filename in get_conflicts():
+            if (gitmergeonefile.merge(filename)):
+                # interactive merge succeeded
+                resolved([filename])
 
 
-    err_output = None
-    # the fast case where we don't track renames (used when the
-    # distance between base and heads is small, i.e. folding or
-    # synchronising patches)
-    try:
-        GRun('read-tree', '-u', '-m', '--aggressive', base, head1, head2
-             ).run()
-    except GitRunException:
-        raise GitException, 'read-tree failed (local changes maybe?)'
-
-    # check the index for unmerged entries
-    files = {}
-    stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
-
-    for line in GRun('ls-files', '--unmerged', '--stage', '-z'
-                     ).raw_output().split('\0'):
-        if not line:
-            continue
-
-        mode, hash, stage, path = stages_re.findall(line)[0]
-
-        if not path in files:
-            files[path] = {}
-            files[path]['1'] = ('', '')
-            files[path]['2'] = ('', '')
-            files[path]['3'] = ('', '')
-
-        files[path][stage] = (mode, hash)
-
-    if err_output and not files:
-        # if no unmerged files, there was probably a different type of
-        # error and we have to abort the merge
-        raise GitException, err_output
-
-    # merge the unmerged files
-    errors = False
-    for path in files:
-        # remove additional files that might be generated for some
-        # newer versions of GIT
-        for suffix in [base, head1, head2]:
-            if not suffix:
-                continue
-            fname = path + '~' + suffix
-            if os.path.exists(fname):
-                os.remove(fname)
-
-        stages = files[path]
-        if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
-                                 stages['3'][1], path, stages['1'][0],
-                                 stages['2'][0], stages['3'][0]) != 0:
-            errors = True
-
-    if errors:
-        raise GitException, 'GIT index merging failed (possible conflicts)'
+        # any conflicts left unsolved?
+        cn = len(get_conflicts())
+        if cn:
+            raise GitException, "%d conflict(s)" % cn
 
 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
          binary = True):
 
 def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [],
          binary = True):
@@ -890,6 +828,17 @@ def reset(files = None, tree_id = None, check_out = True):
     if not files:
         __set_head(tree_id)
 
     if not files:
         __set_head(tree_id)
 
+def resolved(filenames, reset = None):
+    if reset:
+        stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset]
+        GRun('checkout-index', '--no-create', '--stage=%d' % stage,
+             '--stdin', '-z').input_nulterm(filenames).no_output()
+    GRun('update-index', '--add', '--').xargs(filenames)
+    for filename in filenames:
+        gitmergeonefile.clean_up(filename)
+        # update the access and modificatied times
+        os.utime(filename, None)
+
 def fetch(repository = 'origin', refspec = None):
     """Fetches changes from the remote repository, using 'git fetch'
     by default.
 def fetch(repository = 'origin', refspec = None):
     """Fetches changes from the remote repository, using 'git fetch'
     by default.
@@ -984,7 +933,7 @@ def apply_patch(filename = None, diff = None, base = None,
         top = commit(message = 'temporary commit used for applying a patch',
                      parents = [base])
         switch(orig_head)
         top = commit(message = 'temporary commit used for applying a patch',
                      parents = [base])
         switch(orig_head)
-        merge(base, orig_head, top)
+        merge_recursive(base, orig_head, top)
 
 def clone(repository, local_dir):
     """Clone a remote repository. At the moment, just use the
 
 def clone(repository, local_dir):
     """Clone a remote repository. At the moment, just use the
index c85e347dcdc7eae960321f1c583362c55cc4e775..1fe226e0fcb1bb824764b84609f12a975d19ba19 100644 (file)
@@ -33,7 +33,7 @@ class GitMergeException(StgException):
 #
 # Options
 #
 #
 # Options
 #
-merger = ConfigOption('stgit', 'merger')
+autoimerge = ConfigOption('stgit', 'autoimerge')
 keeporig = ConfigOption('stgit', 'keeporig')
 
 #
 keeporig = ConfigOption('stgit', 'keeporig')
 
 #
@@ -48,72 +48,46 @@ def __str2none(x):
 class MRun(Run):
     exc = GitMergeException # use a custom exception class on errors
 
 class MRun(Run):
     exc = GitMergeException # use a custom exception class on errors
 
-def __checkout_files(orig_hash, file1_hash, file2_hash,
-                     path,
-                     orig_mode, file1_mode, file2_mode):
-    """Check out the files passed as arguments
+def __checkout_stages(filename):
+    """Check-out the merge stages in the index for the give file
     """
     """
-    global orig, src1, src2
-
     extensions = file_extensions()
     extensions = file_extensions()
+    line = MRun('git', 'checkout-index', '--stage=all', '--', filename
+                ).output_one_line()
+    stages, path = line.split('\t')
+    stages = dict(zip(['ancestor', 'current', 'patched'],
+                      stages.split(' ')))
+
+    for stage, fn in stages.iteritems():
+        if stages[stage] == '.':
+            stages[stage] = None
+        else:
+            newname = filename + extensions[stage]
+            if os.path.exists(newname):
+                # remove the stage if it is already checked out
+                os.remove(newname)
+            os.rename(stages[stage], newname)
+            stages[stage] = newname
 
 
-    if orig_hash:
-        orig = path + extensions['ancestor']
-        tmp = MRun('git', 'unpack-file', orig_hash).output_one_line()
-        os.chmod(tmp, int(orig_mode, 8))
-        os.renames(tmp, orig)
-    if file1_hash:
-        src1 = path + extensions['current']
-        tmp = MRun('git', 'unpack-file', file1_hash).output_one_line()
-        os.chmod(tmp, int(file1_mode, 8))
-        os.renames(tmp, src1)
-    if file2_hash:
-        src2 = path + extensions['patched']
-        tmp = MRun('git', 'unpack-file', file2_hash).output_one_line()
-        os.chmod(tmp, int(file2_mode, 8))
-        os.renames(tmp, src2)
-
-    if file1_hash and not os.path.exists(path):
-        # the current file might be removed by GIT when it is a new
-        # file added in both branches. Just re-generate it
-        tmp = MRun('git', 'unpack-file', file1_hash).output_one_line()
-        os.chmod(tmp, int(file1_mode, 8))
-        os.renames(tmp, path)
+    return stages
 
 
-def __remove_files(orig_hash, file1_hash, file2_hash):
-    """Remove any temporary files
+def __remove_stages(filename):
+    """Remove the merge stages from the working directory
     """
     """
-    if orig_hash:
-        os.remove(orig)
-    if file1_hash:
-        os.remove(src1)
-    if file2_hash:
-        os.remove(src2)
+    extensions = file_extensions()
+    for ext in extensions.itervalues():
+        fn = filename + ext
+        if os.path.isfile(fn):
+            os.remove(fn)
 
 
-def __conflict(path):
-    """Write the conflict file for the 'path' variable and exit
+def interactive_merge(filename):
+    """Run the interactive merger on the given file. Stages will be
+    removed according to stgit.keeporig. If successful and stages
+    kept, they will be removed via git.resolved().
     """
     """
-    append_string(os.path.join(basedir.get(), 'conflicts'), path)
-
+    stages = __checkout_stages(filename)
 
 
-def interactive_merge(filename):
-    """Run the interactive merger on the given file."""
     try:
     try:
-        extensions = file_extensions()
-        line = MRun('git', 'checkout-index', '--stage=all', '--', filename
-                    ).output_one_line()
-        stages, path = line.split('\t')
-        stages = dict(zip(['ancestor', 'current', 'patched'],
-                          stages.split(' ')))
-        for stage, fn in stages.iteritems():
-            if stages[stage] == '.':
-                stages[stage] = None
-            else:
-                newname = filename + extensions[stage]
-                if not os.path.exists(newname):
-                    os.rename(stages[stage], newname)
-                    stages[stage] = newname
-
         # Check whether we have all the files for the merge.
         if not (stages['current'] and stages['patched']):
             raise GitMergeException('Cannot run the interactive merge')
         # Check whether we have all the files for the merge.
         if not (stages['current'] and stages['patched']):
             raise GitMergeException('Cannot run the interactive merge')
@@ -148,183 +122,29 @@ def interactive_merge(filename):
         if mtime == os.path.getmtime(filename):
             raise GitMergeException, 'The "%s" file was not modified' % filename
     finally:
         if mtime == os.path.getmtime(filename):
             raise GitMergeException, 'The "%s" file was not modified' % filename
     finally:
-        for fn in stages.itervalues():
-            if os.path.isfile(fn):
-                os.remove(fn)
+        # keep the merge stages?
+        if str(keeporig) != 'yes':
+            __remove_stages(filename)
 
 
-#
-# Main algorithm
-#
-def merge(orig_hash, file1_hash, file2_hash,
-          path,
-          orig_mode, file1_mode, file2_mode):
-    """Three-way merge for one file algorithm
+def clean_up(filename):
+    """Remove merge conflict stages if they were generated.
     """
     """
-    __checkout_files(orig_hash, file1_hash, file2_hash,
-                     path,
-                     orig_mode, file1_mode, file2_mode)
-
-    # file exists in origin
-    if orig_hash:
-        # modified in both
-        if file1_hash and file2_hash:
-            # if modes are the same (git-read-tree probably dealt with it)
-            if file1_hash == file2_hash:
-                if os.system('git update-index --cacheinfo %s %s %s'
-                             % (file1_mode, file1_hash, path)) != 0:
-                    out.error('git update-index failed')
-                    __conflict(path)
-                    return 1
-                if os.system('git checkout-index -u -f -- %s' % path):
-                    out.error('git checkout-index failed')
-                    __conflict(path)
-                    return 1
-                if file1_mode != file2_mode:
-                    out.error('File added in both, permissions conflict')
-                    __conflict(path)
-                    return 1
-            # 3-way merge
-            else:
-                merge_ok = os.system(str(merger) % {'branch1': src1,
-                                                    'ancestor': orig,
-                                                    'branch2': src2,
-                                                    'output': path }) == 0
-
-                if merge_ok:
-                    os.system('git update-index -- %s' % path)
-                    __remove_files(orig_hash, file1_hash, file2_hash)
-                    return 0
-                else:
-                    out.error('Three-way merge tool failed for file "%s"'
-                              % path)
-                    # reset the cache to the first branch
-                    os.system('git update-index --cacheinfo %s %s %s'
-                              % (file1_mode, file1_hash, path))
+    if str(keeporig) == 'yes':
+        __remove_stages(filename)
 
 
-                    if config.get('stgit.autoimerge') == 'yes':
-                        try:
-                            interactive_merge(path)
-                        except GitMergeException, ex:
-                            # interactive merge failed
-                            out.error(str(ex))
-                            if str(keeporig) != 'yes':
-                                __remove_files(orig_hash, file1_hash,
-                                               file2_hash)
-                            __conflict(path)
-                            return 1
-                        # successful interactive merge
-                        os.system('git update-index -- %s' % path)
-                        __remove_files(orig_hash, file1_hash, file2_hash)
-                        return 0
-                    else:
-                        # no interactive merge, just mark it as conflict
-                        if str(keeporig) != 'yes':
-                            __remove_files(orig_hash, file1_hash, file2_hash)
-                        __conflict(path)
-                        return 1
-
-        # file deleted in both or deleted in one and unchanged in the other
-        elif not (file1_hash or file2_hash) \
-               or file1_hash == orig_hash or file2_hash == orig_hash:
-            if os.path.exists(path):
-                os.remove(path)
-            __remove_files(orig_hash, file1_hash, file2_hash)
-            return os.system('git update-index --remove -- %s' % path)
-        # file deleted in one and changed in the other
-        else:
-            # Do something here - we must at least merge the entry in
-            # the cache, instead of leaving it in U(nmerged) state. In
-            # fact, stg resolved does not handle that.
-
-            # Do the same thing cogito does - remove the file in any case.
-            os.system('git update-index --remove -- %s' % path)
-
-            #if file1_hash:
-                ## file deleted upstream and changed in the patch. The
-                ## patch is probably going to move the changes
-                ## elsewhere.
-
-                #os.system('git update-index --remove -- %s' % path)
-            #else:
-                ## file deleted in the patch and changed upstream. We
-                ## could re-delete it, but for now leave it there -
-                ## and let the user check if he still wants to remove
-                ## the file.
-
-                ## reset the cache to the first branch
-                #os.system('git update-index --cacheinfo %s %s %s'
-                #          % (file1_mode, file1_hash, path))
-            __conflict(path)
-            return 1
-
-    # file does not exist in origin
-    else:
-        # file added in both
-        if file1_hash and file2_hash:
-            # files are the same
-            if file1_hash == file2_hash:
-                if os.system('git update-index --add --cacheinfo %s %s %s'
-                             % (file1_mode, file1_hash, path)) != 0:
-                    out.error('git update-index failed')
-                    __conflict(path)
-                    return 1
-                if os.system('git checkout-index -u -f -- %s' % path):
-                    out.error('git checkout-index failed')
-                    __conflict(path)
-                    return 1
-                if file1_mode != file2_mode:
-                    out.error('File "s" added in both, permissions conflict'
-                              % path)
-                    __conflict(path)
-                    return 1
-            # files added in both but different
-            else:
-                out.error('File "%s" added in branches but different' % path)
-                # reset the cache to the first branch
-                os.system('git update-index --cacheinfo %s %s %s'
-                          % (file1_mode, file1_hash, path))
-
-                if config.get('stgit.autoimerge') == 'yes':
-                    try:
-                        interactive_merge(path)
-                    except GitMergeException, ex:
-                        # interactive merge failed
-                        out.error(str(ex))
-                        if str(keeporig) != 'yes':
-                            __remove_files(orig_hash, file1_hash,
-                                           file2_hash)
-                        __conflict(path)
-                        return 1
-                    # successful interactive merge
-                    os.system('git update-index -- %s' % path)
-                    __remove_files(orig_hash, file1_hash, file2_hash)
-                    return 0
-                else:
-                    # no interactive merge, just mark it as conflict
-                    if str(keeporig) != 'yes':
-                        __remove_files(orig_hash, file1_hash, file2_hash)
-                    __conflict(path)
-                    return 1
-        # file added in one
-        elif file1_hash or file2_hash:
-            if file1_hash:
-                mode = file1_mode
-                obj = file1_hash
-            else:
-                mode = file2_mode
-                obj = file2_hash
-            if os.system('git update-index --add --cacheinfo %s %s %s'
-                         % (mode, obj, path)) != 0:
-                out.error('git update-index failed')
-                __conflict(path)
-                return 1
-            __remove_files(orig_hash, file1_hash, file2_hash)
-            return os.system('git checkout-index -u -f -- %s' % path)
-
-    # Unhandled case
-    out.error('Unhandled merge conflict: "%s" "%s" "%s" "%s" "%s" "%s" "%s"'
-              % (orig_hash, file1_hash, file2_hash,
-                 path,
-                 orig_mode, file1_mode, file2_mode))
-    __conflict(path)
-    return 1
+def merge(filename):
+    """Merge one file if interactive is allowed or check out the stages
+    if keeporig is set.
+    """
+    if str(autoimerge) == 'yes':
+        try:
+            interactive_merge(filename)
+        except GitMergeException, ex:
+            out.error(str(ex))
+            return False
+        return True
+
+    if str(keeporig) == 'yes':
+        __checkout_stages(filename)
+
+    return False
index 4203931905956fe0592481b39b2f24c2b04b5025..7fe9f2b618fc6b60b883cc1e009867b1451eceef 100644 (file)
@@ -1019,8 +1019,6 @@ class Series(PatchSet):
             # merge can fail but the patch needs to be pushed
             try:
                 git.merge_recursive(bottom, head, top)
             # merge can fail but the patch needs to be pushed
             try:
                 git.merge_recursive(bottom, head, top)
-            except git.GitConflictException, ex:
-                ex.list()
             except git.GitException, ex:
                 out.error('The merge failed during "push".',
                           'Revert the operation with "push --undo".')
             except git.GitException, ex:
                 out.error('The merge failed during "push".',
                           'Revert the operation with "push --undo".')
index 9b4e60d70646c3f57697572ca9822fd6586a0333..0a70f15c31389e8934a672ddb7e036c2680e28b4 100755 (executable)
@@ -107,6 +107,9 @@ test_expect_success 'Make a conflicting patch' '
 '
 
 cat > expected.txt <<EOF
 '
 
 cat > expected.txt <<EOF
+? foo/bar.ancestor
+? foo/bar.current
+? foo/bar.patched
 A fie
 C foo/bar
 EOF
 A fie
 C foo/bar
 EOF