Skip to content

Instantly share code, notes, and snippets.

@ericyd
Last active January 19, 2023 18:22
Show Gist options
  • Save ericyd/c36ff66a40c3b7ede7ac9dd95ae260af to your computer and use it in GitHub Desktop.
Save ericyd/c36ff66a40c3b7ede7ac9dd95ae260af to your computer and use it in GitHub Desktop.
Git repo stats
/**
* 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()
@ericyd
Copy link
Author

ericyd commented Dec 13, 2022

Might require Node 16+

cd my-local-git-repo
curl https://gist.githubusercontent.com/ericyd/c36ff66a40c3b7ede7ac9dd95ae260af/raw/git-stats.js > git-stats.js
node git-stats.js

@ericyd
Copy link
Author

ericyd commented Jan 9, 2023

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