mirror of
https://github.com/Smaug123/managed-git
synced 2025-10-13 03:38:43 +00:00
Ref disambiguation, and barebones Log (doesn't correspond to any of git's logs in order)
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FsCheck" Version="2.14.2" />
|
||||||
<PackageReference Include="FsUnit" Version="3.8.1" />
|
<PackageReference Include="FsUnit" Version="3.8.1" />
|
||||||
<PackageReference Include="nunit" Version="3.12.0" />
|
<PackageReference Include="nunit" Version="3.12.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||||
@@ -21,7 +22,10 @@
|
|||||||
<Compile Include="TestBlob.fs" />
|
<Compile Include="TestBlob.fs" />
|
||||||
<Compile Include="TestTree.fs" />
|
<Compile Include="TestTree.fs" />
|
||||||
<Compile Include="TestFromGitBook.fs" />
|
<Compile Include="TestFromGitBook.fs" />
|
||||||
|
<Compile Include="Utils.fs" />
|
||||||
<Compile Include="TestCommit.fs" />
|
<Compile Include="TestCommit.fs" />
|
||||||
|
<Compile Include="TestObject.fs" />
|
||||||
|
<Compile Include="TestLog.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -7,7 +7,7 @@ open System
|
|||||||
open System.IO.Abstractions.TestingHelpers
|
open System.IO.Abstractions.TestingHelpers
|
||||||
|
|
||||||
[<TestFixture>]
|
[<TestFixture>]
|
||||||
module TestObject =
|
module TestBlob =
|
||||||
[<Test>]
|
[<Test>]
|
||||||
let ``Commit hash from Git Book`` () =
|
let ``Commit hash from Git Book`` () =
|
||||||
let t = "what is up, doc?".ToCharArray () |> Array.map byte
|
let t = "what is up, doc?".ToCharArray () |> Array.map byte
|
||||||
|
@@ -13,7 +13,7 @@ open Git
|
|||||||
module TestFromGitBook =
|
module TestFromGitBook =
|
||||||
|
|
||||||
[<Test>]
|
[<Test>]
|
||||||
let ``Test ch 10.2 up to Commit`` () =
|
let ``Test ch 10.2, 10.3`` () =
|
||||||
let fs = MockFileSystem ()
|
let fs = MockFileSystem ()
|
||||||
let dir = fs.Path.GetTempFileName ()
|
let dir = fs.Path.GetTempFileName ()
|
||||||
let versionDir = fs.DirectoryInfo.FromDirectoryName (dir + "_test")
|
let versionDir = fs.DirectoryInfo.FromDirectoryName (dir + "_test")
|
||||||
@@ -312,3 +312,21 @@ module TestFromGitBook =
|
|||||||
"d8", "329fc1cc938780ffdd9f94e0d364e0ea74f579" // tree 1
|
"d8", "329fc1cc938780ffdd9f94e0d364e0ea74f579" // tree 1
|
||||||
"fa", "49b077972391ad58037050f2a75f74e3671e92" // new.txt
|
"fa", "49b077972391ad58037050f2a75f74e3671e92" // new.txt
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// References
|
||||||
|
|
||||||
|
let refsDir = fs.Path.Combine (Repository.gitDir(repo).FullName, "refs") |> fs.DirectoryInfo.FromDirectoryName
|
||||||
|
refsDir.EnumerateDirectories ("*", SearchOption.AllDirectories)
|
||||||
|
|> Seq.map (fun i -> i.Name)
|
||||||
|
|> Seq.toList
|
||||||
|
|> List.sort
|
||||||
|
|> shouldEqual [
|
||||||
|
"heads"
|
||||||
|
"tags"
|
||||||
|
]
|
||||||
|
|
||||||
|
Reference.write repo c3Hash "master"
|
||||||
|
|> shouldEqual { Was = None ; Now = c3Hash }
|
||||||
|
|
||||||
|
Reference.write repo c2Hash "test"
|
||||||
|
|> shouldEqual { Was = None ; Now = c2Hash }
|
||||||
|
26
Git.Test/TestLog.fs
Normal file
26
Git.Test/TestLog.fs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
namespace Git.Test
|
||||||
|
|
||||||
|
open System.IO.Abstractions.TestingHelpers
|
||||||
|
open NUnit.Framework
|
||||||
|
open FsUnitTyped
|
||||||
|
open Git
|
||||||
|
open Git.Commands
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestLog =
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Log can be taken`` () =
|
||||||
|
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
|
||||||
|
|
||||||
|
let commits = Utils.gitBookSetup repo
|
||||||
|
|
||||||
|
// Test the log
|
||||||
|
Hash.ofString "95cce637b4e889eee8042515db402128bd62c0d2"
|
||||||
|
|> Log.log repo
|
||||||
|
|> shouldEqual commits
|
77
Git.Test/TestObject.fs
Normal file
77
Git.Test/TestObject.fs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
namespace Git.Test
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Collections.Generic
|
||||||
|
open System.IO.Abstractions.TestingHelpers
|
||||||
|
open NUnit.Framework
|
||||||
|
open FsUnitTyped
|
||||||
|
open FsCheck
|
||||||
|
open Git
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestObject =
|
||||||
|
|
||||||
|
let private intToChar (i : int) (upper : bool) : char =
|
||||||
|
if i < 10 then (byte i + byte '0') else (byte i - 10uy + byte (if upper then 'A' else 'a'))
|
||||||
|
|> char
|
||||||
|
|
||||||
|
let hashPrefixGenerator (len : byte) =
|
||||||
|
gen {
|
||||||
|
let! n = Gen.choose (0, int len)
|
||||||
|
let! c = Gen.listOfLength n (Gen.zip (Gen.choose (0, 15)) (Gen.choose (0, 1) |> Gen.map (fun i -> i = 0)))
|
||||||
|
let ans = c |> List.map (fun (i, u) -> intToChar i u) |> Array.ofList
|
||||||
|
return String ans
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefixesOf (s : string) : Gen<string> =
|
||||||
|
Gen.choose (0, s.Length)
|
||||||
|
|> Gen.map (fun i -> s.Substring(0, i))
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``prefixesOf generates prefixes`` () =
|
||||||
|
let property (s1 : string, pref : string) =
|
||||||
|
s1.StartsWith pref
|
||||||
|
|
||||||
|
let gen =
|
||||||
|
gen {
|
||||||
|
let! s = Arb.Default.String().Generator |> Gen.filter (fun i -> not <| Object.ReferenceEquals (i, null))
|
||||||
|
let! pref = prefixesOf s
|
||||||
|
return (s, pref)
|
||||||
|
}
|
||||||
|
|
||||||
|
property
|
||||||
|
|> Prop.forAll (Arb.fromGen gen)
|
||||||
|
|> Check.QuickThrowOnFailure
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can look up a partial hash`` () =
|
||||||
|
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
|
||||||
|
|
||||||
|
let h =
|
||||||
|
"test content\n".ToCharArray ()
|
||||||
|
|> Array.map byte
|
||||||
|
|> Object.Blob
|
||||||
|
|> EncodedObject.encode
|
||||||
|
|> EncodedObject.write repo
|
||||||
|
let expected = "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
|
||||||
|
let expectedHash = Hash.ofString expected
|
||||||
|
h
|
||||||
|
|> shouldEqual (Hash.ofString expected)
|
||||||
|
|
||||||
|
let property (prefix : string) : bool =
|
||||||
|
if expected.StartsWith prefix then
|
||||||
|
Object.disambiguate repo prefix = [expectedHash]
|
||||||
|
else
|
||||||
|
Object.disambiguate repo prefix = []
|
||||||
|
|
||||||
|
property
|
||||||
|
|> Prop.forAll (Arb.fromGen (hashPrefixGenerator 40uy))
|
||||||
|
|> Check.QuickThrowOnFailure
|
||||||
|
property
|
||||||
|
|> Prop.forAll (Arb.fromGen (prefixesOf expected))
|
||||||
|
|> Check.QuickThrowOnFailure
|
159
Git.Test/Utils.fs
Normal file
159
Git.Test/Utils.fs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
namespace Git.Test
|
||||||
|
|
||||||
|
open Git
|
||||||
|
open FsUnitTyped
|
||||||
|
open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Utils =
|
||||||
|
|
||||||
|
let gitBookSetup (repo : Repository) : Map<Hash, CommitEntry> =
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
let scott =
|
||||||
|
{
|
||||||
|
Name = "Scott Chacon"
|
||||||
|
Email = "schacon@gmail.com"
|
||||||
|
DateTimezone = "-0700"
|
||||||
|
Date = 1243040974<second>
|
||||||
|
}
|
||||||
|
|
||||||
|
let commit1 =
|
||||||
|
{
|
||||||
|
Committer = scott
|
||||||
|
Author = scott
|
||||||
|
CommitMessage = "First commit\n"
|
||||||
|
Parents = []
|
||||||
|
Tree = tree1
|
||||||
|
}
|
||||||
|
|> Object.Commit
|
||||||
|
|
||||||
|
let c1Hash =
|
||||||
|
commit1
|
||||||
|
|> EncodedObject.encode
|
||||||
|
|> EncodedObject.write repo
|
||||||
|
c1Hash
|
||||||
|
|> Hash.toString
|
||||||
|
|> shouldEqual "70d4408b5020e81d19906d6abdd87a73233ebf34"
|
||||||
|
|
||||||
|
let commit2 =
|
||||||
|
{
|
||||||
|
Committer = scott
|
||||||
|
Author = scott
|
||||||
|
CommitMessage = "Second commit\n"
|
||||||
|
Parents = [c1Hash]
|
||||||
|
Tree = tree2
|
||||||
|
}
|
||||||
|
|> Object.Commit
|
||||||
|
let c2Hash =
|
||||||
|
commit2
|
||||||
|
|> EncodedObject.encode
|
||||||
|
|> EncodedObject.write repo
|
||||||
|
c2Hash
|
||||||
|
|> Hash.toString
|
||||||
|
|> shouldEqual "1513b13a72f5277252cfce4ed0eda0620aca2f6a"
|
||||||
|
|
||||||
|
let commit3 =
|
||||||
|
{
|
||||||
|
Committer = scott
|
||||||
|
Author = scott
|
||||||
|
CommitMessage = "Third commit\n"
|
||||||
|
Parents = [c2Hash]
|
||||||
|
Tree = tree3
|
||||||
|
}
|
||||||
|
|> Object.Commit
|
||||||
|
let c3Hash =
|
||||||
|
commit3
|
||||||
|
|> EncodedObject.encode
|
||||||
|
|> EncodedObject.write repo
|
||||||
|
c3Hash
|
||||||
|
|> Hash.toString
|
||||||
|
|> shouldEqual "95cce637b4e889eee8042515db402128bd62c0d2"
|
||||||
|
|
||||||
|
[
|
||||||
|
c1Hash, match commit1 with | Object.Commit c -> c | _ -> failwith ""
|
||||||
|
c2Hash, match commit2 with | Object.Commit c -> c | _ -> failwith ""
|
||||||
|
c3Hash, match commit3 with | Object.Commit c -> c | _ -> failwith ""
|
||||||
|
]
|
||||||
|
|> Map.ofList
|
||||||
|
|
26
Git/Commands/Log.fs
Normal file
26
Git/Commands/Log.fs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
namespace Git.Commands
|
||||||
|
|
||||||
|
open Git
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Log =
|
||||||
|
|
||||||
|
let log (repo : Repository) (h : Hash) : Map<Hash, CommitEntry> =
|
||||||
|
let rec log (h : Hash) (c : CommitEntry) : seq<Hash * CommitEntry> =
|
||||||
|
seq {
|
||||||
|
yield (h, c)
|
||||||
|
yield!
|
||||||
|
c.Parents
|
||||||
|
|> List.map (fun i ->
|
||||||
|
match EncodedObject.catFile repo i |> EncodedObject.decode with
|
||||||
|
| Object.Commit c -> (i, c)
|
||||||
|
| s -> failwithf "Not a commit: %O (%+A)" i s)
|
||||||
|
|> Seq.collect (fun (i, c) -> log i c)
|
||||||
|
}
|
||||||
|
|
||||||
|
h
|
||||||
|
|> EncodedObject.catFile repo
|
||||||
|
|> EncodedObject.decode
|
||||||
|
|> function | Object.Commit h -> h | s -> failwithf "Not a commit: %+A" s
|
||||||
|
|> log h
|
||||||
|
|> Map.ofSeq
|
@@ -16,6 +16,8 @@
|
|||||||
<Compile Include="Commit.fs" />
|
<Compile Include="Commit.fs" />
|
||||||
<Compile Include="Object.fs" />
|
<Compile Include="Object.fs" />
|
||||||
<Compile Include="EncodedObject.fs" />
|
<Compile Include="EncodedObject.fs" />
|
||||||
|
<Compile Include="Reference.fs" />
|
||||||
|
<Compile Include="Commands\Log.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
namespace Git
|
namespace Git
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
|
||||||
type Object =
|
type Object =
|
||||||
| Blob of byte array
|
| Blob of byte array
|
||||||
| Tree of TreeEntry list
|
| Tree of TreeEntry list
|
||||||
@@ -16,3 +18,35 @@ type Object =
|
|||||||
|> sprintf "tree:\n%+A"
|
|> sprintf "tree:\n%+A"
|
||||||
| Commit c ->
|
| Commit c ->
|
||||||
sprintf "commit:\n%O" c
|
sprintf "commit:\n%O" c
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Object =
|
||||||
|
/// Get the object hashes which match this start.
|
||||||
|
let disambiguate (r : Repository) (startOfHash : string) : Hash list =
|
||||||
|
match startOfHash.Length with
|
||||||
|
| 0 ->
|
||||||
|
(Repository.objectDir r).EnumerateFiles("*", SearchOption.AllDirectories)
|
||||||
|
| 1 ->
|
||||||
|
(Repository.objectDir r).EnumerateFiles("*", SearchOption.AllDirectories)
|
||||||
|
|> Seq.filter (fun i -> i.Directory.Name.StartsWith startOfHash.[0])
|
||||||
|
| 2 ->
|
||||||
|
let subDir =
|
||||||
|
r.Fs.Path.Combine ((Repository.objectDir r).FullName, startOfHash)
|
||||||
|
|> r.Fs.DirectoryInfo.FromDirectoryName
|
||||||
|
if subDir.Exists then
|
||||||
|
subDir.EnumerateFiles ()
|
||||||
|
else Seq.empty
|
||||||
|
| _ ->
|
||||||
|
let prefix = startOfHash.Substring (0, 2)
|
||||||
|
let suffix = startOfHash.Substring (2, startOfHash.Length - 2)
|
||||||
|
let subDir =
|
||||||
|
r.Fs.Path.Combine ((Repository.objectDir r).FullName, prefix)
|
||||||
|
|> r.Fs.DirectoryInfo.FromDirectoryName
|
||||||
|
if subDir.Exists then
|
||||||
|
subDir.EnumerateFiles ()
|
||||||
|
|> Seq.filter (fun i -> i.Name.StartsWith suffix)
|
||||||
|
else Seq.empty
|
||||||
|
|
||||||
|
|> Seq.map (fun i -> sprintf "%s%s" i.Directory.Name i.Name)
|
||||||
|
|> Seq.map Hash.ofString
|
||||||
|
|> List.ofSeq
|
||||||
|
29
Git/Reference.fs
Normal file
29
Git/Reference.fs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace Git
|
||||||
|
|
||||||
|
|
||||||
|
type ReferenceUpdate =
|
||||||
|
{
|
||||||
|
Was : Hash option
|
||||||
|
Now : Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Reference =
|
||||||
|
let write (r : Repository) (hash : Hash) (name : string) : ReferenceUpdate =
|
||||||
|
let refFile = r.Fs.Path.Combine ((Repository.refDir r).FullName, "heads", name) |> r.Fs.FileInfo.FromFileName
|
||||||
|
let was =
|
||||||
|
if refFile.Exists then
|
||||||
|
r.Fs.File.ReadAllText refFile.FullName
|
||||||
|
|> Hash.ofString
|
||||||
|
|> Some
|
||||||
|
else
|
||||||
|
do
|
||||||
|
use _v = refFile.Create ()
|
||||||
|
()
|
||||||
|
None
|
||||||
|
r.Fs.File.WriteAllText (refFile.FullName, hash.ToString ())
|
||||||
|
{
|
||||||
|
Was = was
|
||||||
|
Now = hash
|
||||||
|
}
|
||||||
|
|
@@ -22,6 +22,9 @@ module Repository =
|
|||||||
let internal objectDir (r : Repository) : IDirectoryInfo =
|
let internal objectDir (r : Repository) : IDirectoryInfo =
|
||||||
r.Fs.Path.Combine((gitDir r).FullName, "objects") |> r.Fs.DirectoryInfo.FromDirectoryName
|
r.Fs.Path.Combine((gitDir r).FullName, "objects") |> r.Fs.DirectoryInfo.FromDirectoryName
|
||||||
|
|
||||||
|
let internal refDir (r : Repository) : IDirectoryInfo =
|
||||||
|
r.Fs.Path.Combine((gitDir r).FullName, "refs") |> r.Fs.DirectoryInfo.FromDirectoryName
|
||||||
|
|
||||||
let internal createSubdir (r : IDirectoryInfo) (name : string) : IDirectoryInfo =
|
let internal createSubdir (r : IDirectoryInfo) (name : string) : IDirectoryInfo =
|
||||||
let output =
|
let output =
|
||||||
r.FileSystem.Path.Combine(r.FullName, name)
|
r.FileSystem.Path.Combine(r.FullName, name)
|
||||||
|
Reference in New Issue
Block a user