├── .eslintignore ├── .gitignore ├── .eslintrc.yml ├── package.json ├── LICENSE ├── README.md ├── git-pulse-rank.js └── git-pulse /.eslintignore: -------------------------------------------------------------------------------- 1 | !.* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: eslint:recommended 2 | env: 3 | browser: true 4 | es2021: true 5 | node: true 6 | parserOptions: 7 | ecmaVersion: latest 8 | sourceType: module 9 | rules: 10 | prefer-const: warn 11 | no-var: warn 12 | object-curly-spacing: 13 | - warn 14 | - always 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "git-pulse-snapshots", 4 | "version": "1.0.0", 5 | "repository": "git@github.com:git-pulse/snapshots.git", 6 | "author": "Michael[tm] Smith ", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">=15.0.0" 10 | }, 11 | "scripts": { 12 | "build": "node .snapshots-process.js" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^8.10.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael[tm] Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **How to:** Show a [pulse snapshot](#pulse-snapshots) of the past 6 months of committer activity and issue/PR activity for any git/GitHub repo by doing this: `cd` into your local clone of any repo, and run the following: 2 | 3 | ```bash 4 | curl -fsSLO https://git-pulse.github.io/tools/git-pulse && bash ./git-pulse 6 5 | ``` 6 | 7 | Example output: 8 | 9 | ``` 10 | Committers 11 | ┌────────────────────┬───────┐ 12 | │ Monthly │ Cu- │ 13 | ├─────┬───────┬──────┤ mula- │ Monthly 14 | │ New │ Total │ %New │ tive │ commits Month range 15 | ├─────┼───────┼──────┼───────┼───────┬───────────────────────────────────────┐ 16 | │ 120 │ 167 │ 71 │ 1366 │ 385 │ Nov 01 – Dec 01 2021 ending 5mo ago │ 17 | │ 115 │ 166 │ 69 │ 1481 │ 398 │ Dec 01 – Jan 01 2022 ending 4mo ago │ 18 | │ 157 │ 221 │ 71 │ 1638 │ 572 │ Jan 01 – Feb 01 2022 ending 3mo ago │ 19 | │ 111 │ 169 │ 65 │ 1749 │ 468 │ Feb 01 – Mar 01 2022 ending 2mo ago │ 20 | │ 163 │ 222 │ 73 │ 1912 │ 692 │ Mar 01 – Apr 01 2022 ending 1mo ago │ 21 | │ 120 │ 192 │ 62 │ 2032 │ 679 │ Apr 01 – May 01 2022 ending today │ 22 | └─────┴───────┴──────┴───────┴───────┴───────────────────────────────────────┘ 23 | 24 | For the last 6 months: 25 | 26 | 131 new committers per month on average. 27 | 189 total active committers per month on average. 28 | 69 percent of committers were new committers. 29 | 532 commits per month on average. 30 | 31 | Cumulative total committers grew by 666 (from 1366 to 2032) in 5 months. 32 | ``` 33 | 34 | ![image](https://user-images.githubusercontent.com/194984/166609194-9380ed86-1282-45da-842a-d8ac4e6d4e96.png) 35 | 36 | ``` 37 | ┌────────────────┬─────────────────────┐ 38 | │ Issues │ PRs │ 39 | ├────────────────┼─────────────────────┤ 40 | │ Clsd Opnd +- │ Mrgd Clsd Opnd +- │ Month range 41 | ├────────────────┼─────────────────────┼───────────────────────────────────────┐ 42 | │ 169 215 46 │ 373 42 424 9 │ Nov 01 - Dec 01 2021 ending 5mo ago │ 43 | │ 229 244 15 │ 401 36 440 3 │ Dec 01 - Jan 01 2022 ending 4mo ago │ 44 | │ 223 227 4 │ 569 90 669 10 │ Jan 01 - Feb 01 2022 ending 3mo ago │ 45 | │ 188 195 7 │ 470 49 526 7 │ Feb 01 - Mar 01 2022 ending 2mo ago │ 46 | │ 305 336 31 │ 692 55 744 -3 │ Mar 01 - Apr 01 2022 ending 1mo ago │ 47 | │ 162 199 37 │ 673 62 755 20 │ Apr 01 - May 01 2022 ending today │ 48 | └────────────────┴─────────────────────┴───────────────────────────────────────┘ 49 | For the last 6 months: 50 | 51 | 212 issues closed per month on average. 52 | 236 issues opened per month on average. 53 | 23 issue increase in open issues per month on average. 54 | 55 | 529 PRs merged per month on average. 56 | 99 percent merged PRs / commits ratio 57 | (average 529 merged PRs vs average 532 commits). 58 | 55 PRs closed (unmerged) per month on average. 59 | 593 PRs opened per month on average. 60 | 7 PR increase in open PRs per month on average. 61 | 62 | 140 issue increase in open issues overall. 63 | 46 PR increase in open PRs overall. 64 | ``` 65 | 66 | ![image](https://user-images.githubusercontent.com/194984/166609488-ea747459-6477-4b41-b26f-4859f55c104b.png) 67 | 68 | ## Pulse snapshots 69 | 70 | A **pulse snapshot** is a report that answers questions about a project — questions such as the following: 71 | 72 | - **How many contributors are actually active each month?** 73 | - **How many new contributors is the project gaining each month?** 74 | - **How many commits are getting merged each month?** 75 | - **How well is the project managing issues and PRs?** 76 | - **How long does an issue or PR typically stay open before it’s resolved?** 77 | - **How many issues and PRs are there which have stayed open for a long time without being resolved?** 78 | - **At what rate are the lists of open issues and PRs increasing or decreasing?** 79 | 80 | The `git-pulse` tool generates snapshots with data and graphs that answer those questions. 81 | 82 | ### Notes on using the git-pulse tool 83 | 84 | - The month ranges shown are for logical months based on the current date (today) rather than calendar months. That is, each “month” range shown ends on the same calendar day as today — rather than being a calendar month starting on the 1st of a given month and ending on the 30th, 31st, 28th, or 29th. 85 | 86 | - Unless you have either `GITHUB_TOKEN` or `GH_TOKEN` set in your environment, running this tool will likely cause you to quickly exceed the GitHub API rate limits for requests, and start getting 403 error responses. 87 | 88 | - Even with `GITHUB_TOKEN`/`GH_TOKEN` set, the tool takes several minutes to run, due to some throttling (delay between API calls) added to avoid hitting GitHub API rate limits for (authenticated) requests. 89 | 90 | To adjust the throttling between API call), run with the `-s` option set to a number of seconds (default 9): 91 | 92 | ``` 93 | bash ./git-pulse -s10 94 | ``` 95 | 96 | - To generate a snapshot for a date range going back from an earlier day than today, install `faketime` (e.g., with `apt install faketime` or `brew install faketime`) and run `git-pulse` like this: 97 | 98 | ``` 99 | faketime -f "@2021-05-01 23:59:59" bash ./git-pulse 100 | ``` 101 | 102 | **Note:** `faketime` won’t work on macOS or other systems with the BSD `date` command. On such systems, use the `-d` option to specify the GNU `date` command rather than the default `date` command; for example, on a macOS system, install the [Homebrew `coreutils` package](https://formulae.brew.sh/formula/coreutils), and run with the `-d` option set like this: 103 | 104 | ``` 105 | brew install coreutils 106 | faketime -f "@2021-05-01 23:59:59" bash ./git-pulse -d/usr/local/bin/gdate 107 | ``` 108 | 109 | - If a repo/clone has a remote named `upstream` defined, the tool uses that remote; otherwise it uses `origin`. 110 | 111 | - Add the directory containing the `git-pulse` script to your `$PATH`, so you can just type `git pulse` to run it: 112 | 113 | ```bash 114 | mkdir git-pulse && cd git-pulse 115 | git clone https://github.com/git-pulse/tools.git 116 | cd tools 117 | echo export PATH=\"$PATH:$PWD\" >> ~/.bash_profile 118 | ``` 119 | 120 | Now you can run `git pulse` in any repo/clone directory to get a pulse snapshot for that repo. 121 | 122 | ## Pulse rankings 123 | 124 | **How to:** Generate a [pulse rankings](https://git-pulse.github.io/snapshots/) report in a directory containing multiple `*-pulse.json` snapshots: 125 | 126 | ```bash 127 | node tools/git-pulse-rank.js 128 | ``` 129 | -------------------------------------------------------------------------------- /git-pulse-rank.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* Any copyright is dedicated to the Public Domain. 3 | * http://creativecommons.org/publicdomain/zero/1.0/ */ 4 | 5 | "use strict"; 6 | import { readFileSync, readdirSync, appendFileSync, writeFileSync } from "fs"; 7 | 8 | const committersRankingsHTMLfile = "index.html"; 9 | const issuesRankingsHTMLfile = "index-rankings-issues.html"; 10 | const PRsRankingsHTMLfile = "index-rankings-PRs.html"; 11 | 12 | const filenames = readdirSync(".").filter((file) => 13 | file.endsWith("pulse.json") 14 | ); 15 | 16 | const snapshots = []; 17 | for (const filename of filenames) { 18 | console.log(filename); 19 | const snapshot = JSON.parse(readFileSync(filename, "utf-8").trim()); 20 | snapshot.filename = 21 | filename.substring(0, filename.lastIndexOf(".json")) + ".html"; 22 | const index = snapshots.findIndex( 23 | ({ full_name }) => full_name === snapshot.full_name 24 | ); 25 | if (index === -1) { 26 | snapshots.push(snapshot); 27 | } else { 28 | snapshots[index] = snapshot; 29 | } 30 | } 31 | 32 | const generateTableFor = (key, caption, HTMLfile) => { 33 | if (caption === "") { 34 | caption = key.replaceAll("_", " "); 35 | } 36 | appendFileSync(HTMLfile, " \n"); 37 | appendFileSync(HTMLfile, ` ` + 51 | `
${caption}\n`); 38 | for (const s of snapshots) { 39 | let className = s.full_name 40 | .replaceAll("/", "_") 41 | .replaceAll("-", "_") 42 | .replaceAll(".", "_"); 43 | if (s.full_name === "mdn/content") { 44 | className += " highlight"; 45 | } 46 | const value = s[key] !== undefined ? s[key] : "N/A"; 47 | const title = `Click to highlight the ${s.full_name} project’s rows in all tables.`; 48 | appendFileSync( 49 | HTMLfile, 50 | `
${s.full_name}${value}\n` 52 | ); 53 | console.log(s.full_name + ": " + value); 54 | } 55 | appendFileSync(HTMLfile, "
\n"); 56 | }; 57 | 58 | const rankCommittersDataBy = (key, caption = "") => { 59 | snapshots.sort((a, b) => b[key] - a[key]); 60 | // snapshots.sort((a, b) => { 61 | // return (b[key]===undefined)-(a[key]===undefined) || +(b[key]>a[key])||-(b[key] { 67 | snapshots.sort((a, b) => { 68 | return ( 69 | (a[key] === undefined) - (b[key] === undefined) || 70 | +(b[key] > a[key]) || 71 | -(b[key] < a[key]) 72 | ); 73 | }); 74 | generateTableFor(key, caption, issuesRankingsHTMLfile); 75 | }; 76 | 77 | const rankIssuesDataAscdendingBy = (key, caption = "") => { 78 | snapshots.sort((a, b) => { 79 | return ( 80 | (a[key] === undefined) - (b[key] === undefined) || 81 | -(b[key] > a[key]) || 82 | +(b[key] < a[key]) 83 | ); 84 | }); 85 | generateTableFor(key, caption, issuesRankingsHTMLfile); 86 | }; 87 | 88 | const rankPRsDataBy = (key, caption = "") => { 89 | snapshots.sort((a, b) => { 90 | return ( 91 | (a[key] === undefined) - (b[key] === undefined) || 92 | +(b[key] > a[key]) || 93 | -(b[key] < a[key]) 94 | ); 95 | }); 96 | generateTableFor(key, caption, PRsRankingsHTMLfile); 97 | }; 98 | 99 | const rankPRsDataAscdendingBy = (key, caption = "") => { 100 | snapshots.sort((a, b) => { 101 | return ( 102 | (a[key] === undefined) - (b[key] === undefined) || 103 | -(b[key] > a[key]) || 104 | +(b[key] < a[key]) 105 | ); 106 | }); 107 | generateTableFor(key, caption, PRsRankingsHTMLfile); 108 | }; 109 | 110 | const writeHTMLfileHeader = (HTMLfile, type) => { 111 | writeFileSync( 112 | HTMLfile, 113 | ` 114 | Pulse rankings: ${type} data 115 | 152 |
153 |
154 | 155 | git-pulse logo 156 | 157 |
158 |
159 |

Pulse rankings: ${type} data

160 | ` 161 | ); 162 | appendFileSync( 163 | HTMLfile, 164 | `

165 | Committers 166 | • 167 | Issues 168 | • 169 | PRs 170 |

171 | Click on any row to highlight the given project’s rows in all tables. 172 |

173 | Click on any project name to view the latest 174 | pulse snapshot 175 | for that project. 176 |

177 |
178 |
179 | ` 180 | ); 181 | }; 182 | 183 | const writeHTMLfileFooter = (HTMLfile) => { 184 | appendFileSync( 185 | HTMLfile, 186 | `
187 |

188 | 189 | git-pulse logo 191 | Generated with git-pulse-rank.js 192 | 216 | ` 217 | ); 218 | }; 219 | 220 | writeHTMLfileHeader(committersRankingsHTMLfile, "committer"); 221 | rankCommittersDataBy("new_committers_per_month"); 222 | rankCommittersDataBy("new_committers_percentage"); 223 | rankCommittersDataBy("total_active_committers_per_month"); 224 | rankCommittersDataBy("commits_per_month"); 225 | rankCommittersDataBy("cumulative_total_committers"); 226 | rankCommittersDataBy("forks_count"); 227 | rankCommittersDataBy("commits_per_committer_per_month"); 228 | writeHTMLfileFooter(committersRankingsHTMLfile); 229 | 230 | writeHTMLfileHeader(issuesRankingsHTMLfile, "issue"); 231 | rankIssuesDataBy("issues_opened_per_month"); 232 | rankIssuesDataBy("issues_closed_per_month"); 233 | rankIssuesDataAscdendingBy( 234 | "issues_open_to_monthly_ratio", 235 | `How many times as many open issues as the average # of issues 236 | opened per month?` 237 | ); 238 | rankIssuesDataBy( 239 | "issues_resolved_to_commits_percentage", 240 | "Percentage ratio of total closed issues to total commits" 241 | ); 242 | rankIssuesDataBy( 243 | "issues_closed_percentage", 244 | `Net closure rate for issues (% of new issues resolved each month 245 | on average)` 246 | ); 247 | rankIssuesDataBy("issues_resolved_count"); 248 | rankIssuesDataBy( 249 | "issues_resolved_to_open_ratio", 250 | "How many times as many closed issues as open issues?" 251 | ); 252 | rankIssuesDataAscdendingBy("issues_open_count"); 253 | rankIssuesDataAscdendingBy( 254 | "issues_increase_decrease", 255 | "Average increase or decrease in open issues per month" 256 | ); 257 | writeHTMLfileFooter(issuesRankingsHTMLfile); 258 | 259 | writeHTMLfileHeader(PRsRankingsHTMLfile, "PR"); 260 | rankPRsDataBy("PRs_opened_per_month"); 261 | rankPRsDataBy("PRs_merged_per_month"); 262 | rankPRsDataBy("PRs_closed_per_month"); 263 | rankPRsDataAscdendingBy( 264 | "PRs_open_to_monthly_ratio", 265 | `How many times as many open PRs as the average # of PRs 266 | opened per month?` 267 | ); 268 | rankPRsDataBy( 269 | "PRs_merged_vs_commits_ratio", 270 | `What % of commits come from PRs?
271 | (av. monthly commits / av. monthly PRs)` 272 | ); 273 | rankPRsDataBy( 274 | "PRs_resolved_to_open_ratio", 275 | "How many times as many closed PRs as open PRs?" 276 | ); 277 | rankPRsDataBy("PRs_resolved_count"); 278 | rankPRsDataBy( 279 | "PRs_merged_or_closed_percentage", 280 | `Net closure rate for PRs (% of new PRs resolved each month 281 | on average)` 282 | ); 283 | rankPRsDataAscdendingBy("PRs_open_count"); 284 | rankPRsDataAscdendingBy( 285 | "PRs_increase_decrease", 286 | "Average increase or decrease in open PRs per month" 287 | ); 288 | writeHTMLfileFooter(PRsRankingsHTMLfile); 289 | -------------------------------------------------------------------------------- /git-pulse: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://github.com/git-pulse/tools#readme 4 | 5 | date="date" 6 | CURLOPTS=--insecure 7 | 8 | remoteURLproperty=remote.origin.url 9 | if [ -n "$(git config --get remote.upstream.url)" ]; then 10 | remoteURLproperty=remote.upstream.url 11 | fi 12 | 13 | repoURL=$(git config --get $remoteURLproperty | sed -r \ 14 | 's/.*(\@|\/\/)(.*)(\:|\/)([^:\/]*)\/([^\/]*)\.git/https:\/\/\2\/\4\/\5/') 15 | orgAndRepo=$(echo "$repoURL" | rev | cut -d '/' -f-2 | rev) 16 | 17 | isGitHubRepo=false 18 | [[ $repoURL == *"github"* ]] && isGitHubRepo=true 19 | 20 | repoMetadata="" 21 | if [ $isGitHubRepo = true ]; then 22 | repoMetadata=$(curl -fsSL "https://api.github.com/repos/$orgAndRepo") 23 | fi 24 | 25 | echo "https://api.github.com/repos/$orgAndRepo/commits?per_page=1&page=1" 26 | 27 | totalCommits=$(curl -I -fsSL \ 28 | "https://api.github.com/repos/$orgAndRepo/commits?per_page=1&page=1" \ 29 | | grep -i "^link:" | cut -d ";" -f2 | cut -d "=" -f4 | sed 's/>//') 30 | 31 | forksCount=$(echo "$repoMetadata" | grep -m1 '"forks_count":' \ 32 | | cut -d ":" -f2 | tr -d " ,") 33 | 34 | hasIssues=false 35 | if echo "$repoMetadata" | grep -q '"has_issues": true,'; then 36 | hasIssues=true 37 | fi 38 | 39 | searchBase="https://api.github.com/search/issues?q=repo:$orgAndRepo" 40 | linkBase="https://github.com/search?q=repo:$orgAndRepo" 41 | 42 | hasGnuplot=false 43 | [ "$(command -v gnuplot >/dev/null 2>&1; echo $?)" ] && hasGnuplot=true 44 | 45 | sleepseconds=9 46 | 47 | while getopts "d:is:" opt; do 48 | case "$opt" in 49 | d) date=$OPTARG 50 | ;; 51 | i) interactive=true 52 | ;; 53 | s) sleepseconds=$OPTARG 54 | ;; 55 | *) 56 | exit 1 57 | ;; 58 | esac 59 | done 60 | shift $((OPTIND-1)) 61 | # if no months given from command line arg, show data for past 13 months 62 | months=${1:-13} 63 | [[ $months =~ ^[0-9]+$ ]] || months=13 # if arg not integer, default to 13 64 | [ "$months" -gt 0 ] || months=13 65 | 66 | date="$date -u" 67 | $date --date="" > /dev/null 2>&1 || hasGnuDate=$? 68 | 69 | today=$($date +"%d %B %Y") 70 | now="$($date +"%H:%M") UTC" 71 | nowIso8601=$(date -u +"%Y-%m-%dT%H:%M:%SZ") 72 | 73 | htmlFile=pulse.html 74 | htmlFile=$(echo "$orgAndRepo" | tr '/' '-')-$($date +"%Y-%m-%d")-"$htmlFile" 75 | 76 | jsonFile=pulse.json 77 | jsonFile=$(echo "$orgAndRepo" | tr '/' '-')-$($date +"%Y-%m-%d")-"$jsonFile" 78 | 79 | hiEscape="\033[0;1m" 80 | hi=$hiEscape 81 | hiOff="\033[0m" 82 | 83 | writeDualAxisGraphToHTMLFile() { 84 | LC_CTYPE="en_US.UTF-8" 85 | title=$1 86 | righttitle=$2 87 | lefttitle=$3 88 | rightcolor=$4 89 | leftcolor=$5 90 | id=$6 91 | shift 6 92 | printf '%s\n' "$@" | tac | cat - <(echo "e") \ 93 | | gnuplot -p -e "\ 94 | set term svg name '$(echo "$title" | tr -d " /+%")' size 668,440 fixed font ',16'; \ 95 | set style line 1 lw 2 pointtype 7 pointsize 0.5 linecolor rgb '$leftcolor'; 96 | set style line 2 lw 2 pointtype 7 pointsize 0.5 linecolor rgb '$rightcolor'; 97 | set xrange reverse; 98 | set ylabel '$lefttitle' offset +1,0 textcolor rgb '$leftcolor'; 99 | set ytics nomirror textcolor rgb '$leftcolor'; 100 | set y2label '$righttitle' offset -1,0 textcolor rgb '$rightcolor' rotate by 270; 101 | set y2tics textcolor rgb '$rightcolor'; 102 | set xlabel 'Months before $today' offset 0,+0.5; 103 | set title '{/:Bold $title – $orgAndRepo}' textcolor rgb '#32cd32'; 104 | set key left top opaque fillcolor '0x7fcccccc' font ',12' textcolor variable; 105 | set rmargin 9; 106 | plot 107 | '-' with linespoints title '$lefttitle' axes x1y1 ls 1, 108 | '-' with linespoints title '$righttitle' axes x1y2 ls 2" \ 109 | | sed -E "s|(]+>)$title – $orgAndRepo()|\1🔗 $title – $orgAndRepo\2|" \ 110 | | tail -n +2 >> "$htmlFile" \ 111 | 2> >( grep -v "Fontconfig warning" 1>&2) 112 | } 113 | 114 | writeGraphToHTMLFile () { 115 | LC_CTYPE="en_US.UTF-8" 116 | title=$1 117 | ylabel=$2 118 | id=$3 119 | shift 3 120 | printf '%s\n' "$@" | tac | \ 121 | gnuplot -p -e "\ 122 | set term svg name '$(echo "$title" | tr -d " /+%")' size 668,440 fixed font ',16'; \ 123 | set xrange reverse; 124 | set ylabel '$ylabel' offse +1,0 tc lt 1; \ 125 | set ytics tc lt 1; 126 | set xlabel 'Months before $today' offset 0,+0.5; 127 | set title '{/:Bold $title – $orgAndRepo}' textcolor rgb '#32cd32'; 128 | plot '/dev/stdin' with linespoints title '' lw 2 pointtype 7 pointsize 0.5" \ 129 | | sed -E "s|(]+>)$title – $orgAndRepo()|\1🔗 $title – $orgAndRepo\2|" \ 130 | | tail -n +2 >> "$htmlFile" \ 131 | 2> >( grep -v "Fontconfig warning" 1>&2) 132 | 133 | } 134 | 135 | writeBoxChartToHTMLFile() { 136 | LC_CTYPE="en_US.UTF-8" 137 | title=$1 138 | ylabel=$2 139 | key=$3 140 | color=$4 141 | labelcolor=$5 142 | replacement="\1" 143 | href="$(echo "$6" | sed 's/\//\\\//g')" 144 | [ -n "$href" ] && replacement="\1<\/a>" 145 | [ -n "$href" ] && yticscolor="web-blue" || yticscolor="black" 146 | shift 6 147 | [[ $key == *"PRs"* ]] && xlabel="Number of PRs" 148 | height=$(($(echo "$@" | wc -l) * 28)) 149 | [ "$height" -lt 600 ] && height="600" 150 | printf '%s\n' "$@" | tac | \ 151 | gnuplot -p -e " 152 | set term svg name '$(echo "$title" | tr -d " /+%")' \ 153 | size 668,$height fixed font ',16'; 154 | set style fill transparent solid 0.5; 155 | set title '$title' tc rgb '#ba5313'; 156 | set key opaque fillcolor '0x7fdddddd' font ',12' textcolor variable; 157 | set ylabel '$ylabel' offset +1,0 tc rgb 'web-blue'; 158 | set ytics tc rgb '$yticscolor' scale 0; 159 | set xlabel; 160 | unset xtics; 161 | unset border; 162 | data = system('cat -'); set print \$db; print data; unset print; 163 | plot \$db using (\$2*0.5):0:(\$2*0.5):(0.4):yticlabels(1) with \ 164 | boxxyerrorbars lc '$color' title '$key', 165 | \$db using (\$2*0.5):0:(\$2*0.5):(0.5):yticlabels(1) with \ 166 | labels font 'arial,13' tc rgb '$labelcolor' t ''" \ 167 | | tail -n +2 \ 168 | | sed -E "s/([0-9]{4}-[0-9][0-9])/$replacement/" \ 169 | >> "$htmlFile" \ 170 | 2> >( grep -v "Fontconfig warning" 1>&2) 171 | } 172 | 173 | showDualAxisGraphInTerminal() { 174 | LC_CTYPE="en_US.UTF-8" 175 | title=$1 176 | righttitle=$2 177 | lefttitle=$3 178 | rightcolor=$4 179 | leftcolor=$5 180 | shift 5 181 | printf '%s\n' "$@" | tac | cat - <(echo "e") | \ 182 | gnuplot -p -e "\ 183 | set term sixelgd scroll size 1225,800 font arial 22; 184 | set style line 1 lw 3 pointtype 7 pointsize 1.5 linecolor rgb '$leftcolor'; 185 | set style line 2 lw 3 pointtype 7 pointsize 1.5 linecolor rgb '$rightcolor'; 186 | set xrange reverse; 187 | set ylabel '$lefttitle' textcolor rgb '$leftcolor'; 188 | set ytics nomirror textcolor rgb '$leftcolor'; 189 | set y2label '$righttitle' textcolor rgb '$rightcolor' rotate by 270; 190 | set y2tics textcolor rgb '$rightcolor'; 191 | set xlabel 'Months before $today'; 192 | set title '$title – $orgAndRepo'; 193 | set key left top opaque fillcolor '0x7fdddddd' height .8 font ',16'; 194 | plot 195 | '-' with linespoints title '$lefttitle' axes x1y1 ls 1, 196 | '-' with linespoints title '$righttitle' axes x1y2 ls 2" \ 197 | 2> >( grep -v "Fontconfig warning" 1>&2) 198 | } 199 | 200 | showGraphInTerminal() { 201 | LC_CTYPE="en_US.UTF-8" 202 | title=$1 203 | ylabel=$2 204 | shift 2 205 | printf '%s\n' "$@" | tac | \ 206 | gnuplot -p -e "\ 207 | set term sixelgd scroll size 1225,800 font arial 22; 208 | set xrange reverse; \ 209 | set ylabel '$ylabel' tc lt 1; 210 | set ytics tc lt 1; 211 | set xlabel 'Months before $today'; 212 | set title '$title – $orgAndRepo'; 213 | plot '/dev/stdin' with linespoints title '' lw 3 pointtype 7 pointsize 1.5" \ 214 | 2> >( grep -v "Fontconfig warning" 1>&2) 215 | } 216 | 217 | showBoxChartInTerminal() { 218 | LC_CTYPE="en_US.UTF-8" 219 | title=$1 220 | ylabel=$2 221 | key=$3 222 | color=$4 223 | labelcolor=$5 224 | shift 5 225 | xlabel="Number of issues" 226 | [[ $key == *"PRs"* ]] && xlabel="Number of PRs" 227 | height=$(($(echo "$@" | wc -l) * 55)) 228 | [ "$height" -lt 600 ] && height="600" 229 | printf '%s\n' "$@" | tac | \ 230 | gnuplot -p -e "\ 231 | set term sixelgd scroll size 1225,$height font arial 20; 232 | set ylabel '$ylabel'; 233 | set title '$title – $orgAndRepo'; 234 | set xlabel '$xlabel'; 235 | set style fill transparent solid 0.5; 236 | set key opaque fillcolor '0x7fdddddd' height .8 font ',16'; 237 | data = system('cat -'); set print \$db; print data; unset print; 238 | plot \$db using (\$2*0.5):0:(\$2*0.5):(0.4):yticlabels(1) with \ 239 | boxxyerrorbars lc '$color' title '$key', 240 | \$db using (\$2*0.5):0:(\$2*0.5):(0.5):yticlabels(1) with \ 241 | labels font 'arial,17' tc rgb '$labelcolor' offset 0,-.2 t ''" \ 242 | 2> >( grep -v "Fontconfig warning" 1>&2) 243 | } 244 | 245 | collectStats() { 246 | stats=$(echo "$@" | gnuplot -e 'stats "-"' 2>&1) 247 | records=$(echo "$stats" | grep "Records:" \ 248 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 249 | minimum=$(echo "$stats" | grep "Minimum:" \ 250 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 251 | maximum=$(echo "$stats" | grep "Maximum:" \ 252 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 253 | firstQu=$(echo "$stats" | grep "Quartile:" | head -n1 \ 254 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 255 | median=$(echo "$stats" | grep "Median:" \ 256 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 257 | mean=$(echo "$stats" | grep "Mean:" \ 258 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 259 | thirdQu=$(echo "$stats" | grep "Quartile:" | tail -n1 \ 260 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 261 | stdDev=$(echo "$stats" | grep "Sample StdDev:" \ 262 | | cut -d ":" -f2 | tr -d ' ' | awk '{print int($1+0.5)}') 263 | days90th="$(echo "$@" \ 264 | | sed -n "$(( 90 * $(echo "$@" | wc -l) / 100))"p)" 265 | if [ -z "$days90th" ]; then 266 | days90th="$1" 267 | fi 268 | } 269 | 270 | showStatsInTerminal() { 271 | minimu="$(printf "%7s" "$1")" 272 | maximu="$(printf "%7s" "$2")" 273 | shift 2 274 | collectStats "$@" 275 | echo "┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐" 276 | echo "│ $minimu │ 1st Qu │ Median │ Mean │ 3rd Qu │ 𝜂90th │ $maximu │" 277 | echo "├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤" 278 | echo -n "│ $(printf "%7s" "$minimum")" 279 | echo -n " │ $(printf "%7s" "$firstQu")" 280 | echo -n " │ $(printf "%7s" "$median")" 281 | echo -n " │ $(printf "%7s" "$mean")" 282 | echo -n " │ $(printf "%7s" "$thirdQu")" 283 | echo -n " │ $(printf "%7s" "$days90th")" 284 | echo -n " │ $(printf "%7s" "$maximum")" 285 | echo " │" 286 | echo "│ days │ days │ days │ days │ days │ days │ days │" 287 | echo "└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘" 288 | echo " Standard deviation: $stdDev days" 289 | } 290 | 291 | writeStatsToHTMLFile() { 292 | minim="$(printf "%6s" "$1")" 293 | maxim="$(printf "%6s" "$2")" 294 | id="$3" 295 | shift 3 296 | collectStats "$@" 297 | cat << EOF >> "$htmlFile" 298 | 299 | 301 | 302 | 310 | 311 | 319 | 320 |
🔗 $heading 300 |
$minim 303 | 1st Qu 304 | Median 305 | Mean 306 | 3rd Qu 307 | 𝜂90th 308 | $maxim 309 |
$minimum days 312 | $firstQu days 313 | $median days 314 | $mean days 315 | $thirdQu days 316 | $days90th days 317 | $maximum days 318 |
Standard deviation: $stdDev days 321 |
322 | EOF 323 | } 324 | 325 | getDaysBetweenTwoDates() { 326 | if [ "$hasGnuDate" != 1 ]; then 327 | days=$((($($date -u --date="$1" +"%s") \ 328 | - $($date -u --date="$2" +"%s"))/86400)) 329 | else 330 | # BSD date(1) (used on, e.g., macOS) 331 | days=$((($($date -jf "%Y-%m-%dT%H:%M:%SZ" "$1" +%s) \ 332 | - $($date -jf "%Y-%m-%dT%H:%M:%SZ" "$2" +%s))/86400)) 333 | fi 334 | echo -n "$days" 335 | } 336 | 337 | divide() { 338 | if [ "$2" = 0 ]; then 339 | echo "$1 1" | awk '{ printf("%.2f\n", ( $1 / $2 )) }' 340 | else 341 | echo "$1 $2" | awk '{ printf("%.2f\n", ( $1 / $2 )) }' 342 | fi 343 | } 344 | 345 | if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then 346 | echo 347 | echo -n -e \ 348 | "${hi}Error:${hiOff} This tool must be run from within a git repo." >&2 349 | echo -e " Stopping." >&2 350 | exit 1; 351 | fi 352 | 353 | if [ "$hasGnuDate" != 1 ]; then 354 | startDate=$($date -u --date="-$months month" +"%Y-%m-%dT%H:%M:%SZ") 355 | else 356 | # BSD date(1) (used on, e.g., macOS) 357 | startDate=$($date -u -v-"$months"m +"%Y-%m-%dT%H:%M:%SZ") 358 | fi 359 | endDate=$($date -u +"%Y-%m-%dT%H:%M:%SZ") 360 | 361 | cat << EOF > "$htmlFile" 362 | 363 | $orgAndRepo - pulse snapshot for $today 364 | 423 |

424 |
425 | git-pulse logo 426 |
427 |
428 |

Pulse snapshot for $today

429 |

$repoURL

430 |

JSON snapshot also available 431 |

432 |
433 |

🔗 502 | Committer pulse

503 |
504 | 505 | 506 | 507 | 511 | 514 | 518 | EOF 519 | for (( i=months - 1; i >= 0; i-- )); do 520 | hi='' 521 | [ $((i%2)) -eq 0 ] && hi=$hiEscape 522 | if [ -z "$allCommittersPrevious" ]; then 523 | allCommittersPrevious=$(git shortlog -s --no-merges \ 524 | --before="$((i + 1)) months back" | wc -l) 525 | else 526 | allCommittersPrevious=$allCommitters 527 | fi 528 | allCommitters=$(git shortlog -s --no-merges \ 529 | --before="$i months back" | wc -l | xargs) 530 | if [ "$i" -eq "$((months - 1))" ]; then 531 | initialAllCommitters=$allCommitters 532 | fi 533 | newCommitters=$((allCommitters - allCommittersPrevious)) 534 | uniqCommitters=$(git shortlog -s --no-merges --before="$i months back" \ 535 | --since="$((i + 1)) months back" | wc -l | xargs) 536 | newToUniqPercentage=0 537 | [ "$uniqCommitters" -gt 0 ] && newToUniqPercentage="$(echo \ 538 | "$(echo "$newCommitters / $uniqCommitters" | bc -l) \ 539 | * 100 / 1" | bc)" 540 | monthlyCommits=$(git log --no-merges --oneline --before="$i months back" \ 541 | --since="$((i + 1)) months back" | wc -l | xargs) 542 | newCommittersArray+=("$newCommitters") 543 | uniqCommittersArray+=("$uniqCommitters") 544 | newToUniqArray+=("$newToUniqPercentage") 545 | commitsArray+=("$monthlyCommits") 546 | allCommittersArray+=("$allCommitters") 547 | echo -n -e "│ ${hi}$(printf "%3s" "$newCommitters")${hiOff} " 548 | echo -n -e "│ ${hi}$(printf "%5s" "$uniqCommitters")${hiOff} " 549 | echo -n -e "│ ${hi}$(printf "%4s" "$newToUniqPercentage")${hiOff} " 550 | echo -n -e "│ ${hi}$(printf "%5s" "$allCommitters")${hiOff} " 551 | echo -n -e "│ ${hi}$(printf "%5s" "$monthlyCommits")${hiOff} " 552 | cat << EOF >> "$htmlFile" 553 | 554 |
Committers 508 | Mo-
nthly
com-
mits 509 |
Month range 510 |
Monthly 512 | Cu-
mula-
tive 513 |
New 515 | Uniq 516 | %New 517 |
$newCommitters 555 | $uniqCommitters 556 | $newToUniqPercentage 557 | $allCommitters 558 | $monthlyCommits 559 | EOF 560 | echo -n ' ' >> "$htmlFile" 561 | if [ "$hasGnuDate" != 1 ]; then 562 | # echo -n -e "│ ${hi}$($date \ 563 | # --date "$($date --date="-$((i + 1)) month") +1 day" +"%b %d")" 564 | echo -n -e "│ ${hi}$($date --date="-$((i + 1)) month" +"%b %d")" 565 | echo -n -e " – $($date --date="-$i month" +"%b %d %Y")" 566 | # echo -n "$($date \ 567 | # --date "$($date --date="-$((i + 1)) month") +1 day" +"%b %d")" \ 568 | # >> "$htmlFile" 569 | echo -n "$($date --date="-$((i + 1)) month" +"%b %d")" \ 570 | >> "$htmlFile" 571 | echo -n " – $($date --date="-$i month" +"%b %d %Y")" >> "$htmlFile" 572 | else 573 | # BSD date(1) (used on, e.g., macOS) 574 | # echo -n -e "│ ${hi}$($date -j -v+1d -f "%Y-%m-%d" \ 575 | # "$($date -v-"$((i + 1))"m +"%Y-%m-%d")" +"%b %d")" 576 | echo -n -e "│ ${hi}$($date -v-"$((i + 1))"m +"%b %d")" 577 | echo -n -e " – $($date -v-"$i"m +"%b %d %Y")" 578 | # echo -n "$($date -j -v+1d -f "%Y-%m-%d" \ 579 | # "$($date -v-"$((i + 1))"m +"%Y-%m-%d")" +"%b %d")" >> "$htmlFile" 580 | echo -n "$($date -v-"$((i + 1))"m +"%b %d")" >> "$htmlFile" 581 | echo -n " – $($date -v-"$i"m +"%b %d %Y")" >> "$htmlFile" 582 | fi 583 | if [ "$i" -eq 0 ]; then 584 | echo -e " ending $now${hiOff} │" 585 | echo " ending $now" >> "$htmlFile" 586 | else 587 | echo -e " ending $(printf "%-10s" $i"mo back")${hiOff} │" 588 | echo " ending ${i}mo back" >> "$htmlFile" 589 | fi 590 | done 591 | echo "└─────┴───────┴──────┴───────┴───────┴────────────────────────────────────────┘" 592 | echo "
" >> "$htmlFile" 593 | 594 | avgNewCommitters=$(($(tot=0; 595 | for i in "${newCommittersArray[@]}"; \ 596 | do ((tot+=i)); done; echo $tot) / months)) 597 | avgUniqCommitters=$(($(tot=0; 598 | for i in "${uniqCommittersArray[@]}"; \ 599 | do ((tot+=i)); done; echo $tot) / months)) 600 | avgCommits=$(($(tot=0; 601 | for i in "${commitsArray[@]}"; \ 602 | do ((tot+=i)); done; echo $tot) / months)) 603 | newToUniqPercentage=0 604 | [ $avgUniqCommitters -gt 0 ] && newToUniqPercentage="$(echo \ 605 | "$(echo "$avgNewCommitters / $avgUniqCommitters" | bc -l) \ 606 | * 100 / 1" | bc)" 607 | 608 | hi=$hiEscape 609 | [ "$months" -gt 1 ] || exit 0 610 | echo 611 | echo " For the last $months months:" 612 | echo 613 | echo -n "$(printf "%8s" "$avgNewCommitters")" 614 | echo " new committers per month on average." 615 | echo -n -e "${hi}$(printf "%8s" "$avgUniqCommitters")" 616 | echo -e " total active committers per month on average.${hiOff}" 617 | echo -n -e "$(printf "%8s" "$newToUniqPercentage")" 618 | echo -e " percent of committers were new committers." 619 | echo -n -e "${hi}$(printf "%8s" "$avgCommits")" 620 | echo -e " commits per month on average.${hiOff}" 621 | echo -n -e "$(printf "%8s" "$(divide $avgCommits $avgUniqCommitters)")" 622 | echo -e " commits per committer per month on average." 623 | echo 624 | 625 | echo -n -e " Cumulative total committers grew by" 626 | echo -n -e " ${hi}$((allCommitters - initialAllCommitters))${hiOff}" 627 | echo -n -e " (from ${hi}$initialAllCommitters${hiOff}" 628 | echo -n -e " to ${hi}$allCommitters${hiOff})" 629 | echo -n -e " in $((months - 1))" 630 | 631 | if [ $hasGnuplot != true ]; then 632 | cat << EOF >> "$htmlFile" 633 |

634 | To see graphs, install gnuplot — e.g., 635 | with apt install gnuplot (Ubuntu), 636 | or brew install gnuplot (macOS). 637 | EOF 638 | else 639 | writeDualAxisGraphToHTMLFile \ 640 | "Committers per month" \ 641 | "Total committers" \ 642 | "New committers" \ 643 | "dark-salmon" "forest-green" \ 644 | "committers_per_month" \ 645 | "${uniqCommittersArray[@]}" "e" "${newCommittersArray[@]}" 646 | fi 647 | 648 | cat << EOF >> "$htmlFile" 649 |

650 |

🔗 651 | For the last $months months:

652 |
    653 |
  • $avgNewCommitters new committers per month on average. 654 |
  • $avgUniqCommitters total active committers per month on average. 655 |
  • $newToUniqPercentage percent of committers were new committers. 656 |
  • $avgCommits commits per month on average. 657 |
  • $(divide $avgCommits $avgUniqCommitters) commits per committer 658 | per month on average. 659 |
660 |

Cumulative total committers grew by 661 | $((allCommitters - initialAllCommitters)) 662 | (from $initialAllCommitters to $allCommitters) in $((months - 1)) 663 | EOF 664 | if [ "$months" -gt 2 ]; then 665 | echo -e " months.${hiOff}" 666 | echo " months." >> "$htmlFile" 667 | else 668 | echo -e " month.${hiOff}" 669 | echo " month." >> "$htmlFile" 670 | fi 671 | echo "

" >> "$htmlFile" 672 | # end of committer summary section 673 | echo "
" >> "$htmlFile" 674 | # end of committer upper container 675 | 676 | if [ $hasGnuplot = true ]; then 677 | writeDualAxisGraphToHTMLFile \ 678 | "Total committers and % new" \ 679 | "Cumulative total committers" \ 680 | "New commiters percentage" \ 681 | "orange" "web-blue" \ 682 | "total_committers" \ 683 | "${allCommittersArray[@]}" "e" "${newToUniqArray[@]}" 684 | writeGraphToHTMLFile \ 685 | "Commits per month" \ 686 | "Commits per month" \ 687 | "commits_per_month" \ 688 | "${commitsArray[@]}" 689 | else 690 | echo -n " To see graphs, install gnuplot;"; 691 | echo " e.g., with 'apt install gnuplot' (Ubuntu),"; 692 | echo "or 'brew install gnuplot' (macOS)."; 693 | fi 694 | 695 | echo 696 | echo " ────────────────────────────────────────────────────────────────────────────" 697 | echo 698 | 699 | if [[ $interactive = true ]]; then 700 | echo -n -e " ${hi}$htmlFile${hiOff}" 701 | echo -e " will be generated as you page" 702 | echo -e " through the rest of this snapshot in the terminal." 703 | echo 704 | read -rsp $' Press any key to continue, or Ctrl-C to exit...\n' -n1 \ 705 | && tput cuu1 && tput el 706 | echo 707 | fi 708 | 709 | showDualAxisGraphInTerminal "Committers per month" \ 710 | "Total active committers" "New committers" \ 711 | "dark-salmon" "forest-green" \ 712 | "${uniqCommittersArray[@]}" "e" "${newCommittersArray[@]}" 713 | [[ $interactive = true ]] && read -rsp \ 714 | $'Press any key to see next committer graph...\n' -n1 \ 715 | && tput cuu1 && tput el; echo 716 | showDualAxisGraphInTerminal "Total committers and % new" \ 717 | "Cumulative total committers" "New committers percentage" \ 718 | "orange" "medium-blue" \ 719 | "${allCommittersArray[@]}" "e" "${newToUniqArray[@]}" 720 | [[ $interactive = true ]] && read -rsp \ 721 | $'Press any key to see final committer graph...\n' -n1 \ 722 | && tput cuu1 && tput el; echo 723 | showGraphInTerminal "Commits per month" \ 724 | "Commits per month" "${commitsArray[@]}" 725 | 726 | if [ $isGitHubRepo != true ]; then 727 | exit 728 | fi 729 | 730 | if [ -z "$orgAndRepo" ]; then 731 | echo 732 | echo -n -e \ 733 | "${hi}Error:${hiOff} This tool must be run from within a clone " >&2 734 | echo -e "of a GitHub repo. Stopping." >&2 735 | exit 1; 736 | fi 737 | 738 | if [ $hasIssues = true ]; then 739 | snapshotType="issues" 740 | else 741 | snapshotType="PRs" 742 | echo -n -e "${hi}" 743 | cat << EOF 744 | Note: The GitHub issue tracker for this repo is disabled. 745 | The snapshot will include data for PRs only. 746 | 747 | EOF 748 | echo -n -e "${hiOff}" 749 | fi 750 | 751 | [[ $interactive = true ]] && read -rsp \ 752 | "Press any key to start generating the $snapshotType snapshot..."$'\n' \ 753 | -n1 && tput cuu1 && tput el; echo 754 | 755 | cat << EOF 756 | ─────────────────────────────────────────────────────────────────────────── 757 | EOF 758 | 759 | cat << EOF 760 | Generating the $snapshotType snapshot takes a few minutes, due to GitHub 761 | API rate limits. If you hit the limits, you’ll get 403 errors here. So, 762 | to adjust the number of seconds between API calls, use the -s option: 763 | EOF 764 | echo 765 | echo -e " ${hi}bash ${BASH_SOURCE[*]} -s10${hiOff}" 766 | cat << EOF 767 | ─────────────────────────────────────────────────────────────────────────── 768 | EOF 769 | 770 | if [ -n "$GITHUB_TOKEN" ]; then 771 | token="$GITHUB_TOKEN"; 772 | elif [ -n "$GH_TOKEN" ]; then 773 | token="$GH_TOKEN"; 774 | else 775 | echo 776 | echo -n -e \ 777 | "${hi}Warning:${hiOff} No GITHUB_TOKEN or GH_TOKEN set. " >&2 778 | echo -e "Requests will be unauthenticated." >&2 779 | fi 780 | 781 | if [ -n "$token" ]; then 782 | authorizationHeader="Authorization: token $token" 783 | fi 784 | 785 | issuesClosedArray=() 786 | issuesOpenedArray=() 787 | issuesDeltaArray=() 788 | PRsMergedArray=() 789 | PRsClosedArray=() 790 | PRsMergedOrClosedArray=() 791 | PRsOpenedArray=() 792 | PRsDeltaArray=() 793 | 794 | $date --date="" > /dev/null 2>&1 || hasGnuDate=$? 795 | echo 796 | echo -e " ${hi}$repoURL${hiOff}" 797 | echo "┌────────────────┬─────────────────────┐" 798 | echo "│ Issues │ PRs │" 799 | echo "├────────────────┼─────────────────────┤" 800 | echo "│ Clsd Opnd +- │ Mrgd Clsd Opnd +- │ Month range" 801 | echo "├────────────────┼─────────────────────┼────────────────────────────────────────┐" 802 | 803 | issuesNote="" 804 | if [ $hasIssues != true ]; then 805 | issuesNote="\ 806 |

Note: The GitHub issue tracker for this repo is disabled. 807 | This snapshot includes data for PRs only." 808 | fi 809 | cat << EOF >> "$htmlFile" 810 |

🔗 811 | Issue and PR pulse

812 | $issuesNote 813 |
814 | 815 | 816 | 817 | 821 | 829 | EOF 830 | 831 | currentStart=$startDate 832 | currentEnd=$endDate 833 | for (( i=months - 1; i >= 0; i-- )); do 834 | tput sc 835 | echo -n "☕️ Getting monthly issue and PR data ($((months - i))/$months)" 836 | if [ $i -ne $((months - 1)) ]; then 837 | for (( j=1; j <=sleepseconds; j++)); do 838 | sleep 1 839 | echo -n "." 840 | done 841 | fi 842 | hi='' 843 | [ $((i%2)) -eq 0 ] && hi=$hiEscape 844 | if [ "$hasGnuDate" != 1 ]; then 845 | currentStart=$($date --date \ 846 | "$($date --date="-$((i + 1)) month") +1 day" +"%Y-%m-%d") 847 | currentEnd=$($date --date="-$i month" +"%Y-%m-%d") 848 | else 849 | # BSD date(1) (used on, e.g., macOS) 850 | currentStart="$($date -j -v+1d -f "%Y-%m-%d" \ 851 | "$($date -v-"$((i + 1))"m +"%Y-%m-%d")" +"%Y-%m-%d")" 852 | currentEnd=$($date -v-"$i"m +"%Y-%m-%d") 853 | fi 854 | issuesClosedQuery="type:issue+closed:$currentStart..$currentEnd" 855 | issuesOpenedQuery="type:issue+created:$currentStart..$currentEnd" 856 | PRsMergedQuery="type:pr+merged:$currentStart..$currentEnd" 857 | PRsClosedQuery="type:pr+is:unmerged+closed:$currentStart..$currentEnd" 858 | PRsOpenedQuery="type:pr+created:$currentStart..$currentEnd" 859 | issuesClosedResponseString="$(printf "%4s" \ 860 | "$(curl -i $CURLOPTS -H "$authorizationHeader" -fsSL \ 861 | "$searchBase+$issuesClosedQuery")")" 862 | echo -n "." 863 | realdate="$(echo "$issuesClosedResponseString" \ 864 | | grep -i "^date:" | cut -c12-22)" 865 | if [ "$hasGnuDate" != 1 ]; then 866 | realdateYmd="$($date --date="$realdate" +"%Y-%m-%d")" 867 | else 868 | realdateYmd="$(date -j -f "%d %b %Y" "$realdate" +"%Y-%m-%d")" 869 | fi 870 | issuesClosed="$(printf "%4s" "$(echo "$issuesClosedResponseString" \ 871 | | grep '^ "total_count"' | cut -d ':' -f2- | sed 's/,//' | xargs)")" 872 | issuesOpened="$(printf "%4s" \ 873 | "$(curl $CURLOPTS -H "$authorizationHeader" -fsSL \ 874 | "$searchBase+$issuesOpenedQuery" \ 875 | | grep '^ "total_count"' | cut -d ':' -f2- | sed 's/,//' | xargs)")" 876 | echo -n "." 877 | issuesDelta=$((issuesOpened - issuesClosed)) 878 | sign="" && [[ "$issuesDelta" -gt 0 ]] && sign="+" 879 | issuesDelta=$(printf "%4s" "$sign$issuesDelta") 880 | PRsMerged="$(printf "%4s" \ 881 | "$(curl $CURLOPTS -H "$authorizationHeader" -fsSL \ 882 | "$searchBase+$PRsMergedQuery" \ 883 | | grep '^ "total_count"' | cut -d ':' -f2- | sed 's/,//' | xargs)")" 884 | echo -n "." 885 | PRsClosed="$(printf "%4s" \ 886 | "$(curl $CURLOPTS -H "$authorizationHeader" -fsSL \ 887 | "$searchBase+$PRsClosedQuery" \ 888 | | grep '^ "total_count"' | cut -d ':' -f2- | sed 's/,//' | xargs)")" 889 | echo -n "." 890 | PRsMergedOrClosed=$((PRsMerged + PRsClosed)) 891 | PRsOpened="$(printf "%4s" \ 892 | "$(curl $CURLOPTS -H "$authorizationHeader" -fsSL \ 893 | "$searchBase+$PRsOpenedQuery" \ 894 | | grep '^ "total_count"' | cut -d ':' -f2- | sed 's/,//' | xargs)")" 895 | # erase coffee line 896 | tput rc && tput el 897 | 898 | PRsDelta=$((PRsOpened - PRsMerged - PRsClosed)) 899 | sign="" && [[ "$PRsDelta" -gt 0 ]] && sign="+" 900 | PRsDelta=$(printf "%4s" "$sign$PRsDelta") 901 | issuesClosedArray+=("$issuesClosed") 902 | issuesOpenedArray+=("$issuesOpened") 903 | issuesDeltaArray+=("$issuesDelta") 904 | PRsMergedArray+=("$PRsMerged") 905 | PRsClosedArray+=("$PRsClosed") 906 | PRsMergedOrClosedArray+=("$PRsMergedOrClosed") 907 | PRsOpenedArray+=("$PRsOpened") 908 | PRsDeltaArray+=("$PRsDelta") 909 | echo -n -e "│ ${hi}$issuesClosed${hiOff}" 910 | echo -n -e " ${hi}$issuesOpened${hiOff}" 911 | echo -n -e " ${hi}$issuesDelta${hiOff} " 912 | echo -n -e "│ ${hi}$PRsMerged${hiOff}" 913 | echo -n -e " ${hi}$PRsClosed${hiOff}" 914 | echo -n -e " ${hi}$PRsOpened${hiOff}" 915 | echo -n -e " ${hi}$PRsDelta${hiOff} " 916 | 917 | cat << EOF >> "$htmlFile" 918 | 919 |
Issues 818 | PRs 819 | Month range 820 |
Clsd 822 | Opnd 823 | + - 824 | Mrgd 825 | Clsd 826 | Opnd 827 | + - 828 |
$issuesClosed 920 | $issuesOpened 921 | $issuesDelta 922 | $PRsMerged 923 | $PRsClosed 924 | $PRsOpened 925 | $PRsDelta 926 | EOF 927 | echo -n ' ' >> "$htmlFile" 928 | if [ "$hasGnuDate" != 1 ]; then 929 | # echo -n -e "│ ${hi}$($date --date \ 930 | # "$($date --date="-$((i + 1)) month") +1 day" +"%b %d")" 931 | echo -n -e "│ ${hi}$($date --date="-$((i + 1)) month" +"%b %d")" 932 | echo -n -e " - $($date --date="-$i month" +"%b %d %Y")" 933 | # echo -n "$($date --date \ 934 | # "$($date --date="-$((i + 1)) month") +1 day" +"%b %d")" \ 935 | # >> "$htmlFile" 936 | echo -n "$($date --date="-$((i + 1)) month" +"%b %d")" \ 937 | >> "$htmlFile" 938 | echo -n " – $($date --date="-$i month" +"%b %d %Y")" >> "$htmlFile" 939 | else 940 | # BSD date(1) (used on, e.g., macOS) 941 | # echo -n -e "│ ${hi}$($date -j -v+1d -f "%Y-%m-%d" \ 942 | # "$($date -v-"$((i + 1))"m +"%Y-%m-%d")" +"%b %d")" 943 | echo -n -e "│ ${hi}$($date -v-"$((i + 1))"m +"%b %d")" 944 | echo -n -e " – $($date -v-"$i"m +"%b %d %Y")" 945 | # echo -n "$($date -j -v+1d -f "%Y-%m-%d" \ 946 | # "$($date -v-"$((i + 1))"m +"%Y-%m-%d")" +"%b %d")" >> "$htmlFile" 947 | echo -n "$($date -v-"$((i + 1))"m +"%b %d")" >> "$htmlFile" 948 | echo -n " – $($date -v-"$i"m +"%b %d %Y")" >> "$htmlFile" 949 | fi 950 | if [ "$i" -eq 0 ]; then 951 | echo -e " ending $now${hiOff} │" 952 | echo " ending $now" >> "$htmlFile" 953 | else 954 | echo -e " ending $(printf "%-10s" $i"mo back")${hiOff} │" 955 | echo " ending ${i}mo back" >> "$htmlFile" 956 | fi 957 | done 958 | 959 | echo "└────────────────┴─────────────────────┴────────────────────────────────────────┘" 960 | echo "
" >> "$htmlFile" 961 | 962 | avgIssuesClosed=$(($(total=0; 963 | for i in "${issuesClosedArray[@]}"; \ 964 | do ((total+=i)); done; echo $total) / months)) 965 | avgIssuesOpened=$(($(total=0; 966 | for i in "${issuesOpenedArray[@]}"; \ 967 | do ((total+=i)); done; echo $total) / months)) 968 | avgIssuesDelta=$(($(total=0; 969 | for i in "${issuesDeltaArray[@]}"; \ 970 | do ((total+=i)); done; echo $total) / months)) 971 | 972 | avgPRsMerged=$(($(total=0; 973 | for i in "${PRsMergedArray[@]}"; \ 974 | do ((total+=i)); done; echo $total) / months)) 975 | avgPRsClosed=$(($(total=0; 976 | for i in "${PRsClosedArray[@]}"; \ 977 | do ((total+=i)); done; echo $total) / months)) 978 | avgPRsOpened=$(($(total=0; 979 | for i in "${PRsOpenedArray[@]}"; \ 980 | do ((total+=i)); done; echo $total) / months)) 981 | avgPRsDelta=$(($(total=0; 982 | for i in "${PRsDeltaArray[@]}"; \ 983 | do ((total+=i)); done; echo $total) / months)) 984 | avgPRsDelta=$(($(total=0; 985 | for i in "${PRsDeltaArray[@]}"; \ 986 | do ((total+=i)); done; echo $total) / months)) 987 | 988 | hi=$hiEscape 989 | if [ "$months" -gt 1 ]; then 990 | echo " For the last $months months:" 991 | echo 992 | if [ $hasIssues = true ]; then 993 | echo -n -e "${hi}" 994 | if [ $avgIssuesOpened = 0 ]; then 995 | echo -n "$(printf "%8s" $((avgIssuesClosed * 100 / 1)))" 996 | else 997 | echo -n "$(printf "%8s" $((avgIssuesClosed * 100 / avgIssuesOpened)))" 998 | fi 999 | echo -n -e " percent net closure rate for issues" 1000 | echo " (issues resolved during this time period vs issues opened)." 1001 | fi 1002 | echo -n -e "${hiOff}" 1003 | if [ $avgPRsOpened = 0 ]; then 1004 | echo -n "$(printf "%8s" \ 1005 | $(((avgPRsMerged + avgPRsClosed) * 100 / 1 )))" 1006 | else 1007 | echo -n "$(printf "%8s" \ 1008 | $(((avgPRsMerged + avgPRsClosed) * 100 / avgPRsOpened )))" 1009 | fi 1010 | echo -n " percent net closure rate for PRs" 1011 | echo " (PRs merged/closed during this time period vs PRs opened)." 1012 | 1013 | if [ $hasIssues = true ]; then 1014 | echo 1015 | echo -n -e "${hi}$(printf "%8s" $avgIssuesClosed)" 1016 | echo " issues closed per month on average." 1017 | echo -n -e "${hiOff}" 1018 | 1019 | echo -n "$(printf "%8s" $avgIssuesOpened)" 1020 | echo " issues opened per month on average." 1021 | increaseOrDecrease="increase" 1022 | [[ $avgIssuesDelta -lt 0 ]] && increaseOrDecrease="decrease" 1023 | [[ $avgIssuesDelta -gt 0 ]] && avgIssuesDelta="+$avgIssuesDelta" 1024 | echo -n -e "${hi}$(printf "%8s" $avgIssuesDelta)" 1025 | echo " issue $increaseOrDecrease in open issues per month on average." 1026 | echo -n -e "${hiOff}" 1027 | fi 1028 | 1029 | PRtoCommitPercentage=0 1030 | [ "$avgCommits" -gt 0 ] && PRtoCommitPercentage="$(echo \ 1031 | "$(echo "$avgPRsMerged / $avgCommits" | bc -l) \ 1032 | * 100 / 1" | bc)" 1033 | 1034 | echo 1035 | echo -n -e "${hi}" 1036 | echo -n "$(printf "%8s" $avgPRsMerged)" 1037 | echo " PRs merged per month on average." 1038 | echo -n -e "${hiOff}" 1039 | echo -n "$(printf "%8s" "$PRtoCommitPercentage")" 1040 | echo " percent merged PRs / commits ratio" 1041 | echo -n " (average $avgPRsMerged merged PRs" 1042 | echo " vs average $avgCommits commits)." 1043 | echo -n "$(printf "%8s" $avgPRsClosed)" 1044 | echo " PRs closed (unmerged) per month on average." 1045 | echo -n -e "${hi}" 1046 | echo -n "$(printf "%8s" $avgPRsOpened)" 1047 | echo " PRs opened per month on average." 1048 | echo -n -e "${hiOff}" 1049 | increaseOrDecrease="increase" 1050 | [[ $avgPRsDelta -lt 0 ]] && increaseOrDecrease="decrease" 1051 | [[ $avgPRsDelta -gt 0 ]] && avgPRsDelta="+$avgPRsDelta" 1052 | echo -n -e "$(printf "%8s" $avgPRsDelta)" 1053 | echo -e " PR $increaseOrDecrease in open PRs per month on average." 1054 | fi 1055 | 1056 | # Issue averages 1057 | totalIssuesOpened=$(total=0; 1058 | for i in "${issuesOpenedArray[@]}"; \ 1059 | do ((total+=i)); done; echo $total) 1060 | totalIssuesClosed=$(total=0; 1061 | for i in "${issuesClosedArray[@]}"; \ 1062 | do ((total+=i)); done; echo $total) 1063 | 1064 | echo 1065 | 1066 | if [ "$months" -gt 1 ]; then 1067 | netIssuesOpened=$((totalIssuesOpened - totalIssuesClosed)) 1068 | 1069 | if [ $hasIssues = true ]; then 1070 | echo -n -e "${hi}" 1071 | increaseOrDecrease="increase" 1072 | [[ $netIssuesOpened -lt 0 ]] && increaseOrDecrease="decrease" 1073 | [[ $netIssuesOpened -gt 0 ]] && netIssuesOpened="+$netIssuesOpened" 1074 | echo -n -e "$(printf "%8s" $netIssuesOpened) issue" 1075 | echo -n -e " $increaseOrDecrease" 1076 | echo -e " in open issues overall." 1077 | fi 1078 | fi 1079 | echo -ne "${hiOff}" 1080 | 1081 | # PR averages 1082 | totalPRsClosed=$(total=0; 1083 | for i in "${PRsClosedArray[@]}"; \ 1084 | do ((total+=i)); done; echo $total) 1085 | totalPRsMerged=$(total=0; 1086 | for i in "${PRsMergedArray[@]}"; \ 1087 | do ((total+=i)); done; echo $total) 1088 | totalPRsOpened=$(total=0; 1089 | for i in "${PRsOpenedArray[@]}"; \ 1090 | do ((total+=i)); done; echo $total) 1091 | 1092 | if [ "$months" -gt 1 ]; then 1093 | netPRsOpened=$((totalPRsOpened - totalPRsMerged - totalPRsClosed)) 1094 | 1095 | increaseOrDecrease="increase" 1096 | [[ $netPRsOpened -lt 0 ]] && increaseOrDecrease="decrease" 1097 | [[ $netPRsOpened -gt 0 ]] && netPRsOpened="+$netPRsOpened" 1098 | echo -n -e "$(printf "%8s" "$netPRsOpened") PR" 1099 | increaseOrDecrease="increase" 1100 | if [[ $netPRsOpened -lt 0 ]]; then 1101 | increaseOrDecrease="decrease" 1102 | fi 1103 | echo -n -e " $increaseOrDecrease" 1104 | echo -e " in open PRs overall." 1105 | fi 1106 | 1107 | echo 1108 | echo " ────────────────────────────────────────────────────────────────────────────" 1109 | echo 1110 | 1111 | if [ $hasGnuplot = true ]; then 1112 | if [ $hasIssues = true ]; then 1113 | writeDualAxisGraphToHTMLFile \ 1114 | "Increase/decrease in open issues/PRs" \ 1115 | "Increase/decrease in open PRs" \ 1116 | "Increase/decrease in open issues" \ 1117 | "dark-salmon" "forest-green" \ 1118 | "increase_decrease" \ 1119 | "${PRsDeltaArray[@]}" "e" "${issuesDeltaArray[@]}" 1120 | else 1121 | writeGraphToHTMLFile \ 1122 | "Increase/decrease in open PRs" \ 1123 | "Increase/decrease in open PRs" \ 1124 | "increase_decrease" \ 1125 | "${PRsDeltaArray[@]}" 1126 | fi 1127 | fi 1128 | 1129 | # Issues summary section 1130 | 1131 | cat << EOF >> "$htmlFile" 1132 |
1133 |

🔗 1134 | For the last $months months:

1135 | EOF 1136 | 1137 | issuesClosedPercentage=0 1138 | issuesPercentItem="" 1139 | if [ $hasIssues = true ]; then 1140 | if [ $avgIssuesOpened = 0 ]; then 1141 | issuesClosedPercentage=$((avgIssuesClosed * 100 / 1)) 1142 | else 1143 | issuesClosedPercentage=$((avgIssuesClosed * 100 / avgIssuesOpened)) 1144 | fi 1145 | issuesPercentItem="\ 1146 |
  • $issuesClosedPercentage 1147 | percent net closure rate for issues (issues resolved during this 1148 | time period vs issues opened)." 1149 | fi 1150 | if [ $avgPRsOpened = 0 ]; then 1151 | PRsMergedOrClosedPercentage=$((( \ 1152 | avgPRsMerged + avgPRsClosed) * 100 / 1)) 1153 | else 1154 | PRsMergedOrClosedPercentage=$((( \ 1155 | avgPRsMerged + avgPRsClosed) * 100 / avgPRsOpened )) 1156 | fi 1157 | cat << EOF >> "$htmlFile" 1158 |
      1159 | $issuesPercentItem 1160 |
    • $PRsMergedOrClosedPercentage 1161 | percent net closure rate for PRs (PRs merged/closed during this time 1162 | period vs PRs opened). 1163 |
    1164 | EOF 1165 | 1166 | if [ $hasIssues = true ]; then 1167 | cat << EOF >> "$htmlFile" 1168 |
      1169 |
    • $avgIssuesClosed issues closed per month on average. 1170 |
    • $avgIssuesOpened issues opened per month on average. 1171 | EOF 1172 | increaseOrDecrease="increase" 1173 | if [[ $avgIssuesDelta -lt 0 ]]; then 1174 | increaseOrDecrease="decrease" 1175 | fi 1176 | cat << EOF >> "$htmlFile" 1177 |
    • $avgIssuesDelta issue $increaseOrDecrease in open issues per 1178 | month on average. 1179 |
    1180 | EOF 1181 | fi 1182 | 1183 | cat << EOF >> "$htmlFile" 1184 |
      1185 |
    • $avgPRsMerged PRs merged per month on average. 1186 |
    • $PRtoCommitPercentage percent merged PRs / commits ratio 1187 | (avg. $avgPRsMerged merged PRs vs avg. $avgCommits commits). 1188 |
    • $avgPRsClosed PRs closed (unmerged) per month on average. 1189 |
    • $avgPRsOpened PRs opened per month on average. 1190 | EOF 1191 | increaseOrDecrease="increase" 1192 | if [[ $avgPRsDelta -lt 0 ]]; then 1193 | increaseOrDecrease="decrease" 1194 | fi 1195 | cat << EOF >> "$htmlFile" 1196 |
    • $avgPRsDelta PR $increaseOrDecrease in open PRs per 1197 | month on average. 1198 |
    1199 |
      1200 | EOF 1201 | 1202 | if [ $hasIssues = true ]; then 1203 | increaseOrDecrease="increase" 1204 | [[ $netIssuesOpened -lt 0 ]] && increaseOrDecrease="decrease" 1205 | 1206 | cat << EOF >> "$htmlFile" 1207 |
    • $netIssuesOpened issue $increaseOrDecrease 1208 | in open issues overall. 1209 | EOF 1210 | fi 1211 | 1212 | increaseOrDecrease="increase" 1213 | [[ $netPRsOpened -lt 0 ]] && increaseOrDecrease="decrease" 1214 | increaseOrDecrease="increase" 1215 | if [[ $netPRsOpened -lt 0 ]]; then 1216 | increaseOrDecrease="decrease" 1217 | fi 1218 | 1219 | cat << EOF >> "$htmlFile" 1220 |
    • $netPRsOpened PR $increaseOrDecrease 1221 | in open PRs overall. 1222 |
    1223 |
  • 1224 |
    1225 | EOF 1226 | 1227 | if [ $hasGnuplot = true ]; then 1228 | 1229 | if [ $hasIssues = true ]; then 1230 | writeDualAxisGraphToHTMLFile \ 1231 | "Issues closed and opened" \ 1232 | "Issues opened" \ 1233 | "Issues closed" \ 1234 | "orange" "web-blue" \ 1235 | "issues_closed_opened" \ 1236 | "${issuesOpenedArray[@]}" "e" "${issuesClosedArray[@]}" 1237 | fi 1238 | 1239 | writeDualAxisGraphToHTMLFile \ 1240 | "PRs merged/closed and opened" \ 1241 | "PRs opened" \ 1242 | "PRs merged+closed" \ 1243 | "light-red" "dark-violet" \ 1244 | "PRs_merged_closed_opened" \ 1245 | "${PRsOpenedArray[@]}" "e" "${PRsMergedOrClosedArray[@]}" 1246 | 1247 | [[ $interactive = true ]] && read -rsp \ 1248 | $'Press any key to view issue/PR graphs...\n' -n1 \ 1249 | && tput cuu1 && tput el; echo 1250 | 1251 | if [ $hasIssues = true ]; then 1252 | showDualAxisGraphInTerminal "Increase/decrease in open issues/PRs" \ 1253 | "Increase/decrease in open PRs" "Increase/decrease in open issues" \ 1254 | "dark-salmon" "forest-green" \ 1255 | "${PRsDeltaArray[@]}" "e" "${issuesDeltaArray[@]}" 1256 | else 1257 | showGraphInTerminal "Increase/decrease in open PRs" \ 1258 | "Increase/decrease in open PRs" "${PRsDeltaArray[@]}" 1259 | fi 1260 | 1261 | if [ $hasIssues = true ]; then 1262 | [[ $interactive = true ]] && read -rsp \ 1263 | $'Press any key to see next issue/PR graph...\n' -n1 \ 1264 | && tput cuu1 && tput el; echo 1265 | showDualAxisGraphInTerminal "Issues closed and opened" \ 1266 | "Issues opened" "Issues closed" \ 1267 | "orange" "medium-blue" \ 1268 | "${issuesOpenedArray[@]}" "e" "${issuesClosedArray[@]}" 1269 | fi 1270 | 1271 | [[ $interactive = true ]] && read -rsp \ 1272 | $'Press any key to see final issue/PR graph...\n' -n1 \ 1273 | && tput cuu1 && tput el; echo 1274 | showDualAxisGraphInTerminal "PRs merged+closed and opened" \ 1275 | "PRs opened" "PRs merged+closed" \ 1276 | "light-red" "dark-violet" \ 1277 | "${PRsOpenedArray[@]}" "e" "${PRsMergedOrClosedArray[@]}" 1278 | 1279 | fi 1280 | issuesOpenStats="" 1281 | if [ $hasIssues = true ]; then 1282 | 1283 | echo 1284 | echo " ────────────────────────────────────────────────────────────────────────────" 1285 | echo 1286 | 1287 | tput sc 1288 | echo -n "☕️ Getting count of open issues" 1289 | 1290 | sleep $((sleepseconds / 3)) 1291 | echo -n "." 1292 | sleep $((sleepseconds / 3)) 1293 | echo -n "." 1294 | 1295 | issuesCurrentlyOpenQuery='type:issue+state:open+created:%3C='"$realdateYmd"'' 1296 | issuesCurrentlyOpenResults=$(curl -i \ 1297 | $CURLOPTS -H "$authorizationHeader" -fsSL \ 1298 | "$searchBase+$issuesCurrentlyOpenQuery&per_page=100") 1299 | echo -n "." 1300 | issuesCurrentlyOpen="$(echo "$issuesCurrentlyOpenResults" \ 1301 | | grep '^ "total_count"' | cut -d ':' -f2- | sed 's/,//' | xargs)" 1302 | 1303 | # erase coffee line 1304 | tput rc && tput el 1305 | 1306 | echo -n -e "${hi}" 1307 | echo -n -e "$(printf "%8s" "$issuesCurrentlyOpen") issues were open" 1308 | echo -e " as of $realdate." 1309 | echo -n -e "${hiOff}" 1310 | echo -n -e "$(printf "%8s" \ 1311 | "$(divide "$issuesCurrentlyOpen" "$avgIssuesOpened")")" 1312 | echo -n -e " times as many issues currently open as the average number" 1313 | echo -e " of issues" 1314 | echo -e " opened per month." 1315 | 1316 | echo 1317 | echo " $linkBase+$issuesCurrentlyOpenQuery" 1318 | echo 1319 | 1320 | if [ "$issuesCurrentlyOpen" != 0 ]; then 1321 | issuesCurrentlyOpenPageCount=$(echo "$issuesCurrentlyOpenResults" \ 1322 | | grep -i "^link:" | cut -d ";" -f2 | cut -d "=" -f5 | sed 's/>//') 1323 | 1324 | tput sc 1325 | if [ "$issuesCurrentlyOpen" -lt 1000 ]; then 1326 | echo -n "☕️ Getting data for all $issuesCurrentlyOpen open issues" 1327 | else 1328 | echo -n "☕️ Getting data for latest 1000 open issues" 1329 | fi 1330 | 1331 | if [ -n "$issuesCurrentlyOpenPageCount" ]; then 1332 | for i in $(seq 2 "$issuesCurrentlyOpenPageCount"); do 1333 | echo -n "." 1334 | issuesCurrentlyOpenResults+=$(curl \ 1335 | $CURLOPTS -H "$authorizationHeader" -fsSL \ 1336 | "$searchBase+$issuesCurrentlyOpenQuery&sort=updated&per_page=100&page=$i") 1337 | done 1338 | fi 1339 | 1340 | issuesCurrentlyOpenDates=$(echo "$issuesCurrentlyOpenResults" \ 1341 | | grep '^ "created_at"' | cut -d'"' -f4) 1342 | 1343 | issueDaysCount=0 1344 | issuesOpenRawDaysArray=() 1345 | for issueDate in $issuesCurrentlyOpenDates; do 1346 | if ((issueDaysCount % 300 == 0)); then 1347 | echo -n "." 1348 | fi 1349 | ((issueDaysCount++)) 1350 | days=$(getDaysBetweenTwoDates "$nowIso8601" "$issueDate") 1351 | issuesOpenRawDaysArray+=("$days") 1352 | done 1353 | 1354 | # erase coffee line 1355 | tput rc && tput el 1356 | 1357 | issuesCurrentlyOpenByMonth=$(echo "$issuesCurrentlyOpenDates" \ 1358 | | cut -d "-" -f1-2 | sort | uniq -c | sed -E 's/^ *([^ ]+) (.+)/\2 \1/') 1359 | 1360 | echo 1361 | if [ "$issuesCurrentlyOpen" -lt 1000 ]; then 1362 | [[ $interactive = true ]] && read -rsp \ 1363 | "Press any key to view distribution of all $issuesCurrentlyOpen open \ 1364 | issues by month created..."$'\n' -n1 1365 | else 1366 | [[ $interactive = true ]] && read -rsp \ 1367 | "Press any key to view distribution of latest 1000 open \ 1368 | issues by month created..."$'\n' -n1 1369 | fi 1370 | tput cuu1 && tput el 1371 | 1372 | echo 1373 | if [ $hasGnuplot = true ]; then 1374 | title="Open issues" 1375 | [ "$issuesCurrentlyOpen" -ge 1000 ] && title="Latest 1000 open issues" 1376 | showBoxChartInTerminal "$title" "Month created" \ 1377 | "Number of issues still open" "orange" "#444444"\ 1378 | "$issuesCurrentlyOpenByMonth" 1379 | else 1380 | if [ "$issuesCurrentlyOpen" -ge 1000 ]; then 1381 | echo " Latest 1000" 1382 | echo " open issues" 1383 | fi 1384 | echo "┌─────────┬────────┐" 1385 | echo "│ Month │ Issues │" 1386 | echo "│ created │ open │" 1387 | echo "├─────────┼────────┤" 1388 | while IFS= read -r line; do 1389 | echo -n "│ " 1390 | echo -n "$(echo "$line" | cut -d ' ' -f1)" 1391 | echo -n " │ " 1392 | echo -n "$(printf "%6s" "$(echo "$line" | cut -d ' ' -f2)")" 1393 | echo " │" 1394 | done <<< "$issuesCurrentlyOpenByMonth" 1395 | echo "└─────────┴────────┘" 1396 | fi 1397 | 1398 | echo 1399 | if [ $hasGnuplot = true ]; then 1400 | if [ -n "${issuesOpenRawDaysArray[*]}" ]; then 1401 | if [ "$issuesCurrentlyOpen" -lt 1000 ]; then 1402 | echo " Age statistics for all open issues" 1403 | else 1404 | echo " Age statistics for latest 1000 open issues" 1405 | fi 1406 | showStatsInTerminal "Newest" "Oldest" \ 1407 | "$(printf '%s\n' "${issuesOpenRawDaysArray[@]}" | sort -n)" 1408 | read -r -d '' issuesOpenStats <" >> "$htmlFile" 1895 | 1896 | # HTML open issues ################################################### 1897 | 1898 | if [ $hasIssues = true ]; then 1899 | if [ "$issuesCurrentlyOpen" != 0 ]; then 1900 | cat << EOF >> "$htmlFile" 1901 |
    1902 |

    🔗 1903 | Open issues

    1904 |

    1905 | $issuesCurrentlyOpen 1906 | issues were open as of $realdate 1907 |

    $(divide "$issuesCurrentlyOpen" "$avgIssuesOpened") times as many 1908 | issues currently open as the
    1909 | average number of issues opened per month. 1910 |

    $(divide "$issuesClosed" "$issuesCurrentlyOpen") times as many 1911 | closed issues as open issues. 1912 |

    1913 | EOF 1914 | 1915 | issuesCurrentlyOpenQueryBase='type:issue+state:open+created' 1916 | 1917 | if [ $hasGnuplot = true ]; then 1918 | title="" 1919 | [ "$issuesCurrentlyOpen" -ge 1000 ] && title="Latest 1000 open issues" 1920 | writeBoxChartToHTMLFile "$title" "Month created" \ 1921 | "Number of issues still open" "orange" "#444444"\ 1922 | "$linkBase+$issuesCurrentlyOpenQueryBase" \ 1923 | "$issuesCurrentlyOpenByMonth" 1924 | else 1925 | if [ "$issuesCurrentlyOpen" -ge 1000 ]; then 1926 | caption="Latest 1000
    open issues" 1927 | fi 1928 | 1929 | cat << EOF >> "$htmlFile" 1930 | 1931 | 1933 | 1934 | 1937 | EOF 1938 | 1939 | while IFS= read -r line; do 1940 | date=$(echo -n "$line" | cut -d " " -f1) 1941 | count=$(echo -n "$line" | cut -d " " -f2) 1942 | href="$linkBase+$issuesCurrentlyOpenQueryBase:$date" 1943 | cat << EOF >> "$htmlFile" 1944 | 1945 |
    $caption 1932 |
    Month
    created 1935 |
    Open
    issues 1936 |
    $date 1946 | $count 1947 | EOF 1948 | done <<< "$issuesCurrentlyOpenByMonth" 1949 | cat << EOF >> "$htmlFile" 1950 |
    1951 | EOF 1952 | fi 1953 | 1954 | if [ $hasGnuplot = true ]; then 1955 | if [ -n "${issuesOpenRawDaysArray[*]}" ]; then 1956 | if [ "$issuesCurrentlyOpen" -lt 1000 ]; then 1957 | heading="Age statistics for all open issues" 1958 | else 1959 | heading="Age statistics for latest 1000 open issues" 1960 | fi 1961 | writeStatsToHTMLFile "Newest" "Oldest" "open-issues-age-stats" \ 1962 | "$(printf '%s\n' "${issuesOpenRawDaysArray[@]}" | sort -n)" 1963 | fi 1964 | fi 1965 | echo "

    " >> "$htmlFile" 1966 | fi 1967 | fi 1968 | 1969 | # HTML open PRs ###################################################### 1970 | 1971 | if [ "$PRsCurrentlyOpen" != 0 ]; then 1972 | cat << EOF >> "$htmlFile" 1973 |
    1974 |

    🔗 1975 | Open PRs

    1976 |

    1977 | $PRsCurrentlyOpen PRs 1978 | were open as of $realdate 1979 |

    $(divide "$PRsCurrentlyOpen" "$avgPRsOpened") times as many 1980 | PRs currently open as the
    1981 | average number of PRs opened per month. 1982 |

    $(divide "$PRsClosed" "$PRsCurrentlyOpen") times as many 1983 | closed PRs as open PRs. 1984 |

    1985 | EOF 1986 | 1987 | PRsCurrentlyOpenQueryBase='type:pr+state:open+created' 1988 | 1989 | if [ $hasGnuplot = true ]; then 1990 | title="" 1991 | [ "$PRsCurrentlyOpen" -ge 1000 ] && title="Latest 1000 open PRs" 1992 | writeBoxChartToHTMLFile "$title" "Month created" \ 1993 | "Number of PRs still open" "light-red" "#444444"\ 1994 | "$linkBase+$PRsCurrentlyOpenQueryBase" \ 1995 | "$PRsCurrentlyOpenByMonth" 1996 | else 1997 | if [ "$PRsCurrentlyOpen" -ge 1000 ]; then 1998 | caption="Latest 1000
    open PRs" 1999 | fi 2000 | 2001 | cat << EOF >> "$htmlFile" 2002 | 2003 | 2005 | 2006 | 2009 | EOF 2010 | 2011 | while IFS= read -r line; do 2012 | date=$(echo -n "$line" | cut -d " " -f1) 2013 | count=$(echo -n "$line" | cut -d " " -f2) 2014 | href="$linkBase+$PRsCurrentlyOpenQueryBase:$date" 2015 | cat << EOF >> "$htmlFile" 2016 | 2017 |
    $caption 2004 |
    Month
    created 2007 |
    Open
    PRs 2008 |
    $date 2018 | $count 2019 | EOF 2020 | done <<< "$PRsCurrentlyOpenByMonth" 2021 | cat << EOF >> "$htmlFile" 2022 |
    2023 | EOF 2024 | fi 2025 | 2026 | if [ $hasGnuplot = true ]; then 2027 | if [ -n "${PRsOpenRawDaysArray[*]}" ]; then 2028 | if [ "$PRsCurrentlyOpen" -lt 1000 ]; then 2029 | heading="Age statistics for all open PRs" 2030 | else 2031 | heading="Age statistics for latest 1000 open PRs" 2032 | fi 2033 | writeStatsToHTMLFile "Newest" "Oldest" "open-prs-age-stats" \ 2034 | "$(printf '%s\n' "${PRsOpenRawDaysArray[@]}" | sort -n)" 2035 | fi 2036 | fi 2037 | echo "

    " >> "$htmlFile" 2038 | fi 2039 | 2040 | # HTML closed issues ################################################# 2041 | 2042 | if [ $hasIssues = true ]; then 2043 | if [ "$issuesClosed" != 0 ]; then 2044 | cat << EOF >> "$htmlFile" 2045 |
    2046 |

    🔗 2047 | Issues resolved

    2048 |

    2049 | $issuesClosed issues 2050 | were closed as of $realdate 2051 |

    $(divide "$issuesClosed" "$issuesCurrentlyOpen") times as many 2052 | closed issues as open issues. 2053 |

    $closedIssuesToCommitsPercentage percent ratio of total closed 2054 | issues to total commits. 2055 |

    2056 | EOF 2057 | 2058 | issuesClosedQueryBase='type:issue+state:closed+created' 2059 | 2060 | if [ $hasGnuplot = true ]; then 2061 | title="" 2062 | [ "$issuesClosed" -ge 1000 ] && title="Latest 1000 closed issues" 2063 | writeBoxChartToHTMLFile "$title" "Month created" \ 2064 | "Number of issues closed" "forest-green" "white"\ 2065 | "$linkBase+$issuesClosedQueryBase" \ 2066 | "$issuesClosedByMonth" 2067 | else 2068 | if [ "$issuesClosed" -ge 1000 ]; then 2069 | caption="Latest 1000
    closed issues" 2070 | fi 2071 | cat << EOF >> "$htmlFile" 2072 | 2073 | 2075 | 2076 | 2079 | EOF 2080 | while IFS= read -r line; do 2081 | date=$(echo -n "$line" | cut -d " " -f1) 2082 | count=$(echo -n "$line" | cut -d " " -f2) 2083 | cat << EOF >> "$htmlFile" 2084 | 2085 |
    $caption 2074 |
    Month
    closed 2077 |
    Closed
    issues 2078 |
    $date 2086 | $count 2087 | EOF 2088 | done <<< "$issuesClosedByMonth" 2089 | cat << EOF >> "$htmlFile" 2090 |
    2091 | EOF 2092 | fi 2093 | 2094 | if [ $hasGnuplot = true ]; then 2095 | if [ -n "${issuesClosedRawDaysArray[*]}" ]; then 2096 | if [ "$issuesClosed" -lt 1000 ]; then 2097 | heading="Time-to-close statistics for all closed issues" 2098 | else 2099 | heading="Time-to-close statistics for latest 1000 closed issues" 2100 | fi 2101 | writeStatsToHTMLFile "Fastest" "Longest" "closed-issues-ttc-stats" \ 2102 | "$(printf '%s\n' "${issuesClosedRawDaysArray[@]}" | sort -n)" 2103 | fi 2104 | fi 2105 | echo "

    " >> "$htmlFile" 2106 | fi 2107 | fi 2108 | 2109 | # HTML closed PRs #################################################### 2110 | 2111 | if [ "$PRsClosed" != 0 ]; then 2112 | cat << EOF >> "$htmlFile" 2113 |
    2114 |

    🔗 2115 | PRs merged/closed

    2116 |

    2117 | $PRsClosed PRs 2118 | were merged/closed as of $realdate 2119 |

    $(divide "$PRsClosed" "$PRsCurrentlyOpen") times as many 2120 | closed PRs as open PRs. 2121 |

    2122 | EOF 2123 | 2124 | PRsClosedQueryBase='type:issue+state:closed+created' 2125 | 2126 | if [ $hasGnuplot = true ]; then 2127 | title="" 2128 | [ "$PRsClosed" -ge 1000 ] && title="Latest 1000 merged/closed PRs" 2129 | writeBoxChartToHTMLFile "$title" "Month created" \ 2130 | "Number of PRs merged/closed" "web-blue" "white"\ 2131 | "$linkBase+$PRsClosedQueryBase" \ 2132 | "$PRsClosedByMonth" 2133 | else 2134 | if [ "$PRsClosed" -ge 1000 ]; then 2135 | caption="Latest 1000
    closed PRs" 2136 | fi 2137 | cat << EOF >> "$htmlFile" 2138 | 2139 | 2141 | 2142 | 2145 | EOF 2146 | while IFS= read -r line; do 2147 | date=$(echo -n "$line" | cut -d " " -f1) 2148 | count=$(echo -n "$line" | cut -d " " -f2) 2149 | cat << EOF >> "$htmlFile" 2150 | 2151 |
    $caption 2140 |
    Month
    closed 2143 |
    Closed
    PRs 2144 |
    $date 2152 | $count 2153 | EOF 2154 | done <<< "$PRsClosedByMonth" 2155 | cat << EOF >> "$htmlFile" 2156 |
    2157 | EOF 2158 | fi 2159 | 2160 | if [ $hasGnuplot = true ]; then 2161 | if [ -n "${PRsClosedRawDaysArray[*]}" ]; then 2162 | if [ "$PRsClosed" -lt 1000 ]; then 2163 | heading="Time-to-close statistics for all merged/closed PRs" 2164 | else 2165 | heading="Time-to-close statistics for latest 1000 merged/closed PRs" 2166 | fi 2167 | writeStatsToHTMLFile "Fastest" "Longest" "merged-closed-prs-ttc-stats" \ 2168 | "$(printf '%s\n' "${PRsClosedRawDaysArray[@]}" | sort -n)" 2169 | fi 2170 | fi 2171 | fi 2172 | 2173 | cat << EOF >> "$htmlFile" 2174 |

    2175 | 2176 | EOF 2177 | 2178 | # HTML page footer ################################################### 2179 | 2180 | cat << EOF >> "$htmlFile" 2181 |

    2182 | git-pulse logo 2183 | Generated with git-pulse 2184 | EOF 2185 | 2186 | cat << EOF > "$jsonFile" 2187 | { 2188 | "full_name": "$orgAndRepo", 2189 | "html_url": "$repoURL", 2190 | "start_date": "$startDate", 2191 | "end_date": "$endDate", 2192 | "months": $months, 2193 | "forks_count": $forksCount, 2194 | "cumulative_total_committers": $allCommitters, 2195 | "new_committers_per_month": $avgNewCommitters, 2196 | "total_active_committers_per_month": $avgUniqCommitters, 2197 | "new_committers_percentage": $newToUniqPercentage, 2198 | "commits_per_month": $avgCommits, 2199 | "commits_per_committer_per_month": $(divide \ 2200 | $avgCommits $avgUniqCommitters), 2201 | "has_issues": $hasIssues, 2202 | EOF 2203 | if [ $hasIssues = true ]; then 2204 | cat << EOF >> "$jsonFile" 2205 | "issues_closed_percentage": $issuesClosedPercentage, 2206 | "issues_closed_per_month": $avgIssuesClosed, 2207 | "issues_opened_per_month": $avgIssuesOpened, 2208 | "issues_increase_decrease": $(echo $avgIssuesDelta | tr -d '+'), 2209 | $issuesOpenStats 2210 | $issuesResolvedStats 2211 | "issues_open_count": $issuesCurrentlyOpen, 2212 | "issues_resolved_count": $issuesClosed, 2213 | "issues_resolved_to_open_ratio": $(divide \ 2214 | "$issuesClosed" "$issuesCurrentlyOpen"), 2215 | "issues_resolved_to_commits_percentage": $closedIssuesToCommitsPercentage, 2216 | "issues_open_to_monthly_ratio": $(divide \ 2217 | "$issuesCurrentlyOpen" "$avgIssuesOpened"), 2218 | EOF 2219 | fi 2220 | cat << EOF >> "$jsonFile" 2221 | "PRs_merged_or_closed_percentage": $PRsMergedOrClosedPercentage, 2222 | "PRs_merged_vs_commits_ratio": $PRtoCommitPercentage, 2223 | "PRs_merged_per_month": $avgPRsMerged, 2224 | "PRs_closed_per_month": $avgPRsClosed, 2225 | "PRs_opened_per_month": $avgPRsOpened, 2226 | "PRs_increase_decrease": $(echo $avgPRsDelta | tr -d '+'), 2227 | $PRsOpenStats 2228 | $PRsResolvedStats 2229 | "PRs_open_count": $PRsCurrentlyOpen, 2230 | "PRs_resolved_count": $PRsClosed, 2231 | "PRs_resolved_to_open_ratio": $(divide \ 2232 | "$PRsClosed" "$PRsCurrentlyOpen"), 2233 | "PRs_open_to_monthly_ratio": $(divide \ 2234 | "$PRsCurrentlyOpen" "$avgPRsOpened") 2235 | } 2236 | EOF 2237 | 2238 | # console footer ##################################################### 2239 | 2240 | if [[ $interactive != true ]]; then 2241 | echo -n "To interactively page through this snapshot in your terminal," 2242 | echo " use the -i option:" 2243 | echo 2244 | echo " bash ${BASH_SOURCE[*]} -i" 2245 | fi 2246 | 2247 | echo 2248 | echo -e "Open ${hi}$htmlFile${hiOff} to view this snapshot in your browser." 2249 | --------------------------------------------------------------------------------