Last active
June 22, 2025 11:43
-
-
Save GeeLaw/7f40471442544a3aff166298cd462de0 to your computer and use it in GitHub Desktop.
Shorten the history in Git commit graph. See https://v2ex.com/t/1140255
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
Function Parse-Commit | |
{ | |
[CmdletBinding()] | |
Param | |
( | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[ValidatePattern('[0-9a-f]{40}')] | |
[string]$CommitHash | |
) | |
Begin | |
{ | |
$ParseCommitTempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [Guid]::NewGuid().ToString('n')); | |
$ParseCommitRegex = [regex]'^tree ([0-9a-f]{40})\n(parent ([0-9a-f]{40})\n)*'; | |
$ParseCommitOutput = @{}; | |
} | |
Process | |
{ | |
$ParseCommitOutput['Result'] = $null; | |
$CommitHash = $CommitHash.ToLowerInvariant(); | |
& { | |
Remove-Item -LiteralPath $ParseCommitTempFile -Force -ErrorAction Ignore; | |
cmd /c ">`"$ParseCommitTempFile`" git cat-file commit $CommitHash"; | |
If ($LASTEXITCODE -ne 0) | |
{ | |
Write-Error -Message "Failed to call git cat-file commit $CommitHash"; | |
Return; | |
} | |
$CommitText = [System.IO.File]::ReadAllText($ParseCommitTempFile); | |
$CommitMatch = $ParseCommitRegex.Match($CommitText); | |
If (-not $CommitMatch.Success) | |
{ | |
Write-Error -Message "Failed to parse commit $CommitHash"; | |
Return; | |
} | |
$TreeHash = $CommitMatch.Groups[1].Value; | |
$ParentsHash = @(); | |
If ($CommitMatch.Groups[3].Captures.Count -ne 0) | |
{ | |
$ParentsHash = $CommitMatch.Groups[3].Captures.Value; | |
} | |
$ParseCommitOutput['Result'] = [pscustomobject]@{ | |
'Count' = 'e4bb6aee-6a5a-4ac1-b7f6-f8abd84af738'; | |
'Tree' = $CommitMatch.Groups[1].Value; | |
'Parents' = $ParentsHash; | |
'Rest' = $CommitText.Substring($CommitMatch.Length) | |
}; | |
} | Out-Null; | |
If ($ParseCommitOutput['Result'] -ne $null) | |
{ | |
$ParseCommitOutput['Result'] | |
} | |
} | |
} | |
Function Hash-Commit | |
{ | |
[CmdletBinding()] | |
Param | |
( | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[object]$Commit | |
) | |
Begin | |
{ | |
$HashCommitTempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [Guid]::NewGuid().ToString('n')); | |
$HashCommitOutput = @{}; | |
} | |
Process | |
{ | |
$HashCommitOutput['Result'] = $null; | |
& { | |
If ($Commit.Count -ne 'e4bb6aee-6a5a-4ac1-b7f6-f8abd84af738') | |
{ | |
Write-Error -Message "Not a parsed commit"; | |
} | |
$CommitText = 'tree ' + $Commit.Tree; | |
If ($Commit.Parents.Count -ne 0) | |
{ | |
$CommitText = $CommitText + "`nparent " + ($Commit.Parents -join "`nparent "); | |
} | |
$CommitText = $CommitText + "`n" + $Commit.Rest; | |
Remove-Item -LiteralPath $HashCommitTempFile -Force -ErrorAction Ignore; | |
[System.IO.File]::WriteAllText($HashCommitTempFile, $CommitText); | |
$CommitHash = git hash-object -t commit -w -- $HashCommitTempFile; | |
If ($LASTEXITCODE -ne 0) | |
{ | |
Write-Error -Message "Failed to call git hash-object -t commit -w -- $HashCommitTemptFile"; | |
Return; | |
} | |
$HashCommitOutput['Result'] = ($CommitHash -join '').Trim().ToLowerInvariant(); | |
} | Out-Null; | |
If ($HashCommitOutput['Result'] -ne $null) | |
{ | |
$HashCommitOutput['Result'] | |
} | |
} | |
} | |
$OldHashes = git rev-list HEAD; | |
$Commits = $OldHashes | Parse-Commit; | |
$old2new = [System.Collections.Generic.Dictionary[string, string]]::new(); | |
<# | |
1. Migrate the new root. | |
The new root is "first merge" in our example. | |
#> | |
$NewRootIndex = $OldHashes.IndexOf('2006875f944363580335ec893aa29bc1fc489695'); | |
$Commits[$NewRootIndex].Parents = @(); | |
$old2new[$OldHashes[$NewRootIndex]] = $Commits[$NewRootIndex] | Hash-Commit; | |
<# | |
This script block recursively migrates a commit. | |
Note that if a commit is an ancestor of the old version of the intended new root, | |
then it is not migrated and will be preserved in the new tree. | |
This is a fail-safe option. | |
#> | |
$MigrateCommit = { | |
$OldHash = $args[0]; | |
If (-not $old2new.ContainsKey($OldHash)) | |
{ | |
$TheIndex = $OldHashes.IndexOf($OldHash); | |
$TheCommit = $Commits[$TheIndex]; | |
If ($TheCommit.Parents.Count -ne 0) | |
{ | |
$TheCommit.Parents = $TheCommit.Parents | ForEach-Object { & $MigrateCommit $_ }; | |
} | |
$old2new[$OldHashes[$TheIndex]] = $TheCommit | Hash-Commit; | |
} | |
$old2new[$OldHash] | |
} | |
<# 2. Migrate the head commit. #> | |
$HeadCommitToMigrate = git rev-parse HEAD; | |
$HeadCommitNewHash = & $MigrateCommit $HeadCommitToMigrate; | |
git checkout -b main-new $HeadCommitNewHash; |
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
$repo = (Get-Location).Path; | |
$timestamp = 1750585471; | |
$env:GIT_AUTHOR_NAME = 'Someone'; | |
$env:GIT_AUTHOR_EMAIL = '[email protected]'; | |
$env:GIT_COMMITTER_NAME = 'Someone'; | |
$env:GIT_COMMITTER_EMAIL = '[email protected]'; | |
git checkout -b main; | |
[System.IO.File]::WriteAllText($repo + '/0.txt', "something`n"); | |
git add 0.txt; | |
$env:GIT_AUTHOR_DATE = "$timestamp -0700"; | |
$env:GIT_COMMITTER_DATE = "$timestamp -0700"; | |
$timestamp += 1; | |
git commit -m 'commit 0'; | |
$MakeExampleCommit = { | |
[System.IO.File]::WriteAllText($repo + "/$_.txt", "something`n"); | |
git add "$_.txt"; | |
$msg = "commit $_"; | |
$env:GIT_AUTHOR_DATE = "$timestamp -0700"; | |
$env:GIT_COMMITTER_DATE = "$timestamp -0700"; | |
$timestamp += 1; | |
git commit -m $msg; | |
}; | |
git checkout -b branch1 main; | |
1..4 | foreach $MakeExampleCommit; | |
git checkout -b branch2 main; | |
5..8 | foreach $MakeExampleCommit; | |
git checkout main | |
$env:GIT_AUTHOR_DATE = "$timestamp -0700"; | |
$env:GIT_COMMITTER_DATE = "$timestamp -0700"; | |
$timestamp += 1; | |
git merge -m 'first merge' --no-ff branch1 branch2 | |
git branch -d branch1 branch2 | |
git checkout -b branch1 main; | |
11..14 | foreach $MakeExampleCommit; | |
git checkout -b branch2 main; | |
15..18 | foreach $MakeExampleCommit; | |
git checkout main | |
$env:GIT_AUTHOR_DATE = "$timestamp -0700"; | |
$env:GIT_COMMITTER_DATE = "$timestamp -0700"; | |
$timestamp += 1; | |
git merge -m 'second merge' --no-ff branch1 branch2 | |
git branch -d branch1 branch2 | |
del Env:\GIT_AUTHOR_NAME, Env:\GIT_AUTHOR_EMAIL, Env:\GIT_AUTHOR_DATE, Env:\GIT_COMMITTER_NAME, Env:\GIT_COMMITTER_EMAIL, Env:\GIT_COMMITTER_DATE; | |
<# | |
The following commits will be created: | |
2f45eabb49b6a1126ce1be3bd8718cdb8004b01a | |
abc3b36fe23dfeb35c9325086672d3b2b094138e | |
2c76649332eeecc98144750962e971c2e4f2538d | |
9f0e26dc41c839d4075c4166f7204f21a5e18655 | |
321626a1fc1f3c21561f57c42a04071afc56ade6 | |
0568997c0e7b4b70518e87eeac086c23f7a5cd26 | |
933146ef03ba051768b371cdc784495310602355 | |
a24433d050fd71ba11489bb054d72e037d938793 | |
4c4124c4af6f1d127a61588f66ccb3de58d2576e | |
2006875f944363580335ec893aa29bc1fc489695 | |
b110d1092d7107e0600e7ae8fcc11b0132e636c6 | |
8e7193fe8c8a4be908201fe2e5f3c8882b4901ce | |
e4b39c2850783d3a2889f350e2febac0b8071cfb | |
de212539bce10483abac532313bf5090048586f8 | |
94771a888014b7dbbe30de979cfcc56091823a18 | |
948a4a6e75ab69115fa3871d0c88df9110e7921c | |
ed6aee4b22d4787408f21c01113731e360dec835 | |
63bb3ea1b35a255576eea1056a0cdb5994b9a3d0 | |
5f11688cc6726baea030c44b949a21668efdc512 | |
#> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment