README_DOCS.rst: wordsmith some of the commentary
[topgit/pro.git] / tg--merging.sh
blobc73cb629f8e342e979f80cbf74dd59ef28dcd51c
1 #!/bin/sh
2 # TopGit merging utility functions
3 # Copyright (C) 2015,2016,2017,2018,2019,2021 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved
5 # License GPLv2
7 # git_topmerge will need this even on success and since it might otherwise
8 # be called many times do it just the once here and now
9 ensure_work_tree
10 v_get_show_toplevel repotoplvl
12 # If HEAD is a symref to "$1" detach it at its current value
13 detach_symref_head_on_branch() {
14 _hsr="$(git symbolic-ref -q HEAD --)" && [ -n "$_hsr" ] || return 0
15 _hrv="$(git rev-parse --quiet --verify HEAD --)" && [ -n "$_hrv" ] ||
16 die "cannot detach_symref_head_on_branch from unborn branch $_hsr"
17 git update-ref --no-deref -m "detaching HEAD from $_hsr to safely update it" HEAD "$_hrv"
20 # Run an in-tree recursive merge but make sure we get the desired version of
21 # any .topdeps and .topmsg files. The $auhopt and --no-stat options are
22 # always implicitly in effect. If successful, a new commit is performed on HEAD
23 # unless the optional --no-commit option has been given.
25 # The "git merge-recursive" tool (and others) must be run to get the desired
26 # result. And --no-ff is always implicitly in effect as well.
28 # NOTE: [optional] arguments MUST appear in the order shown
29 # [optional] '-v' varname => optional variable to return original HEAD hash in
30 # [optional] '--no-commit' => update worktree and index but do not commit
31 # [optional] '--merge', '--theirs' or '--remove' to alter .topfile handling
32 # [optional] '--name' <name-for-ours> [--name <name-for-theirs>]
33 # $1 => '-m' MUST be '-m'
34 # $2 => commit message
35 # $3 => commit-ish to merge as "theirs"
36 git_topmerge()
38 _ovar=
39 [ "$1" != "-v" ] || [ $# -lt 2 ] || [ -z "$2" ] || { _ovar="$2"; shift 2; }
40 _ncmode=
41 [ "$1" != "--no-commit" ] || { _ncmode=1; shift; }
42 _mmode=
43 case "$1" in --theirs|--remove|--merge) _mmode="${1#--}"; shift; esac
44 _nameours=
45 _nametheirs=
46 if [ "$1" = "--name" ] && [ $# -ge 2 ]; then
47 _nameours="$2"
48 shift 2
49 if [ "$1" = "--name" ] && [ $# -ge 2 ]; then
50 _nametheirs="$2"
51 shift 2
54 : "${_nameours:=HEAD}"
55 [ "$#" -eq 3 ] && [ "$1" = "-m" ] && [ -n "$2" ] && [ -n "$3" ] ||
56 die "programmer error: invalid arguments to git_topmerge: $*"
57 _ours="$(git rev-parse --verify HEAD^0)" || die "git rev-parse failed"
58 _theirs="$(git rev-parse --verify "$3^0")" || die "git rev-parse failed"
59 [ -z "$_ovar" ] || eval "$_ovar="'"$_ours"'
60 eval "GITHEAD_$_ours="'"$_nameours"' && eval export "GITHEAD_$_ours"
61 if [ -n "$_nametheirs" ]; then
62 eval "GITHEAD_$_theirs="'"$_nametheirs"' && eval export "GITHEAD_$_theirs"
64 _mdriver='touch %A'
65 if [ "$_mmode" = "merge" ]; then
66 TG_L1="$_nameours" && export TG_L1
67 TG_L2="merged common ancestors" && export TG_L2
68 TG_L3="${_nametheirs:-$3}" && export TG_L3
69 _mdriver='git merge-file -L "$TG_L1" -L "$TG_L2" -L "$TG_L3" --marker-size=%L %A %O %B'
71 _msg="$2"
72 _mt=
73 _mb="$(git merge-base --all "$_ours" "$_theirs")" && [ -n "$_mb" ] ||
74 { _mt=1; _mb="$(git mktree < /dev/null)"; }
75 # any .topdeps or .topmsg output needs to be stripped from stdout
76 tmpstdout="$tg_tmp_dir/stdout.$$"
77 _ret=0
78 git -c "merge.ours.driver=$_mdriver" merge-recursive \
79 $_mb -- "$_ours" "$_theirs" >"$tmpstdout" || _ret=$?
80 # success or failure is not relevant until after fixing up the
81 # .topdeps and .topmsg files and running rerere unless _ret >= 126
82 [ $_ret -lt 126 ] || return $_ret
83 if [ "$_mmode" = "merge" ]; then
84 cat "$tmpstdout"
85 else
86 case "$_mmode" in
87 theirs) _source="$_theirs";;
88 remove) _source="";;
89 *) _source="$_ours";;
90 esac
91 _newinfo=
92 [ -z "$_source" ] ||
93 _newinfo="$(git cat-file --batch-check="%(objecttype) %(objectname)$tab%(rest)" <<-EOT |
94 $_source:.topdeps .topdeps
95 $_source:.topmsg .topmsg
96 EOT
97 sed -n 's/^blob /100644 /p'
99 [ -z "$_newinfo" ] || _newinfo="$lf$_newinfo"
100 git update-index --index-info <<-EOT ||
101 0 $nullsha$tab.topdeps
102 0 $nullsha$tab.topmsg$_newinfo
104 die "git update-index failed"
105 if [ "$_mmode" = "remove" ] &&
106 { [ -e "$repotoplvl/.topdeps" ] || [ -e "$repotoplvl/.topmsg" ]; }
107 then
108 rm -r -f "$repotoplvl/.topdeps" "$repotoplvl/.topmsg" >/dev/null 2>&1 || :
109 else
110 for zapbad in "$repotoplvl/.topdeps" "$repotoplvl/.topmsg"; do
111 if [ -e "$zapbad" ] && { [ -L "$zapbad" ] || [ ! -f "$zapbad" ]; }; then
112 rm -r -f "$zapbad"
114 done
115 # Since Git v2.30.1, even with "-q" checkout-index can spuriously fail!
116 # It must only be called with the names of files actually in the index to avoid that.
117 idxtopfiles="$(git ls-files --full-name -- :/.topdeps :/.topmsg)" || :
118 [ -z "$idxtopfiles" ] ||
119 (cd "$repotoplvl" && git checkout-index -q -f -u -- $idxtopfiles) ||
120 die "git checkout-index failed"
122 # dump output without any .topdeps or .topmsg messages
123 sed -e '/ \.topdeps/d' -e '/ \.topmsg/d' <"$tmpstdout"
125 # rerere will be a nop unless rerere.enabled is true, but might complete the merge!
126 eval git "${setautoupdate:+-c rerere.autoupdate=1}" rerere || :
127 git ls-files --unmerged --full-name --abbrev :/ >"$tmpstdout" 2>&1 ||
128 die "git ls-files failed"
129 if [ -s "$tmpstdout" ]; then
130 [ "$_ret" != "0" ] || _ret=1
131 else
132 _ret=0
134 if [ $_ret -ne 0 ]; then
135 # merge failed, spit out message, enter "merge" mode and return
137 printf '%s\n\n# Conflicts:\n' "$_msg"
138 sed -n "/$tab/s/^[^$tab]*/#/p" <"$tmpstdout" | sort -u
139 } >"$git_dir/MERGE_MSG"
140 git update-ref MERGE_HEAD "$_theirs" || :
141 echo 'Automatic merge failed; fix conflicts and then commit the result.'
142 rm -f "$tmpstdout"
143 return $_ret
145 if [ -n "$_ncmode" ]; then
146 # merge succeeded, but --no-commit requested, enter "merge" mode and return
147 printf '%s\n' "$_msg" >"$git_dir/MERGE_MSG"
148 git update-ref MERGE_HEAD "$_theirs" || :
149 echo 'Automatic merge went well; stopped before committing as requested.'
150 rm -f "$tmpstdout"
151 return $_ret
153 # commit time at last!
154 thetree="$(git write-tree)" || die "git write-tree failed"
155 # avoid an extra "already up-to-date" commit (can't happen if _mt though)
156 origtree=
157 [ -n "$_mt" ] || {
158 origtree="$(git rev-parse --quiet --verify "$_ours^{tree}" --)" &&
159 [ -n "$origtree" ]
160 } || die "git rev-parse failed"
161 if [ "$origtree" != "$thetree" ] || ! contained_by "$_theirs" "$_ours"; then
162 thecommit="$(git commit-tree -p "$_ours" -p "$_theirs" -m "$_msg" "$thetree")" &&
163 [ -n "$thecommit" ] || die "git commit-tree failed"
164 git update-ref -m "$_msg" HEAD "$thecommit" || die "git update-ref failed"
166 # mention how the merge was made
167 echo "Merge made by the 'recursive' strategy."
168 rm -f "$tmpstdout"
169 return 0
172 # run git_topmerge with the passed in arguments (it always does --no-stat)
173 # then return the exit status of git_topmerge
174 # if the returned exit status is no error show a shortstat before
175 # returning assuming the merge was done into the previous HEAD but exclude
176 # .topdeps and .topmsg info from the stat unless doing a --merge
177 # if the first argument is --merge or --theirs or --remove handle .topmsg/.topdeps
178 # as follows:
179 # (default) .topmsg and .topdeps always keep ours
180 # --merge a normal merge takes place
181 # --theirs .topmsg and .topdeps always keep theirs
182 # --remove .topmsg and .topdeps are removed from the result and working tree
183 # note this function should only be called after attempt_index_merge fails as
184 # it implicity always does --no-ff (except for --merge which will --ff)
185 git_merge() {
186 _ret=0
187 git_topmerge -v _oldhead "$@" || _ret=$?
188 if [ "$1" != "--no-commit" ] && [ "$_ret" = "0" ]; then
189 _exclusions=
190 [ "$1" = "--merge" ] || _exclusions=":/ :!/.topdeps :!/.topmsg"
191 git --no-pager diff-tree --shortstat "$_oldhead" HEAD^0 -- $_exclusions
193 return $_ret
196 # $1 => .topfile handling ([--]merge, [--]theirs, [--]remove or else do ours)
197 # $2 => current "HEAD"
198 # $3 => proposed fast-forward-to "HEAD"
199 # result is success if fast-forward satisfies $1
200 topff_ok() {
201 case "${1#--}" in
202 merge|theirs)
203 # merge and theirs will always be correct
205 remove)
206 # okay if both blobs are "missing" in $3
207 printf '%s\n' "$3:.topdeps" "$3:.topmsg" |
208 git cat-file --batch-check="%(objectname) %(objecttype)" |
210 read _tdo _tdt &&
211 read _tmo _tmt &&
212 [ "$_tdt" = "missing" ] &&
213 [ "$_tmt" = "missing" ]
214 } || return 1
217 # "ours"
218 # okay if both blobs are the same (same hash or missing)
219 printf '%s\n' "$2:.topdeps" "$2:.topmsg" "$3:.topdeps" "$3:.topmsg" |
220 git cat-file --batch-check="%(objectname) %(objecttype)" |
222 read _td1o _td1t &&
223 read _tm1o _tm1t &&
224 read _td2o _td2t &&
225 read _tm2o _tm2t &&
226 { [ "$_td1t" = "$_td2t" ] &&
227 { [ "$_td1o" = "$_td2o" ] ||
228 [ "$_td1t" = "missing" ]; }; } &&
229 { [ "$_tm1t" = "$_tm2t" ] &&
230 { [ "$_tm1o" = "$_tm2o" ] ||
231 [ "$_tm1t" = "missing" ]; }; }
232 } || return 1
234 esac
235 return 0
238 # similar to git_merge but operates exclusively using a separate index and temp dir
239 # only trivial aggressive automatic (i.e. simple) merges are supported
241 # [optional] '--no-auto' to suppress "automatic" merging, merge fails instead
242 # [optional] '--merge', '--theirs' or '--remove' to alter .topfile handling
243 # $1 => '' to discard result, 'refs/?*' to update the specified ref or a varname
244 # $2 => '-m' MUST be '-m'
245 # $3 => commit message AND, if $1 matches refs/?* the update-ref message
246 # $4 => commit-ish to merge as "ours"
247 # $5 => commit-ish to merge as "theirs"
248 # [$6...] => more commit-ishes to merge as "theirs" in octopus
250 # all merging is done in a separate index (or temporary files for simple merges)
251 # if successful the ref or var is updated with the result
252 # otherwise everything is left unchanged and a silent failure occurs
253 # if successful and $1 matches refs/?* it WILL BE UPDATED to a new commit using the
254 # message and appropriate parents AND HEAD WILL BE DETACHED first if it's a symref
255 # to the same ref
256 # otherwise if $1 does not match refs/?* and is not empty the named variable will
257 # be set to contain the resulting commit from the merge
258 # the working tree and index ARE LEFT COMPLETELY UNTOUCHED no matter what
259 v_attempt_index_merge() {
260 _noauto=
261 if [ "$1" = "--no-auto" ]; then
262 _noauto=1
263 shift
265 _exclusions=
266 [ "$1" = "--merge" ] || _exclusions=":/ :!/.topdeps :!/.topmsg"
267 _mstyle=
268 if [ "$1" = "--merge" ] || [ "$1" = "--theirs" ] || [ "$1" = "--remove" ]; then
269 _mmode="${1#--}"
270 shift
271 if [ "$_mmode" = "merge" ] || [ "$_mmode" = "theirs" ]; then
272 _mstyle="-top$_mmode"
275 [ "$#" -ge 5 ] && [ "$2" = "-m" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] ||
276 die "programmer error: invalid arguments to v_attempt_index_merge: $*"
277 _var="$1"
278 _msg="$3"
279 _head="$4"
280 shift 4
281 rh="$(git rev-parse --quiet --verify "$_head^0" --)" && [ -n "$rh" ] || return 1
282 orh="$rh"
283 oth=
284 _mmsg=
285 newc=
286 _nodt=
287 _same=
288 _mt=
289 _octo=
290 if [ $# -gt 1 ]; then
291 if [ "$_mmode" = "merge" ] || [ "$_mmode" = "theirs" ]; then
292 die "programmer error: invalid octopus .topfile strategy to v_attempt_index_merge: --$_mode"
294 ihl="$(git merge-base --independent "$@")" || return 1
295 set -- $ihl
296 [ $# -ge 1 ] && [ -n "$1" ] || return 1
298 [ $# -eq 1 ] || _octo=1
299 mb="$(git merge-base ${_octo:+--octopus} "$rh" "$@")" && [ -n "$mb" ] || {
300 mb="$(git mktree < /dev/null)"
301 _mt=1
303 if [ -z "$_mt" ]; then
304 if [ -n "$_octo" ]; then
305 while [ $# -gt 1 ] && mbh="$(git merge-base "$rh" "$1")" && [ -n "$mbh" ]; do
306 if [ "$rh" = "$mbh" ]; then
307 if topff_ok "$_mmode" "$rh" "$1"; then
308 _mmsg="Fast-forward (no commit created)"
309 rh="$1"
310 orh="$rh"
311 shift
312 else
313 break
315 elif [ "$1" = "$mbh" ]; then
316 shift
317 else
318 break;
320 done
321 if [ $# -eq 1 ]; then
322 _octo=
323 mb="$(git merge-base "$rh" "$1")" && [ -n "$mb" ] || return 1
326 if [ -z "$_octo" ]; then
327 r1="$(git rev-parse --quiet --verify "$1^0" --)" && [ -n "$r1" ] || return 1
328 oth="$r1"
329 set -- "$r1"
330 if [ "$rh" = "$mb" ]; then
331 if topff_ok "$_mmode" "$rh" "$r1"; then
332 _mmsg="Fast-forward (no commit created)"
333 newc="$r1"
334 _nodt=1
335 _mstyle=
337 elif [ "$r1" = "$mb" ]; then
338 [ -n "$_mmsg" ] || _mmsg="Already up-to-date!"
339 newc="$rh"
340 _nodt=1
341 _same=1
342 _mstyle=
346 if [ -z "$newc" ]; then
347 if [ "$_mmode" = "theirs" ] && [ -z "$oth" ]; then
348 oth="$(git rev-parse --quiet --verify "$1^0" --)" && [ -n "$oth" ] || return 1
349 set -- "$oth"
351 inew="$tg_tmp_dir/index.$$"
352 ! [ -e "$inew" ] || rm -f "$inew"
353 itmp="$tg_tmp_dir/output.$$"
354 imrg="$tg_tmp_dir/auto.$$"
355 [ -z "$_octo" ] || >"$imrg"
356 _auto=
357 _parents=
358 _newrh="$rh"
359 while :; do
360 if [ -n "$_parents" ]; then
361 if contained_by "$1" "$_newrh"; then
362 shift
363 continue
366 GIT_INDEX_FILE="$inew" git read-tree -m --aggressive -i "$mb" "$rh" "$1" || { rm -f "$inew" "$imrg"; return 1; }
367 GIT_INDEX_FILE="$inew" git ls-files --unmerged --full-name --abbrev :/ >"$itmp" 2>&1 || { rm -f "$inew" "$itmp" "$imrg"; return 1; }
368 ! [ -s "$itmp" ] || {
369 if ! GIT_INDEX_FILE="$inew" TG_TMP_DIR="$tg_tmp_dir" git merge-index -q "$TG_INST_CMDDIR/tg--index-merge-one-file$_mstyle" -a >"$itmp" 2>&1; then
370 rm -f "$inew" "$itmp" "$imrg"
371 return 1
373 if [ -s "$itmp" ]; then
374 if [ -n "$_noauto" ]; then
375 rm -f "$inew" "$itmp" "$imrg"
376 return 1
378 if [ -n "$_octo" ]; then
379 cat "$itmp" >>"$imrg"
380 else
381 cat "$itmp"
383 _auto=" automatic"
386 _mstyle=
387 rm -f "$itmp"
388 _parents="${_parents:+$_parents }-p $1"
389 if [ $# -gt 1 ]; then
390 newt="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] || { rm -f "$inew" "$imrg"; return 1; }
391 rh="$newt"
392 shift
393 continue
395 break;
396 done
397 if [ "$_mmode" != "merge" ]; then
398 case "$_mmode" in
399 theirs) _source="$oth";;
400 remove) _source="";;
401 *) _source="$orh";;
402 esac
403 _newinfo=
404 [ -z "$_source" ] ||
405 _newinfo="$(git cat-file --batch-check="%(objecttype) %(objectname)$tab%(rest)" <<-EOT |
406 $_source:.topdeps .topdeps
407 $_source:.topmsg .topmsg
409 sed -n 's/^blob /100644 /p'
411 [ -z "$_newinfo" ] || _newinfo="$lf$_newinfo"
412 GIT_INDEX_FILE="$inew" git update-index --index-info <<-EOT || { rm -f "$inew" "$imrg"; return 1; }
413 0 $nullsha$tab.topdeps
414 0 $nullsha$tab.topmsg$_newinfo
417 newt="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] || { rm -f "$inew" "$imrg"; return 1; }
418 [ -z "$_octo" ] || sort -u <"$imrg"
419 rm -f "$inew" "$imrg"
420 newc="$(git commit-tree -p "$orh" $_parents -m "$_msg" "$newt")" && [ -n "$newc" ] || return 1
421 _mmsg="Merge made by the 'trivial aggressive$_auto${_octo:+ octopus}' strategy."
423 case "$_var" in
424 refs/?*)
425 if [ -n "$_same" ]; then
426 _same=
427 if rv="$(git rev-parse --quiet --verify "$_var" --)" && [ "$rv" = "$newc" ]; then
428 _same=1
431 if [ -z "$_same" ]; then
432 detach_symref_head_on_branch "$_var" || return 1
433 # git update-ref returns 0 even on failure :(
434 git update-ref -m "$_msg" "$_var" "$newc" || return 1
438 eval "$_var="'"$newc"'
440 esac
441 echo "$_mmsg"
442 [ -n "$_nodt" ] || git --no-pager diff-tree --shortstat "$orh" "$newc" -- $_exclusions
443 return 0
446 # shortcut that passes $3 as a preceding argument (which must match refs/?*)
447 attempt_index_merge() {
448 _noauto=
449 _mmode=
450 if [ "$1" = "--no-auto" ]; then
451 _noauto="$1"
452 shift
454 if [ "$1" = "--merge" ] || [ "$1" = "--theirs" ] || [ "$1" = "--remove" ]; then
455 _mmode="$1"
456 shift
458 case "$3" in refs/?*);;*)
459 die "programmer error: invalid arguments to attempt_index_merge: $*"
460 esac
461 v_attempt_index_merge $_noauto $_mmode "$3" "$@"
464 # write empty blob and store its hash in the variable named by $1 (if not empty)
465 v_write_mt_blob() {
466 __gitmtblob="$(git hash-object -t blob -w --stdin </dev/null 2>/dev/null)" || :
467 [ -n "$__gitmtblob" ] || die "could not write empty blob to object database"
468 [ -z "$1" ] || eval "$1=\"\$__gitmtblob\""
469 return 0
472 # $1 is variable name to receive written tree (may be empty)
473 # $2 is full blob hash to assign to single "blob" file in created tree
474 v_write_blob_tree() {
475 _mktreet="$(git mktree <<EOT 2>/dev/null
476 100644 blob $2${tab}blob
478 )" || :
479 [ -n "$_mktreet" ] || return 1
480 [ -z "$1" ] || eval "$1=\"\$_mktreet\""
481 return 0
484 # $1 is variable name to receive type of object (may be empty)
485 # $2 is variable name to receive hash of object (may be empty)
486 # $3 is a suitable object name for cat-file --batch-check
487 # $4 [optional] if non-empty will be joined to $3 with a ':'
488 # if object does not exist status return is not 0
489 v_get_object_type() {
490 _goth=
491 _gott=
492 read -r _goth _gott <<EOT || :
493 $(git cat-file --batch-check="%(objectname) %(objecttype)" 2>/dev/null <<EOD
494 $3${4:+:$4}
498 case "$_gott" in *"missing")
499 return 1
500 esac
501 [ -n "$_goth" ] && [ -n "$_gott" ] || return 1
502 [ -z "$1" ] || eval "$1=\"\$_gott\""
503 [ -z "$2" ] || eval "$2=\"\$_goth\""
504 return 0
507 # attempt a 3-way index merge of a single file and return the resulting blob
508 # similar to v_attempt_index_merge except there are always exactly two heads
509 # and only the specified file will be merged (and if successful a blob created)
511 # optional arguments MUST be given in the order shown
513 # [optional] '--use-empty' use empty blob if file does not exist in either head
514 # [optional] '--non-blob-empty' use empty blob if non-blob found
515 # [optional] '--auh' allow unrelated histories (use empty tree if no merge base)
516 # $1 => '' to discard result or a varname to receive the blob hash
517 # $2 => commit-ish to merge as "ours"
518 # $3 => commit-ish to merge as "theirs"
519 # $4 => full path to file to merge (as shown by ls-tree -rt --full-tree <tree>)
520 # $5 => [optional] full path of file in "theirs" (defaults to $4)
521 # $6 => [optional] full path of file in merge base (defaults to $4)
523 # if either tree $2 and/or tree $3 is invalid a silent failure always occurs
524 # if merge-base $2 $3 fails and --auh has NOT been given a silent failure occurs
525 # if either $2 and/or $3 has a non-blob at the specified path, a silent failure
526 # occurs unless --non-blob-empty has been given
527 # if either $2 and/or $3 has nothing at the specified path, a silent failure
528 # occurs unless --use-empty has been given
530 # With `--auh` $2 and/or $3 can be a tree rather than a commit in which case
531 # the merge base will be an empty tree
533 # $6 will always be ignored if no merge base has been found which will also
534 # always be a silent failure unless `--auh` has been given
536 # If the "ours" blob is the same as the "theirs" blob it's returned early and
537 # the check for a merge base is skipped (--auh is also irrelevant in this case)
539 # all merging is done in a separate index (or temporary files for simple merges)
540 # if successful the var $1 (if not empty) is updated with the result blob hash
541 # otherwise everything is left unchanged and a silent failure occurs
542 # the working tree and index ARE LEFT COMPLETELY UNTOUCHED no matter what
543 # on success (regardless of whether $1 is empty) the new blob will always be
544 # written to the object database even if it's not returned ($1 is empty)
545 v_attempt_index_merge_file() {
546 _usemt=
547 _nonmt=
548 _useuh=
549 [ "$1" != "--use-empty" ] || { _usemt=1; shift; }
550 [ "$1" != "--non-blob-empty" ] || { _nonmt=1; shift; }
551 [ "$1" != "--auh" ] || { _useuh=1; shift; }
552 [ $# -eq 4 ] || [ $# -eq 5 ] || [ $# -eq 6 ] ||
553 die "programmer error: wrong number of args ($#) to v_attempt_index_merge_file"
554 [ -n "$4" ] || die "programmer error: empty path for arg \$2 to v_attempt_index_merge_file"
555 _treeo="$(git rev-parse --verify --quiet "$2^{tree}" -- 2>/dev/null)" || :
556 [ -n "$_treeo" ] || return 1
557 _treet="$(git rev-parse --verify --quiet "$3^{tree}" -- 2>/dev/null)" || :
558 [ -n "$_treet" ] || return 1
559 _mtblob=
560 if ! v_get_object_type _blobot _bloboh "$_treeo" "$4"; then
561 [ -n "$_usemt" ] || return 1
562 [ -n "$_mtblob" ] || v_write_mt_blob _mtblob
563 _blobot="blob"
564 _bloboh="$_mtblob"
566 if [ "$_blobot" != "blob" ]; then
567 [ -n "$_nonmt" ] || return 1
568 [ -n "$_mtblob" ] || v_write_mt_blob _mtblob
569 _blobot="blob"
570 _bloboh="$_mtblob"
572 if ! v_get_object_type _blobtt _blobth "$_treet" "${5:-$4}"; then
573 [ -n "$_usemt" ] || return 1
574 [ -n "$_mtblob" ] || v_write_mt_blob _mtblob
575 _blobtt="blob"
576 _blobth="$_mtblob"
578 if [ "$_blobtt" != "blob" ]; then
579 [ -n "$_nonmt" ] || return 1
580 [ -n "$_mtblob" ] || v_write_mt_blob _mtblob
581 _blobtt="blob"
582 _blobth="$_mtblob"
584 if [ "$_bloboh" = "$_blobth" ]; then
585 # nothing to do, blobs are the same
586 [ -z "$1" ] || eval "$1=\"\$_bloboh\""
587 return 0
589 _mbf="$(git merge-base "$2" "$3" 2>/dev/null)" || :
590 [ -n "$_mbf" ] || [ -n "$_useuh" ] || return 1
591 _mbtmt=
592 if [ -n "$_mbf" ]; then
593 _treeb="$(git rev-parse --verify --quiet "$_mbf^{tree}" -- 2>/dev/null)" || :
594 [ -n "$_treeb" ] || return 1 # somehow the commit doesn't have a tree
595 else
596 _treeb="$mttree"
597 _mbtmt=1
600 test -n "$_mbtmt" ||
601 ! v_get_object_type _blobbt _blobbh "$_treeb" "${6:-$4}"
602 then
603 [ -n "$_mbtmt" ] || [ -n "$_usemt" ] || return 1
604 [ -n "$_mtblob" ] || v_write_mt_blob _mtblob
605 _blobbt="blob"
606 _blobbh="$_mtblob"
608 if [ "$_blobbt" != "blob" ]; then
609 [ -n "$_nonmt" ] || return 1
610 [ -n "$_mtblob" ] || v_write_mt_blob _mtblob
611 _blobbt="blob"
612 _blobbh="$_mtblob"
614 v_write_blob_tree _treeob "$_bloboh" || return 1
615 v_write_blob_tree _treetb "$_blobth" || return 1
616 v_write_blob_tree _treebb "$_blobbh" || return 1
617 inew="$tg_tmp_dir/index.$$"
618 ! [ -e "$inew" ] || rm -f "$inew"
619 GIT_INDEX_FILE="$inew" \
620 git read-tree -m --aggressive -i "$_treebb" "$_treeob" "$_treetb" >/dev/null 2>&1 || {
621 rm -f "$inew"
622 return 1
624 _unmerged="$(GIT_INDEX_FILE="$inew" \
625 git ls-files --unmerged --full-name --abbrev -- :/ 2>/dev/null)" || {
626 rm -f "$inew"
627 return 1
629 if [ -n "$_unmerged" ]; then
630 # try an automatic merge
631 GIT_INDEX_FILE="$inew" TG_TMP_DIR="$tg_tmp_dir" \
632 git merge-index -q "$TG_INST_CMDDIR/tg--index-merge-one-file" -a >/dev/null 2>&1 || {
633 rm -f "$inew"
634 return 1
636 _unmerged="$(GIT_INDEX_FILE="$inew" \
637 git ls-files --unmerged --full-name --abbrev -- :/ 2>/dev/null)" || {
638 rm -f "$inew"
639 return 1
642 [ -z "$_unmerged" ] || {
643 # merge failed
644 rm -f "$inew"
645 return 1
647 _rsltm=
648 _rslth=
649 _rslts=
650 _rsltf=
651 read -r _rsltm _rslth _rslts _rsltf <<EOT || :
652 $(GIT_INDEX_FILE="$inew" git ls-files --full-name -s -- :/)
654 rm -f "$inew"
656 [ "$_rsltm" = "100644" ] && [ -n "$_rslth" ] &&
657 [ "$_rslts" = "0" ] && [ "$_rsltf" = "blob" ]
658 then
659 # success
660 [ -z "$1" ] || eval "$1=\"\$_rslth\""
661 return 0
663 return 1