unixorn/git-extra-commands

View on GitHub
bin/git-branch-status

Summary

Maintainability
Test Coverage
#!/usr/bin/env bash

#  git-branch-status - print pretty git branch sync status reports
#
#  Copyright 2011      Jehiah Czebotar     <https://github.com/jehiah>
#  Copyright 2013      Fredrik Strandin    <https://github.com/kd35a>
#  Copyright 2014      Kristijan Novoselić <https://github.com/knovoselic>
#  Copyright 2014-2018 bill-auger          <https://github.com/bill-auger>
#
#  git-branch-status is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License version 3
#  as published by the Free Software Foundation.
#
#  git-branch-status is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License version 3
#  along with git-branch-status.  If not, see <http://www.gnu.org/licenses/>.

#  credits:
#    * original `git rev-list` grepping - by Jehiah Czebotar
#    * "s'all good!" message            - by Fredrik Strandin
#    * ANSI colors                      - by Kristijan Novoselić
#    * various features and maintenance - by bill-auger

#  please direct bug reports, feature requests, or PRs to one of the upstream repos:
#    * https://github.com/bill-auger/git-branch-status/issues/
#    * https://notabug.org/bill-auger/git-branch-status/issues/
#    * https://pagure.io/git-branch-status/issues/


read -r -d '' USAGE <<-'USAGE_MSG'
USAGE:
  git-branch-status
  git-branch-status [ base-branch-name compare-branch-name ]
  git-branch-status [ -a | --all ]
  git-branch-status [ -b | --branch ] [ filter-branch-name ]
  git-branch-status [ -d | --dates ]
  git-branch-status [ -h | --help ]
  git-branch-status [ -l | --local ]
  git-branch-status [ -r | --remotes ]
  git-branch-status [ -v | --verbose ]
EXAMPLES:
  # show only branches for which upstream differs from local
  $ git-branch-status
    | collab-branch  | (behind 1) | (ahead 2) | origin/collab-branch  |
    | feature-branch | (even)     | (ahead 2) | origin/feature-branch |
    | master         | (behind 1) | (even)    | origin/master         |
  # compare two arbitrary branches (either local and either remote)
  $ git-branch-status local-arbitrary-branch fork/arbitrary-branch
    | local-arbitrary-branch | (even)     | (ahead 1) | fork/arbitrary-branch  |
  $ git-branch-status fork/arbitrary-branch local-arbitrary-branch
    | fork/arbitrary-branch  | (behind 1) | (even)    | local-arbitrary-branch |
  # show all branches - including those synchronized, non-tracking, or not checked-out
  $ git-branch-status -a
  $ git-branch-status --all
    | master         | (even)     | (ahead 1) | origin/master             |
    | tracked-branch | (even)     | (even)    | origin/tracked-branch     |
    | (no local)     | n/a        | n/a       | origin/untracked-branch   |
    | local-branch   | n/a        | n/a       | (no upstream)             |
    | master         | (behind 1) | (ahead 1) | a-remote/master           |
    | (no local)     | n/a        | n/a       | a-remote/untracked-branch |
  # show the current branch
  $ git-branch-status -b
  $ git-branch-status --branch
    | current-branch | (even) | (ahead 2) | origin/current-branch |
  # show a specific branch
  $ git-branch-status          specific-branch
  $ git-branch-status -b       specific-branch
  $ git-branch-status --branch specific-branch
    | specific-branch | (even) | (ahead 2) | origin/specific-branch |
  # show the timestamp of each HEAD
  $ git-branch-status -d
  $ git-branch-status --dates
    | 1999-12-31 master | (behind 2) | (even) | 2000-01-01 origin/master |
  # print this usage message
  $ git-branch-status -h
  $ git-branch-status --help
      "prints this usage message"
  # show all local branches - including those synchronized or non-tracking
  $ git-branch-status -l
  $ git-branch-status --local
    | master         | (even)     | (ahead 1) | origin/master         |
    | tracked-branch | (even)     | (even)    | origin/tracked-branch |
    | local-branch   | n/a        | n/a       | (no upstream)         |
  # show all remote branches - including those not checked-out
  $ git-branch-status -r
  $ git-branch-status --remotes
    | master     | (behind 1) | (even) | a-remote/master           |
    | (no local) | n/a        | n/a    | a-remote/untracked-branch |
  # show all branches with timestamps (like -a -d)
  $ git-branch-status -v
  $ git-branch-status --verbose
    | 1999-12-31 local   | n/a        | n/a    | (no upstream)             |
    | 1999-12-31 master  | (behind 1) | (even) | 2000-01-01 origin/master  |
    | 1999-12-31 tracked | (even)     | (even) | 2000-01-01 origin/tracked |
USAGE_MSG


## constants ##

readonly INNER_PADDING_W=13 # '| ' + ' | ' + ' | ' + ' | ' + ' |'
readonly MARGIN_PAD_W=2
readonly MARGINS_PAD_W=$(( $MARGIN_PAD_W * 2 ))
readonly ALL_PADDING_W=$(( $INNER_PADDING_W + $MARGINS_PAD_W ))
readonly MAX_DIVERGENCE_W=12 # e.g. "(behind 999)"
readonly MIN_TTY_W=60 # ASSERT: (0 < ($ALL_PADDING_W + (MAX_DIVERGENCE_W * 2)) <= $MIN_TTY_W)
readonly MARGIN_PAD=$(printf "%$(( $MARGIN_PAD_W ))s")
readonly CWHITE='\033[0;37m'
readonly CRED='\033[0;31m'
readonly CGREEN='\033[0;32m'
readonly CYELLOW='\033[1;33m'
readonly CBLUE='\033[1;34m'
readonly CEND='\033[0m'
readonly CDEFAULT=$CWHITE
readonly CTRACKING=$CBLUE
readonly CAHEAD=$CYELLOW
readonly CBEHIND=$CRED
readonly CEVEN=$CGREEN
readonly CNOUPSTREAM=$CRED
readonly CNOLOCAL=$CRED
readonly HRULE_CHAR='-'
readonly JOIN_CHAR='~'
readonly JOIN_REGEX="s/$JOIN_CHAR/ /g"
readonly TRIM_REGEX="s/.*\(.\{$MAX_DIVERGENCE_W\}\)$/\1/"
readonly STAR='*'
readonly DELIM='|'
readonly NO_UPSTREAM="(no${JOIN_CHAR}upstream)"
readonly NO_LOCAL="(no${JOIN_CHAR}local)"
readonly NOT_REPO_MSG="Not a git repo"
readonly BARE_REPO_MSG="Bare repo"
readonly NO_COMMITS_MSG="No commits"
readonly TTY_W_MSG="TTY must be wider than $MIN_TTY_W chars"
readonly INVALID_BRANCH_MSG="No such branch:"
readonly INVALID_LOCAL_BRANCH_MSG="No such local branch:"
readonly NO_RESULTS_MSG="Nothing to compare"
readonly NO_REFS_MSG="(No data)"
readonly LOCALS_SYNCED_MSG="All tracking branches are synchronized with their upstreams"
readonly BRANCH_SYNCED_MSG="This tracking branch is synchronized with it's upstream"
readonly UNTRACKED_SYNCHED_MSG="These branches are synchronized with no tracking relationship"
readonly REMOTES_SYNCED_MSG="All local branches with corresponding names on this remote are synchronized with that remote"
readonly EXIT_SUCCESS=0
readonly EXIT_FAILURE=1
# readonly SHOW_DATES      # Init()
# readonly SHOW_ALL_SYNCED # Init()
# readonly SHOW_ALL_LOCAL  # Init()
# readonly SHOW_ALL_REMOTE # Init()
# readonly FILTER_BRANCH   # Init()
# readonly COMPARE_BRANCH  # Init()


## variables ##

WereAnyDivergences=0
WereAnyCompared=0
LocalW=0
BehindW=0
AheadW=0
RemoteW=0
declare -a LocalMsgs=()
declare -a BehindMsgs=()
declare -a AheadMsgs=()
declare -a RemoteMsgs=()
declare -a LocalColors=()
declare -a BehindColors=()
declare -a AheadColors=()
declare -a RemoteColors=()


## helpers ##

function AssertIsValidRepo
{
  [ "$(git rev-parse --is-inside-work-tree 2> /dev/null)" == 'true' ] || \
  ! (( $(AssertIsNotBareRepo) ))                                      && echo 1 || echo 0
}

function AssertHasCommits
{
  [ "$(git cat-file -t HEAD 2> /dev/null)" ] && echo 1 || echo 0
}

function AssertIsNotBareRepo
{
  [ "$(git rev-parse --is-bare-repository 2> /dev/null)" != 'true' ] && echo 1 || echo 0
}

function GetRefs # (refs_dir)
{
  local refs_dir=$1

  git for-each-ref --format="%(refname:short) %(upstream:short)" "$refs_dir" 2> /dev/null
}

function GetLocalRefs
{
  GetRefs refs/heads
}

function GetRemoteRefs # (remote_repo_name)
{
  local remote_repo=$1

  GetRefs "refs/remotes/$remote_repo"
}

function GetStatus # (base_commit compare_commit)
{
  local base_commit=$1
  local compare_commit=$2

  git rev-list --left-right ${base_commit}...${compare_commit} -- 2>/dev/null
}

function GetCurrentBranch
{
  git rev-parse --abbrev-ref HEAD
}

function GetUpstreamBranch # (local_branch)
{
  local local_branch=$1

  git rev-parse --abbrev-ref $local_branch@{upstream} 2> /dev/null
}

function IsCurrentBranch # (branch_name)
{
  local branch=$1
  local this_branch=$(AppendHeadDate $branch)
  local current_branch=$(AppendHeadDate $(GetCurrentBranch))

  [ "$this_branch" == "$current_branch" ] && echo 1 || echo 0
}

function IsLocalBranch # (branch_name)
{
  local branch=$1
  local is_local_branch=$(git branch -a | grep -E "^.* $branch$")

  [ "$is_local_branch" ] && echo 1 || echo 0
}

function IsTrackedBranch # (base_branch_name compare_branch_name)
{
  local base_branch=$1
  local compare_branch=$2
  local upstream_branch=$(GetUpstreamBranch $base_branch)

  [ "$compare_branch" == "$upstream_branch" ] && echo 1 || echo 0
}

function DoesBranchExist # (branch_name)
{
  local branch=$1
  local is_known_branch=$(git branch -a | grep -E "^.* (remotes\/)?$branch$")

  [ "$is_known_branch" ] && echo 1 || echo 0
}

function AppendHeadDate # (commit_ref)
{
  local commit=$1
  local author_date=$(git log -n 1 --format=format:"%ai" $commit 2> /dev/null)
  local date=''

  (($SHOW_DATES)) && [ "$author_date" ] && date="${author_date:0:10}$JOIN_CHAR"

  echo $date$commit
}

function CurrentTtyW
{
  local tty_dims=$(stty -F /dev/tty size 2> /dev/null || stty -f /dev/tty size 2> /dev/null)
  local tty_w=$(echo $tty_dims | cut -d ' ' -f 2)

  (( $tty_w )) && echo "$tty_w" || echo "$MIN_TTY_W"
}

function PrintHRule # (rule_width)
{
  local rule_w=$1
  local h_rule="$(dd if=/dev/zero bs=$rule_w count=1 2> /dev/null | tr '\0' $HRULE_CHAR)"

  echo "$MARGIN_PAD$h_rule"
}

function EXIT # (exit_msg exit_status)
{
  local exit_msg=$1
  local exit_status=$2

  case "$exit_status" in
       $EXIT_SUCCESS      ) echo "$exit_msg"        ; exit $EXIT_SUCCESS ;;
       $EXIT_FAILURE | '' ) echo "fatal: $exit_msg" ; exit $EXIT_FAILURE ;;
  esac
}


## business ##

function Init # (cli_args)
{
  local show_dates=0
  local show_all=0
  local show_all_synced=0
  local show_all_local=0
  local show_all_remote=0
  local branch_a=''
  local branch_b=''

  # parse CLI switches
  case "$1" in
       '-a'|'--all'     ) show_all=1                                                ;;
       '-b'|'--branch'  ) [ "$2" ] && branch_a="$2" || branch_a=$(GetCurrentBranch) ;;
       '-d'|'--dates'   ) show_dates=1                                              ;;
       '-h'|'--help'    ) EXIT "$USAGE" $EXIT_SUCCESS                               ;;
       '-l'|'--local'   ) show_all_local=1  ; show_all_synced=1 ;                   ;;
       '-r'|'--remotes' ) show_all_remote=1 ; show_all_synced=1 ;                   ;;
       '-v'|'--verbose' ) show_all=1        ; show_dates=1      ;                   ;;
       *                ) branch_a="$1"     ; branch_b="$2"     ;                   ;;
  esac

  # sanity checks
  (( $(AssertIsValidRepo  )       ))                                        || EXIT "$NOT_REPO_ERR"
  (( $(AssertIsNotBareRepo)       ))                                        || EXIT "$BARE_REPO_MSG"
  (( $(AssertHasCommits   )       ))                                        || EXIT "$NO_COMMITS_MSG"
  (( $(CurrentTtyW) >= $MIN_TTY_W ))                                        || EXIT "$TTY_W_MSG"
  [ -z "$branch_a" ] || (($(DoesBranchExist $branch_a)))                    || EXIT "$INVALID_BRANCH_MSG '$branch_a'"
  [ -z "$branch_b" ] || (($(DoesBranchExist $branch_b)))                    || EXIT "$INVALID_BRANCH_MSG '$branch_b'"
  [ -z "$branch_a" ] || (($(IsLocalBranch   $branch_a))) || [ "$branch_b" ] || EXIT "$INVALID_LOCAL_BRANCH_MSG '$branch_a'"
  [ -z "$branch_a" ] || show_all_local=1 # force "no upstream" message for non-tracking branches

  readonly SHOW_DATES=$show_dates
  readonly SHOW_ALL_SYNCED=$(( $show_all + $show_all_synced )) # show branches that are synchronized with their counterparts
  readonly SHOW_ALL_LOCAL=$((  $show_all + $show_all_local  )) # show local branches that are not tracking any upstream
  readonly SHOW_ALL_REMOTE=$(( $show_all + $show_all_remote )) # show all remote branches
  readonly FILTER_BRANCH=$branch_a
  readonly COMPARE_BRANCH=$branch_b
}

function GenerateReports
{
  if   [ "$COMPARE_BRANCH" ]
  then CustomReport $(IsTrackedBranch $FILTER_BRANCH $COMPARE_BRANCH)
  else (( !(( $SHOW_ALL_REMOTE )) || (( $SHOW_ALL_LOCAL )) )) && LocalReport
       (( $SHOW_ALL_REMOTE                                 )) && RemoteReport
  fi
}

function CustomReport # (is_tracked_branch)
{
  local is_tracked_branch=$1
  local synchronized_msg
  (($is_tracked_branch)) && synchronized_msg="$BRANCH_SYNCED_MSG" || synchronized_msg="$UNTRACKED_SYNCHED_MSG"

  # compare sync status of arbitrary branches per cli args
  GenerateReport $FILTER_BRANCH $COMPARE_BRANCH
  PrintReport "$FILTER_BRANCH <-> $COMPARE_BRANCH" "$synchronized_msg"
}

function LocalReport
{
  local synchronized_msg
  [ "$FILTER_BRANCH" ] && synchronized_msg="$BRANCH_SYNCED_MSG" || synchronized_msg="$LOCALS_SYNCED_MSG"

  # compare sync status of local branches to their upstreams
  while read local upstream ; do GenerateReport $local $upstream ; done < <(GetLocalRefs) ;
  PrintReport "local <-> upstream" "$synchronized_msg"
}

function RemoteReport
{
  local synchronized_msg="$REMOTES_SYNCED_MSG"
  local remote_repo

  # compare sync status of remote branches to local branches with the same names
  for remote_repo in $(git remote)
  do  while read remote_branch
      do    local local_branch=${remote_branch#$remote_repo/}

            GenerateReport $local_branch $remote_branch
      done < <(GetRemoteRefs $remote_repo)

      PrintReport "local <-> $remote_repo" "$synchronized_msg"
  done
}

function GenerateReport # (base_branch compare_branch)
{
  local base_branch=$1
  local compare_branch=$2
  local does_base_branch_exist=$(   DoesBranchExist $base_branch                )
  local does_compare_branch_exist=$(DoesBranchExist $compare_branch             )
  local is_tracked_branch=$(        IsTrackedBranch $base_branch $compare_branch)
  local local_msg   ; local behind_msg   ; local ahead_msg   ; local remote_msg   ;
  local local_color ; local behind_color ; local ahead_color ; local remote_color ;

  # filter heads
  [ "$base_branch" != 'HEAD' ] || return

  # filter branches per CLI arg
  [ -z "$FILTER_BRANCH" -o "$base_branch" == "$FILTER_BRANCH" ] || return

  # parse local<->remote or arbitrary branches sync status
  if   (( ! $does_base_branch_exist )) && (( ! $does_compare_branch_exist )) ; then return ;
  elif ((   $does_base_branch_exist )) && ((   $does_compare_branch_exist ))
  then local status=$(GetStatus $base_branch $compare_branch) ; (( ! $? )) || return ;

       local n_behind=$(echo $status | tr " " "\n" | grep -c '^>')
       local n_ahead=$( echo $status | tr " " "\n" | grep -c '^<')
       local n_divergences=$(( $n_behind + $n_ahead ))
       (( $WereAnyDivergences + $n_divergences )) && WereAnyDivergences=1
       WereAnyCompared=1

       # filter branches by status
       (( $SHOW_ALL_SYNCED )) || (( $n_divergences > 0 )) || return

       # set data for branches with remote counterparts or arbitrary branches
       (($is_tracked_branch)) && color=$CTRACKING || color=$CDEFAULT
       local_color=$color
       if   (( $n_behind ))
       then behind_msg="(behind$JOIN_CHAR$n_behind)" ; behind_color=$CBEHIND ;
       else behind_msg="(even)"                      ; behind_color=$CEVEN   ;
       fi
       if   (( $n_ahead ))
       then ahead_msg="(ahead$JOIN_CHAR$n_ahead)"    ; ahead_color=$CAHEAD   ;
       else ahead_msg="(even)"                       ; ahead_color=$CEVEN    ;
       fi
       remote_color=$color
  elif ((   $does_base_branch_exist )) && (( $SHOW_ALL_LOCAL ))
  then # dummy data for local branches with no upstream counterpart
       local_color=$CDEFAULT
       behind_color="$CDEFAULT"  ; behind_msg="n/a"              ;
       ahead_color="$CDEFAULT"   ; ahead_msg="n/a"               ;
       remote_color=$CNOUPSTREAM ; compare_branch="$NO_UPSTREAM" ;
  elif (( ! $does_base_branch_exist )) && (( $SHOW_ALL_REMOTE ))
  then # dummy data for remote branches with no local counterpart
       local_color=$CNOLOCAL     ; base_branch="$NO_LOCAL"       ;
       behind_color="$CDEFAULT"  ; behind_msg="n/a"              ;
       ahead_color="$CDEFAULT"   ; ahead_msg="n/a"               ;
       remote_color=$CDEFAULT
  else return
  fi
  local_msg="$( AppendHeadDate $base_branch         )"
  behind_msg=$( echo $behind_msg | sed "$TRIM_REGEX")
  ahead_msg=$(  echo $ahead_msg  | sed "$TRIM_REGEX")
  remote_msg="$(AppendHeadDate $compare_branch      )"

  # populate lists
  LocalMsgs=(    ${LocalMsgs[@]}    "$local_msg"    )
  BehindMsgs=(   ${BehindMsgs[@]}   "$behind_msg"   )
  AheadMsgs=(    ${AheadMsgs[@]}    "$ahead_msg"    )
  RemoteMsgs=(   ${RemoteMsgs[@]}   "$remote_msg"   )
  LocalColors=(  ${LocalColors[@]}  "$local_color"  )
  BehindColors=( ${BehindColors[@]} "$behind_color" )
  AheadColors=(  ${AheadColors[@]}  "$ahead_color"  )
  RemoteColors=( ${RemoteColors[@]} "$remote_color" )

  # determine uniform column widths
  if [ ${#local_msg}  -gt $LocalW  ] ; then LocalW=${#local_msg}   ; fi ;
  if [ ${#behind_msg} -gt $BehindW ] ; then BehindW=${#behind_msg} ; fi ;
  if [ ${#ahead_msg}  -gt $AheadW  ] ; then AheadW=${#ahead_msg}   ; fi ;
  if [ ${#remote_msg} -gt $RemoteW ] ; then RemoteW=${#remote_msg} ; fi ;
}

function PrintReport # (table_header_line synchronized_msg)
{
  local table_header_line=$1
  local synchronized_msg=$2
  local available_w=$(( $(CurrentTtyW) - $AheadW - $BehindW - $ALL_PADDING_W ))
  local n_results=${#LocalMsgs[@]}
  local are_all_in_sync
  local result_n
  (( $WereAnyCompared )) && !(( $WereAnyDivergences )) && are_all_in_sync=1 || are_all_in_sync=0

  # truncate column widths to fit
  while (( $LocalW + $RemoteW > $available_w ))
  do    (( $LocalW >= $RemoteW )) && LocalW=$((  $LocalW  - 1 ))
        (( $LocalW <= $RemoteW )) && RemoteW=$(( $RemoteW - 1 ))
  done

  # print comparison header
  if   (( $are_all_in_sync ))
  then printf "\n$CGREEN$MARGIN_PAD$table_header_line$CEND\n"
  elif (( $n_results > 0 ))
  then printf "\n$MARGIN_PAD$table_header_line\n"
  else printf "\n$CRED$MARGIN_PAD$table_header_line$CEND$MARGIN_PAD"
  fi

  # pretty print divergence results
  if   (( $n_results > 0 ))
  then local rule_w=$(( $LocalW + $BehindW + $AheadW + $RemoteW + $INNER_PADDING_W ))

       PrintHRule $rule_w
       for (( result_n = 0 ; result_n < $n_results ; ++result_n ))
       do  PrintReportLine
       done
       PrintHRule $rule_w
  else ([ -z "$(GetRemoteRefs $remote_repo)" ] && printf "$CRED$NO_REFS_MSG$CEND\n") || \
       (!(( $are_all_in_sync ))                && echo "$NO_RESULTS_MSG"           )
  fi

  # print "synchronized" message if all compared upstreams had no divergence
  if   (( $are_all_in_sync ))
  then local l_border="$DELIM " ; local r_border=" $DELIM" ;
       local borders_pad_w=$(( ${#l_border} + ${#r_border} ))
       local wrap_w=$(( $(CurrentTtyW) - $MARGINS_PAD_W - $borders_pad_w ))
       local line
       local rule_w=0

       # wrap message and determine box width
       synchronized_msg=$(echo "$synchronized_msg" | fold -s -w $wrap_w | tr ' ' "$JOIN_CHAR")
       for line in $synchronized_msg
       do  line=${line/%$JOIN_CHAR/}
           [ ${#line} -gt $rule_w ] && rule_w=${#line}
       done
       rule_w=$(( $rule_w + $MARGINS_PAD_W ))

       # display message
       PrintHRule $rule_w
       for line in $synchronized_msg
       do  line=${line/%$JOIN_CHAR/}
           local pad_w=$(( $rule_w - ${#line} - $MARGINS_PAD_W ))
           local line_fmt="$MARGIN_PAD$l_border$CEVEN$line$CEND%$(( $pad_w ))s$r_border"
           printf "$line_fmt\n" | sed "$JOIN_REGEX"
       done
       PrintHRule $rule_w
  fi

  Reset
}

function PrintReportLine
{
  # select data
  local local_msg=$( echo ${LocalMsgs[ $result_n]}         | sed "$JOIN_REGEX")
  local behind_msg=$(echo ${BehindMsgs[$result_n]:$trim_w} | sed "$JOIN_REGEX")
  local ahead_msg=$( echo ${AheadMsgs[ $result_n]:$trim_w} | sed "$JOIN_REGEX")
  local remote_msg=$(echo ${RemoteMsgs[$result_n]}         | sed "$JOIN_REGEX")
  local end_msg ; local star ;
  local_msg="${local_msg:0:$LocalW}"
  remote_msg="${remote_msg:0:$RemoteW}"
  local local_color="${LocalColors[$result_n]}"
  local behind_color="${BehindColors[$result_n]}"
  local ahead_color="${AheadColors[$result_n]}"
  local remote_color="${RemoteColors[$result_n]}"

  # calculate column offsets
  local local_offset=1
  local behind_offset=$(( $LocalW  - ${#local_msg}  ))
  local ahead_offset=$((  $BehindW - ${#behind_msg} ))
  local remote_offset=$(( $AheadW  - ${#ahead_msg}  ))
  local end_offset=$((    $RemoteW - ${#remote_msg} ))

  # build output messages and display
  (( $(IsCurrentBranch $local_msg) )) && star=$STAR || star=" "
  local_msg="%$((  $local_offset  ))s$star$(echo -e $DELIM $local_color$local_msg$CEND  )"
  behind_msg="%$(( $behind_offset ))s $(    echo -e $DELIM $behind_color$behind_msg$CEND)"
  ahead_msg="%$((  $ahead_offset  ))s $(    echo -e $DELIM $ahead_color$ahead_msg$CEND  )"
  remote_msg="%$(( $remote_offset ))s $(    echo -e $DELIM $remote_color$remote_msg$CEND)"
  end_msg="%$((    $end_offset    ))s $DELIM"
  printf "$local_msg$behind_msg$ahead_msg$remote_msg$end_msg\n"
}

function Reset
{
  WereAnyDivergences=0
  WereAnyCompared=0
  LocalW=0
  BehindW=0
  AheadW=0
  RemoteW=0
  LocalMsgs=()
  BehindMsgs=()
  AheadMsgs=()
  RemoteMsgs=()
  LocalColors=()
  BehindColors=()
  AheadColors=()
  RemoteColors=()
}

## main entry ##

Init $*
GenerateReports