Skip to content

Instantly share code, notes, and snippets.

@GeeLaw
Last active June 22, 2025 11:43
Show Gist options
  • Save GeeLaw/7f40471442544a3aff166298cd462de0 to your computer and use it in GitHub Desktop.
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
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;
$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