A TypeScript script that analyzes the lines of code (LOC) growth of a Git repository month by month using cloc
's git-aware functionality.
- π Monthly LOC tracking over any time period
- π― Git-aware analysis - no need to checkout commits
- π Language breakdown - see which languages grow over time
- π Terminal visualization with ASCII bar charts
- πΎ CSV export for further analysis
- β‘ Flexible exclusions - exclude directories, file types, or languages
- Nix (for cloc installation)
- Bun or Node.js (for running TypeScript)
- Git repository to analyze
- Save the script as
analyze-loc-history.ts
- Make it executable:
chmod +x analyze-loc-history.ts
- Run it:
nix shell nixpkgs#cloc -c bun run analyze-loc-history.ts
#!/usr/bin/env -S nix shell nixpkgs#cloc -c bun run
import { execSync } from 'child_process'
import { writeFileSync } from 'fs'
interface LocData {
month: string
commit: string
totalLines: number
languages: Record<string, number>
}
// Get commits for the last day of each month from Oct 2023 to Aug 2025
function getMonthlyCommits(): { month: string; commit: string }[] {
const months = []
// Generate month list from Oct 2023 to Aug 2025
for (let year = 2023; year <= 2025; year++) {
const startMonth = year === 2023 ? 10 : 1 // Start from October 2023
const endMonth = year === 2025 ? 8 : 12 // End at August 2025
for (let month = startMonth; month <= endMonth; month++) {
const monthStr = `${year}-${month.toString().padStart(2, '0')}`
// Get last commit of the month
try {
const lastDayOfMonth = new Date(year, month, 0).getDate()
const untilDate = `${year}-${month.toString().padStart(2, '0')}-${lastDayOfMonth}`
const commitCmd = `git log --until="${untilDate}" --format="%H" -1`
const commit = execSync(commitCmd, { encoding: 'utf8' }).trim()
if (commit) {
months.push({ month: monthStr, commit })
}
} catch (error) {
console.warn(`No commits found for ${monthStr}`)
}
}
}
return months
}
// Run cloc on a specific git commit
function runClocForCommit(commit: string): LocData['languages'] {
try {
const clocCmd = `cloc --git ${commit} --exclude-dir=node_modules,dist,.direnv,.wrangler,.vercel,.netlify,test-results,playwright-report --exclude-lang=SQL --json --quiet`
const output = execSync(clocCmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })
const data = JSON.parse(output)
const languages: Record<string, number> = {}
// Parse cloc JSON output
Object.entries(data).forEach(([key, value]: [string, any]) => {
if (key !== 'header' && key !== 'SUM' && typeof value === 'object' && value.code) {
languages[key] = value.code
}
})
return languages
} catch (error) {
console.warn(`Failed to run cloc for commit ${commit.slice(0, 7)}`)
return {}
}
}
// Generate terminal chart
function generateTerminalChart(data: LocData[]): string {
const maxLines = Math.max(...data.map(d => d.totalLines))
const maxBarLength = 50
let chart = '\nLines of Code Over Time:\n'
chart += '=' .repeat(70) + '\n'
data.forEach(({ month, totalLines }) => {
const barLength = Math.round((totalLines / maxLines) * maxBarLength)
const bar = 'β'.repeat(barLength)
const spaces = ' '.repeat(Math.max(0, 15 - bar.length))
chart += `${month}: ${bar}${spaces} ${totalLines.toLocaleString()} lines\n`
})
chart += '=' .repeat(70) + '\n'
chart += `Peak: ${maxLines.toLocaleString()} lines\n`
return chart
}
// Generate CSV content
function generateCSV(data: LocData[]): string {
const allLanguages = new Set<string>()
data.forEach(d => Object.keys(d.languages).forEach(lang => allLanguages.add(lang)))
const languageColumns = Array.from(allLanguages).sort()
const headers = ['Month', 'Commit', 'Total_Lines', ...languageColumns.map(lang => `${lang}_Lines`)]
let csv = headers.join(',') + '\n'
data.forEach(({ month, commit, totalLines, languages }) => {
const row = [
month,
commit.slice(0, 7),
totalLines.toString(),
...languageColumns.map(lang => (languages[lang] || 0).toString())
]
csv += row.join(',') + '\n'
})
return csv
}
// Main execution
function main() {
console.log('π Analyzing repository history (excluding SQL files)...')
const monthlyCommits = getMonthlyCommits()
console.log(`π
Found ${monthlyCommits.length} monthly snapshots`)
const locData: LocData[] = []
monthlyCommits.forEach(({ month, commit }, index) => {
process.stdout.write(`\rπ Processing ${month} (${index + 1}/${monthlyCommits.length})...`)
const languages = runClocForCommit(commit)
const totalLines = Object.values(languages).reduce((sum, lines) => sum + lines, 0)
locData.push({
month,
commit,
totalLines,
languages
})
})
console.log('\nβ
Analysis complete!')
// Generate outputs
const csv = generateCSV(locData)
const chart = generateTerminalChart(locData)
// Write CSV file
writeFileSync('loc-history-analysis.csv', csv)
console.log('πΎ CSV saved to loc-history-analysis.csv')
// Print terminal chart
console.log(chart)
// Print summary
const firstMonth = locData[0]
const lastMonth = locData[locData.length - 1]
const growth = lastMonth.totalLines - firstMonth.totalLines
const growthPercent = ((growth / firstMonth.totalLines) * 100).toFixed(1)
console.log('\nπ Summary:')
console.log(`β’ Start (${firstMonth.month}): ${firstMonth.totalLines.toLocaleString()} lines`)
console.log(`β’ End (${lastMonth.month}): ${lastMonth.totalLines.toLocaleString()} lines`)
console.log(`β’ Growth: +${growth.toLocaleString()} lines (+${growthPercent}%)`)
// Top languages in final snapshot
const topLanguages = Object.entries(lastMonth.languages)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
console.log('\nπ Top Languages (current):')
topLanguages.forEach(([lang, lines]) => {
const percent = ((lines / lastMonth.totalLines) * 100).toFixed(1)
console.log(`β’ ${lang}: ${lines.toLocaleString()} lines (${percent}%)`)
})
}
if (import.meta.main) {
main()
}
Modify the getMonthlyCommits()
function to change the analysis period:
// Change these values to analyze different periods
const startMonth = year === 2023 ? 10 : 1 // Start from October 2023
const endMonth = year === 2025 ? 8 : 12 // End at August 2025
Modify the clocCmd
in runClocForCommit()
:
// Add more exclusions as needed
const clocCmd = `cloc --git ${commit} \\
--exclude-dir=node_modules,dist,.direnv,build,coverage \\
--exclude-lang=SQL,JSON \\
--json --quiet`
Change the CSV filename:
writeFileSync('my-custom-analysis.csv', csv)
node_modules
- Dependenciesdist
,build
- Build outputs.direnv
,.git
- Tool directoriestest-results
,playwright-report
- Test artifactscoverage
- Coverage reports
SQL
- Database schema files (often auto-generated)JSON
- Configuration files (if too verbose)YAML
- CI/CD configs (if not relevant to code analysis)
Lines of Code Over Time:
======================================================================
2023-10: βββββββ 12,384 lines
2023-11: ββββββββββ 18,759 lines
2024-01: βββββββββββ 20,157 lines
2024-06: βββββββββββββββββ 31,112 lines
2025-08: ββββββββββββββββββββββββββββββββββββββββββββββββββ 91,630 lines
======================================================================
Peak: 91,630 lines
π Summary:
β’ Start (2023-10): 12,384 lines
β’ End (2025-08): 91,630 lines
β’ Growth: +79,246 lines (+639.9%)
π Top Languages (current):
β’ TypeScript: 45,372 lines (49.5%)
β’ YAML: 23,399 lines (25.5%)
β’ JavaScript: 11,143 lines (12.2%)
β’ Markdown: 4,949 lines (5.4%)
β’ JSON: 3,028 lines (3.3%)
For very large repos, you might want to:
- Reduce the frequency (quarterly instead of monthly)
- Focus on specific directories:
--include-dir=src,lib
- Run during off-peak hours
# Only analyze source code directories
cloc --git ${commit} --include-dir=src,lib,packages
Modify the script to analyze different branches:
git log branch-name --until="${untilDate}" --format="%H" -1
The CSV can be imported into:
- Excel/Google Sheets for advanced charting
- Grafana for time-series dashboards
- Python/pandas for statistical analysis
- Git-aware: Uses
cloc --git
to analyze historical commits without checking them out - Efficient: No file system operations, just git object analysis
- Accurate: Respects
.gitignore
and git history automatically - Flexible: Easy to customize exclusions and date ranges
- Reproducible: Same results every time, independent of working directory state
Make sure you're running with nix shell:
nix shell nixpkgs#cloc -c bun run analyze-loc-history.ts
Check if commits exist in your date range:
git log --oneline --since="2023-01-01" --until="2023-12-31"
For large repos, consider:
- Reducing date range
- Adding more directory exclusions
- Running on a machine with more RAM/CPU
Created by: Lines of Code Analysis Script
License: MIT
Dependencies: Nix (cloc), Bun/Node.js