X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/fe692f2f318f566a6d21b1eacda5ce9658647570..5b428fce4566ed627ad61f204935aaa8bc367932:/wizard/merge.py diff --git a/wizard/merge.py b/wizard/merge.py index 94cc13c..1b99a7a 100644 --- a/wizard/merge.py +++ b/wizard/merge.py @@ -71,18 +71,6 @@ def git_diff_text(a, b): files.append(name) return files -def git_newline_style(rev, name): - """ - Returns the newline style for a blob, identified by Git revision - ``rev`` and filename ``name``. - """ - f = tempfile.NamedTemporaryFile(prefix="wizardResolve", delete=False) - shell.call("git", "cat-file", "blob", "%s:%s" % (rev, name), stdout=f) - f.close() - nl = get_newline(f.name) - os.unlink(f.name) - return nl - # only works on Unix def get_newline(filename): """ @@ -136,48 +124,94 @@ def merge(common_id, theirs_id, so we require the common and theirs commits, instead of using the normal Git algorithm. """ + if prepare_config is None: prepare_config = lambda: None + if resolve_conflicts is None: resolve_conflicts = lambda: False + ours_id = shell.eval("git", "rev-parse", "HEAD") - theirs_newline_cache = {} - def get_theirs_newline(file): - if file not in theirs_newline_cache: - nl = git_newline_style(theirs_id, file) - if not isinstance(nl, str): + ours_theirs_diff = git_diff_text(ours_id, theirs_id) + + # What files can the merge fail on? Only if ours is different from + # theirs (we don't care about common for this calculation). Of + # course, this will be conservative, because we need to apply + # prepare_config to ours. Can we miss a file? Not unless + # prepare_config introduces a merge conflict, as opposed to + # eliminates them; and it is equally likely to do so on common_id as + # well. We can deal, since we offer the user the ability to resolve + # merges manually. + theirs_newlines = {} + shell.call("git", "reset", "--hard", theirs_id) + for file in ours_theirs_diff: + # XXX Should be able to skip stats if file was removed + # for the ours tree + try: + nl = get_newline(file) + except IOError: + # File not present in theirs, don't bother + continue + if not isinstance(nl, str): + if nl is not None: # A case of incompetent upstream, unfortunately logging.warning("Canonical version (theirs) of %s has mixed newline style, forced to \\n", file) - nl = "\n" - theirs_newline_cache[file] = nl - return theirs_newline_cache[file] + else: + logging.debug("Canonical version (theirs) of %s had no newline style, using \\n", file) + nl = "\n" + theirs_newlines[file] = nl + + shell.call("git", "reset", "--hard", ours_id) theirs_tree = shell.eval("git", "rev-parse", "%s^{tree}" % theirs_id) - # operations on the ours tree - for file in git_diff_text(ours_id, theirs_id): - theirs_nl = get_theirs_newline(file) - ours_nl = get_newline(file) # current checkout is ours_id + for file in ours_theirs_diff: + try: + theirs_nl = theirs_newlines[file] + except KeyError: + # No need to handle newlines + continue + try: + ours_nl = get_newline(file) # current checkout is ours_id + except (IOError, shell.CallError): # hack + continue if theirs_nl != ours_nl: - logging.info("Converting our file (3) from %s to %s newlines", repr(ours_nl), repr(theirs_nl)) - convert_newline(file, theirs_nl) - shell.eval("git", "add", file) + if ours_nl is None: + logging.debug("Our file %s had no newlines, ignoring newline style", file) + else: + logging.info("Converting our file %s (3) from %s to %s newlines", file, repr(ours_nl), repr(theirs_nl)) + convert_newline(file, theirs_nl) + shell.eval("git", "add", file) # XXX batch this prepare_config() # for Wizard, this usually genericizes config files ours_tree = shell.eval("git", "write-tree") logging.info("Merge wrote virtual tree for ours: %s", ours_tree) - # operations on the common tree + + # operations on the common tree (pretty duplicate with the above) shell.call("git", "reset", "--hard", common_id) for file in git_diff_text(common_id, theirs_id): - theirs_nl = get_theirs_newline(file) - common_nl = get_newline(file) # current checkout is common_id + try: + theirs_nl = theirs_newlines[file] + except KeyError: + # The merge trivially succeeds, so don't bother. + logging.debug("Merge trivially succeeds for %s, ignoring line check", file) + continue + try: + common_nl = get_newline(file) # current checkout is common_id + except (IOError, shell.CallError): # hack + continue if theirs_nl != common_nl: - logging.info("Converting common file (1) from %s to %s newlines", repr(common_nl), repr(theirs_nl)) - convert_newline(file, theirs_nl) - shell.eval("git", "add", file) + if common_nl is None: + logging.debug("Common file %s had no newlines, ignoring newline style", file) + else: + logging.info("Converting common file %s (1) from %s to %s newlines", file, repr(common_nl), repr(theirs_nl)) + convert_newline(file, theirs_nl) + shell.eval("git", "add", file) # XXX batch common_tree = shell.eval("git", "write-tree") logging.info("Merge wrote virtual tree for common: %s", common_tree) + # construct merge commit graph common_virtual_id = git_commit_tree(common_tree) ours_virtual_id = git_commit_tree(ours_tree, common_virtual_id) theirs_virtual_id = git_commit_tree(theirs_tree, common_virtual_id) + # perform merge shell.call("git", "reset", "--hard", ours_virtual_id) try: @@ -189,6 +223,7 @@ def merge(common_id, theirs_id, shell.call("git", "commit", "-a", "-m", "merge") else: raise MergeError + # post-merge operations result_tree = shell.eval("git", "write-tree") logging.info("Resolution tree is %s", result_tree)