From: Catalin Marinas Date: Fri, 10 Mar 2006 21:44:44 +0000 (+0000) Subject: Deal with merge conflicts directly X-Git-Tag: v0.14.3~529 X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/commitdiff_plain/3659ef88d90c0c76acb34c448fb8f61087ff25b8 Deal with merge conflicts directly This patch removes the need of calling the external git-merge-index and gitmergeonefile.py. These are replaced by internal solving of the unmerged stages in the index file. Signed-off-by: Catalin Marinas --- diff --git a/gitmergeonefile.py b/gitmergeonefile.py deleted file mode 100755 index 99882c8..0000000 --- a/gitmergeonefile.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python -"""Performs a 3-way merge for GIT files -""" - -__copyright__ = """ -Copyright (C) 2005, Catalin Marinas - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License version 2 as -published by the Free Software Foundation. - -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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -""" - -import sys, os - -# Try to detect where it is run from and set prefix and the search path. -# It is assumed that the user installed StGIT using the --prefix= option -prefix, bin = os.path.split(sys.path[0]) - -if bin == 'bin' and prefix != sys.prefix: - major, minor = sys.version_info[0:2] - local_path = [os.path.join(prefix, 'lib', 'python'), - os.path.join(prefix, 'lib', 'python%s.%s' % (major, minor)), - os.path.join(prefix, 'lib', 'python%s.%s' % (major, minor), - 'site-packages')] - sys.path = local_path + sys.path - -from stgit.config import config -from stgit.utils import append_string -from stgit.git import get_base_dir - - -# -# Options -# -try: - merger = config.get('gitmergeonefile', 'merger') -except Exception, err: - print >> sys.stderr, 'Configuration error: %s' % err - sys.exit(1) - -if config.has_option('gitmergeonefile', 'keeporig'): - keeporig = config.get('gitmergeonefile', 'keeporig') -else: - keeporig = 'yes' - - -# -# Utility functions -# -def __str2none(x): - if x == '': - return None - else: - return x - -def __output(cmd): - f = os.popen(cmd, 'r') - string = f.readline().strip() - if f.close(): - print >> sys.stderr, 'Error: failed to execute "%s"' % cmd - sys.exit(1) - return string - -def __checkout_files(): - """Check out the files passed as arguments - """ - global orig, src1, src2 - - if orig_hash: - orig = '%s.older' % path - tmp = __output('git-unpack-file %s' % orig_hash) - os.chmod(tmp, int(orig_mode, 8)) - os.renames(tmp, orig) - if file1_hash: - src1 = '%s.local' % path - tmp = __output('git-unpack-file %s' % file1_hash) - os.chmod(tmp, int(file1_mode, 8)) - os.renames(tmp, src1) - if file2_hash: - src2 = '%s.remote' % path - tmp = __output('git-unpack-file %s' % file2_hash) - os.chmod(tmp, int(file2_mode, 8)) - os.renames(tmp, src2) - -def __remove_files(): - """Remove any temporary files - """ - if orig_hash: - os.remove(orig) - if file1_hash: - os.remove(src1) - if file2_hash: - os.remove(src2) - pass - -def __conflict(): - """Write the conflict file for the 'path' variable and exit - """ - append_string(os.path.join(get_base_dir(), 'conflicts'), path) - sys.exit(1) - - -# $1 - original file SHA1 (or empty) -# $2 - file in branch1 SHA1 (or empty) -# $3 - file in branch2 SHA1 (or empty) -# $4 - pathname in repository -# $5 - original file mode (or empty) -# $6 - file in branch1 mode (or empty) -# $7 - file in branch2 mode (or empty) -# -#print 'gitmergeonefile.py "%s" "%s" "%s" "%s" "%s" "%s" "%s"' \ -# % tuple(sys.argv[1:8]) -orig_hash, file1_hash, file2_hash, path, orig_mode, file1_mode, file2_mode = \ - [__str2none(x) for x in sys.argv[1:8]] - - -# -# Main algorithm -# -__checkout_files() - -# 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: - print >> sys.stderr, 'Error: git-update-index failed' - __conflict() - if os.system('git-checkout-index -u -f -- %s' % path): - print >> sys.stderr, 'Error: git-checkout-index failed' - __conflict() - if file1_mode != file2_mode: - print >> sys.stderr, \ - 'Error: File added in both, permissions conflict' - __conflict() - # 3-way merge - else: - merge_ok = os.system(merger % {'branch1': src1, - 'ancestor': orig, - 'branch2': src2, - 'output': path }) == 0 - - if merge_ok: - os.system('git-update-index -- %s' % path) - __remove_files() - sys.exit(0) - else: - print >> sys.stderr, \ - '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 keeporig != 'yes': - __remove_files() - __conflict() - # 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() - sys.exit(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() - -# 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: - print >> sys.stderr, 'Error: git-update-index failed' - __conflict() - if os.system('git-checkout-index -u -f -- %s' % path): - print >> sys.stderr, 'Error: git-checkout-index failed' - __conflict() - if file1_mode != file2_mode: - print >> sys.stderr, \ - 'Error: File "s" added in both, permissions conflict' \ - % path - __conflict() - # files are different - else: - print >> sys.stderr, \ - 'Error: File "%s" added in branches but different' % path - __conflict() - # 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: - print >> sys.stderr, 'Error: git-update-index failed' - __conflict() - __remove_files() - sys.exit(os.system('git-checkout-index -u -f -- %s' % path)) - -# Unhandled case -print >> sys.stderr, 'Error: Unhandled merge conflict' -print >> sys.stderr, 'gitmergeonefile.py "%s" "%s" "%s" "%s" "%s" "%s" "%s"' \ - % tuple(sys.argv[1:8]) -__conflict() diff --git a/setup.py b/setup.py index 1a7a3f0..7888f29 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup(name = 'stgit', url = 'http://www.procode.org/stgit/', description = 'Stacked GIT', long_description = 'Push/pop utility on top of GIT', - scripts = ['stg', 'gitmergeonefile.py'], + scripts = ['stg'], packages = ['stgit', 'stgit.commands'], data_files = [('share/stgit/templates', glob.glob('templates/*.tmpl')), ('share/stgit/examples', glob.glob('examples/*.tmpl')), diff --git a/stgit/git.py b/stgit/git.py index 8306c85..7b47267 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -18,7 +18,7 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -import sys, os, popen2 +import sys, os, popen2, re, gitmergeonefile from stgit.utils import * @@ -496,9 +496,35 @@ def merge(base, head1, head2): if __run('git-read-tree -u -m --aggressive', [base, head1, head2]) != 0: raise GitException, 'git-read-tree failed (local changes maybe?)' - # this can fail if there are conflicts - if __run('git-merge-index -o -q gitmergeonefile.py -a') != 0: - raise GitException, 'git-merge-index failed (possible conflicts)' + # 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 _output('git-ls-files --unmerged --stage -z').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) + + # merge the unmerged files + errors = False + for path in files: + 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)' def status(files = None, modified = False, new = False, deleted = False, conflict = False, unknown = False, noexclude = False): diff --git a/stgit/gitmergeonefile.py b/stgit/gitmergeonefile.py new file mode 100644 index 0000000..586cef9 --- /dev/null +++ b/stgit/gitmergeonefile.py @@ -0,0 +1,249 @@ +"""Performs a 3-way merge for GIT files +""" + +__copyright__ = """ +Copyright (C) 2006, Catalin Marinas + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +import sys, os +from stgit.config import config +from stgit.utils import append_string + + +class GitMergeException(Exception): + pass + + +# +# Options +# +try: + merger = config.get('gitmergeonefile', 'merger') +except Exception, err: + raise GitMergeException, 'Configuration error: %s' % err + +if config.has_option('gitmergeonefile', 'keeporig'): + keeporig = config.get('gitmergeonefile', 'keeporig') +else: + keeporig = 'yes' + + +# +# Utility functions +# +def __str2none(x): + if x == '': + return None + else: + return x + +def __output(cmd): + f = os.popen(cmd, 'r') + string = f.readline().rstrip() + if f.close(): + raise GitMergeException, 'Error: failed to execute "%s"' % cmd + return string + +def __checkout_files(orig_hash, file1_hash, file2_hash, + path, + orig_mode, file1_mode, file2_mode): + """Check out the files passed as arguments + """ + global orig, src1, src2 + + if orig_hash: + orig = '%s.older' % path + tmp = __output('git-unpack-file %s' % orig_hash) + os.chmod(tmp, int(orig_mode, 8)) + os.renames(tmp, orig) + if file1_hash: + src1 = '%s.local' % path + tmp = __output('git-unpack-file %s' % file1_hash) + os.chmod(tmp, int(file1_mode, 8)) + os.renames(tmp, src1) + if file2_hash: + src2 = '%s.remote' % path + tmp = __output('git-unpack-file %s' % file2_hash) + os.chmod(tmp, int(file2_mode, 8)) + os.renames(tmp, src2) + +def __remove_files(orig_hash, file1_hash, file2_hash): + """Remove any temporary files + """ + if orig_hash: + os.remove(orig) + if file1_hash: + os.remove(src1) + if file2_hash: + os.remove(src2) + pass + +# GIT_DIR value cached +__base_dir = None + +def __conflict(path): + """Write the conflict file for the 'path' variable and exit + """ + global __base_dir + + if not __base_dir: + if 'GIT_DIR' in os.environ: + __base_dir = os.environ['GIT_DIR'] + else: + __base_dir = __output('git-rev-parse --git-dir') + + append_string(os.path.join(__base_dir, 'conflicts'), path) + + +# +# Main algorithm +# +def merge(orig_hash, file1_hash, file2_hash, + path, + orig_mode, file1_mode, file2_mode): + """Three-way merge for one file algorithm + """ + __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: + print >> sys.stderr, 'Error: git-update-index failed' + __conflict(path) + return 1 + if os.system('git-checkout-index -u -f -- %s' % path): + print >> sys.stderr, 'Error: git-checkout-index failed' + __conflict(path) + return 1 + if file1_mode != file2_mode: + print >> sys.stderr, \ + 'Error: File added in both, permissions conflict' + __conflict(path) + return 1 + # 3-way merge + else: + merge_ok = os.system(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: + print >> sys.stderr, \ + '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 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: + print >> sys.stderr, 'Error: git-update-index failed' + __conflict(path) + return 1 + if os.system('git-checkout-index -u -f -- %s' % path): + print >> sys.stderr, 'Error: git-checkout-index failed' + __conflict(path) + return 1 + if file1_mode != file2_mode: + print >> sys.stderr, \ + 'Error: File "s" added in both, ' \ + 'permissions conflict' % path + __conflict(path) + return 1 + # files are different + else: + print >> sys.stderr, \ + 'Error: File "%s" added in branches but different' % path + __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: + print >> sys.stderr, '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 + print >> sys.stderr, '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 diff --git a/stgit/main.py b/stgit/main.py index 008912b..d96ff2e 100644 --- a/stgit/main.py +++ b/stgit/main.py @@ -22,7 +22,7 @@ import sys, os from optparse import OptionParser, make_option from stgit.utils import * -from stgit import stack, git +from stgit import stack, git, gitmergeonefile from stgit.version import version from stgit.config import config from stgit.commands.common import * @@ -183,8 +183,8 @@ def main(): stgit.commands.common.crt_series = command.crt_series command.func(parser, options, args) - except (IOError, CmdException, stack.StackException, git.GitException), \ - err: + except (IOError, CmdException, stack.StackException, git.GitException, + gitmergeonefile.GitMergeException), err: print >> sys.stderr, '%s %s: %s' % (prog, cmd, err) sys.exit(2) except KeyboardInterrupt: