Skip to content

Instantly share code, notes, and snippets.

@kritultrathod
Last active January 5, 2026 08:03
Show Gist options
  • Select an option

  • Save kritultrathod/5c7bd4456586024794203dbd20a4abb5 to your computer and use it in GitHub Desktop.

Select an option

Save kritultrathod/5c7bd4456586024794203dbd20a4abb5 to your computer and use it in GitHub Desktop.
Code Churn for Git Repositories
param(
[string]$RepoPath = ".",
[string]$AuthorCsv = "churn_by_author.csv",
[string]$FileCsv = "churn_by_file.csv",
[string]$JsonOutput = "churn_dashboard.json",
[string[]]$ExcludeFolders = @("bin/", "obj/", "node_modules/", "dist/", "packages/", "out/")
)
Set-Location $RepoPath
# Pull churn data from the entire repo history
$log = git log --numstat --pretty="%H|%an" --no-merges
$results = @()
$currentCommit = $null
$currentAuthor = $null
foreach ($line in $log) {
# Commit + Author line
if ($line -match "^([0-9a-f]{40})\|(.+)$") {
$currentCommit = $matches[1]
$currentAuthor = $matches[2].Trim()
continue
}
# numstat line
if ($line -match "^(\d+|-)\s+(\d+|-)\s+(.+)$") {
$added = if ($matches[1] -eq "-") { 0 } else { [int]$matches[1] }
$deleted = if ($matches[2] -eq "-") { 0 } else { [int]$matches[2] }
$file = $matches[3]
# Determine top-level folder
$topFolder = if ($file -match "^(.*?)/") { $matches[1] + "/" } else { "<root>" }
# Skip excluded folders
if ($ExcludeFolders -contains $topFolder) {
continue
}
$results += [pscustomobject]@{
Commit = $currentCommit
Author = $currentAuthor
File = $file
Added = $added
Deleted = $deleted
}
}
}
# -------------------------------
# GROUP 1: Churn by Author
# -------------------------------
$byAuthor = $results |
Group-Object Author |
ForEach-Object {
[pscustomobject]@{
Author = $_.Name
Added = ($_.Group | Measure-Object Added -Sum).Sum
Deleted = ($_.Group | Measure-Object Deleted -Sum).Sum
Net = (($_.Group | Measure-Object Added -Sum).Sum - ($_.Group | Measure-Object Deleted -Sum).Sum)
}
}
$byAuthor | Export-Csv -Path $AuthorCsv -NoTypeInformation
# -------------------------------
# GROUP 2: Churn by File
# -------------------------------
$byFile = $results |
Group-Object File |
ForEach-Object {
[pscustomobject]@{
File = $_.Name
Added = ($_.Group | Measure-Object Added -Sum).Sum
Deleted = ($_.Group | Measure-Object Deleted -Sum).Sum
Net = (($_.Group | Measure-Object Added -Sum).Sum - ($_.Group | Measure-Object Deleted -Sum).Sum)
}
}
$byFile | Export-Csv -Path $FileCsv -NoTypeInformation
# -------------------------------
# JSON OUTPUT FOR DASHBOARDS
# -------------------------------
$dashboard = [pscustomobject]@{
GeneratedOn = (Get-Date)
ExcludedFolders = $ExcludeFolders
ChurnByAuthor = $byAuthor
ChurnByFile = $byFile
}
$dashboard | ConvertTo-Json -Depth 10 | Out-File $JsonOutput -Encoding utf8
Write-Host "`nExports complete:"
Write-Host " - $AuthorCsv"
Write-Host " - $FileCsv"
Write-Host " - $JsonOutput"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment