diff --git a/README.md b/README.md index 92bd26d..3bbaadc 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,18 @@ git-summary summarizes the status of all cloned git repositories founds within a * Linux * MacOS -* Cygwin +* Cygwin/Windows Busybox/MinGW * SunOS * Alpine based containers on Google Cloud Platform - Container Optimised OS (Chromium OS) ## Requirements -### Linux -* `sudo apt-get install gawk` - ### MacOS * `brew install coreutils` -### Alpine based containers on Google Cloud Platform (Chromium OS) -* `apk add gawk findutils` - -> xargs in Chromium OS does not support -L option, findutils puts an xargs with support for -L - ## Installation (on Linux-based machines) -Clone this repo somewhere and alias the script by adding this line to `~\.bashrc` (modify `$PATH` to point to the location of the cloned repo on your machine): +Clone this repo somewhere and alias the script by adding this line to your shell's rc file such as `~/.profile`, `~/.bashrc` or `~/.zshrc` (modify `$PATH` to point to the location of the cloned repo on your machine): ``` alias git-summary='/git-summary/git-summary' diff --git a/git-summary b/git-summary index 88f758b..5123f2a 100755 --- a/git-summary +++ b/git-summary @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # git-summary - summarize git repos at some path # @@ -93,15 +93,15 @@ git_summary() { # Use provided path, or default to pwd. # Here we also decide if we want relative or full path names printouts. - local target=$(${readlink_cmd} -f ${1:-`pwd`}) - if [ $full_path -eq 0 ] && command -v realpath >/dev/null && command -v python >/dev/null ; then - local rtarget=$(get_relative_path $target) + local target="$(${readlink_cmd} -f ${1:-$PWD})" + if [ $full_path -eq 0 ] && command -v realpath >/dev/null; then + local rtarget="$(get_relative_path $target)" else - local rtarget=$target + local rtarget="$target" fi - local repos=$(list_repos $rtarget $deeplookup) + local repos="$(list_repos $rtarget $deeplookup)" - if [[ -z $repos ]]; then + if [ -z "$repos" ]; then exit fi @@ -117,37 +117,40 @@ git_summary() { local header_space_branch=${#HEADER_BRANCH} local header_space_state=${#HEADER_STATE} - local longest_repo_name=$(max_len "$repos") + local longest_repo_name=$(( $(max_len "$repos") - 11 )) # echo '/.git/HEAD' | wc -c = 11 local space_repo=$(( $longest_repo_name < $header_space_repo ? $header_space_repo : $longest_repo_name )) - local branches=$(repo_branches $rtarget) + local branches="$(printf '%s' "${repos}" | xargs -I {} cut -d/ -f3- '{}')" local longest_branch_name=$(max_len "$branches") - local longest_branch_name=$(( $longest_branch_name < $header_space_branch ? $header_space_branch : $longest_branch_name )) # Factor in the header + longest_branch_name=$(( $longest_branch_name < $header_space_branch ? $header_space_branch : $longest_branch_name )) # Factor in the header + local space_branch=$longest_branch_name # If we don't have tput, allow full width if command -v tput >/dev/null && eval 'tput cols' >/dev/null 2>&1; then # If we have tput, adapt to terminal local TERM_COLUMNS=$(tput cols) - local space_branch=$(( $TERM_COLUMNS - $space_repo - $header_space_state - 4)) # The 4 is the spaces between columns - local space_branch=$(( $space_branch < $header_space_branch ? $header_space_branch : $space_branch )) - local space_branch=$(( $space_branch > $longest_branch_name ? $longest_branch_name : $space_branch )) # If this is commented then prompt will be full terminal width - else - local space_branch=$longest_branch_name # If we don't have tput, allow full width + space_branch=$(( $TERM_COLUMNS - $space_repo - $header_space_state - 4)) # The 4 is the spaces between columns + space_branch=$(( $space_branch < $header_space_branch ? $header_space_branch : $space_branch )) + space_branch=$(( $space_branch > $longest_branch_name ? $longest_branch_name : $space_branch )) # If this is commented then prompt will be full terminal width fi - local template=$(printf "%%b%%-%ds %%-%d.%ds %%-%ds" $space_repo $space_branch $space_branch $header_space_state) # template that truncates branch names to fit terminal width + local template="$(printf "%%b%%-%ds %%-%d.%ds %%-%ds" $space_repo $space_branch $space_branch $header_space_state)" # template that truncates branch names to fit terminal width print_header "$template" $space_repo $space_branch $header_space_state local repo_count=0 + IFS=' +' local repo - for repo in $repos ; do + for repo in $repos; do + repo_count=$((repo_count + 1)) + repo="${repo%/.git/HEAD}" + branch=$(printf "%s" "$branches" | sed -n "${repo_count}p") if [ $sort -eq 0 ] ; then - summarize_one_git_repo $repo "$template" "$local_only" "$quiet" >&1 & # parallelized - FIFO stdout + summarize_one_git_repo "$repo" "$branch" "$template" "$local_only" "$quiet" >&1 & # parallelized - FIFO stdout else - summarize_one_git_repo $repo "$template" "$local_only" "$quiet" >&1 # sequential - sorted stdout + summarize_one_git_repo "$repo" "$branch" "$template" "$local_only" "$quiet" >&1 # sequential - sorted stdout fi - (( repo_count+=1 )) done wait @@ -159,39 +162,23 @@ git_summary() { # Autodetect the OS detect_OS() { - - if [ "$(uname)" == "Darwin" ]; then # macOS - OS=Darwin - readlink_cmd="greadlink" - dirname_cmd="gdirname" - gawk_cmd="awk" - elif [ "$(uname)" == "SunOS" ]; then # SunOS (solaris/smartos/illumos) - OS=SunOS - readlink_cmd="greadlink" - dirname_cmd="gdirname" - gawk_cmd="awk" - elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then # Linux-based system - if [[ $(cat /proc/version) == *"Chromium OS"* ]]; then # Chromium OS - OS=Linux - readlink_cmd="readlink" - dirname_cmd="dirname_zero" - gawk_cmd="gawk" - else # Vanilla Linux - OS=Linux - readlink_cmd="readlink" - dirname_cmd="dirname" - gawk_cmd="gawk" - fi - elif [ "$(expr substr $(uname -s) 1 6)" == "CYGWIN" ]; then # Cygwin - OS=CYGWIN + OS="$(uname -s)" readlink_cmd="readlink" dirname_cmd="dirname" - gawk_cmd="gawk" - else - echo "Oups, I cannot identify your current OS." - echo "Please open a ticket or a pull request so we can add your OS to the list of supported systems." - exit 1 - fi + case "$OS" in + Darwin) # macOS + readlink_cmd="greadlink" + dirname_cmd="gdirname" ;; + SunOS) # SunOS (solaris/smartos/illumos) + readlink_cmd="greadlink" + dirname_cmd="gdirname" ;; + Linux) + case "$(grep ^NAME /etc/os-release | cut -d'=' -f2 | tr -d \")" in + Chromium\ OS) + dirname_cmd="dirname_zero" ;; + esac ;; + esac + } GIT4WINDOWS=1 @@ -202,9 +189,10 @@ dirname_zero(){ } detect_Git4Windows() { - if [[ "$OS" == "CYGWIN" && "$(git --version)" == *"windows"* ]]; then - GIT4WINDOWS=0 - fi + if [ "${OS#CYGWIN}" != "$OS" ]; then case "$(git --version)" in + *windows*) GIT4WINDOWS=0 ;; + esac + fi } gitC() { @@ -236,30 +224,29 @@ print_header () { print_divider } - summarize_one_git_repo () { - - local f=$1 - local template=$2 - local local_only=$3 - local quiet=$4 - - local app_name=$f - local branch_name=`gitC $f symbolic-ref HEAD | sed -e "s/^refs\/heads\///"` + local f="$1" + local branch_name=$2 + local template=$3 + local local_only=$4 + local quiet=$5 + + local app_name="$f" + local git_status="$(LC_ALL=C gitC "$f" status --porcelain=v1 2>/dev/null)" local numState=0 ### Check remote state local rstate="" - local has_upstream=`gitC $f rev-parse --abbrev-ref @{u} 2> /dev/null | wc -l` - if [ $has_upstream -ne 0 ] ; then + local has_upstream=$(gitC "$f" rev-list --count '@{u}..' 2>/dev/null) + if [ ${has_upstream:=0} -ne 0 ] ; then if [ $local_only -eq 0 ] ; then - gitC $f fetch -q &> /dev/null + gitC "$f" fetch -q & >/dev/null fi # Unpulled and unpushed on *current* branch - local unpulled=`gitC $f log --pretty=format:'%h' ..@{u} | wc -c` - local unpushed=`gitC $f log --pretty=format:'%h' @{u}.. | wc -c` + local unpulled="$(gitC "$f" rev-list --count '..@{u}' 2>/dev/null)" + local unpushed="$has_upstream" - if [ $unpulled -ne 0 ]; then + if [ ${unpulled:=0} -ne 0 ]; then rstate="${rstate}v" numState=1 else @@ -279,9 +266,10 @@ summarize_one_git_repo () { ### Check local state local state="" - local untracked=`LC_ALL=C gitC $f status | grep Untracked -c` - local new_files=`LC_ALL=C gitC $f status | grep "new file" -c` - local modified=`LC_ALL=C gitC $f status | grep modified -c` + + local untracked="$(echo "${git_status}" | grep -c '^??')" + local new_files="$(echo "${git_status}" | grep -c '^A')" + local modified="$(echo "${git_status}" | grep -c '^ [MRD]')" if [ $untracked -ne 0 ]; then state="${state}?" @@ -307,74 +295,37 @@ summarize_one_git_repo () { ### Print to stdout if [ $numState -eq 0 ]; then if [ $quiet -eq 0 ]; then - printf "$template\n" $GREEN $app_name $branch_name "$state$rstate" >&1 + printf "$template\n" $GREEN "$app_name" $branch_name "$state$rstate" >&1 fi elif [ $numState -eq 1 ]; then - printf "$template\n" $ORANGE $app_name $branch_name "$state$rstate" >&1 + printf "$template\n" $ORANGE "$app_name" $branch_name "$state$rstate" >&1 elif [ $numState -eq 2 ]; then - printf "$template\n" $RED $app_name $branch_name "$state$rstate" >&1 + printf "$template\n" $RED "$app_name" $branch_name "$state$rstate" >&1 fi } - -# Given the path to a git repo, compute its current branch name. -repo_branch () { - gitC "$1" symbolic-ref HEAD | sed -e "s/^refs\/heads\///" -} - - # Given a path to a folder containing some git repos, compute the # names of the folders which actually do contain git repos. list_repos () { - # https://stackoverflow.com/questions/23356779/how-can-i-store-find-command-result-as-arrays-in-bash - git_directories=() - - local find_cmd if [ $deeplookup -eq 0 ]; then - find_cmd="find -L $1 -maxdepth 2 -type d -name .git -print0" - else - find_cmd="find -L $1 -type d -name .git -print0" + find_args="-maxdepth 3" fi - while IFS= read -r -d $'\0'; do - git_directories+=("$REPLY") - done < <($find_cmd | sort -z 2>/dev/null) - - for i in ${git_directories[*]}; do - if [[ ! -z $i ]]; then - $dirname_cmd -z $i | xargs -0 -L1 - fi - done -} - - -# Given the path to a folder containing git some repos, compute the -# names of the current branches in the repos. -repo_branches () { - local path=$1 - local repo - for repo in $(list_repos $path) ; do - echo $(repo_branch $repo) - done + find -L $1 $find_args -wholename '*/.git/HEAD' -type f 2>/dev/null | sort } - max_len () { - echo "$1" | $gawk_cmd '{ print length }' | sort -rn | head -1 + # TODO Replace with wc -L after checking portability + echo "$1" | tr -c '\n' 1 | sort -rn | head -n1 | wc -c } - # Function to get the relative path of a file or directory get_relative_path() { - local target="$1" - local base=$(pwd) - # Convert paths to absolute paths - local target_absolute=$(realpath "$target") - local base_absolute=$(realpath "$base") + local target_absolute="$(realpath "$1")/" - # Use Python to calculate the relative path - python -c "import os.path; print(os.path.relpath('$target_absolute', '$base_absolute'))" + local out="${target_absolute#"$PWD/"}" + echo "${out:-./}" } trap "printf '$NC'" EXIT