From 4f81045680620a2475b14193f63712120e066580 Mon Sep 17 00:00:00 2001 From: Smaug123 Date: Sat, 2 May 2020 22:15:40 +0100 Subject: [PATCH] Trees --- Git.Test/Git.Test.fsproj | 1 + Git.Test/TestBlob.fs | 5 +- Git.Test/TestFromGitBook.fs | 222 ++++++++++++++++++++++++++++++++++++ Git.Test/TestInit.fs | 2 +- Git.Test/TestTree.fs | 1 + Git/EncodedObject.fs | 18 +-- Git/Git.fsproj | 2 + Git/Repository.fs | 7 +- 8 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 Git.Test/TestFromGitBook.fs diff --git a/Git.Test/Git.Test.fsproj b/Git.Test/Git.Test.fsproj index 9eeba33..b061d5d 100644 --- a/Git.Test/Git.Test.fsproj +++ b/Git.Test/Git.Test.fsproj @@ -20,6 +20,7 @@ + diff --git a/Git.Test/TestBlob.fs b/Git.Test/TestBlob.fs index 3054aeb..7c4d680 100644 --- a/Git.Test/TestBlob.fs +++ b/Git.Test/TestBlob.fs @@ -9,7 +9,7 @@ open System.IO.Abstractions.TestingHelpers [] module TestObject = [] - let hashFromDocs () = + let ``Commit hash from Git Book`` () = let t = "what is up, doc?".ToCharArray () |> Array.map byte Object.Blob t @@ -19,7 +19,7 @@ module TestObject = |> shouldEqual "bd9dbf5aae1a3862dd1526723246b20206e5fc37" [] - let writeFromDocs () = + let ``Write the commit hash to a file`` () = let t = "what is up, doc?".ToCharArray () |> Array.map byte let b = Object.Blob t @@ -34,6 +34,7 @@ module TestObject = b |> EncodedObject.write repo + |> ignore let backIn = EncodedObject.catFile repo (EncodedObject.hash b) diff --git a/Git.Test/TestFromGitBook.fs b/Git.Test/TestFromGitBook.fs new file mode 100644 index 0000000..be20345 --- /dev/null +++ b/Git.Test/TestFromGitBook.fs @@ -0,0 +1,222 @@ +namespace Git.Test + +open System +open System.IO +open System.IO.Abstractions.TestingHelpers +open NUnit.Framework +open FsUnitTyped + +open Git + +[] +module TestFromGitBook = + + [] + let ``Test ch 10.2 up to Commit`` () = + let fs = MockFileSystem () + let dir = fs.Path.GetTempFileName () + let versionDir = fs.DirectoryInfo.FromDirectoryName (dir + "_test") + versionDir.Create() + + let repo = match Repository.init versionDir with | Ok r -> r | Error e -> failwithf "Oh no: %+A" e + + // Directory structure is correct: + let objectsDir = fs.Path.Combine (Repository.gitDir(repo).FullName, "objects") |> fs.DirectoryInfo.FromDirectoryName + objectsDir.EnumerateDirectories () + |> Seq.map (fun d -> d.Name) + |> Seq.toList + |> List.sort + |> shouldEqual [ + "info" + "pack" + ] + objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) + |> shouldBeEmpty + + // Write our first object + let h = + "test content\n".ToCharArray () + |> Array.map byte + |> Object.Blob + |> EncodedObject.encode + |> EncodedObject.write repo + h + |> shouldEqual (Hash.ofString "d670460b4b4aece5915caf5c68d12f560a9fe3e4") + + // Check that it's appeared + objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) + |> Seq.map (fun f -> f.Directory.Name, f.Name) + |> Seq.exactlyOne + |> shouldEqual ("d6", "70460b4b4aece5915caf5c68d12f560a9fe3e4") + + // Read it back in + match EncodedObject.catFile repo h |> EncodedObject.decode with + | Object.Blob b -> + b + |> Array.map char + |> String + |> shouldEqual "test content\n" + | s -> failwithf "Oh no: +%A" s + + // Version control + // TODO - add helper methods for dealing with file contents + let h1 = + "version 1\n".ToCharArray () + |> Array.map byte + |> Object.Blob + |> EncodedObject.encode + |> EncodedObject.write repo + h1 + |> shouldEqual (Hash.ofString "83baae61804e65cc73a7201a7252750c76066a30") + + let h2 = + "version 2\n".ToCharArray () + |> Array.map byte + |> Object.Blob + |> EncodedObject.encode + |> EncodedObject.write repo + h2 + |> shouldEqual (Hash.ofString "1f7a7a472abf3dd9643fd615f6da379c4acb3e3a") + + objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) + |> Seq.map (fun f -> f.Directory.Name, f.Name) + |> Seq.toList + |> List.sort + |> shouldEqual [ + "1f", "7a7a472abf3dd9643fd615f6da379c4acb3e3a" + "83", "baae61804e65cc73a7201a7252750c76066a30" + "d6", "70460b4b4aece5915caf5c68d12f560a9fe3e4" + ] + + match EncodedObject.catFile repo h1 |> EncodedObject.decode with + | Object.Blob b -> + b + |> Array.map char + |> String + |> shouldEqual "version 1\n" + | s -> failwithf "Oh no: +%A" s + + match EncodedObject.catFile repo h2 |> EncodedObject.decode with + | Object.Blob b -> + b + |> Array.map char + |> String + |> shouldEqual "version 2\n" + | s -> failwithf "Oh no: +%A" s + + // TODO - implement the staging area and then test it here + // Add to the tree + let tree1 = + [ + { + Mode = 100644 + Name = "test.txt" + Hash = h1 + } + ] + |> Object.Tree + |> EncodedObject.encode + |> EncodedObject.write repo + tree1 |> shouldEqual (Hash.ofString "d8329fc1cc938780ffdd9f94e0d364e0ea74f579") + + match EncodedObject.catFile repo tree1 |> EncodedObject.decode with + | Object.Tree t -> + t + |> List.exactlyOne + |> shouldEqual { + Mode = 100644 + Name = "test.txt" + Hash = h1 + } + | s -> failwithf "Oh no: +%A" s + + let newHash = + "new file\n".ToCharArray () + |> Array.map byte + |> Object.Blob + |> EncodedObject.encode + |> EncodedObject.write repo + newHash |> shouldEqual (Hash.ofString "fa49b077972391ad58037050f2a75f74e3671e92") + + let tree2 = + [ + { + Mode = 100644 + Name = "new.txt" + Hash = newHash + } + { + Mode = 100644 + Name = "test.txt" + Hash = h2 + } + ] + |> Object.Tree + |> EncodedObject.encode + |> EncodedObject.write repo + tree2 |> shouldEqual (Hash.ofString "0155eb4229851634a0f03eb265b69f5a2d56f341") + + match EncodedObject.catFile repo tree2 |> EncodedObject.decode with + | Object.Tree t -> + t + |> shouldEqual [ + { + Mode = 100644 + Name = "new.txt" + Hash = newHash + } + { + Mode = 100644 + Name = "test.txt" + Hash = h2 + } + ] + | s -> failwithf "Oh no: +%A" s + + // and the prefix one + let tree3 = + [ + { + Mode = 40000 + Name = "bak" + Hash = tree1 + } + { + Mode = 100644 + Name = "new.txt" + Hash = newHash + } + { + Mode = 100644 + Name = "test.txt" + Hash = h2 + } + ] + |> Object.Tree + |> EncodedObject.encode + |> EncodedObject.write repo + tree3 |> shouldEqual (Hash.ofString "3c4e9cd789d88d8d89c1073707c3585e41b0e614") + + match EncodedObject.catFile repo tree3 |> EncodedObject.decode with + | Object.Tree t -> + t + |> shouldEqual [ + { + Mode = 40000 + Name = "bak" + Hash = tree1 + } + { + Mode = 100644 + Name = "new.txt" + Hash = newHash + } + { + Mode = 100644 + Name = "test.txt" + Hash = h2 + } + ] + | s -> failwithf "Oh no: +%A" s + + // TODO: the section on commits diff --git a/Git.Test/TestInit.fs b/Git.Test/TestInit.fs index 44d9eb6..cb32a15 100644 --- a/Git.Test/TestInit.fs +++ b/Git.Test/TestInit.fs @@ -10,7 +10,7 @@ open Git module TestInit = [] - let Test1 () = + let ``test initialisation`` () = let fs = MockFileSystem () let dir = fs.Path.GetTempFileName () let gitDir = fs.DirectoryInfo.FromDirectoryName (dir + "_test") diff --git a/Git.Test/TestTree.fs b/Git.Test/TestTree.fs index 59c66de..ff4adb0 100644 --- a/Git.Test/TestTree.fs +++ b/Git.Test/TestTree.fs @@ -52,6 +52,7 @@ module TestTree = b |> EncodedObject.write repo + |> ignore let backIn = EncodedObject.catFile repo (EncodedObject.hash b) diff --git a/Git/EncodedObject.fs b/Git/EncodedObject.fs index 4f295bf..756d894 100644 --- a/Git/EncodedObject.fs +++ b/Git/EncodedObject.fs @@ -1,9 +1,8 @@ namespace Git -open System open System.IO open System.Security.Cryptography -open System.IO.Compression +open Ionic.Zlib type EncodedObject = { @@ -51,7 +50,7 @@ module EncodedObject = |> Array.concat use ms = new MemoryStream(toWrite) - use ds = new DeflateStream(dest, CompressionMode.Compress) + use ds = new Ionic.Zlib.ZlibStream(dest, CompressionMode.Compress) ms.CopyTo ds /// Read the header of the stream seeked to the beginning of the content. @@ -73,7 +72,7 @@ module EncodedObject = let private uncompress (s : Stream) : EncodedObject = use ms = new MemoryStream () - use ds = new DeflateStream(s, CompressionMode.Decompress) + use ds = new Ionic.Zlib.ZlibStream(s, CompressionMode.Decompress) ds.CopyTo ms ms.Seek(0L, SeekOrigin.Begin) |> ignore @@ -91,16 +90,19 @@ module EncodedObject = if r.PeekChar () <> -1 then failwith "unexpectedly not at end" result - let write (r : Repository) (o : EncodedObject) : unit = - let hash = hash o |> Hash.toString - let objectName = hash.[2..] - let subdir = hash.[0..1] + let write (r : Repository) (o : EncodedObject) : Hash = + let hash = hash o + let hashStr = Hash.toString hash + let objectName = hashStr.[2..] + let subdir = hashStr.[0..1] let d = Repository.createSubdir (Repository.objectDir r) subdir use filestream = r.Fs.File.Create (r.Fs.Path.Combine (d.FullName, objectName)) compress o filestream + hash + let catFile (r : Repository) (hash : Hash) : EncodedObject = let hash = hash |> Hash.toString let objectName = hash.[2..] diff --git a/Git/Git.fsproj b/Git/Git.fsproj index 35797b3..51ab3c1 100644 --- a/Git/Git.fsproj +++ b/Git/Git.fsproj @@ -16,6 +16,8 @@ + + diff --git a/Git/Repository.fs b/Git/Repository.fs index c2a2697..12e6970 100644 --- a/Git/Repository.fs +++ b/Git/Repository.fs @@ -16,7 +16,7 @@ type InitFailure = [] module Repository = - let internal gitDir (r : Repository) : IDirectoryInfo = + let gitDir (r : Repository) : IDirectoryInfo = r.Fs.Path.Combine(r.Directory.FullName, ".git") |> r.Fs.DirectoryInfo.FromDirectoryName let internal objectDir (r : Repository) : IDirectoryInfo = @@ -29,6 +29,11 @@ module Repository = output.Create () output + let make (dir : IDirectoryInfo) : Repository option = + if dir.Exists && dir.EnumerateDirectories () |> Seq.map (fun i -> i.Name) |> Seq.contains ".git" then + Some { Directory = dir } + else None + let init (dir : IDirectoryInfo) : Result = if not dir.Exists then Error DirectoryDoesNotExist elif not <| Seq.isEmpty (dir.EnumerateDirectories ".git") then Error AlreadyGit