mirror of
https://github.com/git-quick-stats/git-quick-stats.git
synced 2026-02-03 11:33:32 +01:00
* The previous commands were Lower CamelCase style and had no equivalent short options. If you wanted to see the branch tree via non-interactive mode, you always needed to supply "branchTree" as the passing argument to the git-quick-stats script. This commit changes the argument style to be more akin to the POSIX and GNU styles of arguments commonly seen in many other applications. As of this commit, there is no compatibility with legacy commands, so those who have been using the old commands will unfortunately need to get familiar with the new ones. All documentation and tests have been updated accordingly to reflect the new changes. * The main interactive loop contained a non-variable constant that was only getting parsed correctly due to legacy fallback behavior. This commit fixes the main loop and cleans up the formatting a little bit. * Added -r to more areas where read reads in a variable to help prevent it from mangling backslashes. * Changed everything to use bash's built-in [[ notation and did some minor formatting changes to reduce the LOC. * Removed some unnecessary echo statements and did some other minor cleanup.
441 lines
15 KiB
Bash
Executable File
441 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -o nounset
|
|
set -o errexit
|
|
|
|
_since=${_GIT_SINCE:-}
|
|
[[ -n "${_since}" ]] && _since="--since=$_since"
|
|
|
|
_until=${_GIT_UNTIL:-}
|
|
[[ -n "${_until}" ]] && _until="--until=$_until"
|
|
|
|
_pathspec=${_GIT_PATHSPEC:-}
|
|
[[ -n "${_pathspec}" ]] && _pathspec="-- $_pathspec"
|
|
|
|
|
|
_limit=${_GIT_LIMIT:-}
|
|
if [[ -n "${_limit}" ]];
|
|
then _limit=$_limit
|
|
else
|
|
_limit=10
|
|
fi
|
|
|
|
function check_utils() {
|
|
local HELP_MSG="not found. Please make sure this is installed and in PATH."
|
|
|
|
command -v awk >/dev/null 2>&1 || { echo >&2 "awk ${HELP_MSG}"; exit 1; }
|
|
command -v basename >/dev/null 2>&1 || { echo >&2 "basename ${HELP_MSG}"; exit 1; }
|
|
command -v cat > /dev/null 2>&1 || { echo >&2 "cat ${HELP_MSG}"; exit 1; }
|
|
command -v column > /dev/null 2>&1 || { echo >&2 "column ${HELP_MSG}"; exit 1; }
|
|
command -v echo > /dev/null 2>&1 || { echo >&2 "echo ${HELP_MSG}"; exit 1; }
|
|
command -v git > /dev/null 2>&1 || { echo >&2 "git ${HELP_MSG}"; exit 1; }
|
|
command -v grep > /dev/null 2>&1 || { echo >&2 "grep ${HELP_MSG}"; exit 1; }
|
|
command -v head > /dev/null 2>&1 || { echo >&2 "head ${HELP_MSG}"; exit 1; }
|
|
command -v seq > /dev/null 2>&1 || { echo >&2 "seq ${HELP_MSG}"; exit 1; }
|
|
command -v sort > /dev/null 2>&1 || { echo >&2 "sort ${HELP_MSG}"; exit 1; }
|
|
command -v tput > /dev/null 2>&1 || { echo >&2 "tput ${HELP_MSG}"; exit 1; }
|
|
command -v tr > /dev/null 2>&1 || { echo >&2 "tr ${HELP_MSG}"; exit 1; }
|
|
command -v uniq > /dev/null 2>&1 || { echo >&2 "uniq ${HELP_MSG}"; exit 1; }
|
|
command -v wc > /dev/null 2>&1 || { echo >&2 "wc ${HELP_MSG}"; exit 1; }
|
|
}
|
|
|
|
function usage() {
|
|
local program=$(basename "$0")
|
|
|
|
echo "
|
|
NAME
|
|
${program} - Simple and efficient way to access various stats in a git repo
|
|
|
|
SYNOPSIS
|
|
For non-interactive mode: ${program} [OPTIONS]
|
|
For interactive mode: ${program}
|
|
|
|
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.
|
|
|
|
OPTIONS
|
|
-r, --suggest-reviewers
|
|
show the best people to contact to review code
|
|
-T, --detailed-git-stats
|
|
give a detailed list of git stats
|
|
-d, --commits-per-day
|
|
displays a list of commits per day
|
|
-m, --commits-by-month
|
|
displays a list of commits per month
|
|
-w, --commits-by-weekday
|
|
displays a list of commits per weekday
|
|
-o, --commits-by-hour
|
|
displays a list of commits per hour
|
|
-A, --commits-by-author-by-hour
|
|
displays a list of commits per hour by author
|
|
-a, --commits-per-author
|
|
displays a list of commits per author
|
|
-S, --my-daily-stats
|
|
see your current daily stats
|
|
-C, --contributors
|
|
see a list of everyone who contributed to the repo
|
|
-b, --branch-tree
|
|
show an ASCII graph of the git repo branch history
|
|
-D, --branches-by-date
|
|
show branches by date
|
|
-c, --changelogs
|
|
see changelogs
|
|
-L, --changelogs-by-author
|
|
see changelogs by author
|
|
-h, -?, --help
|
|
display this help text in the terminal
|
|
|
|
ADDITIONAL USAGE
|
|
You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log
|
|
ex: export _GIT_SINCE=\"2017-20-01\"
|
|
You can set _GIT_LIMIT for limited output log
|
|
ex: export _GIT_LIMIT=20
|
|
You can exclude a directory from the stats by using pathspec
|
|
ex: export _GIT_PATHSPEC=':!directory'"
|
|
}
|
|
|
|
function show_menu() {
|
|
local NORMAL=$(tput sgr0)
|
|
local CYAN_TEXT=$(tput setaf 6)
|
|
local RED_TEXT=$(tput setaf 1)
|
|
local YELLOW_TEXT=$(tput setaf 3)
|
|
|
|
echo -e "\n${RED_TEXT} Generate: ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 1)${CYAN_TEXT} Contribution stats (by author) ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 2)${CYAN_TEXT} Git changelogs (last $_limit days)${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 3)${CYAN_TEXT} Git changelogs by author ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 4)${CYAN_TEXT} My daily status ${NORMAL}"
|
|
echo -e "${RED_TEXT} List: ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 5)${CYAN_TEXT} Branch tree view (last $_limit)${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 6)${CYAN_TEXT} All branches (sorted by most recent commit) ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 7)${CYAN_TEXT} All contributors (sorted by name) ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 8)${CYAN_TEXT} Git commits per author ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 9)${CYAN_TEXT} Git commits per date ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 10)${CYAN_TEXT} Git commits per month ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 11)${CYAN_TEXT} Git commits per weekday ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 12)${CYAN_TEXT} Git commits per hour ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 13)${CYAN_TEXT} Git commits by author per hour ${NORMAL}"
|
|
echo -e "${RED_TEXT} Suggest: ${NORMAL}"
|
|
echo -e "${CYAN_TEXT} ${YELLOW_TEXT} 14)${CYAN_TEXT} Code reviewers (based on git history) ${NORMAL}"
|
|
echo -e "\n${YELLOW_TEXT}Please enter a menu option or ${RED_TEXT}press enter to exit. ${NORMAL}"
|
|
read -r opt
|
|
}
|
|
|
|
function option_picked() {
|
|
local BOLD=$(tput bold)
|
|
local RED_TEXT=$(tput setaf 1)
|
|
local COLOR="${BOLD}${RED_TEXT}"
|
|
local RESET=$(tput sgr0)
|
|
local MESSAGE=${*:-"${RESET}Error: No message passed"}
|
|
|
|
echo -e "${COLOR}${MESSAGE}${RESET}\n"
|
|
}
|
|
|
|
function detailedGitStats() {
|
|
option_picked "Contribution stats (by author):"
|
|
git log --use-mailmap --no-merges --numstat --pretty="format:commit %H%nAuthor: %aN <%aE>%nDate: %ad%n%n%w(0,4,4)%B%n" $_since $_until $_pathspec | LC_ALL=C awk '
|
|
function printStats(author) {
|
|
printf "\t%s:\n", author
|
|
|
|
if( more["total"] > 0 ) {
|
|
printf "\t insertions: %d (%.0f%%)\n", more[author], (more[author] / more["total"] * 100)
|
|
}
|
|
|
|
if( less["total"] > 0 ) {
|
|
printf "\t deletions: %d (%.0f%%)\n", less[author], (less[author] / less["total"] * 100)
|
|
}
|
|
|
|
if( file["total"] > 0 ) {
|
|
printf "\t files: %d (%.0f%%)\n", file[author], (file[author] / file["total"] * 100)
|
|
}
|
|
|
|
if(commits["total"] > 0) {
|
|
printf "\t commits: %d (%.0f%%)\n", commits[author], (commits[author] / commits["total"] * 100)
|
|
}
|
|
|
|
if ( first[author] != "" ) {
|
|
printf "\t lines changed: %s\n", more[author] + less[author]
|
|
printf "\t first commit: %s\n", first[author]
|
|
printf "\t last commit: %s\n", last[author]
|
|
}
|
|
|
|
printf "\n"
|
|
}
|
|
|
|
/^Author:/ {
|
|
$1 = ""
|
|
author = $0
|
|
commits[author] += 1
|
|
commits["total"] += 1
|
|
}
|
|
|
|
/^Date:/ {
|
|
$1="";
|
|
first[author] = substr($0, 2)
|
|
if(last[author] == "" ) { last[author] = first[author] }
|
|
}
|
|
|
|
/^[0-9]/ {
|
|
more[author] += $1
|
|
less[author] += $2
|
|
file[author] += 1
|
|
|
|
more["total"] += $1
|
|
less["total"] += $2
|
|
file["total"] += 1
|
|
}
|
|
|
|
END {
|
|
for (author in commits) {
|
|
if (author != "total") {
|
|
printStats(author)
|
|
}
|
|
}
|
|
printStats("total")
|
|
}'
|
|
}
|
|
|
|
function suggestReviewers() {
|
|
option_picked "Suggested code reviewers (based on git history):"
|
|
git log --use-mailmap --no-merges $_since $_until --pretty=%aN $_pathspec $* | head -n 100 | sort | uniq -c | sort -nr | LC_ALL=C awk '
|
|
{ args[NR] = $0; }
|
|
END {
|
|
for (i = 1; i <= NR; ++i) {
|
|
printf "%s\n", args[i]
|
|
}
|
|
}' | column -t -s,
|
|
}
|
|
|
|
function commitsByMonth() {
|
|
option_picked "Git commits by month:"
|
|
echo -e "\tmonth\tsum"
|
|
for i in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
|
|
do
|
|
echo -en "\t$i\t"
|
|
git shortlog -n --no-merges --format='%ad %s' $_since $_until | grep " $i " | wc -l
|
|
done | awk '{
|
|
count[$1] = $2
|
|
total += $2
|
|
}
|
|
END{
|
|
for (month in count) {
|
|
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 );
|
|
}
|
|
}
|
|
}' | LC_TIME="en_EN.UTF-8" sort -M
|
|
}
|
|
|
|
function commitsByWeekday() {
|
|
option_picked "Git commits by weekday:"
|
|
echo -e "\tday\tsum"
|
|
for i in Mon Tue Wed Thu Fri Sat Sun
|
|
do
|
|
echo -en "\t$i\t"
|
|
git shortlog -n --no-merges --format='%ad %s' $_since $_until | grep "$i " | wc -l
|
|
done | awk '{
|
|
|
|
}
|
|
NR == FNR {
|
|
count[$1] = $2;
|
|
total += $2;
|
|
next
|
|
}
|
|
END{
|
|
|
|
for (day in count) {
|
|
s="|";
|
|
if (total > 0) {
|
|
percent = ((count[day] / total) * 100) / 1.25;
|
|
for (i = 1; i <= percent; ++i) {
|
|
s=s"█"
|
|
}
|
|
printf( "\t%s\t%-0s\t%s\n", day, count[day], s );
|
|
}
|
|
}
|
|
}' | sort -k 2 -n -r
|
|
}
|
|
|
|
function commitsByHour() {
|
|
local author="${1:-}"
|
|
local _author=""
|
|
|
|
if [[ -z "${author}" ]]; then
|
|
option_picked "Git commits by hour:"
|
|
_author="--author=**"
|
|
else
|
|
option_picked "Git commits by hour for author '${author}':"
|
|
_author="--author=${author}"
|
|
fi
|
|
echo -e "\thour\tsum"
|
|
for i in $(seq -w 0 23)
|
|
do
|
|
echo -ne "\t$i\t"
|
|
git shortlog -n --no-merges --format='%ad %s' "${_author}" $_since $_until | grep ' '$i: | wc -l
|
|
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=s"█"
|
|
}
|
|
printf( "\t%s\t%-0s\t%s\n", hour, count[hour], s );
|
|
}
|
|
}
|
|
}' | sort
|
|
}
|
|
|
|
function commitsPerDay() {
|
|
option_picked "Git commits per date:";
|
|
git log --use-mailmap --no-merges $_since $_until --date=short --format='%ad' $_pathspec | sort | uniq -c
|
|
}
|
|
|
|
function commitsPerAuthor() {
|
|
option_picked "Git commits per author:"
|
|
git shortlog $_since $_until --no-merges -n -s | sort -nr | LC_ALL=C awk '
|
|
{ args[NR] = $0; sum += $0 }
|
|
END {
|
|
for (i = 1; i <= NR; ++i) {
|
|
printf "%s,%2.1f%%\n", args[i], 100 * args[i] / sum
|
|
}
|
|
}' | column -t -s,
|
|
}
|
|
|
|
function myDailyStats() {
|
|
option_picked "My daily status:"
|
|
git diff --shortstat '@{0 day ago}' | sort -nr | tr ',' '\n' | LC_ALL=C awk '
|
|
{ args[NR] = $0; }
|
|
END {
|
|
for (i = 1; i <= NR; ++i) {
|
|
printf "\t%s\n", args[i]
|
|
}
|
|
}'
|
|
|
|
echo -e "\t" $(git log --use-mailmap --author="$(git config user.name)" --no-merges --since=$(date "+%Y-%m-%dT00:00:00") --until=$(date "+%Y-%m-%dT23:59:59") --reverse | grep commit | wc -l) "commits"
|
|
}
|
|
|
|
function contributors() {
|
|
option_picked "All contributors (sorted by name):"
|
|
git log --use-mailmap --no-merges $_since $_until --format='%aN' $_pathspec | sort -u | cat -n
|
|
}
|
|
|
|
function branchTree() {
|
|
option_picked "Branching tree view:"
|
|
git log --use-mailmap --graph --abbrev-commit $_since $_until --decorate --format=format:'--+ Commit: %h %n | Date: %aD (%ar) %n'' | Message: %s %d %n'' + Author: %aN %n' --all | head -n $((_limit*5))
|
|
}
|
|
|
|
|
|
function branchesByDate() {
|
|
option_picked "All branches (sorted by most recent commit):"
|
|
git for-each-ref --sort=committerdate refs/heads/ --format='[%(authordate:relative)] %(authorname) %(refname:short)' | cat -n
|
|
}
|
|
|
|
function changelogs() {
|
|
local author="${1:-}"
|
|
local _author=""
|
|
|
|
if [[ -z "${author}" ]]; then
|
|
option_picked "Git changelogs:"
|
|
_author="--author=**"
|
|
else
|
|
option_picked "Git changelogs for author '${author}':"
|
|
_author="--author=${author}"
|
|
fi
|
|
|
|
NEXT=$(date +%F)
|
|
git log --use-mailmap --no-merges --format="%cd" --date=short "${_author}" $_since $_until $_pathspec | sort -u -r | head -n $_limit | while read DATE ; do
|
|
echo
|
|
echo "[$DATE]"
|
|
GIT_PAGER=cat git log --use-mailmap --no-merges --format=" * %s (%aN)" "${_author}" --since=$DATE --until=$NEXT
|
|
NEXT=$DATE
|
|
done
|
|
}
|
|
|
|
# Check to make sure all utilities required for this script are installed
|
|
check_utils
|
|
|
|
# Check if we are currently in a git repo.
|
|
git rev-parse --is-inside-work-tree > /dev/null
|
|
|
|
# Parse non-interative commands
|
|
if [[ "$#" -eq 1 ]]; then
|
|
case "$1" in
|
|
-r|--suggest-reviewers) suggestReviewers;;
|
|
-T|--detailed-git-stats) detailedGitStats;;
|
|
-b|--branch-tree) branchTree;;
|
|
-d|--commits-per-day) commitsPerDay;;
|
|
-a|--commits-per-author) commitsPerAuthor;;
|
|
-S|--my-daily-stats) myDailyStats;;
|
|
-C|--contributors) contributors;;
|
|
-D|--branches-by-date) branchesByDate;;
|
|
-c|--changelogs) changelogs;;
|
|
-L|--changelogs-by-author)
|
|
author="${_GIT_AUTHOR:-}"
|
|
while [[ -z "${author}" ]]; do
|
|
read -r -p "Which author? " author
|
|
done
|
|
changelogs "${author}";;
|
|
-w|--commits-by-weekday) commitsByWeekday;;
|
|
-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}";;
|
|
-m|--commits-by-month) commitsByMonth;;
|
|
-h|-\?|--help) usage;;
|
|
*) echo "Invalid argument"; usage; exit 1;;
|
|
esac
|
|
exit 0;
|
|
fi
|
|
[[ "$#" -gt 1 ]] && { echo "Invalid arguments"; usage; exit 1; }
|
|
|
|
# Parse interactive commands
|
|
clear
|
|
show_menu
|
|
|
|
while [[ "${opt}" != "" ]]; do
|
|
clear
|
|
case "${opt}" in
|
|
1) detailedGitStats; show_menu;;
|
|
2) changelogs; show_menu;;
|
|
3) author=""
|
|
while [[ -z "${author}" ]]; do
|
|
read -r -p "Which author? " author
|
|
done
|
|
changelogs "${author}"; show_menu;;
|
|
4) myDailyStats; show_menu;;
|
|
5) branchTree; show_menu;;
|
|
6) branchesByDate; show_menu;;
|
|
7) contributors; show_menu;;
|
|
8) commitsPerAuthor; show_menu;;
|
|
9) commitsPerDay; show_menu;;
|
|
10) commitsByMonth; show_menu;;
|
|
11) commitsByWeekday; show_menu;;
|
|
12) commitsByHour; show_menu;;
|
|
13) author=""
|
|
while [[ -z "${author}" ]]; do
|
|
read -r -p "Which author? " author
|
|
done
|
|
commitsByHour "${author}"; show_menu;;
|
|
14) suggestReviewers; show_menu;;
|
|
q|"\n") exit;;
|
|
*) clear; option_picked "Pick an option from the menu"; show_menu;;
|
|
esac
|
|
done
|