From dd7719c3b0fe4776b0e34bf30ebfa9a5e436bcd1 Mon Sep 17 00:00:00 2001 From: arzzen Date: Mon, 16 Jun 2025 14:03:21 +0200 Subject: [PATCH 1/5] sorting contribution stats --- README.md | 10 + git-quick-stats | 618 +++++++++++++++++++++++------------------ git-quick-stats.1 | 4 + tests/commands_test.sh | 17 +- 4 files changed, 373 insertions(+), 276 deletions(-) diff --git a/README.md b/README.md index 79b57eb..ca94e68 100644 --- a/README.md +++ b/README.md @@ -204,12 +204,22 @@ You can set the variable `_GIT_BRANCH` to set the branch of the stats. Works wit export _GIT_BRANCH="master" ``` +### Ignore authors + You can set the variable `_GIT_IGNORE_AUTHORS` to filter out specific authors. It will affect the "All contributors", ""Suggested code reviewers" and "New contributors" options. ```bash export _GIT_IGNORE_AUTHORS="(author@examle.com|username)" ``` +### Sorting contribution stats + +You can sort contribution stats by field `name`, `commits`, `insertions`, `deletions`, or `lines` (total lines changed) and order (`asc`, `desc`). e.g.: `commits-desc` + +```bash +export _GIT_SORT_BY="name-asc" +``` + ### Color themes You can change to the legacy color scheme by toggling the variable `_MENU_THEME` between `default` and `legacy` diff --git a/git-quick-stats b/git-quick-stats index 72bb501..952206a 100755 --- a/git-quick-stats +++ b/git-quick-stats @@ -14,7 +14,8 @@ set -o errexit # If $_GIT_SINCE is never set, look at the repository to find the first date. # NOTE: previously this put the date at the fixed GIT epoch (May 2005) _since=${_GIT_SINCE:-} -if [[ -n "${_since}" ]]; then +if [[ -n "${_since}" ]]; +then _since="--since=$_since" else _since="--since=$(git log --reverse --format='%ad' --date=iso | head -n1)" @@ -24,7 +25,8 @@ fi # If $_GIT_UNTIL is never set, choose the latest system # time from the user's current environment _until=${_GIT_UNTIL:-} -if [[ -n "${_until}" ]]; then +if [[ -n "${_until}" ]]; +then _until="--until=$_until" else _until="--until=$(LC_TIME=C date '+%a, %d %b %Y %H:%M:%S %Z')" @@ -33,7 +35,8 @@ fi # Set files or directories to be excluded in stats # If $_GIT_PATHSPEC is not set, shift over the option completely _pathspec=${_GIT_PATHSPEC:-} -if [[ -n "${_pathspec}" ]]; then +if [[ -n "${_pathspec}" ]]; +then _pathspec="-- $_pathspec" else _pathspec="--" @@ -44,9 +47,11 @@ fi # Enable shows regular commits together with normal commits _merges=${_GIT_MERGE_VIEW:-} _merges=$(echo "$_merges" | awk '{print tolower($0)}') -if [[ "${_merges}" == "exclusive" ]]; then +if [[ "${_merges}" == "exclusive" ]]; +then _merges="--merges" -elif [[ "${_merges}" == "enable" ]]; then +elif [[ "${_merges}" == "enable" ]]; +then _merges="" else _merges="--no-merges" @@ -54,7 +59,8 @@ fi # Limit git log output _limit=${_GIT_LIMIT:-} -if [[ -n "${_limit}" ]]; then +if [[ -n "${_limit}" ]]; +then _limit=$_limit else _limit=10 @@ -62,7 +68,8 @@ fi # Log options _log_options=${_GIT_LOG_OPTIONS:-} -if [[ -n "${_log_options}" ]]; then +if [[ -n "${_log_options}" ]]; +then _log_options=$_log_options else _log_options="" @@ -70,12 +77,22 @@ fi # Ignore author regex _ignore_authors=${_GIT_IGNORE_AUTHORS:-} -if [[ -n "${_ignore_authors}" ]]; then +if [[ -n "${_ignore_authors}" ]]; +then _ignore_authors=$_ignore_authors else _ignore_authors="" fi +# Sort by field and order for contribution stats +_GIT_SORT_BY=${_GIT_SORT_BY:-name-asc} +# If the user has not set a sort order, default to name-asc +if [[ ! "$_GIT_SORT_BY" =~ ^(name|commits|insertions|deletions|lines)-(asc|desc)$ ]]; +then + echo "Invalid sort option: $_GIT_SORT_BY. Defaulting to 'name-asc'." + _GIT_SORT_BY="name-asc" +fi + # Default menu theme # Set the legacy theme by typing "export _MENU_THEME=legacy" _theme="${_MENU_THEME:=default}" @@ -96,37 +113,25 @@ function commitsCalendarByAuthor() { --date=iso --author="$author" "$_since" "$_until" $_log_options \ --pretty='%ad' $_pathspec | awk ' { - split($0, a, " "); - # a[1] = YYYY-MM-DD - split(a[1], date_fields, "-"); - mon = date_fields[2] + 0; + split($0, a, " "); # a[1] = YYYY-MM-DD + split(a[1], date_fields, "-"); mon = date_fields[2] + 0; cmd = "date -d \"" a[1] "\" +%u"; cmd | getline weekday; - close(cmd); - # weekday: 1=Mon, ..., 7=Sun - count[weekday][mon]++; - } + close(cmd); # weekday: 1=Mon, ..., 7=Sun + count[weekday][mon]++; } END { # Output matrix for (d=1; d<=7; d++) { - if (d==1) printf "Mon "; - else if (d==2) printf "Tue "; - else if (d==3) printf "Wed "; - else if (d==4) printf "Thu "; - else if (d==5) printf "Fri "; - else if (d==6) printf "Sat "; - else if (d==7) printf "Sun "; - for (m=1; m<=12; m++) { - c = count[d][m]+0; - if (c==0) out="..."; + if (d==1) printf "Mon "; else if (d==2) printf "Tue "; + else if (d==3) printf "Wed "; else if (d==4) printf "Thu "; + else if (d==5) printf "Fri "; else if (d==6) printf "Sat "; + else if (d==7) printf "Sun "; for (m=1; m<=12; m++) { + c = count[d][m]+0; if (c==0) out="..."; else if (c<=9) out="░░░"; - else if (c<=19) out="▒▒▒"; - else out="▓▓▓"; - printf "%s%s", out, (m<12?" ":"\n"); - } + else if (c<=19) out="▒▒▒"; else out="▓▓▓"; + printf "%s%s", out, (m<12?" ":"\n"); } } - printf "\nLegend: ... = 0 ░░░ = 1–9 ▒▒▒ = 10–19 ▓▓▓ = 20+ commits\n"; - } + printf "\nLegend: ... = 0 ░░░ = 1–9 ▒▒▒ = 10–19 ▓▓▓ = 20+ commits\n"; } ' } @@ -195,9 +200,8 @@ SYNOPSIS DESCRIPTION Any git repository contains tons of information about commits, contributors, and files. Extracting this information is not always trivial, mostly because - of a gadzillion options to a gadzillion git commands. - - This program allows you to see detailed information about a git repository. + of a gadzillion options to a gadzillion git commands. This program allows + you to see detailed information about a git repository. GENERATE OPTIONS -T, --detailed-git-stats @@ -269,7 +273,9 @@ ADDITIONAL USAGE You can set _GIT_BRANCH to set the branch of the stats ex: export _GIT_BRANCH=master You can set _GIT_IGNORE_AUTHORS to filter out specific authors - ex: export _GIT_IGNORE_AUTHORS=\"(author1|author2)\"" + ex: export _GIT_IGNORE_AUTHORS=\"(author1|author2)\" + You can sort contribution stats by field name, commits, insertions, deletions, or lines - total lines changed and order - asc, desc. e.g.: commits-desc + ex: export _GIT_SORT_BY=name-asc" } ################################################################################ @@ -292,7 +298,8 @@ function showMenu() { EXIT_TXT="" # Adjustable color menu option - if [[ "${_theme}" == "legacy" ]]; then + if [[ "${_theme}" == "legacy" ]]; + then TITLES="${BOLD}${RED}" TEXT="${NORMAL}${CYAN}" NUMS="${BOLD}${YELLOW}" @@ -339,7 +346,8 @@ function showMenu() { } filter_ignored_authors() { - if [[ -n "$_ignore_authors" ]]; then + if [[ -n "$_ignore_authors" ]]; + then grep -Ev "$_ignore_authors" else cat @@ -351,7 +359,7 @@ filter_ignored_authors() { ################################################################################ # DESC: Shows detailed contribution stats per author by parsing every commit in -# the repo and outputting their contribution stats +# the repo and outputting their contribution stats, including bar graphs. # ARGS: $branch (optional): Users can specify an alternative branch instead of # the current default one # OUTS: None @@ -362,9 +370,11 @@ function detailedGitStats() { local _branch="" # Check if requesting for a specific branch - if [[ -n "${branch}" ]]; then + if [[ -n "${branch}" ]]; + then # Check if branch exist - if [[ $(git show-ref refs/heads/"${branch}") ]] ; then + if [[ $(git show-ref refs/heads/"${branch}") ]] ; + then is_branch_existing=true _branch="${branch}" else @@ -374,91 +384,162 @@ function detailedGitStats() { fi # Prompt message - if [[ "${is_branch_existing}" && -n "${_branch}" ]]; then + if [[ "${is_branch_existing}" && -n "${_branch}" ]]; + then optionPicked "Contribution stats (by author) on ${_branch} branch:" - elif [[ -n "${branch}" && -z "${_branch}" ]]; then + elif [[ -n "${branch}" && -z "${_branch}" ]]; + then optionPicked "Branch ${branch} does not exist." optionPicked "Contribution stats (by author) on the current branch:" else optionPicked "Contribution stats (by author) on the current branch:" fi + local sort_by="${_GIT_SORT_BY:-name-asc}" + local sort_field + local sort_order + local sort_command + + sort_field=$(echo "$sort_by" | awk -F- '{print $1}') + sort_order=$(echo "$sort_by" | awk -F- '{print $2}') + + local sort_key + case "$sort_field" in + name) sort_key=1 ;; + commits) sort_key=2 ;; + insertions) sort_key=3 ;; + deletions) sort_key=4 ;; + lines) sort_key=5 ;; + *) + echo "Invalid sort field: $sort_field. Defaulting to 'name'." + sort_key=1 + ;; + esac + + echo -e "Sorting by: $sort_field ($sort_order)\n" + + local sort_flags="-t'|' -k${sort_key},${sort_key}" + if [[ "$sort_field" != "name" ]]; then + sort_flags="${sort_flags}n" + fi + if [[ "$sort_order" == "desc" ]]; then + sort_flags="${sort_flags}r" + fi + sort_command="sort ${sort_flags}" + + # 1. git log -> awk (extract data) -> sort -> awk (format output with graphs) git -c log.showSignature=false log ${_branch} --use-mailmap $_merges --numstat \ --pretty="format:commit %H%nAuthor: %aN <%aE>%nDate: %ad%n%n%w(0,4,4)%B%n" \ - "$_since" "$_until" $_log_options $_pathspec | LC_ALL=C awk ' - function printStats(author) { - printf "\t%s:\n", author - - if(more["total"] > 0) { - printf "\t insertions: %d\t(%.0f%%)\n", more[author], \ - (more[author] / more["total"] * 100) - } else { - printf "\t insertions: %d\t(%.0f%%)\n", 0, 0 - } - - if(less["total"] > 0) { - printf "\t deletions: %d\t(%.0f%%)\n", less[author], \ - (less[author] / less["total"] * 100) - } else { - printf "\t deletions: %d\t(%.0f%%)\n", 0, 0 - } - - if(file["total"] > 0) { - printf "\t files: %d\t(%.0f%%)\n", file[author], \ - (file[author] / file["total"] * 100) - } - - if(commits["total"] > 0) { - printf "\t commits: %d\t(%.0f%%)\n", commits[author], \ - (commits[author] / commits["total"] * 100) - } - - if (first[author] != "") { - if ( ((more["total"] + less["total"]) * 100) > 0) { - printf "\t lines changed: %d\t", more[author] + less[author] - printf "(%.0f%%)\n", ((more[author] + less[author]) / \ - (more["total"] + less["total"]) * 100) - } - else { - printf "\t lines changed: %d\t(0%%)\n", (more[author] + less[author]) - } - printf "\t first commit: %s\n", first[author] - printf "\t last commit: %s\n", last[author] - } - - printf "\n" - } - + "$_since" "$_until" $_log_options $_pathspec | + LC_ALL=C awk ' + # This first awk script extracts raw data into a delimited format /^Author:/ { - $1 = "" - author = $0 - commits[author] += 1 - commits["total"] += 1 + $1 = ""; author = $0; + commits[author] += 1; } - /^Date:/ { - $1=""; - first[author] = substr($0, 2) - if(last[author] == "" ) { last[author] = first[author] } + $1=""; current_date = substr($0, 2); + if (last[author] == "") { last[author] = current_date; } + first[author] = current_date; } - /^[0-9]/ { - more[author] += $1 - less[author] += $2 - - file[author] += 1 - more["total"] += $1 - less["total"] += $2 - file["total"] += 1 + more[author] += $1; + less[author] += $2; + file[author] += 1; } - END { - for (author in commits) { - if (author != "total") { - printStats(author) + for (author in commits) { + lines_changed = more[author] + less[author]; + printf "%s|%d|%d|%d|%d|%d|%s|%s\n", + author, commits[author]+0, more[author]+0, less[author]+0, + lines_changed, file[author]+0, first[author], last[author]; } + }' | + eval "$sort_command" | + LC_ALL=C awk ' + # This second awk script stores data, calculates totals, and then formats output with graphs + BEGIN { + FS = "|"; + total_commits = 0; total_insertions = 0; total_deletions = 0; + total_lines = 0; total_files = 0; + num_authors = 0; # Counter for stored authors } - printStats("total") + { + # Store all data for a second pass after totals are known + authors[num_authors] = $1; + commits_arr[num_authors] = $2; + insertions_arr[num_authors] = $3; + deletions_arr[num_authors] = $4; + lines_changed_arr[num_authors] = $5; + files_arr[num_authors] = $6; + first_commit_arr[num_authors] = $7; + last_commit_arr[num_authors] = $8; + + # Accumulate overall totals + total_commits += $2; + total_insertions += $3; + total_deletions += $4; + total_lines += $5; + total_files += $6; + num_authors++; + } + END { + for (j = 0; j < num_authors; j++) { + author = authors[j]; + current_commits = commits_arr[j]; + current_insertions = insertions_arr[j]; + current_deletions = deletions_arr[j]; + current_lines_changed = lines_changed_arr[j]; + current_files = files_arr[j]; + current_first_commit = first_commit_arr[j]; + current_last_commit = last_commit_arr[j]; + + printf "\t%s:\n", author; + + # Commits graph + if (total_commits > 0) { + commit_percent = (current_commits * 100.0) / total_commits; + printf "\t commits: %d (%.1f%%)\n", current_commits, commit_percent; + } else { + printf "\t commits: %d\n", current_commits; + } + + # Insertions graph + if (total_insertions > 0) { + insert_percent = (current_insertions * 100.0) / total_insertions; + printf "\t insertions: %d (%.1f%%)\n", current_insertions, insert_percent; + } else { + printf "\t insertions: %d\n", current_insertions; + } + + # Deletions graph + if (total_deletions > 0) { + delete_percent = (current_deletions * 100.0) / total_deletions; + printf "\t deletions: %d (%.1f%%)\n", current_deletions, delete_percent; + } else { + printf "\t deletions: %d\n", current_deletions; + } + + # Lines changed graph + if (total_lines > 0) { + lines_percent = (current_lines_changed * 100.0) / total_lines; + printf "\t lines changed: %d (%.1f%%)\n", current_lines_changed, lines_percent; + } else { + printf "\t lines changed: %d\n", current_lines_changed; + } + + printf "\t files: %d\n", current_files; + printf "\t first commit: %s\n", current_first_commit; + printf "\t last commit: %s\n\n", current_last_commit; + } + + # Print overall totals + printf "\t%s:\n", " total"; + printf "\t commits: %d\n", total_commits; + printf "\t insertions: %d\n", total_insertions; + printf "\t deletions: %d\n", total_deletions; + printf "\t lines changed: %d\n", total_lines; + printf "\t files: %d\n\n", total_files; }' } @@ -473,7 +554,8 @@ function changelogs() { local author="${1:-}" local next=$(date +%F) - if [[ -z "${author}" ]]; then + if [[ -z "${author}" ]]; + then optionPicked "Git changelogs:" _author="--author=**" else @@ -494,7 +576,8 @@ function changelogs() { --since="$DATE 00:00:00" --until="$DATE 23:59:59" \ --date-order) - if [[ -n "$commits" ]]; then + if [[ -n "$commits" ]]; + then echo -e "\n[$DATE]" echo "$commits" else @@ -543,9 +626,11 @@ function csvOutput() { local _branch="" # Check if requesting for a specific branch - if [[ -n "${branch}" ]]; then + if [[ -n "${branch}" ]]; + then # Check if branch exist - if [[ $(git show-ref refs/heads/"${branch}") ]] ; then + if [[ $(git show-ref refs/heads/"${branch}") ]] ; + then is_branch_existing=true _branch="${branch}" else @@ -660,7 +745,7 @@ function toJsonProp() { # add in hold buffer and loop while the string is not finished. /^'"$propTag"',?\r?$/ ! { H; n; b eos; } # end of the string, flip buffer to current pattern. - # keeps the comma if any, or a space as an empty placeholder. + # keeps the comma if any, or a space as an empty placeholder. /,\r?$/ ! { x; s/\r?$/ / } /,\r?$/ { x; s/\r?$/,/ } } @@ -790,7 +875,8 @@ function commitsPerAuthor() { $_merges "$_since" "$_until" $_log_options \ | grep -i '^ Co-Authored-by:' | cut -c21-) - if [[ -z "${coAuthorCommits}" ]]; then + if [[ -z "${coAuthorCommits}" ]]; + then allCommits="${authorCommits}" else allCommits="${authorCommits}\n${coAuthorCommits}" @@ -832,7 +918,8 @@ function parse_year() { local default_git_date_regex # Handle the raw UNIX timestamp format i.e. 1697375696 +0000 - if [[ "$date_str" =~ ^[0-9]+(\ [+-][0-9]{4})?$ ]]; then + if [[ "$date_str" =~ ^[0-9]+(\ [+-][0-9]{4})?$ ]]; + then timestamp=$(echo "$date_str" | awk '{print $1}') year=$(date -d "@$timestamp" '+%Y' 2>/dev/null) else @@ -847,10 +934,12 @@ function parse_year() { default_git_date_regex+='[0-9]{4}\ ' # Year default_git_date_regex+='[+-][0-9]{4}$' # Timezone offset - if [[ "$date_str" =~ $default_git_date_regex ]]; then + if [[ "$date_str" =~ $default_git_date_regex ]]; + then # Move the year before the time to match a format that Date can parse date_str=$(echo "$date_str" | awk '{print $1, $2, $3, $5, $4, $6}') - elif [[ "$date_str" =~ ^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4} ]]; then + elif [[ "$date_str" =~ ^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4} ]]; + then # Handle DD/MM/YYYY format date_str=$(echo "$date_str" | awk -F'/' '{print $2"/"$1"/"$3}') fi @@ -880,11 +969,14 @@ function commitsByYear() { echo -e "\tyear\tsum" # Add time strings to make these a touch more robust - for year in $(seq "$startYear" "$endYear"); do - if [[ "$year" = "$startYear" ]]; then + for year in $(seq "$startYear" "$endYear"); + do + if [[ "$year" = "$startYear" ]]; + then __since=$_since __until="--until=$year-12-31 23:59:59" - elif [[ "$year" = "$endYear" ]]; then + elif [[ "$year" = "$endYear" ]]; + then __since="--since=$year-01-01 00:00:00" __until=$_until else @@ -907,14 +999,11 @@ function commitsByYear() { } END{ for (year in count) { - s="|"; - if (total > 0) { - percent = ((count[year] / total) * 100) / 1.25; - for (i = 1; i <= percent; ++i) { + s="|"; if (total > 0) { + percent = ((count[year] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf( "\t%s\t%-0s\t%s\n", year, count[year], s ); - } + printf( "\t%s\t%-0s\t%s\n", year, count[year], s ); } } }' | sort } @@ -940,14 +1029,11 @@ function commitsByMonth() { } END{ for (month in count) { - s="|"; - if (total > 0) { - percent = ((count[month] / total) * 100) / 1.25; - for (i = 1; i <= percent; ++i) { + s="|"; if (total > 0) { + percent = ((count[month] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf( "\t%s\t%-0s\t%s\n", month, count[month], s ); - } + printf( "\t%s\t%-0s\t%s\n", month, count[month], s ); } } }' | LC_TIME="en_EN.UTF-8" sort -M } @@ -961,7 +1047,8 @@ function commitsByWeekday() { local author="${1:-}" local _author="" - if [[ -z "${author}" ]]; then + if [[ -z "${author}" ]]; + then optionPicked "Git commits by weekday:" _author="--author=**" else @@ -975,26 +1062,21 @@ function commitsByWeekday() { do echo -en "\t$counter\t$i\t" git -c log.showSignature=false shortlog -n $_merges --format='%ad %s' \ - "${_author}" "$_since" "$_until" $_log_options | - grep -cE "^ * $i \w\w\w [0-9]{1,2} " || continue + "${_author}" "$_since" "$_until" $_log_options | grep -cE "^ * $i \w\w\w [0-9]{1,2} " || continue counter=$((counter+1)) done | awk '{ } NR == FNR { - count[$1" "$2] = $3; - total += $3; + count[$1" "$2] = $3; total += $3; next } END{ for (day in count) { - s="|"; - if (total > 0) { - percent = ((count[day] / total) * 100) / 1.25; - for (i = 1; i <= percent; ++i) { + s="|"; if (total > 0) { + percent = ((count[day] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf("\t%s\t%s\t%-0s\t%s\n", substr(day,0,1), substr(day,3,5), count[day], s); - } + printf("\t%s\t%s\t%-0s\t%s\n", substr(day,0,1), substr(day,3,5), count[day], s); } } }' | sort -k 1 -n | awk '{$1=""}1' | awk '{$1=$1}1' \ | awk '{printf("\t%s\t%s\t%s\n", $1, $2, $3)}' @@ -1009,7 +1091,8 @@ function commitsByHour() { local author="${1:-}" local _author="" - if [[ -z "${author}" ]]; then + if [[ -z "${author}" ]]; + then optionPicked "Git commits by hour:" _author="--author=**" else @@ -1023,22 +1106,18 @@ function commitsByHour() { do echo -ne "\t$i\t" git -c log.showSignature=false shortlog -n $_merges --format='%ad %s' \ - "${_author}" "$_since" "$_until" $_log_options | - grep -cE '[0-9] '$i':[0-9]' || continue + "${_author}" "$_since" "$_until" $_log_options | grep -cE '[0-9] '$i':[0-9]' || continue done | awk '{ count[$1] = $2 total += $2 } END{ for (hour in count) { - s="|"; - if (total > 0) { - percent = ((count[hour] / total) * 100) / 1.25; - for (i = 1; i <= percent; ++i) { + s="|"; if (total > 0) { + percent = ((count[hour] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf( "\t%s\t%-0s\t%s\n", hour, count[hour], s ); - } + printf( "\t%s\t%-0s\t%s\n", hour, count[hour], s ); } } }' | sort } @@ -1052,7 +1131,8 @@ function commitsByTimezone() { local author="${1:-}" local _author="" - if [[ -z "${author}" ]]; then + if [[ -z "${author}" ]]; + then optionPicked "Git commits by timezone:" _author="--author=**" else @@ -1100,31 +1180,32 @@ if ! git rev-parse --is-inside-work-tree > /dev/null; then exit 1 fi -# Parse non-interative commands -if [[ "$#" -eq 1 ]]; then + +# Parse non-interactive commands +while [[ $# -gt 0 ]]; do case "$1" in # GENERATE OPTIONS - -T|--detailed-git-stats) detailedGitStats;; + -T|--detailed-git-stats) detailedGitStats; shift;; -R|--git-stats-by-branch) branch="${_GIT_BRANCH:-}" while [[ -z "${branch}" ]]; do read -r -p "Which branch? " branch done - detailedGitStats "${branch}";; - -c|--changelogs) changelogs;; + detailedGitStats "${branch}"; shift;; + -c|--changelogs) changelogs; shift;; -L|--changelogs-by-author) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - changelogs "${author}";; - -S|--my-daily-stats) myDailyStats;; + changelogs "${author}"; shift;; + -S|--my-daily-stats) myDailyStats; shift;; -V|--csv-output-by-branch) branch="${_GIT_BRANCH:-}" while [[ -z "${branch}" ]]; do read -r -p "Which branch? " branch done - csvOutput "${branch}";; + csvOutput "${branch}"; shift;; -j|--json-output) json_path="" while [[ -z "${json_path}" ]]; do @@ -1142,11 +1223,11 @@ if [[ "$#" -eq 1 ]]; then json_path="" fi done - jsonOutput "${json_path}";; + jsonOutput "${json_path}"; shift;; # LIST OPTIONS - -b|--branch-tree) branchTree;; - -D|--branches-by-date) branchesByDate;; - -C|--contributors) contributors;; + -b|--branch-tree) branchTree; shift;; + -D|--branches-by-date) branchesByDate; shift;; + -C|--contributors) contributors; shift;; -n|--new-contributors) newDate="" while [[ -z "${newDate}" ]]; do @@ -1160,131 +1241,132 @@ if [[ "$#" -eq 1 ]]; then newDate="" fi done - newContributors "${newDate}";; - -a|--commits-per-author) commitsPerAuthor;; - -d|--commits-per-day) commitsPerDay;; - -Y|--commits-by-year ) commitsByYear;; - -m|--commits-by-month) commitsByMonth;; - -w|--commits-by-weekday) commitsByWeekday;; + newContributors "${newDate}"; shift;; + -a|--commits-per-author) commitsPerAuthor; shift;; + -d|--commits-per-day) commitsPerDay; shift;; + -Y|--commits-by-year ) commitsByYear; shift;; + -m|--commits-by-month) commitsByMonth; shift;; + -w|--commits-by-weekday) commitsByWeekday; shift;; -W|--commits-by-author-by-weekday) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByWeekday "${author}";; - -o|--commits-by-hour) commitsByHour;; + commitsByWeekday "${author}"; shift;; + -o|--commits-by-hour) commitsByHour; shift;; -A|--commits-by-author-by-hour) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByHour "${author}";; - -z|--commits-by-timezone) commitsByTimezone;; + commitsByHour "${author}"; shift;; + -z|--commits-by-timezone) commitsByTimezone; shift;; -Z|--commits-by-author-by-timezone) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByTimezone "${author}";; + commitsByTimezone "${author}"; shift;; # ACTIVITY OPTIONS -k|--commits-calendar-by-author) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsCalendarByAuthor "${author}";; + commitsCalendarByAuthor "${author}"; shift;; # SUGGEST OPTIONS - -r|--suggest-reviewers) suggestReviewers;; - -h|-\?|--help) usage;; - *) echo "Invalid argument"; usage; exit 1;; + -r|--suggest-reviewers) suggestReviewers; shift;; + -h|-\?|--help) usage; shift;; + *) echo "Invalid argument: $1"; usage; exit 1;; esac exit 0; -fi -[[ "$#" -gt 1 ]] && { echo "Invalid arguments"; usage; exit 1; } - -# Parse interactive commands -clear -showMenu - -while [[ "${opt}" != "" ]]; do - clear - case "${opt}" in - 1) detailedGitStats; showMenu;; - 2) branch="" - while [[ -z "${branch}" ]]; do - read -r -p "Which branch? " branch - done - detailedGitStats "${branch}"; showMenu;; - 3) changelogs; showMenu;; - 4) author="" - while [[ -z "${author}" ]]; do - read -r -p "Which author? " author - done - changelogs "${author}"; showMenu;; - 5) myDailyStats; showMenu;; - 6) branch="" - while [[ -z "${branch}" ]]; do - read -r -p "Which branch? " branch - done - csvOutput "${branch}"; showMenu;; - 7) json_path="" - while [[ -z "${json_path}" ]]; do - echo "NOTE: This feature is in beta!" - echo "The file name will be saved as \"output.json\"." - echo "The full path must be provided." - echo "Variables, subshell commands, or shorthands such as ~ may not be valid." - echo "You do not need the final slash at the end of a directory path." - echo "You must have write permission to the folder you are trying to save this to." - echo "This feature only works interactively and cannot be combined with other options." - echo -e "Example of a valid path: /home/$(whoami)\n" - read -r -p "Please provide the full path to directory to save JSON file: " json_path - if [[ ! -w "${json_path}" ]]; then - echo "Invalid path or permission denied to write to given area." - json_path="" - fi - done - jsonOutput "${json_path}"; showMenu;; - 8) branchTree; showMenu;; - 9) branchesByDate; showMenu;; - 10) contributors; showMenu;; - 11) newDate="" - while [[ -z "${newDate}" ]]; do - read -r -p "Since what date? (e.g. '2023-04-13', '13 April 2023', 'last Thursday') " newDate - # Test if the date provide is valid and try again if it isn't. - if ! date -d "${newDate}" +%s > /dev/null 2>&1; then - newDate="" - fi - done - newContributors "${newDate}"; showMenu;; - 12) commitsPerAuthor; showMenu;; - 13) commitsPerDay; showMenu;; - 14) commitsByMonth; showMenu;; - 15) commitsByYear; showMenu;; - 16) commitsByWeekday; showMenu;; - 17) author="" - while [[ -z "${author}" ]]; do - read -r -p "Which author? " author - done - commitsByWeekday "${author}"; showMenu;; - 18) commitsByHour; showMenu;; - 19) author="" - while [[ -z "${author}" ]]; do - read -r -p "Which author? " author - done - commitsByHour "${author}"; showMenu;; - 20) commitsByTimezone; showMenu;; - 21) author="" - while [[ -z "${author}" ]]; do - read -r -p "Which author? " author - done - commitsByTimezone "${author}"; showMenu;; - 22) suggestReviewers; showMenu;; - 23) author="" - while [[ -z "${author}" ]]; do - read -r -p "Which author? " author - done - commitsCalendarByAuthor "${author}"; showMenu;; - q|"\n") exit;; - *) clear; optionPicked "Pick an option from the menu"; showMenu;; - esac done + +# If no args, run interactive mode +if [[ "$#" -eq 0 ]]; then + # Parse interactive commands + clear + showMenu + + while [[ "${opt}" != "" ]]; do + clear + case "${opt}" in + 1) detailedGitStats; showMenu;; + 2) branch="" + while [[ -z "${branch}" ]]; do + read -r -p "Which branch? " branch + done + detailedGitStats "${branch}"; showMenu;; + 3) changelogs; showMenu;; + 4) author="" + while [[ -z "${author}" ]]; do + read -r -p "Which author? " author + done + changelogs "${author}"; showMenu;; + 5) myDailyStats; showMenu;; + 6) branch="" + while [[ -z "${branch}" ]]; do + read -r -p "Which branch? " branch + done + csvOutput "${branch}"; showMenu;; + 7) json_path="" + while [[ -z "${json_path}" ]]; do + echo "NOTE: This feature is in beta!" + echo "The file name will be saved as \"output.json\"." + echo "The full path must be provided." + echo "Variables, subshell commands, or shorthands such as ~ may not be valid." + echo "You do not need the final slash at the end of a directory path." + echo "You must have write permission to the folder you are trying to save this to." + echo "This feature only works interactively and cannot be combined with other options." + echo -e "Example of a valid path: /home/$(whoami)\n" + read -r -p "Please provide the full path to directory to save JSON file: " json_path + if [[ ! -w "${json_path}" ]]; then + echo "Invalid path or permission denied to write to given area." + json_path="" + fi + done + jsonOutput "${json_path}"; showMenu;; + 8) branchTree; showMenu;; + 9) branchesByDate; showMenu;; + 10) contributors; showMenu;; + 11) newDate="" + while [[ -z "${newDate}" ]]; do + read -r -p "Since what date? (e.g. '2023-04-13', '13 April 2023', 'last Thursday') " newDate + if ! date -d "${newDate}" +%s > /dev/null 2>&1; then + newDate="" + fi + done + newContributors "${newDate}"; showMenu;; + 12) commitsPerAuthor; showMenu;; + 13) commitsPerDay; showMenu;; + 14) commitsByMonth; showMenu;; + 15) commitsByYear; showMenu;; + 16) commitsByWeekday; showMenu;; + 17) author="" + while [[ -z "${author}" ]]; do + read -r -p "Which author? " author + done + commitsByWeekday "${author}"; showMenu;; + 18) commitsByHour; showMenu;; + 19) author="" + while [[ -z "${author}" ]]; do + read -r -p "Which author? " author + done + commitsByHour "${author}"; showMenu;; + 20) commitsByTimezone; showMenu;; + 21) author="" + while [[ -z "${author}" ]]; do + read -r -p "Which author? " author + done + commitsByTimezone "${author}"; showMenu;; + 22) suggestReviewers; showMenu;; + 23) author="" + while [[ -z "${author}" ]]; do + read -r -p "Which author? " author + done + commitsCalendarByAuthor "${author}"; showMenu;;\ + q|"\n") exit;;\ + *) clear; optionPicked "Pick an option from the menu"; showMenu;;\ + esac + done +fi diff --git a/git-quick-stats.1 b/git-quick-stats.1 index 34dfb51..61f328d 100644 --- a/git-quick-stats.1 +++ b/git-quick-stats.1 @@ -165,6 +165,10 @@ You can also set _GIT_MERGE_VIEW to only show merge commits, example: .PP .B export _GIT_MERGE_VIEW="exclusive" .PP +You can sort contribution stats by field name, commits, insertions, deletions, or lines (total lines changed) and order (asc, desc). e.g.: commits-desc +.PP +.B export _GIT_SORT_BY="name-asc" +.PP You can switch to the legacy color scheme, example: .PP .B export _MENU_THEME=legacy diff --git a/tests/commands_test.sh b/tests/commands_test.sh index 9e2acf5..447e7fb 100755 --- a/tests/commands_test.sh +++ b/tests/commands_test.sh @@ -11,7 +11,7 @@ . tests/assert.sh -v src="./git-quick-stats" -assert "$src fail" "Invalid argument +assert "$src fail" "Invalid argument: fail NAME git-quick-stats - Simple and efficient way to access various stats in a git repo @@ -23,9 +23,8 @@ SYNOPSIS DESCRIPTION Any git repository contains tons of information about commits, contributors, and files. Extracting this information is not always trivial, mostly because - of a gadzillion options to a gadzillion git commands. - - This program allows you to see detailed information about a git repository. + of a gadzillion options to a gadzillion git commands. This program allows + you to see detailed information about a git repository. GENERATE OPTIONS -T, --detailed-git-stats @@ -81,11 +80,11 @@ SUGGEST OPTIONS ADDITIONAL USAGE You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log - ex: export _GIT_SINCE=\"2017-01-20\" + ex: export _GIT_SINCE="2017-01-20" You can set _GIT_LIMIT for limited output log ex: export _GIT_LIMIT=20 You can set _GIT_LOG_OPTIONS for git log options - ex: export _GIT_LOG_OPTIONS=\"--ignore-all-space --ignore-blank-lines\" + ex: export _GIT_LOG_OPTIONS="--ignore-all-space --ignore-blank-lines" You can exclude directories or files from the stats by using pathspec ex: export _GIT_PATHSPEC=':!pattern' You can set _GIT_MERGE_VIEW to view merge commits with normal commits @@ -97,14 +96,16 @@ ADDITIONAL USAGE You can set _GIT_BRANCH to set the branch of the stats ex: export _GIT_BRANCH=master You can set _GIT_IGNORE_AUTHORS to filter out specific authors - ex: export _GIT_IGNORE_AUTHORS=\"(author1|author2)\"" + ex: export _GIT_IGNORE_AUTHORS=\"(author1|author2)\" + You can sort contribution stats by field name, commits, insertions, deletions, or lines - total lines changed and order - asc, desc. e.g.: commits-desc + ex: export _GIT_SORT_BY=name-asc" assert_raises "$src fail" 1 assert_contains "$src --suggest-reviewers" "Suggested code reviewers (based on git history)" assert_success "$src --suggest-reviewers" -assert_contains "$src --detailed-git-stats" "Contribution stats" +assert_contains "$src --detailed-git-stats" "Contribution stats (by author) on the current branch" assert_success "$src --detailed-git-stats" assert_contains "$src --commits-per-day" "Git commits per date" From b65b100cd853350a728d3acb2b98523b0cccb779 Mon Sep 17 00:00:00 2001 From: arzzen Date: Mon, 16 Jun 2025 14:29:24 +0200 Subject: [PATCH 2/5] fix tests --- git-quick-stats | 153 ++++++++++++++++++++--------------------- tests/commands_test.sh | 11 +-- 2 files changed, 81 insertions(+), 83 deletions(-) diff --git a/git-quick-stats b/git-quick-stats index 952206a..2228ad6 100755 --- a/git-quick-stats +++ b/git-quick-stats @@ -14,8 +14,7 @@ set -o errexit # If $_GIT_SINCE is never set, look at the repository to find the first date. # NOTE: previously this put the date at the fixed GIT epoch (May 2005) _since=${_GIT_SINCE:-} -if [[ -n "${_since}" ]]; -then +if [[ -n "${_since}" ]]; then _since="--since=$_since" else _since="--since=$(git log --reverse --format='%ad' --date=iso | head -n1)" @@ -25,8 +24,7 @@ fi # If $_GIT_UNTIL is never set, choose the latest system # time from the user's current environment _until=${_GIT_UNTIL:-} -if [[ -n "${_until}" ]]; -then +if [[ -n "${_until}" ]]; then _until="--until=$_until" else _until="--until=$(LC_TIME=C date '+%a, %d %b %Y %H:%M:%S %Z')" @@ -35,8 +33,7 @@ fi # Set files or directories to be excluded in stats # If $_GIT_PATHSPEC is not set, shift over the option completely _pathspec=${_GIT_PATHSPEC:-} -if [[ -n "${_pathspec}" ]]; -then +if [[ -n "${_pathspec}" ]]; then _pathspec="-- $_pathspec" else _pathspec="--" @@ -47,11 +44,9 @@ fi # Enable shows regular commits together with normal commits _merges=${_GIT_MERGE_VIEW:-} _merges=$(echo "$_merges" | awk '{print tolower($0)}') -if [[ "${_merges}" == "exclusive" ]]; -then +if [[ "${_merges}" == "exclusive" ]]; then _merges="--merges" -elif [[ "${_merges}" == "enable" ]]; -then +elif [[ "${_merges}" == "enable" ]]; then _merges="" else _merges="--no-merges" @@ -59,8 +54,7 @@ fi # Limit git log output _limit=${_GIT_LIMIT:-} -if [[ -n "${_limit}" ]]; -then +if [[ -n "${_limit}" ]]; then _limit=$_limit else _limit=10 @@ -68,8 +62,7 @@ fi # Log options _log_options=${_GIT_LOG_OPTIONS:-} -if [[ -n "${_log_options}" ]]; -then +if [[ -n "${_log_options}" ]]; then _log_options=$_log_options else _log_options="" @@ -77,8 +70,7 @@ fi # Ignore author regex _ignore_authors=${_GIT_IGNORE_AUTHORS:-} -if [[ -n "${_ignore_authors}" ]]; -then +if [[ -n "${_ignore_authors}" ]]; then _ignore_authors=$_ignore_authors else _ignore_authors="" @@ -87,8 +79,7 @@ fi # Sort by field and order for contribution stats _GIT_SORT_BY=${_GIT_SORT_BY:-name-asc} # If the user has not set a sort order, default to name-asc -if [[ ! "$_GIT_SORT_BY" =~ ^(name|commits|insertions|deletions|lines)-(asc|desc)$ ]]; -then +if [[ ! "$_GIT_SORT_BY" =~ ^(name|commits|insertions|deletions|lines)-(asc|desc)$ ]]; then echo "Invalid sort option: $_GIT_SORT_BY. Defaulting to 'name-asc'." _GIT_SORT_BY="name-asc" fi @@ -113,25 +104,40 @@ function commitsCalendarByAuthor() { --date=iso --author="$author" "$_since" "$_until" $_log_options \ --pretty='%ad' $_pathspec | awk ' { - split($0, a, " "); # a[1] = YYYY-MM-DD - split(a[1], date_fields, "-"); mon = date_fields[2] + 0; + split($0, a, " "); + # a[1] = YYYY-MM-DD + split(a[1], date_fields, "-"); + mon = date_fields[2] + 0; cmd = "date -d \"" a[1] "\" +%u"; cmd | getline weekday; - close(cmd); # weekday: 1=Mon, ..., 7=Sun - count[weekday][mon]++; } + close(cmd); + # weekday: 1=Mon, ..., 7=Sun + count[weekday][mon]++; + } END { # Output matrix for (d=1; d<=7; d++) { - if (d==1) printf "Mon "; else if (d==2) printf "Tue "; - else if (d==3) printf "Wed "; else if (d==4) printf "Thu "; - else if (d==5) printf "Fri "; else if (d==6) printf "Sat "; - else if (d==7) printf "Sun "; for (m=1; m<=12; m++) { - c = count[d][m]+0; if (c==0) out="..."; - else if (c<=9) out="░░░"; - else if (c<=19) out="▒▒▒"; else out="▓▓▓"; + if (d==1) printf "Mon "; + else if (d==2) printf "Tue "; + else if (d==3) printf "Wed "; + else if (d==4) printf "Thu "; + else if (d==5) printf "Fri "; + else if (d==6) printf "Sat "; + else if (d==7) printf "Sun "; + for (m=1; m<=12; m++) { + c = count[d][m]+0; + if (c==0) + out="..."; + else if (c<=9) + out="░░░"; + else if (c<=19) + out="▒▒▒"; + else + out="▓▓▓"; printf "%s%s", out, (m<12?" ":"\n"); } } - printf "\nLegend: ... = 0 ░░░ = 1–9 ▒▒▒ = 10–19 ▓▓▓ = 20+ commits\n"; } + printf "\nLegend: ... = 0 ░░░ = 1–9 ▒▒▒ = 10–19 ▓▓▓ = 20+ commits\n"; + } ' } @@ -200,8 +206,9 @@ SYNOPSIS DESCRIPTION Any git repository contains tons of information about commits, contributors, and files. Extracting this information is not always trivial, mostly because - of a gadzillion options to a gadzillion git commands. This program allows - you to see detailed information about a git repository. + of a gadzillion options to a gadzillion git commands. + + This program allows you to see detailed information about a git repository. GENERATE OPTIONS -T, --detailed-git-stats @@ -274,7 +281,7 @@ ADDITIONAL USAGE ex: export _GIT_BRANCH=master You can set _GIT_IGNORE_AUTHORS to filter out specific authors ex: export _GIT_IGNORE_AUTHORS=\"(author1|author2)\" - You can sort contribution stats by field name, commits, insertions, deletions, or lines - total lines changed and order - asc, desc. e.g.: commits-desc + You can sort contribution stats by field name, commits, insertions, deletions, or lines - total lines changed and order - asc, desc ex: export _GIT_SORT_BY=name-asc" } @@ -298,8 +305,7 @@ function showMenu() { EXIT_TXT="" # Adjustable color menu option - if [[ "${_theme}" == "legacy" ]]; - then + if [[ "${_theme}" == "legacy" ]]; then TITLES="${BOLD}${RED}" TEXT="${NORMAL}${CYAN}" NUMS="${BOLD}${YELLOW}" @@ -346,8 +352,7 @@ function showMenu() { } filter_ignored_authors() { - if [[ -n "$_ignore_authors" ]]; - then + if [[ -n "$_ignore_authors" ]]; then grep -Ev "$_ignore_authors" else cat @@ -359,7 +364,7 @@ filter_ignored_authors() { ################################################################################ # DESC: Shows detailed contribution stats per author by parsing every commit in -# the repo and outputting their contribution stats, including bar graphs. +# the repo and outputting their contribution stats. # ARGS: $branch (optional): Users can specify an alternative branch instead of # the current default one # OUTS: None @@ -370,11 +375,9 @@ function detailedGitStats() { local _branch="" # Check if requesting for a specific branch - if [[ -n "${branch}" ]]; - then + if [[ -n "${branch}" ]]; then # Check if branch exist - if [[ $(git show-ref refs/heads/"${branch}") ]] ; - then + if [[ $(git show-ref refs/heads/"${branch}") ]] ; then is_branch_existing=true _branch="${branch}" else @@ -384,11 +387,9 @@ function detailedGitStats() { fi # Prompt message - if [[ "${is_branch_existing}" && -n "${_branch}" ]]; - then + if [[ "${is_branch_existing}" && -n "${_branch}" ]]; then optionPicked "Contribution stats (by author) on ${_branch} branch:" - elif [[ -n "${branch}" && -z "${_branch}" ]]; - then + elif [[ -n "${branch}" && -z "${_branch}" ]]; then optionPicked "Branch ${branch} does not exist." optionPicked "Contribution stats (by author) on the current branch:" else @@ -554,8 +555,7 @@ function changelogs() { local author="${1:-}" local next=$(date +%F) - if [[ -z "${author}" ]]; - then + if [[ -z "${author}" ]]; then optionPicked "Git changelogs:" _author="--author=**" else @@ -576,8 +576,7 @@ function changelogs() { --since="$DATE 00:00:00" --until="$DATE 23:59:59" \ --date-order) - if [[ -n "$commits" ]]; - then + if [[ -n "$commits" ]]; then echo -e "\n[$DATE]" echo "$commits" else @@ -626,8 +625,7 @@ function csvOutput() { local _branch="" # Check if requesting for a specific branch - if [[ -n "${branch}" ]]; - then + if [[ -n "${branch}" ]]; then # Check if branch exist if [[ $(git show-ref refs/heads/"${branch}") ]] ; then @@ -875,8 +873,7 @@ function commitsPerAuthor() { $_merges "$_since" "$_until" $_log_options \ | grep -i '^ Co-Authored-by:' | cut -c21-) - if [[ -z "${coAuthorCommits}" ]]; - then + if [[ -z "${coAuthorCommits}" ]]; then allCommits="${authorCommits}" else allCommits="${authorCommits}\n${coAuthorCommits}" @@ -918,8 +915,7 @@ function parse_year() { local default_git_date_regex # Handle the raw UNIX timestamp format i.e. 1697375696 +0000 - if [[ "$date_str" =~ ^[0-9]+(\ [+-][0-9]{4})?$ ]]; - then + if [[ "$date_str" =~ ^[0-9]+(\ [+-][0-9]{4})?$ ]]; then timestamp=$(echo "$date_str" | awk '{print $1}') year=$(date -d "@$timestamp" '+%Y' 2>/dev/null) else @@ -934,12 +930,10 @@ function parse_year() { default_git_date_regex+='[0-9]{4}\ ' # Year default_git_date_regex+='[+-][0-9]{4}$' # Timezone offset - if [[ "$date_str" =~ $default_git_date_regex ]]; - then + if [[ "$date_str" =~ $default_git_date_regex ]]; then # Move the year before the time to match a format that Date can parse date_str=$(echo "$date_str" | awk '{print $1, $2, $3, $5, $4, $6}') - elif [[ "$date_str" =~ ^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4} ]]; - then + elif [[ "$date_str" =~ ^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4} ]]; then # Handle DD/MM/YYYY format date_str=$(echo "$date_str" | awk -F'/' '{print $2"/"$1"/"$3}') fi @@ -971,12 +965,10 @@ function commitsByYear() { # Add time strings to make these a touch more robust for year in $(seq "$startYear" "$endYear"); do - if [[ "$year" = "$startYear" ]]; - then + if [[ "$year" = "$startYear" ]]; then __since=$_since __until="--until=$year-12-31 23:59:59" - elif [[ "$year" = "$endYear" ]]; - then + elif [[ "$year" = "$endYear" ]]; then __since="--since=$year-01-01 00:00:00" __until=$_until else @@ -999,11 +991,13 @@ function commitsByYear() { } END{ for (year in count) { - s="|"; if (total > 0) { + s="|"; + if (total > 0) { percent = ((count[year] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf( "\t%s\t%-0s\t%s\n", year, count[year], s ); } + printf( "\t%s\t%-0s\t%s\n", year, count[year], s ); + } } }' | sort } @@ -1029,11 +1023,13 @@ function commitsByMonth() { } END{ for (month in count) { - s="|"; if (total > 0) { + s="|"; + if (total > 0) { percent = ((count[month] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf( "\t%s\t%-0s\t%s\n", month, count[month], s ); } + printf( "\t%s\t%-0s\t%s\n", month, count[month], s ); + } } }' | LC_TIME="en_EN.UTF-8" sort -M } @@ -1047,8 +1043,7 @@ function commitsByWeekday() { local author="${1:-}" local _author="" - if [[ -z "${author}" ]]; - then + if [[ -z "${author}" ]]; then optionPicked "Git commits by weekday:" _author="--author=**" else @@ -1073,10 +1068,12 @@ function commitsByWeekday() { END{ for (day in count) { s="|"; if (total > 0) { - percent = ((count[day] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { + percent = ((count[day] / total) * 100) / 1.25; + for (i = 1; i <= percent; ++i) { s=s"█" } - printf("\t%s\t%s\t%-0s\t%s\n", substr(day,0,1), substr(day,3,5), count[day], s); } + printf("\t%s\t%s\t%-0s\t%s\n", substr(day,0,1), substr(day,3,5), count[day], s); + } } }' | sort -k 1 -n | awk '{$1=""}1' | awk '{$1=$1}1' \ | awk '{printf("\t%s\t%s\t%s\n", $1, $2, $3)}' @@ -1091,8 +1088,7 @@ function commitsByHour() { local author="${1:-}" local _author="" - if [[ -z "${author}" ]]; - then + if [[ -z "${author}" ]]; then optionPicked "Git commits by hour:" _author="--author=**" else @@ -1113,11 +1109,13 @@ function commitsByHour() { } END{ for (hour in count) { - s="|"; if (total > 0) { + s="|"; + if (total > 0) { percent = ((count[hour] / total) * 100) / 1.25; for (i = 1; i <= percent; ++i) { s=s"█" } - printf( "\t%s\t%-0s\t%s\n", hour, count[hour], s ); } + printf( "\t%s\t%-0s\t%s\n", hour, count[hour], s ); + } } }' | sort } @@ -1131,8 +1129,7 @@ function commitsByTimezone() { local author="${1:-}" local _author="" - if [[ -z "${author}" ]]; - then + if [[ -z "${author}" ]]; then optionPicked "Git commits by timezone:" _author="--author=**" else diff --git a/tests/commands_test.sh b/tests/commands_test.sh index 447e7fb..bf981b8 100755 --- a/tests/commands_test.sh +++ b/tests/commands_test.sh @@ -23,8 +23,9 @@ SYNOPSIS DESCRIPTION Any git repository contains tons of information about commits, contributors, and files. Extracting this information is not always trivial, mostly because - of a gadzillion options to a gadzillion git commands. This program allows - you to see detailed information about a git repository. + of a gadzillion options to a gadzillion git commands. + + This program allows you to see detailed information about a git repository. GENERATE OPTIONS -T, --detailed-git-stats @@ -80,11 +81,11 @@ SUGGEST OPTIONS ADDITIONAL USAGE You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log - ex: export _GIT_SINCE="2017-01-20" + ex: export _GIT_SINCE=\"2017-01-20\" You can set _GIT_LIMIT for limited output log ex: export _GIT_LIMIT=20 You can set _GIT_LOG_OPTIONS for git log options - ex: export _GIT_LOG_OPTIONS="--ignore-all-space --ignore-blank-lines" + ex: export _GIT_LOG_OPTIONS=\"--ignore-all-space --ignore-blank-lines\" You can exclude directories or files from the stats by using pathspec ex: export _GIT_PATHSPEC=':!pattern' You can set _GIT_MERGE_VIEW to view merge commits with normal commits @@ -97,7 +98,7 @@ ADDITIONAL USAGE ex: export _GIT_BRANCH=master You can set _GIT_IGNORE_AUTHORS to filter out specific authors ex: export _GIT_IGNORE_AUTHORS=\"(author1|author2)\" - You can sort contribution stats by field name, commits, insertions, deletions, or lines - total lines changed and order - asc, desc. e.g.: commits-desc + You can sort contribution stats by field name, commits, insertions, deletions, or lines - total lines changed and order - asc, desc ex: export _GIT_SORT_BY=name-asc" assert_raises "$src fail" 1 From ec6a95d2efd7d1f92f9a9df8be1705eb109709e9 Mon Sep 17 00:00:00 2001 From: arzzen Date: Mon, 16 Jun 2025 14:33:03 +0200 Subject: [PATCH 3/5] cleanup --- git-quick-stats | 53 +++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/git-quick-stats b/git-quick-stats index 2228ad6..6be52f5 100755 --- a/git-quick-stats +++ b/git-quick-stats @@ -1179,30 +1179,30 @@ fi # Parse non-interactive commands -while [[ $# -gt 0 ]]; do +if [[ "$#" -eq 1 ]]; then case "$1" in # GENERATE OPTIONS - -T|--detailed-git-stats) detailedGitStats; shift;; + -T|--detailed-git-stats) detailedGitStats;; -R|--git-stats-by-branch) branch="${_GIT_BRANCH:-}" while [[ -z "${branch}" ]]; do read -r -p "Which branch? " branch done - detailedGitStats "${branch}"; shift;; - -c|--changelogs) changelogs; shift;; + detailedGitStats "${branch}";; + -c|--changelogs) changelogs;; -L|--changelogs-by-author) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - changelogs "${author}"; shift;; - -S|--my-daily-stats) myDailyStats; shift;; + changelogs "${author}";; + -S|--my-daily-stats) myDailyStats;; -V|--csv-output-by-branch) branch="${_GIT_BRANCH:-}" while [[ -z "${branch}" ]]; do read -r -p "Which branch? " branch done - csvOutput "${branch}"; shift;; + csvOutput "${branch}";; -j|--json-output) json_path="" while [[ -z "${json_path}" ]]; do @@ -1220,11 +1220,11 @@ while [[ $# -gt 0 ]]; do json_path="" fi done - jsonOutput "${json_path}"; shift;; + jsonOutput "${json_path}";; # LIST OPTIONS - -b|--branch-tree) branchTree; shift;; - -D|--branches-by-date) branchesByDate; shift;; - -C|--contributors) contributors; shift;; + -b|--branch-tree) branchTree;; + -D|--branches-by-date) branchesByDate;; + -C|--contributors) contributors;; -n|--new-contributors) newDate="" while [[ -z "${newDate}" ]]; do @@ -1238,46 +1238,47 @@ while [[ $# -gt 0 ]]; do newDate="" fi done - newContributors "${newDate}"; shift;; - -a|--commits-per-author) commitsPerAuthor; shift;; - -d|--commits-per-day) commitsPerDay; shift;; - -Y|--commits-by-year ) commitsByYear; shift;; - -m|--commits-by-month) commitsByMonth; shift;; - -w|--commits-by-weekday) commitsByWeekday; shift;; + newContributors "${newDate}";; + -a|--commits-per-author) commitsPerAuthor;; + -d|--commits-per-day) commitsPerDay;; + -Y|--commits-by-year ) commitsByYear;; + -m|--commits-by-month) commitsByMonth;; + -w|--commits-by-weekday) commitsByWeekday;; -W|--commits-by-author-by-weekday) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByWeekday "${author}"; shift;; - -o|--commits-by-hour) commitsByHour; shift;; + commitsByWeekday "${author}";; + -o|--commits-by-hour) commitsByHour;; -A|--commits-by-author-by-hour) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByHour "${author}"; shift;; - -z|--commits-by-timezone) commitsByTimezone; shift;; + commitsByHour "${author}";; + -z|--commits-by-timezone) commitsByTimezone;; -Z|--commits-by-author-by-timezone) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByTimezone "${author}"; shift;; + commitsByTimezone "${author}";; # ACTIVITY OPTIONS -k|--commits-calendar-by-author) author="${_GIT_AUTHOR:-}" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsCalendarByAuthor "${author}"; shift;; + commitsCalendarByAuthor "${author}";; # SUGGEST OPTIONS - -r|--suggest-reviewers) suggestReviewers; shift;; - -h|-\?|--help) usage; shift;; + -r|--suggest-reviewers) suggestReviewers;; + -h|-\?|--help) usage;; *) echo "Invalid argument: $1"; usage; exit 1;; esac exit 0; -done +fi +[[ "$#" -gt 1 ]] && { echo "Invalid arguments"; usage; exit 1; } # If no args, run interactive mode if [[ "$#" -eq 0 ]]; then From 79c735c8145ec9abe5c61bf2a509d45eef1fa96c Mon Sep 17 00:00:00 2001 From: arzzen Date: Mon, 16 Jun 2025 14:36:11 +0200 Subject: [PATCH 4/5] fix backslash --- git-quick-stats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git-quick-stats b/git-quick-stats index 6be52f5..dabc228 100755 --- a/git-quick-stats +++ b/git-quick-stats @@ -1362,9 +1362,9 @@ if [[ "$#" -eq 0 ]]; then while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsCalendarByAuthor "${author}"; showMenu;;\ - q|"\n") exit;;\ - *) clear; optionPicked "Pick an option from the menu"; showMenu;;\ + commitsCalendarByAuthor "${author}"; showMenu;; + q|"\n") exit;; + *) clear; optionPicked "Pick an option from the menu"; showMenu;; esac done fi From 95136d3f92ee8a5720888a921e73a153c9816fb0 Mon Sep 17 00:00:00 2001 From: arzzen Date: Mon, 16 Jun 2025 14:40:49 +0200 Subject: [PATCH 5/5] add filter ignored authors --- git-quick-stats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-quick-stats b/git-quick-stats index dabc228..9c61ac2 100755 --- a/git-quick-stats +++ b/git-quick-stats @@ -456,7 +456,7 @@ function detailedGitStats() { lines_changed, file[author]+0, first[author], last[author]; } }' | - eval "$sort_command" | + eval "$sort_command" | filter_ignored_authors | LC_ALL=C awk ' # This second awk script stores data, calculates totals, and then formats output with graphs BEGIN {