diff --git a/src/scripts/merge_tools/gitmerge_ort.sh b/src/scripts/merge_tools/gitmerge_ort.sh index a72d9ee50c..d8a695734b 100755 --- a/src/scripts/merge_tools/gitmerge_ort.sh +++ b/src/scripts/merge_tools/gitmerge_ort.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s ort" -if ! "$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/gitmerge_ort_ignorespace.sh b/src/scripts/merge_tools/gitmerge_ort_ignorespace.sh index a085edcaa6..ef1a5df5f8 100755 --- a/src/scripts/merge_tools/gitmerge_ort_ignorespace.sh +++ b/src/scripts/merge_tools/gitmerge_ort_ignorespace.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s ort -Xignore-space-change" -if ! "$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/gitmerge_ort_imports.sh b/src/scripts/merge_tools/gitmerge_ort_imports.sh index cf0309d91f..eb83295136 100755 --- a/src/scripts/merge_tools/gitmerge_ort_imports.sh +++ b/src/scripts/merge_tools/gitmerge_ort_imports.sh @@ -13,5 +13,9 @@ fi cd "$clone_dir" || exit 1 if ! "$MERGE_SCRIPTS_DIR"/resolve-import-conflicts; then + echo "Conflict" + git merge --abort exit 1 fi + +exit 0 diff --git a/src/scripts/merge_tools/gitmerge_ort_imports_ignorespace.sh b/src/scripts/merge_tools/gitmerge_ort_imports_ignorespace.sh index b6ae36b948..41ee46a2e1 100755 --- a/src/scripts/merge_tools/gitmerge_ort_imports_ignorespace.sh +++ b/src/scripts/merge_tools/gitmerge_ort_imports_ignorespace.sh @@ -13,5 +13,9 @@ fi cd "$clone_dir" || exit 1 if ! "$MERGE_SCRIPTS_DIR"/resolve-import-conflicts; then + echo "Conflict" + git merge --abort exit 1 fi + +exit 0 diff --git a/src/scripts/merge_tools/gitmerge_recursive_histogram.sh b/src/scripts/merge_tools/gitmerge_recursive_histogram.sh index 3d8238775a..6453c6e00e 100755 --- a/src/scripts/merge_tools/gitmerge_recursive_histogram.sh +++ b/src/scripts/merge_tools/gitmerge_recursive_histogram.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s recursive -Xdiff-algorithm=histogram" -if ! "$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/gitmerge_recursive_ignorespace.sh b/src/scripts/merge_tools/gitmerge_recursive_ignorespace.sh index 519db73d40..812d2dcd18 100755 --- a/src/scripts/merge_tools/gitmerge_recursive_ignorespace.sh +++ b/src/scripts/merge_tools/gitmerge_recursive_ignorespace.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s recursive -Xignore-space-change" -if ! "$MERGE_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/gitmerge_recursive_minimal.sh b/src/scripts/merge_tools/gitmerge_recursive_minimal.sh index e5a6edff9f..e7d916539a 100755 --- a/src/scripts/merge_tools/gitmerge_recursive_minimal.sh +++ b/src/scripts/merge_tools/gitmerge_recursive_minimal.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s recursive -Xdiff-algorithm=minimal" -if ! "$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/gitmerge_recursive_myers.sh b/src/scripts/merge_tools/gitmerge_recursive_myers.sh index 3fafe5e977..f95f093ba7 100755 --- a/src/scripts/merge_tools/gitmerge_recursive_myers.sh +++ b/src/scripts/merge_tools/gitmerge_recursive_myers.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s recursive -Xdiff-algorithm=myers" -if ! "$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/gitmerge_recursive_patience.sh b/src/scripts/merge_tools/gitmerge_recursive_patience.sh index ef47dc2cab..77e5a8e449 100755 --- a/src/scripts/merge_tools/gitmerge_recursive_patience.sh +++ b/src/scripts/merge_tools/gitmerge_recursive_patience.sh @@ -7,6 +7,4 @@ clone_dir=$1 branch1=$2 branch2=$3 strategy="-s recursive -Xdiff-algorithm=patience" -if ! "$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy"; then - exit 1 -fi +"$MERGE_SCRIPTS_DIR"/gitmerge.sh "$clone_dir" "$branch1" "$branch2" "$strategy" diff --git a/src/scripts/merge_tools/resolve-conflicts.py b/src/scripts/merge_tools/resolve-conflicts.py new file mode 100755 index 0000000000..ef1781cea5 --- /dev/null +++ b/src/scripts/merge_tools/resolve-conflicts.py @@ -0,0 +1,273 @@ +#! /usr/bin/env python + +# This is a helper script for `resolve-adjacent-conflicts` and +# `resolve-import-conflicts`. + +"""Edits a file in place to remove certain conflict markers. + +Usage: resolve-conflicts.py [options] + +--java_imports: Resolves conflicts related to Java import statements +The output includes every `import` statements that is in either of the parents. + +--adjacent_lines: Resolves conflicts on adjacent lines, by accepting both edits. + +Exit status is 0 (success) if no conflicts remain. +Exit status is 1 (failure) if conflicts remain. +""" + +from argparse import ArgumentParser +import itertools +import os +import shutil +import sys +import tempfile + +arg_parser = ArgumentParser() +arg_parser.add_argument("filename") +arg_parser.add_argument( + "--java_imports", + action="store_true", + help="If set, resolve conflicts related to Java import statements", +) +arg_parser.add_argument( + "--adjacent_lines", + action="store_true", + help="If set, resolve conflicts on adjacent lines", +) +args = arg_parser.parse_args() + +# Global variables: `filename` and `lines`. + +filename = args.filename + +with open(filename) as file: + lines = file.readlines() + + +def main(): + """The main entry point.""" + # Exit status 0 means no conflicts remain, 1 means some merge conflict remains. + conflicts_remain = False + with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp: + file_len = len(lines) + i = 0 + while i < file_len: + conflict = looking_at_conflict(i, lines) + if conflict is None: + tmp.write(lines[i]) + i = i + 1 + else: + (base, parent1, parent2, num_lines) = conflict + merged = merge(base, parent1, parent2) + if merged is None: + tmp.write(lines[i]) + i = i + 1 + conflicts_remain = True + else: + for line in merged: + tmp.write(line) + i = i + num_lines + + tmp.close() + shutil.copy(tmp.name, filename) + os.unlink(tmp.name) + + if conflicts_remain: + sys.exit(1) + else: + sys.exit(0) + + +def looking_at_conflict(start_index, lines): # pylint: disable=R0911 + """Tests whether the following text starts a conflict. + If not, returns None. + If so, returns a 4-tuple of (base, parent1, parent2, num_lines_in_conflict) + where the first 3 elements of the tuple are lists of lines. + """ + + if not lines[start_index].startswith("<<<<<<<"): + return None + + base = [] + parent1 = [] + parent2 = [] + + num_lines = len(lines) + index = start_index + 1 + if index == num_lines: + return None + while not ( + lines[index].startswith("|||||||") or lines[index].startswith("=======") + ): + parent1.append(lines[index]) + index = index + 1 + if index == num_lines: + print( + "Starting at line " + + start_index + + ", did not find ||||||| or ======= in " + + filename + ) + return None + if lines[index].startswith("|||||||"): + index = index + 1 + if index == num_lines: + print("File ends with |||||||: " + filename) + return None + while not lines[index].startswith("======="): + base.append(lines[index]) + index = index + 1 + if index == num_lines: + print( + "Starting at line " + + start_index + + ", did not find ======= in " + + filename + ) + return None + assert lines[index].startswith("=======") + index = index + 1 # skip over "=======" line + if index == num_lines: + print("File ends with =======: " + filename) + return None + while not lines[index].startswith(">>>>>>>"): + parent2.append(lines[index]) + index = index + 1 + if index == num_lines: + print( + "Starting at line " + + start_index + + ", did not find >>>>>>> in " + + filename + ) + return None + index = index + 1 + + return (base, parent1, parent2, index - start_index) + + +def merge(base, parent1, parent2): + """Given text for the base and two parents, return merged text. + + Args: + base: a list of lines + parent1: a list of lines + parent2: a list of lines + + Returns: + a list of lines, or None if it cannot do merging. + """ + + print(base, parent1, parent2) + + if args.java_imports: + if ( + all_import_lines(base) + and all_import_lines(parent1) + and all_import_lines(parent2) + ): + # A simplistic merge that retains all import lines in either parent. + return list(set(parent1 + parent2)).sort() + + if args.adjacent_lines: + adjacent_line_merge = merge_edits_on_different_lines(base, parent1, parent2) + if adjacent_line_merge is not None: + return adjacent_line_merge + + return None + + +def all_import_lines(lines): + """Return true if every given line is a Java import line or is blank.""" + return all(line.startswith("import ") or line.strip() == "" for line in lines) + + +def merge_edits_on_different_lines(base, parent1, parent2): + """Return a merged version, if at most parent1 or parent2 edits each line. + Otherwise, return None. + """ + + print("Entered merge_edits_on_different_lines") + + ### No lines are added or removed, only modified. + base_len = len(base) + result = None + if base_len == len(parent1) and base_len == len(parent2): + result = [] + for base_line, parent1_line, parent2_line in itertools.zip_longest( + base, parent1, parent2 + ): + print("Considering line:", base_line, parent1_line, parent2_line) + if parent1_line == parent2_line: + result.append(parent1_line) + elif base_line == parent1_line: + result.append(parent2_line) + elif base_line == parent2_line: + result.append(parent1_line) + else: + result = None + break + print("merge_edits_on_different_lines =>", result) + if result is not None: + print("merge_edits_on_different_lines =>", result) + return result + + ### Deletions at the beginning or end. + if base_len != 0: + result = merge_base_is_prefix_or_suffix(base, parent1, parent2) + if result is None: + result = merge_base_is_prefix_or_suffix(base, parent2, parent1) + if result is not None: + return result + + ### Interleaved deletions, with an empty merge outcome. + if base_len != 0: + if issubsequence(parent1, base) and issubsequence(parent2, base): + return [] + + print("merge_edits_on_different_lines =>", result) + return result + + +def merge_base_is_prefix_or_suffix(base, parent1, parent2): + """Special cases when the base is a prefix or suffix of parent1. + That is, parent1 is pure additions at the beginning or end of base. Parent2 + deleted all the lines, possibly replacing them by something else. (We know + this because there is no common line in base and parent2. If there were, it + would also be in parent1, and the hunk would have been split into two at the + common line that's in all three texts.) + We know the relative position of the additions in parent1. + """ + base_len = len(base) + parent1_len = len(parent1) + parent2_len = len(parent2) + if base_len < parent1_len: + if parent1[:base_len] == base: + print("startswith", parent1, base) + return parent2 + parent1[base_len:] + if parent1[-base_len:] == base: + print("endswith", parent1, base) + return parent1[:-base_len] + parent2 + return None + + +def issubsequence(s1, s2): + """Returns true if s1 is subsequence of s2.""" + + # Iterative implementation. + + n, m = len(s1), len(s2) + i, j = 0, 0 + while i < n and j < m: + if s1[i] == s2[j]: + i += 1 + j += 1 + + # If i reaches end of s1, we found all characters of s1 in s2, + # so s1 is a subsequence of s2. + return i == n + + +if __name__ == "__main__": + main() diff --git a/src/scripts/merge_tools/resolve-import-conflicts b/src/scripts/merge_tools/resolve-import-conflicts index fc4605f54a..73db46473d 100755 --- a/src/scripts/merge_tools/resolve-import-conflicts +++ b/src/scripts/merge_tools/resolve-import-conflicts @@ -1,17 +1,24 @@ #!/bin/bash # bash, not POSIX sh, because of "readarray". -# This script edits files to remove conflict markers related to Java imports. -# It works on all files given on the command line; -# if none are given, it works on all files in or under the current directory. +# This script edits files in place to remove conflict markers related to Java +# imports. For a given conflict that involves only `import` statements and +# blank lines, the output includes every `import` statement that is in either +# of the parents. This script leaves other conflicts untouched. + +# Usage: +# resolve-import-conflicts [file ...] +# +# The script works on all files given on the command line. +# If none are given, the script works on all files in or under the current directory. +# +# The exit status code is 0 (success) if all conflicts are resolved in all the files. +# The exit status code is 1 (failure) if any conflict remains. # This script is not a git mergetool. A git mergetool is given the base, parent 1, and # parent 2 files, all without conflict markers. -# However, this script can be run instead of a git mergetool, or after a git mergetool. - -# Exit status is 1 if conflicts remain after running this script or if -# there is an error generated in some file. -# Exit status is 0 if there are no conflicts after running this script. +# However, this can be run after a git mergetool that leaves conflict markers +# in files, as the default git mergetool does. if [ "$#" -eq 0 ] ; then readarray -t files < <(grep -l -r '^<<<<<<< HEAD' .) @@ -21,20 +28,12 @@ fi SCRIPTDIR="$(cd "$(dirname "$0")" && pwd -P)" +status=0 + for file in "${files[@]}" ; do - if ! "${SCRIPTDIR}"/resolve-import-conflicts-in-file.py "$file" ; then - echo "Error in $file" - git merge --abort - exit 1 + if ! "${SCRIPTDIR}"/resolve-conflicts.py --java_imports "$file" ; then + status=1 fi done -# From https://stackoverflow.com/questions/41246415/ -if git diff --exit-code -S '<<<<<<< HEAD' -S "=======" -S ">>>>>>> $(git name-rev --name-only MERGE_HEAD)" HEAD ; then - exit 0 -fi - -echo "Conflict" -git merge --abort -exit 1 - +exit $status diff --git a/src/scripts/merge_tools/resolve-import-conflicts-in-file.py b/src/scripts/merge_tools/resolve-import-conflicts-in-file.py deleted file mode 100755 index 2398ec4586..0000000000 --- a/src/scripts/merge_tools/resolve-import-conflicts-in-file.py +++ /dev/null @@ -1,153 +0,0 @@ -#! /usr/bin/env python - -"""Edits a file in place to remove conflict markers related to Java imports. -The merged version contains all the imports that appear in either parent. -This is simplistic, but is often adequate. - -Exit status is 1 only if the program halts exceptionally. -With an exit status of 0, some conflicts may still exist in the file. -""" - - -# TODO: merge both scripts into one. - -import os -import shutil -import sys -import tempfile - -if len(sys.argv) != 2: - print( - "resolve-import-conflicts-in-file: Provide exactly one command-line argument." - ) - sys.exit(1) - -filename = sys.argv[1] -with open(filename) as file: - lines = file.readlines() - - -def all_import_lines(lines): - """Return true if every line is a Java import line.""" - return all(line.startswith("import ") for line in lines) - - -def merge(base, parent1, parent2): - """Given text for the base and two parents, return merged text. - - This currently does a simplistic merge that retains all lines in either parent. - - Args: - base: a list of lines - parent1: a list of lines - parent2: a list of lines - - Returns: - a list of lines, or None if it cannot do merging. - """ - - if ( - all_import_lines(base) - and all_import_lines(parent1) - and all_import_lines(parent2) - ): - return parent1 + parent2 - - return None - - -def looking_at_conflict(start_index, lines): # pylint: disable=R0911 - """Tests whether the text starting at line `start_index` is the beginning of a conflict. - If not, returns None. - If so, returns a 4-tuple of (base, parent1, parent2, num_lines_in_conflict) - where the first 3 elements of the tuple are lists of lines. - Args: - start_index: an index into `lines`. - lines: all the lines of the file with name `filename`. - """ - - if not lines[start_index].startswith("<<<<<<<"): - return None - - base = [] - parent1 = [] - parent2 = [] - - num_lines = len(lines) - index = start_index + 1 - if index == num_lines: - return None - while not ( - lines[index].startswith("|||||||") or lines[index].startswith("=======") - ): - parent1.append(lines[index]) - index = index + 1 - if index == num_lines: - print( - "Starting at line " - + start_index - + ", did not find ||||||| or ======= in " - + filename - ) - return None - if lines[index].startswith("|||||||"): - index = index + 1 - if index == num_lines: - print("File ends with |||||||: " + filename) - return None - while not lines[index].startswith("======="): - base.append(lines[index]) - index = index + 1 - if index == num_lines: - print( - "Starting at line " - + start_index - + ", did not find ======= in " - + filename - ) - return None - assert lines[index].startswith("=======") - index = index + 1 # skip over "=======" line - if index == num_lines: - print("File ends with =======: " + filename) - return None - while not lines[index].startswith(">>>>>>>"): - parent2.append(lines[index]) - index = index + 1 - if index == num_lines: - print( - "Starting at line " - + start_index - + ", did not find >>>>>>> in " - + filename - ) - return None - index = index + 1 - - return (base, parent1, parent2, index - start_index) - - -## Main starts here. - -with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp: - file_len = len(lines) - i = 0 - while i < file_len: - conflict = looking_at_conflict(i, lines) - if conflict is None: - tmp.write(lines[i]) - i = i + 1 - else: - (base, parent1, parent2, num_lines) = conflict - merged = merge(base, parent1, parent2) - if merged is None: - tmp.write(lines[i]) - i = i + 1 - else: - for line in merged: - tmp.write(line) - i = i + num_lines - - tmp.close() - shutil.copy(tmp.name, filename) - os.unlink(tmp.name)