X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/d7fade4be437ade1eaf92ca29cc99b566b0e3c18..764d110156e4951ca5671a700ee2402fa3597734:/stgit/gitmergeonefile.py diff --git a/stgit/gitmergeonefile.py b/stgit/gitmergeonefile.py index 136552b..2aa5ef8 100644 --- a/stgit/gitmergeonefile.py +++ b/stgit/gitmergeonefile.py @@ -20,9 +20,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os from stgit import basedir -from stgit.config import config, file_extensions +from stgit.config import config, file_extensions, ConfigOption from stgit.utils import append_string - +from stgit.out import * +from stgit.run import * class GitMergeException(Exception): pass @@ -31,12 +32,8 @@ class GitMergeException(Exception): # # Options # -try: - merger = config.get('stgit', 'merger') - keeporig = config.get('stgit', 'keeporig') -except Exception, err: - raise GitMergeException, 'Configuration error: %s' % err - +merger = ConfigOption('stgit', 'merger') +keeporig = ConfigOption('stgit', 'keeporig') # # Utility functions @@ -47,12 +44,8 @@ def __str2none(x): 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 +class MRun(Run): + exc = GitMergeException # use a custom exception class on errors def __checkout_files(orig_hash, file1_hash, file2_hash, path, @@ -61,22 +54,31 @@ def __checkout_files(orig_hash, file1_hash, file2_hash, """ global orig, src1, src2 + extensions = file_extensions() + if orig_hash: - orig = path + file_extensions()['ancestor'] - tmp = __output('git-unpack-file %s' % 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 + file_extensions()['current'] - tmp = __output('git-unpack-file %s' % 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 + file_extensions()['patched'] - tmp = __output('git-unpack-file %s' % 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) + def __remove_files(orig_hash, file1_hash, file2_hash): """Remove any temporary files """ @@ -86,7 +88,6 @@ def __remove_files(orig_hash, file1_hash, file2_hash): os.remove(src1) if file2_hash: os.remove(src2) - pass def __conflict(path): """Write the conflict file for the 'path' variable and exit @@ -94,6 +95,53 @@ def __conflict(path): append_string(os.path.join(basedir.get(), 'conflicts'), path) +def interactive_merge(filename): + """Run the interactive merger on the given file. Note that the + index should not have any conflicts. + """ + extensions = file_extensions() + + ancestor = filename + extensions['ancestor'] + current = filename + extensions['current'] + patched = filename + extensions['patched'] + + if os.path.isfile(ancestor): + three_way = True + files_dict = {'branch1': current, + 'ancestor': ancestor, + 'branch2': patched, + 'output': filename} + imerger = config.get('stgit.i3merge') + else: + three_way = False + files_dict = {'branch1': current, + 'branch2': patched, + 'output': filename} + imerger = config.get('stgit.i2merge') + + if not imerger: + raise GitMergeException, 'No interactive merge command configured' + + # check whether we have all the files for the merge + for fn in [filename, current, patched]: + if not os.path.isfile(fn): + raise GitMergeException, \ + 'Cannot run the interactive merge: "%s" missing' % fn + + mtime = os.path.getmtime(filename) + + out.info('Trying the interactive %s merge' + % (three_way and 'three-way' or 'two-way')) + + err = os.system(imerger % files_dict) + if err != 0: + raise GitMergeException, 'The interactive merge failed: %d' % err + if not os.path.isfile(filename): + raise GitMergeException, 'The "%s" file is missing' % filename + if mtime == os.path.getmtime(filename): + raise GitMergeException, 'The "%s" file was not modified' % filename + + # # Main algorithm # @@ -114,40 +162,57 @@ def merge(orig_hash, file1_hash, file2_hash, 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' + out.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' + out.error('git-checkout-index failed') __conflict(path) return 1 if file1_mode != file2_mode: - print >> sys.stderr, \ - 'Error: File added in both, permissions conflict' + out.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 + 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: - print >> sys.stderr, \ - 'Error: three-way merge tool failed for file "%s"' \ - % path + 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 keeporig != 'yes': + + 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) - __conflict(path) - return 1 + 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: @@ -190,27 +255,46 @@ def merge(orig_hash, file1_hash, file2_hash, 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' + out.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' + out.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 + out.error('File "s" added in both, permissions conflict' + % path) __conflict(path) return 1 - # files are different + # files added in both but different else: - # reset the index to the current file - os.system('git-update-index -- %s' % path) - print >> sys.stderr, \ - 'Error: File "%s" added in branches but different' % path - __conflict(path) - return 1 + 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: @@ -221,17 +305,16 @@ def merge(orig_hash, file1_hash, file2_hash, 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' + 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 - 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) + 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