topgit: version 0.19.13
[topgit/pro.git] / tg-update.sh
blob64a9658e200caad81ceea2fac76c0f202fc0ee19
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,2018,2019,2021
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
24 setautoupdate=1 # temporarily set rerere.autoUpdate to true
26 if [ "$(git config --get --bool topgit.autostash 2>/dev/null)" != "false" ]; then
27 # topgit.autostash is true (or unset)
28 stash=1
31 ## Parse options
33 USAGE="\
34 Usage: ${tgname:-tg} [...] update [--[no-]stash] [--skip-missing] ([<name>...] | -a [<pattern>...])
35 Or: ${tgname:-tg} [...] update --base [-F <file> | -m <msg>] [--[no-]edit] [-f] <base-branch> <ref>
36 Or: ${tgname:-tg} [...] update --continue | --skip | --stop | --abort"
38 usage()
40 if [ "${1:-0}" != 0 ]; then
41 printf '%s\n' "$USAGE" >&2
42 else
43 printf '%s\n' "$USAGE"
45 exit ${1:-0}
48 # Remove any currently untracked files that also appear in
49 # $1's tree AND have an identical blob hash. Never fail, but
50 # instead simply ignore any operations with problems.
51 clear_matching_untracked() {
52 _cmutree="$(git rev-parse --verify "$1^{tree}" 2>/dev/null)" &&
53 [ -n "$_cmutree" ] || return 0
54 _idxtree="$(git write-tree 2>/dev/null)" &&
55 [ -n "$_idxtree" ] || return 0
56 v_get_show_toplevel _tplvl
57 # Save the list of untracked files in a temp file, then feed the
58 # "A" file lines from diff-tree index to the tree to an awk script
59 # along with the list of untracked files and let it spit out
60 # a list of blob matches which includes the file mode of the match
61 # and then remove any untracked files with a matching hash and mode.
62 # Due to limitations of awk, files with '\n' in their names are skipped.
63 _utfl="$(get_temp untracked)" || return 0
64 _hvrl=1
65 command -v readlink >/dev/null 2>&1 || _hvrl=
66 git status --porcelain -z | tr '\n\000' '\177\n' | awk '!/^\?\? ./||/\177/{next}{print}' >"$_utfl" || :
67 saveIFS="$IFS"
68 IFS=" "
69 git diff-tree --raw --ignore-submodules=all --no-renames -r -z --diff-filter=A "$_idxtree" "$_cmutree" |
70 tr '\n\000' '\177\n' | paste - - | awk -v u="$_utfl" '
71 BEGIN {x=""}
72 function exitnow(c) {x=c;exit x}
73 END {if(x!="")exit x}
74 function init(e,l) {
75 while ((e=(getline l<u))>0) {
76 if(l!~/^\?\? ./||l~/\177/)continue
77 f[substr(l,4)]=1
79 close(u)
81 BEGIN {if(u=="")exitnow(2);init()}
82 NF<5||/\177/{next}
84 if($1!=":000000"||($2!="100644"&&$2!="100755"&&$2!="120000")||
85 $3!~/^0+$/||$4!~/^[0-9a-f][0-9a-f][0-9a-f][0-9a-f]+$/||
86 $5!="A")next
87 t=$0;sub(/^[^\t]*\t/,"",t)
88 if(t!=""&&f[t])print $4" "$2" "t
90 ' |
91 while read -r _uthsh _utmod _utnam && [ -n "$_utnam" ] && [ -n "$_uthsh" ]; do
92 case "$_utmod" in "100644"|"100755"|"120000");;*) continue; esac
94 [ -L "$_tplvl/$_utnam" ] ||
95 [ -f "$_tplvl/$_utnam" ]
96 then
97 case "$_utmod" in
98 "100644") test ! -L "$_tplvl/$_utnam" &&
99 test ! -x "$_tplvl/$_utnam" || continue;;
100 "100755") test ! -L "$_tplvl/$_utnam" &&
101 test -x "$_tplvl/$_utnam" || continue;;
102 "120000") test -n "$_hvrl" &&
103 test -L "$_tplvl/$_utnam" || continue;;
104 *) ! :;;
105 esac &&
106 case "$_utmod" in
107 "100644"|"100755") _flhsh="$(git hash-object -t blob -- "$_tplvl/$_utnam")";;
108 "120000") _flhsh="$(readlink -n "$_tplvl/$_utnam" 2>/dev/null |
109 git hash-object -t blob --stdin)";;
110 *) ! :;;
111 esac &&
112 [ "$_flhsh" = "$_uthsh" ] || continue
113 rm -f "$_tplvl/$_utnam" >/dev/null 2>&1 || :
115 done || :
116 IFS="$saveIFS"
117 return 0
120 # --base mode comes here with $1 set to <base-branch> and $2 set to <ref>
121 # and all options already parsed and validated into above-listed flags
122 # this function should exit after returning to "$current"
123 do_base_mode()
125 v_verify_topgit_branch tgbranch "$1"
126 depcnt="$(git cat-file blob "refs/heads/$tgbranch:.topdeps" 2>/dev/null | awk 'END {print NR}')"
127 if [ $depcnt -gt 0 ]; then
128 grammar="dependency"
129 [ $depcnt -eq 1 ] || grammar="dependencies"
130 die "'$tgbranch' is not a TopGit [BASE] branch (it has $depcnt $grammar)"
132 newrev="$(git rev-parse --verify "$2^0" --)" && [ -n "$newrev" ] ||
133 die "not a valid commit-ish: $2"
134 v_ref_exists_rev baserev "refs/$topbases/$tgbranch" && [ -n "$baserev" ] ||
135 die "unable to get current base commit for branch '$tgbranch'"
136 if [ "$baserev" = "$newrev" ]; then
137 [ -n "$quiet" ] || echo "No change"
138 exit 0
140 if [ -z "$basefrc" ] && ! contained_by "$baserev" "$newrev"; then
141 die "Refusing non-fast-forward update of base without --force"
144 # make sure the new base commit does not have any .topdeps or .topmsg files
146 newrevt="$(git rev-parse --verify "$newrev^{tree}" --)" && [ -n "$newrevt" ] || die "$2 disappeared"
147 v_pretty_tree newptree "$newrev" -r || die "v_pretty_tree ... $2 -r (via git mktree) failed"
148 if [ "$newrevt" != "$newptree" ]; then
149 ensure_ident_available
150 bmsg="tg create $tgbranch base"
151 newrev="$(git commit-tree -p "$newrev" -m "$bmsg" "$newptree")" || die "git commit-tree failed"
154 # check that we can checkout the branch
156 alreadymerged=
157 ! contained_by "$newrev" "refs/heads/$tgbranch" || alreadymerged=1
158 [ -n "$alreadymerged" ] || git read-tree -n -u -m "refs/heads/$tgbranch" ||
159 die "git checkout \"$branch\" would fail"
161 # and make sure everything's clean and we know who we are
163 [ -n "$alreadymerged" ] || ensure_clean_tree
164 ensure_ident_available
166 # collect the message from the user unless "$alreadymerged" or --no-edit
168 if [ -z "$alreadymerged" ]; then
169 ncopt=
170 [ -z "$basenc" ] || ncopt="--no-commit"
171 msgopt=
172 # options validation guarantees that at most one of basemsg or basefile is set
173 [ -z "$basemsg" ] || msgopt='-m "$basemsg"'
174 if [ -n "$basefile" ]; then
175 # git merge does not accept a -F <msgfile> option so we have to fake it
176 basefilemsg="$(cat "$basefile")" || die "could not read file '$basefile'"
177 msgopt='-m "$basefilemsg"'
179 editopt=
180 if [ -n "$editmode" ]; then
181 if [ "$editmode" = "0" ]; then
182 editopt="--no-edit"
183 else
184 editopt="--edit"
187 if [ -z "$basemsg$basefile" ]; then
188 [ -n "$editopt" ] || editopt="--edit"
189 basemsg="tg update --base $tgbranch $2"
190 msgopt='-m "$basemsg"'
191 else
192 [ -n "$editopt" ] || editopt="--no-edit"
194 if [ "$editopt" = "--edit" ] || [ "$ncopt" = "--no-commit" ]; then
195 if [ -n "$basefile" ]; then
196 eval 'printf "%s\n"' "${msgopt#-m }" >"$git_dir/MERGE_MSG"
197 else
198 eval 'printf "%s\n"' "${msgopt#-m }" | git stripspace >"$git_dir/MERGE_MSG"
200 if [ "$ncopt" != "--no-commit" ]; then
201 cat <<-\EOT >>"$git_dir/MERGE_MSG"
203 # Please enter a commit message to explain why this merge is necessary,
204 # especially if it merges an updated upstream into a topic branch.
206 # Lines starting with '#' will be ignored, and an empty message aborts
207 # the commit.
209 run_editor "$git_dir/MERGE_MSG" ||
210 die "there was a problem with the editor '$tg_editor'"
211 git -c core.commentchar='#' stripspace -s <"$git_dir/MERGE_MSG" >"$git_dir/MERGE_MSG"+
212 mv -f "$git_dir/MERGE_MSG"+ "$git_dir/MERGE_MSG"
213 if [ ! -s "$git_dir/MERGE_MSG" ]; then
214 # Restore the (unannotated) MERGE_MSG even though we are aborting
215 if [ -n "$basefile" ]; then
216 eval 'printf "%s\n"' "${msgopt#-m }" >"$git_dir/MERGE_MSG"
217 else
218 eval 'printf "%s\n"' "${msgopt#-m }" |
219 git stripspace >"$git_dir/MERGE_MSG"
221 die "Aborting update due to empty commit message."
223 mergemsg="$(cat "$git_dir/MERGE_MSG")" || die "could not read file '$git_dir/MERGE_MSG'"
224 msgopt='-m "$mergemsg"'
229 # always auto stash even if it's just to the anonymous stash TG_STASH
231 stashmsg="tgupdate: autostash before --base $tgbranch update"
232 if [ -n "$stash" ]; then
233 tg tag -q -q -m "$stashmsg" --stash "$tgbranch" &&
234 stashhash="$(git rev-parse --quiet --verify refs/tgstash --)" &&
235 [ -n "$stashhash" ] &&
236 [ "$(git cat-file -t "$stashhash" 2>/dev/null)" = "tag" ] ||
237 die "requested --stash failed"
238 else
239 tg tag --anonymous "$tgbranch" &&
240 stashhash="$(git rev-parse --quiet --verify TG_STASH --)" &&
241 [ -n "$stashhash" ] &&
242 [ "$(git cat-file -t "$stashhash" 2>/dev/null)" = "tag" ] ||
243 die "anonymous --stash failed"
246 # proceed with the update
248 git update-ref -m "tg update --base $tgbranch $2" "refs/$topbases/$tgbranch" "$newrev" "$baserev" ||
249 die "Unable to update base ref"
250 if [ -n "$alreadymerged" ]; then
251 [ -n "$quiet" ] || echo "Already contained in branch (base updated)"
252 exit 0
254 git checkout -q $iowopt "$tgbranch" || die "git checkout failed"
255 eval git_topmerge $ncopt "$msgopt" "refs/$topbases/$tgbranch" || exit
256 ec=0
257 if [ -z "$basenc" ]; then
258 clear_matching_untracked "$current"
259 checkout_symref_full "$current" || ec=$?
260 tmpdir_cleanup || :
261 if [ "${ec:-0}" != "0" ]; then
262 info "Unable to switch to ${current#refs/heads/}"
264 git rev-parse -q --verify HEAD >/dev/null 2>&1 &&
265 ! git symbolic-ref -q HEAD >/dev/null 2>&1
266 then
267 info "HEAD is currently detached"
268 info "Use 'git checkout ${current#refs/heads/}' to reattach"
272 exit ${ec:-0}
275 state_dir="$git_dir/tg-state"
276 mergeours=
277 mergetheirs=
278 mergeresult=
279 stashhash=
280 next_no_auto=
281 merging_topfiles=
283 is_active() {
284 [ -d "$state_dir" ] || return 1
285 [ -s "$state_dir/state" ] || return 1
286 read -r _astate <"$state_dir/state" &&
287 [ "$_astate" = "update" ] || return 1
288 [ -s "$state_dir/fullcmd" ] || return 1
289 [ -f "$state_dir/remote" ] || return 1
290 [ -f "$state_dir/skipms" ] || return 1
291 [ -f "$state_dir/all" ] || return 1
292 [ -s "$state_dir/current" ] || return 1
293 [ -s "$state_dir/stashhash" ] || return 1
294 [ -s "$state_dir/name" ] || return 1
295 [ -s "$state_dir/names" ] || return 1
296 [ -f "$state_dir/processed" ] || return 1
297 [ -f "$state_dir/no_auto" ] || return 1
298 [ -f "$state_dir/setautoupdate" ] || return 1
299 [ -f "$state_dir/merging_topfiles" ] || return 1
300 [ -f "$state_dir/mergeours" ] || return 1
301 [ -f "$state_dir/mergeours" ] || return 1
302 if [ -s "$state_dir/mergeours" ]; then
303 [ -s "$state_dir/mergetheirs" ] || return 1
304 else
305 ! [ -s "$state_dir/mergetheirs" ] || return 1
309 restore_state() {
310 is_active || die "programmer error"
311 IFS= read -r fullcmd <"$state_dir/fullcmd" && [ -n "$fullcmd" ]
312 IFS= read -r base_remote <"$state_dir/remote" || :
313 IFS= read -r skipms <"$state_dir/skipms" || :
314 IFS= read -r all <"$state_dir/all" || :
315 IFS= read -r current <"$state_dir/current" && [ -n "$current" ]
316 IFS= read -r stashhash <"$state_dir/stashhash" && [ -n "$stashhash" ]
317 IFS= read -r name <"$state_dir/name" && [ -n "$name" ]
318 IFS= read -r names <"$state_dir/names" && [ -n "$names" ]
319 IFS= read -r processed <"$state_dir/processed" || :
320 IFS= read -r next_no_auto <"$state_dir/no_auto" || :
321 IFS= read -r setautoupdate <"$state_dir/setautoupdate" || :
322 # merging_topfiles is for outside info but not to be restored
323 IFS= read -r mergeours <"$state_dir/mergeours" || :
324 IFS= read -r mergetheirs <"$state_dir/mergetheirs" || :
325 if [ -n "$mergeours" ] && [ -n "$mergetheirs" ]; then
326 headhash="$(git rev-parse --quiet --verify HEAD --)" || :
327 if [ -n "$headhash" ]; then
328 parents="$(git --no-pager log -n 1 --format='format:%P' "$headhash" -- 2>/dev/null)" || :
329 if [ "$parents" = "$mergeours $mergetheirs" ]; then
330 mergeresult="$headhash"
333 if [ -z "$mergeresult" ]; then
334 mergeours=
335 mergetheirs=
338 restored=1
341 clear_state() {
342 ! [ -e "$state_dir" ] || rm -rf "$state_dir" >/dev/null 2>&1 || :
345 restarted=
346 isactive=
347 ! is_active || isactive=1
348 isactiveopt=
349 if [ -z "$isactive" ] && [ $# -eq 1 ]; then
350 case "$1" in --abort|--stop|--continue|--skip) isactiveopt=1; esac
352 if [ -n "$isactive" ] || [ -n "$isactiveopt" ]; then
353 [ $# -eq 1 ] && [ x"$1" != x"--status" ] || { do_status; exit 0; }
354 ensure_work_tree
355 if [ -z "$isactive" ]; then
356 clear_state
357 info "No update is currently active"
358 exit 0
360 case "$1" in
361 --abort)
362 current=
363 stashhash=
364 IFS= read -r current <"$state_dir/current" || :
365 IFS= read -r stashhash <"$state_dir/stashhash" || :
366 clear_state
367 if [ -n "$stashhash" ]; then
368 tg revert -f -q -q --no-stash "$stashhash" >/dev/null 2>&1 || :
370 if [ -n "$current" ]; then
371 info "Ok, update aborted, returning to ${current#refs/heads/}"
372 checkout_symref_full -f "$current"
373 else
374 info "Ok, update aborted. Now, you just need to"
375 info "switch back to some sane branch using \`git$gitcdopt checkout\`."
377 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
378 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
379 exit 0
381 --stop)
382 clear_state
383 info "Ok, update stopped. Now, you just need to"
384 info "switch back to some sane branch using \`git$gitcdopt checkout\`."
385 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
386 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
387 exit 0
389 --continue|--skip)
390 restore_state
391 if [ "$1" = "--skip" ]; then
392 info "Ok, I will try to continue without updating this branch."
393 git reset --hard -q
394 case " $processed " in *" $name "*);;*)
395 processed="${processed:+$processed }$name"
396 esac
398 # assume user fixed it
399 # we could be left on a detached HEAD if we were resolving
400 # a conflict while merging a base in, fix it with a checkout
401 v_strip_ref bname "$name"
402 git checkout -q $iowopt "$bname"
405 do_status
406 exit 1
407 esac
409 clear_state
411 if [ -z "$restored" ]; then
412 setautoupdate=1
413 [ "$(git config --get --bool topgit.setAutoUpdate 2>/dev/null)" != "false" ] ||
414 setautoupdate=
416 while [ -n "$1" ]; do
417 arg="$1"; shift
418 case "$arg" in
420 usage;;
421 -a|--all)
422 [ -z "$names$pattern" ] || usage 1
423 all=1;;
424 --skip-missing)
425 skipms=1;;
426 --stash)
427 stash=1;;
428 --no-stash)
429 stash=;;
430 --auto|--auto-update|--set-auto|--set-auto-update)
431 setautoupdate=1;;
432 --no-auto|--no-auto-update|--no-set-auto|--no-set-auto-update)
433 setautoupdate=;;
434 --quiet|-q)
435 quiet=1;;
436 --base)
437 basemode=1;;
438 --edit|-e)
439 editmode=1;;
440 --no-edit)
441 editmode=0;;
442 --no-commit)
443 basenc=1;;
444 --force|-f)
445 basefrc=1;;
446 -m|--message)
447 [ $# -gt 0 ] && [ -n "$1" ] || die "option $arg requires an argument"
448 basemsg="$1"
449 shift;;
450 -m?*)
451 basemsg="${1#-m}";;
452 --message=*)
453 basemsg="${1#--message=}";;
454 -F|--file)
455 [ $# -gt 0 ] && [ -n "$1" ] || die "option $arg requires an argument"
456 basefile="$1"
457 shift;;
458 -F?*)
459 basefile="${1#-F}";;
460 --file=*)
461 basefile="${1#--file=}"
462 [ -n "$basefile" ] || die "option --file= requires an argument"
464 -?*)
465 usage 1;;
467 break;;
471 if [ -z "$all" ]; then
472 namecnt=$(( $namecnt + 1 ))
473 [ "$namecnt" != "1" ] || name1="$arg"
474 [ "$namecnt" != "2" ] || name2="$arg"
475 names="${names:+$names }$arg"
476 else
477 v_strip_ref arg "$arg"
478 pattern="${pattern:+$pattern }refs/$topbases/$arg"
481 esac
482 done
483 ensure_work_tree
484 while [ $# -gt 0 ]; do
485 if [ -z "$all" ]; then
486 namecnt=$(( $namecnt + 1 ))
487 [ "$namecnt" != "1" ] || name1="$1"
488 [ "$namecnt" != "2" ] || name2="$1"
489 names="${names:+$names }$*"
490 else
491 v_strip_ref arg "$1"
492 pattern="${pattern:+$pattern }refs/$topbases/$arg"
494 shift
495 done
496 [ -n "$basemode" ] || [ -z "$editmode$basemsg$basefile$basenc$basefrc" ] || usage 1
497 [ -z "$basemode" ] || [ -z "$all$skipms" ] || usage 1
498 [ -z "$basemode" ] || [ -z "$basemsg" ] || [ -z "$basefile" ] || usage 1
499 [ -z "$basemode" ] || [ "$namecnt" -eq 2 ] || usage 1
501 current="$(git symbolic-ref -q HEAD)" || :
502 if [ -n "$current" ]; then
503 [ -n "$(git rev-parse --verify --quiet HEAD --)" ] ||
504 die "cannot return to unborn branch; switch to another branch"
505 else
506 current="$(git rev-parse --verify --quiet HEAD)" ||
507 die "cannot return to invalid HEAD; switch to another branch"
510 [ -z "$basemode" ] || do_base_mode "$name1" "$name2"
512 origpattern="$pattern"
513 [ -z "$pattern" ] && pattern="refs/$topbases"
515 processed=
516 [ -n "$all$names" ] || names="HEAD"
517 if [ -z "$all" ]; then
518 clean_names() {
519 names=
520 while [ $# -gt 0 ]; do
521 v_verify_topgit_branch name "$1"
522 case " $names " in *" $name "*);;*)
523 names="${names:+$names }$name"
524 esac
525 shift
526 done
528 clean_names $names
529 if [ "$namecnt" -eq 1 ]; then
530 case "$fullcmd" in *" @"|*" HEAD")
531 namecnt=0
532 fullcmd="${fullcmd% *}"
533 esac
535 [ "$namecnt" -ne 0 ] || fullcmd="$fullcmd $names"
537 ensure_clean_tree
540 save_state() {
541 mkdir -p "$state_dir"
542 >"$state_dir/state"
543 printf '%s\n' "$fullcmd" >"$state_dir/fullcmd"
544 printf '%s\n' "$base_remote" >"$state_dir/remote"
545 printf '%s\n' "$skipms" >"$state_dir/skipms"
546 printf '%s\n' "$all" >"$state_dir/all"
547 printf '%s\n' "$current" >"$state_dir/current"
548 printf '%s\n' "$stashhash" >"$state_dir/stashhash"
549 printf '%s\n' "$name" >"$state_dir/name"
550 printf '%s\n' "$names" >"$state_dir/names"
551 printf '%s\n' "$processed" >"$state_dir/processed"
552 printf '%s\n' "$no_auto" >"$state_dir/no_auto"
553 printf '%s\n' "$setautoupdate" >"$state_dir/setautoupdate"
554 # this one is an external flag and needs to be zero length for false
555 printf '%s' "$merging_topfiles" >"$state_dir/merging_topfiles"
556 printf '%s\n' "$1" >"$state_dir/mergeours"
557 printf '%s\n' "$2" >"$state_dir/mergetheirs"
558 # state not valid until $state_dir/state contains "update"
559 echo update >"$state_dir/state"
562 stash_now_if_requested() {
563 [ -z "$TG_RECURSIVE" ] || return 0
564 [ -z "$stashhash" ] || return 0
565 ensure_ident_available
566 msg="tgupdate: autostash before update"
567 if [ -n "$all" ]; then
568 msg="$msg --all${origpattern:+ $origpattern}"
569 else
570 msg="$msg $names"
572 set -- $names
573 if [ -n "$stash" ]; then
574 tg tag -q -q -m "$msg" --stash "$@" &&
575 stashhash="$(git rev-parse --quiet --verify refs/tgstash --)" &&
576 [ -n "$stashhash" ] &&
577 [ "$(git cat-file -t "$stashhash" 2>/dev/null)" = "tag" ] ||
578 die "requested --stash failed"
579 else
580 tg tag --anonymous "$@" &&
581 stashhash="$(git rev-parse --quiet --verify TG_STASH --)" &&
582 [ -n "$stashhash" ] &&
583 [ "$(git cat-file -t "$stashhash" 2>/dev/null)" = "tag" ] ||
584 die "anonymous --stash failed"
586 [ -z "$next_no_auto" ] || no_auto="$next_no_auto"
587 next_no_auto=
590 recursive_update() {
591 _ret=0
592 on_base=
594 if [ -n "$TG_RECURSIVE" ]; then
595 TG_RECURSIVE="==> [$1]${TG_RECURSIVE#==>}"
596 else
597 TG_RECURSIVE="==> [$1]$lf"
599 update_branch "$1"
600 ) || _ret=$?
601 [ $_ret -eq 3 ] && exit 3
602 return $_ret
605 on_base=
606 do_base_switch() {
607 [ -n "$1" ] || return 0
609 [ "$1" != "$on_base" ] ||
610 [ "$(git symbolic-ref -q HEAD)" != "refs/$topbases/$1" ]
611 then
612 switch_to_base "$1"
613 on_base="$1"
617 # $1 varname to hold space-separated+surrounded result(s) (can be empty)
618 # $2 is "our" full ref name
619 # $3 is "their" full ref name
620 # return status is 0 if any removed deps found, non-0 otherwise in which
621 # case $1 will always be set to the empty string
622 v_get_removed_deps() {
623 __goneec=1
624 [ -z "$1" ] || eval "$1="
626 v_attempt_index_merge_file _newdepsblob \
627 "refs/heads/$_update_name" "$_rname" .topdeps
628 then
629 rm -f "$tg_tmp_dir/tgblob.$$"
630 git cat-file blob "refs/heads/$_update_name:.topdeps" >"$tg_tmp_dir/tgblob.$$" 2>/dev/null || :
631 __gonedeps="$(git cat-file blob "$_newdepsblob" 2>/dev/null |
632 awk -v td="$tg_tmp_dir/tgblob.$$" '
633 BEGIN {x=""}
634 function exitnow(c) {x=c;exit x}
635 END {if(x!="")exit x}
636 function init(e,l,a) {
637 while ((e=(getline l<td))>0) {
638 if (split(l,a," ") == 1) t[a[1]] = 1
640 close(td)
642 BEGIN {if(td=="")exitnow(2);init()}
643 NF==1 {r[$1]=1}
644 END {
646 for (d in t) {
647 if (!(d in r)) {
648 if (!n) printf " "
649 n+=1
650 printf d " "
653 exit (n?0:1)
655 ')" && __goneec=0 || __goneec=$?
656 [ -z "$1" ] || [ "$__goneec" != "0" ] || eval "$1=\"\$__gonedeps\""
658 return $__goneec
661 update_branch_internal() {
662 # We are cacheable until the first change
663 become_cacheable
665 _update_name="$1"
666 _rname=
667 _bcrname=
668 _removedeps=
669 if has_remote "$_update_name"; then
670 _rname="refs/remotes/$base_remote/$_update_name"
671 if branch_contains "refs/heads/$_update_name" "$_rname"; then
672 _bcrname=1
673 else
674 # Check for removed dependencies
675 v_get_removed_deps _removedeps "refs/heads/$_update_name" "$_rname" || :
680 ## First, take care of our base
682 _depcheck="$(get_temp tg-depcheck)"
683 missing_deps=
684 needs_update "$_update_name" >"$_depcheck" || :
685 if [ -n "$missing_deps" ]; then
686 msg="Some dependencies are missing: $missing_deps"
687 if [ -n "$skipms" ]; then
688 info "$msg; skipping"
689 elif [ -z "$all" ]; then
690 die "$msg"
691 else
692 info "$msg; skipping branch $_update_name"
693 return 0
696 # allow automatic simple merges by default until a failure occurs
697 no_auto=
698 if [ -s "$_depcheck" ]; then
699 # (1) last word is $_update_name, remove it
700 # (2) keep only immediate dependencies of a chain adding a leading '+'
701 # (3) one-level deep dependencies get a '-' prefix instead
702 <"$_depcheck" sed \
703 -e 's/ [^ ]* *$//; # (1)' \
704 -e 's/.* \([^ ]*\)$/+\1/; # (2)' \
705 -e 's/^\([^+]\)/-\1/; # (3)' |
706 # now each line is +branch or -branch (+ == recurse)
707 >"$_depcheck.ideps" \
708 uniq -s 1 # fold branch lines; + always comes before - and thus wins within uniq
710 stash_now_if_requested
712 while read -r depline; do
713 dep="${depline#?}"
714 action="${depline%$dep}"
716 # We do not distinguish between dependencies out-of-date
717 # and base/remote out-of-date cases for $dep here,
718 # but thanks to needs_update returning : or :refs/remotes/...
719 # for the latter, we do correctly recurse here
720 # in both cases.
722 if [ x"$action" = x+ ]; then
723 case " $missing_deps " in *" $dep "*)
724 info "Skipping recursing to missing dependency: $dep"
725 continue
726 esac
727 case " $_removedeps " in *" $dep "*)
728 info "Skipping recursing to remote-removed dependency: $dep"
729 continue
730 esac
731 info "Recursing to $dep..."
732 recursive_update "$dep" || exit 3
734 done <"$_depcheck.ideps"
736 # Create a list of all the fully qualified ref names that need
737 # to be merged into $_update_name's base. This will be done
738 # as an octopus merge if there are no conflicts.
739 deplist=
740 deplines=
741 set --
742 while read -r dep; do
743 dep="${dep#?}"
744 case " $missing_deps " in *" $dep "*)
745 # message already shown above
746 continue
747 esac
748 case " $_removedeps " in *" $dep "*)
749 # message already shown above
750 continue
751 esac
752 case "$dep" in
754 d="${dep#?}"
755 set -- "$@" "$d"
756 case "$d" in
757 "refs/heads"/*)
758 d="${d#refs/heads/}"
759 deplist="${deplist:+$deplist }$d"
760 deplines="$deplines$d$lf"
763 d="${d#refs/}"
764 deplist="${deplist:+$deplist }$d"
765 deplines="$deplines$d$lf"
767 esac
770 set -- "$@" "refs/heads/$dep"
771 deplist="${deplist:+$deplist }$dep"
772 deplines="$deplines$dep$lf"
774 esac
775 done <"$_depcheck.ideps"
777 # Make sure we end up on the correct base branch
778 on_base=
779 if [ $# -ge 2 ]; then
780 info "Updating $_update_name base with deps: $deplist"
781 become_non_cacheable
782 msg="tgupdate: octopus merge $# deps into $_update_name base$lf$lf$deplines"
783 if attempt_index_merge --remove -m "$msg" "refs/$topbases/$_update_name" "$@"; then
784 set --
785 else
786 info "Octopus merge failed; falling back to multiple 3-way merges"
787 no_auto="--no-auto"
791 for fulldep in "$@"; do
792 # This will be either a proper topic branch
793 # or a remote base. (branch_needs_update() is called
794 # only on the _dependencies_, not our branch itself!)
796 case "$fulldep" in
797 "refs/heads"/*)
798 dep="${fulldep#refs/heads/}";;
799 "refs"/*)
800 dep="${fulldep#refs/}";;
802 dep="$fulldep";; # this should be a programmer error
803 esac
805 info "Updating $_update_name base with $dep changes..."
806 become_non_cacheable
807 msg="tgupdate: merge $dep into $_update_name base"
809 ! attempt_index_merge $no_auto --remove -m "$msg" "refs/$topbases/$_update_name" "$fulldep^0" &&
811 # We need to switch to the base branch
812 # ...but only if we aren't there yet (from failed previous merge)
813 do_base_switch "$_update_name" || die "do_base_switch failed" &&
814 git_merge --remove --name "$_update_name base" --name "$dep" -m "$msg" "$fulldep^0"
816 then
817 rm "$_depcheck"
818 save_state
819 unset TG_RECURSIVE
820 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
821 info "(use \`$tgdisplayac status\` to see more options)"
822 exit 3
824 done
825 else
826 info "The base is up-to-date."
830 ## Second, update our head with the remote branch
832 plusextra=
833 merge_with="refs/$topbases/$_update_name"
834 brmmode=
835 if [ -n "$_rname" ]; then
836 if [ -n "$_bcrname" ]; then
837 info "The $_update_name head is up-to-date wrt. its remote branch."
838 else
839 stash_now_if_requested
840 info "Reconciling $_update_name base with remote branch updates..."
841 become_non_cacheable
842 msg="tgupdate: merge ${_rname#refs/} onto $_update_name base"
843 checkours=
844 checktheirs=
845 got_merge_with=
846 brmmode="--merge"
847 if [ -n "$mergeresult" ]; then
848 checkours="$(git rev-parse --verify --quiet "refs/$topbases/$_update_name^0" --)" || :
849 checktheirs="$(git rev-parse --verify --quiet "$_rname^0" --)" || :
850 if [ "$mergeours" = "$checkours" ] && [ "$mergetheirs" = "$checktheirs" ]; then
851 got_merge_with="$mergeresult"
855 [ -z "$got_merge_with" ] &&
856 ! v_attempt_index_merge $no_auto --theirs "merge_with" -m "$msg" "refs/$topbases/$_update_name" "$_rname^0" &&
858 # *DETACH* our HEAD now!
859 no_auto="--no-auto"
860 git checkout -q --detach $iowopt "refs/$topbases/$_update_name" || die "git checkout failed" &&
861 git_merge --theirs --name "$_update_name base content" --name "${_rname#refs/}" -m "$msg" "$_rname^0" &&
862 merge_with="$(git rev-parse --verify HEAD --)"
864 then
865 save_state \
866 "$(git rev-parse --verify --quiet "refs/$topbases/$_update_name^0" --)" \
867 "$(git rev-parse --verify --quiet "$_rname^0" --)"
868 unset TG_RECURSIVE
869 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
870 info "(use \`$tgdisplayac status\` to see more options)"
871 exit 3
873 # Go back but remember we want to merge with this, not base
874 [ -z "$got_merge_with" ] || merge_with="$got_merge_with"
875 plusextra="${_rname#refs/} + "
880 ## Third, update our head with the base
882 if branch_contains "refs/heads/$_update_name" "$merge_with"; then
883 info "The $_update_name head is up-to-date wrt. the base."
884 return 0
886 stash_now_if_requested
887 info "Updating $_update_name against ${plusextra}new base..."
888 become_non_cacheable
889 msg="tgupdate: merge ${plusextra}$_update_name base into $_update_name"
890 b4deps=
891 if [ -n "$brmmode" ] && [ "$base_remote" ]; then
892 b4deps="$(git rev-parse --verify --quiet "refs/heads/$_update_name:.topdeps" --)" && [ -n "$b4deps" ] ||
893 b4deps="$(git hash-object -t blob -w --stdin </dev/null)"
896 ! attempt_index_merge $no_auto $brmmode -m "$msg" "refs/heads/$_update_name" "$merge_with^0" &&
898 # Home, sweet home...
899 # (We want to always switch back, in case we were
900 # on the base from failed previous merge.)
901 git checkout -q $iowopt "$_update_name" || die "git checkout failed" &&
902 git_merge $brmmode --name "$_update_name" --name "${plusextra}$topbases/$_update_name" -m "$msg" "$merge_with^0"
904 then
905 no_auto=
906 merging_topfiles="${brmmode:+1}"
907 save_state
908 unset TG_RECURSIVE
909 info "Please commit merge resolution and call \`$tgdisplayac update --continue\`"
910 info "(use \`$tgdisplayac status\` to see more options)"
911 exit 3
914 # Fourth, auto create locally any newly depended on branches we got from the remote
916 _result=0
917 if [ -n "$b4deps" ] &&
918 l8rdeps="$(git rev-parse --verify --quiet "refs/heads/$_update_name:.topdeps" --)" &&
919 [ -n "$l8rdeps" ] && [ "$b4deps" != "$l8rdeps" ]
920 then
921 _olddeps=
922 while read -r newdep; do
923 if [ -n "$newdep" ]; then
924 if auto_create_local_remote "$newdep"; then
925 _result=75
926 else
927 if ref_exists "refs/heads/$newdep"; then
928 # maybe the line just moved around
929 [ -n "$_olddeps" ] && [ -f "$_olddeps" ] || {
930 _olddeps="$(get_temp b4deps)" &&
931 git cat-file blob "$b4deps" >"$_olddeps"
933 if awk -v "newdep=$newdep" '$0 == newdep {exit 1}' <"$_olddeps"; then
934 # nope, it's a new head already existing locally
935 _result=75
937 else
938 # helpfully check to see if there's such a remote branch
939 _rntgb=
940 ! ref_exists "refs/remotes/$base_remote/$newdep" || _rntgb=1
941 # maybe a blocking local orphan base too
942 _blocked=
943 if [ -n "$_rntgb" ] &&
944 ref_exists "refs/remotes/$base_remote/${topbases#heads/}/$newdep" &&
945 ref_exists "refs/$topbases/$newdep"
946 then
947 _blocked=1
949 # spew the flexibly adjustable warning
950 warn "-------------------------------------"
951 warn "MISSING DEPENDENCY MERGED FROM REMOTE"
952 warn "-------------------------------------"
953 warn "Local Branch: $_update_name"
954 warn " Remote Name: $base_remote"
955 warn " Dependency: $newdep"
956 if [ -n "$_blocked" ]; then
957 warn "Blocking Ref: refs/$topbases/$newdep"
958 elif [ -n "$_rntgb" ]; then
959 warn "Existing Ref: refs/remotes/$base_remote/$newdep"
961 warn ""
962 if [ -n "$_blocked" ]; then
963 warn "There is no local branch by that name, but"
964 warn "there IS a remote TopGit branch available by"
965 warn "that name, but creation of a local version has"
966 warn "been blocked by existence of the ref shown above."
967 elif [ -n "$_rntgb" ]; then
968 warn "There is no local branch or remote TopGit"
969 warn "branch available by that name, but there is an"
970 warn "existing non-TopGit remote branch ref shown above."
971 warn "Non-TopGit branches are not set up automatically"
972 warn "by TopGit and must be maintained manually."
973 else
974 warn "There is no local branch or remote branch"
975 warn "(TopGit or otherwise) available by that name."
977 warn "-------------------------------------"
981 done <<-EOT
982 $(git diff --ignore-space-at-eol "$b4deps" "$l8rdeps" -- | diff_added_lines)
985 return $_result
988 update_branch() {
989 _ubicode=0
990 _maxdeploop=3
991 update_branch_internal "$@" || _ubicode=$?
992 while [ "$_maxdeploop" -gt 0 ] && [ "$_ubicode" = "75" ]; do
993 _maxdeploop="$(( $maxdeploop - 1 ))"
994 info "Updating $1 again with newly added dependencies..."
995 _ubicode=0
996 update_branch_internal "$@" || _ubicode=$?
997 done
998 return $_ubicode
1001 # We are "read-only" and cacheable until the first change
1002 tg_read_only=1
1003 v_create_ref_cache
1005 do_non_annihilated_branches_patterns() {
1006 while read -r _pat && [ -n "$_pat" ]; do
1007 set -- "$@" "$_pat"
1008 done
1009 non_annihilated_branches "$@"
1012 do_non_annihilated_branches() {
1013 if [ -z "$pattern" ]; then
1014 non_annihilated_branches
1015 else
1016 do_non_annihilated_branches_patterns <<-EOT
1017 $(sed 'y/ /\n/' <<-LIST
1018 $pattern
1019 LIST
1025 if [ -n "$all" ] && [ -z "$restored" ]; then
1026 names=
1027 while read name && [ -n "$name" ]; do
1028 case " $names " in *" $name "*);;*)
1029 names="${names:+$names }$name"
1030 esac
1031 done <<-EOT
1032 $(do_non_annihilated_branches)
1036 for name in $names; do
1037 case " $processed " in *" $name "*) continue; esac
1038 [ -z "$all" ] && case "$names" in *" "*) ! :; esac || info "Proccessing $name..."
1039 update_branch "$name" || exit
1040 processed="${processed:+$processed }$name"
1041 done
1043 [ -z "$all" ] && case "$names" in *" "*) ! :; esac ||
1044 info "Returning to ${current#refs/heads/}..."
1045 ec=0
1046 clear_matching_untracked "$current"
1047 checkout_symref_full "$current" || ec=$?
1048 ! [ -f "$git_dir/TGMERGE_MSG" ] || [ -e "$git_dir/MERGE_MSG" ] ||
1049 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" || :
1050 tmpdir_cleanup || :
1051 git gc --auto || :
1052 if [ "${ec:-0}" != "0" ]; then
1053 info "Unable to switch to ${current#refs/heads/}"
1055 git rev-parse -q --verify HEAD >/dev/null 2>&1 &&
1056 ! git symbolic-ref -q HEAD >/dev/null 2>&1
1057 then
1058 info "HEAD is currently detached"
1059 info "Use 'git checkout ${current#refs/heads/}' to reattach"
1062 exit ${ec:-0}