tg-summary.sh: support --rdeps-once and --rdeps-full
[topgit/pro.git] / tg-update.sh
blob481090cfa58f3796b98462e4d1c1a4d6a77c8fe5
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 isactiveopt=
204 if [ -z "$isactive" ] && [ $# -eq 1 ]; then
205 case "$1" in --abort|--stop|--continue|--skip) isactiveopt=1; esac
207 if [ -n "$isactive" ] || [ -n "$isactiveopt" ]; then
208 [ $# -eq 1 ] && [ x"$1" != x"--status" ] || { do_status; exit 0; }
209 if [ -z "$isactive" ]; then
210 clear_state
211 info "No update is currently active"
212 exit 0
214 case "$1" in
215 --abort)
216 current=
217 stashhash=
218 IFS= read -r current <"$state_dir/current" || :
219 IFS= read -r stashhash <"$state_dir/stashhash" || :
220 clear_state
221 if [ -n "$stashhash" ]; then
222 tg revert -f -q -q --no-stash "$stashhash" >/dev/null 2>&1 || :
224 if [ -n "$current" ]; then
225 info "Ok, update aborted, returning to ${current#refs/heads/}"
226 checkout_symref_full -f "$current"
227 else
228 info "Ok, update aborted. Now, you just need to"
229 info "switch back to some sane branch using \`git$gitcdopt checkout\`."
231 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
232 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
233 exit 0
235 --stop)
236 clear_state
237 info "Ok, update stopped. Now, you just need to"
238 info "switch back to some sane branch using \`git$gitcdopt checkout\`."
239 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
240 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
241 exit 0
243 --continue|--skip)
244 restore_state
245 if [ "$1" = "--skip" ]; then
246 info "Ok, I will try to continue without updating this branch."
247 git reset --hard -q
248 case " $processed " in *" $name "*);;*)
249 processed="${processed:+$processed }$name"
250 esac
252 # assume user fixed it
253 # we could be left on a detached HEAD if we were resolving
254 # a conflict while merging a base in, fix it with a checkout
255 git checkout -q $iowopt "$(strip_ref "$name")"
258 do_status
259 exit 1
260 esac
262 clear_state
264 if [ -z "$restored" ]; then
265 while [ -n "$1" ]; do
266 arg="$1"; shift
267 case "$arg" in
269 usage;;
270 -a|--all)
271 [ -z "$names$pattern" ] || usage 1
272 all=1;;
273 --skip-missing)
274 skipms=1;;
275 --stash)
276 stash=1;;
277 --no-stash)
278 stash=;;
279 --quiet|-q)
280 quiet=1;;
281 --base)
282 basemode=1;;
283 --edit|-e)
284 editmode=1;;
285 --no-edit)
286 editmode=0;;
287 --no-commit)
288 basenc=1;;
289 --force|-f)
290 basefrc=1;;
292 [ $# -gt 0 ] && [ -n "$1" ] || die "option -m requires an argument"
293 basemsg="$1"
294 shift;;
295 -m?*)
296 basemsg="${1#-m}";;
297 --message=*)
298 basemsg="${1#--message=}";;
300 [ $# -gt 0 ] && [ -n "$1" ] || die "option -F requires an argument"
301 basefile="$1"
302 shift;;
303 -F?*)
304 basefile="${1#-F}";;
305 --file=*)
306 basefile="${1#--file=}"
307 [ -n "$basefile" ] || die "option --file= requires an argument"
309 -?*)
310 usage 1;;
312 break;;
316 if [ -z "$all" ]; then
317 namecnt=$(( $namecnt + 1 ))
318 [ "$namecnt" != "1" ] || name1="$arg"
319 [ "$namecnt" != "2" ] || name2="$arg"
320 names="${names:+$names }$arg"
321 else
322 pattern="${pattern:+$pattern }refs/$topbases/$(strip_ref "$arg")"
325 esac
326 done
327 while [ $# -gt 0 ]; do
328 if [ -z "$all" ]; then
329 namecnt=$(( $namecnt + 1 ))
330 [ "$namecnt" != "1" ] || name1="$1"
331 [ "$namecnt" != "2" ] || name2="$1"
332 names="${names:+$names }$*"
333 else
334 pattern="${pattern:+$pattern }refs/$topbases/$(strip_ref "$1")"
336 shift
337 done
338 [ -n "$basemode" ] || [ -z "$editmode$basemsg$basefile$basenc$basefrc" ] || usage 1
339 [ -z "$basemode" ] || [ -z "$all$skipms" ] || usage 1
340 [ -z "$basemode" ] || [ -z "$basemsg" ] || [ -z "$basefile" ] || usage 1
341 [ -z "$basemode" ] || [ "$namecnt" -eq 2 ] || usage 1
343 current="$(git symbolic-ref -q HEAD)" || :
344 if [ -n "$current" ]; then
345 [ -n "$(git rev-parse --verify --quiet HEAD --)" ] ||
346 die "cannot return to unborn branch; switch to another branch"
347 else
348 current="$(git rev-parse --verify --quiet HEAD)" ||
349 die "cannot return to invalid HEAD; switch to another branch"
352 [ -z "$basemode" ] || do_base_mode "$name1" "$name2"
354 origpattern="$pattern"
355 [ -z "$pattern" ] && pattern="refs/$topbases"
357 processed=
358 [ -n "$all$names" ] || names="HEAD"
359 if [ -z "$all" ]; then
360 clean_names() {
361 names=
362 while [ $# -gt 0 ]; do
363 name="$(verify_topgit_branch "$1")"
364 case " $names " in *" $name "*);;*)
365 names="${names:+$names }$name"
366 esac
367 shift
368 done
370 clean_names $names
372 ensure_clean_tree
375 save_state() {
376 mkdir -p "$state_dir"
377 printf '%s\n' "$fullcmd" >"$state_dir/fullcmd"
378 printf '%s\n' "$base_remote" >"$state_dir/remote"
379 printf '%s\n' "$skipms" >"$state_dir/skipms"
380 printf '%s\n' "$all" >"$state_dir/all"
381 printf '%s\n' "$current" >"$state_dir/current"
382 printf '%s\n' "$stashhash" >"$state_dir/stashhash"
383 printf '%s\n' "$name" >"$state_dir/name"
384 printf '%s\n' "$names" >"$state_dir/names"
385 printf '%s\n' "$processed" >"$state_dir/processed"
386 printf '%s\n' "$no_auto" >"$state_dir/no_auto"
387 # this one is an external flag and needs to be zero length for false
388 printf '%s' "$merging_topfiles" >"$state_dir/merging_topfiles"
389 printf '%s\n' "$1" >"$state_dir/mergeours"
390 printf '%s\n' "$2" >"$state_dir/mergetheirs"
393 stash_now_if_requested() {
394 [ -z "$TG_RECURSIVE" ] || return 0
395 [ -z "$stashhash" ] || return 0
396 ensure_ident_available
397 msg="tgupdate: autostash before update"
398 if [ -n "$all" ]; then
399 msg="$msg --all${origpattern:+ $origpattern}"
400 else
401 msg="$msg $names"
403 set -- $names
404 if [ -n "$stash" ]; then
405 tg tag -q -q -m "$msg" --stash "$@" &&
406 stashhash="$(git rev-parse --quiet --verify refs/tgstash --)" &&
407 [ -n "$stashhash" ] &&
408 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
409 die "requested --stash failed"
410 else
411 tg tag --anonymous "$@" &&
412 stashhash="$(git rev-parse --quiet --verify TG_STASH --)" &&
413 [ -n "$stashhash" ] &&
414 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
415 die "anonymous --stash failed"
417 [ -z "$next_no_auto" ] || no_auto="$next_no_auto"
418 next_no_auto=
421 recursive_update() {
422 _ret=0
423 on_base=
425 if [ -n "$TG_RECURSIVE" ]; then
426 TG_RECURSIVE="==> [$1]${TG_RECURSIVE#==>}"
427 else
428 TG_RECURSIVE="==> [$1]$lf"
430 update_branch "$1"
431 ) || _ret=$?
432 [ $_ret -eq 3 ] && exit 3
433 return $_ret
436 # If HEAD is a symref to "$1" detach it at its current value
437 detach_symref_head_on_branch() {
438 _hsr="$(git symbolic-ref -q HEAD --)" && [ -n "$_hsr" ] || return 0
439 _hrv="$(git rev-parse --quiet --verify HEAD --)" && [ -n "$_hrv" ] ||
440 die "cannot detach_symref_head_on_branch from unborn branch $_hsr"
441 git update-ref --no-deref -m "detaching HEAD from $_hsr to safely update it" HEAD "$_hrv"
444 # git_topmerge will need this even on success and since it might otherwise
445 # be called many times do it just the once here and now
446 repotoplvl="$(git rev-parse --show-toplevel)" && [ -n "$repotoplvl" ] && [ -d "$repotoplvl" ] ||
447 die "git rev-parse --show-toplevel failed"
449 # Run an in-tree recursive merge but make sure we get the desired version of
450 # any .topdeps and .topmsg files. The $auhopt and --no-stat options are
451 # always implicitly in effect. If successful, a new commit is performed on HEAD.
453 # The "git merge-recursive" tool (and others) must be run to get the desired
454 # result. And --no-ff is always implicitly in effect as well.
456 # NOTE: [optional] arguments MUST appear in the order shown
457 # [optional] '-v' varname => optional variable to return original HEAD hash in
458 # [optional] '--merge', '--theirs' or '--remove' to alter .topfile handling
459 # [optional] '--name' <name-for-ours [--name <name-for-theirs>]
460 # $1 => '-m' MUST be '-m'
461 # $2 => commit message
462 # $3 => commit-ish to merge as "theirs"
463 git_topmerge()
465 _ovar=
466 [ "$1" != "-v" ] || [ $# -lt 2 ] || [ -z "$2" ] || { _ovar="$2"; shift 2; }
467 _mmode=
468 case "$1" in --theirs|--remove|--merge) _mmode="${1#--}"; shift; esac
469 _nameours=
470 _nametheirs=
471 if [ "$1" = "--name" ] && [ $# -ge 2 ]; then
472 _nameours="$2"
473 shift 2
474 if [ "$1" = "--name" ] && [ $# -ge 2 ]; then
475 _nametheirs="$2"
476 shift 2
479 : "${_nameours:=HEAD}"
480 [ "$#" -eq 3 ] && [ "$1" = "-m" ] && [ -n "$2" ] && [ -n "$3" ] ||
481 die "programmer error: invalid arguments to git_topmerge: $*"
482 _ours="$(git rev-parse --verify HEAD^0)" || die "git rev-parse failed"
483 _theirs="$(git rev-parse --verify "$3^0")" || die "git rev-parse failed"
484 [ -z "$_ovar" ] || eval "$_ovar="'"$_ours"'
485 eval "GITHEAD_$_ours="'"$_nameours"' && eval export "GITHEAD_$_ours"
486 if [ -n "$_nametheirs" ]; then
487 eval "GITHEAD_$_theirs="'"$_nametheirs"' && eval export "GITHEAD_$_theirs"
489 _mdriver='touch %A'
490 if [ "$_mmode" = "merge" ]; then
491 TG_L1="$_nameours" && export TG_L1
492 TG_L2="merged common ancestors" && export TG_L2
493 TG_L3="${_nametheirs:-$3}" && export TG_L3
494 _mdriver='git merge-file -L "$TG_L1" -L "$TG_L2" -L "$TG_L3" --marker-size=%L %A %O %B'
496 _msg="$2"
497 _mt=
498 _mb="$(git merge-base --all "$_ours" "$_theirs")" && [ -n "$_mb" ] ||
499 { _mt=1; _mb="$(git hash-object -w -t tree --stdin < /dev/null)"; }
500 # any .topdeps or .topmsg output needs to be stripped from stdout
501 tmpstdout="$tg_tmp_dir/stdout.$$"
502 _ret=0
503 git -c "merge.ours.driver=$_mdriver" merge-recursive \
504 $_mb -- "$_ours" "$_theirs" >"$tmpstdout" || _ret=$?
505 # success or failure is not relevant until after fixing up the
506 # .topdeps and .topmsg files and running rerere unless _ret >= 126
507 [ $_ret -lt 126 ] || return $_ret
508 if [ "$_mmode" = "merge" ]; then
509 cat "$tmpstdout"
510 else
511 case "$_mmode" in
512 theirs) _source="$_theirs";;
513 remove) _source="";;
514 *) _source="$_ours";;
515 esac
516 _newinfo=
517 [ -z "$_source" ] ||
518 _newinfo="$(git cat-file --batch-check="%(objecttype) %(objectname)$tab%(rest)" <<-EOT |
519 $_source:.topdeps .topdeps
520 $_source:.topmsg .topmsg
522 sed -n 's/^blob /100644 /p'
524 [ -z "$_newinfo" ] || _newinfo="$lf$_newinfo"
525 git update-index --index-info <<-EOT ||
526 0 $nullsha$tab.topdeps
527 0 $nullsha$tab.topmsg$_newinfo
529 die "git update-index failed"
530 if [ "$_mmode" = "remove" ] &&
531 { [ -e "$repotoplvl/.topdeps" ] || [ -e "$repotoplvl/.topmsg" ]; }
532 then
533 rm -r -f "$repotoplvl/.topdeps" "$repotoplvl/.topmsg" >/dev/null 2>&1 || :
534 else
535 for zapbad in "$repotoplvl/.topdeps" "$repotoplvl/.topmsg"; do
536 if [ -e "$zapbad" ] && { [ -L "$zapbad" ] || [ ! -f "$zapbad" ]; }; then
537 rm -r -f "$zapbad"
539 done
540 (cd "$repotoplvl" && git checkout-index -q -f -u -- .topdeps .topmsg) ||
541 die "git checkout-index failed"
543 # dump output without any .topdeps or .topmsg messages
544 sed -e '/ \.topdeps/d' -e '/ \.topmsg/d' <"$tmpstdout"
546 # rerere will be a nop unless rerere.enabled is true, but might complete the merge!
547 git rerere || :
548 git ls-files --unmerged --full-name --abbrev :/ >"$tmpstdout" 2>&1 ||
549 die "git ls-files failed"
550 if [ -s "$tmpstdout" ]; then
551 [ "$_ret" != "0" ] || _ret=1
552 else
553 _ret=0
555 if [ $_ret -ne 0 ]; then
556 # merge failed, spit out message, enter "merge" mode and return
558 printf '%s\n\n# Conflicts:\n' "$_msg"
559 sed -n "/$tab/s/^[^$tab]*/#/p" <"$tmpstdout" | sort -u
560 } >"$git_dir/MERGE_MSG"
561 git update-ref MERGE_HEAD "$_theirs" || :
562 echo 'Automatic merge failed; fix conflicts and then commit the result.'
563 rm -f "$tmpstdout"
564 return $_ret
566 # commit time at last!
567 thetree="$(git write-tree)" || die "git write-tree failed"
568 # avoid an extra "already up-to-date" commit (can't happen if _mt though)
569 origtree=
570 [ -n "$_mt" ] || origtree="$(git rev-parse --quiet --verify "$_ours^{tree}" --)" &&
571 [ -n "$origtree" ] || die "git rev-parse failed"
572 if [ "$origtree" != "$thetree" ] || ! contained_by "$_theirs" "$_ours"; then
573 thecommit="$(git commit-tree -p "$_ours" -p "$_theirs" -m "$_msg" "$thetree")" &&
574 [ -n "$thecommit" ] || die "git commit-tree failed"
575 git update-ref -m "$_msg" HEAD "$thecommit" || die "git update-ref failed"
577 # mention how the merge was made
578 echo "Merge made by the 'recursive' strategy."
579 rm -f "$tmpstdout"
580 return 0
583 # run git_topmerge with the passed in arguments (it always does --no-stat)
584 # then return the exit status of git_topmerge
585 # if the returned exit status is no error show a shortstat before
586 # returning assuming the merge was done into the previous HEAD but exclude
587 # .topdeps and .topmsg info from the stat unless doing a --merge
588 # if the first argument is --merge or --theirs or --remove handle .topmsg/.topdeps
589 # as follows:
590 # (default) .topmsg and .topdeps always keep ours
591 # --merge a normal merge takes place
592 # --theirs .topmsg and .topdeps always keep theirs
593 # --remove .topmsg and .topdeps are removed from the result and working tree
594 # note this function should only be called after attempt_index_merge fails as
595 # it implicity always does --no-ff (except for --merge which will --ff)
596 git_merge() {
597 _ret=0
598 git_topmerge -v _oldhead "$@" || _ret=$?
599 _exclusions=
600 [ "$1" = "--merge" ] || _exclusions=":/ :!/.topdeps :!/.topmsg"
601 [ "$_ret" != "0" ] || git --no-pager diff-tree --shortstat "$_oldhead" HEAD^0 -- $_exclusions
602 return $_ret
605 # similar to git_merge but operates exclusively using a separate index and temp dir
606 # only trivial aggressive automatic (i.e. simple) merges are supported
608 # [optional] '--no-auto' to suppress "automatic" merging, merge fails instead
609 # [optional] '--merge', '--theirs' or '--remove' to alter .topfile handling
610 # $1 => '' to discard result, 'refs/?*' to update the specified ref or a varname
611 # $2 => '-m' MUST be '-m'
612 # $3 => commit message AND, if $1 matches refs/?* the update-ref message
613 # $4 => commit-ish to merge as "ours"
614 # $5 => commit-ish to merge as "theirs"
615 # [$6...] => more commit-ishes to merge as "theirs" in octopus
617 # all merging is done in a separate index (or temporary files for simple merges)
618 # if successful the ref or var is updated with the result
619 # otherwise everything is left unchanged and a silent failure occurs
620 # if successful and $1 matches refs/?* it WILL BE UPDATED to a new commit using the
621 # message and appropriate parents AND HEAD WILL BE DETACHED first if it's a symref
622 # to the same ref
623 # otherwise if $1 does not match refs/?* and is not empty the named variable will
624 # be set to contain the resulting commit from the merge
625 # the working tree and index ARE LEFT COMPLETELY UNTOUCHED no matter what
626 v_attempt_index_merge() {
627 _noauto=
628 if [ "$1" = "--no-auto" ]; then
629 _noauto=1
630 shift
632 _exclusions=
633 [ "$1" = "--merge" ] || _exclusions=":/ :!/.topdeps :!/.topmsg"
634 _mstyle=
635 if [ "$1" = "--merge" ] || [ "$1" = "--theirs" ] || [ "$1" = "--remove" ]; then
636 _mmode="${1#--}"
637 shift
638 if [ "$_mmode" = "merge" ] || [ "$_mmode" = "theirs" ]; then
639 _mstyle="-top$_mmode"
642 [ "$#" -ge 5 ] && [ "$2" = "-m" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] ||
643 die "programmer error: invalid arguments to v_attempt_index_merge: $*"
644 _var="$1"
645 _msg="$3"
646 _head="$4"
647 shift 4
648 rh="$(git rev-parse --quiet --verify "$_head^0" --)" && [ -n "$rh" ] || return 1
649 orh="$rh"
650 oth=
651 _mmsg=
652 newc=
653 _nodt=
654 _same=
655 _mt=
656 _octo=
657 if [ $# -gt 1 ]; then
658 if [ "$_mmode" = "merge" ] || [ "$_mmode" = "theirs" ]; then
659 die "programmer error: invalid octopus .topfile strategy to v_attempt_index_merge: --$_mode"
661 ihl="$(git merge-base --independent "$@")" || return 1
662 set -- $ihl
663 [ $# -ge 1 ] && [ -n "$1" ] || return 1
665 [ $# -eq 1 ] || _octo=1
666 mb="$(git merge-base ${_octo:+--octopus} "$rh" "$@")" && [ -n "$mb" ] || {
667 mb="$(git hash-object -w -t tree --stdin < /dev/null)"
668 _mt=1
670 if [ -z "$_mt" ]; then
671 if [ -n "$_octo" ]; then
672 while [ $# -gt 1 ] && mbh="$(git merge-base "$rh" "$1")" && [ -n "$mbh" ]; do
673 if [ "$rh" = "$mbh" ]; then
674 _mmsg="Fast-forward (no commit created)"
675 rh="$1"
676 shift
677 elif [ "$1" = "$mbh" ]; then
678 shift
679 else
680 break;
682 done
683 if [ $# -eq 1 ]; then
684 _octo=
685 mb="$(git merge-base "$rh" "$1")" && [ -n "$mb" ] || return 1
688 if [ -z "$_octo" ]; then
689 r1="$(git rev-parse --quiet --verify "$1^0" --)" && [ -n "$r1" ] || return 1
690 oth="$r1"
691 set -- "$r1"
692 if [ "$rh" = "$mb" ]; then
693 _mmsg="Fast-forward (no commit created)"
694 newc="$r1"
695 _nodt=1
696 _mstyle=
697 elif [ "$r1" = "$mb" ]; then
698 [ -n "$_mmsg" ] || _mmsg="Already up-to-date!"
699 newc="$rh"
700 _nodt=1
701 _same=1
702 _mstyle=
706 if [ -z "$newc" ]; then
707 if [ "$_mmode" = "theirs" ] && [ -z "$oth" ]; then
708 oth="$(git rev-parse --quiet --verify "$1^0" --)" && [ -n "$oth" ] || return 1
709 set -- "$oth"
711 inew="$tg_tmp_dir/index.$$"
712 ! [ -e "$inew" ] || rm -f "$inew"
713 itmp="$tg_tmp_dir/output.$$"
714 imrg="$tg_tmp_dir/auto.$$"
715 [ -z "$_octo" ] || >"$imrg"
716 _auto=
717 _parents=
718 _newrh="$rh"
719 while :; do
720 if [ -n "$_parents" ]; then
721 if contained_by "$1" "$_newrh"; then
722 shift
723 continue
726 GIT_INDEX_FILE="$inew" git read-tree -m --aggressive -i "$mb" "$rh" "$1" || { rm -f "$inew" "$imrg"; return 1; }
727 GIT_INDEX_FILE="$inew" git ls-files --unmerged --full-name --abbrev :/ >"$itmp" 2>&1 || { rm -f "$inew" "$itmp" "$imrg"; return 1; }
728 ! [ -s "$itmp" ] || {
729 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
730 rm -f "$inew" "$itmp" "$imrg"
731 return 1
733 if [ -s "$itmp" ]; then
734 if [ -n "$_noauto" ]; then
735 rm -f "$inew" "$itmp" "$imrg"
736 return 1
738 if [ -n "$_octo" ]; then
739 cat "$itmp" >>"$imrg"
740 else
741 cat "$itmp"
743 _auto=" automatic"
746 _mstyle=
747 rm -f "$itmp"
748 _parents="${_parents:+$_parents }-p $1"
749 if [ $# -gt 1 ]; then
750 newt="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] || { rm -f "$inew" "$imrg"; return 1; }
751 rh="$newt"
752 shift
753 continue
755 break;
756 done
757 if [ "$_mmode" != "merge" ]; then
758 case "$_mmode" in
759 theirs) _source="$oth";;
760 remove) _source="";;
761 *) _source="$orh";;
762 esac
763 _newinfo=
764 [ -z "$_source" ] ||
765 _newinfo="$(git cat-file --batch-check="%(objecttype) %(objectname)$tab%(rest)" <<-EOT |
766 $_source:.topdeps .topdeps
767 $_source:.topmsg .topmsg
769 sed -n 's/^blob /100644 /p'
771 [ -z "$_newinfo" ] || _newinfo="$lf$_newinfo"
772 GIT_INDEX_FILE="$inew" git update-index --index-info <<-EOT || { rm -f "$inew" "$imrg"; return 1; }
773 0 $nullsha$tab.topdeps
774 0 $nullsha$tab.topmsg$_newinfo
777 newt="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] || { rm -f "$inew" "$imrg"; return 1; }
778 [ -z "$_octo" ] || sort -u <"$imrg"
779 rm -f "$inew" "$imrg"
780 newc="$(git commit-tree -p "$orh" $_parents -m "$_msg" "$newt")" && [ -n "$newc" ] || return 1
781 _mmsg="Merge made by the 'trivial aggressive$_auto${_octo:+ octopus}' strategy."
783 case "$_var" in
784 refs/?*)
785 if [ -n "$_same" ]; then
786 _same=
787 if rv="$(git rev-parse --quiet --verify "$_var" --)" && [ "$rv" = "$newc" ]; then
788 _same=1
791 if [ -z "$_same" ]; then
792 detach_symref_head_on_branch "$_var" || return 1
793 # git update-ref returns 0 even on failure :(
794 git update-ref -m "$_msg" "$_var" "$newc" || return 1
798 eval "$_var="'"$newc"'
800 esac
801 echo "$_mmsg"
802 [ -n "$_nodt" ] || git --no-pager diff-tree --shortstat "$orh" "$newc" -- $_exclusions
803 return 0
806 # shortcut that passes $3 as a preceding argument (which must match refs/?*)
807 attempt_index_merge() {
808 _noauto=
809 _mmode=
810 if [ "$1" = "--no-auto" ]; then
811 _noauto="$1"
812 shift
814 if [ "$1" = "--merge" ] || [ "$1" = "--theirs" ] || [ "$1" = "--remove" ]; then
815 _mmode="$1"
816 shift
818 case "$3" in refs/?*);;*)
819 die "programmer error: invalid arguments to attempt_index_merge: $*"
820 esac
821 v_attempt_index_merge $_noauto $_mmode "$3" "$@"
824 on_base=
825 do_base_switch() {
826 [ -n "$1" ] || return 0
828 [ "$1" != "$on_base" ] ||
829 [ "$(git symbolic-ref -q HEAD)" != "refs/$topbases/$1" ]
830 then
831 switch_to_base "$1"
832 on_base="$1"
836 update_branch_internal() {
837 # We are cacheable until the first change
838 become_cacheable
840 _update_name="$1"
841 ## First, take care of our base
843 _depcheck="$(get_temp tg-depcheck)"
844 missing_deps=
845 needs_update "$_update_name" >"$_depcheck" || :
846 if [ -n "$missing_deps" ]; then
847 msg="Some dependencies are missing: $missing_deps"
848 if [ -n "$skipms" ]; then
849 info "$msg; skipping"
850 elif [ -z "$all" ]; then
851 die "$msg"
852 else
853 info "$msg; skipping branch $_update_name"
854 return 0
857 # allow automatic simple merges by default until a failure occurs
858 no_auto=
859 if [ -s "$_depcheck" ]; then
860 <"$_depcheck" \
861 sed 's/ [^ ]* *$//' | # last is $_update_name
862 sed 's/.* \([^ ]*\)$/+\1/' | # only immediate dependencies
863 sed 's/^\([^+]\)/-\1/' | # now each line is +branch or -branch (+ == recurse)
864 >"$_depcheck.ideps" \
865 uniq -s 1 # fold branch lines; + always comes before - and thus wins within uniq
867 stash_now_if_requested
869 while read -r depline; do
870 dep="${depline#?}"
871 action="${depline%$dep}"
873 # We do not distinguish between dependencies out-of-date
874 # and base/remote out-of-date cases for $dep here,
875 # but thanks to needs_update returning : or refs/remotes/<remote>/<name>
876 # for the latter, we do correctly recurse here
877 # in both cases.
879 if [ x"$action" = x+ ]; then
880 case " $missing_deps " in *" $dep "*)
881 info "Skipping recursing to missing dependency: $dep"
882 continue
883 esac
884 info "Recursing to $dep..."
885 recursive_update "$dep" || exit 3
887 done <"$_depcheck.ideps"
889 # Create a list of all the fully qualified ref names that need
890 # to be merged into $_update_name's base. This will be done
891 # as an octopus merge if there are no conflicts.
892 deplist=
893 deplines=
894 set --
895 while read -r dep; do
896 dep="${dep#?}"
897 case "$dep" in
898 "refs"/*)
899 set -- "$@" "$dep"
900 case "$dep" in
901 "refs/heads"/*)
902 d="${dep#refs/heads/}"
903 deplist="${deplist:+$deplist }$d"
904 deplines="$deplines$d$lf"
907 d="${dep#refs/}"
908 deplist="${deplist:+$deplist }$d"
909 deplines="$deplines$d$lf"
911 esac
914 set -- "$@" "refs/heads/$dep"
915 deplist="${deplist:+$deplist }$dep"
916 deplines="$deplines$dep$lf"
918 esac
919 done <"$_depcheck.ideps"
921 # Make sure we end up on the correct base branch
922 on_base=
923 if [ $# -ge 2 ]; then
924 info "Updating $_update_name base with deps: $deplist"
925 become_non_cacheable
926 msg="tgupdate: octopus merge $# deps into $topbases/$_update_name$lf$lf$deplines"
927 if attempt_index_merge --remove -m "$msg" "refs/$topbases/$_update_name" "$@"; then
928 set --
929 else
930 info "Octopus merge failed; falling back to multiple 3-way merges"
931 no_auto="--no-auto"
935 for fulldep in "$@"; do
936 # This will be either a proper topic branch
937 # or a remote base. (branch_needs_update() is called
938 # only on the _dependencies_, not our branch itself!)
940 case "$fulldep" in
941 "refs/heads"/*)
942 dep="${fulldep#refs/heads/}";;
943 "refs"/*)
944 dep="${fulldep#refs/}";;
946 dep="$fulldep";; # this should be a programmer error
947 esac
949 info "Updating $_update_name base with $dep changes..."
950 become_non_cacheable
951 msg="tgupdate: merge $dep into $topbases/$_update_name"
953 ! attempt_index_merge $no_auto --remove -m "$msg" "refs/$topbases/$_update_name" "$fulldep^0" &&
955 # We need to switch to the base branch
956 # ...but only if we aren't there yet (from failed previous merge)
957 do_base_switch "$_update_name" || die "do_base_switch failed" &&
958 git_merge --remove --name "$_update_name base" --name "$dep" -m "$msg" "$fulldep^0"
960 then
961 rm "$_depcheck"
962 save_state
963 unset TG_RECURSIVE
964 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
965 info "(use \`$tgdisplayac status\` to see more options)"
966 exit 3
968 done
969 else
970 info "The base is up-to-date."
974 ## Second, update our head with the remote branch
976 plusextra=
977 merge_with="refs/$topbases/$_update_name"
978 brmmode=
979 if has_remote "$_update_name"; then
980 _rname="refs/remotes/$base_remote/$_update_name"
981 if branch_contains "refs/heads/$_update_name" "$_rname"; then
982 info "The $_update_name head is up-to-date wrt. its remote branch."
983 else
984 stash_now_if_requested
985 info "Reconciling $_update_name base with remote branch updates..."
986 become_non_cacheable
987 msg="tgupdate: merge ${_rname#refs/} onto $topbases/$_update_name"
988 checkours=
989 checktheirs=
990 got_merge_with=
991 brmmode="--merge"
992 if [ -n "$mergeresult" ]; then
993 checkours="$(git rev-parse --verify --quiet "refs/$topbases/$_update_name^0" --)" || :
994 checktheirs="$(git rev-parse --verify --quiet "$_rname^0" --)" || :
995 if [ "$mergeours" = "$checkours" ] && [ "$mergetheirs" = "$checktheirs" ]; then
996 got_merge_with="$mergeresult"
1000 [ -z "$got_merge_with" ] &&
1001 ! v_attempt_index_merge $no_auto --theirs "merge_with" -m "$msg" "refs/$topbases/$_update_name" "$_rname^0" &&
1003 # *DETACH* our HEAD now!
1004 no_auto="--no-auto"
1005 git checkout -q --detach $iowopt "refs/$topbases/$_update_name" || die "git checkout failed" &&
1006 git_merge --theirs --name "$_update_name base content" --name "${_rname#refs/}" -m "$msg" "$_rname^0" &&
1007 merge_with="$(git rev-parse --verify HEAD --)"
1009 then
1010 save_state \
1011 "$(git rev-parse --verify --quiet "refs/$topbases/$_update_name^0" --)" \
1012 "$(git rev-parse --verify --quiet "$_rname^0" --)"
1013 unset TG_RECURSIVE
1014 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
1015 info "(use \`$tgdisplayac status\` to see more options)"
1016 exit 3
1018 # Go back but remember we want to merge with this, not base
1019 [ -z "$got_merge_with" ] || merge_with="$got_merge_with"
1020 plusextra="${_rname#refs/} + "
1025 ## Third, update our head with the base
1027 if branch_contains "refs/heads/$_update_name" "$merge_with"; then
1028 info "The $_update_name head is up-to-date wrt. the base."
1029 return 0
1031 stash_now_if_requested
1032 info "Updating $_update_name against ${plusextra}new base..."
1033 become_non_cacheable
1034 msg="tgupdate: merge ${plusextra}$topbases/$_update_name into $_update_name"
1035 b4deps=
1036 if [ -n "$brmmode" ] && [ "$base_remote" ]; then
1037 b4deps="$(git rev-parse --verify --quiet "refs/heads/$_update_name:.topdeps" --)" && [ -n "$b4deps" ] ||
1038 b4deps="$(git hash-object -t blob -w --stdin </dev/null)"
1041 ! attempt_index_merge $no_auto $brmmode -m "$msg" "refs/heads/$_update_name" "$merge_with^0" &&
1043 # Home, sweet home...
1044 # (We want to always switch back, in case we were
1045 # on the base from failed previous merge.)
1046 git checkout -q $iowopt "$_update_name" || die "git checkout failed" &&
1047 git_merge $brmmode --name "$_update_name" --name "${plusextra}$topbases/$_update_name" -m "$msg" "$merge_with^0"
1049 then
1050 no_auto=
1051 merging_topfiles="${brmmode:+1}"
1052 save_state
1053 unset TG_RECURSIVE
1054 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
1055 info "(use \`$tgdisplayac status\` to see more options)"
1056 exit 3
1059 # Fourth, auto create locally any newly depended on branches we got from the remote
1061 _result=0
1062 if [ -n "$b4deps" ] &&
1063 l8rdeps="$(git rev-parse --verify --quiet "refs/heads/$_update_name:.topdeps" --)" &&
1064 [ -n "$l8rdeps" ] && [ "$b4deps" != "$l8rdeps" ]
1065 then
1066 _olddeps=
1067 while read -r newdep; do
1068 if [ -n "$newdep" ]; then
1069 if auto_create_local_remote "$newdep"; then
1070 _result=75
1071 else
1072 if ref_exists "refs/heads/$newdep"; then
1073 # maybe the line just moved around
1074 [ -n "$_olddeps" ] && [ -f "$_olddeps" ] || {
1075 _olddeps="$(get_temp b4deps)" &&
1076 git cat-file blob "$b4deps" >"$_olddeps"
1078 if awk -v "newdep=$newdep" '$0 == newdep {exit 1}' <"$_olddeps"; then
1079 # nope, it's a new head already existing locally
1080 _result=75
1082 else
1083 # helpfully check to see if there's such a remote branch
1084 _rntgb=
1085 ! ref_exists "refs/remotes/$base_remote/$newdep" || _rntgb=1
1086 # maybe a locking local orphan base too
1087 _blocked=
1088 if [ -n "$_rntgb" ] &&
1089 ref_exists "refs/remotes/$base_remote/${topbases#heads/}$newdep" &&
1090 ref_exists "refs/$topbases/$newdep"
1091 then
1092 _blocked=1
1094 # spew the flexibly adjustable warning
1095 warn "-------------------------------------"
1096 warn "MISSING DEPENDENCY MERGED FROM REMOTE"
1097 warn "-------------------------------------"
1098 warn "Local Branch: $_update_name"
1099 warn " Remote Name: $base_remote"
1100 warn " Dependency: $newdep"
1101 if [ -n "$_blocked" ]; then
1102 warn "Blocking Ref: refs/$topbases/$newdep"
1103 elif [ -n "$_rntgb" ]; then
1104 warn "Existing Ref: refs/remotes/$base_remote/$newdep"
1106 warn ""
1107 if [ -n "$_blocked" ]; then
1108 warn "There is no local branch by that name, but"
1109 warn "there IS a remote TopGit branch available by"
1110 warn "that name, but creation of a local version has"
1111 warn "been blocked by existence of the ref shown above."
1112 elif [ -n "$_rntgb" ]; then
1113 warn "There is no local branch or remote TopGit"
1114 warn "branch available by that name, but there is an"
1115 warn "existing non-TopGit remote branch ref shown above."
1116 warn "Non-TopGit branches are not set up automatically"
1117 warn "by TopGit and must be maintained manually."
1118 else
1119 warn "There is no local branch or remote branch"
1120 warn "(TopGit or otherwise) available by that name."
1122 warn "-------------------------------------"
1126 done <<-EOT
1127 $(git diff "$b4deps" "$l8rdeps" -- | diff_added_lines)
1130 return $_result
1133 update_branch() {
1134 _ubicode=0
1135 _maxdeploop=3
1136 update_branch_internal "$@" || _ubicode=$?
1137 while [ "$_maxdeploop" -gt 0 ] && [ "$_ubicode" = "75" ]; do
1138 _maxdeploop="$(( $maxdeploop - 1 ))"
1139 info "Updating $1 again with newly added dependencies..."
1140 _ubicode=0
1141 update_branch_internal "$@" || _ubicode=$?
1142 done
1143 return $_ubicode
1146 # We are "read-only" and cacheable until the first change
1147 tg_read_only=1
1148 v_create_ref_cache
1150 do_non_annihilated_branches_patterns() {
1151 while read -r _pat && [ -n "$_pat" ]; do
1152 set -- "$@" "$_pat"
1153 done
1154 non_annihilated_branches "$@"
1157 do_non_annihilated_branches() {
1158 if [ -z "$pattern" ]; then
1159 non_annihilated_branches
1160 else
1161 do_non_annihilated_branches_patterns <<-EOT
1162 $(sed 'y/ /\n/' <<-LIST
1163 $pattern
1164 LIST
1170 if [ -n "$all" ] && [ -z "$restored" ]; then
1171 names=
1172 while read name && [ -n "$name" ]; do
1173 case " $names " in *" $name "*);;*)
1174 names="${names:+$names }$name"
1175 esac
1176 done <<-EOT
1177 $(do_non_annihilated_branches)
1181 for name in $names; do
1182 case " $processed " in *" $name "*) continue; esac
1183 [ -z "$all" ] && case "$names" in *" "*) ! :; esac || info "Proccessing $name..."
1184 update_branch "$name" || exit
1185 processed="${processed:+$processed }$name"
1186 done
1188 [ -z "$all" ] && case "$names" in *" "*) ! :; esac ||
1189 info "Returning to ${current#refs/heads/}..."
1190 checkout_symref_full "$current"
1191 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
1192 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :