Git fork
at reftables-rust 579 lines 12 kB view raw
1#!/bin/sh 2# 3# This program resolves merge conflicts in git 4# 5# Copyright (c) 2006 Theodore Y. Ts'o 6# Copyright (c) 2009-2016 David Aguilar 7# 8# This file is licensed under the GPL v2, or a later version 9# at the discretion of Junio C Hamano. 10# 11 12USAGE='[--tool=tool] [--tool-help] [-y|--no-prompt|--prompt] [-g|--gui|--no-gui] [-O<orderfile>] [file to merge] ...' 13SUBDIRECTORY_OK=Yes 14NONGIT_OK=Yes 15OPTIONS_SPEC= 16TOOL_MODE=merge 17. git-sh-setup 18. git-mergetool--lib 19 20# Returns true if the mode reflects a symlink 21is_symlink () { 22 test "$1" = 120000 23} 24 25is_submodule () { 26 test "$1" = 160000 27} 28 29local_present () { 30 test -n "$local_mode" 31} 32 33remote_present () { 34 test -n "$remote_mode" 35} 36 37base_present () { 38 test -n "$base_mode" 39} 40 41mergetool_tmpdir_init () { 42 if test "$(git config --bool mergetool.writeToTemp)" != true 43 then 44 MERGETOOL_TMPDIR=. 45 return 0 46 fi 47 if MERGETOOL_TMPDIR=$(mktemp -d -t "git-mergetool-XXXXXX" 2>/dev/null) 48 then 49 return 0 50 fi 51 die "error: mktemp is needed when 'mergetool.writeToTemp' is true" 52} 53 54cleanup_temp_files () { 55 if test "$1" = --save-backup 56 then 57 rm -rf -- "$MERGED.orig" 58 test -e "$BACKUP" && mv -- "$BACKUP" "$MERGED.orig" 59 rm -f -- "$LOCAL" "$REMOTE" "$BASE" 60 else 61 rm -f -- "$LOCAL" "$REMOTE" "$BASE" "$BACKUP" 62 fi 63 if test "$MERGETOOL_TMPDIR" != "." 64 then 65 rmdir "$MERGETOOL_TMPDIR" 66 fi 67} 68 69describe_file () { 70 mode="$1" 71 branch="$2" 72 file="$3" 73 74 printf " {%s}: " "$branch" 75 if test -z "$mode" 76 then 77 echo "deleted" 78 elif is_symlink "$mode" 79 then 80 echo "a symbolic link -> '$(cat "$file")'" 81 elif is_submodule "$mode" 82 then 83 echo "submodule commit $file" 84 elif base_present 85 then 86 echo "modified file" 87 else 88 echo "created file" 89 fi 90} 91 92resolve_symlink_merge () { 93 while true 94 do 95 printf "Use (l)ocal or (r)emote, or (a)bort? " 96 read ans || return 1 97 case "$ans" in 98 [lL]*) 99 git checkout-index -f --stage=2 -- "$MERGED" 100 git add -- "$MERGED" 101 cleanup_temp_files --save-backup 102 return 0 103 ;; 104 [rR]*) 105 git checkout-index -f --stage=3 -- "$MERGED" 106 git add -- "$MERGED" 107 cleanup_temp_files --save-backup 108 return 0 109 ;; 110 [aA]*) 111 return 1 112 ;; 113 esac 114 done 115} 116 117resolve_deleted_merge () { 118 while true 119 do 120 if base_present 121 then 122 printf "Use (m)odified or (d)eleted file, or (a)bort? " 123 else 124 printf "Use (c)reated or (d)eleted file, or (a)bort? " 125 fi 126 read ans || return 1 127 case "$ans" in 128 [mMcC]*) 129 git add -- "$MERGED" 130 if test "$merge_keep_backup" = "true" 131 then 132 cleanup_temp_files --save-backup 133 else 134 cleanup_temp_files 135 fi 136 return 0 137 ;; 138 [dD]*) 139 git rm -- "$MERGED" > /dev/null 140 cleanup_temp_files 141 return 0 142 ;; 143 [aA]*) 144 if test "$merge_keep_temporaries" = "false" 145 then 146 cleanup_temp_files 147 fi 148 return 1 149 ;; 150 esac 151 done 152} 153 154resolve_submodule_merge () { 155 while true 156 do 157 printf "Use (l)ocal or (r)emote, or (a)bort? " 158 read ans || return 1 159 case "$ans" in 160 [lL]*) 161 if ! local_present 162 then 163 if test -n "$(git ls-tree HEAD -- "$MERGED")" 164 then 165 # Local isn't present, but it's a subdirectory 166 git ls-tree --full-name -r HEAD -- "$MERGED" | 167 git update-index --index-info || exit $? 168 else 169 test -e "$MERGED" && mv -- "$MERGED" "$BACKUP" 170 git update-index --force-remove "$MERGED" 171 cleanup_temp_files --save-backup 172 fi 173 elif is_submodule "$local_mode" 174 then 175 stage_submodule "$MERGED" "$local_sha1" 176 else 177 git checkout-index -f --stage=2 -- "$MERGED" 178 git add -- "$MERGED" 179 fi 180 return 0 181 ;; 182 [rR]*) 183 if ! remote_present 184 then 185 if test -n "$(git ls-tree MERGE_HEAD -- "$MERGED")" 186 then 187 # Remote isn't present, but it's a subdirectory 188 git ls-tree --full-name -r MERGE_HEAD -- "$MERGED" | 189 git update-index --index-info || exit $? 190 else 191 test -e "$MERGED" && mv -- "$MERGED" "$BACKUP" 192 git update-index --force-remove "$MERGED" 193 fi 194 elif is_submodule "$remote_mode" 195 then 196 ! is_submodule "$local_mode" && 197 test -e "$MERGED" && 198 mv -- "$MERGED" "$BACKUP" 199 stage_submodule "$MERGED" "$remote_sha1" 200 else 201 test -e "$MERGED" && mv -- "$MERGED" "$BACKUP" 202 git checkout-index -f --stage=3 -- "$MERGED" 203 git add -- "$MERGED" 204 fi 205 cleanup_temp_files --save-backup 206 return 0 207 ;; 208 [aA]*) 209 return 1 210 ;; 211 esac 212 done 213} 214 215stage_submodule () { 216 path="$1" 217 submodule_sha1="$2" 218 mkdir -p "$path" || 219 die "fatal: unable to create directory for module at $path" 220 # Find $path relative to work tree 221 work_tree_root=$(cd_to_toplevel && pwd) 222 work_rel_path=$(cd "$path" && 223 GIT_WORK_TREE="${work_tree_root}" git rev-parse --show-prefix 224 ) 225 test -n "$work_rel_path" || 226 die "fatal: unable to get path of module $path relative to work tree" 227 git update-index --add --replace --cacheinfo 160000 "$submodule_sha1" "${work_rel_path%/}" || die 228} 229 230checkout_staged_file () { 231 tmpfile="$(git checkout-index --temp --stage="$1" "$2" 2>/dev/null)" && 232 tmpfile=${tmpfile%%' '*} 233 234 if test $? -eq 0 && test -n "$tmpfile" 235 then 236 mv -- "$(git rev-parse --show-cdup)$tmpfile" "$3" 237 else 238 >"$3" 239 fi 240} 241 242hide_resolved () { 243 git merge-file --ours -q -p "$LOCAL" "$BASE" "$REMOTE" >"$LCONFL" 244 git merge-file --theirs -q -p "$LOCAL" "$BASE" "$REMOTE" >"$RCONFL" 245 mv -- "$LCONFL" "$LOCAL" 246 mv -- "$RCONFL" "$REMOTE" 247} 248 249merge_file () { 250 MERGED="$1" 251 252 f=$(git ls-files -u -- "$MERGED") 253 if test -z "$f" 254 then 255 if test ! -f "$MERGED" 256 then 257 echo "$MERGED: file not found" 258 else 259 echo "$MERGED: file does not need merging" 260 fi 261 return 1 262 fi 263 264 # extract file extension from the last path component 265 case "${MERGED##*/}" in 266 *.*) 267 ext=.${MERGED##*.} 268 BASE=${MERGED%"$ext"} 269 ;; 270 *) 271 BASE=$MERGED 272 ext= 273 esac 274 275 initialize_merge_tool "$merge_tool" || return 276 277 mergetool_tmpdir_init 278 279 if test "$MERGETOOL_TMPDIR" != "." 280 then 281 # If we're using a temporary directory then write to the 282 # top-level of that directory. 283 BASE=${BASE##*/} 284 fi 285 286 BACKUP="$MERGETOOL_TMPDIR/${BASE}_BACKUP_$$$ext" 287 LOCAL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_$$$ext" 288 LCONFL="$MERGETOOL_TMPDIR/${BASE}_LOCAL_LCONFL_$$$ext" 289 REMOTE="$MERGETOOL_TMPDIR/${BASE}_REMOTE_$$$ext" 290 RCONFL="$MERGETOOL_TMPDIR/${BASE}_REMOTE_RCONFL_$$$ext" 291 BASE="$MERGETOOL_TMPDIR/${BASE}_BASE_$$$ext" 292 293 base_mode= local_mode= remote_mode= 294 295 # here, $IFS is just a LF 296 for line in $f 297 do 298 mode=${line%% *} # 1st word 299 sha1=${line#"$mode "} 300 sha1=${sha1%% *} # 2nd word 301 case "${line#$mode $sha1 }" in # remainder 302 '1 '*) 303 base_mode=$mode 304 ;; 305 '2 '*) 306 local_mode=$mode local_sha1=$sha1 307 ;; 308 '3 '*) 309 remote_mode=$mode remote_sha1=$sha1 310 ;; 311 esac 312 done 313 314 if is_submodule "$local_mode" || is_submodule "$remote_mode" 315 then 316 echo "Submodule merge conflict for '$MERGED':" 317 describe_file "$local_mode" "local" "$local_sha1" 318 describe_file "$remote_mode" "remote" "$remote_sha1" 319 resolve_submodule_merge 320 return 321 fi 322 323 if test -f "$MERGED" 324 then 325 mv -- "$MERGED" "$BACKUP" 326 cp -- "$BACKUP" "$MERGED" 327 fi 328 # Create a parent directory to handle delete/delete conflicts 329 # where the base's directory no longer exists. 330 mkdir -p "$(dirname "$MERGED")" 331 332 checkout_staged_file 1 "$MERGED" "$BASE" 333 checkout_staged_file 2 "$MERGED" "$LOCAL" 334 checkout_staged_file 3 "$MERGED" "$REMOTE" 335 336 # hideResolved preferences hierarchy. 337 global_config="mergetool.hideResolved" 338 tool_config="mergetool.${merge_tool}.hideResolved" 339 340 if enabled=$(git config --type=bool "$tool_config") 341 then 342 # The user has a specific preference for a specific tool and no 343 # other preferences should override that. 344 : ; 345 elif enabled=$(git config --type=bool "$global_config") 346 then 347 # The user has a general preference for all tools. 348 # 349 # 'true' means the user likes the feature so we should use it 350 # where possible but tool authors can still override. 351 # 352 # 'false' means the user doesn't like the feature so we should 353 # not use it anywhere. 354 if test "$enabled" = true && hide_resolved_enabled 355 then 356 enabled=true 357 else 358 enabled=false 359 fi 360 else 361 # The user does not have a preference. Default to disabled. 362 enabled=false 363 fi 364 365 if test "$enabled" = true 366 then 367 hide_resolved 368 fi 369 370 if test -z "$local_mode" || test -z "$remote_mode" 371 then 372 echo "Deleted merge conflict for '$MERGED':" 373 describe_file "$local_mode" "local" "$LOCAL" 374 describe_file "$remote_mode" "remote" "$REMOTE" 375 resolve_deleted_merge 376 status=$? 377 rmdir -p "$(dirname "$MERGED")" 2>/dev/null 378 return $status 379 fi 380 381 if is_symlink "$local_mode" || is_symlink "$remote_mode" 382 then 383 echo "Symbolic link merge conflict for '$MERGED':" 384 describe_file "$local_mode" "local" "$LOCAL" 385 describe_file "$remote_mode" "remote" "$REMOTE" 386 resolve_symlink_merge 387 return 388 fi 389 390 echo "Normal merge conflict for '$MERGED':" 391 describe_file "$local_mode" "local" "$LOCAL" 392 describe_file "$remote_mode" "remote" "$REMOTE" 393 if test "$guessed_merge_tool" = true || test "$prompt" = true 394 then 395 printf "Hit return to start merge resolution tool (%s): " "$merge_tool" 396 read ans || return 1 397 fi 398 399 if base_present 400 then 401 present=true 402 else 403 present=false 404 fi 405 406 if ! run_merge_tool "$merge_tool" "$present" 407 then 408 echo "merge of $MERGED failed" 1>&2 409 mv -- "$BACKUP" "$MERGED" 410 411 if test "$merge_keep_temporaries" = "false" 412 then 413 cleanup_temp_files 414 fi 415 416 return 1 417 fi 418 419 if test "$merge_keep_backup" = "true" 420 then 421 mv -- "$BACKUP" "$MERGED.orig" 422 else 423 rm -- "$BACKUP" 424 fi 425 426 git add -- "$MERGED" 427 cleanup_temp_files 428 return 0 429} 430 431prompt_after_failed_merge () { 432 while true 433 do 434 printf "Continue merging other unresolved paths [y/n]? " 435 read ans || return 1 436 case "$ans" in 437 [yY]*) 438 return 0 439 ;; 440 [nN]*) 441 return 1 442 ;; 443 esac 444 done 445} 446 447print_noop_and_exit () { 448 echo "No files need merging" 449 exit 0 450} 451 452main () { 453 prompt=$(git config --bool mergetool.prompt) 454 GIT_MERGETOOL_GUI= 455 guessed_merge_tool=false 456 orderfile= 457 458 while test $# != 0 459 do 460 case "$1" in 461 --tool-help=*) 462 TOOL_MODE=${1#--tool-help=} 463 show_tool_help 464 ;; 465 --tool-help) 466 show_tool_help 467 ;; 468 -t|--tool*) 469 case "$#,$1" in 470 *,*=*) 471 merge_tool=${1#*=} 472 ;; 473 1,*) 474 usage ;; 475 *) 476 merge_tool="$2" 477 shift ;; 478 esac 479 ;; 480 --no-gui) 481 GIT_MERGETOOL_GUI=false 482 ;; 483 -g|--gui) 484 GIT_MERGETOOL_GUI=true 485 ;; 486 -y|--no-prompt) 487 prompt=false 488 ;; 489 --prompt) 490 prompt=true 491 ;; 492 -O*) 493 orderfile="${1#-O}" 494 ;; 495 --) 496 shift 497 break 498 ;; 499 -*) 500 usage 501 ;; 502 *) 503 break 504 ;; 505 esac 506 shift 507 done 508 509 git_dir_init 510 require_work_tree 511 512 if test -z "$merge_tool" 513 then 514 merge_tool=$(get_merge_tool) 515 subshell_exit_status=$? 516 if test $subshell_exit_status = 1 517 then 518 guessed_merge_tool=true 519 elif test $subshell_exit_status -gt 1 520 then 521 exit $subshell_exit_status 522 fi 523 fi 524 merge_keep_backup="$(git config --bool mergetool.keepBackup || echo true)" 525 merge_keep_temporaries="$(git config --bool mergetool.keepTemporaries || echo false)" 526 527 prefix=$(git rev-parse --show-prefix) || exit 1 528 cd_to_toplevel 529 530 if test -n "$orderfile" 531 then 532 orderfile=$( 533 git rev-parse --prefix "$prefix" -- "$orderfile" | 534 sed -e 1d 535 ) 536 fi 537 538 if test $# -eq 0 && test -e "$GIT_DIR/MERGE_RR" 539 then 540 set -- $(git rerere remaining) 541 if test $# -eq 0 542 then 543 print_noop_and_exit 544 fi 545 elif test $# -ge 0 546 then 547 # rev-parse provides the -- needed for 'set' 548 eval "set $(git rev-parse --sq --prefix "$prefix" -- "$@")" 549 fi 550 551 files=$(git -c core.quotePath=false \ 552 diff --name-only --diff-filter=U \ 553 ${orderfile:+"-O$orderfile"} -- "$@") 554 555 if test -z "$files" 556 then 557 print_noop_and_exit 558 fi 559 560 printf "Merging:\n" 561 printf "%s\n" "$files" 562 563 rc=0 564 set -- $files 565 while test $# -ne 0 566 do 567 printf "\n" 568 if ! merge_file "$1" 569 then 570 rc=1 571 test $# -ne 1 && prompt_after_failed_merge || exit 1 572 fi 573 shift 574 done 575 576 exit $rc 577} 578 579main "$@"