mirror of
https://github.com/Smaug123/managed-git
synced 2025-10-08 01:08:44 +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