mirror of
https://github.com/git-quick-stats/git-quick-stats.git
synced 2026-02-03 11:33:32 +01:00
* In some situations, the awk statements in the functions commitsByMonth, commitsByDay, and commitsByHour attempt to divide by zero and display an error to the user. To invoke this bug, checkout any of the previous commits and attempt to see commits by hour from some user who has never committed to this repository. This commit fixes this bug by making sure the awk statement only ever executes when the total commits is greater than zero. * Added a utility checker to make sure that the user has all of the necessary tools in their path in order to run this script. If they do not exist, it exits and informs the user that this script cannot locate them in PATH. * Adjusted shell logic to prevent double negatives from confusing developers as ! -z is technically read as something akin to "not has no value" * Changed some statements to utilize safer and more predictable bash-isms * Minor white space fixes and adjusted the README.md slightly
457 lines
13 KiB
Bash
Executable File
457 lines
13 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 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 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"
|
|
echo $(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"
|
|
echo $(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:"
|
|
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"
|
|
echo "$(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
|
|
|
|
if [ $# -eq 1 ]
|
|
then
|
|
case $1 in
|
|
"suggestReviewers")
|
|
suggestReviewers
|
|
;;
|
|
"detailedGitStats")
|
|
detailedGitStats
|
|
;;
|
|
"branchTree")
|
|
branchTree
|
|
;;
|
|
"commitsPerDay")
|
|
commitsPerDay
|
|
;;
|
|
"commitsPerAuthor")
|
|
commitsPerAuthor
|
|
;;
|
|
"myDailyStats")
|
|
myDailyStats
|
|
;;
|
|
"contributors")
|
|
contributors
|
|
;;
|
|
"branchesByDate")
|
|
branchesByDate
|
|
;;
|
|
"changelogs")
|
|
changelogs
|
|
;;
|
|
"changelogsByAuthor")
|
|
author="${_GIT_AUTHOR:-}"
|
|
while [ -z "$author" ]; do read -p "Which author? " author; done
|
|
changelogs "$author"
|
|
;;
|
|
"commitsByWeekday")
|
|
commitsByWeekday
|
|
;;
|
|
"commitsByHour")
|
|
commitsByHour
|
|
;;
|
|
"commitsByAuthorByHour")
|
|
author="${_GIT_AUTHOR:-}"
|
|
while [ -z "$author" ]; do read -p "Which author? " author; done
|
|
commitsByHour "$author"
|
|
;;
|
|
"commitsByMonth")
|
|
commitsByMonth
|
|
;;
|
|
*)
|
|
echo "Invalid argument. Possible arguments: suggestReviewers, detailedGitStats, commitsPerDay, commitsByMonth, commitsByWeekday, commitsByHour, commitsByAuthorByHour, commitsPerAuthor, myDailyStats, contributors, branchTree, branchesByDate, changelogs, changelogsByAuthor"
|
|
exit 1
|
|
;;
|
|
esac
|
|
exit 0;
|
|
fi
|
|
|
|
[[ $# -gt 1 ]] && { echo "Usage: git quick-stats <optional-command-to-execute-directly>"; exit 1; }
|
|
|
|
clear
|
|
show_menu
|
|
|
|
while [ opt != '' ]
|
|
do
|
|
if [[ $opt = "" ]]; then
|
|
exit;
|
|
else
|
|
clear
|
|
case $opt in
|
|
1)
|
|
detailedGitStats
|
|
show_menu
|
|
;;
|
|
2)
|
|
changelogs
|
|
show_menu
|
|
;;
|
|
3)
|
|
author=''
|
|
while [ -z "$author" ]; do read -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 -p "Which author? " author; done
|
|
commitsByHour "$author"
|
|
show_menu
|
|
;;
|
|
14)
|
|
suggestReviewers
|
|
show_menu
|
|
;;
|
|
q)
|
|
exit
|
|
;;
|
|
\n)
|
|
exit
|
|
;;
|
|
*)
|
|
clear
|
|
option_picked "Pick an option from the menu"
|
|
show_menu
|
|
;;
|
|
|
|
esac
|
|
fi
|
|
done
|