Skip to content

Instantly share code, notes, and snippets.

@fabienmw
Created February 25, 2026 08:27
Show Gist options
  • Select an option

  • Save fabienmw/d41ca6003386b6da5e57e0c4f37e816c to your computer and use it in GitHub Desktop.

Select an option

Save fabienmw/d41ca6003386b6da5e57e0c4f37e816c to your computer and use it in GitHub Desktop.
Delete old git branch by specifying the date
param(
[Parameter(Mandatory = $false)]
[string] $Remote = "origin",
# Cutoff in ISO format, e.g. 2025-01-01
[Parameter(Mandatory = $true)]
[datetime] $CutoffDate,
# Never delete these exact branch names
[string[]] $ProtectBranches = @("main", "master", "develop"),
# Never delete branches matching these wildcards (PowerShell -like)
[string[]] $ProtectPatterns = @("release/*", "hotfix/*"),
# If set, prints what would be deleted but does not delete
[switch] $WhatIf
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Run-Git([string[]]$Args) {
$out = & git @Args 2>&1
if ($LASTEXITCODE -ne 0) { throw "git $($Args -join ' ') failed:`n$out" }
return $out
}
# Ensure we're in a git repo
Run-Git @("rev-parse", "--is-inside-work-tree") | Out-Null
Write-Host "Fetching latest refs from '$Remote'..."
Run-Git @("fetch", "--prune", $Remote) | Out-Null
# Protect default branch (origin/HEAD -> origin/<default>)
$defaultBranch = $null
try {
$sym = Run-Git @("symbolic-ref", "--quiet", "refs/remotes/$Remote/HEAD")
# refs/remotes/origin/main -> main
$defaultBranch = ($sym.Trim() -replace "^refs/remotes/$Remote/", "")
} catch {
# ignore if not set
}
if ($defaultBranch) {
if ($ProtectBranches -notcontains $defaultBranch) { $ProtectBranches += $defaultBranch }
Write-Host "Detected default branch: $defaultBranch (protected)"
}
$cutoffUtc = $CutoffDate.ToUniversalTime()
# List remote branches + unix commit time
# Output format: origin/feature/foo<TAB>1700000000
$lines = Run-Git @("for-each-ref", "--format=%(refname:short)`t%(committerdate:unix)", "refs/remotes/$Remote")
$candidates = @()
foreach ($line in ($lines -split "`r?`n")) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split "`t"
if ($parts.Count -lt 2) { continue }
$ref = $parts[0].Trim() # origin/branch
$unix = [int64]$parts[1].Trim() # seconds
if (-not $ref.StartsWith("$Remote/")) { continue }
$branch = $ref.Substring($Remote.Length + 1) # remove "origin/"
if ($branch -eq "HEAD") { continue }
if ($ProtectBranches -contains $branch) { continue }
$isProtectedByPattern = $false
foreach ($pat in $ProtectPatterns) {
if ($branch -like $pat) { $isProtectedByPattern = $true; break }
}
if ($isProtectedByPattern) { continue }
$commitUtc = [DateTimeOffset]::FromUnixTimeSeconds($unix).UtcDateTime
if ($commitUtc -lt $cutoffUtc) {
$candidates += [pscustomobject]@{
Branch = $branch
CommitUtc = $commitUtc
}
}
}
$candidates = $candidates | Sort-Object CommitUtc
if ($candidates.Count -eq 0) {
Write-Host "No branches older than $($cutoffUtc.ToString("u")) to delete."
exit 0
}
Write-Host "Branches to delete (tip commit older than $($cutoffUtc.ToString("u"))):"
$candidates | ForEach-Object { Write-Host (" - {0} (last commit {1:u})" -f $_.Branch, $_.CommitUtc) }
if ($WhatIf) {
Write-Host "`nWhatIf set; no deletions performed."
exit 0
}
Write-Host "`nDeleting branches on remote '$Remote'..."
foreach ($b in $candidates.Branch) {
try {
Run-Git @("push", $Remote, "--delete", $b) | Out-Null
Write-Host "Deleted: $b"
} catch {
Write-Warning "Failed to delete '$b': $($_.Exception.Message)"
}
}
@fabienmw
Copy link
Author

Usage

Dry Run

.\Delete-OldBranches.ps1 -CutoffDate "2025-01-01" -WhatIf

Actual Delete

.\Delete-OldBranches.ps1 -CutoffDate "2025-01-01"

Protect extra patterns:

.\Delete-OldBranches.ps1 -CutoffDate "2025-01-01" -ProtectPatterns @("release/*","hotfix/*","keep/*") -WhatIf

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment