123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- #!/bin/bash
- # git-graph - print pretty git commit logs
- #
- # Copyright 2016-2018,2020,2023-2024 bill-auger <https://github.com/bill-auger>
- #
- # git-graph is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # git-graph 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-graph. If not, see <http://www.gnu.org/licenses/>.
- # USAGE: git-graph [ -a | -n <N_COMMITS> | -u ] [ branch_name | tag_name | commit_id | file ]
- ## configuration ##
- # PUB_BRANCH eg: 'master', 'upstream/stable-1.0' - is remote tracking branch, if empty
- PUB_BRANCH=
- ## constants ##
- readonly DEF_HASH_LEN=7 # git may extend to the minimum unique prefix
- readonly JOIN_CHAR='~'
- readonly HRULE_CHAR='-'
- readonly GRAPH_REGEX="(.+)"
- readonly ID_REGEX="(.+)"
- readonly DATE_REGEX="(.+)"
- readonly AUTHOR_REGEX="(.+)"
- readonly SIG_REGEX="\[(([^\(\)]*) + *\(?.*\)? *<.*>|)\]"
- readonly STAT_REGEX="\[(.)\]"
- readonly MSG_REGEX="(.*)"
- readonly REFS_REGEX="\((.*)\)"
- readonly GIT_LOG_FMT="%%h${JOIN_CHAR}%%ad${JOIN_CHAR}%%an${JOIN_CHAR}[%%GS]${JOIN_CHAR}[%%G?]${JOIN_CHAR}%%s${JOIN_CHAR}(%%D)"
- readonly GIT_LOG_CMD_FMT="git log --graph --date=short -n %d --pretty=format:$GIT_LOG_FMT --abbrev=${DEF_HASH_LEN}"
- readonly LOG_REGEX="^${GRAPH_REGEX} ${ID_REGEX}${JOIN_CHAR}${DATE_REGEX}${JOIN_CHAR}${AUTHOR_REGEX}${JOIN_CHAR}${SIG_REGEX}${JOIN_CHAR}${STAT_REGEX}${JOIN_CHAR}${MSG_REGEX}${JOIN_CHAR}${REFS_REGEX}$"
- readonly CWHITE='\033[0;37m'
- readonly CGREEN='\033[0;32m'
- readonly CYELLOW='\033[0;33m'
- readonly CRED='\033[0;31m'
- readonly CAQUA='\033[1;36m'
- readonly CEND='\033[0m'
- readonly CGOOD=$CGREEN
- readonly CUNKNOWN=$CYELLOW
- readonly CEXPIRED=$CYELLOW
- readonly CBAD=$CRED
- readonly CNONE=$CWHITE
- readonly HASH_COLOR=$CNONE
- readonly DATE_COLOR=$CNONE
- readonly AUTHOR_COLOR=$CNONE
- readonly MSG_COLOR=$CNONE
- readonly REF_COLOR=$CAQUA
- readonly REF_ERR_MSG="no such ref or file:"
- declare -i USE_ANSI_COLOR=1 # (deferred)
- declare -i N_COMMITS=12 # (deferred)
- declare -i HIDE_MERGED=0 # (deferred)
- REF= # (deferred)
- FILE= # (deferred)
- ## variables ##
- declare -a Graphs=()
- declare -a Ids=()
- declare -a Dates=()
- declare -a Authors=()
- declare -a Sigs=()
- declare -a Stats=()
- declare -a Msgs=()
- declare -a Refs=()
- declare -a SigColors=()
- declare -i AuthorW=0
- declare -i NCommits=0
- ## helpers ##
- GetUpstreamBranch() # ( local_branch )
- {
- local local_branch=$1
- git rev-parse --abbrev-ref $local_branch@{upstream} 2> /dev/null
- }
- GetCurrentBranch()
- {
- git rev-parse --abbrev-ref HEAD
- }
- DoesBranchExist() # ( branch_name )
- {
- local branch_name=$1
- [[ "$branch_name" && "$(git branch --all --list $branch_name)" ]]
- }
- DoesTagExist() # ( tag_name )
- {
- local tag_name=$1
- [[ "$tag_name" && "$(git tag | grep -G "$tag_name$")" ]]
- }
- DoesCidExist() # ( tag_name )
- {
- local commit_id=$1
- [[ "$commit_id" ]] && git rev-parse --verify ${commit_id}^{commit} &> /dev/null
- }
- ValidateRef() # ( ref ) # where param is a branch_name, tag_name, commit_id
- {
- local ref=$1
- DoesBranchExist $ref || DoesTagExist $ref || DoesCidExist $ref || ref=''
- echo $ref ; [[ -n "$ref" ]] ;
- }
- ValidateParam() # ( ref ) # where param is a branch_name, tag_name, commit_id, or file
- {
- local ref=$1
- ref="$( ValidateRef $ref || [[ ! -f "$ref" ]] || echo $ref )"
- echo $ref ; [[ -n "$ref" ]] ;
- }
- IsAncestor() # ( ref_a ref_b )
- {
- local ref_a=$1
- local ref_b=$2
- git merge-base --is-ancestor $ref_a $ref_b
- }
- Ancestor() # ( ref_a ref_b )
- {
- local ref_a=$1
- local ref_b=$2
- git merge-base $ref_a $ref_b
- }
- JoinChars() # ( "a_spaced_string" )
- {
- local a_string="$1"
- echo "${a_string// /$JOIN_CHAR}"
- }
- FilterJoinChars() # ( an_unspaced_string )
- {
- local a_string=$1
- echo $a_string | tr "$JOIN_CHAR" " "
- }
- ## business ##
- Init() # ( cli_args* )
- {
- local arg
- local valid_param
- local valid_ref
- local is_valid_param
- local is_valid_ref
- local is_file
- # parse cli args
- while getopts 'acn:u' arg
- do case "${arg}" in
- a) N_COMMITS="$(git rev-list --count HEAD)" ;;
- c) USE_ANSI_COLOR=0 ;;
- n) N_COMMITS="${OPTARG}" ;;
- u) HIDE_MERGED=1 ;;
- *) echo "Invalid argument: '${arg}'" ;;
- esac
- done
- shift $(( OPTIND - 1 ))
- # process cli args
- valid_param=$(ValidateParam "$1")
- valid_ref=$( ValidateRef "$1")
- is_valid_param=$( [[ -n "$valid_param" ]] ; echo $((!$?)) ; )
- is_valid_ref=$( [[ -n "$valid_ref" ]] ; echo $((!$?)) ; )
- is_file=$( (( is_valid_param && ! is_valid_ref )) ; echo $((!$?)) ; )
- REF=$( (( is_file || ! $# )) && echo HEAD || echo $valid_ref)
- FILE=$((( is_file )) && echo $valid_param || echo $2 )
- PUB_BRANCH=${PUB_BRANCH:-$(GetUpstreamBranch $(GetCurrentBranch))}
- readonly USE_ANSI_COLOR
- readonly N_COMMITS
- readonly HIDE_MERGED
- readonly REF
- readonly FILE
- readonly PUB_BRANCH
- # echo "is_valid_param=$is_valid_param is_valid_ref=$is_valid_ref is_file=$is_file REF=$REF FILE=$FILE" # DEBUG
- (( ! $# )) || (( is_valid_param )) || ! echo "$REF_ERR_MSG $1"
- }
- CompileResults()
- {
- local log_data graph id date author sig stat msg ref
- # reset data
- Graphs=() Ids=() Dates=() Authors=() Sigs=() Stats=() Msgs=() Refs=() SigColors=()
- # compile results
- while read -r log_data
- do [[ $log_data =~ $LOG_REGEX ]] || continue
- # TODO: graph colors and fork/merge node lines
- # printf "log_data='%s'\n" "$log_data" ; [[ $log_data =~ $LOG_REGEX ]] && printf "graph='%s'\n" "${BASH_REMATCH[1]}" || printf "graph NFG='%s'\n" "$log_data"
- graph=${BASH_REMATCH[ 1]} ; Graphs=( ${Graphs[*]} $(JoinChars "$graph" )) ;
- id=${BASH_REMATCH[ 2]} ; Ids=( ${Ids[*]} $(JoinChars "$id" )) ;
- date=${BASH_REMATCH[ 3]} ; Dates=( ${Dates[*]} $(JoinChars "$date" )) ;
- author=${BASH_REMATCH[ 4]} ; Authors=(${Authors[*]} $(JoinChars "$author")) ;
- sig=${BASH_REMATCH[ 6]} ; Sigs=( ${Sigs[*]} $(JoinChars "$sig" )) ;
- stat=${BASH_REMATCH[ 7]} ; Stats=( ${Stats[*]} $(JoinChars "$stat" )) ;
- msg=${BASH_REMATCH[ 8]} ; Msgs=( ${Msgs[*]} $(JoinChars "$msg" )) ;
- ref=${BASH_REMATCH[ 9]} ; Refs=( ${Refs[*]} $(JoinChars "$ref" )) ;
- [[ -n "$msg" ]] || Msgs=( ${Msgs[*]} "<EMPTY>" )
- [[ "$stat" == 'E' ]] && Sigs=( ${Sigs[*]} "<UNKNOWN>" ) || \
- [[ -n "$sig" ]] || Sigs=( ${Sigs[*]} "$JOIN_CHAR" )
- [[ -n "$ref" ]] || Refs=( ${Refs[*]} "$JOIN_CHAR" )
- (( ${#author} > AuthorW )) && AuthorW=${#author}
- done
- }
- PrintReport() # ( "header" )
- {
- local header="$1"
- local n_results=$(( ${#Ids[*]} ))
- local pad_w=$(( ( -${#header} + ${#Ids[0]} + 1 + ${#Dates[0]} + AuthorW ) / 2 ))
- local pad="$(printf "%${pad_w}s" ' ' | tr ' ' "$HRULE_CHAR")"
- local hrule="|<${pad} ${header} ${pad:$(( pad_w > 0 && ! ( AuthorW % 2 ) ))}>|"
- local result_n graph id date author sig stat msg ref pad
- local has_author_sig sig_color hash_color date_color author_color msg_color ref_color
- # pretty print results
- (( ! HIDE_MERGED )) && echo "${hrule}"
- (( ! n_results )) && sed 's/[^|]/ /g ; s/^| /| <None>/' <<<"${hrule}"
- for (( result_n = 0 ; result_n < n_results ; ++result_n ))
- do graph=${Graphs[$result_n]}
- id=${Ids[$result_n]}
- date=${Dates[$result_n]}
- author=${Authors[$result_n]}
- sig=${Sigs[$result_n]}
- stat=${Stats[$result_n]}
- msg=${Msgs[$result_n]}
- ref=${Refs[$result_n]}
- pad=$(printf "%$(( AuthorW - ${#author} ))s" '')
- if (( USE_ANSI_COLOR ))
- then has_author_sig=$([[ "$author" == "$sig" ]] ; echo $((!$?)) ;)
- sig_color=$(case "$stat" in
- 'G') echo $CGOOD ;; # good signature
- 'X') echo $CEXPIRED ;; # good signature that has expired
- 'U') echo $CGOOD ;; # good signature with unknown trust
- 'E') echo $CUNKNOWN ;; # cannot be checked (e.g. missing key)
- 'B') echo $CBAD ;; # bad signature
- 'Y') echo $CEXPIRED ;; # good signature made by an expired key
- 'R') echo $CBAD ;; # good signature made by a revoked key
- 'N') echo $CNONE ;; # no signature
- esac)
- hash_color=$HASH_COLOR
- date_color=$DATE_COLOR
- author_color=$((( has_author_sig )) && echo $sig_color || echo $AUTHOR_COLOR)
- msg_color=$MSG_COLOR
- ref_color=$REF_COLOR
- fi
- # printf "$graph_color$(FilterJoinChars $graph) $CEND"
- printf "| $hash_color$id$CEND"
- printf " $date_color$date$CEND"
- printf " $author_color%s$CEND" "$(FilterJoinChars $author)"
- printf " $pad| $msg_color%s$CEND" "$(FilterJoinChars $msg )"
- [[ "$ref" != "$JOIN_CHAR" ]] && printf " $ref_color($(FilterJoinChars $ref))$CEND"
- [[ "$sig" != "$JOIN_CHAR" ]] && printf " $sig_color[$(FilterJoinChars $sig)]$CEND"
- printf "\n"
- done
- NCommits=$(( NCommits + ${#Ids[*]} ))
- }
- Main()
- {
- local ancestor=$(Ancestor $PUB_BRANCH $REF)
- local log_cmd header
- if [[ -z "$PUB_BRANCH" ]]
- then log_cmd="$(printf "$GIT_LOG_CMD_FMT" $N_COMMITS)" header='NO UPSTREAM'
- CompileResults < <($log_cmd $REF $FILE ; echo ;) ; PrintReport "${header}" ;
- elif ! IsAncestor $PUB_BRANCH $REF && [[ -z ${ancestor:-} ]]
- then log_cmd="$(printf "$GIT_LOG_CMD_FMT" $N_COMMITS)" header='UNRELATED'
- CompileResults < <($log_cmd $REF $FILE ; echo ;) ; PrintReport ${header} ;
- else log_cmd="$(printf "$GIT_LOG_CMD_FMT" $N_COMMITS)" header='UNMERGED'
- CompileResults < <($log_cmd $ancestor..$REF $FILE ; echo ;) ; PrintReport ${header} ;
- if (( ! HIDE_MERGED && NCommits < N_COMMITS ))
- then log_cmd="$(printf "$GIT_LOG_CMD_FMT" $(( N_COMMITS - NCommits )))" header='MERGED'
- CompileResults < <($log_cmd $ancestor $FILE ; echo ;) ; PrintReport ${header} ;
- fi
- fi
- }
- ## main entry ##
- Init "$@" && Main
|