diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 14:47:53 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 14:47:53 +0000 |
commit | c8bae7493d2f2910b57f13ded012e86bdcfb0532 (patch) | |
tree | 24e09d9f84dec336720cf393e156089ca2835791 /git-mergetool.sh | |
parent | Initial commit. (diff) | |
download | git-c8bae7493d2f2910b57f13ded012e86bdcfb0532.tar.xz git-c8bae7493d2f2910b57f13ded012e86bdcfb0532.zip |
Adding upstream version 1:2.39.2.upstream/1%2.39.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rwxr-xr-x | git-mergetool.sh | 574 |
1 files changed, 574 insertions, 0 deletions
diff --git a/git-mergetool.sh b/git-mergetool.sh new file mode 100755 index 0000000..f751d9c --- /dev/null +++ b/git-mergetool.sh @@ -0,0 +1,574 @@ +#!/bin/sh +# +# This program resolves merge conflicts in git +# +# Copyright (c) 2006 Theodore Y. Ts'o +# Copyright (c) 2009-2016 David Aguilar +# +# This file is licensed under the GPL v2, or a later version +# at the discretion of Junio C Hamano. +# + +USAGE='[--tool=tool] [--tool-help] [-y|--no-prompt|--prompt] [-g|--gui|--no-gui] [-O<orderfile>] [file to merge] ...' +SUBDIRECTORY_OK=Yes +NONGIT_OK=Yes +OPTIONS_SPEC= +TOOL_MODE=merge +. git-sh-setup +. git-mergetool--lib + +# Returns true if the mode reflects a symlink +is_symlink () { + test "$1" = 120000 +} + +is_submodule () { + test "$1" = 160000 +} + +local_present () { + test -n "$local_mode" +} + +remote_present () { + test -n "$remote_mode" +} + +base_present () { + test -n "$base_mode" +} + +mergetool_tmpdir_init () { + if test "$(git config --bool mergetool.writeToTemp)" != true + then + MERGETOOL_TMPDIR=. + return 0 + fi + if MERGETOOL_TMPDIR=$(mktemp -d -t "git-mergetool-XXXXXX" 2>/dev/null) + then + return 0 + fi + die "error: mktemp is needed when 'mergetool.writeToTemp' is true" +} + +cleanup_temp_files () { + if test "$1" = --save-backup + then + rm -rf -- "$MERGED.orig" + test -e "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig" + rm -f -- "$LOCAL" "$REMOTE" "$BASE" + else + rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP" + fi + if test "$MERGETOOL_TMPDIR" != "." + then + rmdir "$MERGETOOL_TMPDIR" + fi +} + +describe_file () { + mode="$1" + branch="$2" + file="$3" + + printf " {%s}: " "$branch" + if test -z "$mode" + then + echo "deleted" + elif is_symlink "$mode" + then + echo "a symbolic link -> '$(cat "$file")'" + elif is_submodule "$mode" + then + echo "submodule commit $file" + elif base_present + then + echo "modified file" + else + echo "created file" + fi +} + +resolve_symlink_merge () { + while true + do + printf "Use (l)ocal or (r)emote, or (a)bort? " + read ans || return 1 + case "$ans" in + [lL]*) + git checkout-index -f --stage=2 -- "$MERGED" + git add -- "$MERGED" + cleanup_temp_files --save-backup + return 0 + ;; + [rR]*) + git checkout-index -f --stage=3 -- "$MERGED" + git add -- "$MERGED" + cleanup_temp_files --save-backup + return 0 + ;; + [aA]*) + return 1 + ;; + esac + done +} + +resolve_deleted_merge () { + while true + do + if base_present + then + printf "Use (m)odified or (d)eleted file, or (a)bort? " + else + printf "Use (c)reated or (d)eleted file, or (a)bort? " + fi + read ans || return 1 + case "$ans" in + [mMcC]*) + git add -- "$MERGED" + if test "$merge_keep_backup" = "true" + then + cleanup_temp_files --save-backup + else + cleanup_temp_files + fi + return 0 + ;; + [dD]*) + git rm -- "$MERGED" > /dev/null + cleanup_temp_files + return 0 + ;; + [aA]*) + if test "$merge_keep_temporaries" = "false" + then + cleanup_temp_files + fi + return 1 + ;; + esac + done +} + +resolve_submodule_merge () { + while true + do + printf "Use (l)ocal or (r)emote, or (a)bort? " + read ans || return 1 + case "$ans" in + [lL]*) + if ! local_present + then + if test -n "$(git ls-tree HEAD -- "$MERGED")" + then + # Local isn't present, but it's a subdirectory + git ls-tree --full-name -r HEAD -- "$MERGED" | + git update-index --index-info || exit $? + else + test -e "$MERGED" && mv -- "$MERGED" "$BACKUP" + git update-index --force-remove "$MERGED" + cleanup_temp_files --save-backup + fi + elif is_submodule "$local_mode" + then + stage_submodule "$MERGED" "$local_sha1" + else + git checkout-index -f --stage=2 -- "$MERGED" + git add -- "$MERGED" + fi + return 0 + ;; + [rR]*) + if ! remote_present + then + if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")" + then + # Remote isn't present, but it's a subdirectory + git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" | + git update-index --index-info || exit $? + else + test -e "$MERGED" && mv -- "$MERGED" "$BACKUP" + git update-index --force-remove "$MERGED" + fi + elif is_submodule "$remote_mode" + then + ! is_submodule "$local_mode" && + test -e "$MERGED" && + mv -- "$MERGED" "$BACKUP" + stage_submodule "$MERGED" "$remote_sha1" + else + test -e "$MERGED" && mv -- "$MERGED" "$BACKUP" + git checkout-index -f --stage=3 -- "$MERGED" + git add -- "$MERGED" + fi + cleanup_temp_files --save-backup + return 0 + ;; + [aA]*) + return 1 + ;; + esac + done +} + +stage_submodule () { + path="$1" + submodule_sha1="$2" + mkdir -p "$path" || + die "fatal: unable to create directory for module at $path" + # Find $path relative to work tree + work_tree_root=$(cd_to_toplevel && pwd) + work_rel_path=$(cd "$path" && + GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix + ) + test -n "$work_rel_path" || + die "fatal: unable to get path of module $path relative to work tree" + git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die +} + +checkout_staged_file () { + tmpfile="$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" && + tmpfile=${tmpfile%%' '*} + + if test $? -eq 0 && test -n "$tmpfile" + then + mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3" + else + >"$3" + fi +} + +hide_resolved () { + git merge-file --ours -q -p "$LOCAL" "$BASE" "$REMOTE" >"$LCONFL" + git merge-file --theirs -q -p "$LOCAL" "$BASE" "$REMOTE" >"$RCONFL" + mv -- "$LCONFL" "$LOCAL" + mv -- "$RCONFL" "$REMOTE" +} + +merge_file () { + MERGED="$1" + + f=$(git ls-files -u -- "$MERGED") + if test -z "$f" + then + if test ! -f "$MERGED" + then + echo "$MERGED: file not found" + else + echo "$MERGED: file does not need merging" + fi + return 1 + fi + + # extract file extension from the last path component + case "${MERGED##*/}" in + *.*) + ext=.${MERGED##*.} + BASE=${MERGED%"$ext"} + ;; + *) + BASE=$MERGED + ext= + esac + + initialize_merge_tool "$merge_tool" || return + + mergetool_tmpdir_init + + if test "$MERGETOOL_TMPDIR" != "." + then + # If we're using a temporary directory then write to the + # top-level of that directory. + BASE=${BASE##*/} + fi + + BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext" + LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext" + LCONFL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_LCONFL_$$$ext" + REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext" + RCONFL="$MERGETOOL_TMPDIR/${BASE}_REMOTE_RCONFL_$$$ext" + BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext" + + base_mode= local_mode= remote_mode= + + # here, $IFS is just a LF + for line in $f + do + mode=${line%% *} # 1st word + sha1=${line#"$mode "} + sha1=${sha1%% *} # 2nd word + case "${line#$mode $sha1 }" in # remainder + '1 '*) + base_mode=$mode + ;; + '2 '*) + local_mode=$mode local_sha1=$sha1 + ;; + '3 '*) + remote_mode=$mode remote_sha1=$sha1 + ;; + esac + done + + if is_submodule "$local_mode" || is_submodule "$remote_mode" + then + echo "Submodule merge conflict for '$MERGED':" + describe_file "$local_mode" "local" "$local_sha1" + describe_file "$remote_mode" "remote" "$remote_sha1" + resolve_submodule_merge + return + fi + + if test -f "$MERGED" + then + mv -- "$MERGED" "$BACKUP" + cp -- "$BACKUP" "$MERGED" + fi + # Create a parent directory to handle delete/delete conflicts + # where the base's directory no longer exists. + mkdir -p "$(dirname "$MERGED")" + + checkout_staged_file 1 "$MERGED" "$BASE" + checkout_staged_file 2 "$MERGED" "$LOCAL" + checkout_staged_file 3 "$MERGED" "$REMOTE" + + # hideResolved preferences hierarchy. + global_config="mergetool.hideResolved" + tool_config="mergetool.${merge_tool}.hideResolved" + + if enabled=$(git config --type=bool "$tool_config") + then + # The user has a specific preference for a specific tool and no + # other preferences should override that. + : ; + elif enabled=$(git config --type=bool "$global_config") + then + # The user has a general preference for all tools. + # + # 'true' means the user likes the feature so we should use it + # where possible but tool authors can still override. + # + # 'false' means the user doesn't like the feature so we should + # not use it anywhere. + if test "$enabled" = true && hide_resolved_enabled + then + enabled=true + else + enabled=false + fi + else + # The user does not have a preference. Default to disabled. + enabled=false + fi + + if test "$enabled" = true + then + hide_resolved + fi + + if test -z "$local_mode" || test -z "$remote_mode" + then + echo "Deleted merge conflict for '$MERGED':" + describe_file "$local_mode" "local" "$LOCAL" + describe_file "$remote_mode" "remote" "$REMOTE" + resolve_deleted_merge + status=$? + rmdir -p "$(dirname "$MERGED")" 2>/dev/null + return $status + fi + + if is_symlink "$local_mode" || is_symlink "$remote_mode" + then + echo "Symbolic link merge conflict for '$MERGED':" + describe_file "$local_mode" "local" "$LOCAL" + describe_file "$remote_mode" "remote" "$REMOTE" + resolve_symlink_merge + return + fi + + echo "Normal merge conflict for '$MERGED':" + describe_file "$local_mode" "local" "$LOCAL" + describe_file "$remote_mode" "remote" "$REMOTE" + if test "$guessed_merge_tool" = true || test "$prompt" = true + then + printf "Hit return to start merge resolution tool (%s): " "$merge_tool" + read ans || return 1 + fi + + if base_present + then + present=true + else + present=false + fi + + if ! run_merge_tool "$merge_tool" "$present" + then + echo "merge of $MERGED failed" 1>&2 + mv -- "$BACKUP" "$MERGED" + + if test "$merge_keep_temporaries" = "false" + then + cleanup_temp_files + fi + + return 1 + fi + + if test "$merge_keep_backup" = "true" + then + mv -- "$BACKUP" "$MERGED.orig" + else + rm -- "$BACKUP" + fi + + git add -- "$MERGED" + cleanup_temp_files + return 0 +} + +prompt_after_failed_merge () { + while true + do + printf "Continue merging other unresolved paths [y/n]? " + read ans || return 1 + case "$ans" in + [yY]*) + return 0 + ;; + [nN]*) + return 1 + ;; + esac + done +} + +print_noop_and_exit () { + echo "No files need merging" + exit 0 +} + +main () { + prompt=$(git config --bool mergetool.prompt) + GIT_MERGETOOL_GUI=false + guessed_merge_tool=false + orderfile= + + while test $# != 0 + do + case "$1" in + --tool-help=*) + TOOL_MODE=${1#--tool-help=} + show_tool_help + ;; + --tool-help) + show_tool_help + ;; + -t|--tool*) + case "$#,$1" in + *,*=*) + merge_tool=${1#*=} + ;; + 1,*) + usage ;; + *) + merge_tool="$2" + shift ;; + esac + ;; + --no-gui) + GIT_MERGETOOL_GUI=false + ;; + -g|--gui) + GIT_MERGETOOL_GUI=true + ;; + -y|--no-prompt) + prompt=false + ;; + --prompt) + prompt=true + ;; + -O*) + orderfile="${1#-O}" + ;; + --) + shift + break + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift + done + + git_dir_init + require_work_tree + + if test -z "$merge_tool" + then + if ! merge_tool=$(get_merge_tool) + then + guessed_merge_tool=true + fi + fi + merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)" + merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)" + + prefix=$(git rev-parse --show-prefix) || exit 1 + cd_to_toplevel + + if test -n "$orderfile" + then + orderfile=$( + git rev-parse --prefix "$prefix" -- "$orderfile" | + sed -e 1d + ) + fi + + if test $# -eq 0 && test -e "$GIT_DIR/MERGE_RR" + then + set -- $(git rerere remaining) + if test $# -eq 0 + then + print_noop_and_exit + fi + elif test $# -ge 0 + then + # rev-parse provides the -- needed for 'set' + eval "set $(git rev-parse --sq --prefix "$prefix" -- "$@")" + fi + + files=$(git -c core.quotePath=false \ + diff --name-only --diff-filter=U \ + ${orderfile:+"-O$orderfile"} -- "$@") + + if test -z "$files" + then + print_noop_and_exit + fi + + printf "Merging:\n" + printf "%s\n" "$files" + + rc=0 + set -- $files + while test $# -ne 0 + do + printf "\n" + if ! merge_file "$1" + then + rc=1 + test $# -ne 1 && prompt_after_failed_merge || exit 1 + fi + shift + done + + exit $rc +} + +main "$@" |