diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce1d613 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.json +*.db +.DS_Store* +._* +.*.swp +.*.swo +.Spotlight* +.Trash* +**/*~ diff --git a/README.md b/README.md index c48bba7..d517614 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ Possible arguments in short and long form: see changelogs -L, --changelogs-by-author see changelogs by author +-j, --json-output + save git log as a JSON formatted file to a specified area -h, -?, --help display this help text in the terminal ``` diff --git a/git-quick-stats b/git-quick-stats index 8ff0b27..15a2119 100755 --- a/git-quick-stats +++ b/git-quick-stats @@ -33,8 +33,8 @@ _theme="${_MENU_THEME:=default}" # ARGS: None # OUTS: None ################################################################################ -function check_utils() { - local msg="not found. Please make sure this is installed and in PATH." +function checkUtils() { + local -r msg="not found. Please make sure this is installed and in PATH." command -v awk >/dev/null 2>&1 || { echo >&2 "awk ${msg}"; exit 1; } command -v basename >/dev/null 2>&1 || { echo >&2 "basename ${msg}"; exit 1; } @@ -58,7 +58,7 @@ function check_utils() { # OUTS: None ################################################################################ function usage() { - local program=$(basename "$0") + local -r program=$(basename "$0") echo " NAME @@ -106,6 +106,8 @@ OPTIONS see changelogs -L, --changelogs-by-author see changelogs by author + -j, --json-output + save git log as a JSON formatted file to a specified area -h, -?, --help display this help text in the terminal @@ -125,13 +127,13 @@ ADDITIONAL USAGE # ARGS: None # OUTS: $opt: Option selected by the user based on menu choice ################################################################################ -function show_menu() { - local normal=$(tput sgr0) - local cyan=$(tput setaf 6) - local bold=$(tput bold) - local red=$(tput setaf 1) - local yellow=$(tput setaf 3) - local white=$(tput setaf 7) +function showMenu() { + local -r normal=$(tput sgr0) + local -r cyan=$(tput setaf 6) + local -r bold=$(tput bold) + local -r red=$(tput setaf 1) + local -r yellow=$(tput setaf 3) + local -r white=$(tput setaf 7) local titles="" local text="" local nums="" @@ -140,37 +142,38 @@ function show_menu() { # Adjustable color menu option if [[ "${_theme}" == "legacy" ]]; then - titles="${bold}${red}" - text="${normal}${cyan}" - nums="${bold}${yellow}" - help_txt="${normal}${yellow}" - exit_txt="${bold}${red}" + titles="${bold}${red}" && readonly titles + text="${normal}${cyan}" && readonly text + nums="${bold}${yellow}" && readonly nums + help_txt="${normal}${yellow}" && readonly help_txt + exit_txt="${bold}${red}" && readonly exit_txt else - titles="${bold}${cyan}" - text="${normal}${white}" - nums="${normal}${bold}${white}" - help_txt="${normal}${cyan}" - exit_txt="${bold}${cyan}" + titles="${bold}${cyan}" && readonly titles + text="${normal}${white}" && readonly text + nums="${normal}${bold}${white}" && readonly nums + help_txt="${normal}${cyan}" && readonly help_txt + exit_txt="${bold}${cyan}" && readonly exit_txt fi - + echo -e "\n${titles} Generate:${normal}" echo -e "${nums} 1)${text} Contribution stats (by author)" echo -e "${nums} 2)${text} Contribution stats (by author) on a specific branch" echo -e "${nums} 3)${text} Git changelogs (last $_limit days)" echo -e "${nums} 4)${text} Git changelogs by author" echo -e "${nums} 5)${text} My daily status" + echo -e "${nums} 6)${text} Save git log output in JSON format" echo -e "\n${titles} List:" - echo -e "${nums} 6)${text} Branch tree view (last $_limit)" - echo -e "${nums} 7)${text} All branches (sorted by most recent commit)" - echo -e "${nums} 8)${text} All contributors (sorted by name)" - echo -e "${nums} 9)${text} Git commits per author" - echo -e "${nums} 10)${text} Git commits per date" - echo -e "${nums} 11)${text} Git commits per month" - echo -e "${nums} 12)${text} Git commits per weekday" - echo -e "${nums} 13)${text} Git commits per hour" - echo -e "${nums} 14)${text} Git commits by author per hour" + echo -e "${nums} 7)${text} Branch tree view (last $_limit)" + echo -e "${nums} 8)${text} All branches (sorted by most recent commit)" + echo -e "${nums} 9)${text} All contributors (sorted by name)" + echo -e "${nums} 10)${text} Git commits per author" + echo -e "${nums} 11)${text} Git commits per date" + echo -e "${nums} 12)${text} Git commits per month" + echo -e "${nums} 13)${text} Git commits per weekday" + echo -e "${nums} 14)${text} Git commits per hour" + echo -e "${nums} 15)${text} Git commits by author per hour" echo -e "\n${titles} Suggest:" - echo -e "${nums} 15)${text} Code reviewers (based on git history)" + echo -e "${nums} 16)${text} Code reviewers (based on git history)" echo -e "\n${help_txt}Please enter a menu option or ${exit_txt}press Enter to exit." echo -n "${text}> ${normal}" read -r opt @@ -181,10 +184,10 @@ function show_menu() { # ARGS: $* (required): String to print (usually provided by other functions) # OUTS: None ################################################################################ -function option_picked() { - local bold=$(tput bold) - local red=$(tput setaf 1) - local reset=$(tput sgr0) +function optionPicked() { + local -r bold=$(tput bold) + local -r red=$(tput setaf 1) + local -r reset=$(tput sgr0) local msg=${*:-"${reset}Error: No message passed"} echo -e "${bold}${red}${msg}${reset}\n" @@ -193,7 +196,8 @@ function option_picked() { ################################################################################ # DESC: Shows detailed contribution stats per author by parsing every commit in # the repo and outputting their contribution stats -# ARGS: None +# ARGS: $branch (optional): Users can specify an alternative branch instead of +# the current default one # OUTS: None ################################################################################ function detailedGitStats() { @@ -214,12 +218,12 @@ function detailedGitStats() { fi # Prompt message - if [[ $is_branch_existing && -n "${_branch}" ]]; then - option_picked "Contribution stats (by author) on ${_branch} branch:" + if [[ "${is_branch_existing}" && -n "${_branch}" ]]; then + optionPicked "Contribution stats (by author) on ${_branch} branch:" elif [[ -n "${branch}" && -z "${_branch}" ]]; then - option_picked "Branch \"${branch}\" does not exist.\nContribution stats (by author) on the current branch:" + optionPicked "Branch ${branch} does not exist.\nContribution stats (by author) on the current branch:" else - option_picked "Contribution stats (by author) on the current branch:" + optionPicked "Contribution stats (by author) on the current branch:" fi git -c log.showSignature=false log ${_branch} --use-mailmap --no-merges --numstat \ @@ -296,7 +300,7 @@ function detailedGitStats() { # OUTS: None ################################################################################ function suggestReviewers() { - option_picked "Suggested code reviewers (based on git history):" + optionPicked "Suggested code reviewers (based on git history):" git -c log.showSignature=false 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; } @@ -307,13 +311,28 @@ function suggestReviewers() { }' | column -t -s, } +################################################################################ +# DESC: Saves the git log output in a JSON format +# ARGS: $json_path (required): Path to where the file is saved +# OUTS: A JSON formatted file +################################################################################ +function jsonOutput() { + optionPicked "Output log saved to file at: ${json_path:?}/output.json" + git -c log.showSignature=false log --use-mailmap --no-merges $_since $_until \ + --pretty=format:'{%n "commit": "%H",%n "abbreviated_commit": "%h",%n "tree": "%T",%n "abbreviated_tree": "%t",%n "parent": "%P",%n "abbreviated_parent": "%p",%n "refs": "%D",%n "encoding": "%e",%n "subject": "%s",%n "sanitized_subject_line": "%f",%n "body": "%b",%n "commit_notes": "%N",%n "verification_flag": "%G?",%n "signer": "%GS",%n "signer_key": "%GK",%n "author": {%n "name": "%aN",%n "email": "%aE",%n "date": "%aD"%n },%n "commiter": {%n "name": "%cN",%n "email": "%cE",%n "date": "%cD"%n }%n},' \ + | sed "$ s/,$//" \ + | sed ':a;N;$!ba;s/\r\n\([^{]\)/\\n\1/g' \ + | awk 'BEGIN { print("[") } { print($0) } END { print("]") }' \ + > "${json_path:?}"/output.json +} + ################################################################################ # DESC: Displays a horizontal bar graph based on total commits per month # ARGS: None # OUTS: None ################################################################################ function commitsByMonth() { - option_picked "Git commits by month:" + optionPicked "Git commits by month:" echo -e "\tmonth\tsum" for i in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec do @@ -344,7 +363,7 @@ function commitsByMonth() { # OUTS: None ################################################################################ function commitsByWeekday() { - option_picked "Git commits by weekday:" + optionPicked "Git commits by weekday:" echo -e "\tday\tsum" for i in Mon Tue Wed Thu Fri Sat Sun do @@ -384,10 +403,10 @@ function commitsByHour() { local _author="" if [[ -z "${author}" ]]; then - option_picked "Git commits by hour:" + optionPicked "Git commits by hour:" _author="--author=**" else - option_picked "Git commits by hour for author '${author}':" + optionPicked "Git commits by hour for author '${author}':" _author="--author=${author}" fi echo -e "\thour\tsum" @@ -421,7 +440,7 @@ function commitsByHour() { # OUTS: None ################################################################################ function commitsPerDay() { - option_picked "Git commits per date:"; + optionPicked "Git commits per date:"; git -c log.showSignature=false log --use-mailmap --no-merges $_since $_until \ --date=short --format='%ad' $_pathspec | sort | uniq -c } @@ -433,7 +452,7 @@ function commitsPerDay() { # OUTS: None ################################################################################ function commitsPerAuthor() { - option_picked "Git commits per author:" + optionPicked "Git commits per author:" git -c log.showSignature=false shortlog $_since $_until --no-merges -n -s \ | sort -nr | LC_ALL=C awk ' { args[NR] = $0; sum += $0 } @@ -450,7 +469,7 @@ function commitsPerAuthor() { # OUTS: None ################################################################################ function myDailyStats() { - option_picked "My daily status:" + optionPicked "My daily status:" git diff --shortstat '@{0 day ago}' | sort -nr | tr ',' '\n' | LC_ALL=C awk ' { args[NR] = $0; } END { @@ -472,7 +491,7 @@ function myDailyStats() { # OUTS: None ################################################################################ function contributors() { - option_picked "All contributors (sorted by name):" + optionPicked "All contributors (sorted by name):" git -c log.showSignature=false log --use-mailmap --no-merges $_since $_until \ --format='%aN' $_pathspec | sort -u | cat -n } @@ -483,7 +502,7 @@ function contributors() { # OUTS: None ################################################################################ function branchTree() { - option_picked "Branching tree view:" + optionPicked "Branching tree view:" git -c log.showSignature=false 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' \ @@ -496,7 +515,7 @@ function branchTree() { # OUTS: None ################################################################################ function branchesByDate() { - option_picked "All branches (sorted by most recent commit):" + optionPicked "All branches (sorted by most recent commit):" git for-each-ref --sort=committerdate refs/heads/ \ --format='[%(authordate:relative)] %(authorname) %(refname:short)' | cat -n } @@ -509,36 +528,37 @@ function branchesByDate() { function changelogs() { local author="${1:-}" local _author="" + local next=$(date +%F) if [[ -z "${author}" ]]; then - option_picked "Git changelogs:" + optionPicked "Git changelogs:" _author="--author=**" else - option_picked "Git changelogs for author '${author}':" + optionPicked "Git changelogs for author '${author}':" _author="--author=${author}" fi - NEXT=$(date +%F) git -c log.showSignature=false log \ --use-mailmap \ --no-merges \ --format="%cd" \ --date=short "${_author}" $_since $_until $_pathspec \ - | sort -u -r | head -n $_limit | while read DATE; do - echo -e "\n[$DATE]" - GIT_PAGER=cat git -c log.showSignature=false log \ - --use-mailmap --no-merges \ - --format=" * %s (%aN)" "${_author}" \ - --since=$DATE --until=$NEXT - NEXT=$DATE - done + | sort -u -r | head -n $_limit \ + | while read DATE; do + echo -e "\n[$DATE]" + GIT_PAGER=cat git -c log.showSignature=false log \ + --use-mailmap --no-merges \ + --format=" * %s (%aN)" "${_author}" \ + --since=$DATE --until=$next + next=$DATE + done } ################################################################################ # MAIN # Check to make sure all utilities required for this script are installed -check_utils +checkUtils # Check if we are currently in a git repo. git rev-parse --is-inside-work-tree > /dev/null @@ -576,6 +596,16 @@ if [[ "$#" -eq 1 ]]; then done commitsByHour "${author}";; -m|--commits-by-month) commitsByMonth;; + -j|--json-output) + json_path="" + while [[ -z "${json_path}" ]]; do + read -r -p "Path 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}";; -h|-\?|--help) usage;; *) echo "Invalid argument"; usage; exit 1;; esac @@ -585,39 +615,48 @@ fi # Parse interactive commands clear -show_menu +showMenu while [[ "${opt}" != "" ]]; do clear case "${opt}" in - 1) detailedGitStats; show_menu;; + 1) detailedGitStats; showMenu;; 2) branch="" while [[ -z "${branch}" ]]; do read -r -p "Which branch? " branch done - detailedGitStats "${branch}"; show_menu;; - 3) changelogs; show_menu;; + detailedGitStats "${branch}"; showMenu;; + 3) changelogs; showMenu;; 4) author="" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - changelogs "${author}"; show_menu;; - 5) myDailyStats; show_menu;; - 6) branchTree; show_menu;; - 7) branchesByDate; show_menu;; - 8) contributors; show_menu;; - 9) commitsPerAuthor; show_menu;; - 10) commitsPerDay; show_menu;; - 11) commitsByMonth; show_menu;; - 12) commitsByWeekday; show_menu;; - 13) commitsByHour; show_menu;; - 14) author="" + changelogs "${author}"; showMenu;; + 5) myDailyStats; showMenu;; + 6) json_path="" + while [[ -z "${json_path}" ]]; do + read -r -p "Path 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;; + 7) branchTree; showMenu;; + 8) branchesByDate; showMenu;; + 9) contributors; showMenu;; + 10) commitsPerAuthor; showMenu;; + 11) commitsPerDay; showMenu;; + 12) commitsByMonth; showMenu;; + 13) commitsByWeekday; showMenu;; + 14) commitsByHour; showMenu;; + 15) author="" while [[ -z "${author}" ]]; do read -r -p "Which author? " author done - commitsByHour "${author}"; show_menu;; - 15) suggestReviewers; show_menu;; + commitsByHour "${author}"; showMenu;; + 16) suggestReviewers; showMenu;; q|"\n") exit;; - *) clear; option_picked "Pick an option from the menu"; show_menu;; + *) clear; optionPicked "Pick an option from the menu"; showMenu;; esac done diff --git a/git-quick-stats.1 b/git-quick-stats.1 index faa3e21..a4d682b 100644 --- a/git-quick-stats.1 +++ b/git-quick-stats.1 @@ -94,6 +94,11 @@ see changelogs see changelogs by author .HP .PP +\fB\-j\fR, \fB\-\-json\-output\fR +.IP +save git log as a JSON formatted file to a specified area +.HP +.PP \fB\-h\fR, \-?, \fB\-\-help\fR .IP display this help text in the terminal diff --git a/tests/commands_test.sh b/tests/commands_test.sh index c0f2806..ed29778 100755 --- a/tests/commands_test.sh +++ b/tests/commands_test.sh @@ -3,8 +3,7 @@ . tests/assert.sh -v src="./git-quick-stats" -#assert "$src fail" "Invalid argument\n\nNAME\n git-quick-stats - Simple and efficient way to access various stats in a git repo\n\nSYNOPSIS\n For non-interactive mode: git-quick-stats [OPTIONS]\n For interactive mode: git-quick-stats\n\nDESCRIPTION\n Any git repository contains tons of information about commits, contributors,\n and files. Extracting this information is not always trivial, mostly because\n of a gadzillion options to a gadzillion git commands.\n\n This program allows you to see detailed information about a git repository.\n\nOPTIONS\n suggestReviewers - see best people to contact to review code\n detailedGitStats - displays a detailed list of git status\n commitsPerDay - displays a list of commits per day\n commitsByMonth - displays a list of commits per month\n commitsByWeekday - displays a list of commits per weekday\n commitsByHour - displays a list of commits per hour\n commitsByAuthorByHour - see a list of commits per hour by author\n commitsPerAuthor - displays a list of commits per author\n myDailyStats - see your current daily stats\n contributors - see a list of all contributors\n branchTree - see an ASCII graph of the git repo\n branchesByDate - show branches by date\n changelogs - see changelogs\n changelogsByAuthor - see changelogs by author\n\nADDITIONAL USAGE\n You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log\n ex: export _GIT_SINCE=2017-01-20\n You can set _GIT_LIMIT for limited output log\n ex: export _GIT_LIMIT=20\n You can exclude a directory from the stats by using pathspec\n ex: export _GIT_PATHSPEC=':!directory'\n \nCONTRIBUTION\n For details regarding contribution, please see the contribution.md document\n\nLICENSE\n This is under the MIT license. See LICENSE in the repo for more info" -assert "$src fail" "Invalid argument\n\nNAME\n git-quick-stats - Simple and efficient way to access various stats in a git repo\n\nSYNOPSIS\n For non-interactive mode: git-quick-stats [OPTIONS]\n For interactive mode: git-quick-stats\n\nDESCRIPTION\n Any git repository contains tons of information about commits, contributors,\n and files. Extracting this information is not always trivial, mostly because\n of a gadzillion options to a gadzillion git commands.\n\n This program allows you to see detailed information about a git repository.\n\nOPTIONS\n -r, --suggest-reviewers\n show the best people to contact to review code\n -T, --detailed-git-stats\n give a detailed list of git stats\n -R, --git-stats-by-branch\n see detailed list of git stats by branch\n -d, --commits-per-day\n displays a list of commits per day\n -m, --commits-by-month\n displays a list of commits per month\n -w, --commits-by-weekday\n displays a list of commits per weekday\n -o, --commits-by-hour\n displays a list of commits per hour\n -A, --commits-by-author-by-hour\n displays a list of commits per hour by author\n -a, --commits-per-author\n displays a list of commits per author\n -S, --my-daily-stats\n see your current daily stats\n -C, --contributors\n see a list of everyone who contributed to the repo\n -b, --branch-tree\n show an ASCII graph of the git repo branch history\n -D, --branches-by-date\n show branches by date\n -c, --changelogs\n see changelogs\n -L, --changelogs-by-author\n see changelogs by author\n -h, -?, --help\n display this help text in the terminal\n\nADDITIONAL USAGE\n You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log\n ex: export _GIT_SINCE=\"2017-01-20\"\n You can set _GIT_LIMIT for limited output log\n ex: export _GIT_LIMIT=20\n You can exclude a directory from the stats by using pathspec\n ex: export _GIT_PATHSPEC=':!directory'\n You can set _MENU_THEME to display the legacy color scheme\n ex: export _MENU_THEME=legacy" +assert "$src fail" "Invalid argument\n\nNAME\n git-quick-stats - Simple and efficient way to access various stats in a git repo\n\nSYNOPSIS\n For non-interactive mode: git-quick-stats [OPTIONS]\n For interactive mode: git-quick-stats\n\nDESCRIPTION\n Any git repository contains tons of information about commits, contributors,\n and files. Extracting this information is not always trivial, mostly because\n of a gadzillion options to a gadzillion git commands.\n\n This program allows you to see detailed information about a git repository.\n\nOPTIONS\n -r, --suggest-reviewers\n show the best people to contact to review code\n -T, --detailed-git-stats\n give a detailed list of git stats\n -R, --git-stats-by-branch\n see detailed list of git stats by branch\n -d, --commits-per-day\n displays a list of commits per day\n -m, --commits-by-month\n displays a list of commits per month\n -w, --commits-by-weekday\n displays a list of commits per weekday\n -o, --commits-by-hour\n displays a list of commits per hour\n -A, --commits-by-author-by-hour\n displays a list of commits per hour by author\n -a, --commits-per-author\n displays a list of commits per author\n -S, --my-daily-stats\n see your current daily stats\n -C, --contributors\n see a list of everyone who contributed to the repo\n -b, --branch-tree\n show an ASCII graph of the git repo branch history\n -D, --branches-by-date\n show branches by date\n -c, --changelogs\n see changelogs\n -L, --changelogs-by-author\n see changelogs by author\n -j, --json-output\n save git log as a JSON formatted file to a specified area\n -h, -?, --help\n display this help text in the terminal\n\nADDITIONAL USAGE\n You can set _GIT_SINCE and _GIT_UNTIL to limit the git time log\n ex: export _GIT_SINCE=\"2017-01-20\"\n You can set _GIT_LIMIT for limited output log\n ex: export _GIT_LIMIT=20\n You can exclude a directory from the stats by using pathspec\n ex: export _GIT_PATHSPEC=':!directory'\n You can set _MENU_THEME to display the legacy color scheme\n ex: export _MENU_THEME=legacy" assert_raises "$src fail" 1 assert_contains "$src --suggest-reviewers" "Suggested code reviewers (based on git history)" 127