git-graph 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. #!/bin/bash
  2. # git-graph - print pretty git commit logs
  3. #
  4. # Copyright 2016-2018,2020,2023-2024 bill-auger <https://github.com/bill-auger>
  5. #
  6. # git-graph is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # git-graph is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License version 3
  17. # along with git-graph. If not, see <http://www.gnu.org/licenses/>.
  18. # USAGE: git-graph [ -a | -n <N_COMMITS> | -u ] [ branch_name | tag_name | commit_id | file ]
  19. ## configuration ##
  20. # PUB_BRANCH eg: 'master', 'upstream/stable-1.0' - is remote tracking branch, if empty
  21. PUB_BRANCH=
  22. ## constants ##
  23. readonly DEF_HASH_LEN=7 # git may extend to the minimum unique prefix
  24. readonly JOIN_CHAR='~'
  25. readonly HRULE_CHAR='-'
  26. readonly GRAPH_REGEX="(.+)"
  27. readonly ID_REGEX="(.+)"
  28. readonly DATE_REGEX="(.+)"
  29. readonly AUTHOR_REGEX="(.+)"
  30. readonly SIG_REGEX="\[(([^\(\)]*) + *\(?.*\)? *<.*>|)\]"
  31. readonly STAT_REGEX="\[(.)\]"
  32. readonly MSG_REGEX="(.*)"
  33. readonly REFS_REGEX="\((.*)\)"
  34. readonly GIT_LOG_FMT="%%h${JOIN_CHAR}%%ad${JOIN_CHAR}%%an${JOIN_CHAR}[%%GS]${JOIN_CHAR}[%%G?]${JOIN_CHAR}%%s${JOIN_CHAR}(%%D)"
  35. readonly GIT_LOG_CMD_FMT="git log --graph --date=short -n %d --pretty=format:$GIT_LOG_FMT --abbrev=${DEF_HASH_LEN}"
  36. 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}$"
  37. readonly CWHITE='\033[0;37m'
  38. readonly CGREEN='\033[0;32m'
  39. readonly CYELLOW='\033[0;33m'
  40. readonly CRED='\033[0;31m'
  41. readonly CAQUA='\033[1;36m'
  42. readonly CEND='\033[0m'
  43. readonly CGOOD=$CGREEN
  44. readonly CUNKNOWN=$CYELLOW
  45. readonly CEXPIRED=$CYELLOW
  46. readonly CBAD=$CRED
  47. readonly CNONE=$CWHITE
  48. readonly HASH_COLOR=$CNONE
  49. readonly DATE_COLOR=$CNONE
  50. readonly AUTHOR_COLOR=$CNONE
  51. readonly MSG_COLOR=$CNONE
  52. readonly REF_COLOR=$CAQUA
  53. readonly REF_ERR_MSG="no such ref or file:"
  54. declare -i USE_ANSI_COLOR=1 # (deferred)
  55. declare -i N_COMMITS=12 # (deferred)
  56. declare -i HIDE_MERGED=0 # (deferred)
  57. REF= # (deferred)
  58. FILE= # (deferred)
  59. ## variables ##
  60. declare -a Graphs=()
  61. declare -a Ids=()
  62. declare -a Dates=()
  63. declare -a Authors=()
  64. declare -a Sigs=()
  65. declare -a Stats=()
  66. declare -a Msgs=()
  67. declare -a Refs=()
  68. declare -a SigColors=()
  69. declare -i AuthorW=0
  70. declare -i NCommits=0
  71. ## helpers ##
  72. GetUpstreamBranch() # ( local_branch )
  73. {
  74. local local_branch=$1
  75. git rev-parse --abbrev-ref $local_branch@{upstream} 2> /dev/null
  76. }
  77. GetCurrentBranch()
  78. {
  79. git rev-parse --abbrev-ref HEAD
  80. }
  81. DoesBranchExist() # ( branch_name )
  82. {
  83. local branch_name=$1
  84. [[ "$branch_name" && "$(git branch --all --list $branch_name)" ]]
  85. }
  86. DoesTagExist() # ( tag_name )
  87. {
  88. local tag_name=$1
  89. [[ "$tag_name" && "$(git tag | grep -G "$tag_name$")" ]]
  90. }
  91. DoesCidExist() # ( tag_name )
  92. {
  93. local commit_id=$1
  94. [[ "$commit_id" ]] && git rev-parse --verify ${commit_id}^{commit} &> /dev/null
  95. }
  96. ValidateRef() # ( ref ) # where param is a branch_name, tag_name, commit_id
  97. {
  98. local ref=$1
  99. DoesBranchExist $ref || DoesTagExist $ref || DoesCidExist $ref || ref=''
  100. echo $ref ; [[ -n "$ref" ]] ;
  101. }
  102. ValidateParam() # ( ref ) # where param is a branch_name, tag_name, commit_id, or file
  103. {
  104. local ref=$1
  105. ref="$( ValidateRef $ref || [[ ! -f "$ref" ]] || echo $ref )"
  106. echo $ref ; [[ -n "$ref" ]] ;
  107. }
  108. IsAncestor() # ( ref_a ref_b )
  109. {
  110. local ref_a=$1
  111. local ref_b=$2
  112. git merge-base --is-ancestor $ref_a $ref_b
  113. }
  114. Ancestor() # ( ref_a ref_b )
  115. {
  116. local ref_a=$1
  117. local ref_b=$2
  118. git merge-base $ref_a $ref_b
  119. }
  120. JoinChars() # ( "a_spaced_string" )
  121. {
  122. local a_string="$1"
  123. echo "${a_string// /$JOIN_CHAR}"
  124. }
  125. FilterJoinChars() # ( an_unspaced_string )
  126. {
  127. local a_string=$1
  128. echo $a_string | tr "$JOIN_CHAR" " "
  129. }
  130. ## business ##
  131. Init() # ( cli_args* )
  132. {
  133. local arg
  134. local valid_param
  135. local valid_ref
  136. local is_valid_param
  137. local is_valid_ref
  138. local is_file
  139. # parse cli args
  140. while getopts 'acn:u' arg
  141. do case "${arg}" in
  142. a) N_COMMITS="$(git rev-list --count HEAD)" ;;
  143. c) USE_ANSI_COLOR=0 ;;
  144. n) N_COMMITS="${OPTARG}" ;;
  145. u) HIDE_MERGED=1 ;;
  146. *) echo "Invalid argument: '${arg}'" ;;
  147. esac
  148. done
  149. shift $(( OPTIND - 1 ))
  150. # process cli args
  151. valid_param=$(ValidateParam "$1")
  152. valid_ref=$( ValidateRef "$1")
  153. is_valid_param=$( [[ -n "$valid_param" ]] ; echo $((!$?)) ; )
  154. is_valid_ref=$( [[ -n "$valid_ref" ]] ; echo $((!$?)) ; )
  155. is_file=$( (( is_valid_param && ! is_valid_ref )) ; echo $((!$?)) ; )
  156. REF=$( (( is_file || ! $# )) && echo HEAD || echo $valid_ref)
  157. FILE=$((( is_file )) && echo $valid_param || echo $2 )
  158. PUB_BRANCH=${PUB_BRANCH:-$(GetUpstreamBranch $(GetCurrentBranch))}
  159. readonly USE_ANSI_COLOR
  160. readonly N_COMMITS
  161. readonly HIDE_MERGED
  162. readonly REF
  163. readonly FILE
  164. readonly PUB_BRANCH
  165. # echo "is_valid_param=$is_valid_param is_valid_ref=$is_valid_ref is_file=$is_file REF=$REF FILE=$FILE" # DEBUG
  166. (( ! $# )) || (( is_valid_param )) || ! echo "$REF_ERR_MSG $1"
  167. }
  168. CompileResults()
  169. {
  170. local log_data graph id date author sig stat msg ref
  171. # reset data
  172. Graphs=() Ids=() Dates=() Authors=() Sigs=() Stats=() Msgs=() Refs=() SigColors=()
  173. # compile results
  174. while read -r log_data
  175. do [[ $log_data =~ $LOG_REGEX ]] || continue
  176. # TODO: graph colors and fork/merge node lines
  177. # 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"
  178. graph=${BASH_REMATCH[ 1]} ; Graphs=( ${Graphs[*]} $(JoinChars "$graph" )) ;
  179. id=${BASH_REMATCH[ 2]} ; Ids=( ${Ids[*]} $(JoinChars "$id" )) ;
  180. date=${BASH_REMATCH[ 3]} ; Dates=( ${Dates[*]} $(JoinChars "$date" )) ;
  181. author=${BASH_REMATCH[ 4]} ; Authors=(${Authors[*]} $(JoinChars "$author")) ;
  182. sig=${BASH_REMATCH[ 6]} ; Sigs=( ${Sigs[*]} $(JoinChars "$sig" )) ;
  183. stat=${BASH_REMATCH[ 7]} ; Stats=( ${Stats[*]} $(JoinChars "$stat" )) ;
  184. msg=${BASH_REMATCH[ 8]} ; Msgs=( ${Msgs[*]} $(JoinChars "$msg" )) ;
  185. ref=${BASH_REMATCH[ 9]} ; Refs=( ${Refs[*]} $(JoinChars "$ref" )) ;
  186. [[ -n "$msg" ]] || Msgs=( ${Msgs[*]} "<EMPTY>" )
  187. [[ "$stat" == 'E' ]] && Sigs=( ${Sigs[*]} "<UNKNOWN>" ) || \
  188. [[ -n "$sig" ]] || Sigs=( ${Sigs[*]} "$JOIN_CHAR" )
  189. [[ -n "$ref" ]] || Refs=( ${Refs[*]} "$JOIN_CHAR" )
  190. (( ${#author} > AuthorW )) && AuthorW=${#author}
  191. done
  192. }
  193. PrintReport() # ( "header" )
  194. {
  195. local header="$1"
  196. local n_results=$(( ${#Ids[*]} ))
  197. local pad_w=$(( ( -${#header} + ${#Ids[0]} + 1 + ${#Dates[0]} + AuthorW ) / 2 ))
  198. local pad="$(printf "%${pad_w}s" ' ' | tr ' ' "$HRULE_CHAR")"
  199. local hrule="|<${pad} ${header} ${pad:$(( pad_w > 0 && ! ( AuthorW % 2 ) ))}>|"
  200. local result_n graph id date author sig stat msg ref pad
  201. local has_author_sig sig_color hash_color date_color author_color msg_color ref_color
  202. # pretty print results
  203. (( ! HIDE_MERGED )) && echo "${hrule}"
  204. (( ! n_results )) && sed 's/[^|]/ /g ; s/^| /| <None>/' <<<"${hrule}"
  205. for (( result_n = 0 ; result_n < n_results ; ++result_n ))
  206. do graph=${Graphs[$result_n]}
  207. id=${Ids[$result_n]}
  208. date=${Dates[$result_n]}
  209. author=${Authors[$result_n]}
  210. sig=${Sigs[$result_n]}
  211. stat=${Stats[$result_n]}
  212. msg=${Msgs[$result_n]}
  213. ref=${Refs[$result_n]}
  214. pad=$(printf "%$(( AuthorW - ${#author} ))s" '')
  215. if (( USE_ANSI_COLOR ))
  216. then has_author_sig=$([[ "$author" == "$sig" ]] ; echo $((!$?)) ;)
  217. sig_color=$(case "$stat" in
  218. 'G') echo $CGOOD ;; # good signature
  219. 'X') echo $CEXPIRED ;; # good signature that has expired
  220. 'U') echo $CGOOD ;; # good signature with unknown trust
  221. 'E') echo $CUNKNOWN ;; # cannot be checked (e.g. missing key)
  222. 'B') echo $CBAD ;; # bad signature
  223. 'Y') echo $CEXPIRED ;; # good signature made by an expired key
  224. 'R') echo $CBAD ;; # good signature made by a revoked key
  225. 'N') echo $CNONE ;; # no signature
  226. esac)
  227. hash_color=$HASH_COLOR
  228. date_color=$DATE_COLOR
  229. author_color=$((( has_author_sig )) && echo $sig_color || echo $AUTHOR_COLOR)
  230. msg_color=$MSG_COLOR
  231. ref_color=$REF_COLOR
  232. fi
  233. # printf "$graph_color$(FilterJoinChars $graph) $CEND"
  234. printf "| $hash_color$id$CEND"
  235. printf " $date_color$date$CEND"
  236. printf " $author_color%s$CEND" "$(FilterJoinChars $author)"
  237. printf " $pad| $msg_color%s$CEND" "$(FilterJoinChars $msg )"
  238. [[ "$ref" != "$JOIN_CHAR" ]] && printf " $ref_color($(FilterJoinChars $ref))$CEND"
  239. [[ "$sig" != "$JOIN_CHAR" ]] && printf " $sig_color[$(FilterJoinChars $sig)]$CEND"
  240. printf "\n"
  241. done
  242. NCommits=$(( NCommits + ${#Ids[*]} ))
  243. }
  244. Main()
  245. {
  246. local ancestor=$(Ancestor $PUB_BRANCH $REF)
  247. local log_cmd header
  248. if [[ -z "$PUB_BRANCH" ]]
  249. then log_cmd="$(printf "$GIT_LOG_CMD_FMT" $N_COMMITS)" header='NO UPSTREAM'
  250. CompileResults < <($log_cmd $REF $FILE ; echo ;) ; PrintReport "${header}" ;
  251. elif ! IsAncestor $PUB_BRANCH $REF && [[ -z ${ancestor:-} ]]
  252. then log_cmd="$(printf "$GIT_LOG_CMD_FMT" $N_COMMITS)" header='UNRELATED'
  253. CompileResults < <($log_cmd $REF $FILE ; echo ;) ; PrintReport ${header} ;
  254. else log_cmd="$(printf "$GIT_LOG_CMD_FMT" $N_COMMITS)" header='UNMERGED'
  255. CompileResults < <($log_cmd $ancestor..$REF $FILE ; echo ;) ; PrintReport ${header} ;
  256. if (( ! HIDE_MERGED && NCommits < N_COMMITS ))
  257. then log_cmd="$(printf "$GIT_LOG_CMD_FMT" $(( N_COMMITS - NCommits )))" header='MERGED'
  258. CompileResults < <($log_cmd $ancestor $FILE ; echo ;) ; PrintReport ${header} ;
  259. fi
  260. fi
  261. }
  262. ## main entry ##
  263. Init "$@" && Main