Initial commit

This commit is contained in:
Smaug123
2020-05-02 10:33:38 +01:00
commit f263f81968
13 changed files with 348 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
Git.sln.DotSettings.user

28
Git.Test/Git.Test.fsproj Normal file
View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FsUnit" Version="3.8.1" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="System.IO.Abstractions" Version="11.0.4" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="11.0.4" />
</ItemGroup>
<ItemGroup>
<Compile Include="TestInit.fs" />
<Compile Include="TestObject.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Git\Git.fsproj" />
</ItemGroup>
</Project>

29
Git.Test/TestInit.fs Normal file
View File

@@ -0,0 +1,29 @@
namespace Git.Test
open NUnit.Framework
open FsUnitTyped
open System.IO.Abstractions.TestingHelpers
open Git
[<TestFixture>]
module TestInit =
[<Test>]
let Test1 () =
let fs = MockFileSystem ()
let dir = fs.Path.GetTempFileName ()
let gitDir = fs.DirectoryInfo.FromDirectoryName (dir + "_test")
Repository.init gitDir
|> shouldEqual (Error DirectoryDoesNotExist)
gitDir.Create ()
let r =
match Repository.init gitDir with
| Ok r -> r
| Error r -> failwithf "Failed to init repo: %+A" r
Repository.init gitDir
|> shouldEqual (Error AlreadyGit)

38
Git.Test/TestObject.fs Normal file
View File

@@ -0,0 +1,38 @@
namespace Git.Test
open Git
open NUnit.Framework
open FsUnitTyped
open System
open System.IO.Abstractions.TestingHelpers
[<TestFixture>]
module TestObject =
[<Test>]
let hashFromDocs () =
let t = "what is up, doc?".ToCharArray () |> Array.map byte
let b = Blob.create t
Object.hash b
|> Hash.toString
|> shouldEqual "bd9dbf5aae1a3862dd1526723246b20206e5fc37"
[<Test>]
let writeFromDocs () =
let t = "what is up, doc?".ToCharArray () |> Array.map byte
let b = Blob.create t
let fs = MockFileSystem ()
let dir = fs.Path.GetTempFileName ()
let gitDir = fs.DirectoryInfo.FromDirectoryName (dir + "_test")
gitDir.Create()
let repo = match Repository.init gitDir with | Ok r -> r | Error e -> failwithf "Oh no: %+A" e
Object.write repo b
let backIn = Object.catFile repo (Object.hash b)
backIn.Content
|> Array.map char
|> String
|> shouldEqual "what is up, doc?"

22
Git.sln Normal file
View File

@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Git", "Git\Git.fsproj", "{0ABD60D8-0B57-478C-8CA4-7081A74E5BCD}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Git.Test", "Git.Test\Git.Test.fsproj", "{CABAAA8F-186F-434A-A9F8-E847B69C3164}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0ABD60D8-0B57-478C-8CA4-7081A74E5BCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0ABD60D8-0B57-478C-8CA4-7081A74E5BCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0ABD60D8-0B57-478C-8CA4-7081A74E5BCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0ABD60D8-0B57-478C-8CA4-7081A74E5BCD}.Release|Any CPU.Build.0 = Release|Any CPU
{CABAAA8F-186F-434A-A9F8-E847B69C3164}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CABAAA8F-186F-434A-A9F8-E847B69C3164}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CABAAA8F-186F-434A-A9F8-E847B69C3164}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CABAAA8F-186F-434A-A9F8-E847B69C3164}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

9
Git/Blob.fs Normal file
View File

@@ -0,0 +1,9 @@
namespace Git
[<RequireQualifiedAccess>]
module Blob =
let create (content : byte array) =
{
Header = Header.Blob content.Length
Content = content
}

3
Git/Domain.fs Normal file
View File

@@ -0,0 +1,3 @@
namespace Git

20
Git/Git.fsproj Normal file
View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Header.fs" />
<Compile Include="Repository.fs" />
<Compile Include="Hash.fs" />
<Compile Include="Object.fs" />
<Compile Include="Blob.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Abstractions" Version="11.0.4" />
</ItemGroup>
</Project>

17
Git/Hash.fs Normal file
View File

@@ -0,0 +1,17 @@
namespace Git
open System.Text
[<Struct>]
type Hash = Hash of byte list
[<RequireQualifiedAccess>]
module Hash =
let ofBytes s = s |> Seq.toList |> Hash
let toString (Hash h) : string =
let t = StringBuilder (List.length h * 2)
h
|> List.iter (fun b -> t.AppendFormat ("{0:x2}" , b) |> ignore)
t.ToString ()

31
Git/Header.fs Normal file
View File

@@ -0,0 +1,31 @@
namespace Git
open System
type Header =
| Blob of int // length of content
// | Commit
// | Tree
// | Tag
[<RequireQualifiedAccess>]
module internal Header =
let toBytes (h : Header) : byte array =
match h with
| Header.Blob length ->
// TODO - internationalisation issue here
let s = sprintf "blob %i" length
[|
s.ToCharArray () |> Array.map byte
[| 0uy |]
|]
|> Array.concat
let ofBytes (s : byte array) : Header option =
if s.[0..3] = ("blob".ToCharArray () |> Array.map byte) then
let number = s.[5..] |> Array.map char |> String |> Int32.Parse
Header.Blob number
|> Some
else
None

86
Git/Object.fs Normal file
View File

@@ -0,0 +1,86 @@
namespace Git
open System.IO
open System.Security.Cryptography
open System.IO.Compression
type Object =
{
Header : Header
Content : byte array
}
[<RequireQualifiedAccess>]
module Object =
let hash (o : Object) : Hash =
use hasher = SHA1.Create ()
let content = Array.concat [| Header.toBytes o.Header ; o.Content |]
hasher.ComputeHash content
|> Hash.ofBytes
let private compress (o : Object) (dest : Stream) : unit =
let toWrite =
[| Header.toBytes o.Header ; o.Content |]
|> Array.concat
use ms = new MemoryStream(toWrite)
use ds = new DeflateStream(dest, CompressionMode.Compress)
ms.CopyTo ds
/// Read the header of the stream seeked to the beginning of the content.
let private consumeHeader (s : BinaryReader) : Header =
let rec bytes () : byte seq =
seq {
let newByte = s.Read ()
if newByte < 0 then failwith "ran out of bytes"
elif newByte > 0 then
yield (byte newByte)
yield! bytes ()
// stop reading the header at the 0 byte
}
match bytes () |> Seq.toArray |> Header.ofBytes with
| None ->
failwith "malformed header"
| Some b -> b
let private uncompress (s : Stream) : Object =
use ms = new MemoryStream ()
use ds = new DeflateStream(s, CompressionMode.Decompress)
ds.CopyTo ms
ms.Seek(0L, SeekOrigin.Begin) |> ignore
use r = new BinaryReader(ms)
let header = consumeHeader r
let expectedLength =
match header with
| Header.Blob i -> i
let result =
{
Header = header
Content = r.ReadBytes expectedLength
}
if r.PeekChar () <> -1 then failwith "unexpectedly not at end"
result
let write (r : Repository) (o : Object) : unit =
let hash = hash o |> Hash.toString
let objectName = hash.[2..]
let subdir = hash.[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
let catFile (r : Repository) (hash : Hash) : Object =
let hash = hash |> Hash.toString
let objectName = hash.[2..]
let subdir = hash.[0..1]
use filestream =
r.Fs.Path.Combine ((Repository.objectDir r).FullName, subdir, objectName)
|> r.Fs.File.OpenRead
uncompress filestream

49
Git/Repository.fs Normal file
View File

@@ -0,0 +1,49 @@
namespace Git
open System.IO.Abstractions
/// If you have a Repository in scope, you know it existed at the time it was created.
type Repository =
private
{
Directory : IDirectoryInfo
}
member this.Fs = this.Directory.FileSystem
type InitFailure =
| DirectoryDoesNotExist
| AlreadyGit
[<RequireQualifiedAccess>]
module Repository =
let internal gitDir (r : Repository) : IDirectoryInfo =
r.Fs.Path.Combine(r.Directory.FullName, ".git") |> r.Fs.DirectoryInfo.FromDirectoryName
let internal objectDir (r : Repository) : IDirectoryInfo =
r.Fs.Path.Combine((gitDir r).FullName, "objects") |> r.Fs.DirectoryInfo.FromDirectoryName
let internal createSubdir (r : IDirectoryInfo) (name : string) : IDirectoryInfo =
let output =
r.FileSystem.Path.Combine(r.FullName, name)
|> r.FileSystem.DirectoryInfo.FromDirectoryName
output.Create ()
output
let init (dir : IDirectoryInfo) : Result<Repository, InitFailure> =
if not dir.Exists then Error DirectoryDoesNotExist
elif not <| Seq.isEmpty (dir.EnumerateDirectories ".git") then Error AlreadyGit
else
let r =
{
Directory = dir
}
let gitDir = createSubdir dir ".git"
let objectDir = createSubdir gitDir "objects"
let packDir = createSubdir objectDir "pack"
let infoDir = createSubdir objectDir "info"
r
|> Ok

9
README.md Normal file
View File

@@ -0,0 +1,9 @@
# Git
This is an F# implementation of Git.
As of the first commit, it is liable to be highly inefficient; it might not close streams it opens, it might read streams to the end when it need not, and so forth.
# Why?
LibGit2Sharp is all fine and dandy except that it has to act on a real filesystem.