├── .dockerignore ├── .gitignore ├── .gitmodules ├── Dockerfile ├── GitRewrite.Tests ├── CommitTests.cs ├── GitRewrite.Tests.csproj └── TagTests.cs ├── GitRewrite.sln ├── GitRewrite ├── CleanupTask │ ├── CleanupTaskBase.cs │ ├── Delete │ │ ├── DeletionTask.cs │ │ ├── FileDeletionStrategies.cs │ │ ├── FileEndsWithDeletionStrategy.cs │ │ ├── FileExactDeletionStrategy.cs │ │ ├── FileSimpleDeleteStrategy.cs │ │ ├── FileStartsWithDeletionStrategy.cs │ │ ├── FolderDeletionStrategies.cs │ │ ├── FolderEndsWithDeletionStrategy.cs │ │ ├── FolderExactDeletionStrategy.cs │ │ ├── FolderSimpleDeleteStrategy.cs │ │ ├── FolderStartsWithDeletionStrategy.cs │ │ ├── IFileDeletionStrategy.cs │ │ └── IFolderDeletionStrategy.cs │ ├── RemoveEmptyCommitsTask.cs │ └── RewriteContributorTask.cs ├── CommandLineOptions.cs ├── CommitWalker.cs ├── Diff │ ├── DiffInstruction.cs │ └── PackDiff.cs ├── GitObjectFactory.cs ├── GitObjectType.cs ├── GitObjects │ ├── Blob.cs │ ├── ByteArrayExtensions.cs │ ├── Commit.cs │ ├── GitObjectBase.cs │ ├── ObjectHash.cs │ ├── ObjectPrefixes.cs │ ├── Tag.cs │ ├── Tree.cs │ └── TreeLineByHashComparer.cs ├── GitRewrite.csproj ├── Hash.cs ├── IO │ ├── Adler32Computer.cs │ ├── HashContent.cs │ ├── IdxOffsetReader.cs │ ├── PackObject.cs │ └── PackReader.cs ├── Program.cs ├── Ref.cs └── Refs.cs ├── README.md └── license.md /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.vs 6 | **/.vscode 7 | **/*.*proj.user 8 | **/azds.yaml 9 | **/charts 10 | **/bin 11 | **/obj 12 | **/Dockerfile 13 | **/Dockerfile.develop 14 | **/docker-compose.yml 15 | **/docker-compose.*.yml 16 | **/*.dbmdl 17 | **/*.jfm 18 | **/secrets.dev.yaml 19 | **/values.dev.yaml 20 | **/.toolstarget -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | launchSettings.json 352 | published/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiavision/GitRewrite/3ce4520dd5766079960f5249cdc7690bd05bda84/.gitmodules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye AS publish 2 | WORKDIR /src 3 | RUN apt update && apt install -y wget 4 | RUN wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb 5 | RUN apt update && apt install -y dotnet-sdk-7.0 clang zlib1g-dev 6 | COPY ["GitRewrite/GitRewrite.csproj", "GitRewrite/"] 7 | RUN dotnet restore "GitRewrite/GitRewrite.csproj" 8 | COPY . . 9 | WORKDIR "/src/GitRewrite" 10 | RUN dotnet publish "GitRewrite.csproj" -c Release -o /app/publish 11 | 12 | FROM debian:bullseye-slim AS final 13 | WORKDIR /app 14 | COPY --from=publish /app/publish/GitRewrite . 15 | ENTRYPOINT ["/app/GitRewrite"] 16 | -------------------------------------------------------------------------------- /GitRewrite.Tests/CommitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text; 3 | using GitRewrite.GitObjects; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace GitRewrite.Tests 7 | { 8 | [TestClass] 9 | public class CommitTests 10 | { 11 | private const string CommitWithPgpSig = 12 | "tree 413f6b2e45859c6a962d2f2e8437598c63d92e3a\n" + 13 | "parent d5191b5c947f79bb35960075621c273dc0bd0109\n" + 14 | "author Test User <2929650+test@users.noreply.github.com> 1561663031 +0200\n" + 15 | "committer GitHub 1561663031 +0200\n" + 16 | "gpgsig -----BEGIN PGP SIGNATURE-----\n\n wsBcBAABCAAQBQJdFRY3CRBK7hj4Ov3rIwAAdHIIAFZzg7W6IVM3wyRvDXklc+yB\n kPFEkE89/G45rzvt+uY0T65itwjLlLVU9bvvSZsbzir9Fr3RlE3RaynDuebVyoBF\n m2ZYggN95R/MQvSvMnE64J5kguziGCnb+vWKFyfb51Iz4sw8JaX6hLQkmttPWAFQ\n 9cRaxyCJALGdKuIYS1POKa0LctU2lBHlUqO/Lqh5344gErenYHJFbBbd3M9OmFYL\n dxld9mPnC6K9NmdZHsBXDgQBqoJ7btKpqP2NUPvZEvYdLI0XkGS/OgN862QZFQoY\n dHqkLrNGhjFd1GPHJoVyAke0Q8z8V4P34uMCZpuAoqSkn2ODhhrNqfqGrjwUBaE=\n =MC9b\n -----END PGP SIGNATURE-----\n" + 17 | "\n\nUpdated build instructions"; 18 | 19 | private const string CommitWithPgpSigHash = "699834ab5a722a0dea84743d7bf92e4e6082531f"; 20 | 21 | private const string CommitWithoutPgpSig = 22 | "tree f7bbd02edd480914d28633f4404e2600d93af690\n" + 23 | "parent 6f6fa334d886eb104e46452d7e2aae5b9fbcc102\n" + 24 | "author Test User 1561201571 +0200\ncommitter Test User 1561201571 +0200\n\nGitignore"; 25 | 26 | private const string CommitHash = "57516518659c81012449f16bacda5b36ddc25433"; 27 | 28 | [TestMethod] 29 | public void CommitFromByte() 30 | { 31 | var commitBytes = Encoding.UTF8.GetBytes(CommitWithoutPgpSig); 32 | var commit = new Commit(new ObjectHash(CommitHash), commitBytes); 33 | 34 | Assert.AreEqual("f7bbd02edd480914d28633f4404e2600d93af690", commit.TreeHash.ToString()); 35 | Assert.AreEqual("6f6fa334d886eb104e46452d7e2aae5b9fbcc102", commit.Parents.Single().ToString()); 36 | Assert.AreEqual("Test User ", commit.GetAuthorName()); 37 | Assert.AreEqual("\nGitignore", commit.CommitMessage); 38 | 39 | Assert.AreEqual(commitBytes, commit.SerializeToBytes()); 40 | 41 | var sameCommitBytes = 42 | Commit.GetSerializedCommitWithChangedTreeAndParents(commit, commit.TreeHash, commit.Parents); 43 | CollectionAssert.AreEqual(commitBytes, sameCommitBytes); 44 | 45 | var newTreeHash = "1234567890123456789012345678901234567890"; 46 | var newParent1 = "3216549870321654987032165498703216549870"; 47 | var newParent2 = "9999999999999999999999999999999999999999"; 48 | var newCommitBytes = Commit.GetSerializedCommitWithChangedTreeAndParents(commit, 49 | new ObjectHash(newTreeHash), 50 | new[] 51 | { 52 | new ObjectHash(newParent1), 53 | new ObjectHash(newParent2) 54 | }.ToList()); 55 | 56 | var newCommit = new Commit(new ObjectHash("1234567890123456789012345678901234567890"), newCommitBytes); 57 | 58 | Assert.AreEqual(newTreeHash, newCommit.TreeHash.ToString()); 59 | Assert.AreEqual(2, newCommit.Parents.Count); 60 | Assert.AreEqual(newParent1, newCommit.Parents.First().ToString()); 61 | Assert.AreEqual(newParent2, newCommit.Parents.Last().ToString()); 62 | Assert.AreEqual("Test User ", newCommit.GetAuthorName()); 63 | Assert.AreEqual("\nGitignore", newCommit.CommitMessage); 64 | } 65 | 66 | [TestMethod] 67 | public void CommitFromByteWithPgpSig() 68 | { 69 | var commitBytes = Encoding.UTF8.GetBytes(CommitWithPgpSig); 70 | var commit = new Commit(new ObjectHash(CommitWithPgpSigHash), commitBytes); 71 | 72 | Assert.AreEqual("413f6b2e45859c6a962d2f2e8437598c63d92e3a", commit.TreeHash.ToString()); 73 | Assert.AreEqual("d5191b5c947f79bb35960075621c273dc0bd0109", commit.Parents.Single().ToString()); 74 | Assert.AreEqual("Test User <2929650+test@users.noreply.github.com>", commit.GetAuthorName()); 75 | Assert.AreEqual("\nUpdated build instructions", commit.CommitMessage); 76 | 77 | Assert.AreEqual(commitBytes, commit.SerializeToBytes()); 78 | 79 | var newTreeHash = "1234567890123456789012345678901234567890"; 80 | var newParent1 = "3216549870321654987032165498703216549870"; 81 | var newParent2 = "9999999999999999999999999999999999999999"; 82 | var newCommitBytes = Commit.GetSerializedCommitWithChangedTreeAndParents(commit, 83 | new ObjectHash(newTreeHash), 84 | new[] 85 | { 86 | new ObjectHash(newParent1), 87 | new ObjectHash(newParent2) 88 | }.ToList()); 89 | 90 | var newCommit = new Commit(new ObjectHash("1234567890123456789012345678901234567890"), newCommitBytes); 91 | 92 | Assert.AreEqual(newTreeHash, newCommit.TreeHash.ToString()); 93 | Assert.AreEqual(2, newCommit.Parents.Count); 94 | Assert.AreEqual(newParent1, newCommit.Parents.First().ToString()); 95 | Assert.AreEqual(newParent2, newCommit.Parents.Last().ToString()); 96 | Assert.AreEqual("Test User <2929650+test@users.noreply.github.com>", newCommit.GetAuthorName()); 97 | Assert.AreEqual("\nUpdated build instructions", newCommit.CommitMessage); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /GitRewrite.Tests/GitRewrite.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | 6 | false 7 | 8 | 1.3.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /GitRewrite.Tests/TagTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using GitRewrite.GitObjects; 5 | using Microsoft.VisualBasic; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | namespace GitRewrite.Tests 9 | { 10 | [TestClass] 11 | public class TagTests 12 | { 13 | private const string TagWithComment = "object 21b38c151f130550c42dbb6467855f20f36ca146\n" + 14 | "type commit\n" + 15 | "tag v1.2\n" + 16 | "tagger Test User 1562258217 +0200\n\n" + 17 | "My Test Tag\n"; 18 | 19 | private const string TagWithoutTagger = 20 | "object 1b2bf77246f78a91ffa90456e6d1c393db1d5eb0\ntype commit\ntag 1.1.95.0\n\nTagged Release xy"; 21 | 22 | [TestMethod] 23 | public void TagWithoutTaggerTest() 24 | { 25 | var bytes = Encoding.UTF8.GetBytes(TagWithoutTagger); 26 | var objectHash = new ObjectHash("1234567890123456789012345678901234567890"); 27 | var tag = new Tag(objectHash, bytes); 28 | 29 | Assert.AreEqual("1b2bf77246f78a91ffa90456e6d1c393db1d5eb0", tag.Object); 30 | Assert.AreEqual("commit", tag.TypeName); 31 | Assert.AreEqual("1.1.95.0", tag.TagName); 32 | Assert.AreEqual("\nTagged Release xy", tag.Message); 33 | Assert.AreEqual(string.Empty, tag.Tagger); 34 | } 35 | 36 | [TestMethod] 37 | public void TagWithCommentTest() 38 | { 39 | var bytes = Encoding.UTF8.GetBytes(TagWithComment); 40 | var objectHash = new ObjectHash("1234567890123456789012345678901234567890"); 41 | var tag = new Tag(objectHash, bytes); 42 | 43 | Assert.AreEqual("v1.2", tag.TagName); 44 | Assert.AreEqual("\nMy Test Tag\n", tag.Message); 45 | Assert.IsFalse(tag.PointsToTag); 46 | Assert.IsFalse(tag.PointsToTree); 47 | Assert.AreEqual("Test User 1562258217 +0200", tag.Tagger); 48 | Assert.AreEqual("commit", tag.TypeName); 49 | Assert.AreEqual("21b38c151f130550c42dbb6467855f20f36ca146", tag.Object); 50 | 51 | tag = new Tag( objectHash, tag.SerializeToBytes()); 52 | Assert.AreEqual("v1.2", tag.TagName); 53 | Assert.AreEqual("\nMy Test Tag\n", tag.Message); 54 | Assert.IsFalse(tag.PointsToTag); 55 | Assert.IsFalse(tag.PointsToTree); 56 | Assert.AreEqual("Test User 1562258217 +0200", tag.Tagger); 57 | Assert.AreEqual("commit", tag.TypeName); 58 | Assert.AreEqual("21b38c151f130550c42dbb6467855f20f36ca146", tag.Object); 59 | 60 | const string newObject = "3216549870321654987032165498703216549870"; 61 | tag = tag.WithNewObject(newObject); 62 | Assert.AreEqual("v1.2", tag.TagName); 63 | Assert.AreEqual("\nMy Test Tag\n", tag.Message); 64 | Assert.IsFalse(tag.PointsToTag); 65 | Assert.IsFalse(tag.PointsToTree); 66 | Assert.AreEqual("Test User 1562258217 +0200", tag.Tagger); 67 | Assert.AreEqual("commit", tag.TypeName); 68 | Assert.AreEqual(newObject, tag.Object); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /GitRewrite.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29001.49 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitRewrite", "GitRewrite\GitRewrite.csproj", "{1E422119-7262-49A4-9288-D0B41B1F8828}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitRewrite.Tests", "GitRewrite.Tests\GitRewrite.Tests.csproj", "{64C2FF8F-0733-480E-9B73-D6A1798A179A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {1E422119-7262-49A4-9288-D0B41B1F8828}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {1E422119-7262-49A4-9288-D0B41B1F8828}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {1E422119-7262-49A4-9288-D0B41B1F8828}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {1E422119-7262-49A4-9288-D0B41B1F8828}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {64C2FF8F-0733-480E-9B73-D6A1798A179A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {64C2FF8F-0733-480E-9B73-D6A1798A179A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {64C2FF8F-0733-480E-9B73-D6A1798A179A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {64C2FF8F-0733-480E-9B73-D6A1798A179A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {214214AF-794A-4789-BF25-0BE55526454A} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/CleanupTaskBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using GitRewrite.GitObjects; 7 | using GitRewrite.IO; 8 | 9 | namespace GitRewrite.CleanupTask 10 | { 11 | public abstract class CleanupTaskBase : IDisposable 12 | { 13 | private readonly BlockingCollection _objectsToWrite = new BlockingCollection(); 14 | protected readonly string RepositoryPath; 15 | 16 | private readonly Dictionary 17 | _rewrittenCommits = new Dictionary(); 18 | 19 | protected CleanupTaskBase(string repositoryPath) => RepositoryPath = repositoryPath; 20 | 21 | public void Dispose() 22 | { 23 | _objectsToWrite.Dispose(); 24 | } 25 | 26 | protected void EnqueueCommitWrite(ObjectHash oldHash, ObjectHash newHash, byte[] bytes) 27 | { 28 | if (!oldHash.Equals(newHash)) 29 | { 30 | _objectsToWrite.Add(new BytesToWrite(newHash, bytes)); 31 | RegisterCommitChange(oldHash, newHash); 32 | } 33 | } 34 | 35 | protected void RegisterCommitChange(ObjectHash oldHash, ObjectHash newHash) 36 | { 37 | _rewrittenCommits.Add(oldHash, newHash); 38 | } 39 | 40 | protected void EnqueueTreeWrite(ObjectHash newHash, byte[] bytes) 41 | { 42 | _objectsToWrite.Add(new BytesToWrite(newHash, bytes)); 43 | } 44 | 45 | public void Run() 46 | { 47 | var writeStep = new Thread(o => 48 | { 49 | var threadParams = (ThreadParams) o; 50 | var bytesToWrite = threadParams.BytesToWriteCollection; 51 | 52 | foreach (var commit in bytesToWrite.GetConsumingEnumerable()) 53 | HashContent.WriteFile(threadParams.VcsPath, commit.Bytes, commit.Hash.ToString()); 54 | }); 55 | 56 | writeStep.Start(new ThreadParams(RepositoryPath, _objectsToWrite)); 57 | 58 | Console.WriteLine("Reading commits..."); 59 | 60 | long commitNumber = 1; 61 | foreach (var parallelActionResult in CommitWalker.CommitsInOrder(RepositoryPath) 62 | .AsParallel() 63 | .AsOrdered() 64 | .Select(ParallelStep)) 65 | { 66 | Console.Write("\rProcessing commit " + commitNumber++); 67 | SynchronousStep(parallelActionResult); 68 | } 69 | 70 | _objectsToWrite.CompleteAdding(); 71 | 72 | Console.WriteLine(); 73 | Console.WriteLine("Writing objects..."); 74 | 75 | writeStep.Join(); 76 | 77 | Console.WriteLine("Updating refs..."); 78 | if (_rewrittenCommits.Any()) 79 | Refs.Update(RepositoryPath, _rewrittenCommits); 80 | } 81 | 82 | protected IEnumerable GetRewrittenCommitHashes(IEnumerable hashes) 83 | { 84 | foreach (var hash in hashes) 85 | { 86 | var rewrittenHash = hash; 87 | 88 | while (_rewrittenCommits.TryGetValue(rewrittenHash, out var commitHash)) 89 | rewrittenHash = commitHash; 90 | 91 | yield return rewrittenHash; 92 | } 93 | 94 | } 95 | 96 | protected ObjectHash GetRewrittenCommitHash(ObjectHash objectHash) 97 | { 98 | while (_rewrittenCommits.TryGetValue(objectHash, out var rewrittenObjectHash)) 99 | objectHash = rewrittenObjectHash; 100 | 101 | return objectHash; 102 | } 103 | 104 | protected abstract TParallelActionResult ParallelStep(Commit commit); 105 | 106 | protected abstract void SynchronousStep(TParallelActionResult commit); 107 | 108 | private sealed class BytesToWrite 109 | { 110 | public readonly byte[] Bytes; 111 | 112 | public readonly ObjectHash Hash; 113 | 114 | public BytesToWrite(ObjectHash objectHash, byte[] bytes) 115 | { 116 | Hash = objectHash; 117 | Bytes = bytes; 118 | } 119 | } 120 | 121 | private sealed class ThreadParams 122 | { 123 | public readonly BlockingCollection BytesToWriteCollection; 124 | 125 | public readonly string VcsPath; 126 | 127 | public ThreadParams(string vcsPath, BlockingCollection bytesToWriteCollection) 128 | { 129 | VcsPath = vcsPath; 130 | BytesToWriteCollection = bytesToWriteCollection; 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/DeletionTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using GitRewrite.GitObjects; 7 | 8 | namespace GitRewrite.CleanupTask.Delete 9 | { 10 | public class DeletionTask : CleanupTaskBase<(Commit Commit, ObjectHash NewTreeHash)> 11 | { 12 | private static readonly TreeLineByHashComparer TreeLineByHashComparer = new TreeLineByHashComparer(); 13 | private static readonly ArrayPool FilePathPool = ArrayPool.Shared; 14 | private readonly FileDeletionStrategies _fileDeleteStrategies; 15 | private readonly FolderDeletionStrategies _folderDeleteStrategies; 16 | private readonly List _relevantPathes; 17 | 18 | private readonly ConcurrentDictionary _rewrittenTrees = 19 | new ConcurrentDictionary(); 20 | 21 | private HashSet _commitsToSkip; 22 | 23 | public DeletionTask(string repositoryPath, IEnumerable filesToDelete, 24 | IEnumerable foldersToDelete, bool protectRefs) 25 | : base(repositoryPath) 26 | { 27 | _fileDeleteStrategies = new FileDeletionStrategies(filesToDelete); 28 | _folderDeleteStrategies = new FolderDeletionStrategies(foldersToDelete); 29 | 30 | _commitsToSkip = new HashSet(protectRefs 31 | ? Refs.ReadAll(repositoryPath) 32 | .Select(r => new ObjectHash(r is TagRef tagRef ? tagRef.CommitHash : r.Hash)) 33 | : new HashSet()); 34 | 35 | _relevantPathes = 36 | _fileDeleteStrategies.RelevantPaths.Union(_folderDeleteStrategies.RelevantPaths).ToList(); 37 | } 38 | 39 | protected override (Commit Commit, ObjectHash NewTreeHash) ParallelStep(Commit commit) 40 | => (commit, _commitsToSkip.Contains(commit.Hash) 41 | ? commit.TreeHash 42 | : RemoveObjectFromTree(RepositoryPath, commit.TreeHash, _fileDeleteStrategies, 43 | _folderDeleteStrategies, 44 | _rewrittenTrees, new byte[0], _relevantPathes)); 45 | 46 | protected override void SynchronousStep((Commit Commit, ObjectHash NewTreeHash) removalResult) 47 | { 48 | var rewrittenParentHashes = GetRewrittenCommitHashes(removalResult.Commit.Parents).ToList(); 49 | 50 | if (removalResult.NewTreeHash != removalResult.Commit.TreeHash || !rewrittenParentHashes.SequenceEqual(removalResult.Commit.Parents)) 51 | { 52 | var newCommit = Commit.GetSerializedCommitWithChangedTreeAndParents(removalResult.Commit, 53 | removalResult.NewTreeHash, 54 | rewrittenParentHashes); 55 | 56 | var newCommitBytes = GitObjectFactory.GetBytesWithHeader(GitObjectType.Commit, newCommit); 57 | var newCommitHash = new ObjectHash(Hash.Create(newCommitBytes)); 58 | 59 | EnqueueCommitWrite(removalResult.Commit.Hash, newCommitHash, newCommitBytes); 60 | } 61 | } 62 | 63 | private ObjectHash RemoveObjectFromTree( 64 | string vcsPath, 65 | ObjectHash treeHash, 66 | FileDeletionStrategies filesToRemove, 67 | FolderDeletionStrategies foldersToRemove, 68 | ConcurrentDictionary rewrittenTrees, 69 | in ReadOnlySpan currentPath, 70 | List relevantPathes) 71 | { 72 | if (rewrittenTrees.TryGetValue(treeHash, out var rewrittenHash)) 73 | return rewrittenHash; 74 | 75 | if (!IsPathRelevant(currentPath, relevantPathes)) 76 | return treeHash; 77 | 78 | var tree = GitObjectFactory.ReadTree(vcsPath, treeHash); 79 | var resultingLines = new List(); 80 | foreach (var line in tree.Lines) 81 | if (line.IsDirectory()) 82 | { 83 | if (rewrittenTrees.TryGetValue(line.Hash, out var newHash)) 84 | { 85 | resultingLines.Add(new Tree.TreeLine(line.TextBytes, newHash)); 86 | } 87 | else 88 | { 89 | var pathLength = currentPath.Length + line.FileNameBytes.Length + 1; 90 | var rentedPathBytes = FilePathPool.Rent(pathLength); 91 | var path = rentedPathBytes.AsSpan(0, pathLength); 92 | currentPath.CopyTo(path); 93 | path[currentPath.Length] = (byte) '/'; 94 | line.FileNameBytes.Span.CopyTo(path.Slice(currentPath.Length + 1)); 95 | 96 | if (!foldersToRemove.DeleteObject(path)) 97 | { 98 | var newTreeHash = RemoveObjectFromTree( 99 | vcsPath, 100 | line.Hash, 101 | filesToRemove, 102 | foldersToRemove, 103 | rewrittenTrees, 104 | path, 105 | relevantPathes); 106 | 107 | FilePathPool.Return(rentedPathBytes); 108 | 109 | resultingLines.Add(new Tree.TreeLine(line.TextBytes, newTreeHash)); 110 | } 111 | } 112 | } 113 | else 114 | { 115 | if (!filesToRemove.DeleteObject(line.FileNameBytes.Span, currentPath)) 116 | resultingLines.Add(line); 117 | } 118 | 119 | if (resultingLines.Count == tree.Lines.Count && 120 | resultingLines.SequenceEqual(tree.Lines, TreeLineByHashComparer)) 121 | { 122 | rewrittenTrees.TryAdd(tree.Hash, tree.Hash); 123 | return tree.Hash; 124 | } 125 | 126 | var fixedTree = Tree.GetFixedTree(resultingLines); 127 | if (fixedTree.Hash != tree.Hash) 128 | { 129 | var bytes = GitObjectFactory.GetBytesWithHeader(GitObjectType.Tree, fixedTree.SerializeToBytes()); 130 | EnqueueTreeWrite(fixedTree.Hash, bytes); 131 | } 132 | 133 | rewrittenTrees.TryAdd(treeHash, fixedTree.Hash); 134 | 135 | return fixedTree.Hash; 136 | } 137 | 138 | private static bool IsPathRelevant(in ReadOnlySpan currentPath, List relevantPathes) 139 | { 140 | if (currentPath.Length == 0 || !relevantPathes.Any()) 141 | return true; 142 | 143 | for (var i = relevantPathes.Count - 1; i >= 0; i--) 144 | { 145 | var path = relevantPathes[i]; 146 | 147 | if (currentPath.Length > path.Length) 148 | continue; 149 | 150 | var isRelevant = true; 151 | for (var j = currentPath.Length - 1; j >= 0; j--) 152 | if (currentPath[j] != path[j]) 153 | { 154 | isRelevant = false; 155 | break; 156 | } 157 | 158 | if (isRelevant) 159 | return true; 160 | } 161 | 162 | return false; 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FileDeletionStrategies.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace GitRewrite.CleanupTask.Delete 6 | { 7 | public class FileDeletionStrategies 8 | { 9 | private readonly List _strategies; 10 | 11 | public readonly List RelevantPaths = new List(); 12 | 13 | public FileDeletionStrategies(IEnumerable filePatterns) 14 | { 15 | _strategies = new List(); 16 | foreach (var objectPattern in filePatterns) 17 | if (objectPattern[0] == '*') 18 | { 19 | _strategies.Add(new FileEndsWithDeletionStrategy(objectPattern)); 20 | } 21 | else if (objectPattern[0] == '/') 22 | { 23 | _strategies.Add(new FileExactDeletionStrategy(objectPattern)); 24 | 25 | var indexToCut = objectPattern.LastIndexOf('/'); 26 | var pathString = objectPattern.Substring(0, indexToCut); 27 | var bytes = Encoding.UTF8.GetBytes(pathString); 28 | RelevantPaths.Add(bytes); 29 | } 30 | else if (objectPattern[objectPattern.Length - 1] == '*') 31 | { 32 | _strategies.Add(new FileStartsWithDeletionStrategy(objectPattern)); 33 | } 34 | else 35 | { 36 | _strategies.Add(new FileSimpleDeleteStrategy(objectPattern)); 37 | } 38 | } 39 | 40 | public bool DeleteObject(in ReadOnlySpan fileName, ReadOnlySpan currentPath) 41 | { 42 | foreach (var strategy in _strategies) 43 | if (strategy.DeleteObject(fileName, currentPath)) 44 | return true; 45 | 46 | return false; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FileEndsWithDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | internal class FileEndsWithDeletionStrategy : IFileDeletionStrategy 7 | { 8 | private readonly byte[] _endBytes; 9 | 10 | public FileEndsWithDeletionStrategy(string filePattern) => 11 | _endBytes = Encoding.UTF8.GetBytes(filePattern.Substring(1)); 12 | 13 | public bool DeleteObject(in ReadOnlySpan fileName, ReadOnlySpan currentPath) => 14 | fileName.EndsWith(_endBytes); 15 | } 16 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FileExactDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | class FileExactDeletionStrategy : IFileDeletionStrategy 7 | { 8 | private readonly ReadOnlyMemory _fileName; 9 | 10 | public FileExactDeletionStrategy(string fileName) => _fileName = Encoding.UTF8.GetBytes(fileName); 11 | 12 | public bool DeleteObject(in ReadOnlySpan fileName, ReadOnlySpan currentPath) 13 | { 14 | var len = fileName.Length + currentPath.Length + 1; 15 | if (len != _fileName.Length) 16 | return false; 17 | 18 | var span = _fileName.Span; 19 | return span[currentPath.Length] == (byte) '/' && span.StartsWith(currentPath) && span.EndsWith(fileName); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FileSimpleDeleteStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using GitRewrite.GitObjects; 4 | 5 | namespace GitRewrite.CleanupTask.Delete 6 | { 7 | public class FileSimpleDeleteStrategy : IFileDeletionStrategy 8 | { 9 | private readonly byte[] _fileName; 10 | 11 | public FileSimpleDeleteStrategy(string fileName) => _fileName = Encoding.UTF8.GetBytes(fileName); 12 | 13 | public bool DeleteObject(in ReadOnlySpan fileName, ReadOnlySpan currentPath) => 14 | fileName.SpanEquals(_fileName); 15 | } 16 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FileStartsWithDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | internal class FileStartsWithDeletionStrategy : IFileDeletionStrategy 7 | { 8 | private readonly byte[] _startBytes; 9 | 10 | public FileStartsWithDeletionStrategy(string filePattern) => 11 | _startBytes = Encoding.UTF8.GetBytes(filePattern.Substring(0, filePattern.Length - 1)); 12 | 13 | public bool DeleteObject(in ReadOnlySpan fileName, ReadOnlySpan currentPath) => fileName.StartsWith(_startBytes); 14 | } 15 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FolderDeletionStrategies.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace GitRewrite.CleanupTask.Delete 6 | { 7 | public class FolderDeletionStrategies 8 | { 9 | private readonly List _strategies; 10 | 11 | public readonly List RelevantPaths = new List(); 12 | 13 | public FolderDeletionStrategies(IEnumerable patterns) 14 | { 15 | _strategies = new List(); 16 | foreach (var objectPattern in patterns) 17 | { 18 | if (objectPattern[0] == '*') 19 | _strategies.Add(new FolderEndsWithDeletionStrategy(objectPattern)); 20 | else if (objectPattern[0] == '/') 21 | { 22 | var bytes = Encoding.UTF8.GetBytes(objectPattern); 23 | _strategies.Add(new FolderExactDeletionStrategy(bytes)); 24 | RelevantPaths.Add(bytes); 25 | } 26 | else if (objectPattern[objectPattern.Length - 1] == '*') 27 | _strategies.Add(new FolderStartsWithDeletionStrategy(objectPattern)); 28 | else 29 | _strategies.Add(new FolderSimpleDeleteStrategy(objectPattern)); 30 | } 31 | } 32 | 33 | public bool DeleteObject(ReadOnlySpan currentPath) 34 | { 35 | foreach (var strategy in _strategies) 36 | { 37 | if (strategy.DeleteObject(currentPath)) 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FolderEndsWithDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | internal class FolderEndsWithDeletionStrategy : IFolderDeletionStrategy 7 | { 8 | private readonly byte[] _endBytes; 9 | 10 | public FolderEndsWithDeletionStrategy(string filePattern) => 11 | _endBytes = Encoding.UTF8.GetBytes(filePattern.Substring(1)); 12 | 13 | public bool DeleteObject(in ReadOnlySpan currentPath) => 14 | currentPath.EndsWith(_endBytes); 15 | } 16 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FolderExactDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GitRewrite.GitObjects; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | class FolderExactDeletionStrategy : IFolderDeletionStrategy 7 | { 8 | private readonly Memory _folderName; 9 | 10 | public FolderExactDeletionStrategy(byte[] fileName) => _folderName = fileName; 11 | 12 | public bool DeleteObject(in ReadOnlySpan currentPath) => _folderName.Span.SpanEquals(currentPath); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FolderSimpleDeleteStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | public class FolderSimpleDeleteStrategy : IFolderDeletionStrategy 7 | { 8 | private readonly byte[] _fileName; 9 | 10 | public FolderSimpleDeleteStrategy(string fileName) => _fileName = Encoding.UTF8.GetBytes(fileName); 11 | 12 | public bool DeleteObject(in ReadOnlySpan currentPath) => 13 | currentPath.Length > _fileName.Length && 14 | currentPath[currentPath.Length - _fileName.Length - 1] == (byte) '/' && currentPath.EndsWith(_fileName); 15 | } 16 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/FolderStartsWithDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.CleanupTask.Delete 5 | { 6 | internal class FolderStartsWithDeletionStrategy : IFolderDeletionStrategy 7 | { 8 | private readonly byte[] _startBytes; 9 | 10 | public FolderStartsWithDeletionStrategy(string filePattern) => 11 | _startBytes = Encoding.UTF8.GetBytes(filePattern.Substring(0, filePattern.Length - 1)); 12 | 13 | public bool DeleteObject(in ReadOnlySpan currentPath) => currentPath.StartsWith(_startBytes); 14 | } 15 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/IFileDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitRewrite.CleanupTask.Delete 4 | { 5 | public interface IFileDeletionStrategy 6 | { 7 | bool DeleteObject(in ReadOnlySpan fileName, ReadOnlySpan currentPath); 8 | } 9 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/Delete/IFolderDeletionStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitRewrite.CleanupTask.Delete 4 | { 5 | public interface IFolderDeletionStrategy 6 | { 7 | bool DeleteObject(in ReadOnlySpan currentPath); 8 | } 9 | } -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/RemoveEmptyCommitsTask.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using GitRewrite.GitObjects; 4 | 5 | namespace GitRewrite.CleanupTask 6 | { 7 | class RemoveEmptyCommitsTask : CleanupTaskBase 8 | { 9 | public RemoveEmptyCommitsTask(string repositoryPath) : base(repositoryPath) 10 | { 11 | } 12 | 13 | protected override Commit ParallelStep(Commit commit) => commit; 14 | 15 | private readonly Dictionary _commitsWithTreeHashes = new Dictionary(); 16 | 17 | protected override void SynchronousStep(Commit commit) 18 | { 19 | if (commit.Parents.Count == 1) 20 | { 21 | var parentHash = GetRewrittenCommitHash(commit.Parents.Single()); 22 | var parentTreeHash = _commitsWithTreeHashes[parentHash]; 23 | if (parentTreeHash == commit.TreeHash) 24 | { 25 | // This commit will be removed 26 | RegisterCommitChange(commit.Hash, parentHash); 27 | return; 28 | } 29 | } 30 | 31 | // rewrite this commit 32 | var correctParents = GetRewrittenCommitHashes(commit.Parents).ToList(); 33 | byte[] newCommitBytes; 34 | if (correctParents.SequenceEqual(commit.Parents)) 35 | newCommitBytes = commit.SerializeToBytes(); 36 | else 37 | newCommitBytes = Commit.GetSerializedCommitWithChangedTreeAndParents(commit, commit.TreeHash, 38 | correctParents); 39 | 40 | var resultBytes = GitObjectFactory.GetBytesWithHeader(GitObjectType.Commit, newCommitBytes); 41 | 42 | var newCommitHash = new ObjectHash(Hash.Create(resultBytes)); 43 | var newCommit = new Commit(newCommitHash, newCommitBytes); 44 | 45 | _commitsWithTreeHashes.TryAdd(newCommitHash, newCommit.TreeHash); 46 | 47 | EnqueueCommitWrite(commit.Hash, newCommitHash, resultBytes); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /GitRewrite/CleanupTask/RewriteContributorTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using GitRewrite.GitObjects; 6 | 7 | namespace GitRewrite.CleanupTask 8 | { 9 | class RewriteContributorTask : CleanupTaskBase 10 | { 11 | private readonly Dictionary _contributorMappings = new Dictionary(); 12 | 13 | public RewriteContributorTask(string repositoryPath, string contributorMappingFile) : base(repositoryPath) 14 | { 15 | var contributorMappingLines = File.ReadAllLines(contributorMappingFile); 16 | 17 | foreach (var line in contributorMappingLines.Where(x => !string.IsNullOrWhiteSpace(x))) 18 | { 19 | var contributorMapping = line.Split('=').Select(x => x.Trim()).ToList(); 20 | if (contributorMapping.Count != 2) 21 | throw new ArgumentException("Mapping is not formatted properly."); 22 | 23 | _contributorMappings.Add(contributorMapping[0], contributorMapping[1]); 24 | } 25 | } 26 | 27 | protected override Commit ParallelStep(Commit commit) => commit; 28 | 29 | protected override void SynchronousStep(Commit commit) 30 | { 31 | var rewrittenParentHashes = GetRewrittenCommitHashes(commit.Parents); 32 | var changedCommit = commit.WithChangedContributor(_contributorMappings, rewrittenParentHashes); 33 | 34 | var resultBytes = GitObjectFactory.GetBytesWithHeader(GitObjectType.Commit, changedCommit); 35 | var newCommitHash = new ObjectHash(Hash.Create(resultBytes)); 36 | 37 | EnqueueCommitWrite(commit.Hash, newCommitHash, resultBytes); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GitRewrite/CommandLineOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace GitRewrite 6 | { 7 | internal class CommandLineOptions 8 | { 9 | private CommandLineOptions() 10 | { 11 | } 12 | 13 | public bool ListContributorNames { get; private set; } 14 | 15 | public bool ShowHelp { get; private set; } 16 | 17 | public string RepositoryPath { get; private set; } 18 | 19 | public List FilesToDelete { get; } = new List(); 20 | 21 | public List FoldersToDelete { get; } = new List(); 22 | 23 | public bool FixTrees { get; private set; } 24 | 25 | public bool RemoveEmptyCommits { get; private set; } 26 | 27 | public string ContributorMappingFile { get; private set; } 28 | 29 | public bool ProtectRefs { get; private set; } 30 | 31 | internal static bool TryParse(string[] args, out CommandLineOptions options) 32 | { 33 | options = new CommandLineOptions(); 34 | var deleteFilesStarted = false; 35 | var deleteFoldersStarted = false; 36 | var rewriteContributorsFileExpected = false; 37 | 38 | foreach (var arg in args) 39 | if (deleteFilesStarted) 40 | { 41 | options.FilesToDelete.AddRange(GetFiles(arg)); 42 | deleteFilesStarted = false; 43 | } 44 | else if (deleteFoldersStarted) 45 | { 46 | options.FoldersToDelete.AddRange(GetFiles(arg)); 47 | deleteFoldersStarted = false; 48 | } 49 | else if (rewriteContributorsFileExpected) 50 | { 51 | options.ContributorMappingFile = arg; 52 | rewriteContributorsFileExpected = false; 53 | } 54 | else 55 | { 56 | switch (arg) 57 | { 58 | case "-e": 59 | options.RemoveEmptyCommits = true; 60 | break; 61 | case "-d": 62 | case "--delete-files": 63 | deleteFilesStarted = true; 64 | break; 65 | case "-D": 66 | case "--delete-folders": 67 | deleteFoldersStarted = true; 68 | break; 69 | case "--fix-trees": 70 | options.FixTrees = true; 71 | break; 72 | case "-h": 73 | case "--help": 74 | options.ShowHelp = true; 75 | break; 76 | case "--contributor-names": 77 | options.ListContributorNames = true; 78 | break; 79 | case "--rewrite-contributors": 80 | rewriteContributorsFileExpected = true; 81 | break; 82 | case "--protect-refs": 83 | options.ProtectRefs = true; 84 | break; 85 | default: 86 | if (arg.StartsWith("-")) 87 | throw new ArgumentException("Could not parse arguments."); 88 | 89 | if (!string.IsNullOrWhiteSpace(options.RepositoryPath)) 90 | throw new ArgumentException("Repository path is multiple times."); 91 | 92 | options.RepositoryPath = arg; 93 | 94 | break; 95 | } 96 | } 97 | 98 | var optionsSet = 0; 99 | optionsSet += options.FixTrees ? 1 : 0; 100 | optionsSet += options.FilesToDelete.Any() || options.FoldersToDelete.Any() ? 1 : 0; 101 | optionsSet += options.RemoveEmptyCommits ? 1 : 0; 102 | optionsSet += options.ListContributorNames ? 1 : 0; 103 | optionsSet += !string.IsNullOrWhiteSpace(options.ContributorMappingFile) ? 1 : 0; 104 | 105 | if (optionsSet > 1) 106 | { 107 | Console.WriteLine( 108 | "Cannot mix operations. Only choose one operation at a time (multiple file deletes are allowed)."); 109 | Console.WriteLine(); 110 | PrintHelp(); 111 | return false; 112 | } 113 | 114 | if (optionsSet == 0 || string.IsNullOrWhiteSpace(options.RepositoryPath)) 115 | { 116 | PrintHelp(); 117 | return false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | public static void PrintHelp() 124 | { 125 | Console.WriteLine("GitRewrite [options] repository_path"); 126 | Console.WriteLine(); 127 | Console.WriteLine("Options:"); 128 | Console.WriteLine("-e"); 129 | Console.WriteLine(" Removes empty commits from the repository."); 130 | Console.WriteLine(); 131 | 132 | Console.WriteLine("-d [filePattern...], --delete-files [filePattern...]"); 133 | Console.WriteLine(" Delete files from the repository."); 134 | Console.WriteLine( 135 | " [filePattern...] is a list of comma separated patterns. Option can be defined multiple times."); 136 | Console.WriteLine( 137 | " If filePattern is filename, then the file with the name filename will be deleted from all directories."); 138 | Console.WriteLine( 139 | " If filePattern is filename*, then all files starting with filename will be deleted from all directories."); 140 | Console.WriteLine( 141 | " If filePattern is *filename, then all files ending with filename will be deleted from all directories."); 142 | Console.WriteLine( 143 | " If filePattern is /path/to/filename, then the file will be delete only in the exact directory."); 144 | Console.WriteLine(" Use --protect-refs to not update commits refs point to."); 145 | Console.WriteLine(); 146 | 147 | Console.WriteLine("-D [directoryPattern...], --delete-directories [directoryPattern...]"); 148 | Console.WriteLine( 149 | " Delete whole directories from the repository. Directory specifications follow the same pattern as for files."); 150 | Console.WriteLine(" Can be combined with deleting files."); 151 | Console.WriteLine(); 152 | 153 | Console.WriteLine("--rewrite-contributors [contributors.txt]"); 154 | Console.WriteLine(" Rewrite author and committer information."); 155 | Console.WriteLine(" contributors.txt is the mapping file for the names that should be replaced. Each line represents one contributor to replace."); 156 | Console.WriteLine(" Format is "); 157 | Console.WriteLine(" Test User = New Test User "); 158 | Console.WriteLine(); 159 | 160 | Console.WriteLine("--contributor-names"); 161 | Console.WriteLine(" Writes all authors and committers to stdout"); 162 | Console.WriteLine(); 163 | 164 | Console.WriteLine("--fix-trees"); 165 | Console.WriteLine( 166 | " Checks for trees with duplicate entries. Rewrites the tree taking only the first entry."); 167 | Console.WriteLine(); 168 | } 169 | 170 | private static List GetFiles(string fileString) 171 | { 172 | var result = new List(); 173 | 174 | var fileSpan = fileString.AsSpan(); 175 | 176 | var indexOfSeperator = fileSpan.IndexOf(','); 177 | while (indexOfSeperator >= 0) 178 | { 179 | if (indexOfSeperator == 0) 180 | continue; 181 | 182 | var arg = new string(fileSpan.Slice(0, indexOfSeperator)); 183 | if (!string.IsNullOrWhiteSpace(arg)) 184 | result.Add(arg); 185 | fileSpan = fileSpan.Slice(indexOfSeperator + 1); 186 | indexOfSeperator = fileSpan.IndexOf(','); 187 | } 188 | 189 | var lastArg = new string(fileSpan); 190 | if (!string.IsNullOrWhiteSpace(lastArg)) 191 | result.Add(new string(fileSpan)); 192 | 193 | return result; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /GitRewrite/CommitWalker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using GitRewrite.GitObjects; 7 | 8 | namespace GitRewrite 9 | { 10 | public static class CommitWalker 11 | { 12 | public static IEnumerable CommitsRandomOrder(string vcsPath) 13 | { 14 | var commitsAlreadySeen = new HashSet(); 15 | var commits = ReadCommitsFromRefs(vcsPath); 16 | while (commits.TryPop(out var commit)) 17 | { 18 | if (!commitsAlreadySeen.Add(commit.Hash)) 19 | continue; 20 | 21 | yield return commit; 22 | 23 | foreach (var parent in commit.Parents.Where(parent => !commitsAlreadySeen.Contains(parent))) 24 | commits.Push(GitObjectFactory.ReadCommit(vcsPath, parent)); 25 | } 26 | } 27 | 28 | public static IEnumerable CommitsInOrder(string vcsPath) 29 | { 30 | var commits = ReadCommitsFromRefs(vcsPath); 31 | var parentsSeen = new HashSet(); 32 | var commitsProcessed = new HashSet(); 33 | 34 | while (commits.TryPop(out var commit)) 35 | if (commitsProcessed.Contains(commit.Hash)) 36 | parentsSeen.Remove(commit.Hash); 37 | else 38 | { 39 | if (!parentsSeen.Add(commit.Hash) || !commit.HasParents) 40 | { 41 | commitsProcessed.Add(commit.Hash); 42 | yield return commit; 43 | } 44 | else 45 | { 46 | commits.Push(commit); 47 | foreach (var parent in commit.Parents) 48 | { 49 | if (!commitsProcessed.Contains(parent)) 50 | { 51 | var parentCommit = GitObjectFactory.ReadCommit(vcsPath, parent); 52 | if (parentCommit == null) 53 | throw new Exception("Commit not found: " + parent); 54 | commits.Push(parentCommit); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | private static ConcurrentStack ReadCommitsFromRefs(string vcsPath) 62 | { 63 | var refs = Refs.ReadAll(vcsPath); 64 | var addedCommits = new ConcurrentDictionary(); 65 | var result = new ConcurrentStack(); 66 | 67 | Parallel.ForEach(refs, new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount}, @ref => 68 | { 69 | var gitObject = @ref is TagRef tag 70 | ? GitObjectFactory.ReadGitObject(vcsPath, new ObjectHash(tag.CommitHash)) 71 | : GitObjectFactory.ReadGitObject(vcsPath, new ObjectHash(@ref.Hash)); 72 | 73 | while (gitObject is Tag tagObject) 74 | { 75 | gitObject = GitObjectFactory.ReadGitObject(vcsPath, new ObjectHash(tagObject.Object)); 76 | } 77 | 78 | // Tags pointing to trees are ignored 79 | if (gitObject.Type == GitObjectType.Commit && addedCommits.TryAdd(gitObject.Hash, true)) 80 | { 81 | result.Push((Commit) gitObject); 82 | } 83 | }); 84 | 85 | return result; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /GitRewrite/Diff/DiffInstruction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitRewrite.Diff 4 | { 5 | internal interface DiffInstruction 6 | { 7 | public int Length { get; } 8 | } 9 | 10 | internal class AddInstruction : DiffInstruction 11 | { 12 | internal readonly Memory Bytes; 13 | internal readonly int Start; 14 | internal readonly int End; 15 | 16 | public AddInstruction(Memory bytes, ref int currentOffset) 17 | { 18 | var bytesToCopy = bytes.Span[currentOffset++]; 19 | 20 | Bytes = bytes; 21 | Start = currentOffset; 22 | End = currentOffset + bytesToCopy; 23 | 24 | currentOffset += bytesToCopy; 25 | } 26 | 27 | public AddInstruction(Memory bytes, int start, int end) 28 | { 29 | Bytes = bytes; 30 | Start = start; 31 | End = end; 32 | } 33 | 34 | public int Length => End - Start; 35 | } 36 | 37 | internal class CopyInstruction : DiffInstruction 38 | { 39 | private readonly int _length; 40 | private readonly int _offset; 41 | 42 | public int Length => _length; 43 | public int Offset => _offset; 44 | 45 | public CopyInstruction(Span data, ref int currentOffset) 46 | { 47 | var copyInstruction = data[currentOffset++]; 48 | 49 | var offset = 0; 50 | var len = 0; 51 | 52 | if ((copyInstruction & 0b00000001) != 0) 53 | offset |= data[currentOffset++]; 54 | 55 | if ((copyInstruction & 0b00000010) != 0) 56 | offset |= data[currentOffset++] << 8; 57 | 58 | if ((copyInstruction & 0b00000100) != 0) 59 | offset |= data[currentOffset++] << 16; 60 | 61 | if ((copyInstruction & 0b00001000) != 0) 62 | offset |= data[currentOffset++] << 24; 63 | 64 | if ((copyInstruction & 0b00010000) != 0) 65 | len |= data[currentOffset++]; 66 | 67 | if ((copyInstruction & 0b00100000) != 0) 68 | len |= data[currentOffset++] << 8; 69 | 70 | if ((copyInstruction & 0b01000000) != 0) 71 | len |= data[currentOffset++] << 16; 72 | 73 | if (len == 0) 74 | len = 0x10000; 75 | 76 | _length = len; 77 | _offset = offset; 78 | } 79 | 80 | internal CopyInstruction(int offset, int length) 81 | { 82 | _offset = offset; 83 | _length = length; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /GitRewrite/Diff/PackDiff.cs: -------------------------------------------------------------------------------- 1 | using GitRewrite.Diff; 2 | using GitRewrite.IO; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO.MemoryMappedFiles; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | 11 | namespace GitRewrite.Diff 12 | { 13 | internal class PackDiff 14 | { 15 | private List _instructions; 16 | public readonly int TargetLen; 17 | public readonly long NegativeOffset; 18 | 19 | public PackDiff(MemoryMappedViewAccessor memory, PackObject packObject) 20 | { 21 | var (baseOffset, bytesRead) = ReadDeltaOffset(memory, packObject); 22 | 23 | var difInstructionBytes = HashContent.Unpack(memory, packObject, bytesRead); 24 | 25 | (_, bytesRead) = ReadVariableDeltaOffsetLength(difInstructionBytes, 0); 26 | int targetLength; 27 | (targetLength, bytesRead) = ReadVariableDeltaOffsetLength(difInstructionBytes, bytesRead); 28 | 29 | _instructions = BuildDeltaInstructions(difInstructionBytes, packObject, bytesRead); 30 | TargetLen = targetLength; 31 | NegativeOffset = baseOffset; 32 | } 33 | 34 | private PackDiff(int targetLen, long negativeOffset, List instructions) 35 | { 36 | TargetLen = targetLen; 37 | NegativeOffset = negativeOffset; 38 | _instructions = instructions; 39 | } 40 | 41 | public PackDiff Combine(PackDiff other) 42 | { 43 | var instructions = new List(); 44 | foreach (var instruction in _instructions) 45 | { 46 | if (instruction is CopyInstruction copyInstruction) 47 | instructions.AddRange(GetInstructionsFromCopy(copyInstruction, other)); 48 | else 49 | instructions.Add(instruction); 50 | } 51 | 52 | return new PackDiff(TargetLen, other.NegativeOffset, instructions); 53 | } 54 | 55 | public byte[] Apply(Memory bytes) 56 | { 57 | var target = new byte[TargetLen]; 58 | var targetOffset = 0; 59 | 60 | foreach (var instruction in _instructions) 61 | { 62 | if (instruction is AddInstruction add) 63 | { 64 | var len = add.Length; 65 | add.Bytes.Span[add.Start..add.End].CopyTo(target.AsSpan(targetOffset, len)); 66 | targetOffset += len; 67 | } 68 | else if (instruction is CopyInstruction copy) 69 | { 70 | bytes.Span[copy.Offset..(copy.Offset + copy.Length)].CopyTo(target.AsSpan(targetOffset, copy.Length)); 71 | targetOffset += copy.Length; 72 | } 73 | } 74 | 75 | return target; 76 | } 77 | 78 | private static IEnumerable GetInstructionsFromCopy(CopyInstruction copyInstruction, PackDiff source) 79 | { 80 | var currentSourceOffset = 0; 81 | var copyInstructionConsumed = 0; 82 | 83 | var endOffset = copyInstruction.Offset + copyInstruction.Length; 84 | 85 | foreach (var sourceInstruction in source._instructions) 86 | { 87 | if (copyInstruction.Offset < currentSourceOffset + sourceInstruction.Length 88 | && endOffset > currentSourceOffset) 89 | { 90 | var sourceInstructionOffset = copyInstruction.Offset + copyInstructionConsumed - currentSourceOffset; 91 | var bytesToTake = sourceInstruction.Length - sourceInstructionOffset <= copyInstruction.Length - copyInstructionConsumed 92 | ? sourceInstruction.Length - sourceInstructionOffset 93 | : copyInstruction.Length - copyInstructionConsumed; 94 | 95 | yield return sourceInstruction switch 96 | { 97 | CopyInstruction copy => new CopyInstruction(copy.Offset + sourceInstructionOffset, bytesToTake), 98 | AddInstruction add => new AddInstruction(add.Bytes, add.Start + sourceInstructionOffset, add.Start + sourceInstructionOffset + bytesToTake), 99 | _ => throw new NotImplementedException("unknown diff instruction type") 100 | }; 101 | 102 | copyInstructionConsumed += bytesToTake; 103 | } 104 | else if (endOffset < currentSourceOffset) { 105 | break; 106 | } 107 | 108 | currentSourceOffset += sourceInstruction.Length; 109 | } 110 | } 111 | 112 | private static List BuildDeltaInstructions(byte[] diffData, PackObject packObject, int bytesRead) 113 | { 114 | var result = new List(); 115 | 116 | while (bytesRead < packObject.DataSize) 117 | { 118 | var instruction = diffData[bytesRead]; 119 | 120 | if ((instruction & 0b10000000) != 0) { 121 | var copyInstruction = new CopyInstruction(diffData, ref bytesRead); 122 | result.Add(copyInstruction); 123 | } 124 | else 125 | { 126 | var addInstruction = new AddInstruction(diffData, ref bytesRead); 127 | result.Add(addInstruction); 128 | } 129 | } 130 | 131 | return result; 132 | } 133 | 134 | private static (long NegativeOffset, int BytesRead) ReadDeltaOffset(MemoryMappedViewAccessor packFile, PackObject packObject) 135 | { 136 | var readByte = packFile.ReadByte(packObject.Offset + packObject.HeaderLength); 137 | var bytesRead = 1; 138 | var offset = (long)readByte & 127; 139 | 140 | while ((readByte & 128) != 0) 141 | { 142 | offset += 1; 143 | readByte = packFile.ReadByte(packObject.Offset + packObject.HeaderLength + bytesRead++); 144 | offset <<= 7; 145 | offset += (long)readByte & 127; 146 | } 147 | 148 | return (offset, bytesRead); 149 | } 150 | 151 | private static (int targetLength, int deltaOffset) ReadVariableDeltaOffsetLength(in ReadOnlySpan deltaData, 152 | int offset = 0) 153 | { 154 | var b = deltaData[offset++]; 155 | var length = b & 0b01111111; 156 | var fsbSet = (b & 0b10000000) != 0; 157 | var shift = 7; 158 | while (fsbSet) 159 | { 160 | b = deltaData[offset++]; 161 | fsbSet = (b & 0b10000000) != 0; 162 | length |= (b & 0b01111111) << shift; 163 | shift += 7; 164 | } 165 | 166 | return (length, offset); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /GitRewrite/GitObjectFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using GitRewrite.GitObjects; 4 | using GitRewrite.IO; 5 | 6 | namespace GitRewrite 7 | { 8 | public static class GitObjectFactory 9 | { 10 | public static Commit CommitFromContentBytes(byte[] contentBytes) 11 | { 12 | var hash = GetObjectHash(GitObjectType.Commit, contentBytes); 13 | return new Commit(hash, contentBytes); 14 | } 15 | 16 | public static Tag TagFromContentBytes(byte[] contentBytes) 17 | { 18 | var hash = GetObjectHash(GitObjectType.Tag, contentBytes); 19 | return new Tag(hash, contentBytes); 20 | } 21 | 22 | public static Tree TreeFromContentBytes(byte[] contentBytes) 23 | { 24 | var hash = GetObjectHash(GitObjectType.Tree, contentBytes); 25 | return new Tree(hash, contentBytes); 26 | } 27 | 28 | public static byte[] GetBytesWithHeader(GitObjectType type, byte[] contentBytes) 29 | { 30 | string header; 31 | if (type == GitObjectType.Commit) 32 | header = "commit " + contentBytes.Length + '\0'; 33 | else if (type == GitObjectType.Tag) 34 | header = "tag " + contentBytes.Length + '\0'; 35 | else if (type == GitObjectType.Tree) 36 | header = "tree " + contentBytes.Length + '\0'; 37 | else if (type == GitObjectType.Blob) 38 | header = "blob " + contentBytes.Length + '\0'; 39 | else 40 | throw new NotImplementedException(); 41 | 42 | var headerBytes = Encoding.ASCII.GetBytes(header); 43 | var resultBuffer = new byte[headerBytes.Length + contentBytes.Length]; 44 | Array.Copy(headerBytes, resultBuffer, headerBytes.Length); 45 | Array.Copy(contentBytes, 0, resultBuffer, headerBytes.Length, contentBytes.Length); 46 | 47 | return resultBuffer; 48 | } 49 | 50 | private static ObjectHash GetObjectHash(GitObjectType type, byte[] contentBytes) 51 | { 52 | var bytesWithHeader = GetBytesWithHeader(type, contentBytes); 53 | var hash = new ObjectHash(Hash.Create(bytesWithHeader)); 54 | return hash; 55 | } 56 | 57 | public static GitObjectBase ReadGitObject(string repositoryPath, ObjectHash hash) 58 | { 59 | var gitObject = PackReader.GetObject(hash); 60 | if (gitObject != null) 61 | return gitObject; 62 | 63 | var fileContent = HashContent.FromFile(repositoryPath, hash.ToString()); 64 | var contentIndex = fileContent.AsSpan(7).IndexOf(0) + 8; 65 | 66 | if (IsCommit(fileContent)) 67 | return new Commit(hash, fileContent.AsSpan(contentIndex).ToArray()); 68 | 69 | if (IsTree(fileContent)) 70 | return new Tree(hash, fileContent.AsMemory(contentIndex)); 71 | 72 | if (IsTag(fileContent)) 73 | return new Tag(hash, fileContent.AsMemory(contentIndex)); 74 | 75 | // TODO blobs probably not working atm 76 | if (IsBlob(fileContent)) 77 | return new Blob(hash, fileContent.AsMemory(contentIndex)); 78 | 79 | return null; 80 | } 81 | 82 | public static Commit ReadCommit(string repositoryPath, ObjectHash hash) 83 | { 84 | var commit = PackReader.GetCommit(repositoryPath, hash); 85 | if (commit != null) 86 | return commit; 87 | 88 | var fileContent = HashContent.FromFile(repositoryPath, hash.ToString()); 89 | 90 | if (IsCommit(fileContent)) 91 | return new Commit(hash, 92 | fileContent.AsMemory(fileContent.AsSpan(7).IndexOf(0) + 8).ToArray()); 93 | 94 | throw new ArgumentException("Not a commit: " + hash); 95 | } 96 | 97 | public static Tree ReadTree(string repositoryPath, ObjectHash hash) 98 | { 99 | var tree = PackReader.GetTree(hash); 100 | if (tree != null) 101 | return tree; 102 | 103 | var fileContent = HashContent.FromFile(repositoryPath, hash.ToString()); 104 | 105 | if (IsTree(fileContent)) return new Tree(hash, 106 | fileContent.AsMemory(fileContent.AsSpan(5).IndexOf(0) + 6)); 107 | 108 | return null; 109 | } 110 | 111 | private static bool IsTag(in ReadOnlySpan fileContent) => AsciiBytesStartWith(fileContent, "tag "); 112 | 113 | private static bool IsTree(in ReadOnlySpan fileContent) => AsciiBytesStartWith(fileContent, "tree "); 114 | 115 | private static bool AsciiBytesStartWith(in ReadOnlySpan bytes, string str) 116 | { 117 | if (bytes.Length < str.Length) 118 | return false; 119 | 120 | for (var i = 0; i < str.Length; i++) 121 | if (bytes[i] != str[i]) 122 | return false; 123 | 124 | return true; 125 | } 126 | 127 | private static bool IsBlob(in ReadOnlySpan fileContent) => AsciiBytesStartWith(fileContent, "blob "); 128 | 129 | private static bool IsCommit(in ReadOnlySpan fileContent) => AsciiBytesStartWith(fileContent, "commit "); 130 | } 131 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjectType.cs: -------------------------------------------------------------------------------- 1 | namespace GitRewrite 2 | { 3 | public enum GitObjectType 4 | { 5 | Commit, 6 | Tag, 7 | Tree, 8 | Blob 9 | } 10 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/Blob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace GitRewrite.GitObjects 5 | { 6 | public sealed class Blob : GitObjectBase 7 | { 8 | private readonly ReadOnlyMemory _content; 9 | public Blob(ObjectHash hash, in ReadOnlyMemory plainContent) : base(hash, GitObjectType.Blob) => _content = plainContent; 10 | 11 | public string GetContentAsString() => Encoding.UTF8.GetString(_content.Span); 12 | 13 | public override byte[] SerializeToBytes() => _content.ToArray(); 14 | } 15 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/ByteArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitRewrite.GitObjects 4 | { 5 | static class ByteArrayExtensions 6 | { 7 | public static bool SpanEquals(this in Span span1, in ReadOnlySpan span2) 8 | { 9 | if (span1.Length != span2.Length) 10 | return false; 11 | 12 | for (int i = span1.Length - 1; i >= 0; i--) 13 | { 14 | if (span1[i] != span2[i]) 15 | return false; 16 | } 17 | 18 | return true; 19 | } 20 | 21 | public static bool SpanEquals(this in ReadOnlySpan span1, in ReadOnlySpan span2) 22 | { 23 | if (span1.Length != span2.Length) 24 | return false; 25 | 26 | for (int i = span1.Length - 1; i >= 0; i--) 27 | { 28 | if (span1[i] != span2[i]) 29 | return false; 30 | } 31 | 32 | return true; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GitRewrite/GitObjects/Commit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace GitRewrite.GitObjects 7 | { 8 | public sealed class Commit : GitObjectBase, IEquatable 9 | { 10 | private readonly Memory _authorLine; 11 | private readonly Memory _commitMessage; 12 | private readonly Memory _committerLine; 13 | private readonly byte[] _content; 14 | private readonly List _parents; 15 | private readonly Memory _treeHash; 16 | 17 | public Commit(ObjectHash hash, byte[] bytes) : base(hash, GitObjectType.Commit) 18 | { 19 | _content = bytes; 20 | var content = bytes.AsMemory(); 21 | _parents = new List(); 22 | 23 | var nextNewLine = content.Span.IndexOf((byte) '\n'); 24 | while (nextNewLine != -1) 25 | { 26 | var contentSpan = content.Span; 27 | if (contentSpan.StartsWith(ObjectPrefixes.TreePrefix)) 28 | { 29 | _treeHash = content.Slice(0, nextNewLine); 30 | } 31 | else if (contentSpan.StartsWith(ObjectPrefixes.ParentPrefix)) 32 | { 33 | _parents.Add(new ObjectHash(content.Span.Slice(7, nextNewLine - 7))); 34 | } 35 | else if (contentSpan.StartsWith(ObjectPrefixes.AuthorPrefix)) 36 | { 37 | _authorLine = content.Slice(0, nextNewLine); 38 | } 39 | else if (contentSpan.StartsWith(ObjectPrefixes.CommitterPrefix)) 40 | { 41 | _committerLine = content.Slice(0, nextNewLine); 42 | } 43 | else if (contentSpan.StartsWith(ObjectPrefixes.GpgSigPrefix)) 44 | { 45 | // gpgsig are not really handled, instead a gpgsig is not written back when rewriting the object 46 | var pgpSignatureEnd = content.Span.IndexOf(PgpSignatureEnd); 47 | content = content.Slice(pgpSignatureEnd + PgpSignatureEnd.Length + 1); 48 | nextNewLine = content.Span.IndexOf((byte) '\n'); 49 | } 50 | else 51 | { 52 | // We view everything that is not defined above as commit message 53 | _commitMessage = content; 54 | break; 55 | } 56 | 57 | content = content.Slice(nextNewLine + 1); 58 | nextNewLine = content.Span.IndexOf((byte) '\n'); 59 | } 60 | } 61 | 62 | private static readonly byte[] PgpSignatureEnd = "-----END PGP SIGNATURE-----".Select(c => (byte) c).ToArray(); 63 | 64 | public ReadOnlySpan GetCommitterBytes() => GetContributorName(_committerLine.Slice(10)); 65 | 66 | public string GetCommitterName() => Encoding.UTF8.GetString(GetCommitterBytes()); 67 | 68 | public ReadOnlySpan GetAuthorBytes() => GetContributorName(_authorLine.Slice(7)); 69 | 70 | public string GetAuthorName() => Encoding.UTF8.GetString(GetAuthorBytes()); 71 | 72 | private ReadOnlySpan GetContributorName(ReadOnlyMemory contributorWithTime) 73 | { 74 | var span = contributorWithTime.Span; 75 | int spaces = 0; 76 | int index = 0; 77 | for (int i = contributorWithTime.Length - 1; i >= 0; i--) 78 | { 79 | if (span[i] == ' ' && ++spaces == 2) 80 | { 81 | index = i; 82 | break; 83 | } 84 | } 85 | 86 | return contributorWithTime.Span.Slice(0, index); 87 | } 88 | 89 | public ObjectHash TreeHash => new ObjectHash(_treeHash.Span.Slice(5)); 90 | 91 | public List Parents => _parents; 92 | 93 | public string CommitMessage => Encoding.UTF8.GetString(_commitMessage.Span); 94 | 95 | public bool HasParents => _parents.Any(); 96 | 97 | public bool Equals(Commit other) 98 | { 99 | if (ReferenceEquals(null, other)) return false; 100 | 101 | if (ReferenceEquals(this, other)) return true; 102 | 103 | return base.Equals(other) && Hash.Equals(other.Hash); 104 | } 105 | 106 | public override byte[] SerializeToBytes() 107 | => _content; 108 | 109 | public static byte[] GetSerializedCommitWithChangedTreeAndParents(Commit commit, ObjectHash treeHash, 110 | List parents) 111 | { 112 | const int firstLineLength = 46; 113 | const int treePrefixLength = 5; 114 | const int parentLineLength = 7 + 40 + 1; 115 | 116 | var contentSize = firstLineLength + parents.Count * parentLineLength + commit._authorLine.Length + 1 + 117 | commit._committerLine.Length + 1 + commit._commitMessage.Length; 118 | 119 | var resultBuffer = new byte[contentSize]; 120 | 121 | Array.Copy(ObjectPrefixes.TreePrefix, resultBuffer, treePrefixLength); 122 | Array.Copy(treeHash.ToStringBytes(), 0, resultBuffer, treePrefixLength, 40); 123 | resultBuffer[45] = (byte) '\n'; 124 | 125 | var bytesCopied = firstLineLength; 126 | 127 | foreach (var parent in parents) 128 | { 129 | Array.Copy(ObjectPrefixes.ParentPrefix, 0, resultBuffer, bytesCopied, 7); 130 | Array.Copy(parent.ToStringBytes(), 0, resultBuffer, bytesCopied + 7, 40); 131 | bytesCopied += 47; 132 | resultBuffer[bytesCopied++] = (byte) '\n'; 133 | } 134 | 135 | commit._authorLine.Span.CopyTo(resultBuffer.AsSpan(bytesCopied, commit._authorLine.Length)); 136 | bytesCopied += commit._authorLine.Length; 137 | resultBuffer[bytesCopied++] = (byte) '\n'; 138 | 139 | commit._committerLine.Span.CopyTo(resultBuffer.AsSpan(bytesCopied, commit._committerLine.Length)); 140 | bytesCopied += commit._committerLine.Length; 141 | resultBuffer[bytesCopied++] = (byte) '\n'; 142 | 143 | commit._commitMessage.Span.CopyTo(resultBuffer.AsSpan(bytesCopied, commit._commitMessage.Length)); 144 | 145 | return resultBuffer; 146 | } 147 | 148 | public byte[] WithChangedContributor(Dictionary contributorMapping, IEnumerable parents) 149 | { 150 | const int firstLineLength = 46; 151 | const int parentLineLength = 7 + 40 + 1; 152 | 153 | var author = GetAuthorName(); 154 | var committer = GetCommitterName(); 155 | if (!contributorMapping.TryGetValue(author, out var newAuthor)) 156 | newAuthor = author; 157 | 158 | if (!contributorMapping.TryGetValue(this.GetCommitterName(), out var newCommitter)) 159 | newCommitter = committer; 160 | 161 | var authorLine = Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(this._authorLine.Span).Replace(author, newAuthor)); 162 | var committerLine = Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(this._committerLine.Span).Replace(committer, newCommitter)); 163 | 164 | var contentSize = firstLineLength + _parents.Count * parentLineLength + authorLine.Length + 1 + 165 | committerLine.Length + 1 + _commitMessage.Length; 166 | 167 | var resultBuffer = new byte[contentSize]; 168 | 169 | _treeHash.Span.CopyTo(resultBuffer.AsSpan(0, this._treeHash.Length)); 170 | resultBuffer[45] = (byte) '\n'; 171 | 172 | var bytesCopied = firstLineLength; 173 | 174 | foreach (var parent in parents) 175 | { 176 | Array.Copy(ObjectPrefixes.ParentPrefix, 0, resultBuffer, bytesCopied, 7); 177 | Array.Copy(parent.ToStringBytes(), 0, resultBuffer, bytesCopied + 7, 40); 178 | bytesCopied += 47; 179 | resultBuffer[bytesCopied++] = (byte) '\n'; 180 | } 181 | 182 | Array.Copy(authorLine, 0, resultBuffer, bytesCopied, authorLine.Length); 183 | bytesCopied += authorLine.Length; 184 | resultBuffer[bytesCopied++] = (byte) '\n'; 185 | 186 | Array.Copy(committerLine, 0, resultBuffer, bytesCopied, committerLine.Length); 187 | //commit._committerLine.Span.CopyTo(resultBuffer.AsSpan(bytesCopied, commit._committerLine.Length)); 188 | bytesCopied += committerLine.Length; 189 | resultBuffer[bytesCopied++] = (byte) '\n'; 190 | 191 | _commitMessage.Span.CopyTo(resultBuffer.AsSpan(bytesCopied, _commitMessage.Length)); 192 | 193 | return resultBuffer; 194 | } 195 | 196 | 197 | public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is Commit other && Equals(other); 198 | 199 | public override int GetHashCode() => base.GetHashCode(); 200 | } 201 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/GitObjectBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitRewrite.GitObjects 4 | { 5 | public abstract class GitObjectBase : IEquatable 6 | { 7 | private readonly int _hashCode; 8 | 9 | protected GitObjectBase(ObjectHash hash, GitObjectType type) 10 | { 11 | Hash = hash; 12 | Type = type; 13 | 14 | _hashCode = hash.GetHashCode(); 15 | } 16 | 17 | public readonly ObjectHash Hash; 18 | 19 | public readonly GitObjectType Type; 20 | 21 | public bool Equals(GitObjectBase other) 22 | { 23 | if (ReferenceEquals(null, other)) return false; 24 | 25 | if (ReferenceEquals(this, other)) return true; 26 | 27 | return Hash.Equals(other.Hash); 28 | } 29 | 30 | public abstract byte[] SerializeToBytes(); 31 | 32 | public override bool Equals(object obj) 33 | { 34 | if (ReferenceEquals(null, obj)) return false; 35 | 36 | if (ReferenceEquals(this, obj)) return true; 37 | 38 | if (obj.GetType() != GetType()) return false; 39 | 40 | return Equals((GitObjectBase) obj); 41 | } 42 | 43 | public override int GetHashCode() => _hashCode; 44 | } 45 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/ObjectHash.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitRewrite.GitObjects 4 | { 5 | public readonly struct ObjectHash : IEquatable 6 | { 7 | private readonly int _hashCode; 8 | private const int ByteHashLength = 20; 9 | 10 | public ObjectHash(byte[] hash) 11 | { 12 | if (hash.Length != ByteHashLength) 13 | throw new ArgumentException(); 14 | 15 | Bytes = hash; 16 | 17 | unchecked 18 | { 19 | _hashCode = 0; 20 | for (var index = 0; index < ByteHashLength; index++) 21 | { 22 | var b = hash[index]; 23 | _hashCode = (_hashCode * 31) ^ b; 24 | } 25 | } 26 | } 27 | 28 | public ObjectHash(string hash) : this(Hash.StringToByteArray(hash)) 29 | { 30 | } 31 | 32 | public ObjectHash(in ReadOnlySpan hashStringAsBytes) 33 | : this(Hash.HashStringToByteArray(hashStringAsBytes)) 34 | { 35 | } 36 | 37 | public readonly byte[] Bytes; 38 | 39 | public override string ToString() => Hash.ByteArrayToString(Bytes); 40 | 41 | public byte[] ToStringBytes() => Hash.ByteArrayToTextBytes(Bytes); 42 | 43 | public bool Equals(ObjectHash other) 44 | { 45 | if (_hashCode != other._hashCode) 46 | return false; 47 | 48 | for (int i = 0; i < 20; i++) 49 | { 50 | if (Bytes[i] != other.Bytes[i]) 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | 57 | public override bool Equals(object obj) => obj is ObjectHash other && Equals(other); 58 | 59 | public override int GetHashCode() 60 | => _hashCode; 61 | 62 | public static bool operator !=(ObjectHash h1, ObjectHash h2) => !h1.Equals(h2); 63 | 64 | public static bool operator ==(ObjectHash h1, ObjectHash h2) => h1.Equals(h2); 65 | } 66 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/ObjectPrefixes.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace GitRewrite.GitObjects 4 | { 5 | public static class ObjectPrefixes 6 | { 7 | public static readonly byte[] TreePrefix = "tree ".Select(x => (byte) x).ToArray(); 8 | public static readonly byte[] ParentPrefix = "parent ".Select(x => (byte) x).ToArray(); 9 | public static readonly byte[] AuthorPrefix = "author ".Select(x => (byte) x).ToArray(); 10 | public static readonly byte[] CommitterPrefix = "committer ".Select(x => (byte) x).ToArray(); 11 | public static readonly byte[] GpgSigPrefix = "gpgsig ".Select(x => (byte) x).ToArray(); 12 | public static readonly byte[] TagPrefix = "tag ".Select(x => (byte) x).ToArray(); 13 | public static readonly byte[] TaggerPrefix = "tagger ".Select(x => (byte) x).ToArray(); 14 | public static readonly byte[] ObjectPrefix = "object ".Select(x => (byte) x).ToArray(); 15 | public static readonly byte[] TypePrefix = "type ".Select(x => (byte) x).ToArray(); 16 | } 17 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/Tag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace GitRewrite.GitObjects 6 | { 7 | public sealed class Tag : GitObjectBase 8 | { 9 | private static readonly byte[] TagKey = "tag".Select(x => (byte) x).ToArray(); 10 | private static readonly byte[] TreeKey = "tree".Select(x => (byte) x).ToArray(); 11 | private readonly Memory _content; 12 | private readonly Memory _message; 13 | private readonly Memory _object; 14 | private readonly Memory _tag; 15 | private readonly Memory _tagger; 16 | private readonly Memory _type; 17 | 18 | public Tag(ObjectHash hash, Memory content) : base(hash, GitObjectType.Tag) 19 | { 20 | _content = content; 21 | 22 | var nextNewLine = content.Span.IndexOf(10); 23 | while (nextNewLine != -1) 24 | { 25 | var contentSpan = content.Span; 26 | if (contentSpan.StartsWith(ObjectPrefixes.ObjectPrefix)) 27 | { 28 | _object = content.Slice(0, nextNewLine); 29 | } 30 | else if (contentSpan.StartsWith(ObjectPrefixes.TypePrefix)) 31 | { 32 | _type = content.Slice(0, nextNewLine); 33 | } 34 | else if (contentSpan.StartsWith(ObjectPrefixes.TagPrefix)) 35 | { 36 | _tag = content.Slice(0, nextNewLine); 37 | } 38 | else if (contentSpan.StartsWith(ObjectPrefixes.TaggerPrefix)) 39 | { 40 | _tagger = content.Slice(0, nextNewLine); 41 | } 42 | else 43 | { 44 | _message = content; 45 | break; 46 | } 47 | 48 | content = content.Slice(nextNewLine + 1); 49 | nextNewLine = content.Span.IndexOf(10); 50 | } 51 | } 52 | 53 | public string Object => Encoding.UTF8.GetString(_object.Span.Slice(7)); 54 | public string TypeName => Encoding.UTF8.GetString(_type.Span.Slice(5)); 55 | public string TagName => Encoding.UTF8.GetString(_tag.Span.Slice(4)); 56 | public string Tagger => _tagger.IsEmpty ? "" : Encoding.UTF8.GetString(_tagger.Span.Slice(7)); 57 | public string Message => Encoding.UTF8.GetString(_message.Span); 58 | 59 | public bool PointsToTag => _type.Span.Slice(5).StartsWith(TagKey); 60 | public bool PointsToTree => _type.Span.Slice(5).StartsWith(TreeKey); 61 | 62 | public Tag WithNewObject(string obj) 63 | { 64 | var resultBuffer = new byte[_content.Length]; 65 | var resultIndex = 0; 66 | 67 | for (var i = 0; i < 7; i++) 68 | resultBuffer[resultIndex++] = _object.Span[i]; 69 | 70 | var objBytes = Encoding.ASCII.GetBytes(obj); 71 | for (var i = 0; i < objBytes.Length; i++) 72 | resultBuffer[resultIndex++] = objBytes[i]; 73 | 74 | resultBuffer[resultIndex++] = 10; 75 | 76 | for (var i = 0; i < _type.Length; i++) 77 | resultBuffer[resultIndex++] = _type.Span[i]; 78 | 79 | resultBuffer[resultIndex++] = 10; 80 | 81 | for (var i = 0; i < _tag.Length; i++) 82 | resultBuffer[resultIndex++] = _tag.Span[i]; 83 | 84 | resultBuffer[resultIndex++] = 10; 85 | 86 | if (_tagger.Length > 0) 87 | { 88 | for (var i = 0; i < _tagger.Length; i++) 89 | resultBuffer[resultIndex++] = _tagger.Span[i]; 90 | 91 | resultBuffer[resultIndex++] = 10; 92 | } 93 | 94 | for (var i = 0; i < _message.Length; i++) 95 | resultBuffer[resultIndex++] = _message.Span[i]; 96 | 97 | return GitObjectFactory.TagFromContentBytes(resultBuffer); 98 | } 99 | 100 | public override byte[] SerializeToBytes() => _content.ToArray(); 101 | } 102 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/Tree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace GitRewrite.GitObjects 7 | { 8 | public sealed class Tree : GitObjectBase 9 | { 10 | public Tree(ObjectHash hash, ReadOnlyMemory bytes) : base(hash, GitObjectType.Tree) 11 | { 12 | var nullTerminatorIndex = bytes.Span.IndexOf((byte)'\0'); 13 | 14 | var lines = new List(); 15 | 16 | while (nullTerminatorIndex > 0) 17 | { 18 | var textSpan = bytes.Slice(0, nullTerminatorIndex); 19 | 20 | var lineHashInBytes = bytes.Slice(nullTerminatorIndex + 1, 20); 21 | 22 | var objectHash = new ObjectHash(lineHashInBytes.ToArray()); 23 | 24 | lines.Add(new TreeLine(textSpan, objectHash)); 25 | 26 | bytes = bytes.Slice(nullTerminatorIndex + 21); 27 | 28 | nullTerminatorIndex = bytes.Span.IndexOf((byte)'\0'); 29 | } 30 | 31 | Lines = lines; 32 | } 33 | 34 | public readonly IReadOnlyList Lines; 35 | 36 | public static byte[] GetSerializedObject(IReadOnlyList treeLines) 37 | { 38 | // lines bestehen immer aus text + \0 + hash in bytes 39 | var byteLines = new List, byte[]>>(); 40 | 41 | foreach (var treeLine in treeLines) 42 | { 43 | var textBytes = treeLine.TextBytes; 44 | var hashBytes = treeLine.Hash.Bytes; 45 | byteLines.Add(new Tuple, byte[]>(textBytes, hashBytes)); 46 | } 47 | 48 | var bytesTotal = byteLines.Sum(x => x.Item1.Length + 1 + x.Item2.Length); 49 | var result = new byte[bytesTotal]; 50 | 51 | var bytesCopied = 0; 52 | 53 | foreach (var byteLine in byteLines) 54 | { 55 | var resultSpan = result.AsSpan(bytesCopied, byteLine.Item1.Length); 56 | byteLine.Item1.Span.CopyTo(resultSpan); 57 | bytesCopied += byteLine.Item1.Length; 58 | result[bytesCopied++] = 0; 59 | Array.Copy(byteLine.Item2, 0, result, bytesCopied, 20); 60 | bytesCopied += 20; 61 | } 62 | 63 | return result; 64 | } 65 | 66 | public IEnumerable GetDirectories() => Lines.Where(line => line.IsDirectory()); 67 | 68 | public static bool HasDuplicateLines(IReadOnlyList treeLines) 69 | { 70 | // Treelines are ordered, so we only need to compare with the previous element, not all elements in the tree 71 | TreeLine lastLine = null; 72 | using (var it = treeLines.GetEnumerator()) 73 | { 74 | if (it.MoveNext()) 75 | lastLine = it.Current; 76 | 77 | while (it.MoveNext()) 78 | { 79 | var treeLine = it.Current; 80 | if (treeLine.TextBytes.Span.SpanEquals(lastLine.TextBytes.Span)) 81 | return true; 82 | lastLine = treeLine; 83 | } 84 | } 85 | 86 | return false; 87 | } 88 | 89 | public static Tree GetFixedTree(IEnumerable treeLines) 90 | { 91 | var distinctTreeLines = new List(); 92 | 93 | // Treelines are ordered, so we only need to compare with the previous element, not all elements in the tree 94 | int i = 0; 95 | using (var it = treeLines.GetEnumerator()) 96 | { 97 | if (it.MoveNext()) 98 | { 99 | distinctTreeLines.Add(it.Current); 100 | ++i; 101 | } 102 | 103 | while (it.MoveNext()) 104 | { 105 | var treeLine = it.Current; 106 | if (treeLine.TextBytes.Span.SpanEquals(distinctTreeLines[i++ - 1].TextBytes.Span)) 107 | --i; 108 | else 109 | distinctTreeLines.Add(treeLine); 110 | } 111 | } 112 | 113 | var serializedObject = GetSerializedObject(distinctTreeLines); 114 | 115 | return GitObjectFactory.TreeFromContentBytes(serializedObject); 116 | } 117 | 118 | public override byte[] SerializeToBytes() => GetSerializedObject(Lines); 119 | 120 | public class TreeLine 121 | { 122 | public TreeLine(ReadOnlyMemory text, ObjectHash hash) 123 | { 124 | TextBytes = text; 125 | Hash = hash; 126 | 127 | FileNameBytes = TextBytes.Slice(TextBytes.Span.IndexOf((byte) ' ') + 1); 128 | } 129 | 130 | public string TextString => Encoding.UTF8.GetString(TextBytes.Span); 131 | 132 | public readonly ReadOnlyMemory TextBytes; 133 | 134 | public readonly ReadOnlyMemory FileNameBytes; 135 | 136 | public readonly ObjectHash Hash; 137 | 138 | public bool IsDirectory() => TextBytes.Span[0] != '1'; 139 | 140 | public override string ToString() => Hash + " " + TextString; 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /GitRewrite/GitObjects/TreeLineByHashComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace GitRewrite.GitObjects 6 | { 7 | class TreeLineByHashComparer : IEqualityComparer 8 | { 9 | public bool Equals(Tree.TreeLine x, Tree.TreeLine y) => x.Hash.Equals(y.Hash); 10 | 11 | public int GetHashCode(Tree.TreeLine obj) => obj.Hash.GetHashCode(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GitRewrite/GitRewrite.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net7.0 6 | 1.4.0 7 | Linux 8 | true 9 | true 10 | true 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /GitRewrite/Hash.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Security.Cryptography; 6 | using GitRewrite.GitObjects; 7 | 8 | namespace GitRewrite 9 | { 10 | public static class Hash 11 | { 12 | private static readonly uint[] Lookup32 = CreateLookup32(); 13 | 14 | public static byte[] Create(byte[] data) 15 | { 16 | using (var sha1Hash = SHA1.Create()) 17 | { 18 | var computedHash = sha1Hash.ComputeHash(data); 19 | return computedHash; 20 | } 21 | } 22 | 23 | private static uint[] CreateLookup32() 24 | { 25 | var result = new uint[256]; 26 | for (var i = 0; i < 256; i++) 27 | { 28 | var s = i.ToString("x2"); 29 | result[i] = s[0] + ((uint) s[1] << 16); 30 | } 31 | 32 | return result; 33 | } 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | private static byte GetHashValue(byte c) 37 | { 38 | if (c <= 57) 39 | return (byte) (c - 48); 40 | 41 | return (byte) (c - 87); 42 | } 43 | 44 | public static byte[] HashStringToByteArray(in ReadOnlySpan hashStringAsBytes) 45 | { 46 | var resultSize = hashStringAsBytes.Length / 2; 47 | var result = new byte[resultSize]; 48 | 49 | for (int i = 0; i < resultSize; i++) 50 | result[i] = (byte) ((GetHashValue(hashStringAsBytes[2 * i]) << 4) | 51 | GetHashValue(hashStringAsBytes[2 * i + 1])); 52 | 53 | return result; 54 | } 55 | 56 | public static byte[] StringToByteArray(ReadOnlySpan hash) 57 | { 58 | var resultSize = hash.Length / 2; 59 | var result = new byte[resultSize]; 60 | 61 | for (int i = 0; i < resultSize; i++) 62 | result[i] = (byte) ((GetHashValue((byte)hash[2 * i]) << 4) | GetHashValue((byte)hash[2 * i + 1])); 63 | 64 | return result; 65 | } 66 | 67 | public static byte[] ByteArrayToTextBytes(byte[] bytes) 68 | { 69 | var lookup32 = Lookup32; 70 | var result = new byte[bytes.Length * 2]; 71 | for (int i = 0; i < bytes.Length; i++) 72 | { 73 | var val = lookup32[bytes[i]]; 74 | result[2 * i] = (byte) val; 75 | result[2 * i + 1] = (byte) (val >> 16); 76 | } 77 | 78 | return result; 79 | } 80 | 81 | public static string ByteArrayToString(byte[] bytes) 82 | { 83 | var lookup32 = Lookup32; 84 | 85 | return string.Create<(byte[] Bytes, uint[] Lookup)>( 86 | bytes.Length * 2, 87 | (bytes, lookup32), (result, state) => 88 | { 89 | for (var i = 0; i < state.Bytes.Length; i++) 90 | { 91 | var val = state.Lookup[state.Bytes[i]]; 92 | result[2 * i] = (char) val; 93 | result[2 * i + 1] = (char) (val >> 16); 94 | } 95 | }); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /GitRewrite/IO/Adler32Computer.cs: -------------------------------------------------------------------------------- 1 | namespace GitRewrite.IO 2 | { 3 | public class Adler32Computer 4 | { 5 | private const uint Base = 65521U; 6 | private const int Nmax = 5552; 7 | 8 | public static unsafe uint Checksum(byte[] buffer) 9 | { 10 | uint adler = 1; 11 | int len = buffer.Length; 12 | fixed (byte* fixedBufPointer = buffer) 13 | { 14 | var buf = fixedBufPointer; 15 | uint sum2 = (adler >> 16) & 0xffff; 16 | adler &= 0xffff; 17 | 18 | /* in case user likes doing a byte at a time, keep it fast */ 19 | if (len == 1) 20 | { 21 | adler += buf[0]; 22 | if (adler >= Base) 23 | adler -= Base; 24 | sum2 += adler; 25 | if (sum2 >= Base) 26 | sum2 -= Base; 27 | return adler | (sum2 << 16); 28 | } 29 | 30 | /* initial Adler-32 value (deferred check for len == 1 speed) */ 31 | if (len == 0) 32 | return 1; 33 | 34 | /* in case short lengths are provided, keep it somewhat fast */ 35 | if (len < 16) 36 | { 37 | while (len-- != 0) 38 | { 39 | adler += (*buf)++; //[bufIndex++]; 40 | sum2 += adler; 41 | } 42 | 43 | if (adler >= Base) 44 | adler -= Base; 45 | 46 | sum2 %= Base; 47 | return adler | (sum2 << 16); 48 | } 49 | 50 | /* do length NMAX blocks -- requires just one modulo operation */ 51 | while (len >= Nmax) 52 | { 53 | len -= Nmax; 54 | var n = Nmax / 16; 55 | do 56 | { 57 | adler += buf[0]; 58 | sum2 += adler; 59 | adler += buf[1]; 60 | sum2 += adler; 61 | adler += buf[2]; 62 | sum2 += adler; 63 | adler += buf[3]; 64 | sum2 += adler; 65 | adler += buf[4]; 66 | sum2 += adler; 67 | adler += buf[5]; 68 | sum2 += adler; 69 | adler += buf[6]; 70 | sum2 += adler; 71 | adler += buf[7]; 72 | sum2 += adler; 73 | adler += buf[8]; 74 | sum2 += adler; 75 | adler += buf[9]; 76 | sum2 += adler; 77 | adler += buf[10]; 78 | sum2 += adler; 79 | adler += buf[11]; 80 | sum2 += adler; 81 | adler += buf[12]; 82 | sum2 += adler; 83 | adler += buf[13]; 84 | sum2 += adler; 85 | adler += buf[14]; 86 | sum2 += adler; 87 | adler += buf[15]; 88 | sum2 += adler; 89 | 90 | buf += 16; 91 | } while (--n != 0); 92 | 93 | adler %= Base; 94 | sum2 %= Base; 95 | } 96 | 97 | /* do remaining bytes (less than NMAX, still just one modulo) */ 98 | if (len != 0) 99 | { 100 | while (len >= 16) 101 | { 102 | len -= 16; 103 | 104 | adler += buf[0]; 105 | sum2 += adler; 106 | adler += buf[1]; 107 | sum2 += adler; 108 | adler += buf[2]; 109 | sum2 += adler; 110 | adler += buf[3]; 111 | sum2 += adler; 112 | adler += buf[4]; 113 | sum2 += adler; 114 | adler += buf[5]; 115 | sum2 += adler; 116 | adler += buf[6]; 117 | sum2 += adler; 118 | adler += buf[7]; 119 | sum2 += adler; 120 | adler += buf[8]; 121 | sum2 += adler; 122 | adler += buf[9]; 123 | sum2 += adler; 124 | adler += buf[10]; 125 | sum2 += adler; 126 | adler += buf[11]; 127 | sum2 += adler; 128 | adler += buf[12]; 129 | sum2 += adler; 130 | adler += buf[13]; 131 | sum2 += adler; 132 | adler += buf[14]; 133 | sum2 += adler; 134 | adler += buf[15]; 135 | sum2 += adler; 136 | 137 | buf += 16; 138 | } 139 | 140 | while (len-- != 0) 141 | { 142 | adler += *buf++; 143 | sum2 += adler; 144 | } 145 | 146 | adler %= Base; 147 | sum2 %= Base; 148 | } 149 | 150 | /* return recombined sums */ 151 | return adler | (sum2 << 16); 152 | } 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /GitRewrite/IO/HashContent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.IO.MemoryMappedFiles; 5 | using GitRewrite.GitObjects; 6 | using Microsoft.IO; 7 | 8 | namespace GitRewrite.IO 9 | { 10 | public static class HashContent 11 | { 12 | #region - Methoden oeffentlich - 13 | 14 | public static void WriteFile(string basePath, byte[] bytes, string hash) 15 | { 16 | var directoryPath = Path.Combine(basePath, $"objects/{hash.Substring(0, 2)}"); 17 | var filePath = Path.Combine(directoryPath, $"{hash.Substring(2)}"); 18 | 19 | if (File.Exists(filePath)) 20 | return; 21 | 22 | Directory.CreateDirectory(directoryPath); 23 | 24 | using (var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write)) 25 | { 26 | fileStream.Write(new byte[] {0x78, 0x5E}); 27 | 28 | using (var stream = new DeflateStream(fileStream, CompressionMode.Compress, true)) 29 | { 30 | stream.Write(bytes); 31 | } 32 | 33 | var checksum = Adler32Computer.Checksum(bytes); 34 | var checksumBytes = BitConverter.GetBytes(checksum); 35 | if (BitConverter.IsLittleEndian) 36 | Array.Reverse(checksumBytes); 37 | 38 | fileStream.Write(checksumBytes); 39 | } 40 | } 41 | 42 | public static void WriteObject(string basePath, GitObjectBase gitObject) 43 | { 44 | var bytesWithHeader = GitObjectFactory.GetBytesWithHeader(gitObject.Type, gitObject.SerializeToBytes()); 45 | WriteFile(basePath, bytesWithHeader, gitObject.Hash.ToString()); 46 | } 47 | 48 | private static readonly RecyclableMemoryStreamManager MemoryManager = new RecyclableMemoryStreamManager(); 49 | 50 | public static byte[] FromFile(string basePath, string hashCode) 51 | { 52 | var filePath = Path.Combine(basePath, $"objects/{hashCode.Substring(0, 2)}/{hashCode.Substring(2)}"); 53 | 54 | using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) 55 | using (var stream = new DeflateStream(fileStream, CompressionMode.Decompress, false)) 56 | using (var memoryStream = new RecyclableMemoryStream(MemoryManager, hashCode)) 57 | { 58 | fileStream.Seek(2, SeekOrigin.Begin); 59 | stream.CopyTo(memoryStream); 60 | 61 | var result = new byte[memoryStream.Length]; 62 | Array.Copy(memoryStream.GetBuffer(), result, memoryStream.Length); 63 | 64 | return result; 65 | } 66 | } 67 | 68 | public static void UnpackTo(MemoryMappedViewAccessor fileView, PackObject packObject, 69 | byte[] buffer, int additionalOffset = 0) 70 | { 71 | var realOffset = packObject.Offset + packObject.HeaderLength + additionalOffset + 2; 72 | 73 | var safeHandle = fileView.SafeMemoryMappedViewHandle; 74 | long size = Math.Min(packObject.DataSize + 512, (long)safeHandle.ByteLength - realOffset); 75 | 76 | using (var unmanagedMemoryStream = new UnmanagedMemoryStream(safeHandle, realOffset, size)) 77 | using (var stream = new DeflateStream(unmanagedMemoryStream, CompressionMode.Decompress, true)) 78 | { 79 | var bytesRead = 0; 80 | do 81 | { 82 | bytesRead += stream.Read(buffer, bytesRead, packObject.DataSize - bytesRead); 83 | } while (bytesRead < packObject.DataSize); 84 | 85 | } 86 | } 87 | 88 | public static byte[] Unpack(MemoryMappedViewAccessor fileView, PackObject packObject, int additionalOffset = 0) 89 | { 90 | var realOffset = packObject.Offset + packObject.HeaderLength + additionalOffset + 2; 91 | var buffer = new byte[packObject.DataSize]; 92 | 93 | var safeHandle = fileView.SafeMemoryMappedViewHandle; 94 | long size = Math.Min(packObject.DataSize + 512, (long)safeHandle.ByteLength - realOffset); 95 | 96 | using (var unmanagedMemoryStream = new UnmanagedMemoryStream(safeHandle, realOffset, size)) 97 | using (var stream = new DeflateStream(unmanagedMemoryStream, CompressionMode.Decompress, true)) 98 | { 99 | var bytesRead = 0; 100 | do 101 | { 102 | bytesRead += stream.Read(buffer, bytesRead, packObject.DataSize - bytesRead); 103 | } while (bytesRead < packObject.DataSize); 104 | 105 | } 106 | 107 | return buffer; 108 | } 109 | 110 | #endregion 111 | } 112 | } -------------------------------------------------------------------------------- /GitRewrite/IO/IdxOffsetReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace GitRewrite.IO 7 | { 8 | public static class IdxOffsetReader 9 | { 10 | private const int HeaderLength = 8; 11 | private const int HashLength = 20; 12 | private const int FanoutLength = 4; 13 | private const int HashesTableStart = HeaderLength + 256 * FanoutLength; 14 | 15 | private static void VerifyHeader(byte[] header) 16 | { 17 | if (!IsPack(header)) throw new Exception("Not a idx file"); 18 | } 19 | 20 | private static bool IsPack(byte[] header) => 21 | header[0] == 255 && header[1] == 't' && header[2] == 'O' && header[3] == 'c' && 22 | header[4] == 0 && header[5] == 0 && header[6] == 0 && header[7] == 2; 23 | 24 | private static int GetFileCountFromFanout(in ReadOnlySpan bytes) 25 | { 26 | var result = bytes[3] << 0; 27 | result += bytes[2] << 8; 28 | result += bytes[1] << 16; 29 | result += bytes[0] << 24; 30 | 31 | return result; 32 | } 33 | 34 | public static IEnumerable<(byte[] Hash, long Offset)> GetPackOffsets(string idxFile) 35 | { 36 | using (var fileStream = new FileStream(idxFile, FileMode.Open, FileAccess.Read)) 37 | { 38 | var buffer = new byte[HashesTableStart]; 39 | 40 | fileStream.Read(buffer, 0, HashesTableStart); 41 | VerifyHeader(buffer); 42 | 43 | var objectCount = GetFileCountFromFanout(buffer.AsSpan(HeaderLength + 255 * FanoutLength)); 44 | if (objectCount == 0) 45 | yield break; 46 | 47 | var hashes = new Queue(); 48 | using (var bufferedStream = new BufferedStream(fileStream, 4096)) 49 | { 50 | for (var i = 0; i < objectCount; i++) 51 | { 52 | var hash = new byte[20]; 53 | bufferedStream.Read(hash); 54 | hashes.Enqueue(hash); 55 | } 56 | 57 | bufferedStream.Seek(HashesTableStart + HashLength * objectCount + 4 * objectCount, 58 | SeekOrigin.Begin); 59 | 60 | List largeOffsets = new List(); 61 | 62 | for (var i = 0; i < objectCount; i++) 63 | { 64 | bufferedStream.Read(buffer, 0, 4); 65 | var packOffset = buffer.AsSpan(0, 4); 66 | 67 | long offset = packOffset[3]; 68 | offset += packOffset[2] << 8; 69 | offset += packOffset[1] << 16; 70 | offset += (packOffset[0] & 0b01111111) << 24; 71 | 72 | if (MsbSet(packOffset)) 73 | largeOffsets.Add(hashes.Dequeue()); 74 | else 75 | yield return (hashes.Dequeue(), offset); 76 | } 77 | 78 | bufferedStream.Seek( 79 | HashesTableStart + HashLength * objectCount + 4 * objectCount + 80 | 4 * objectCount, 81 | SeekOrigin.Begin); 82 | 83 | foreach (var largeOffset in largeOffsets) 84 | { 85 | bufferedStream.Read(buffer, 0, 8); 86 | var packOffset = buffer.AsSpan(0, 8); 87 | if (BitConverter.IsLittleEndian) 88 | packOffset.Reverse(); 89 | 90 | yield return (largeOffset, BitConverter.ToInt64(packOffset)); 91 | } 92 | } 93 | } 94 | } 95 | 96 | public static bool MsbSet(in Span packOffset) => (packOffset[0] & 0b10000000) != 0; 97 | } 98 | } -------------------------------------------------------------------------------- /GitRewrite/IO/PackObject.cs: -------------------------------------------------------------------------------- 1 | namespace GitRewrite.IO 2 | { 3 | public readonly struct PackObject 4 | { 5 | public PackObject(int type, long offset, int headerLength, int dataSize) 6 | { 7 | Type = type; 8 | Offset = offset; 9 | HeaderLength = headerLength; 10 | DataSize = dataSize; 11 | } 12 | 13 | public readonly int Type; 14 | public readonly long Offset; 15 | public readonly int HeaderLength; 16 | public readonly int DataSize; 17 | } 18 | } -------------------------------------------------------------------------------- /GitRewrite/IO/PackReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.MemoryMappedFiles; 6 | using System.Linq; 7 | using GitRewrite.Diff; 8 | using GitRewrite.GitObjects; 9 | 10 | namespace GitRewrite.IO 11 | { 12 | public static class PackReader 13 | { 14 | public static Blob GetBlob(ObjectHash objectHash) 15 | { 16 | var result = GetGitObject(objectHash); 17 | if (result == null) 18 | return null; 19 | if (result.Type == GitObjectType.Blob) 20 | return (Blob) result; 21 | 22 | throw new ArgumentException(objectHash + " is not a blob."); 23 | } 24 | 25 | public static readonly object ReadPackFilesLock = new object(); 26 | public static volatile List<(string IdxFile, string PackFile)> PackFiles; 27 | 28 | public static IEnumerable<(string IdxFile, string PackFile)> GetPackFiles(string repositoryPath) 29 | { 30 | if (PackFiles != null) 31 | return PackFiles; 32 | 33 | lock (ReadPackFilesLock) 34 | { 35 | if (PackFiles != null) 36 | return PackFiles; 37 | 38 | PackFiles = Directory.GetFiles(Path.Combine(repositoryPath, "objects/pack"), "*.idx", 39 | SearchOption.TopDirectoryOnly).Select(idxFile => (idxFile, idxFile.Substring(0, idxFile.Length - 3) + "pack")).ToList(); 40 | } 41 | 42 | return PackFiles; 43 | } 44 | 45 | public static Commit GetCommit(string repositoryPath, ObjectHash hash) 46 | { 47 | var result = GetGitObject(hash); 48 | if (result == null) 49 | return null; 50 | if (result.Type == GitObjectType.Commit) 51 | return (Commit) result; 52 | 53 | throw new ArgumentException(hash + " is not a commit."); 54 | } 55 | 56 | public static GitObjectBase GetObject(ObjectHash hash) 57 | => GetGitObject(hash); 58 | 59 | public static Tree GetTree(ObjectHash hash) 60 | { 61 | var result = GetGitObject(hash); 62 | if (result == null) 63 | return null; 64 | if (result.Type == GitObjectType.Tree) 65 | return (Tree) result; 66 | 67 | throw new ArgumentException(hash + " is not a tree."); 68 | } 69 | 70 | private static Dictionary _packOffsets; 71 | 72 | public static void InitializePackFiles(string vcsPath) 73 | { 74 | _packOffsets = BuildPackFileDictionary(GetPackFiles(vcsPath)); 75 | } 76 | 77 | private static DictionaryBuildPackFileDictionary(IEnumerable<(string IdxFilePath, string PackFilePath)> packFiles) 78 | { 79 | var offsets = new Dictionary(); 80 | 81 | foreach (var file in packFiles) 82 | { 83 | var capacity = new FileInfo(file.PackFilePath).Length; 84 | var memoryMappedFile = MemoryMappedFile.CreateFromFile(file.PackFilePath, FileMode.Open, 85 | null, capacity, MemoryMappedFileAccess.Read); 86 | var viewAccessor = memoryMappedFile.CreateViewAccessor(0, capacity, MemoryMappedFileAccess.Read); 87 | 88 | foreach (var offset in IdxOffsetReader.GetPackOffsets(file.IdxFilePath)) 89 | { 90 | offsets.TryAdd(new ObjectHash(offset.Hash), (viewAccessor, offset.Offset)); 91 | } 92 | } 93 | 94 | return offsets; 95 | } 96 | 97 | private static (MemoryMappedViewAccessor ViewAccessor, long Offset) GetOffset(ObjectHash hash) 98 | { 99 | if (_packOffsets.TryGetValue(hash, out var result)) 100 | return result; 101 | return (null, -1); 102 | } 103 | 104 | public static GitObjectBase GetGitObject(ObjectHash hash) 105 | { 106 | var (viewAccessor, offset) = GetOffset(hash); 107 | if (offset == -1) 108 | return null; 109 | 110 | var packObject = ReadPackObject(viewAccessor, offset); 111 | 112 | byte[] unpackedBytes; 113 | var type = packObject.Type; 114 | if (packObject.Type == 6) 115 | { 116 | var unpackedObject = RestoreDiffedObjectBytes(viewAccessor, packObject); 117 | 118 | unpackedBytes = unpackedObject.Bytes; 119 | type = unpackedObject.Type; 120 | } 121 | else if (packObject.Type == 7) 122 | { 123 | throw new NotImplementedException(); 124 | } 125 | else 126 | { 127 | unpackedBytes = HashContent.Unpack(viewAccessor, packObject); 128 | } 129 | 130 | if (type == 1) 131 | return new Commit(hash, unpackedBytes); 132 | 133 | if (type == 2) 134 | return new Tree(hash, unpackedBytes); 135 | 136 | if (type == 3) 137 | return new Blob(hash, unpackedBytes); 138 | 139 | if (type == 4) 140 | return new Tag(hash, unpackedBytes); 141 | 142 | throw new NotImplementedException(); 143 | } 144 | 145 | public static (int Type, byte[] Bytes) RestoreDiffedObjectBytes(MemoryMappedViewAccessor memory, 146 | PackObject packObject) 147 | { 148 | var packDiff = new PackDiff(memory, packObject); 149 | 150 | packObject = ReadPackObject(memory, packObject.Offset - packDiff.NegativeOffset); 151 | 152 | while (packObject.Type == 6) 153 | { 154 | // OFS_DELTA 155 | var targetDiff = new PackDiff(memory, packObject); 156 | packDiff = packDiff.Combine(targetDiff); 157 | packObject = ReadPackObject(memory, packObject.Offset - packDiff.NegativeOffset); 158 | } 159 | 160 | var content = HashContent.Unpack(memory, packObject, 0); 161 | return (packObject.Type, packDiff.Apply(content)); 162 | } 163 | 164 | private static PackObject ReadPackObject(MemoryMappedViewAccessor file, long offset) 165 | { 166 | var readByte = file.ReadByte(offset); 167 | var bytesRead = 1; 168 | const byte typeMask = 0b01110000; 169 | var fsbSet = (readByte & 0b10000000) != 0; 170 | var type = (readByte & typeMask) >> 4; 171 | var dataSize = readByte & 0b00001111; 172 | var shift = 4; 173 | while (fsbSet) 174 | { 175 | readByte = file.ReadByte(offset + bytesRead++); 176 | fsbSet = (readByte & 0b10000000) != 0; 177 | dataSize |= (readByte & 0x7F) << shift; 178 | shift += 7; 179 | } 180 | 181 | return new PackObject(type, offset, bytesRead, dataSize); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /GitRewrite/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using GitRewrite.CleanupTask; 7 | using GitRewrite.CleanupTask.Delete; 8 | using GitRewrite.GitObjects; 9 | using GitRewrite.IO; 10 | using Commit = GitRewrite.GitObjects.Commit; 11 | using Tree = GitRewrite.GitObjects.Tree; 12 | 13 | namespace GitRewrite 14 | { 15 | public static class Program 16 | { 17 | static void Main(string[] args) 18 | { 19 | if (!CommandLineOptions.TryParse(args, out var options)) 20 | { 21 | return; 22 | } 23 | 24 | PackReader.InitializePackFiles(options.RepositoryPath); 25 | 26 | Console.OutputEncoding = Encoding.UTF8; 27 | 28 | if (options.FixTrees) 29 | { 30 | var defectiveCommits = FindCommitsWithDuplicateTreeEntries(options.RepositoryPath).ToList(); 31 | 32 | var rewrittenCommits = FixDefectiveCommits(options.RepositoryPath, defectiveCommits); 33 | if (rewrittenCommits.Any()) 34 | Refs.Update(options.RepositoryPath, rewrittenCommits); 35 | } 36 | else if (options.FilesToDelete.Any() || options.FoldersToDelete.Any()) 37 | { 38 | using (var task = new DeletionTask(options.RepositoryPath, options.FilesToDelete, options.FoldersToDelete, options.ProtectRefs)) 39 | task.Run(); 40 | } 41 | else if (options.RemoveEmptyCommits) 42 | { 43 | using (var removeEmptyCommitsTask = new RemoveEmptyCommitsTask(options.RepositoryPath)) 44 | removeEmptyCommitsTask.Run(); 45 | } 46 | else if (!string.IsNullOrWhiteSpace(options.ContributorMappingFile)) 47 | { 48 | using (var rewriteContributorTask = new RewriteContributorTask(options.RepositoryPath, options.ContributorMappingFile)) 49 | rewriteContributorTask.Run(); 50 | } 51 | else if (options.ListContributorNames) 52 | { 53 | foreach (var contributor in CommitWalker.CommitsRandomOrder(options.RepositoryPath) 54 | .SelectMany(commit => new[] {commit.GetAuthorName(), commit.GetCommitterName()}) 55 | .Distinct() 56 | .AsParallel() 57 | .OrderBy(x => x)) 58 | Console.WriteLine(contributor); 59 | } 60 | } 61 | 62 | public static ObjectHash WriteFixedTree(string vcsPath, Tree tree) 63 | { 64 | var resultingTreeLines = new List(); 65 | 66 | bool fixRequired = false; 67 | 68 | foreach (var treeLine in tree.Lines) 69 | { 70 | if (!treeLine.IsDirectory()) 71 | { 72 | resultingTreeLines.Add(treeLine); 73 | continue; 74 | } 75 | 76 | var childTree = GitObjectFactory.ReadTree(vcsPath, treeLine.Hash); 77 | var fixedTreeHash = WriteFixedTree(vcsPath, childTree); 78 | resultingTreeLines.Add(new Tree.TreeLine(treeLine.TextBytes, fixedTreeHash)); 79 | if (fixedTreeHash != childTree.Hash) 80 | fixRequired = true; 81 | } 82 | 83 | if (fixRequired || Tree.HasDuplicateLines(resultingTreeLines)) 84 | { 85 | tree = Tree.GetFixedTree(resultingTreeLines); 86 | HashContent.WriteObject(vcsPath, tree); 87 | } 88 | 89 | return tree.Hash; 90 | } 91 | 92 | private static bool HasDefectiveTree(string vcsPath, Commit commit) 93 | { 94 | if (SeenTrees.TryGetValue(commit.TreeHash, out bool isDefective)) 95 | return isDefective; 96 | 97 | var tree = GitObjectFactory.ReadTree(vcsPath, commit.TreeHash); 98 | return IsDefectiveTree(vcsPath, tree); 99 | } 100 | 101 | private static readonly ConcurrentDictionary SeenTrees = new ConcurrentDictionary(); 102 | 103 | public static bool IsDefectiveTree(string vcsPath, Tree tree) 104 | { 105 | if (SeenTrees.TryGetValue(tree.Hash, out bool isDefective)) 106 | return isDefective; 107 | 108 | if (Tree.HasDuplicateLines(tree.Lines)) 109 | { 110 | SeenTrees.TryAdd(tree.Hash, true); 111 | return true; 112 | } 113 | 114 | var childTrees = tree.GetDirectories(); 115 | foreach (var childTree in childTrees) 116 | { 117 | if (SeenTrees.TryGetValue(childTree.Hash, out isDefective)) 118 | { 119 | if (isDefective) 120 | return true; 121 | 122 | continue; 123 | } 124 | 125 | var childTreeObject = (Tree) GitObjectFactory.ReadGitObject(vcsPath, childTree.Hash); 126 | if (IsDefectiveTree(vcsPath, childTreeObject)) 127 | { 128 | return true; 129 | } 130 | } 131 | 132 | SeenTrees.TryAdd(tree.Hash, false); 133 | return false; 134 | } 135 | 136 | private static IEnumerable CorrectParents(IEnumerable oldParents, Dictionary rewrittenCommitHashes) 137 | { 138 | foreach (var oldParentHash in oldParents) 139 | { 140 | if (rewrittenCommitHashes.TryGetValue(oldParentHash, out var newParentHash)) 141 | yield return newParentHash; 142 | else 143 | yield return oldParentHash; 144 | } 145 | } 146 | 147 | static IEnumerable FindCommitsWithDuplicateTreeEntries(string vcsPath) 148 | { 149 | foreach (var commit in CommitWalker 150 | .CommitsRandomOrder(vcsPath) 151 | .AsParallel() 152 | .AsUnordered() 153 | .Select(commit => (commit.Hash, Defective: HasDefectiveTree(vcsPath, commit)))) 154 | { 155 | if (commit.Defective) 156 | yield return commit.Hash; 157 | } 158 | } 159 | 160 | static Dictionary FixDefectiveCommits(string vcsPath, List defectiveCommits) 161 | { 162 | var rewrittenCommitHashes = new Dictionary(); 163 | 164 | foreach (var commit in CommitWalker.CommitsInOrder(vcsPath)) 165 | { 166 | if (rewrittenCommitHashes.ContainsKey(commit.Hash)) 167 | continue; 168 | 169 | // Rewrite this commit 170 | byte[] newCommitBytes; 171 | if (defectiveCommits.Contains(commit.Hash)) 172 | { 173 | var fixedTreeHash = WriteFixedTree(vcsPath, GitObjectFactory.ReadTree(vcsPath, commit.TreeHash)); 174 | newCommitBytes = Commit.GetSerializedCommitWithChangedTreeAndParents(commit, fixedTreeHash, 175 | CorrectParents(commit.Parents, rewrittenCommitHashes).ToList()); 176 | } 177 | else 178 | { 179 | newCommitBytes = Commit.GetSerializedCommitWithChangedTreeAndParents(commit, commit.TreeHash, 180 | CorrectParents(commit.Parents, rewrittenCommitHashes).ToList()); 181 | } 182 | 183 | var fileObjectBytes = GitObjectFactory.GetBytesWithHeader(GitObjectType.Commit, newCommitBytes); 184 | var newCommitHash = new ObjectHash(Hash.Create(fileObjectBytes)); 185 | if (newCommitHash != commit.Hash && !rewrittenCommitHashes.ContainsKey(commit.Hash)) 186 | { 187 | HashContent.WriteFile(vcsPath, fileObjectBytes, newCommitHash.ToString()); 188 | rewrittenCommitHashes.Add(commit.Hash, newCommitHash); 189 | } 190 | } 191 | 192 | return rewrittenCommitHashes; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /GitRewrite/Ref.cs: -------------------------------------------------------------------------------- 1 | namespace GitRewrite 2 | { 3 | public class Ref 4 | { 5 | public readonly string Hash; 6 | public readonly string Name; 7 | 8 | public Ref(string hash, string name) 9 | { 10 | Hash = hash; 11 | Name = name; 12 | } 13 | } 14 | 15 | public class TagRef : Ref 16 | { 17 | public readonly string CommitHash; 18 | 19 | public TagRef(string hash, string name, string commitHash) 20 | : base(hash, name) 21 | { 22 | CommitHash = commitHash; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /GitRewrite/Refs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using GitRewrite.GitObjects; 6 | using GitRewrite.IO; 7 | 8 | namespace GitRewrite 9 | { 10 | public static class Refs 11 | { 12 | public static IEnumerable ReadAll(string basePath) 13 | { 14 | var packedRefsPath = Path.Combine(basePath, "packed-refs"); 15 | 16 | var packedRefsLines = !File.Exists(packedRefsPath) 17 | ? Enumerable.Empty() 18 | : GetPackedRefs(File.ReadLines(packedRefsPath)); 19 | 20 | var refs = GetRefs(basePath, "refs").ToList(); 21 | 22 | var refNames = new HashSet(); 23 | foreach (var @ref in refs) 24 | { 25 | refNames.Add(@ref.Name); 26 | } 27 | 28 | return packedRefsLines.Where(x => !refNames.Contains(x.Name)).Union(refs); 29 | } 30 | 31 | private static IEnumerable GetRefs(string basePath, string currentPath) 32 | { 33 | var fullPath = Path.Combine(basePath, currentPath); 34 | foreach (var directory in Directory.GetDirectories(fullPath)) 35 | { 36 | foreach (var @ref in GetRefs(basePath, currentPath + $"/{Path.GetFileName(directory)}")) 37 | { 38 | if (!@ref.Hash.StartsWith("ref: ")) 39 | yield return @ref; 40 | } 41 | } 42 | 43 | foreach (var file in Directory.GetFiles(fullPath)) 44 | { 45 | var hash = File.ReadAllText(file).TrimEnd('\n'); 46 | var name = currentPath + "/" + Path.GetFileName(file); 47 | 48 | yield return new Ref(hash, name); 49 | } 50 | } 51 | 52 | private static IEnumerable GetPackedRefs(IEnumerable lines) 53 | { 54 | using (var enumerator = lines.GetEnumerator()) 55 | { 56 | if (!enumerator.MoveNext()) 57 | yield break; 58 | 59 | var previousLine = enumerator.Current; 60 | var lineStarted = !previousLine.StartsWith("#"); 61 | 62 | while (enumerator.MoveNext()) 63 | { 64 | var currentLine = enumerator.Current; 65 | if (currentLine.StartsWith('^')) 66 | { 67 | yield return new TagRef(previousLine.Substring(0, 40), previousLine.Substring(41), currentLine.Substring(1)); 68 | lineStarted = false; 69 | } 70 | else 71 | { 72 | if (lineStarted) 73 | yield return new Ref(previousLine.Substring(0, 40), previousLine.Substring(41)); 74 | previousLine = currentLine; 75 | lineStarted = !currentLine.StartsWith("#"); 76 | } 77 | } 78 | 79 | if (lineStarted) 80 | yield return new Ref(previousLine.Substring(0, 40), previousLine.Substring(41)); 81 | } 82 | } 83 | 84 | public static void Update(string basePath, Dictionary rewrittenCommits) 85 | { 86 | foreach (var @ref in GetRefs(basePath, "refs")) 87 | { 88 | RewriteRef(basePath, @ref.Hash, @ref.Name, rewrittenCommits); 89 | } 90 | 91 | // Rewrite refs as loose objects and delete packed-refs 92 | var packedRefsPath = Path.Combine(basePath, "packed-refs"); 93 | if (!File.Exists(packedRefsPath)) 94 | return; 95 | 96 | var content = File.ReadAllLines(packedRefsPath); 97 | 98 | foreach (var line in content) 99 | { 100 | if (line[0] == '#' || line[0] == '^') 101 | continue; 102 | 103 | RewriteRef(basePath, line.Substring(0, 40), line.Substring(41), rewrittenCommits); 104 | } 105 | 106 | File.Delete(packedRefsPath); 107 | } 108 | 109 | private static ObjectHash GetRewrittenCommitHash(ObjectHash hash, Dictionary rewrittenCommits) 110 | { 111 | if (rewrittenCommits.TryGetValue(hash, out var rewrittenHash)) 112 | { 113 | var updatedRef = rewrittenHash; 114 | while (rewrittenCommits.TryGetValue(rewrittenHash, out rewrittenHash)) 115 | updatedRef = rewrittenHash; 116 | 117 | return updatedRef; 118 | } 119 | 120 | return hash; 121 | } 122 | 123 | private static ObjectHash RewriteRef(string vcsPath, string hash, string refName, Dictionary rewrittenCommits) 124 | { 125 | var gitObject = GitObjectFactory.ReadGitObject(vcsPath, new ObjectHash(hash)); 126 | if (gitObject.Type == GitObjectType.Commit) 127 | { 128 | var path = Path.Combine(vcsPath, refName); 129 | var correctedHash = GetRewrittenCommitHash(new ObjectHash(hash), rewrittenCommits); 130 | Directory.CreateDirectory(Path.GetDirectoryName(path)); 131 | File.WriteAllText(path, correctedHash.ToString()); 132 | return correctedHash; 133 | } 134 | 135 | if (gitObject.Type == GitObjectType.Tag) 136 | { 137 | var tag = (Tag) gitObject; 138 | 139 | if (tag.PointsToTree) 140 | { 141 | // Do not touch tags pointing to trees right now as this is not properly implemented yet 142 | return tag.Hash; 143 | } 144 | 145 | var rewrittenObjectHash = tag.PointsToTag 146 | ? RewriteRef(vcsPath, tag.Object, "", rewrittenCommits) 147 | : GetRewrittenCommitHash(new ObjectHash(tag.Object), rewrittenCommits); 148 | 149 | // points to commit 150 | var rewrittenTag = tag.WithNewObject(rewrittenObjectHash.ToString()); 151 | HashContent.WriteObject(vcsPath, rewrittenTag); 152 | 153 | var path = Path.Combine(vcsPath, "refs/tags", rewrittenTag.TagName); 154 | File.WriteAllText(path, rewrittenTag.Hash.ToString()); 155 | 156 | return rewrittenTag.Hash; 157 | } 158 | 159 | throw new NotImplementedException(); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitRewrite 2 | > **Note** 3 | > This project is deprecated in favor of its [rewrite in Rust](https://github.com/heinrichti/gitrw). It will not receive new features or bug fixes. 4 | 5 | Rewrite git history. 6 | 7 | Faster alternative to git filter-branch or bfg-repo-cleaner to perform certain rewrite tasks. 8 | It was tested on windows and linux. 9 | 10 | With this tool the repository can be rewritten in a few different ways, like removing deleting files and folders, 11 | removing empty commits or rewriting committer and author information. 12 | 13 | Docker images are available here: https://hub.docker.com/r/lightraven/git-rewrite 14 | 15 | [![Build status](https://ci.appveyor.com/api/projects/status/gqdtitbjcd3mquta?svg=true)](https://ci.appveyor.com/project/TimHeinrich/gitrewrite) 16 | 17 | ## Important notice 18 | This tool will rewrite the git history and therefore change many, if not all, commit hashes. 19 | It will also unsign signed commits. 20 | Only use it if you fully understand the implications of this! 21 | 22 | ## Usage 23 | ### Deleting files 24 | ```cmd 25 | GitRewrite C:/VCS/MyRepo -d file1,file2,file3 26 | GitRewrite C:/VCS/MyRepo --delete-files file1,file2,file3 27 | GitRewrite C:/VCS/MyRepo -d file1,file2,file3 --protect-refs 28 | GitRewrite C:/VCS/MyRepo --delete-files file1,file2,file3 --protect-refs 29 | ``` 30 | Deleting should be pretty fast, especially when specifying the whole path to the file. 31 | Simple wildcards for the beginning and the end of the filename are supported, like *.zip. 32 | It also lets you specify the complete path to the file instead of only a file name. 33 | For this the path has to be prefixed by a forward slash and the path seperator also is a forward slash: /path/to/file.txt 34 | Specifying only files with complete path will result in much better performance as not all subtrees have to be checked. 35 | 36 | If the goal is to delete files but keep them in all refs (branches and tags) use the --protect-refs flag. 37 | With this flag GitRewrite will not touch files in a commit a ref points to. 38 | 39 | ### Deleting directories 40 | ``` 41 | GitRewrite -D folder1,folder2,folder3 42 | GitRewrite --delete-directories folder1,folder2,folder3 43 | GitRewrite -D folder1,folder2,folder3 --protect-refs 44 | GitRewrite --delete-directories folder1,folder2,folder3 --protect-refs 45 | ``` 46 | Patterns and performance characteristics are the same as for deleting files. Can be used in conjunction with -d. 47 | 48 | ### Remove empty commits 49 | Another useful feature is to remove empty commits. 50 | For this tool empty commits are defined as commits that have only a single parent and the same tree as their parent. 51 | With git filter-branch this takes days for huge repositories, with GitRewrite it should only be a matter of seconds to minutes. 52 | ``` 53 | GitRewrite C:/VCS/MyRepo -e 54 | ``` 55 | This should performa really fast as each commit has to be read only once and written if a parent has changed. 56 | 57 | ### Rewrite trees with duplicate entries 58 | The main motivation for this tool was a repository where git gc complained about trees having duplicate entries. 59 | GitRewrite solves this problem by rewriting the trees by removing the duplicates, then rewriting all parent trees, commit and all following commits. 60 | ``` 61 | GitRewrite C:/VCS/MyRepo --fix-trees 62 | ``` 63 | 64 | ### List contributor names 65 | Lists all authors and committers. 66 | ``` 67 | GitRewrite C:/VCS/MyRepo --contributor-names 68 | ``` 69 | 70 | ### Rewrite all contributor names 71 | ``` 72 | GitRewrite C:/VCS/MyRepo --rewrite-contributors [contributors.txt] 73 | ``` 74 | Rewrites authors and committers. 75 | The contributors.txt is the mapping from old contributor name to new contributor name: 76 | Old User \ = New User \ 77 | 78 | ### General 79 | The different actions can only be performed one at a time, for example it is not possible to mix -e and -d. 80 | 81 | ## Cleanup 82 | After a GitRewrite run files are not actually deleted from the file system. To do this you should run 83 | ``` 84 | git reflog expire --expire=now --all && git gc --aggressive 85 | ``` 86 | Instead of git gc --aggressive you might want to use something faster like git gc --prune=now, while the result may not be as good. 87 | 88 | ## Important notes 89 | GitRewrite was tested only on a few repository, so there is a big chance that it might fail for you. 90 | Please let me know of any issues or feature requests, I will update the tool when I find the time for it. 91 | Pull requests very welcome! Still searching for a way to make this even faster, maybe some parallelization options that I have not employed yet or faster file acces (while this should be pretty efficient already using memory mapped files) 92 | 93 | ## Build instructions 94 | Currently we are building with .NET 7, so the SDK should be installed. 95 | ``` 96 | git clone https://github.com/TimHeinrich/GitRewrite.git 97 | cd GitRewrite 98 | dotnet publish -c Release 99 | ``` 100 | 101 | ## Icon attribution 102 | disconnect by Dmitry Baranovskiy from the Noun Project 103 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Tim Heinrich 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | --------------------------------------------------------------------------------