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
8 names
= # Branch(es) to update
9 all
= # Update all branches
10 pattern
= # Branch selection filter for -a
11 current
= # Branch we are currently on
12 skipms
= # skip missing dependencies
13 stash
= # tgstash refs before changes
15 if [ "$(git config --get --bool topgit.autostash 2>/dev/null)" != "false" ]; then
16 # topgit.autostash is true
23 Usage: ${tgname:-tg} [...] update [--[no-]stash] [--skip-missing] ([<name>...] | -a [<pattern>...])
24 Or: ${tgname:-tg} --continue | -skip | --stop | --abort"
28 if [ "${1:-0}" != 0 ]; then
29 printf '%s\n' "$USAGE" >&2
31 printf '%s\n' "$USAGE"
36 state_dir
="$git_dir/tg-update"
43 [ -d "$state_dir" ] ||
return 1
44 [ -s "$state_dir/fullcmd" ] ||
return 1
45 [ -f "$state_dir/remote" ] ||
return 1
46 [ -f "$state_dir/skipms" ] ||
return 1
47 [ -f "$state_dir/all" ] ||
return 1
48 [ -s "$state_dir/current" ] ||
return 1
49 [ -s "$state_dir/stashhash" ] ||
return 1
50 [ -s "$state_dir/name" ] ||
return 1
51 [ -s "$state_dir/names" ] ||
return 1
52 [ -f "$state_dir/processed" ] ||
return 1
53 [ -f "$state_dir/mergeours" ] ||
return 1
54 [ -f "$state_dir/mergeours" ] ||
return 1
55 if [ -s "$state_dir/mergeours" ]; then
56 [ -s "$state_dir/mergetheirs" ] ||
return 1
58 ! [ -s "$state_dir/mergetheirs" ] ||
return 1
63 is_active || die
"programmer error"
64 IFS
= read -r fullcmd
<"$state_dir/fullcmd" && [ -n "$fullcmd" ]
65 IFS
= read -r base_remote
<"$state_dir/remote" ||
:
66 IFS
= read -r skipms
<"$state_dir/skipms" ||
:
67 IFS
= read -r all
<"$state_dir/all" ||
:
68 IFS
= read -r current
<"$state_dir/current" && [ -n "$current" ]
69 IFS
= read -r stashhash
<"$state_dir/stashhash" && [ -n "$stashhash" ]
70 IFS
= read -r name
<"$state_dir/name" && [ -n "$name" ]
71 IFS
= read -r names
<"$state_dir/names" && [ -n "$names" ]
72 IFS
= read -r processed
<"$state_dir/processed" ||
:
73 IFS
= read -r mergeours
<"$state_dir/mergeours" ||
:
74 IFS
= read -r mergetheirs
<"$state_dir/mergetheirs" ||
:
75 if [ -n "$mergeours" ] && [ -n "$mergetheirs" ]; then
76 headhash
="$(git rev-parse --quiet --verify HEAD --)" ||
:
77 if [ -n "$headhash" ]; then
78 parents
="$(git --no-pager log -n 1 --format='format:%P' "$headhash" -- 2>/dev/null)" ||
:
79 if [ "$parents" = "$mergeours $mergetheirs" ]; then
80 mergeresult
="$headhash"
83 if [ -z "$mergeresult" ]; then
92 ! [ -e "$state_dir" ] ||
rm -rf "$state_dir" >/dev
/null
2>&1 ||
:
97 ! is_active || isactive
=1
98 if [ -n "$isactive" ] ||
[ $# -eq 1 -a x
"$1" = x
"--abort" ]; then
99 [ $# -eq 1 ] && [ x
"$1" != x
"--status" ] ||
{ do_status
; exit 0; }
104 if [ -n "$isactive" ]; then
105 IFS
= read -r current
<"$state_dir/current" ||
:
106 IFS
= read -r stashhash
<"$state_dir/stashhash" ||
:
109 if [ -n "$isactive" ]; then
110 if [ -n "$stashhash" ]; then
111 $tg revert
-f -q -q --no-stash "$stashhash" >/dev
/null
2>&1 ||
:
113 if [ -n "$current" ]; then
114 info
"Ok, update aborted, returning to ${current#refs/heads/}"
115 checkout_symref_full
-f "$current"
117 info
"Ok, update aborted. Now, you just need to"
118 info
"switch back to some sane branch using \`git$gitcdopt checkout\`."
120 ! [ -f "$git_dir/TGMERGE_MSG" ] ||
[ -e "$git_dir/MERGE_MSG" ] ||
121 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" ||
:
123 info
"No update was active"
129 info
"Ok, update stopped. Now, you just need to"
130 info
"switch back to some sane branch using \`git$gitcdopt checkout\`."
131 ! [ -f "$git_dir/TGMERGE_MSG" ] ||
[ -e "$git_dir/MERGE_MSG" ] ||
132 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" ||
:
137 if [ "$1" = "--skip" ]; then
138 info
"Ok, I will try to continue without updating this branch."
140 case " $processed " in *" $name "*);;*)
141 processed
="${processed:+$processed }$name"
144 # assume user fixed it
145 # we could be left on a detached HEAD if we were resolving
146 # a conflict while merging a base in, fix it with a checkout
147 git checkout
-q "$(strip_ref "$name")"
156 if [ -z "$restored" ]; then
157 while [ -n "$1" ]; do
161 [ -z "$names$pattern" ] || usage
1
174 if [ -z "$all" ]; then
175 names
="${names:+$names }$arg"
177 pattern
="${pattern:+$pattern }refs/$topbases/$(strip_ref "$arg")"
182 origpattern
="$pattern"
183 [ -z "$pattern" ] && pattern
="refs/$topbases"
186 current
="$(git symbolic-ref -q HEAD)" ||
:
187 if [ -n "$current" ]; then
188 [ -n "$(git rev-parse --verify --quiet HEAD --)" ] ||
189 die
"cannot return to unborn branch; switch to another branch"
191 current
="$(git rev-parse --verify --quiet HEAD)" ||
192 die
"cannot return to invalid HEAD; switch to another branch"
194 [ -n "$all$names" ] || names
="HEAD"
195 if [ -z "$all" ]; then
198 while [ $# -gt 0 ]; do
199 name
="$(verify_topgit_branch "$1")"
200 case " $names " in *" $name "*);;*)
201 names
="${names:+$names }$name"
212 mkdir
-p "$state_dir"
213 printf '%s\n' "$fullcmd" >"$state_dir/fullcmd"
214 printf '%s\n' "$base_remote" >"$state_dir/remote"
215 printf '%s\n' "$skipms" >"$state_dir/skipms"
216 printf '%s\n' "$all" >"$state_dir/all"
217 printf '%s\n' "$current" >"$state_dir/current"
218 printf '%s\n' "$stashhash" >"$state_dir/stashhash"
219 printf '%s\n' "$name" >"$state_dir/name"
220 printf '%s\n' "$names" >"$state_dir/names"
221 printf '%s\n' "$processed" >"$state_dir/processed"
222 printf '%s\n' "$1" >"$state_dir/mergeours"
223 printf '%s\n' "$2" >"$state_dir/mergetheirs"
226 stash_now_if_requested
() {
227 [ -z "$TG_RECURSIVE" ] ||
return 0
228 [ -z "$stashhash" ] ||
return 0
229 ensure_ident_available
230 msg
="tgupdate: autostash before update"
231 if [ -n "$all" ]; then
232 msg
="$msg --all${origpattern:+ $origpattern}"
237 if [ -n "$stash" ]; then
238 $tg tag
-q -q -m "$msg" --stash "$@" &&
239 stashhash
="$(git rev-parse --quiet --verify refs/tgstash --)" &&
240 [ -n "$stashhash" ] &&
241 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
242 die
"requested --stash failed"
244 $tg tag
--anonymous "$@" &&
245 stashhash
="$(git rev-parse --quiet --verify TG_STASH --)" &&
246 [ -n "$stashhash" ] &&
247 [ "$(git cat-file -t "$stashhash" -- 2>/dev/null)" = "tag" ] ||
248 die
"anonymous --stash failed"
256 TG_RECURSIVE
="[$1] $TG_RECURSIVE"
261 [ $_ret -eq 3 ] && exit 3
265 # If HEAD is a symref to "$1" detach it at its current value
266 detach_symref_head_on_branch
() {
267 _hsr
="$(git symbolic-ref -q HEAD --)" && [ -n "$_hsr" ] ||
return 0
268 _hrv
="$(git rev-parse --quiet --verify HEAD --)" && [ -n "$_hrv" ] ||
269 die
"cannot detach_symref_head_on_branch from unborn branch $_hsr"
270 git update-ref
--no-deref -m "detaching HEAD from $_hsr to safely update it" HEAD
"$_hrv"
273 # run git merge with the passed in arguments AND --no-stat
274 # return the exit status of git merge
275 # if the returned exit status is no error show a shortstat before
276 # returning assuming the merge was done into the previous HEAD
278 _oldhead
="$(git rev-parse --verify HEAD^0)"
280 git merge
$auhopt --no-stat "$@" || _ret
=$?
281 [ "$_ret" != "0" ] || git
--no-pager diff-tree
--shortstat "$_oldhead" HEAD^
0 --
285 # similar to git_merge but operates exclusively using a separate index and temp dir
286 # only trivial aggressive automatic (i.e. simple) merges are supported
288 # [optional] '--no-auto' to suppress "automatic" merging, merge fails instead
289 # $1 => '' to discard result, 'refs/?*' to update the specified ref or a varname
290 # $2 => '-m' MUST be '-m'
291 # $3 => commit message AND, if $1 matches refs/?* the update-ref message
292 # $4 => commit-ish to merge as "ours"
293 # $5 => commit-ish to merge as "theirs"
294 # [$6...] => more commit-ishes to merge as "theirs" in octopus
296 # all merging is done in a separate index (or temporary files for simple merges)
297 # if successful the ref or var is updated with the result
298 # otherwise everything is left unchanged and a silent failure occurs
299 # if successful and $1 matches refs/?* it WILL BE UPDATED to a new commit using the
300 # message and appropriate parents AND HEAD WILL BE DETACHED first if it's a symref
302 # otherwise if $1 does not match refs/?* and is not empty the named variable will
303 # be set to contain the resulting commit from the merge
304 # the working tree and index ARE LEFT COMPLETELY UNTOUCHED no matter what
305 v_attempt_index_merge
() {
307 if [ "$1" = "--no-auto" ]; then
311 [ "$#" -ge 5 ] && [ "$2" = "-m" ] && [ -n "$3" ] && [ -n "$4" ] && [ -n "$5" ] ||
312 die
"programmer error: invalid arguments to v_attempt_index_merge: $*"
317 rh
="$(git rev-parse --quiet --verify "$_head^
0" --)" && [ -n "$rh" ] ||
return 1
325 if [ $# -gt 1 ]; then
326 ihl
="$(git merge-base --independent "$@
")" ||
return 1
328 [ $# -ge 1 ] && [ -n "$1" ] ||
return 1
330 [ $# -eq 1 ] || _octo
=1
331 mb
="$(git merge-base ${_octo:+--octopus} "$rh" "$@
")" && [ -n "$mb" ] ||
{
332 mb
="$(git hash-object -w -t tree --stdin < /dev/null)"
335 if [ -z "$_mt" ]; then
336 if [ -n "$_octo" ]; then
337 while [ $# -gt 1 ] && mbh
="$(git merge-base "$rh" "$1")" && [ -n "$mbh" ]; do
338 if [ "$rh" = "$mbh" ]; then
339 _mmsg
="Fast-forward (no commit created)"
342 elif [ "$1" = "$mbh" ]; then
348 if [ $# -eq 1 ]; then
350 mb
="$(git merge-base "$rh" "$1")" && [ -n "$mb" ] ||
return 1
353 if [ -z "$_octo" ]; then
354 r1
="$(git rev-parse --quiet --verify "$1^
0" --)" && [ -n "$r1" ] ||
return 1
356 if [ "$rh" = "$mb" ]; then
357 _mmsg
="Fast-forward (no commit created)"
360 elif [ "$r1" = "$mb" ]; then
361 [ -n "$_mmsg" ] || _mmsg
="Already up-to-date!"
368 if [ -z "$newc" ]; then
369 inew
="$tg_tmp_dir/index.$$"
370 ! [ -e "$inew" ] ||
rm -f "$inew"
371 itmp
="$tg_tmp_dir/output.$$"
372 imrg
="$tg_tmp_dir/auto.$$"
373 [ -z "$_octo" ] ||
>"$imrg"
378 if [ -n "$_parents" ]; then
379 if [ "$(git rev-list --count --max-count=1 "$1" --not "$_newrh" --)" = "0" ]; then
384 GIT_INDEX_FILE
="$inew" git read-tree
-m --aggressive -i "$mb" "$rh" "$1" ||
{ rm -f "$inew" "$imrg"; return 1; }
385 GIT_INDEX_FILE
="$inew" git ls-files
--unmerged --full-name --abbrev :/ >"$itmp" 2>&1 ||
{ rm -f "$inew" "$itmp" "$imrg"; return 1; }
386 ! [ -s "$itmp" ] ||
{
387 if ! GIT_INDEX_FILE
="$inew" TG_TMP_DIR
="$tg_tmp_dir" git merge-index
-q "$TG_INST_CMDDIR/tg--index-merge-one-file" -a >"$itmp" 2>&1; then
388 rm -f "$inew" "$itmp" "$imrg"
391 if [ -s "$itmp" ]; then
392 if [ -n "$_noauto" ]; then
393 rm -f "$inew" "$itmp" "$imrg"
396 if [ -n "$_octo" ]; then
397 cat "$itmp" >>"$imrg"
405 newt
="$(GIT_INDEX_FILE="$inew" git write-tree)" && [ -n "$newt" ] ||
{ rm -f "$inew" "$imrg"; return 1; }
406 _parents
="${_parents:+$_parents }-p $1"
407 if [ $# -gt 1 ]; then
414 [ -z "$_octo" ] || LC_ALL
=C
sort -u <"$imrg"
415 rm -f "$inew" "$imrg"
416 newc
="$(git commit-tree -p "$orh" $_parents -m "$_msg" "$newt")" && [ -n "$newc" ] ||
return 1
417 _mmsg
="Merge made by the 'trivial aggressive$_auto${_octo:+ octopus}' strategy."
421 if [ -n "$_same" ]; then
423 if rv
="$(git rev-parse --quiet --verify "$_var" --)" && [ "$rv" = "$newc" ]; then
427 if [ -z "$_same" ]; then
428 detach_symref_head_on_branch
"$_var" ||
return 1
429 # git update-ref returns 0 even on failure :(
430 git update-ref
-m "$_msg" "$_var" "$newc" ||
return 1
434 eval "$_var="'"$newc"'
438 [ -n "$_nodt" ] || git
--no-pager diff-tree
--shortstat "$orh" "$newc" --
442 # shortcut that passes $3 as a preceding argument (which must match refs/?*)
443 attempt_index_merge
() {
445 if [ "$1" = "--no-auto" ]; then
449 case "$3" in refs
/?
*);;*)
450 die
"programmer error: invalid arguments to attempt_index_merge: $*"
452 v_attempt_index_merge
$_noauto "$3" "$@"
457 [ -n "$1" ] ||
return 0
459 [ "$1" != "$on_base" ] ||
460 [ "$(git symbolic-ref -q HEAD)" != "refs/$topbases/$1" ]
468 # We are cacheable until the first change
472 ## First, take care of our base
474 _depcheck
="$(get_temp tg-depcheck)"
476 needs_update
"$_update_name" >"$_depcheck" ||
:
477 if [ -n "$missing_deps" ]; then
478 msg
="Some dependencies are missing: $missing_deps"
479 if [ -n "$skipms" ]; then
480 info
"$msg; skipping"
481 elif [ -z "$all" ]; then
484 info
"$msg; skipping branch $_update_name"
488 if [ -s "$_depcheck" ]; then
490 sed 's/ [^ ]* *$//' |
# last is $_update_name
491 sed 's/.* \([^ ]*\)$/+\1/' |
# only immediate dependencies
492 sed 's/^\([^+]\)/-\1/' |
# now each line is +branch or -branch (+ == recurse)
493 >"$_depcheck.ideps" \
494 uniq -s 1 # fold branch lines; + always comes before - and thus wins within uniq
496 stash_now_if_requested
498 while read -r depline
; do
500 action
="${depline%$dep}"
502 # We do not distinguish between dependencies out-of-date
503 # and base/remote out-of-date cases for $dep here,
504 # but thanks to needs_update returning : or refs/remotes/<remote>/<name>
505 # for the latter, we do correctly recurse here
508 if [ x
"$action" = x
+ ]; then
509 case " $missing_deps " in *" $dep "*)
510 info
"Skipping recursing to missing dependency: $dep"
513 info
"Recursing to $dep..."
514 recursive_update
"$dep" ||
exit 3
516 done <"$_depcheck.ideps"
518 # Create a list of all the fully qualified ref names that need
519 # to be merged into $_update_name's base. This will be done
520 # as an octopus merge if there are no conflicts.
524 while read -r dep
; do
531 d
="${dep#refs/heads/}"
532 deplist
="${deplist:+$deplist }$d"
533 deplines
="$deplines$d$lf"
537 deplist
="${deplist:+$deplist }$d"
538 deplines
="$deplines$d$lf"
543 set -- "$@" "refs/heads/$dep"
544 deplist
="${deplist:+$deplist }$dep"
545 deplines
="$deplines$dep$lf"
548 done <"$_depcheck.ideps"
550 # Make sure we end up on the correct base branch
553 if [ $# -ge 2 ]; then
554 info
"Updating $_update_name base with deps: $deplist"
556 msg
="tgupdate: octopus merge $# deps into $topbases/$_update_name$lf$lf$deplines"
557 if attempt_index_merge
-m "$msg" "refs/$topbases/$_update_name" "$@"; then
560 info
"Octopus merge failed; falling back to multiple 3-way merges"
565 for fulldep
in "$@"; do
566 # This will be either a proper topic branch
567 # or a remote base. (branch_needs_update() is called
568 # only on the _dependencies_, not our branch itself!)
572 dep
="${fulldep#refs/heads/}";;
574 dep
="${fulldep#refs/}";;
576 dep
="$fulldep";; # this should be a programmer error
579 info
"Updating $_update_name base with $dep changes..."
581 msg
="tgupdate: merge $dep into $topbases/$_update_name"
583 ! attempt_index_merge
$no_auto -m "$msg" "refs/$topbases/$_update_name" "$fulldep^0" &&
585 # We need to switch to the base branch
586 # ...but only if we aren't there yet (from failed previous merge)
587 do_base_switch
"$_update_name" || die
"do_base_switch failed" &&
588 git_merge
-m "$msg" "$fulldep^0"
593 info
"Please commit merge resolution and call \`$tgdisplay update --continue\`"
594 info
"(use \`$tgdisplay status\` to see more options)"
599 info
"The base is up-to-date."
603 ## Second, update our head with the remote branch
606 merge_with
="refs/$topbases/$_update_name"
607 if has_remote
"$_update_name"; then
608 _rname
="refs/remotes/$base_remote/$_update_name"
609 if branch_contains
"refs/heads/$_update_name" "$_rname"; then
610 info
"The $_update_name head is up-to-date wrt. its remote branch."
612 stash_now_if_requested
613 info
"Reconciling $_update_name base with remote branch updates..."
615 msg
="tgupdate: merge ${_rname#refs/} onto $topbases/$_update_name"
619 if [ -n "$mergeresult" ]; then
620 checkours
="$(git rev-parse --verify --quiet "refs
/$topbases/$_update_name^
0" --)" ||
:
621 checktheirs
="$(git rev-parse --verify --quiet "$_rname^
0" --)" ||
:
622 if [ "$mergeours" = "$checkours" ] && [ "$mergetheirs" = "$checktheirs" ]; then
623 got_merge_with
="$mergeresult"
627 [ -z "$got_merge_with" ] &&
628 ! v_attempt_index_merge
"merge_with" -m "$msg" "refs/$topbases/$_update_name" "$_rname^0" &&
630 # *DETACH* our HEAD now!
631 git checkout
-q --detach "refs/$topbases/$_update_name" || die
"git checkout failed" &&
632 git_merge
-m "$msg" "$_rname^0" &&
633 merge_with
="$(git rev-parse --verify HEAD --)"
637 "$(git rev-parse --verify --quiet "refs
/$topbases/$_update_name^
0" --)" \
638 "$(git rev-parse --verify --quiet "$_rname^
0" --)"
639 info
"Please commit merge resolution and call \`$tgdisplay update --continue\`"
640 info
"(use \`$tgdisplay status\` to see more options)"
643 # Go back but remember we want to merge with this, not base
644 [ -z "$got_merge_with" ] || merge_with
="$got_merge_with"
645 plusextra
="${_rname#refs/}+"
650 ## Third, update our head with the base
652 if branch_contains
"refs/heads/$_update_name" "$merge_with"; then
653 info
"The $_update_name head is up-to-date wrt. the base."
656 stash_now_if_requested
657 info
"Updating $_update_name against new base..."
659 msg
="tgupdate: merge ${plusextra}$topbases/$_update_name into $_update_name"
661 ! attempt_index_merge
-m "$msg" "refs/heads/$_update_name" "$merge_with^0" &&
663 # Home, sweet home...
664 # (We want to always switch back, in case we were
665 # on the base from failed previous merge.)
666 git checkout
-q "$_update_name" || die
"git checkout failed" &&
667 git_merge
-m "$msg" "$merge_with^0"
671 info
"Please commit merge resolution and call \`$tgdisplay update --continue\`"
672 info
"(use \`$tgdisplay status\` to see more options)"
677 # We are "read-only" and cacheable until the first change
681 do_non_annihilated_branches_patterns
() {
682 while read -r _pat
&& [ -n "$_pat" ]; do
685 non_annihilated_branches
"$@"
688 do_non_annihilated_branches
() {
689 if [ -z "$pattern" ]; then
690 non_annihilated_branches
692 do_non_annihilated_branches_patterns
<<-EOT
693 $(sed 'y/ /\n/' <<-LIST
701 if [ -n "$all" ] && [ -z "$restored" ]; then
703 while read name
&& [ -n "$name" ]; do
704 case " $names " in *" $name "*);;*)
705 names
="${names:+$names }$name"
708 $(do_non_annihilated_branches)
712 for name
in $names; do
713 case " $processed " in *" $name "*) continue; esac
714 [ -z "$all" ] && case "$names" in *" "*) ! :; esac || info
"Proccessing $name..."
715 update_branch
"$name" ||
exit
716 processed
="${processed:+$processed }$name"
719 [ -z "$all" ] && case "$names" in *" "*) ! :; esac ||
720 info
"Returning to ${current#refs/heads/}..."
721 checkout_symref_full
"$current"
722 ! [ -f "$git_dir/TGMERGE_MSG" ] ||
[ -e "$git_dir/MERGE_MSG" ] ||
723 mv -f "$git_dir/TGMERGE_MSG" "$git_dir/MERGE_MSG" ||
: