Created
February 25, 2026 08:27
-
-
Save fabienmw/d41ca6003386b6da5e57e0c4f37e816c to your computer and use it in GitHub Desktop.
Delete old git branch by specifying the date
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( | |
| [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)" | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
Dry Run
Actual Delete
Protect extra patterns: