mirror of
https://github.com/Smaug123/managed-git
synced 2025-10-05 15:58:41 +00:00
Initial commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal 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
28
Git.Test/Git.Test.fsproj
Normal 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
29
Git.Test/TestInit.fs
Normal 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
38
Git.Test/TestObject.fs
Normal 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
22
Git.sln
Normal 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
9
Git/Blob.fs
Normal 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
3
Git/Domain.fs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Git
|
||||
|
||||
|
20
Git/Git.fsproj
Normal file
20
Git/Git.fsproj
Normal 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
17
Git/Hash.fs
Normal 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
31
Git/Header.fs
Normal 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
86
Git/Object.fs
Normal 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
49
Git/Repository.fs
Normal 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
9
README.md
Normal 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.
|
Reference in New Issue
Block a user