Last active
January 5, 2026 08:03
-
-
Save kritultrathod/5c7bd4456586024794203dbd20a4abb5 to your computer and use it in GitHub Desktop.
Code Churn for Git Repositories
This file contains hidden or 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
| 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