|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# git-summary - summarize git repos at some path |
| 4 | +# |
| 5 | +# Forked from https://github.com/lordadamson/git-summary |
| 6 | +# |
| 7 | +# Freely distributed under the MIT license. 2018@MirkoLedda |
| 8 | + |
| 9 | +set -eu |
| 10 | + |
| 11 | +# Colorcode |
| 12 | +GREEN='\e[0;32m' |
| 13 | +ORANGE='\e[0;33m' |
| 14 | +RED='\e[0;31m' |
| 15 | +PURPLE='\e[0;35m' |
| 16 | +NC='\e[0m' # No Color |
| 17 | + |
| 18 | +usage() { |
| 19 | + sed 's/^ //' <<EOF |
| 20 | + git-summary - summarize git repos at some path |
| 21 | +
|
| 22 | + Usage: git-summary.sh [-h] [-l] [-d] [-q] [path] |
| 23 | +
|
| 24 | + Given a path to a folder containing one or more git repos, |
| 25 | + print a status summary table showing, for each repo: |
| 26 | +
|
| 27 | + - the folder name |
| 28 | + - the currently checked out branch |
| 29 | + - a short 2-column status string showing whether there are: |
| 30 | + * Local Changes: |
| 31 | + - untracked files "?_" |
| 32 | + - uncommitted new files "+_" |
| 33 | + - uncommitted changes "M_" |
| 34 | + - (nothing) " _" |
| 35 | + * Remote Changes: |
| 36 | + - unpulled commits for the current branch "_v" |
| 37 | + - unpushed commits for the current branch "_^" |
| 38 | + - (nothing) "_ " |
| 39 | +
|
| 40 | + Arguments: |
| 41 | +
|
| 42 | + -h Print this message |
| 43 | +
|
| 44 | + -l Local operation only. Without this the script runs |
| 45 | + "git fetch" in each repo before checking for unpushed/ |
| 46 | + unpulled commits. As this can be time consuming, this |
| 47 | + flag lets you skip that. |
| 48 | +
|
| 49 | + -d Deep lookup. Will search within the entire tree of the |
| 50 | + current folder. |
| 51 | +
|
| 52 | + -q Print nothing for repos that are up to date. Also print |
| 53 | + a final tally. |
| 54 | +
|
| 55 | + path Path to folder containing git repos; if omitted, the |
| 56 | + current working directory is used. |
| 57 | +
|
| 58 | +EOF |
| 59 | +} |
| 60 | + |
| 61 | +# Main |
| 62 | +git_summary() { |
| 63 | + |
| 64 | + detect_OS |
| 65 | + detect_Git4Windows |
| 66 | + |
| 67 | + local local_only=0 |
| 68 | + local opt |
| 69 | + local deeplookup=0 |
| 70 | + local quiet=0 |
| 71 | + while getopts "hldq" opt; do |
| 72 | + case "${opt}" in |
| 73 | + h) usage ; exit 1 ;; |
| 74 | + l) local_only=1 ;; # Will skip "git fetch" |
| 75 | + d) deeplookup=1 ;; |
| 76 | + q) quiet=1 ;; |
| 77 | + esac |
| 78 | + done |
| 79 | + shift $((OPTIND-1)) |
| 80 | + |
| 81 | + # Use provided path, or default to pwd |
| 82 | + local target=$(${readlink_cmd} -f ${1:-`pwd`}) |
| 83 | + local repos=$(list_repos $target $deeplookup) |
| 84 | + |
| 85 | + if [[ -z $repos ]]; then |
| 86 | + exit |
| 87 | + fi |
| 88 | + |
| 89 | + # We compute the repo names and branch names here so we can |
| 90 | + # compute their maximum lengths and lay things out nicely. This |
| 91 | + # can all be done much more easily via the column(1) utility, but |
| 92 | + # that has to consume all its input before it can write anything |
| 93 | + # out, which isn't great when you're running "git fetch" on a |
| 94 | + # whole bunch of repos. Doing it like this allows us to write the |
| 95 | + # output to stdout incrementally. |
| 96 | + |
| 97 | + local branches=$(repo_branches $target) |
| 98 | + local max_repo_len=$(max_len "$repos") |
| 99 | + local max_branch_len=$(max_len "$branches") |
| 100 | + local template=$(printf "%%b%%-%ds %%-%ds %%-5s" $max_repo_len $max_branch_len) |
| 101 | + print_header "$template" $max_repo_len $max_branch_len |
| 102 | + |
| 103 | + local repo_count=0 |
| 104 | + |
| 105 | + local f |
| 106 | + for f in $repos ; do |
| 107 | + summarize_one_git_repo $f "$template" "$local_only" "$quiet" >&1 & |
| 108 | + (( repo_count+=1 )) |
| 109 | + done |
| 110 | + wait |
| 111 | + |
| 112 | + if [ $quiet -eq 1 ]; then |
| 113 | + echo "Checked ${repo_count} repositories." |
| 114 | + fi |
| 115 | + |
| 116 | +} |
| 117 | + |
| 118 | +# Autodetect the OS |
| 119 | +detect_OS() { |
| 120 | + if [ "$(uname)" == "Darwin" ]; then # macOS |
| 121 | + OS=Darwin |
| 122 | + readlink_cmd="greadlink" |
| 123 | + dirname_cmd="gdirname" |
| 124 | + gawk_cmd="awk" |
| 125 | + elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then # Linux |
| 126 | + OS=Linux |
| 127 | + readlink_cmd="readlink" |
| 128 | + dirname_cmd="dirname" |
| 129 | + gawk_cmd="gawk" |
| 130 | + elif [ "$(expr substr $(uname -s) 1 6)" == "CYGWIN" ]; then # Cygwin |
| 131 | + OS=CYGWIN |
| 132 | + readlink_cmd="readlink" |
| 133 | + dirname_cmd="dirname" |
| 134 | + gawk_cmd="gawk" |
| 135 | + else |
| 136 | + echo "Cannot identify OS." |
| 137 | + exit 1 |
| 138 | + fi |
| 139 | +} |
| 140 | + |
| 141 | +GIT4WINDOWS=1 |
| 142 | + |
| 143 | +detect_Git4Windows() { |
| 144 | + if [[ "$OS" == "CYGWIN" && "$(git --version)" == *"windows"* ]]; then |
| 145 | + GIT4WINDOWS=0 |
| 146 | + fi |
| 147 | +} |
| 148 | + |
| 149 | +gitC() { |
| 150 | + local ldir=$1; shift; |
| 151 | + if [ $GIT4WINDOWS -eq 0 ]; then |
| 152 | + git -C "$(cygpath -w $ldir)" "$@" |
| 153 | + else |
| 154 | + git -C "$ldir" "$@" |
| 155 | + fi |
| 156 | +} |
| 157 | + |
| 158 | + |
| 159 | +print_header () { |
| 160 | + local template="$1" |
| 161 | + local max_repo_len=$2 |
| 162 | + local max_branch_len=$3 |
| 163 | + print_divider () { |
| 164 | + printf '=%.0s' $(seq 1 $max_repo_len) |
| 165 | + printf ' ' |
| 166 | + printf '=%.0s' $(seq 1 $max_branch_len) |
| 167 | + printf ' ' |
| 168 | + printf '=%.0s' $(seq 1 5) |
| 169 | + printf '\n' |
| 170 | + }; |
| 171 | + |
| 172 | + echo |
| 173 | + printf "$template\n" $NC Repository Branch State |
| 174 | + print_divider |
| 175 | +} |
| 176 | + |
| 177 | + |
| 178 | +summarize_one_git_repo () { |
| 179 | + |
| 180 | + local f=$1 |
| 181 | + local template=$2 |
| 182 | + local local_only=$3 |
| 183 | + local quiet=$4 |
| 184 | + |
| 185 | + local app_name=$f |
| 186 | + local branch_name=`gitC $f symbolic-ref HEAD | sed -e "s/^refs\/heads\///"` |
| 187 | + local numState=0 |
| 188 | + |
| 189 | + ### Check remote state |
| 190 | + local rstate="" |
| 191 | + local has_upstream=`gitC $f rev-parse --abbrev-ref @{u} 2> /dev/null | wc -l` |
| 192 | + if [ $has_upstream -ne 0 ] ; then |
| 193 | + if [ $local_only -eq 0 ] ; then |
| 194 | + gitC $f fetch -q &> /dev/null |
| 195 | + fi |
| 196 | + # Unpulled and unpushed on *current* branch |
| 197 | + local unpulled=`gitC $f log --pretty=format:'%h' ..@{u} | wc -c` |
| 198 | + local unpushed=`gitC $f log --pretty=format:'%h' @{u}.. | wc -c` |
| 199 | + |
| 200 | + if [ $unpulled -ne 0 ]; then |
| 201 | + rstate="${rstate}v" |
| 202 | + numState=1 |
| 203 | + else |
| 204 | + rstate="${rstate} " |
| 205 | + fi |
| 206 | + |
| 207 | + if [ $unpushed -ne 0 ]; then |
| 208 | + rstate="${rstate}^" |
| 209 | + numState=1 |
| 210 | + else |
| 211 | + rstate="${rstate} " |
| 212 | + fi |
| 213 | + |
| 214 | + else |
| 215 | + rstate="--" |
| 216 | + fi |
| 217 | + |
| 218 | + ### Check local state |
| 219 | + local state="" |
| 220 | + local untracked=`LC_ALL=C gitC $f status | grep Untracked -c` |
| 221 | + local new_files=`LC_ALL=C gitC $f status | grep "new file" -c` |
| 222 | + local modified=`LC_ALL=C gitC $f status | grep modified -c` |
| 223 | + |
| 224 | + if [ $untracked -ne 0 ]; then |
| 225 | + state="${state}?" |
| 226 | + numState=2 |
| 227 | + else |
| 228 | + state="${state} " |
| 229 | + fi |
| 230 | + |
| 231 | + if [ $new_files -ne 0 ]; then |
| 232 | + state="${state}+" |
| 233 | + numState=2 |
| 234 | + else |
| 235 | + state="${state} " |
| 236 | + fi |
| 237 | + |
| 238 | + if [ $modified -ne 0 ]; then |
| 239 | + state="${state}M" |
| 240 | + numState=2 |
| 241 | + else |
| 242 | + state="${state} " |
| 243 | + fi |
| 244 | + |
| 245 | + ### Print to stdout |
| 246 | + if [ $numState -eq 0 ]; then |
| 247 | + if [ $quiet -eq 0 ]; then |
| 248 | + printf "$template\n" $GREEN $app_name $branch_name "$state$rstate" >&1 |
| 249 | + fi |
| 250 | + elif [ $numState -eq 1 ]; then |
| 251 | + printf "$template\n" $ORANGE $app_name $branch_name "$state$rstate" >&1 |
| 252 | + elif [ $numState -eq 2 ]; then |
| 253 | + printf "$template\n" $RED $app_name $branch_name "$state$rstate" >&1 |
| 254 | + fi |
| 255 | +} |
| 256 | + |
| 257 | + |
| 258 | +# Given the path to a git repo, compute its current branch name. |
| 259 | +repo_branch () { |
| 260 | + gitC "$1" symbolic-ref HEAD | sed -e "s/^refs\/heads\///" |
| 261 | +} |
| 262 | + |
| 263 | + |
| 264 | +# Given a path to a folder containing some git repos, compute the |
| 265 | +# names of the folders which actually do contain git repos. |
| 266 | +list_repos () { |
| 267 | + # https://stackoverflow.com/questions/23356779/how-can-i-store-find-command-result-as-arrays-in-bash |
| 268 | + git_directories=() |
| 269 | + |
| 270 | + local find_cmd |
| 271 | + if [ $deeplookup -eq 0 ]; then |
| 272 | + find_cmd="find -L $1 -maxdepth 2 -type d -name .git -print0" |
| 273 | + else |
| 274 | + find_cmd="find -L $1 -type d -name .git -print0" |
| 275 | + fi |
| 276 | + |
| 277 | + while IFS= read -r -d $'\0'; do |
| 278 | + git_directories+=("$REPLY") |
| 279 | + done < <($find_cmd 2>/dev/null) |
| 280 | + |
| 281 | + for i in ${git_directories[*]}; do |
| 282 | + if [[ ! -z $i ]]; then |
| 283 | + $dirname_cmd -z $i | xargs -0 -L1 |
| 284 | + fi |
| 285 | + done |
| 286 | +} |
| 287 | + |
| 288 | + |
| 289 | +# Given the path to a folder containing git some repos, compute the |
| 290 | +# names of the current branches in the repos. |
| 291 | +repo_branches () { |
| 292 | + local path=$1 |
| 293 | + local repo |
| 294 | + for repo in $(list_repos $path) ; do |
| 295 | + echo $(repo_branch $repo) |
| 296 | + done |
| 297 | +} |
| 298 | + |
| 299 | + |
| 300 | +max_len () { |
| 301 | + echo "$1" | $gawk_cmd '{ print length }' | sort -rn | head -1 |
| 302 | +} |
| 303 | + |
| 304 | +trap "printf '$NC'" EXIT |
| 305 | + |
| 306 | +git_summary $@ |
0 commit comments