tg: support $tgdisplayac for --abort/--continue display
[topgit/pro.git] / tg-update.sh
blobedcc07d1af09157da2c47eedff6232a2ebcd590f
1 #!/bin/sh
2 # TopGit - A different patch queue manager
3 # Copyright (C) Petr Baudis <pasky@suse.cz> 2008
4 # Copyright (C) Kyle J. McKay <mackyle@gmail.com> 2015,2016,2017
5 # All rights reserved.
6 # GPLv2
8 names= # Branch(es) to update
9 name1= # first name
10 name2= # second name
11 namecnt=0 # how many names were seen
12 all= # Update all branches
13 pattern= # Branch selection filter for -a
14 current= # Branch we are currently on
15 skipms= # skip missing dependencies
16 stash= # tgstash refs before changes
17 quiet= # be quieter
18 basemode= # true if --base active
19 editmode= # 0, 1 or empty to force none, force edit or default
20 basemsg= # message for --base merge commit
21 basefile= # message file for --base merge commit
22 basenc= # --no-commit on merge
23 basefrc= # --force non-ff update
25 if [ "$(git config --get --bool topgit.autostash 2>/dev/null)" != "false" ]; then
26 # topgit.autostash is true (or unset)
27 stash=1
30 ## Parse options
32 USAGE="\
33 Usage: ${tgname:-tg} [...] update [--[no-]stash] [--skip-missing] ([<name>...] | -a [<pattern>...])
34 Or: ${tgname:-tg} [...] update --base [-F <file> | -m <msg>] [--[no-]edit] [-f] <base-branch> <ref>
35 Or: ${tgname:-tg} [...] update --continue | -skip | --stop | --abort"
37 usage()
39 if [ "${1:-0}" != 0 ]; then
40 printf '%s\n' "$USAGE" >&2
41 else
42 printf '%s\n' "$USAGE"
44 exit ${1:-0}
47 # --base mode comes here with $1 set to <base-branch> and $2 set to <ref>
48 # and all options already parsed and validated into above-listed flags
49 # this function should exit after returning to "$current"
50 do_base_mode()
52 v_verify_topgit_branch tgbranch "$1"
53 depcnt="$(( $(git cat-file blob "refs/heads/$tgbranch:.topdeps" 2>/dev/null | wc -l) ))"
54 if [ $depcnt -gt 0 ]; then
55 grammar="dependency"
56 [ $depcnt -eq 1 ] || grammar="dependencies"
57 die "'$tgbranch' is not a TopGit [BASE] branch (it has $depcnt $grammar)"
59 newrev="$(git rev-parse --verify "$2^0" --)" && [ -n "$newrev" ] ||
60 die "not a valid commit-ish: $2"
61 baserev="$(ref_exists_rev "refs/$topbases/$tgbranch")" && [ -n "$baserev" ] ||
62 die "unable to get current base commit for branch '$tgbranch'"
63 if [ "$baserev" = "$newrev" ]; then
64 [ -n "$quiet" ] || echo "No change"
65 exit 0
67 alreadymerged=
68 ! contained_by "$newrev" "refs/heads/$tgbranch" || alreadymerged=1
69 if [ -z "$basefrc" ] && ! contained_by "$baserev" "$newrev"; then
70 die "Refusing non-fast-forward update of base without --force"
73 # check that we can checkout the branch
75 [ -n "$alreadymerged" ] || git read-tree -n -u -m "refs/heads/$tgbranch" ||
76 die "git checkout \"$branch\" would fail"
78 # and make sure everything's clean and we know who we are
80 [ -n "$alreadymerged" ] || ensure_clean_tree
81 ensure_ident_available
83 # always auto stash even if it's just to the anonymous stash TG_STASH
85 stashmsg="tgupdate: autostash before --base $tgbranch update"
86 if [ -n "$stash" ]; then
87 $tg tag -q -q -m "$stashmsg" --stash "$tgbranch" &&
88 stashhash="$(git rev-parse --quiet --verify refs/tgstash --)" &&
89 [ -n "$stashhash" ] &&
90 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
91 die "requested --stash failed"
92 else
93 $tg tag --anonymous "$tgbranch" &&
94 stashhash="$(git rev-parse --quiet --verify TG_STASH --)" &&
95 [ -n "$stashhash" ] &&
96 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
97 die "anonymous --stash failed"
100 # proceed with the update
102 git update-ref -m "tg update --base $tgbranch $2" "refs/$topbases/$tgbranch" "$newrev" "$baserev" ||
103 die "Unable to update base ref"
104 if [ -n "$alreadymerged" ]; then
105 [ -n "$quiet" ] || echo "Already contained in branch (base updated)"
106 exit 0
108 git checkout -q $iowopt "$tgbranch" || die "git checkout failed"
109 msgopt=
110 [ -z "$basemsg" ] || msgopt='-m "$basemsg"'
111 fileopt=
112 [ -z "$basefile" ] || fileopt='-F "$basefile"'
113 if [ -n "$editmode" ]; then
114 if [ "$editmode" = "0" ]; then
115 editopt="--no-edit"
116 else
117 editopt="--edit"
119 else
120 if [ -z "$basemsg$basefile" ]; then
121 editopt="--edit"
122 basemsg="tg update --base $tgbranch $2"
123 msgopt='-m "$basemsg"'
124 else
125 editopt="--no-edit"
128 ncopt=
129 [ -z "$basenc" ] || ncopt="--no-commit"
130 eval git merge --no-ff --no-log --no-stat $auhopt $ncopt $editopt "$msgopt" "$fileopt" "refs/$topbases/$tgbranch" -- || exit
131 [ -n "$basenc" ] || checkout_symref_full "$current"
132 exit
135 state_dir="$git_dir/tg-update"
136 mergeours=
137 mergetheirs=
138 mergeresult=
139 stashhash=
140 next_no_auto=
141 merging_topfiles=
143 is_active() {
144 [ -d "$state_dir" ] || return 1
145 [ -s "$state_dir/fullcmd" ] || return 1
146 [ -f "$state_dir/remote" ] || return 1
147 [ -f "$state_dir/skipms" ] || return 1
148 [ -f "$state_dir/all" ] || return 1
149 [ -s "$state_dir/current" ] || return 1
150 [ -s "$state_dir/stashhash" ] || return 1
151 [ -s "$state_dir/name" ] || return 1
152 [ -s "$state_dir/names" ] || return 1
153 [ -f "$state_dir/processed" ] || return 1
154 [ -f "$state_dir/no_auto" ] || return 1
155 [ -f "$state_dir/merging_topfiles" ] || return 1
156 [ -f "$state_dir/mergeours" ] || return 1
157 [ -f "$state_dir/mergeours" ] || return 1
158 if [ -s "$state_dir/mergeours" ]; then
159 [ -s "$state_dir/mergetheirs" ] || return 1
160 else
161 ! [ -s "$state_dir/mergetheirs" ] || return 1
165 restore_state() {
166 is_active || die "programmer error"
167 IFS= read -r fullcmd <"$state_dir/fullcmd" && [ -n "$fullcmd" ]
168 IFS= read -r base_remote <"$state_dir/remote" || :
169 IFS= read -r skipms <"$state_dir/skipms" || :
170 IFS= read -r all <"$state_dir/all" || :
171 IFS= read -r current <"$state_dir/current" && [ -n "$current" ]
172 IFS= read -r stashhash <"$state_dir/stashhash" && [ -n "$stashhash" ]
173 IFS= read -r name <"$state_dir/name" && [ -n "$name" ]
174 IFS= read -r names <"$state_dir/names" && [ -n "$names" ]
175 IFS= read -r processed <"$state_dir/processed" || :
176 IFS= read -r next_no_auto <"$state_dir/no_auto" || :
177 # merging_topfiles is for outside info but not to be restored
178 IFS= read -r mergeours <"$state_dir/mergeours" || :
179 IFS= read -r mergetheirs <"$state_dir/mergetheirs" || :
180 if [ -n "$mergeours" ] && [ -n "$mergetheirs" ]; then
181 headhash="$(git rev-parse --quiet --verify HEAD --)" || :
182 if [ -n "$headhash" ]; then
183 parents="$(git --no-pager log -n 1 --format='format:%P' "$headhash" -- 2>/dev/null)" || :
184 if [ "$parents" = "$mergeours $mergetheirs" ]; then
185 mergeresult="$headhash"
188 if [ -z "$mergeresult" ]; then
189 mergeours=
190 mergetheirs=
193 restored=1
196 clear_state() {
197 ! [ -e "$state_dir" ] || rm -rf "$state_dir" >/dev/null 2>&1 || :
200 restarted=
201 isactive=
202 ! is_active || isactive=1
203 if [ -n "$isactive" ] || [ $# -eq 1 -a x"$1" = x"--abort" ]; then
204 [ $# -eq 1 ] && [ x"$1" != x"--status" ] || { do_status; exit 0; }
205 case "$1" in
206 --abort)
207 current=
208 stashhash=
209 if [ -n "$isactive" ]; then
210 IFS= read -r current <"$state_dir/current" || :
211 IFS= read -r stashhash <"$state_dir/stashhash" || :
213 clear_state
214 if [ -n "$isactive" ]; then
215 if [ -n "$stashhash" ]; then
216 $tg revert -f -q -q --no-stash "$stashhash" >/dev/null 2>&1 || :
218 if [ -n "$current" ]; then
219 info "Ok, update aborted, returning to ${current#refs/heads/}"
220 checkout_symref_full -f "$current"
221 else
222 info "Ok, update aborted. Now, you just need to"
223 info "switch back to some sane branch using \`git$gitcdopt checkout\`."
225 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
226 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
227 else
228 info "No update was active"
230 exit 0
232 --stop)
233 clear_state
234 info "Ok, update stopped. Now, you just need to"
235 info "switch back to some sane branch using \`git$gitcdopt checkout\`."
236 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
237 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
238 exit 0
240 --continue|--skip)
241 restore_state
242 if [ "$1" = "--skip" ]; then
243 info "Ok, I will try to continue without updating this branch."
244 git reset --hard -q
245 case " $processed " in *" $name "*);;*)
246 processed="${processed:+$processed }$name"
247 esac
249 # assume user fixed it
250 # we could be left on a detached HEAD if we were resolving
251 # a conflict while merging a base in, fix it with a checkout
252 git checkout -q $iowopt "$(strip_ref "$name")"
255 do_status
256 exit 1
257 esac
259 clear_state
261 if [ -z "$restored" ]; then
262 while [ -n "$1" ]; do
263 arg="$1"; shift
264 case "$arg" in
266 usage;;
267 -a|--all)
268 [ -z "$names$pattern" ] || usage 1
269 all=1;;
270 --skip-missing)
271 skipms=1;;
272 --stash)
273 stash=1;;
274 --no-stash)
275 stash=;;
276 --quiet|-q)
277 quiet=1;;
278 --base)
279 basemode=1;;
280 --edit|-e)
281 editmode=1;;
282 --no-edit)
283 editmode=0;;
284 --no-commit)
285 basenc=1;;
286 --force|-f)
287 basefrc=1;;
289 [ $# -gt 0 ] && [ -n "$1" ] || die "option -m requires an argument"
290 basemsg="$1"
291 shift;;
292 -m?*)
293 basemsg="${1#-m}";;
294 --message=*)
295 basemsg="${1#--message=}";;
297 [ $# -gt 0 ] && [ -n "$1" ] || die "option -F requires an argument"
298 basefile="$1"
299 shift;;
300 -F?*)
301 basefile="${1#-F}";;
302 --file=*)
303 basefile="${1#--file=}"
304 [ -n "$basefile" ] || die "option --file= requires an argument"
306 -?*)
307 usage 1;;
309 break;;
313 if [ -z "$all" ]; then
314 namecnt=$(( $namecnt + 1 ))
315 [ "$namecnt" != "1" ] || name1="$arg"
316 [ "$namecnt" != "2" ] || name2="$arg"
317 names="${names:+$names }$arg"
318 else
319 pattern="${pattern:+$pattern }refs/$topbases/$(strip_ref "$arg")"
322 esac
323 done
324 while [ $# -gt 0 ]; do
325 if [ -z "$all" ]; then
326 namecnt=$(( $namecnt + 1 ))
327 [ "$namecnt" != "1" ] || name1="$1"
328 [ "$namecnt" != "2" ] || name2="$1"
329 names="${names:+$names }$*"
330 else
331 pattern="${pattern:+$pattern }refs/$topbases/$(strip_ref "$1")"
333 shift
334 done
335 [ -n "$basemode" ] || [ -z "$editmode$basemsg$basefile$basenc$basefrc" ] || usage 1
336 [ -z "$basemode" ] || [ -z "$all$skipms" ] || usage 1
337 [ -z "$basemode" ] || [ -z "$basemsg" ] || [ -z "$basefile" ] || usage 1
338 [ -z "$basemode" ] || [ "$namecnt" -eq 2 ] || usage 1
340 current="$(git symbolic-ref -q HEAD)" || :
341 if [ -n "$current" ]; then
342 [ -n "$(git rev-parse --verify --quiet HEAD --)" ] ||
343 die "cannot return to unborn branch; switch to another branch"
344 else
345 current="$(git rev-parse --verify --quiet HEAD)" ||
346 die "cannot return to invalid HEAD; switch to another branch"
349 [ -z "$basemode" ] || do_base_mode "$name1" "$name2"
351 origpattern="$pattern"
352 [ -z "$pattern" ] && pattern="refs/$topbases"
354 processed=
355 [ -n "$all$names" ] || names="HEAD"
356 if [ -z "$all" ]; then
357 clean_names() {
358 names=
359 while [ $# -gt 0 ]; do
360 name="$(verify_topgit_branch "$1")"
361 case " $names " in *" $name "*);;*)
362 names="${names:+$names }$name"
363 esac
364 shift
365 done
367 clean_names $names
369 ensure_clean_tree
372 save_state() {
373 mkdir -p "$state_dir"
374 printf '%s\n' "$fullcmd" >"$state_dir/fullcmd"
375 printf '%s\n' "$base_remote" >"$state_dir/remote"
376 printf '%s\n' "$skipms" >"$state_dir/skipms"
377 printf '%s\n' "$all" >"$state_dir/all"
378 printf '%s\n' "$current" >"$state_dir/current"
379 printf '%s\n' "$stashhash" >"$state_dir/stashhash"
380 printf '%s\n' "$name" >"$state_dir/name"
381 printf '%s\n' "$names" >"$state_dir/names"
382 printf '%s\n' "$processed" >"$state_dir/processed"
383 printf '%s\n' "$no_auto" >"$state_dir/no_auto"
384 # this one is an external flag and needs to be zero length for false
385 printf '%s' "$merging_topfiles" >"$state_dir/merging_topfiles"
386 printf '%s\n' "$1" >"$state_dir/mergeours"
387 printf '%s\n' "$2" >"$state_dir/mergetheirs"
390 stash_now_if_requested() {
391 [ -z "$TG_RECURSIVE" ] || return 0
392 [ -z "$stashhash" ] || return 0
393 ensure_ident_available
394 msg="tgupdate: autostash before update"
395 if [ -n "$all" ]; then
396 msg="$msg --all${origpattern:+ $origpattern}"
397 else
398 msg="$msg $names"
400 set -- $names
401 if [ -n "$stash" ]; then
402 $tg tag -q -q -m "$msg" --stash "$@" &&
403 stashhash="$(git rev-parse --quiet --verify refs/tgstash --)" &&
404 [ -n "$stashhash" ] &&
405 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
406 die "requested --stash failed"
407 else
408 $tg tag --anonymous "$@" &&
409 stashhash="$(git rev-parse --quiet --verify TG_STASH --)" &&
410 [ -n "$stashhash" ] &&
411 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
412 die "anonymous --stash failed"
414 [ -z "$next_no_auto" ] || no_auto="$next_no_auto"
415 next_no_auto=
418 recursive_update() {
419 _ret=0
420 on_base=
422 TG_RECURSIVE="[$1] $TG_RECURSIVE"
423 PS1="[$1] $PS1"
424 export PS1
425 update_branch "$1"
426 ) || _ret=$?
427 [ $_ret -eq 3 ] && exit 3
428 return $_ret
431 # If HEAD is a symref to "$1" detach it at its current value
432 detach_symref_head_on_branch() {
433 _hsr="$(git symbolic-ref -q HEAD --)" && [ -n "$_hsr" ] || return 0
434 _hrv="$(git rev-parse --quiet --verify HEAD --)" && [ -n "$_hrv" ] ||
435 die "cannot detach_symref_head_on_branch from unborn branch $_hsr"
436 git update-ref --no-deref -m "detaching HEAD from $_hsr to safely update it" HEAD "$_hrv"
439 # git_topmerge will need this even on success and since it might otherwise
440 # be called many times do it just the once here and now
441 repotoplvl="$(git rev-parse --show-toplevel)" && [ -n "$repotoplvl" ] && [ -d "$repotoplvl" ] ||
442 die "git rev-parse --show-toplevel failed"
444 # Run an in-tree recursive merge but make sure we get the desired version of
445 # any .topdeps and .topmsg files. The $auhopt and --no-stat options are
446 # always in effect. If successful a new commit is performed on HEAD.
448 # Except for --merge, the "git merge-recursive" tool (and others) must be
449 # run to get the desired result. And (except for --merge), --no-ff is always
450 # implicitly in effect as well.
452 # [optional] '-v' varname => optional variable to return original HEAD hash in
453 # [optional] '--merge', '--theirs' or '--remove' to alter .topfile handling
454 # [optional] '--name' <name-for-ours [--name <name-for-theirs>]
455 # $1 => '-m' MUST be '-m'
456 # $2 => commit message
457 # $3 => commit-ish to merge as "theirs"
458 git_topmerge()
460 _ours="$(git rev-parse --verify HEAD^0)" || die "git rev-parse failed"
461 _ovar=
462 [ "$1" != "-v" ] || [ $# -lt 2 ] || [ -z "$2" ] || { _ovar="$2"; shift 2; }
463 [ -z "$_ovar" ] || eval "$_ovar="'"$_ours"'
464 _mmode=
465 case "$1" in --theirs|--remove|--merge) _mmode="${1#--}"; shift; esac
466 _nameours=
467 _nametheirs=
468 if [ "$1" = "--name" ] && [ $# -ge 2 ]; then
469 _nameours="$2"
470 shift 2
471 if [ "$1" = "--name" ] && [ $# -ge 2 ]; then
472 _nametheirs="$2"
473 shift 2
476 : "${_nameours:=HEAD}"
477 eval "GITHEAD_$_ours="'"$_nameours"' && eval export "GITHEAD_$_ours"
478 _theirs=
479 if [ -n "$_nametheirs" ]; then
480 _theirs="$(git rev-parse --verify "$3^0")" || die "git rev-parse failed"
481 eval "GITHEAD_$_theirs="'"$_nametheirs"' && eval export "GITHEAD_$_theirs"
483 if [ "$_mmode" = "merge" ]; then
484 TG_L1="$_nameours" && export TG_L1
485 TG_L2="merged common ancestors" && export TG_L2
486 TG_L3="${_nametheirs:-$3}" && export TG_L3
487 # in this one very uncommon case we can use the real "git merge"
488 git -c 'merge.ours.driver=git merge-file -L "$TG_L1" -L "$TG_L2" -L "$TG_L3" --marker-size=%L %A %O %B' \
489 merge $auhopt --no-stat "$@"
490 else
491 [ "$#" -eq 3 ] && [ "$1" = "-m" ] && [ -n "$2" ] && [ -n "$3" ] ||
492 die "programmer error: invalid arguments to git_topmerge: $*"
493 _msg="$2"
494 [ -n "$_theirs" ] || _theirs="$(git rev-parse --verify "$3^0")" || die "git rev-parse failed"
495 _mt=
496 _mb="$(git merge-base --all "$_ours" "$_theirs")" && [ -n "$_mb" ] ||
497 { _mt=1; _mb="$(git hash-object -w -t tree --stdin < /dev/null)"; }
498 # any .topdeps or .topmsg output needs to be stripped from stdout
499 tmpstdout="$tg_tmp_dir/stdout.$$"
500 _ret=0
501 git -c "merge.ours.driver=touch %A" merge-recursive \
502 $_mb -- "$_ours" "$_theirs" >"$tmpstdout" || _ret=$?
503 # success or failure is not relevant until after fixing up the
504 # .topdeps and .topmsg files unless _ret >= 126
505 [ $_ret -lt 126 ] || return $_ret
506 case "$_mmode" in
507 theirs) _source="$_theirs";;
508 remove) _source="";;
509 *) _source="$_ours";;
510 esac
511 _newinfo=
512 [ -z "$_source" ] ||
513 _newinfo="$(git cat-file --batch-check="%(objecttype) %(objectname)$tab%(rest)" <<-EOT |
514 $_source:.topdeps .topdeps
515 $_source:.topmsg .topmsg
517 sed -n 's/^blob /100644 /p'
519 [ -z "$_newinfo" ] || _newinfo="$lf$_newinfo"
520 git update-index --index-info <<-EOT ||
521 0 $nullsha$tab.topdeps
522 0 $nullsha$tab.topmsg$_newinfo
524 die "git update-index failed"
525 if [ "$_mmode" = "remove" ] &&
526 { [ -e "$repotoplvl/.topdeps" ] || [ -e "$repotoplvl/.topmsg" ]; }
527 then
528 rm -r -f "$repotoplvl/.topdeps" "$repotoplvl/.topmsg" >/dev/null 2>&1 || :
529 else
530 for zapbad in "$repotoplvl/.topdeps" "$repotoplvl/.topmsg"; do
531 if [ -e "$zapbad" ] && { [ -L "$zapbad" ] || [ ! -f "$zapbad" ]; }; then
532 rm -r -f "$zapbad"
534 done
535 (cd "$repotoplvl" && git checkout-index -q -f -u -- .topdeps .topmsg) ||
536 die "git checkout-index failed"
538 # dump output without any .topdeps or .topmsg messages
539 sed -e '/ \.topdeps/d' -e '/ \.topmsg/d' <"$tmpstdout"
540 git ls-files --unmerged --full-name --abbrev :/ >"$tmpstdout" 2>&1 ||
541 die "git ls-files failed"
542 if [ -s "$tmpstdout" ]; then
543 [ "$_ret" != "0" ] || _ret=1
544 else
545 _ret=0
547 if [ $_ret -ne 0 ]; then
548 # merge failed, do rerere, spit out message and return
550 # rerere (will be a nop unless rerere.enabled is true)
551 git rerere || :
552 # enter "merge" mode before returning
554 printf '%s\n\n# Conflicts:\n' "$_msg"
555 sed -n "/$tab/s/^[^$tab]*/#/p" <"$tmpstdout" | sort -u
556 } >"$git_dir/MERGE_MSG"
557 git update-ref MERGE_HEAD "$_theirs" || :
558 echo 'Automatic merge failed; fix conflicts and then commit the result.'
559 rm -f "$tmpstdout"
560 return $_ret
562 # commit time at last!
563 thetree="$(git write-tree)" || die "git write-tree failed"
564 # avoid an extra "already up-to-date" commit (can't happen if _mt though)
565 origtree=
566 [ -n "$_mt" ] || origtree="$(git rev-parse --quiet --verify "$_ours^{tree}" --)" &&
567 [ -n "$origtree" ] || die "git rev-parse failed"
568 if [ "$origtree" != "$thetree" ] || ! contained_by "$_theirs" "$_ours"; then
569 thecommit="$(git commit-tree -p "$_ours" -p "$_theirs" -m "$_msg" "$thetree")" &&
570 [ -n "$thecommit" ] || die "git commit-tree failed"
571 git update-ref -m "$_msg" HEAD "$thecommit" || die "git update-ref failed"
573 # mention how the merge was made
574 echo "Merge made by the 'recursive' strategy."
575 rm -f "$tmpstdout"
576 return 0
580 # run git_topmerge with the passed in arguments (it always does --no-stat)
581 # then return the exit status of git_topmerge
582 # if the returned exit status is no error show a shortstat before
583 # returning assuming the merge was done into the previous HEAD but exclude
584 # .topdeps and .topmsg info from the stat unless doing a --merge
585 # if the first argument is --merge or --theirs or --remove handle .topmsg/.topdeps
586 # as follows:
587 # (default) .topmsg and .topdeps always keep ours
588 # --merge a normal merge takes place
589 # --theirs .topmsg and .topdeps always keep theirs
590 # --remove .topmsg and .topdeps are removed from the result and working tree
591 # note this function should only be called after attempt_index_merge fails as
592 # it implicity always does --no-ff (except for --merge which will --ff)
593 git_merge() {
594 _ret=0
595 git_topmerge -v _oldhead "$@" || _ret=$?
596 _exclusions=
597 [ "$1" = "--merge" ] || _exclusions=":/ :!/.topdeps :!/.topmsg"
598 [ "$_ret" != "0" ] || git --no-pager diff-tree --shortstat "$_oldhead" HEAD^0 -- $_exclusions
599 return $_ret
602 # similar to git_merge but operates exclusively using a separate index and temp dir
603 # only trivial aggressive automatic (i.e. simple) merges are supported
605 # [optional] '--no-auto' to suppress "automatic" merging, merge fails instead
606 # [optional] '--merge', '--theirs' or '--remove' to alter .topfile handling
607 # $1 => '' to discard result, 'refs/?*' to update the specified ref or a varname
608 # $2 => '-m' MUST be '-m'
609 # $3 => commit message AND, if $1 matches refs/?* the update-ref message
610 # $4 => commit-ish to merge as "ours"
611 # $5 => commit-ish to merge as "theirs"
612 # [$6...] => more commit-ishes to merge as "theirs" in octopus
614 # all merging is done in a separate index (or temporary files for simple merges)
615 # if successful the ref or var is updated with the result
616 # otherwise everything is left unchanged and a silent failure occurs
617 # if successful and $1 matches refs/?* it WILL BE UPDATED to a new commit using the
618 # message and appropriate parents AND HEAD WILL BE DETACHED first if it's a symref
619 # to the same ref
620 # otherwise if $1 does not match refs/?* and is not empty the named variable will
621 # be set to contain the resulting commit from the merge
622 # the working tree and index ARE LEFT COMPLETELY UNTOUCHED no matter what
623 v_attempt_index_merge() {
624 _noauto=
625 if [ "$1" = "--no-auto" ]; then
626 _noauto=1
627 shift
629 _exclusions=
630 [ "$1" = "--merge" ] || _exclusions=":/ :!/.topdeps :!/.topmsg"
631 _mstyle=
632 if [ "$1" = "--merge" ] || [ "$1" = "--theirs" ] || [ "$1" = "--remove" ]; then
633 _mmode="${1#--}"
634 shift
635 if [ "$_mmode" = "merge" ] || [ "$_mmode" = "theirs" ]; then
636 _mstyle="-top$_mmode"
639 if [ "$_mmode" = "remove" ] || [ "$_mmode" = "merge" ]; then
640 _agstyle="--aggressive"
641 else
642 # --aggressive still happens but in the helper in order to
643 # ensure correct handling of .topdeps and .topmsg with --ours/--theirs
644 _agstyle=
646 [ "$#" -ge 5 ] && [ "$2" = "-m" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] ||
647 die "programmer error: invalid arguments to v_attempt_index_merge: $*"
648 _var="$1"
649 _msg="$3"
650 _head="$4"
651 shift 4
652 rh="$(git rev-parse --quiet --verify "$_head^0" --)" && [ -n "$rh" ] || return 1
653 orh="$rh"
654 _mmsg=
655 newc=
656 _nodt=
657 _same=
658 _mt=
659 _octo=
660 if [ $# -gt 1 ]; then
661 if [ "$_mmode" = "merge" ] || [ "$_mmode" = "theirs" ]; then
662 die "programmer error: invalid octopus .topfile strategy to v_attempt_index_merge: --$_mode"
664 ihl="$(git merge-base --independent "$@")" || return 1
665 set -- $ihl
666 [ $# -ge 1 ] && [ -n "$1" ] || return 1
668 [ $# -eq 1 ] || _octo=1
669 mb="$(git merge-base ${_octo:+--octopus} "$rh" "$@")" && [ -n "$mb" ] || {
670 mb="$(git hash-object -w -t tree --stdin < /dev/null)"
671 _mt=1
673 if [ -z "$_mt" ]; then
674 if [ -n "$_octo" ]; then
675 while [ $# -gt 1 ] && mbh="$(git merge-base "$rh" "$1")" && [ -n "$mbh" ]; do
676 if [ "$rh" = "$mbh" ]; then
677 _mmsg="Fast-forward (no commit created)"
678 rh="$1"
679 shift
680 elif [ "$1" = "$mbh" ]; then
681 shift
682 else
683 break;
685 done
686 if [ $# -eq 1 ]; then
687 _octo=
688 mb="$(git merge-base "$rh" "$1")" && [ -n "$mb" ] || return 1
691 if [ -z "$_octo" ]; then
692 r1="$(git rev-parse --quiet --verify "$1^0" --)" && [ -n "$r1" ] || return 1
693 set -- "$r1"
694 if [ "$rh" = "$mb" ]; then
695 _mmsg="Fast-forward (no commit created)"
696 newc="$r1"
697 _nodt=1
698 _mstyle=
699 elif [ "$r1" = "$mb" ]; then
700 [ -n "$_mmsg" ] || _mmsg="Already up-to-date!"
701 newc="$rh"
702 _nodt=1
703 _same=1
704 _mstyle=
708 if [ -z "$newc" ]; then
709 inew="$tg_tmp_dir/index.$$"
710 ! [ -e "$inew" ] || rm -f "$inew"
711 itmp="$tg_tmp_dir/output.$$"
712 imrg="$tg_tmp_dir/auto.$$"
713 [ -z "$_octo" ] || >"$imrg"
714 _auto=
715 _parents=
716 _newrh="$rh"
717 while :; do
718 if [ -n "$_parents" ]; then
719 if contained_by "$1" "$_newrh"; then
720 shift
721 continue
724 GIT_INDEX_FILE="$inew" git read-tree -m $_agstyle -i "$mb" "$rh" "$1" || { rm -f "$inew" "$imrg"; return 1; }
725 GIT_INDEX_FILE="$inew" git ls-files --unmerged --full-name --abbrev :/ >"$itmp" 2>&1 || { rm -f "$inew" "$itmp" "$imrg"; return 1; }
726 ! [ -s "$itmp" ] || {
727 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
728 rm -f "$inew" "$itmp" "$imrg"
729 return 1
731 if [ -s "$itmp" ]; then
732 if [ -n "$_noauto" ]; then
733 rm -f "$inew" "$itmp" "$imrg"
734 return 1
736 if [ -n "$_octo" ]; then
737 cat "$itmp" >>"$imrg"
738 else
739 cat "$itmp"
741 _auto=" automatic"
744 _mstyle=
745 rm -f "$itmp"
746 newt="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] || { rm -f "$inew" "$imrg"; return 1; }
747 _parents="${_parents:+$_parents }-p $1"
748 if [ $# -gt 1 ]; then
749 rh="$newt"
750 shift
751 continue
753 break;
754 done
755 if [ "$_mmode" = "remove" ]; then
756 GIT_INDEX_FILE="$inew" git update-index --index-info <<-EOT &&
757 0 $nullsha$tab.topdeps
758 0 $nullsha$tab.topmsg
760 newt="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] || { rm -f "$inew" "$imrg"; return 1; }
762 [ -z "$_octo" ] || sort -u <"$imrg"
763 rm -f "$inew" "$imrg"
764 newc="$(git commit-tree -p "$orh" $_parents -m "$_msg" "$newt")" && [ -n "$newc" ] || return 1
765 _mmsg="Merge made by the 'trivial aggressive$_auto${_octo:+ octopus}' strategy."
767 case "$_var" in
768 refs/?*)
769 if [ -n "$_same" ]; then
770 _same=
771 if rv="$(git rev-parse --quiet --verify "$_var" --)" && [ "$rv" = "$newc" ]; then
772 _same=1
775 if [ -z "$_same" ]; then
776 detach_symref_head_on_branch "$_var" || return 1
777 # git update-ref returns 0 even on failure :(
778 git update-ref -m "$_msg" "$_var" "$newc" || return 1
782 eval "$_var="'"$newc"'
784 esac
785 echo "$_mmsg"
786 [ -n "$_nodt" ] || git --no-pager diff-tree --shortstat "$orh" "$newc" -- $_exclusions
787 return 0
790 # shortcut that passes $3 as a preceding argument (which must match refs/?*)
791 attempt_index_merge() {
792 _noauto=
793 _mmode=
794 if [ "$1" = "--no-auto" ]; then
795 _noauto="$1"
796 shift
798 if [ "$1" = "--merge" ] || [ "$1" = "--theirs" ] || [ "$1" = "--remove" ]; then
799 _mmode="$1"
800 shift
802 case "$3" in refs/?*);;*)
803 die "programmer error: invalid arguments to attempt_index_merge: $*"
804 esac
805 v_attempt_index_merge $_noauto $_mmode "$3" "$@"
808 on_base=
809 do_base_switch() {
810 [ -n "$1" ] || return 0
812 [ "$1" != "$on_base" ] ||
813 [ "$(git symbolic-ref -q HEAD)" != "refs/$topbases/$1" ]
814 then
815 switch_to_base "$1"
816 on_base="$1"
820 update_branch_internal() {
821 # We are cacheable until the first change
822 become_cacheable
824 _update_name="$1"
825 ## First, take care of our base
827 _depcheck="$(get_temp tg-depcheck)"
828 missing_deps=
829 needs_update "$_update_name" >"$_depcheck" || :
830 if [ -n "$missing_deps" ]; then
831 msg="Some dependencies are missing: $missing_deps"
832 if [ -n "$skipms" ]; then
833 info "$msg; skipping"
834 elif [ -z "$all" ]; then
835 die "$msg"
836 else
837 info "$msg; skipping branch $_update_name"
838 return 0
841 # allow automatic simple merges by default until a failure occurs
842 no_auto=
843 if [ -s "$_depcheck" ]; then
844 <"$_depcheck" \
845 sed 's/ [^ ]* *$//' | # last is $_update_name
846 sed 's/.* \([^ ]*\)$/+\1/' | # only immediate dependencies
847 sed 's/^\([^+]\)/-\1/' | # now each line is +branch or -branch (+ == recurse)
848 >"$_depcheck.ideps" \
849 uniq -s 1 # fold branch lines; + always comes before - and thus wins within uniq
851 stash_now_if_requested
853 while read -r depline; do
854 dep="${depline#?}"
855 action="${depline%$dep}"
857 # We do not distinguish between dependencies out-of-date
858 # and base/remote out-of-date cases for $dep here,
859 # but thanks to needs_update returning : or refs/remotes/<remote>/<name>
860 # for the latter, we do correctly recurse here
861 # in both cases.
863 if [ x"$action" = x+ ]; then
864 case " $missing_deps " in *" $dep "*)
865 info "Skipping recursing to missing dependency: $dep"
866 continue
867 esac
868 info "Recursing to $dep..."
869 recursive_update "$dep" || exit 3
871 done <"$_depcheck.ideps"
873 # Create a list of all the fully qualified ref names that need
874 # to be merged into $_update_name's base. This will be done
875 # as an octopus merge if there are no conflicts.
876 deplist=
877 deplines=
878 set --
879 while read -r dep; do
880 dep="${dep#?}"
881 case "$dep" in
882 "refs"/*)
883 set -- "$@" "$dep"
884 case "$dep" in
885 "refs/heads"/*)
886 d="${dep#refs/heads/}"
887 deplist="${deplist:+$deplist }$d"
888 deplines="$deplines$d$lf"
891 d="${dep#refs/}"
892 deplist="${deplist:+$deplist }$d"
893 deplines="$deplines$d$lf"
895 esac
898 set -- "$@" "refs/heads/$dep"
899 deplist="${deplist:+$deplist }$dep"
900 deplines="$deplines$dep$lf"
902 esac
903 done <"$_depcheck.ideps"
905 # Make sure we end up on the correct base branch
906 on_base=
907 if [ $# -ge 2 ]; then
908 info "Updating $_update_name base with deps: $deplist"
909 become_non_cacheable
910 msg="tgupdate: octopus merge $# deps into $topbases/$_update_name$lf$lf$deplines"
911 if attempt_index_merge --remove -m "$msg" "refs/$topbases/$_update_name" "$@"; then
912 set --
913 else
914 info "Octopus merge failed; falling back to multiple 3-way merges"
915 no_auto="--no-auto"
919 for fulldep in "$@"; do
920 # This will be either a proper topic branch
921 # or a remote base. (branch_needs_update() is called
922 # only on the _dependencies_, not our branch itself!)
924 case "$fulldep" in
925 "refs/heads"/*)
926 dep="${fulldep#refs/heads/}";;
927 "refs"/*)
928 dep="${fulldep#refs/}";;
930 dep="$fulldep";; # this should be a programmer error
931 esac
933 info "Updating $_update_name base with $dep changes..."
934 become_non_cacheable
935 msg="tgupdate: merge $dep into $topbases/$_update_name"
937 ! attempt_index_merge $no_auto --remove -m "$msg" "refs/$topbases/$_update_name" "$fulldep^0" &&
939 # We need to switch to the base branch
940 # ...but only if we aren't there yet (from failed previous merge)
941 do_base_switch "$_update_name" || die "do_base_switch failed" &&
942 git_merge --remove --name "$_update_name base" --name "$dep" -m "$msg" "$fulldep^0"
944 then
945 rm "$_depcheck"
946 save_state
947 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
948 info "(use \`$tgdisplayac status\` to see more options)"
949 exit 3
951 done
952 else
953 info "The base is up-to-date."
957 ## Second, update our head with the remote branch
959 plusextra=
960 merge_with="refs/$topbases/$_update_name"
961 brmmode=
962 if has_remote "$_update_name"; then
963 _rname="refs/remotes/$base_remote/$_update_name"
964 if branch_contains "refs/heads/$_update_name" "$_rname"; then
965 info "The $_update_name head is up-to-date wrt. its remote branch."
966 else
967 stash_now_if_requested
968 info "Reconciling $_update_name base with remote branch updates..."
969 become_non_cacheable
970 msg="tgupdate: merge ${_rname#refs/} onto $topbases/$_update_name"
971 checkours=
972 checktheirs=
973 got_merge_with=
974 brmmode="--merge"
975 if [ -n "$mergeresult" ]; then
976 checkours="$(git rev-parse --verify --quiet "refs/$topbases/$_update_name^0" --)" || :
977 checktheirs="$(git rev-parse --verify --quiet "$_rname^0" --)" || :
978 if [ "$mergeours" = "$checkours" ] && [ "$mergetheirs" = "$checktheirs" ]; then
979 got_merge_with="$mergeresult"
983 [ -z "$got_merge_with" ] &&
984 ! v_attempt_index_merge $no_auto --theirs "merge_with" -m "$msg" "refs/$topbases/$_update_name" "$_rname^0" &&
986 # *DETACH* our HEAD now!
987 no_auto="--no-auto"
988 git checkout -q --detach $iowopt "refs/$topbases/$_update_name" || die "git checkout failed" &&
989 git_merge --theirs --name "$_update_name base content" --name "${_rname#refs/}" -m "$msg" "$_rname^0" &&
990 merge_with="$(git rev-parse --verify HEAD --)"
992 then
993 save_state \
994 "$(git rev-parse --verify --quiet "refs/$topbases/$_update_name^0" --)" \
995 "$(git rev-parse --verify --quiet "$_rname^0" --)"
996 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
997 info "(use \`$tgdisplayac status\` to see more options)"
998 exit 3
1000 # Go back but remember we want to merge with this, not base
1001 [ -z "$got_merge_with" ] || merge_with="$got_merge_with"
1002 plusextra="${_rname#refs/} + "
1007 ## Third, update our head with the base
1009 if branch_contains "refs/heads/$_update_name" "$merge_with"; then
1010 info "The $_update_name head is up-to-date wrt. the base."
1011 return 0
1013 stash_now_if_requested
1014 info "Updating $_update_name against ${plusextra}new base..."
1015 become_non_cacheable
1016 msg="tgupdate: merge ${plusextra}$topbases/$_update_name into $_update_name"
1017 b4deps=
1018 if [ -n "$brmmode" ] && [ "$base_remote" ]; then
1019 b4deps="$(git rev-parse --verify --quiet "refs/heads/$_update_name:.topdeps" --)" && [ -n "$b4deps" ] ||
1020 b4deps="$(git hash-object -t blob -w --stdin </dev/null)"
1023 ! attempt_index_merge $no_auto $brmmode -m "$msg" "refs/heads/$_update_name" "$merge_with^0" &&
1025 # Home, sweet home...
1026 # (We want to always switch back, in case we were
1027 # on the base from failed previous merge.)
1028 git checkout -q $iowopt "$_update_name" || die "git checkout failed" &&
1029 git_merge $brmmode --name "$_update_name" --name "${plusextra}$topbases/$_update_name" -m "$msg" "$merge_with^0"
1031 then
1032 no_auto=
1033 merging_topfiles="${brmmode:+1}"
1034 save_state
1035 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
1036 info "(use \`$tgdisplayac status\` to see more options)"
1037 exit 3
1040 # Fourth, auto create locally any newly depended on branches we got from the remote
1042 _result=0
1043 if [ -n "$b4deps" ] &&
1044 l8rdeps="$(git rev-parse --verify --quiet "refs/heads/$_update_name:.topdeps" --)" &&
1045 [ -n "$l8rdeps" ] && [ "$b4deps" != "$l8rdeps" ]
1046 then
1047 _olddeps=
1048 while read -r newdep; do
1049 if [ -n "$newdep" ]; then
1050 if auto_create_local_remote "$newdep"; then
1051 _result=75
1052 else
1053 if ref_exists "refs/heads/$newdep"; then
1054 # maybe the line just moved around
1055 [ -n "$_olddeps" ] && [ -f "$_olddeps" ] || {
1056 _olddeps="$(get_temp b4deps)" &&
1057 git cat-file blob "$b4deps" >"$_olddeps"
1059 if awk -v "newdep=$newdep" '$0 == newdep {exit 1}' <"$_olddeps"; then
1060 # nope, it's a new head already existing locally
1061 _result=75
1063 else
1064 # helpfully check to see if there's such a remote branch
1065 _rntgb=
1066 ! ref_exists "refs/remotes/$base_remote/$newdep" || _rntgb=1
1067 # maybe a locking local orphan base too
1068 _blocked=
1069 if [ -n "$_rntgb" ] &&
1070 ref_exists "refs/remotes/$base_remote/${topbases#heads/}$newdep" &&
1071 ref_exists "refs/$topbases/$newdep"
1072 then
1073 _blocked=1
1075 # spew the flexibly adjustable warning
1076 warn "-------------------------------------"
1077 warn "MISSING DEPENDENCY MERGED FROM REMOTE"
1078 warn "-------------------------------------"
1079 warn "Local Branch: $_update_name"
1080 warn " Remote Name: $base_remote"
1081 warn " Dependency: $newdep"
1082 if [ -n "$_blocked" ]; then
1083 warn "Blocking Ref: refs/$topbases/$newdep"
1084 elif [ -n "$_rntgb" ]; then
1085 warn "Existing Ref: refs/remotes/$base_remote/$newdep"
1087 warn ""
1088 if [ -n "$_blocked" ]; then
1089 warn "There is no local branch by that name, but"
1090 warn "there IS a remote TopGit branch available by"
1091 warn "that name, but creation of a local version has"
1092 warn "been blocked by existence of the ref shown above."
1093 elif [ -n "$_rntgb" ]; then
1094 warn "There is no local branch or remote TopGit"
1095 warn "branch available by that name, but there is an"
1096 warn "existing non-TopGit remote branch ref shown above."
1097 warn "Non-TopGit branches are not set up automatically"
1098 warn "by TopGit and must be maintained manually."
1099 else
1100 warn "There is no local branch or remote branch"
1101 warn "(TopGit or otherwise) available by that name."
1103 warn "-------------------------------------"
1107 done <<-EOT
1108 $(git diff "$b4deps" "$l8rdeps" -- | diff_added_lines)
1111 return $_result
1114 update_branch() {
1115 _ubicode=0
1116 _maxdeploop=3
1117 update_branch_internal "$@" || _ubicode=$?
1118 while [ "$_maxdeploop" -gt 0 ] && [ "$_ubicode" = "75" ]; do
1119 _maxdeploop="$(( $maxdeploop - 1 ))"
1120 info "Updating $1 again with newly added dependencies..."
1121 _ubicode=0
1122 update_branch_internal "$@" || _ubicode=$?
1123 done
1124 return $_ubicode
1127 # We are "read-only" and cacheable until the first change
1128 tg_read_only=1
1129 v_create_ref_cache
1131 do_non_annihilated_branches_patterns() {
1132 while read -r _pat && [ -n "$_pat" ]; do
1133 set -- "$@" "$_pat"
1134 done
1135 non_annihilated_branches "$@"
1138 do_non_annihilated_branches() {
1139 if [ -z "$pattern" ]; then
1140 non_annihilated_branches
1141 else
1142 do_non_annihilated_branches_patterns <<-EOT
1143 $(sed 'y/ /\n/' <<-LIST
1144 $pattern
1145 LIST
1151 if [ -n "$all" ] && [ -z "$restored" ]; then
1152 names=
1153 while read name && [ -n "$name" ]; do
1154 case " $names " in *" $name "*);;*)
1155 names="${names:+$names }$name"
1156 esac
1157 done <<-EOT
1158 $(do_non_annihilated_branches)
1162 for name in $names; do
1163 case " $processed " in *" $name "*) continue; esac
1164 [ -z "$all" ] && case "$names" in *" "*) ! :; esac || info "Proccessing $name..."
1165 update_branch "$name" || exit
1166 processed="${processed:+$processed }$name"
1167 done
1169 [ -z "$all" ] && case "$names" in *" "*) ! :; esac ||
1170 info "Returning to ${current#refs/heads/}..."
1171 checkout_symref_full "$current"
1172 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
1173 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :