From: Catalin Marinas Date: Wed, 19 Dec 2007 18:00:15 +0000 (+0000) Subject: Re-add the interactive merge X-Git-Tag: v0.15-rc1~321 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/commitdiff_plain/29197bc02c1b5087017da9a86214e9b620f0439e Re-add the interactive merge 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 --- diff --git a/examples/gitconfig b/examples/gitconfig index 52d2a69..cd9b569 100644 --- a/examples/gitconfig +++ b/examples/gitconfig @@ -60,13 +60,6 @@ # 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 \ diff --git a/stgit/commands/common.py b/stgit/commands/common.py index 2179684..7d9df02 100644 --- a/stgit/commands/common.py +++ b/stgit/commands/common.py @@ -145,18 +145,9 @@ def print_crt_patch(crt_series, branch = None): 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() - 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 diff --git a/stgit/commands/resolved.py b/stgit/commands/resolved.py index adc591f..4ee75b8 100644 --- a/stgit/commands/resolved.py +++ b/stgit/commands/resolved.py @@ -79,4 +79,6 @@ def func(parser, options, args): if options.interactive: for filename in files: interactive_merge(filename) - resolved(files, options.reset) + git.resolved([filename]) + else: + git.resolved(files, options.reset) diff --git a/stgit/commands/status.py b/stgit/commands/status.py index 370e033..708dd16 100644 --- a/stgit/commands/status.py +++ b/stgit/commands/status.py @@ -109,7 +109,8 @@ def func(parser, options, 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() diff --git a/stgit/commands/sync.py b/stgit/commands/sync.py index e8e3bac..99ab7de 100644 --- a/stgit/commands/sync.py +++ b/stgit/commands/sync.py @@ -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) - 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. diff --git a/stgit/config.py b/stgit/config.py index 61f91b0..1d71cd2 100644 --- a/stgit/config.py +++ b/stgit/config.py @@ -34,8 +34,6 @@ class GitConfig: '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', diff --git a/stgit/git.py b/stgit/git.py index 6629e39..0cfcd42 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -43,15 +43,6 @@ class GRun(Run): """ 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 # @@ -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() - 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')] - 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): @@ -890,6 +828,17 @@ def reset(files = None, tree_id = None, check_out = True): 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. @@ -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) - 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 diff --git a/stgit/gitmergeonefile.py b/stgit/gitmergeonefile.py index c85e347..1fe226e 100644 --- a/stgit/gitmergeonefile.py +++ b/stgit/gitmergeonefile.py @@ -33,7 +33,7 @@ class GitMergeException(StgException): # # Options # -merger = ConfigOption('stgit', 'merger') +autoimerge = ConfigOption('stgit', 'autoimerge') keeporig = ConfigOption('stgit', 'keeporig') # @@ -48,72 +48,46 @@ def __str2none(x): 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() + 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: - 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') @@ -148,183 +122,29 @@ def interactive_merge(filename): 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 diff --git a/stgit/stack.py b/stgit/stack.py index 4203931..7fe9f2b 100644 --- a/stgit/stack.py +++ b/stgit/stack.py @@ -1019,8 +1019,6 @@ class Series(PatchSet): # 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".') diff --git a/t/t0002-status.sh b/t/t0002-status.sh index 9b4e60d..0a70f15 100755 --- a/t/t0002-status.sh +++ b/t/t0002-status.sh @@ -107,6 +107,9 @@ test_expect_success 'Make a conflicting patch' ' ' cat > expected.txt <