4 # Management of git commit series
6 # Copyright 2015 Jan H. Schönherr <jan@schnhrr.de>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License version 2 as
10 # published by the Free Software Foundation.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 $B add [--onto <base>] <start> [<series>]
35 $B create <series> [<start>]
39 $B rebase [-i] [<series>]
43 Experimental commands:
44 $B update-base <new-base> [<series>]
45 $B update-start <new-start> [<series>]
46 $B checkout <series-commit>
49 $B rev-parse <series-commit>
51 i,interactive! interactive rebase
52 onto!=base use a <base> different from <start>
54 . $
(git
--exec-path)/git-sh-setup
61 if [ -x /usr
/bin
/tput ]; then
64 YELLOW
=$
(tput setaf
3)
69 function start_reference
() {
72 function rebase_reference
() {
73 echo "refs/series-after-rebase/$1"
75 function base_reference
() {
78 function head_reference
() {
82 function ensure_rev
() {
86 if ! git rev-parse
--quiet --verify "$rev" >/dev
/null
; then
87 echo "$rev does not reference a valid revision." >&2
92 function ensure_sym
() {
97 name
=$
(git rev-parse
--symbolic-full-name "$ref")
98 if [[ -z $name ||
$name = HEAD
]]; then
99 echo "$ref is not a valid symbolic name." >&2
104 function ensure_ancestor
() {
109 if ! git merge-base
--is-ancestor "$ancestor" "$rev"; then
110 echo "$ancestor is not an ancestor of $rev." >&2
115 function series_exists
() {
116 local series start_ref
118 start_ref
=$
(start_reference
"$series")
120 git show-ref
--quiet --verify "$start_ref"
122 function ensure_series_exists
() {
126 if ! series_exists
"$series"; then
127 echo "Series $series does not exist." >&2
132 function ensure_empty
() {
134 echo "Too many arguments." >&2
138 function ensure_set
() {
140 echo "Missing arguments." >&2
145 function normalize_series_name
() {
146 # Regular branches in refs/heads/ are used as series.
147 # The name of the series does not have refs/heads/ in front. Thus,
148 # it has to be removed.
150 # A branch may be specified as master, heads/master, or
152 [[ $1 =~ ^
((refs
/)?
heads
/)?
(.
*) ]]
153 echo ${BASH_REMATCH[3]}
156 function current_branch
() {
157 git rev-parse
--symbolic-full-name HEAD
160 function get_commitcount
() {
162 local series
head start
164 head=$
(head_reference
"$series")
165 start
=$
(start_reference
"$series")
167 git rev-list
--count "$start..$head" --
170 function calc_score
() {
172 local series commit dist
head start commit_sym
173 local start_is_ancestor head_is_descendant head_is_ancestor
177 # highest class: directly on head of a series
178 commit_sym
=$
(git rev-parse
--symbolic-full-name "$commit")
179 head=$
(head_reference
"$series")
180 if [[ $head = $commit_sym ]]; then
181 # shortest series wins
182 dist
=$
(get_commitcount
"$series")
183 echo $
((1000000 - dist
))
187 # second highest score: on some commit, which is part of the series
188 start
=$
(start_reference
"$series")
191 if git merge-base
--is-ancestor "$start" "$commit"; then
194 if git merge-base
--is-ancestor "$commit" "$head"; then
197 if [[ -n $start_is_ancestor && -n $head_is_descendant ]]; then
198 # shortest series wins
199 dist
=$
(get_commitcount
"$series")
200 echo $
((900000 - dist
))
204 # third highest score: branched from some commit of the series
206 if git merge-base
--is-ancestor "$head" "$commit"; then
209 if [[ -n $start_is_ancestor && -z $head_is_ancestor ]]; then
210 # shortest distance to $start wins
211 dist
=$
(git rev-list
--count "$start..$commit" --)
212 echo $
((800000 - dist
))
216 # forth highest score: on top of a series
217 if [[ -n $head_is_ancestor ]]; then
218 # shortest distance to $head wins
219 dist
=$
(git rev-list
--count "$head..$commit" --)
220 echo $
((700000 - dist
))
229 function all_series
() {
230 git for-each-ref
--format="%(refname)" "$(start_reference "*")" |
sed -e 's#'"$(start_reference "\
(.
*\
)")"'#\1#'
233 function current_series
() {
236 local series s score maxscore
239 for s
in $
(all_series
); do
240 score
=$
(calc_score
"$s" HEAD
)
241 if [[ $score -eq $maxscore ]]; then
244 if [[ $score -gt $maxscore ]]; then
250 if [[ -n $series ]]; then
252 elif [[ $maxscore -gt 0 ]]; then
253 echo "Cannot determine series; try specifying one." >&2
256 echo "Currently not on any series; try specifying one." >&2
261 # usage: get_series <pos-of-series-arg-in-$@> "$@"
262 function get_series
() {
267 if [[ $# -ge 1 ]]; then
268 series
=$
(normalize_series_name
"$1")
270 series
=$
(current_series
)
272 ensure_series_exists
"$series"
282 # usage: create <series> [<start>]
283 function create_series
() {
284 local series
head base
285 test $# -ge 1 || usage
286 test $# -le 2 || usage
287 series
=$
(normalize_series_name
"$1")
288 head=$
(head_reference
"$series")
291 if git show-ref
--quiet --verify "$head"; then
292 echo "Series $series already exists."
296 git update-ref
"$head" "$base" ""
297 add_series
"$base" "$series"
300 # $B add [--onto <base>] <start> [<series>]
301 function add_series
() {
302 local base series
head start start_ref
303 test $# -ge 1 || usage
304 test $# -le 2 || usage
306 if [[ $# -ge 2 ]]; then
307 series
=$
(normalize_series_name
"$2")
309 series
=$
(normalize_series_name
"$(current_branch)")
311 head=$
(head_reference
"$series")
313 if [[ -n $onto ]]; then
317 start
=$
(git merge-base
"$head" "$base")
320 # sanity check parameters
323 ensure_ancestor
"$start" "$head"
325 start_ref
=$
(start_reference
"$series")
326 if git show-ref
--quiet --verify "$start_ref"; then
327 echo "Series $series already exists."
331 # create static reference to start, so that we see when
332 # base and start diverge
333 git update-ref
"$start_ref" "$start" ""
335 # create symbolic reference to base, so that it moves with the target
336 local base_ref base_sym
337 base_ref
=$
(base_reference
"$series")
338 base_sym
=$
(git rev-parse
--symbolic-full-name "$base")
339 git symbolic-ref
"$base_ref" "$base_sym"
341 show_series
"$series"
344 # usage: remove [<series>]
345 function remove_series
() {
347 test $# -le 1 || usage
348 series
=$
(get_series
1 "$@")
350 git update-ref
-d "$(start_reference "$series")"
351 git update-ref
-d "$(rebase_reference "$series")"
352 git symbolic-ref
--delete "$(base_reference "$series")"
355 # usage: delete <series>
356 function delete_series
() {
358 test $# -eq 1 || usage
359 series
=$
(normalize_series_name
"$1")
361 ensure_series_exists
"$series"
363 #git update-ref -d "$(head_reference "$series")"
364 git branch
-D "$series"
365 remove_series
"$series"
368 # usage: show [<series>]
369 function show_series
() {
370 local series start base_sym base
371 test $# -le 1 || usage
372 series
=$
(get_series
1 "$@")
374 start
=$
(git show-ref
--verify --hash "$(start_reference "$series")")
376 echo "Commits in series $series:"
378 git
--no-pager log
--oneline "$start..$(head_reference "$series")" --
380 base_sym
=$
(git symbolic-ref
"$(base_reference "$series")")
381 base
=$
(git show-ref
--verify --hash "$base_sym")
382 if [[ $base = $start ]]; then
383 echo "${GREEN}Your series is still on top of its base $base_sym.${NORMAL}"
385 echo "${RED}The base $base_sym of your series has changed!${NORMAL}"
386 echo "Changes in $base_sym since last rebase:"
387 git
--no-pager log
--oneline --left-right "$start...$base" --
391 # usage: rebase [-i] [<series>]
392 function rebase_series
() {
393 local series
head start_ref start base_sym rebase_ref
394 test $# -le 1 || usage
395 series
=$
(get_series
1 "$@")
397 head=$
(head_reference
"$series")
398 start_ref
=$
(start_reference
"$series")
399 start
=$
(git show-ref
--verify --hash "$start_ref")
400 base_sym
=$
(git symbolic-ref
"$(base_reference "$series")")
401 rebase_ref
=$
(rebase_reference
"$series")
403 if [[ -n $interactive ]]; then
404 git rebase
-i "$start" "$series"
408 if git show-ref
--quiet --verify "$rebase_ref"; then
409 if git merge-base
--is-ancestor "$base_sym" "$head"; then
410 echo "It seems, you finished the last rebase successfully. Cleaning up."
411 git update-ref
-d "$rebase_ref"
412 git update-ref
"$start_ref" "$base_sym" "$start"
415 if [[ -d "$GIT_DIR/rebase-merge" ||
-d "$GIT_DIR/rebase-apply" ]]; then
416 echo "A rebase for series $series was not finished."
417 echo "It seems to be still in progress. Please rerun"
418 echo " $0 rebase $series"
419 echo "after you have either finished or aborted the rebase to sort"
420 echo "out the state of the series."
423 echo "It seems, you aborted it. Cleaning up."
424 git update-ref
-d "$rebase_ref"
425 if ! git merge-base
--is-ancestor "$start" "$head"; then
426 echo "(Though the state looks weird.)"
431 git update-ref
"$rebase_ref" "$base_sym" ""
432 echo "Starting rebase of series $series onto $base_sym."
434 if git rebase
--onto "$base_sym" "$start" "$series"; then
435 if git merge-base
--is-ancestor "$base_sym" "$head"; then
436 echo "Rebase finished successfully."
437 git update-ref
-d "$rebase_ref"
438 git update-ref
"$start_ref" "$base_sym" "$start"
440 echo "Rebase still in progress?! Please rerun"
441 echo " $0 rebase $series"
442 echo "after you have either finished or aborted the rebase to sort"
443 echo "out the state of the series."
446 echo "Rebase still in progress. Please rerun"
447 echo " $0 rebase $series"
448 echo "after you have either finished or aborted the rebase to sort"
449 echo "out the state of the series."
453 # usage: checkout <series-commit>
454 function checkout
() {
456 test $# -eq 1 || usage
457 commit
=$
(rev_parse
"$1")
459 git checkout
"$commit" --
462 function get_status
() {
463 local series start base_sym base
465 start
=$
(git show-ref
--verify --hash "$(start_reference "$series")")
466 base_sym
=$
(git symbolic-ref
"$(base_reference "$series")")
467 base
=$
(git show-ref
--verify --hash "$base_sym")
469 if ! git show-ref
--verify --quiet "$(head_reference "$series")"; then
470 echo "${RED}Missing head!${NORMAL} Did you delete the branch?"
471 elif git show-ref
--verify --quiet "$(rebase_reference "$series")"; then
472 echo "${RED}Rebase in progress!${NORMAL} Please finish it."
473 elif [[ $base = $start ]]; then
474 echo "${GREEN}Still on top of $base_sym.${NORMAL}"
476 echo "${YELLOW}Needs rebase onto $base_sym!${NORMAL}"
482 local series_list series pad current
483 test $# -eq 0 || usage
485 series_list
=$
(all_series
)
487 # determine length of longest series
489 for series
in $series_list; do
490 if [[ ${#series} -gt $pad ]]; then
495 current
=$
(current_series
2>/dev
/null ||
:)
496 for series
in $series_list; do
497 #show_series "$series"
498 local status is_current commits
499 status
=$
(get_status
"$series")
502 if [[ $current = $series ]]; then
506 commits
=$
(get_commitcount
"$series" 2>/dev
/null ||
echo "??")
507 if [[ $commits = 1 ]]; then
508 commits
="$commits commit. "
510 commits
="$commits commits."
513 printf '%c %-*s' "$is_current" "$pad" "$series"
514 printf ' %12s ' "$commits"
515 printf '%s\n' "$status"
519 # usage: update-base <new-base> [<series>]
520 function update_base
() {
522 test $# -ge 1 || usage
523 test $# -le 2 || usage
525 series
=$
(get_series
2 "$@")
528 ensure_sym
"$newbase"
530 # Update the symbolic base reference with the provided value
531 local base_ref base_sym
532 base_ref
=$
(base_reference
"$series")
533 base_sym
=$
(git rev-parse
--symbolic-full-name "$newbase")
534 git symbolic-ref
"$base_ref" "$base_sym"
536 show_series
"$series"
539 # usage: update-start <new-start> [<series>]
540 function update_start
() {
541 local newstart series
542 test $# -ge 1 || usage
543 test $# -le 2 || usage
545 series
=$
(get_series
2 "$@")
549 head=$
(head_reference
"$series")
550 ensure_rev
"$newstart"
551 ensure_ancestor
"$newstart" "$head"
553 # Update the start reference with the provided value
554 local start_ref start
555 start_ref
=$
(start_reference
"$series")
556 start
=$
(git show-ref
--verify --hash "$start_ref")
557 git update-ref
"$start_ref" "$newstart" "$start"
559 show_series
"$series"
562 # usage: merged [<series>]
563 function merged_series
() {
564 local series
head start_ref start base_sym merge_base
565 test $# -le 1 || usage
566 series
=$
(get_series
1 "$@")
568 head=$
(head_reference
"$series")
569 start_ref
=$
(start_reference
"$series")
570 start
=$
(git show-ref
--verify --hash "$start_ref")
571 base_sym
=$
(git symbolic-ref
"$(base_reference "$series")")
573 merge_base
=$
(git merge-base
"$head" "$base_sym")
574 git update-ref
"$start_ref" "$merge_base" "$start"
577 # usage: merge [<series>]
578 function merge_series
() {
579 local series start_ref
581 test $# -le 1 || usage
582 series
=$
(get_series
1 "$@")
584 # make sure HEAD forked from series
585 start_ref
=$
(start_reference
"$series")
586 ensure_ancestor
"$start_ref" HEAD
588 echo "Integrating HEAD into $series"
589 git rebase HEAD
"$series"
592 # usage: rev-parse <series-commit>
593 function rev_parse
() {
595 local commit series addendum count
596 test $# -eq 1 || usage
599 [[ $commit =~ ^
([^^~
+:]*)(.
*) ]]
600 series
=${BASH_REMATCH[1]}
601 addendum
=${BASH_REMATCH[2]}
603 if [[ -z $series ]]; then
604 series
=$
(current_series
)
606 ensure_series_exists
"$series"
608 if [[ -n $addendum ]]; then
609 if [[ $addendum =~ ^
([0-9~^
]*)$
]]; then
611 commit
="$series${BASH_REMATCH[1]}"
612 elif [[ $addendum =~ ^\
+([0-9]+)$
]]; then
613 # <series>+<n>: n-th commit in series
614 count
=$
(get_commitcount
"$series")
615 count
=$
((count-
${BASH_REMATCH[1]}))
616 commit
="$series~$count"
617 elif [[ $addendum =~ ^
:([0-9]+)$
]]; then
618 # <series>:<n>: n-th patch in series
619 # (don't count fixups/squashs)
621 count
=$
(get_commitcount
"$series")
623 while [[ $n -gt 0 ]]; do
625 commit
="$series~$count"
626 title
=$
(git log
--format=format
:%s
"$commit" --)
627 if [[ ! $title =~ ^
(squash|fixup
)!\
]]; then
632 echo "Cannot parse series-commit" >&2
641 while [[ $# -gt 0 ]]; do case $1 in
642 --interactive) interactive
=t
;;
643 --onto=*) onto
=${1#*=};;
648 if [[ $# -eq 0 ]]; then
657 create
) create_series
"$@";;
658 add
) add_series
"$@";;
659 remove
) remove_series
"$@";;
660 delete
) delete_series
"$@";;
663 rebase
) rebase_series
"$@";;
664 show
) show_series
"$@";;
665 status
) status
"$@";;
667 # Experimental commands
668 update-base
) update_base
"$@";;
669 update-start
) update_start
"$@";;
670 checkout
) checkout
"$@";;
671 merged
) merged_series
"$@";;
672 merge
) merge_series
"$@";;
673 rev-parse
) rev_parse
"$@";;