Last active
January 19, 2023 18:22
-
-
Save ericyd/c36ff66a40c3b7ede7ac9dd95ae260af to your computer and use it in GitHub Desktop.
Git repo stats
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Hosted at: https://gist.github.com/ericyd/c36ff66a40c3b7ede7ac9dd95ae260af | |
* Usage: | |
* cd my-local-git-repo | |
* curl https://gist.githubusercontent.com/ericyd/c36ff66a40c3b7ede7ac9dd95ae260af/raw/git-stats.js > git-stats.js | |
* node git-stats.js | |
*/ | |
const { exec } = require('child_process') | |
const { promisify } = require('util') | |
const execAsync = promisify(exec) | |
const ignoredFilePatterns = [ | |
/\.json$/, | |
/\.yaml$/, | |
/\.yml$/, | |
/\.d\.ts$/, | |
/\.md$/, | |
/\.graphql$/, | |
/__generated__/, | |
] | |
// https://git-scm.com/docs/pretty-formats | |
const authorFormat = { | |
email: 'ae', | |
name: 'an', | |
committerName: 'cn', | |
committerEmail: 'ce', | |
} | |
async function main() { | |
// shows logs in descending order | |
const logs = await execute( | |
`git log --date=local --pretty=format:'%h,%${authorFormat.email}'` | |
) | |
const lines = logs.split('\n') | |
// how many commits in your repo? | |
console.log({ | |
'total commit count': lines.length, | |
first5: lines | |
.map((head0) => head0.split(',')) | |
.map(([a]) => a) | |
.slice(0, 5), | |
}) | |
const grouped = await Promise.all(lines.map(processLine)) | |
const contributions = grouped | |
.filter(Boolean) | |
.reduce((all, { author, additions, deletions }) => { | |
if (author in all) { | |
all[author] = { | |
additions: all[author].additions + additions, | |
deletions: all[author].deletions + deletions, | |
commits: all[author].commits + 1, | |
} | |
} else { | |
all[author] = { | |
additions, | |
deletions, | |
commits: 1, | |
} | |
} | |
return all | |
}, {}) | |
console.log('\n') | |
printFormattedContributors(contributions) | |
} | |
async function processLine(head0, i, lines) { | |
process.stdout.write('.') | |
// since we're mapping through the array, we have to be more defensive about accessing elements in the array. | |
if (i + 1 >= lines.length) { | |
return null | |
} | |
const head1 = lines[i + 1] | |
const [sha0, author] = head0.split(',') | |
const [sha1] = head1.split(',') | |
const diff = await execute(`git diff --numstat ${sha1} ${sha0}`) | |
return diff.split('\n').reduce( | |
(totals, file) => { | |
if (file.trim() === '') { | |
return totals | |
} | |
const [added, deleted, filename] = file.split('\t') | |
if (ignoredFilePatterns.some((regex) => filename.match(regex))) { | |
return totals | |
} | |
// binary files will come through as "-" | |
if (!Number.isNaN(parseInt(added))) { | |
totals.additions += parseInt(added) | |
} | |
if (!Number.isNaN(parseInt(deleted))) { | |
totals.deletions += parseInt(deleted) | |
} | |
return totals | |
}, | |
{ author, additions: 0, deletions: 0 } | |
) | |
} | |
function printFormattedContributors(contributions) { | |
const contributors = Object.entries(contributions).reduce( | |
(all, [author, values]) => [...all, { author, ...values }], | |
[] | |
) | |
// sorted by additions | deletions | commits | author | |
const sorted = contributors.sort(sortBy('additions')) | |
const longestAuthor = Math.max( | |
...Object.keys(contributions).map((a) => a.length) | |
) | |
console.log( | |
`${'author'.padEnd(longestAuthor, ' ')} | additions | deletions | commits` | |
) | |
console.log( | |
`${'-'.padEnd(longestAuthor, '-')} | --------- | --------- | -------` | |
) | |
sorted | |
.map(({ author, additions, deletions, commits }) => | |
[ | |
author.padEnd(longestAuthor, ' '), | |
additions.toString().padEnd('additions'.length, ' '), | |
deletions.toString().padEnd('deletions'.length, ' '), | |
commits, | |
].join(' | ') | |
) | |
.forEach((line) => console.log(line)) | |
} | |
async function execute(command) { | |
const { stdout, stderr } = await execAsync(command) | |
if (stderr !== '') { | |
throw new Error(stderr) | |
} | |
return stdout | |
} | |
function sortBy(key) { | |
return (a, b) => (a[key] > b[key] ? -1 : a[key] < b[key] ? 1 : 0) | |
} | |
main() |
Consider adding option to analyze PRs. I believe this requires network connectivity so might not be ideal
git ls-remote origin 'pull/*/head'
Or investigate shortlog command, e.g.
git shortlog -n
git shortlog -s -n
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Might require Node 16+