Public/Undo-TaxEditorDataCommit.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Undo-TaxEditorDataCommit { <# .SYNOPSIS Reverts a specific commit in the taxonomy data repository. .DESCRIPTION Creates a new revert commit via the GitHub API that undoes the changes from a specific commit. Never force-pushes. Detects conflicts when the same files have been modified in later commits. Requires GITHUB_TOKEN environment variable. .PARAMETER Sha The full or short SHA of the commit to revert. .PARAMETER Repo GitHub repo in owner/name format. Default: jpsnover/ai-triad-data. .EXAMPLE Undo-TaxEditorDataCommit -Sha 'abc1234' .EXAMPLE Undo-TaxEditorDataCommit -Sha 'abc1234' -WhatIf .LINK Show-AITriadHelp .LINK Get-TaxEditorBlob .LINK Get-TaxEditorDataCommit .LINK Restore-TaxEditorBlob .LINK Restore-TaxEditorKnownGood .LINK Set-TaxEditorKnownGood .LINK Sync-TaxEditorData #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [Parameter(Mandatory, Position = 0)] [string]$Sha, [Parameter()] [string]$Repo = 'jpsnover/ai-triad-data' ) Set-StrictMode -Version Latest $CallerName = 'Undo-TaxEditorDataCommit' # ── Get the commit to revert ───────────────────────────────────────── $Commit = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/commits/$Sha" ` -CallerName $CallerName $Parents = @($Commit.parents) if ($Parents.Count -ne 1) { throw (New-ActionableError ` -Goal "Revert commit '$Sha'" ` -Problem "Cannot revert merge commits ($($Parents.Count) parents)" ` -Location $CallerName ` -NextSteps @('Revert each parent commit individually', 'Or revert via git CLI: git revert -m 1 <sha>')) } $ParentSha = $Parents[0].sha $CommitMsg = ($Commit.commit.message -split "`n")[0] $ChangedFiles = @($Commit.files) if ($ChangedFiles.Count -eq 0) { throw (New-ActionableError ` -Goal "Revert commit '$Sha'" ` -Problem 'Commit has no file changes to revert' ` -Location $CallerName ` -NextSteps @('Verify the SHA is correct: Get-TaxEditorDataCommit')) } # ── Detect conflicts with subsequent commits ───────────────────────── $HeadRef = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/git/ref/heads/main" ` -CallerName $CallerName $HeadSha = $HeadRef.object.sha if ($HeadSha -ne $Commit.sha) { $Comparison = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/compare/$($Commit.sha)...$HeadSha" ` -CallerName $CallerName $SubsequentFiles = @($Comparison.files | ForEach-Object { $_.filename }) $RevertFiles = @($ChangedFiles | ForEach-Object { $_.filename }) $Conflicts = @($RevertFiles | Where-Object { $_ -in $SubsequentFiles }) if (@($Conflicts).Count -gt 0) { throw (New-ActionableError ` -Goal "Revert commit '$Sha'" ` -Problem "Conflict: files modified in both the target and later commits: $($Conflicts -join ', ')" ` -Location $CallerName ` -NextSteps @('Revert manually: git revert <sha>', 'Or revert newer commits first')) } } # ── WhatIf / Confirm gate ──────────────────────────────────────────── $ShortSha = $Commit.sha.Substring(0, 7) if (-not $PSCmdlet.ShouldProcess($Repo, "Revert commit $ShortSha`: '$CommitMsg' ($($ChangedFiles.Count) file(s))")) { return } # ── Build revert tree ──────────────────────────────────────────────── $HeadCommit = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/git/commits/$HeadSha" ` -CallerName $CallerName $BaseTreeSha = $HeadCommit.tree.sha $TreeEntries = [System.Collections.Generic.List[hashtable]]::new() $NeedFullTree = $false foreach ($File in $ChangedFiles) { switch ($File.status) { 'added' { $NeedFullTree = $true } { $_ -in 'removed', 'modified' } { $ParentContents = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/contents/$($File.filename)?ref=$ParentSha" ` -CallerName $CallerName $TreeEntries.Add(@{ path = $File.filename mode = '100644' type = 'blob' sha = $ParentContents.sha }) } 'renamed' { $NeedFullTree = $true } } } if ($NeedFullTree) { $FullTree = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/git/trees/${BaseTreeSha}?recursive=1" ` -CallerName $CallerName $AddedFiles = @($ChangedFiles | Where-Object { $_.status -eq 'added' } | ForEach-Object { $_.filename }) $RenamedNew = @($ChangedFiles | Where-Object { $_.status -eq 'renamed' } | ForEach-Object { $_.filename }) $SkipPaths = $AddedFiles + $RenamedNew $AllEntries = [System.Collections.Generic.List[hashtable]]::new() foreach ($Entry in $FullTree.tree) { if ($Entry.type -ne 'blob') { continue } if ($Entry.path -in $SkipPaths) { continue } $Override = $null foreach ($Te in $TreeEntries) { if ($Te.path -eq $Entry.path) { $Override = $Te; break } } if ($Override) { $AllEntries.Add($Override) } else { $AllEntries.Add(@{ path = $Entry.path mode = $Entry.mode type = 'blob' sha = $Entry.sha }) } } # Restore files that were removed or renamed away in the original commit foreach ($File in $ChangedFiles) { $RestorePath = $null if ($File.status -eq 'removed') { $RestorePath = $File.filename } if ($File.status -eq 'renamed' -and $File.PSObject.Properties['previous_filename']) { $RestorePath = $File.previous_filename } if ($RestorePath) { $Already = $false foreach ($E in $AllEntries) { if ($E.path -eq $RestorePath) { $Already = $true; break } } if (-not $Already) { $ParentContents = Invoke-GitHubApi ` -Endpoint "/repos/$Repo/contents/${RestorePath}?ref=$ParentSha" ` -CallerName $CallerName $AllEntries.Add(@{ path = $RestorePath mode = '100644' type = 'blob' sha = $ParentContents.sha }) } } } $NewTree = Invoke-GitHubApi -Endpoint "/repos/$Repo/git/trees" ` -Method POST -Body @{ tree = @($AllEntries) } ` -CallerName $CallerName } else { $NewTree = Invoke-GitHubApi -Endpoint "/repos/$Repo/git/trees" ` -Method POST -Body @{ base_tree = $BaseTreeSha; tree = @($TreeEntries) } ` -CallerName $CallerName } # ── Create revert commit ───────────────────────────────────────────── $NewCommit = Invoke-GitHubApi -Endpoint "/repos/$Repo/git/commits" ` -Method POST -Body @{ message = "Revert: $CommitMsg`n`nReverts commit $($Commit.sha)." tree = $NewTree.sha parents = @($HeadSha) } ` -CallerName $CallerName # ── Fast-forward main ref ──────────────────────────────────────────── Invoke-GitHubApi -Endpoint "/repos/$Repo/git/refs/heads/main" ` -Method PATCH -Body @{ sha = $NewCommit.sha; force = $false } ` -CallerName $CallerName | Out-Null [PSCustomObject]@{ Action = 'DataRevert' RevertedSha = $Commit.sha RevertedMsg = $CommitMsg NewSha = $NewCommit.sha Repo = $Repo Timestamp = (Get-Date).ToString('o') } } |