2 # TopGit - A different patch queue manager
3 # Copyright (C) 2008 Petr Baudis <pasky@suse.cz>
4 # Copyright (C) 2015-2021 Kyle J. McKay <mackyle@gmail.com>
9 Usage: ${tgname:-tg} [...] export [--collapse] [--force] [<option>...] <newbranch>
10 Or: ${tgname:-tg} [...] export --linearize [--force] [<option>...] <newbranch>
11 Or: ${tgname:-tg} [...] export --quilt [--force] [-a | --all | -b <branch>...]
12 [--binary] [--flatten] [--numbered] [--strip[=N]] <directory>
14 -s <mode> set subject bracketed [strings] strip mode
15 --notes[=<ref>] export .topmsg --- comment to notes ref <ref>
16 --no-notes discard .topmsg --- comment"
20 if [ "${1:-0}" != 0 ]; then
21 printf '%s\n' "$USAGE" >&2
23 printf '%s\n' "$USAGE"
55 branches
="${branches:+$branches }$1"; shift;;
67 if [ "$val" = "--strip" ]; then
70 elif [ -n "$val" ] && [ "${val#*[!0-9]}" = "$val" ]; then
74 die
"invalid --strip parameter $arg"
78 if [ "$val" = "--notes" ]; then
80 notesref
="refs/notes/commits"
81 elif [ -n "$val" ] && [ "${val#-}" = "$val" ]; then
83 refs
/notes
/*) checknref
="$val";;
84 notes
/*) checknref
="refs/$val";;
85 *) checknref
="refs/notes/$val";;
87 git check-ref-format
"$checknref" >/dev
/null
2>&1 ||
88 die
"invalid --notes parameter $arg"
92 die
"invalid --notes parameter $arg"
98 test $# -gt 0 && test -n "$1" || die
"-s requires an argument"
109 [ -z "$output" ] || die
"output already specified ($output)"
114 [ -z "$smode" ] ||
[ "$driver" != "quilt" ] ||
115 die
"-s works only with the collapse/linearize driver"
117 [ "${notesflag:-0}" = "0" ] ||
[ "$driver" != "quilt" ] ||
118 die
"--notes works only with the collapse/linearize driver"
120 if [ "$driver" != "quilt" ]; then
121 test -n "$smode" || smode
="$(git config topgit.subjectmode)" ||
:
122 case "${smode:-tg}" in
125 topgit|
patch|mailinfo|trim|keep
);;
126 *) die
"invalid subject mode: $smode"
128 if [ -z "$notesflag" ]; then
129 if notesflag
="$(git config --bool --get topgit.notesexport 2>/dev/null)"; then
131 true
) notesflag
=1; notesref
="refs/notes/commits";;
132 false
) notesflag
=0; notesref
=;;
135 notesflag
="$(git config --get topgit.notesexport 2>/dev/null)" &&
139 "-"*) checknref
="$notesflag";;
140 refs
/notes
/*) checknref
="$notesflag";;
141 notes
/*) checknref
="refs/$notesflag";;
142 *) checknref
="refs/notes/$notesflag";;
144 git check-ref-format
"$checknref" >/dev
/null
2>&1 ||
145 die
"invalid topgit.notesExport config setting \"$notesflag\""
147 notesref
="$checknref"
152 [ -z "$branches" ] ||
[ "$driver" = "quilt" ] ||
153 die
"-b works only with the quilt driver"
155 [ "$driver" = "quilt" ] ||
[ -z "$numbered" ] ||
156 die
"--numbered works only with the quilt driver"
158 [ "$driver" = "quilt" ] ||
[ -z "$flatten" ] ||
159 die
"--flatten works only with the quilt driver"
161 [ "$driver" = "quilt" ] ||
[ -z "$binary" ] ||
162 die
"--binary works only with the quilt driver"
164 [ "$driver" = "quilt" ] ||
[ -z "$strip" ] ||
165 die
"--strip works only with the quilt driver"
167 [ "$driver" = "quilt" ] ||
[ -z "$allbranches" ] ||
168 die
"--all works only with the quilt driver"
170 [ -z "$branches" ] ||
[ -z "$allbranches" ] ||
171 die
"-b conflicts with the --all option"
173 if [ "$driver" = "linearize" ]; then
174 setup_git_dir_is_bare
175 if [ -n "$git_dir_is_bare" ]; then
176 fatal
'export --linearize does not work on a bare repository...yet!'
177 fatal
'(but you can use `tg -w : export --linearize` instead for now)'
182 if [ -z "$branches" ] && [ -z "$allbranches" ]; then
183 # this check is only needed when no branches have been passed
184 v_verify_topgit_branch name HEAD
187 if [ -n "$branches" ]; then
188 oldbranches
="$branches"
190 while read bname
&& [ -n "$bname" ]; do
191 v_verify_topgit_branch bname
"$bname"
192 branches
="${branches:+$branches }$bname"
194 $(sed 'y/ /\n/' <<-LIST
199 unset oldbranches bname
202 read -r nowsecs nowtzoff
<<EOT
205 playground
="$(get_temp tg-export -d)"
212 nowsecs
=$
(( $nowsecs + 1 ))
218 test z
"$smode" = z
"mailinfo" ||
{ mik
=-k &&
219 test z
"$smode" = z
"keep"; } ||
220 sprefix
="$(git config topgit.subjectprefix)" ||
:
229 # Get commit message and authorship information
230 git cat-file blob
"$name:.topmsg" 2>/dev
/null |
231 awk -v nf
="$playground/^notes" '
232 BEGIN {m=0; printf "%s", "" >nf}
234 /^---[ \t]*$/ {m=1; next}
237 ' | git mailinfo
$mik "$playground/^msg" /dev
/null
> "$playground/^info"
239 [ "${notesflag:-0}" = "1" ] && [ -n "$notesref" ] &&
240 [ -s "$playground/^notes" ]
242 git stripspace
<"$playground/^notes" >"$playground/^notes,"
243 mv -f "$playground/^notes," "$playground/^notes"
246 unset GIT_AUTHOR_NAME
247 unset GIT_AUTHOR_EMAIL
249 GIT_AUTHOR_NAME
="$(sed -n "/^Author
/ s
/Author
:[ $tab]*//p
" "$playground/^info
")"
250 GIT_AUTHOR_EMAIL
="$(sed -n "/^Email
/ s
/Email
:[ $tab]*//p
" "$playground/^info
")"
251 GIT_AUTHOR_DATE
="$(sed -n "/^Date
/ s
/Date
:[ $tab]*//p
" "$playground/^info
")"
252 SUBJECT
="$(sed -n "/^Subject
/ s
/Subject
:[ $tab]*//p
" "$playground/^info
")"
253 if test z
"$smode" != z
"mailinfo" && test z
"$smode" != z
"keep"; then
254 SUBJECT
="$(printf '%s\n' "$SUBJECT" |
255 awk -v mode="$smode" -v prefix="$sprefix" '{
259 if (mode != "trim
") {
261 tolower(substr($0, 1, (lp = 1 + length(prefix)))) == "[" tolower(prefix) &&
263 if (substr($0, 1 + lp, 1) == " ") ++lp
264 lead = tolower(substr($0, 1 + lp, 8))
265 if (lead ~ /^patch/ || (mode == "topgit
" &&
266 lead ~ /^(base|root|stage|release)\]/))
267 $0 = "[" substr($0, 1 + lp)
269 if (!sub(/^\[[Pp][Aa][Tt][Cc][Hh][^]]*\][ \t]*/, "") &&
271 sub(/^\[([Bb][Aa][Ss][Ee]|[Rr][Oo][Oo][Tt]|[Ss][Tt][Aa][Gg][Ee]|[Rr][Ee][Ll][Ee][Aa][Ss][Ee])\][ \t]*/, "")
278 test -n "$GIT_AUTHOR_NAME" && export GIT_AUTHOR_NAME
279 test -n "$GIT_AUTHOR_EMAIL" && export GIT_AUTHOR_EMAIL
281 GIT_COMMITTER_DATE
="$nowsecs $nowtzoff"
282 : ${GIT_AUTHOR_DATE:=$GIT_COMMITTER_DATE}
283 export GIT_AUTHOR_DATE
284 export GIT_COMMITTER_DATE
287 printf "%s\n\n" "${SUBJECT:-$name}"
288 cat "$playground/^msg"
290 git commit-tree "$tree" -p "$parent"'
293 [ "${notesflag:-0}" = "1" ] && [ -n "$notesref" ] &&
294 [ -s "$playground/^notes" ]
296 _notesblob
="$(git hash-object -t blob -w --stdin <"$playground/^notes
")"
297 _cmtnew
="$(eval "$_cmttreecmd")"
298 git notes
--ref="$notesref" add
-f -C "$_notesblob" "$_cmtnew" >/dev
/null
2>&1 ||
:
299 printf '%s\n' "$_cmtnew"
304 unset GIT_AUTHOR_NAME
305 unset GIT_AUTHOR_EMAIL
306 unset GIT_AUTHOR_DATE
307 unset GIT_COMMITTER_DATE
315 while [ $# -gt 0 ]; do
316 [ -z "$1" ] ||
eval "$_vname=\"\${$_vname:+\$$_vname }-p \$1\""
321 # collapsed_commit NAME
322 # Produce a collapsed commit of branch NAME.
327 rm -f "$playground/^pre" "$playground/^post"
331 [ -s "$playground/$name^parents" ] || git rev-parse
--verify "refs/$topbases/$name^0" -- >> "$playground/$name^parents"
332 parent
="$(cut -f 1 "$playground/$name^parents
" 2> /dev/null |
333 while read -r p; do git rev-parse --quiet --verify "$p^
0" -- || :; done)"
334 if [ $
(( $
(cat "$playground/$name^parents" 2>/dev
/null |
wc -l) )) -gt 1 ]; then
335 # Produce a merge commit first
336 v_pretty_tree prtytree
"$name" -b
337 v_get_p_arg_list plist
$parent
339 echo "TopGit-driven merge of branches
:"
341 cut -f 2 "$playground/$name^parents
"
342 } | GIT_AUTHOR_DATE="$nowsecs $nowtzoff" \
343 GIT_COMMITTER_DATE="$nowsecs $nowtzoff" \
344 git commit-tree "$prtytree" $plist)"
347 if branch_empty
"$name"; then
350 v_pretty_tree prtytree
"$name"
351 create_tg_commit
"$name" "$prtytree" "$parent"
354 echol
"$name" >>"$playground/^ticker"
358 # This will collapse a single branch, using information about
359 # previously collapsed branches stored in $playground.
362 if [ -s "$playground/$_dep^commit" ]; then
363 # We've already seen this dep
364 commit
="$(cat "$playground/$_dep^commit
")"
366 elif [ -z "$_dep_is_tgish" ]; then
367 # This dep is not for rewrite
368 commit
="$(git rev-parse --verify "refs
/heads
/$_dep^
0" --)"
371 # First time hitting this dep; the common case
372 echo "Collapsing $_dep"
373 test -d "$playground/${_dep%/*}" || mkdir
-p "$playground/${_dep%/*}"
374 commit
="$(collapsed_commit "$_dep")"
376 echol
"$commit" >"$playground/$_dep^commit"
379 # Propagate our work through the dependency chain
380 test -d "$playground/${_name%/*}" || mkdir
-p "$playground/${_name%/*}"
381 echo "$commit $_dep" >>"$playground/$_name^parents"
389 if [ -z "$_dep_is_tgish" ]; then
390 # This dep is not for rewrite
396 if [ -n "$strip" ]; then
398 while [ "$i" -gt 0 ]; do
399 [ "$_dep_tmp" = "${_dep_tmp#*/}" ] && break
400 _dep_tmp
=${_dep_tmp#*/}
405 dn
="${_dep_tmp%/}.diff"
406 case "$dn" in */*);;*) dn
="./$dn"; esac
409 [ "x$dn" = "x./" ] && dn
=""
411 if [ -n "$flatten" ] && [ "$dn" ]; then
412 bn
="$(echo "$_dep_tmp.
diff" | sed -e 's#_#__#g' -e 's#/#_#g')"
418 if [ -e "$playground/$_dep^commit" ]; then
419 # We've already seen this dep
423 test -d "$playground/${_dep%/*}" || mkdir
-p "$playground/${_dep%/*}"
424 >>"$playground/$_dep^commit"
426 if branch_empty
"$_dep"; then
427 echo "Skip empty patch $_dep"
429 if [ -n "$numbered" ]; then
430 number
="$(echo $(($(cat "$playground/^number
" 2>/dev/null) + 1)))"
431 bn
="$(printf "%04u-$bn" $number)"
432 echo "$number" >"$playground/^number"
435 echo "Exporting $_dep"
436 mkdir
-p "$output/$dn"
437 tg
patch ${binary:+--binary} "$_dep" >"$output/$dn$bn"
438 echol
"$dn$bn -p1" >>"$output/series"
444 if test ! -f "$playground/^BASE"; then
445 if [ -n "$_dep_is_tgish" ]; then
446 head="$(git rev-parse --verify "refs
/$topbases/$_dep^
0" --)"
448 head="$(git rev-parse --verify "refs
/heads
/$_dep^
0" --)"
450 echol
"$head" > "$playground/^BASE"
451 git checkout
-q $iowopt "$head"
452 [ -n "$_dep_is_tgish" ] ||
return 0
455 head=$
(git rev-parse
--verify HEAD
--)
457 if [ -z "$_dep_is_tgish" ]; then
458 # merge in $_dep unless already included
459 rev="$(git rev-parse --verify "refs
/heads
/$_dep^
0" --)"
460 common
="$(git merge-base --all HEAD "$rev")" ||
:
461 if test "$rev" = "$common"; then
462 # already included, just skip
467 git merge
$auhopt -m "tgexport: merge $_dep into base" -s recursive
"refs/heads/$_dep^0" || retmerge
="$?"
468 if test "x$retmerge" != "x0"; then
469 echo "fix up the merge, commit and then exit."
471 "${SHELL:-@SHELL_PATH@}" -i </dev
/tty
477 v_pretty_tree _deptree
"$_dep"
478 v_pretty_tree _depbasetree
"$_dep" -b
479 git merge-recursive
"$_depbasetree" -- HEAD
"$_deptree" || retmerge
="$?"
481 if test "x$retmerge" != "x0"; then
483 echo "fix up the merge, update the index and then exit. Don't commit!"
485 "${SHELL:-@SHELL_PATH@}" -i </dev
/tty
489 result_tree
=$
(git write-tree
)
490 # testing branch_empty might not always give the right answer.
491 # It can happen that the patch is non-empty but still after
492 # linearizing there is no change. So compare the trees.
493 if test "x$result_tree" = "x$(git rev-parse --verify $head^{tree} --)"; then
494 echo "skip empty commit $_dep"
496 newcommit
=$
(create_tg_commit
"$_dep" "$result_tree" HEAD
)
498 git update-ref HEAD
$newcommit $head
499 echo "exported commit $_dep"
507 if [ "$driver" = "collapse" ] ||
[ "$driver" = "linearize" ]; then
509 die
"no target branch specified"
510 if ! ref_exists
"refs/heads/$output"; then
512 elif [ -z "$forceoutput" ]; then
513 die
"target branch '$output' already exists; first run: git$gitcdopt branch -D $output, or run $tgdisplay export with --force"
517 ensure_ident_available
519 [ -z "$wayback" ] || wayback_push
="$(git config --get remote.wayback.url 2>/dev/null)" ||
:
520 [ -z "$wayback" ] ||
[ -n "$wayback_push" ] || die
"failed to configure wayback export"
522 elif [ "$driver" = "quilt" ]; then
524 die
"no target directory specified"
525 [ -n "$forceoutput" ] ||
[ ! -e "$output" ] || is_empty_dir
"$output" . ||
526 die
"non-empty target directory already exists (use --force to override): $output"
534 # FIXME should we abort on missing dependency?
535 [ -z "$_dep_missing" ] ||
return 0
537 [ -z "$_dep_is_tgish" ] ||
[ -z "$_dep_annihilated" ] ||
return 0
539 case $_dep in ":"*) return; esac
540 branch_needs_update
>/dev
/null
542 die
"cancelling export of $_dep (-> $_name): branch not up-to-date"
547 # Call driver on all the branches - this will happen
548 # in topological order.
549 if [ -n "$allbranches" ]; then
551 non_annihilated_branches |
556 elif [ -z "$branches" ]; then
557 recurse_deps driver
"$name"
558 (_ret
=0; _dep
="$name"; _name
=; _dep_is_tgish
=1; _dep_missing
=; driver
)
561 while read _dep
&& [ -n "$_dep" ]; do
565 $(sed 'y/ /\n/' <<-LIST
571 case "$branches" in *" "*) pl
="es"; esac
575 if [ "$driver" = "collapse" ]; then
576 cmd
='git update-ref "refs/heads/$output" "$(cat "$playground/$name^commit")"'
577 [ -n "$forceoutput" ] || cmd
="$cmd \"\""
579 [ -z "$wayback_push" ] || git
-c "remote.wayback.url=$wayback_push" push
-q ${forceoutput:+--force} wayback
"refs/heads/$output:refs/heads/$output"
581 depcount
=$
(( $
(cat "$playground/^ticker" |
wc -l) ))
582 echo "Exported topic branch $name (total $depcount topics) to branch $output"
584 elif [ "$driver" = "quilt" ]; then
585 depcount
=$
(( $
(cat "$output/series" |
wc -l) ))
586 echo "Exported topic branch$pl $name (total $depcount topics) to directory $output"
588 elif [ "$driver" = "linearize" ]; then
589 git checkout
-q --no-track $iowopt $checkout_opt $output
590 [ -z "$wayback_push" ] || git
-c "remote.wayback.url=$wayback_push" push
-q ${forceoutput:+--force} wayback
"refs/heads/$output:refs/heads/$output"
593 v_pretty_tree nametree
"$name"
594 if test $
(git rev-parse
--verify "$nametree^{tree}" --) != $
(git rev-parse
--verify "HEAD^{tree}" --); then
595 echo "Warning: Exported result doesn't match"
596 echo "tg-head=$(git rev-parse --verify "refs
/heads
/$name" --), exported=$(git rev-parse --verify "HEAD
" --)"