Some stuff I found lying around my disk (#11)

This commit is contained in:
Patrick Stevens
2023-09-02 21:48:31 +01:00
committed by GitHub
parent 96593f399a
commit 09ccb7b90c
36 changed files with 501 additions and 359 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"fantomas": { "fantomas": {
"version": "5.0.0-beta-009", "version": "6.2.0",
"commands": [ "commands": [
"fantomas" "fantomas"
] ]

View File

@@ -1,14 +1,19 @@
root = true root = true
[*.{fs,fsi,fsx}] [*.{fs,fsi}]
fsharp_bar_before_discriminated_union_declaration=true
fsharp_space_before_uppercase_invocation=true fsharp_space_before_uppercase_invocation=true
fsharp_space_before_class_constructor=true
fsharp_space_before_member=true fsharp_space_before_member=true
fsharp_space_before_colon=true fsharp_space_before_colon=true
fsharp_space_before_semicolon=true fsharp_space_before_semicolon=true
fsharp_multiline_block_brackets_on_same_column=true fsharp_multiline_bracket_style=aligned
fsharp_newline_between_type_definition_and_members=true fsharp_newline_between_type_definition_and_members=true
fsharp_experimental_keep_indent_in_branch=true
fsharp_align_function_signature_to_indentation=true fsharp_align_function_signature_to_indentation=true
fsharp_alternative_long_member_definitions=true fsharp_alternative_long_member_definitions=true
fsharp_multi_line_lambda_closing_newline=true fsharp_multi_line_lambda_closing_newline=true
fsharp_max_infix_operator_expression=50 fsharp_experimental_keep_indent_in_branch=true
fsharp_max_value_binding_width=80
fsharp_max_record_width=0
max_line_length=120
end_of_line=lf

View File

@@ -22,7 +22,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 6.0.x dotnet-version: 7.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
@@ -16,17 +16,19 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Result.fs" />
<Compile Include="Resource.fs" /> <Compile Include="Resource.fs" />
<Compile Include="Printer.fs" /> <Compile Include="Printer.fs" />
<Compile Include="TestString.fs" />
<Compile Include="TestInit.fs" /> <Compile Include="TestInit.fs" />
<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="Utils.fs" />
<Compile Include="TestCommit.fs" /> <Compile Include="TestCommit.fs" />
<Compile Include="TestObject.fs" />
<Compile Include="TestLog.fs" /> <Compile Include="TestLog.fs" />
<Compile Include="TestPack.fs" /> <Compile Include="TestPack.fs" />
<Compile Include="TestRevParse.fs" />
<EmbeddedResource Include="pack-fd1ac4dab39afd8713d495c8bc30ae9ea6157eea.idx" /> <EmbeddedResource Include="pack-fd1ac4dab39afd8713d495c8bc30ae9ea6157eea.idx" />
<EmbeddedResource Include="pack-fd1ac4dab39afd8713d495c8bc30ae9ea6157eea.pack" /> <EmbeddedResource Include="pack-fd1ac4dab39afd8713d495c8bc30ae9ea6157eea.pack" />
<EmbeddedResource Include="verify-pack.txt" /> <EmbeddedResource Include="verify-pack.txt" />

14
Git.Test/Result.fs Normal file
View File

@@ -0,0 +1,14 @@
namespace Git.Test
[<RequireQualifiedAccess>]
module Result =
let get<'res, 'err> (r : Result<'res, 'err>) : 'res =
match r with
| Ok x -> x
| Error e -> failwithf "Expected Ok, but got Error: %+A" e
let getError<'res, 'err> (r : Result<'res, 'err>) : 'err =
match r with
| Error e -> e
| Ok x -> failwithf "Expected Error, but got Ok: %+A" x

View File

@@ -38,13 +38,8 @@ module TestBlob =
b |> EncodedObject.write repo |> ignore b |> EncodedObject.write repo |> ignore
let backIn = let backIn =
EncodedObject.catFile repo (EncodedObject.hash b) EncodedObject.catFile repo (EncodedObject.hash b) |> EncodedObject.decode
|> EncodedObject.decode
match backIn with match backIn with
| Object.Blob b -> | Object.Blob b -> b |> Array.map char |> String |> shouldEqual "what is up, doc?"
b
|> Array.map char
|> String
|> shouldEqual "what is up, doc?"
| _ -> failwithf "Oh no: %+A" backIn | _ -> failwithf "Oh no: %+A" backIn

View File

@@ -40,12 +40,8 @@ module TestCommit =
} }
|> Object.Commit |> Object.Commit
let h = let h = EncodedObject.encode commit1 |> EncodedObject.write repo
EncodedObject.encode commit1
|> EncodedObject.write repo
let c = let c = EncodedObject.catFile repo h |> EncodedObject.decode
EncodedObject.catFile repo h
|> EncodedObject.decode
c |> shouldEqual commit1 c |> shouldEqual commit1

View File

@@ -36,8 +36,7 @@ module TestFromGitBook =
|> List.sort |> List.sort
|> shouldEqual [ "info" ; "pack" ] |> shouldEqual [ "info" ; "pack" ]
objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) |> shouldBeEmpty
|> shouldBeEmpty
// Write our first object // Write our first object
let h = let h =
@@ -47,8 +46,7 @@ module TestFromGitBook =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
h h |> shouldEqual (Hash.ofString "d670460b4b4aece5915caf5c68d12f560a9fe3e4")
|> shouldEqual (Hash.ofString "d670460b4b4aece5915caf5c68d12f560a9fe3e4")
// Check that it's appeared // Check that it's appeared
objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories)
@@ -57,15 +55,8 @@ module TestFromGitBook =
|> shouldEqual ("d6", "70460b4b4aece5915caf5c68d12f560a9fe3e4") |> shouldEqual ("d6", "70460b4b4aece5915caf5c68d12f560a9fe3e4")
// Read it back in // Read it back in
match match EncodedObject.catFile repo h |> EncodedObject.decode with
EncodedObject.catFile repo h | Object.Blob b -> b |> Array.map char |> String |> shouldEqual "test content\n"
|> EncodedObject.decode
with
| Object.Blob b ->
b
|> Array.map char
|> String
|> shouldEqual "test content\n"
| s -> failwithf "Oh no: +%A" s | s -> failwithf "Oh no: +%A" s
// Version control // Version control
@@ -77,8 +68,7 @@ module TestFromGitBook =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
h1 h1 |> shouldEqual (Hash.ofString "83baae61804e65cc73a7201a7252750c76066a30")
|> shouldEqual (Hash.ofString "83baae61804e65cc73a7201a7252750c76066a30")
let h2 = let h2 =
"version 2\n" "version 2\n"
@@ -87,8 +77,7 @@ module TestFromGitBook =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
h2 h2 |> shouldEqual (Hash.ofString "1f7a7a472abf3dd9643fd615f6da379c4acb3e3a")
|> shouldEqual (Hash.ofString "1f7a7a472abf3dd9643fd615f6da379c4acb3e3a")
objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories)
|> Seq.map (fun f -> f.Directory.Name, f.Name) |> Seq.map (fun f -> f.Directory.Name, f.Name)
@@ -101,26 +90,12 @@ module TestFromGitBook =
"d6", "70460b4b4aece5915caf5c68d12f560a9fe3e4" "d6", "70460b4b4aece5915caf5c68d12f560a9fe3e4"
] ]
match match EncodedObject.catFile repo h1 |> EncodedObject.decode with
EncodedObject.catFile repo h1 | Object.Blob b -> b |> Array.map char |> String |> shouldEqual "version 1\n"
|> EncodedObject.decode
with
| Object.Blob b ->
b
|> Array.map char
|> String
|> shouldEqual "version 1\n"
| s -> failwithf "Oh no: +%A" s | s -> failwithf "Oh no: +%A" s
match match EncodedObject.catFile repo h2 |> EncodedObject.decode with
EncodedObject.catFile repo h2 | Object.Blob b -> b |> Array.map char |> String |> shouldEqual "version 2\n"
|> EncodedObject.decode
with
| Object.Blob b ->
b
|> Array.map char
|> String
|> shouldEqual "version 2\n"
| s -> failwithf "Oh no: +%A" s | s -> failwithf "Oh no: +%A" s
// Add to the tree // Add to the tree
@@ -136,13 +111,9 @@ module TestFromGitBook =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
tree1 tree1 |> shouldEqual (Hash.ofString "d8329fc1cc938780ffdd9f94e0d364e0ea74f579")
|> shouldEqual (Hash.ofString "d8329fc1cc938780ffdd9f94e0d364e0ea74f579")
match match EncodedObject.catFile repo tree1 |> EncodedObject.decode with
EncodedObject.catFile repo tree1
|> EncodedObject.decode
with
| Object.Tree t -> | Object.Tree t ->
t t
|> List.exactlyOne |> List.exactlyOne
@@ -181,13 +152,9 @@ module TestFromGitBook =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
tree2 tree2 |> shouldEqual (Hash.ofString "0155eb4229851634a0f03eb265b69f5a2d56f341")
|> shouldEqual (Hash.ofString "0155eb4229851634a0f03eb265b69f5a2d56f341")
match match EncodedObject.catFile repo tree2 |> EncodedObject.decode with
EncodedObject.catFile repo tree2
|> EncodedObject.decode
with
| Object.Tree t -> | Object.Tree t ->
t t
|> shouldEqual |> shouldEqual
@@ -228,13 +195,9 @@ module TestFromGitBook =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
tree3 tree3 |> shouldEqual (Hash.ofString "3c4e9cd789d88d8d89c1073707c3585e41b0e614")
|> shouldEqual (Hash.ofString "3c4e9cd789d88d8d89c1073707c3585e41b0e614")
match match EncodedObject.catFile repo tree3 |> EncodedObject.decode with
EncodedObject.catFile repo tree3
|> EncodedObject.decode
with
| Object.Tree t -> | Object.Tree t ->
t t
|> shouldEqual |> shouldEqual
@@ -277,10 +240,7 @@ module TestFromGitBook =
} }
|> Object.Commit |> Object.Commit
let c1Hash = let c1Hash = commit1 |> EncodedObject.encode |> EncodedObject.write repo
commit1
|> EncodedObject.encode
|> EncodedObject.write repo
// For reasons I don't understand, `git` diverges from Pro Git at this point. // For reasons I don't understand, `git` diverges from Pro Git at this point.
// Pro Git's version: "fdf4fc3344e67ab068f836878b6c4951e3b15f3d" // Pro Git's version: "fdf4fc3344e67ab068f836878b6c4951e3b15f3d"
// `git` (version 2.26.1): // `git` (version 2.26.1):
@@ -289,9 +249,7 @@ module TestFromGitBook =
|> shouldEqual "70d4408b5020e81d19906d6abdd87a73233ebf34" |> shouldEqual "70d4408b5020e81d19906d6abdd87a73233ebf34"
// Note that we can roundtrip (not done explicitly in the book): // Note that we can roundtrip (not done explicitly in the book):
EncodedObject.catFile repo c1Hash EncodedObject.catFile repo c1Hash |> EncodedObject.decode |> shouldEqual commit1
|> EncodedObject.decode
|> shouldEqual commit1
let commit2 = let commit2 =
{ {
@@ -304,18 +262,13 @@ module TestFromGitBook =
} }
|> Object.Commit |> Object.Commit
let c2Hash = let c2Hash = commit2 |> EncodedObject.encode |> EncodedObject.write repo
commit2
|> EncodedObject.encode
|> EncodedObject.write repo
c2Hash c2Hash
|> Hash.toString |> Hash.toString
|> shouldEqual "1513b13a72f5277252cfce4ed0eda0620aca2f6a" |> shouldEqual "1513b13a72f5277252cfce4ed0eda0620aca2f6a"
EncodedObject.catFile repo c2Hash EncodedObject.catFile repo c2Hash |> EncodedObject.decode |> shouldEqual commit2
|> EncodedObject.decode
|> shouldEqual commit2
let commit3 = let commit3 =
{ {
@@ -328,18 +281,13 @@ module TestFromGitBook =
} }
|> Object.Commit |> Object.Commit
let c3Hash = let c3Hash = commit3 |> EncodedObject.encode |> EncodedObject.write repo
commit3
|> EncodedObject.encode
|> EncodedObject.write repo
c3Hash c3Hash
|> Hash.toString |> Hash.toString
|> shouldEqual "95cce637b4e889eee8042515db402128bd62c0d2" |> shouldEqual "95cce637b4e889eee8042515db402128bd62c0d2"
EncodedObject.catFile repo c3Hash EncodedObject.catFile repo c3Hash |> EncodedObject.decode |> shouldEqual commit3
|> EncodedObject.decode
|> shouldEqual commit3
objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories) objectsDir.EnumerateFiles ("*", SearchOption.AllDirectories)
|> Seq.map (fun f -> f.Directory.Name, f.Name) |> Seq.map (fun f -> f.Directory.Name, f.Name)
@@ -373,27 +321,36 @@ module TestFromGitBook =
c3Hash c3Hash
|> Reference.write repo "master" |> Reference.write repo "master"
|> shouldEqual { Was = None ; Now = c3Hash } |> shouldEqual
{
Was = None
Now = c3Hash
}
Object.disambiguate repo "1513b1" RevParse.disambiguateLooseHash repo "1513b1"
|> List.exactlyOne |> List.exactlyOne
|> Reference.write repo "test" |> Reference.write repo "test"
|> shouldEqual { Was = None ; Now = c2Hash } |> shouldEqual
{
Was = None
Now = c2Hash
}
let exn = let error = SymbolicReference.write repo SymbolicRef.Head "test" |> Result.getError
Assert.Throws<Exception> (fun () -> SymbolicReference.write repo SymbolicRef.Head "test")
exn.Message error
|> shouldEqual "refusing to point HEAD outside of refs/" |> shouldEqual (SymbolicRefWriteError.PointingOutsideRefs SymbolicRef.Head)
SymbolicReference.write repo SymbolicRef.Head "refs/heads/test" error.ToString () |> shouldEqual "refusing to point HEAD outside of refs/"
SymbolicReference.write repo SymbolicRef.Head "refs/heads/test" |> Result.get
repo.Fs.Path.Combine ((Repository.gitDir repo).FullName, "HEAD") repo.Fs.Path.Combine ((Repository.gitDir repo).FullName, "HEAD")
|> repo.Fs.File.ReadAllText |> repo.Fs.File.ReadAllText
|> shouldEqual "ref: refs/heads/test" |> shouldEqual "ref: refs/heads/test\n"
SymbolicReference.lookup repo SymbolicRef.Head SymbolicReference.lookup repo SymbolicRef.Head
|> shouldEqual (Ok (SymbolicRefTarget "refs/heads/test")) |> shouldEqual (Ok (SymbolicRefTarget "refs/heads/test"))
SymbolicReference.lookup repo SymbolicRef.FetchHead SymbolicReference.lookup repo SymbolicRef.FetchHead
|> shouldEqual (Error RefDidNotExist) |> shouldEqual (Error (RefDidNotExist SymbolicRef.FetchHead))

View File

@@ -25,5 +25,4 @@ module TestInit =
| Ok r -> r | Ok r -> r
| Error r -> failwithf "Failed to init repo: %+A" r | Error r -> failwithf "Failed to init repo: %+A" r
Repository.init (BranchName "main") gitDir Repository.init (BranchName "main") gitDir |> shouldEqual (Error AlreadyGit)
|> shouldEqual (Error AlreadyGit)

View File

@@ -4,13 +4,14 @@ open System
open System.IO.Abstractions.TestingHelpers open System.IO.Abstractions.TestingHelpers
open System.Runtime.InteropServices open System.Runtime.InteropServices
open System.Text open System.Text
open FsCheck
open NUnit.Framework open NUnit.Framework
open FsUnitTyped open FsUnitTyped
open FsCheck
open Git open Git
open Git.Commands
[<TestFixture>] [<TestFixture>]
module TestObject = module TestRevParse =
let private intToChar (i : int) (upper : bool) : char = let private intToChar (i : int) (upper : bool) : char =
if i < 10 then if i < 10 then
@@ -63,23 +64,20 @@ module TestObject =
let isMatch = let isMatch =
if RuntimeInformation.IsOSPlatform OSPlatform.Windows then if RuntimeInformation.IsOSPlatform OSPlatform.Windows then
// Windows filesystem is case-insensitive // Windows filesystem is case-insensitive
expected.StartsWith (prefix, StringComparison.InvariantCultureIgnoreCase) expected.StartsWith (prefix, StringComparison.OrdinalIgnoreCase)
else else
expected.StartsWith prefix expected.StartsWith (prefix, StringComparison.Ordinal)
if isMatch then if isMatch then
Object.disambiguate repo prefix = [ expectedHash ] RevParse.disambiguateLooseHash repo prefix = [ expectedHash ]
else else
Object.disambiguate repo prefix = [] RevParse.disambiguateLooseHash repo prefix = []
property property
|> Prop.forAll (Arb.fromGen (hashPrefixGenerator 40uy)) |> Prop.forAll (Arb.fromGen (hashPrefixGenerator 40uy))
|> Check.QuickThrowOnFailure |> Check.QuickThrowOnFailure
for subStringEnd in 0 .. expected.Length - 1 do for subStringEnd in 0 .. expected.Length - 1 do
property expected.[0..subStringEnd] property expected.[0..subStringEnd] |> shouldEqual true
|> shouldEqual true
expected.[0..subStringEnd].ToUpperInvariant () expected.[0..subStringEnd].ToUpperInvariant () |> property |> shouldEqual true
|> property
|> shouldEqual true

86
Git.Test/TestString.fs Normal file
View File

@@ -0,0 +1,86 @@
namespace Git.Test
open System.Threading
open FsCheck
open Git
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
module TestString =
[<Test>]
let ``chopStart does nothing when chopping a non-start`` () =
let badCount = ref 0
let goodCount = ref 0
let property (NonNull s : NonNull<string>) : bool =
if s.StartsWith "hi" then
Interlocked.Increment badCount |> ignore
true
else
Interlocked.Increment goodCount |> ignore
s |> String.chopStart "hi" |> (=) s
Check.QuickThrowOnFailure property
badCount.Value |> shouldBeSmallerThan goodCount.Value
goodCount.Value |> shouldBeGreaterThan 10
[<Test>]
let ``chopStart does nothing when chopping the empty string`` () =
let property (NonNull s) : bool = s |> String.chopStart "" |> (=) s
Check.QuickThrowOnFailure property
[<Test>]
let ``chopStart does nothing when chopping by the empty string`` () =
let property (NonNull s) : bool = "" |> String.chopStart s |> (=) ""
Check.QuickThrowOnFailure property
[<Test>]
let ``chopStart chops the initial`` () =
let property (NonNull toChop) (NonNull from : NonNull<string>) : bool =
(toChop + from) |> String.chopStart toChop |> (=) from
Check.QuickThrowOnFailure property
[<Test>]
let ``chopEnd does nothing when chopping a non-end`` () =
let badCount = ref 0
let goodCount = ref 0
let property (NonNull s : NonNull<string>) : bool =
if s.EndsWith "hi" then
Interlocked.Increment badCount |> ignore
true
else
Interlocked.Increment goodCount |> ignore
s |> String.chopEnd "hi" |> (=) s
Check.QuickThrowOnFailure property
badCount.Value |> shouldBeSmallerThan goodCount.Value
goodCount.Value |> shouldBeGreaterThan 10
[<Test>]
let ``chopEnd does nothing when chopping the empty string`` () =
let property (NonNull s) : bool = s |> String.chopEnd "" |> (=) s
Check.QuickThrowOnFailure property
[<Test>]
let ``chopEnd does nothing when chopping by the empty string`` () =
let property (NonNull s) : bool = "" |> String.chopEnd s |> (=) ""
Check.QuickThrowOnFailure property
[<Test>]
let ``chopEnd chops the final`` () =
let property (NonNull toChop) (NonNull from : NonNull<string>) : bool =
(from + toChop) |> String.chopEnd toChop |> (=) from
Check.QuickThrowOnFailure property

View File

@@ -55,8 +55,7 @@ module TestTree =
b |> EncodedObject.write repo |> ignore b |> EncodedObject.write repo |> ignore
let backIn = let backIn =
EncodedObject.catFile repo (EncodedObject.hash b) EncodedObject.catFile repo (EncodedObject.hash b) |> EncodedObject.decode
|> EncodedObject.decode
match backIn with match backIn with
| Object.Tree entries -> entries |> shouldEqual t | Object.Tree entries -> entries |> shouldEqual t

View File

@@ -16,8 +16,7 @@ module Utils =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
h1 h1 |> shouldEqual (Hash.ofString "83baae61804e65cc73a7201a7252750c76066a30")
|> shouldEqual (Hash.ofString "83baae61804e65cc73a7201a7252750c76066a30")
let h2 = let h2 =
"version 2\n" "version 2\n"
@@ -26,8 +25,7 @@ module Utils =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
h2 h2 |> shouldEqual (Hash.ofString "1f7a7a472abf3dd9643fd615f6da379c4acb3e3a")
|> shouldEqual (Hash.ofString "1f7a7a472abf3dd9643fd615f6da379c4acb3e3a")
// Add to the tree // Add to the tree
let tree1 = let tree1 =
@@ -42,8 +40,7 @@ module Utils =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
tree1 tree1 |> shouldEqual (Hash.ofString "d8329fc1cc938780ffdd9f94e0d364e0ea74f579")
|> shouldEqual (Hash.ofString "d8329fc1cc938780ffdd9f94e0d364e0ea74f579")
let newHash = let newHash =
"new file\n" "new file\n"
@@ -72,8 +69,7 @@ module Utils =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
tree2 tree2 |> shouldEqual (Hash.ofString "0155eb4229851634a0f03eb265b69f5a2d56f341")
|> shouldEqual (Hash.ofString "0155eb4229851634a0f03eb265b69f5a2d56f341")
// and the prefix one // and the prefix one
let tree3 = let tree3 =
@@ -98,8 +94,7 @@ module Utils =
|> EncodedObject.encode |> EncodedObject.encode
|> EncodedObject.write repo |> EncodedObject.write repo
tree3 tree3 |> shouldEqual (Hash.ofString "3c4e9cd789d88d8d89c1073707c3585e41b0e614")
|> shouldEqual (Hash.ofString "3c4e9cd789d88d8d89c1073707c3585e41b0e614")
let scott = let scott =
{ {
@@ -120,10 +115,7 @@ module Utils =
} }
|> Object.Commit |> Object.Commit
let c1Hash = let c1Hash = commit1 |> EncodedObject.encode |> EncodedObject.write repo
commit1
|> EncodedObject.encode
|> EncodedObject.write repo
c1Hash c1Hash
|> Hash.toString |> Hash.toString
@@ -140,10 +132,7 @@ module Utils =
} }
|> Object.Commit |> Object.Commit
let c2Hash = let c2Hash = commit2 |> EncodedObject.encode |> EncodedObject.write repo
commit2
|> EncodedObject.encode
|> EncodedObject.write repo
c2Hash c2Hash
|> Hash.toString |> Hash.toString
@@ -160,10 +149,7 @@ module Utils =
} }
|> Object.Commit |> Object.Commit
let c3Hash = let c3Hash = commit3 |> EncodedObject.encode |> EncodedObject.write repo
commit3
|> EncodedObject.encode
|> EncodedObject.write repo
c3Hash c3Hash
|> Hash.toString |> Hash.toString

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -25,4 +25,26 @@ module Program =
| [| "verify-pack" ; "-v" ; hash |] -> | [| "verify-pack" ; "-v" ; hash |] ->
VerifyPack.verifyVerbose printer repo hash VerifyPack.verifyVerbose printer repo hash
0 0
| [| "rev-parse" ; ref |] ->
let parsed = RevParse.parse repo ref
match parsed with
| Ok h ->
System.Console.WriteLine (Hash.toString h)
0
| Error (e : RevParseError) ->
System.Console.Error.WriteLine (e.ToString ())
1
| [| "branch" ; branchName |] ->
match Branch.createFromHead repo branchName with
| Error e ->
System.Console.Error.WriteLine (e.ToString ())
1
| Ok _ -> 0
| [| "branch" ; branchName ; baseRef |] ->
match Branch.create repo branchName baseRef with
| Error e ->
System.Console.Error.WriteLine (e.ToString ())
1
| Ok _ -> 0
| _ -> failwith "unrecognised args" | _ -> failwith "unrecognised args"

6
Git/AssemblyInfo.fs Normal file
View File

@@ -0,0 +1,6 @@
module AssemblyInfo
open System.Runtime.CompilerServices
[<assembly : InternalsVisibleTo("Git.Test")>]
do ()

36
Git/Commands/Branch.fs Normal file
View File

@@ -0,0 +1,36 @@
namespace Git.Commands
open Git
type BranchCreationError =
| HeadNotBelowRefsHeads of currentHead : string
| HeadDoesNotExist of SymbolicRefLookupError
override this.ToString () =
match this with
| BranchCreationError.HeadNotBelowRefsHeads _ -> "fatal: HEAD not found below refs/heads!"
| HeadDoesNotExist (err : SymbolicRefLookupError) ->
// TODO: determine what Git does here
sprintf "Could not determine head to branch from: %O" err
[<RequireQualifiedAccess>]
module Branch =
let create (r : Repository) (name : string) (baseRef : string) =
match Commands.RevParse.parse r baseRef with
| Ok ref -> Reference.write r name ref |> Ok
| Error (e : RevParseError) ->
// TODO: find out what Git does here
failwithf "Supplied ref is not known: %O" e
let createFromHead (r : Repository) (name : string) : Result<ReferenceUpdate, BranchCreationError> =
// TODO: probably want to type this more strongly, do some more parsing of the target
match SymbolicReference.lookup r SymbolicRef.Head with
| Error e -> Error (BranchCreationError.HeadDoesNotExist e)
| Ok (SymbolicRefTarget currentHead) ->
// Match Git's behaviour here!
if not (currentHead.StartsWith "refs/heads/") then
Error (BranchCreationError.HeadNotBelowRefsHeads currentHead)
else
create r name currentHead

View File

@@ -13,10 +13,7 @@ module Log =
yield! yield!
c.Parents c.Parents
|> List.map (fun i -> |> List.map (fun i ->
match match EncodedObject.catFile repo i |> EncodedObject.decode with
EncodedObject.catFile repo i
|> EncodedObject.decode
with
| Object.Commit c -> (i, c) | Object.Commit c -> (i, c)
| s -> failwithf "Not a commit: %O (%+A)" i s | s -> failwithf "Not a commit: %O (%+A)" i s
) )

View File

@@ -1,8 +1,13 @@
namespace Git.Commands namespace Git.Commands
type Printer = { WriteLine : string -> unit } type Printer =
{
WriteLine : string -> unit
}
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Printer = module Printer =
let make () = let make () =
{ WriteLine = System.Console.WriteLine } {
WriteLine = System.Console.WriteLine
}

22
Git/Commands/RevParse.fs Normal file
View File

@@ -0,0 +1,22 @@
namespace Git.Commands
open Git
type RevParseError =
| MultipleMatches of original : string * candidates : Hash list
| Unrecognised of original : string
override this.ToString () =
match this with
| RevParseError.MultipleMatches (s, candidates) -> sprintf "fatal: ambiguous argument '%s'" s
| RevParseError.Unrecognised s ->
sprintf "fatal: ambiguous argument '%s': unknown revision or path not in the working tree." s
[<RequireQualifiedAccess>]
module RevParse =
let rec parse (r : Repository) (s : string) : Result<Hash, RevParseError> =
match RevParse.parse r s with
| [ s ] -> Ok s
| (_ :: _ :: _) as matches -> Error (RevParseError.MultipleMatches (s, matches))
| [] -> Error (RevParseError.Unrecognised s)

View File

@@ -103,8 +103,7 @@ module Commit =
| Some data when data.[0..6] = Encoding.ASCII.GetBytes "gpgsig " -> | Some data when data.[0..6] = Encoding.ASCII.GetBytes "gpgsig " ->
let result = StringBuilder () let result = StringBuilder ()
result.Append (Encoding.ASCII.GetString data.[7..]) result.Append (Encoding.ASCII.GetString data.[7..]) |> ignore
|> ignore
result.Append '-' |> ignore result.Append '-' |> ignore

View File

@@ -44,9 +44,7 @@ module EncodedObject =
hasher.ComputeHash content |> Hash.ofBytes hasher.ComputeHash content |> Hash.ofBytes
let private compress (o : EncodedObject) (dest : Stream) : unit = let private compress (o : EncodedObject) (dest : Stream) : unit =
let toWrite = let toWrite = [| Header.toBytes o.Header ; o.Content |] |> Array.concat
[| Header.toBytes o.Header ; o.Content |]
|> Array.concat
use ms = new MemoryStream (toWrite) use ms = new MemoryStream (toWrite)
use ds = new DeflateStream (dest, CompressionMode.Compress) use ds = new DeflateStream (dest, CompressionMode.Compress)

View File

@@ -7,7 +7,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Domain.fs" /> <Compile Include="Domain.fs" />
<Compile Include="String.fs" />
<Compile Include="Hash.fs" /> <Compile Include="Hash.fs" />
<Compile Include="Stream.fs" /> <Compile Include="Stream.fs" />
<Compile Include="Parse.fs" /> <Compile Include="Parse.fs" />
@@ -22,15 +24,18 @@
<Compile Include="VerifyPack.fs" /> <Compile Include="VerifyPack.fs" />
<Compile Include="EncodedObject.fs" /> <Compile Include="EncodedObject.fs" />
<Compile Include="Reference.fs" /> <Compile Include="Reference.fs" />
<Compile Include="RevParse.fs" />
<Compile Include="SymbolicReference.fs" /> <Compile Include="SymbolicReference.fs" />
<Compile Include="Commands\Printer.fs" /> <Compile Include="Commands\Printer.fs" />
<Compile Include="Commands\Log.fs" /> <Compile Include="Commands\Log.fs" />
<Compile Include="Commands\VerifyPack.fs" /> <Compile Include="Commands\VerifyPack.fs" />
<Compile Include="Commands\RevParse.fs" />
<Compile Include="Commands\Branch.fs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" /> <PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="SharpZipLib.NETStandard" Version="1.0.7" /> <PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.IO.Abstractions" Version="11.0.4" /> <PackageReference Include="System.IO.Abstractions" Version="11.0.4" />
<PackageReference Include="FSharp.Core" Version="4.3.4" /> <PackageReference Include="FSharp.Core" Version="4.3.4" />
</ItemGroup> </ItemGroup>

View File

@@ -48,16 +48,7 @@ module Hash =
else else
failwithf "Byte '%i' ('%c') is not a hex digit" b (char b) failwithf "Byte '%i' ('%c') is not a hex digit" b (char b)
let rec b (pos : int) = fun i -> value input.[2 * i] * 16uy + value input.[2 * i + 1]
seq {
if pos < input.Length then
yield value input.[pos] * 16uy + value input.[pos + 1]
yield! b (pos + 2)
}
fun i ->
value input.[2 * i] * 16uy
+ value input.[2 * i + 1]
|> Array.init (input.Length / 2) |> Array.init (input.Length / 2)
|> ofBytes |> ofBytes

View File

@@ -34,24 +34,14 @@ module internal Header =
match s.[0] with match s.[0] with
| 98uy -> | 98uy ->
// 'b', then "lob " // 'b', then "lob "
if if s.[1] = 108uy && s.[2] = 111uy && s.[3] = 98uy && s.[4] = 32uy then
s.[1] = 108uy
&& s.[2] = 111uy
&& s.[3] = 98uy
&& s.[4] = 32uy
then
let number = parseIntFromAsciiBytes 5 s let number = parseIntFromAsciiBytes 5 s
(ObjectType.Blob, number) |> Some (ObjectType.Blob, number) |> Some
else else
None None
| 116uy -> | 116uy ->
// 't', then "ree " // 't', then "ree "
if if s.[1] = 114uy && s.[2] = 101uy && s.[3] = 101uy && s.[4] = 32uy then
s.[1] = 114uy
&& s.[2] = 101uy
&& s.[3] = 101uy
&& s.[4] = 32uy
then
let number = parseIntFromAsciiBytes 5 s let number = parseIntFromAsciiBytes 5 s
(ObjectType.Tree, number) |> Some (ObjectType.Tree, number) |> Some
else else

View File

@@ -1,7 +1,5 @@
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
@@ -21,50 +19,6 @@ type Object =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Object = module Object =
/// Get the object hashes which match this start.
let disambiguate (r : Repository) (startOfHash : string) : Hash list =
let objectDir = Repository.objectDir r
match startOfHash.Length with
| 0 -> objectDir.EnumerateFiles ("*", SearchOption.AllDirectories)
| 1 ->
if r.IsCaseSensitive then
objectDir.EnumerateDirectories ("*", SearchOption.AllDirectories)
|> Seq.filter (fun dir -> dir.Name.[0] = startOfHash.[0])
|> Seq.collect (fun dir -> dir.EnumerateFiles "*")
else
objectDir.EnumerateDirectories (sprintf "%c*" startOfHash.[0], SearchOption.AllDirectories)
|> Seq.collect (fun dir -> dir.EnumerateFiles "*")
| 2 ->
let subDir =
r.Fs.Path.Combine (objectDir.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
let subDir =
r.Fs.Path.Combine (objectDir.FullName, prefix)
|> r.Fs.DirectoryInfo.FromDirectoryName
if subDir.Exists then
if r.IsCaseSensitive then
subDir.EnumerateFiles ()
|> Seq.filter (fun i -> i.Name.StartsWith suffix)
else
subDir.EnumerateFiles ()
|> Seq.filter (fun i -> i.Name.StartsWith (suffix, true, null))
else
Seq.empty
|> Seq.map (fun i -> sprintf "%s%s" i.Directory.Name i.Name)
|> Seq.map Hash.ofString
|> List.ofSeq
let getType (o : Object) : ObjectType = let getType (o : Object) : ObjectType =
match o with match o with

View File

@@ -4,7 +4,6 @@ open System
open System.IO open System.IO
open System.IO.Abstractions open System.IO.Abstractions
open Git.Internals open Git.Internals
open Force.Crc32
open ICSharpCode.SharpZipLib.Zip.Compression open ICSharpCode.SharpZipLib.Zip.Compression
open ICSharpCode.SharpZipLib.Zip.Compression.Streams open ICSharpCode.SharpZipLib.Zip.Compression.Streams
@@ -206,23 +205,13 @@ module PackFile =
let toRet = let toRet =
match objectType, preamble with match objectType, preamble with
| PackObjectType.ObjBlob, None -> | PackObjectType.ObjBlob, None -> Object.Blob decompressedObject |> ParsedPackObject.Object
Object.Blob decompressedObject
|> ParsedPackObject.Object
| PackObjectType.ObjCommit, None -> | PackObjectType.ObjCommit, None ->
Commit.decode decompressedObject Commit.decode decompressedObject |> Object.Commit |> ParsedPackObject.Object
|> Object.Commit | PackObjectType.ObjTree, None -> Tree.decode decompressedObject |> Object.Tree |> ParsedPackObject.Object
|> ParsedPackObject.Object
| PackObjectType.ObjTree, None ->
Tree.decode decompressedObject
|> Object.Tree
|> ParsedPackObject.Object
| PackObjectType.ObjOfsDelta, Some (preamble, _) -> ParsedPackObject.Delta (preamble, decompressedObject) | PackObjectType.ObjOfsDelta, Some (preamble, _) -> ParsedPackObject.Delta (preamble, decompressedObject)
| PackObjectType.ObjRefDelta, Some (preamble, _) -> ParsedPackObject.Delta (preamble, decompressedObject) | PackObjectType.ObjRefDelta, Some (preamble, _) -> ParsedPackObject.Delta (preamble, decompressedObject)
| PackObjectType.ObjTag, None -> | PackObjectType.ObjTag, None -> Tag.decode decompressedObject |> Object.Tag |> ParsedPackObject.Object
Tag.decode decompressedObject
|> Object.Tag
|> ParsedPackObject.Object
| PackObjectType.ObjBlob, Some _ | PackObjectType.ObjBlob, Some _
| PackObjectType.ObjTag, Some _ | PackObjectType.ObjTag, Some _
| PackObjectType.ObjTree, Some _ | PackObjectType.ObjTree, Some _
@@ -265,14 +254,10 @@ module PackFile =
let private resolveDeltas (packs : (Hash * uint64 * (ParsedPackObject * PackObjectMetadata)) array) : PackObject[] = let private resolveDeltas (packs : (Hash * uint64 * (ParsedPackObject * PackObjectMetadata)) array) : PackObject[] =
let packsByOffset = let packsByOffset =
packs packs |> Seq.map (fun (hash, offset, data) -> offset, (hash, data)) |> Map.ofSeq
|> Seq.map (fun (hash, offset, data) -> offset, (hash, data))
|> Map.ofSeq
let packsByHash = let packsByHash =
packs packs |> Seq.map (fun (hash, offset, data) -> hash, (offset, data)) |> Map.ofSeq
|> Seq.map (fun (hash, offset, data) -> hash, (offset, data))
|> Map.ofSeq
let rec resolve (object : ParsedPackObject) (name : Hash) (metadata : PackObjectMetadata) : PackObject = let rec resolve (object : ParsedPackObject) (name : Hash) (metadata : PackObjectMetadata) : PackObject =
match object with match object with
@@ -324,8 +309,7 @@ module PackFile =
let nextObjectIndex = let nextObjectIndex =
// TODO probably binary search this, or maintain an incrementing // TODO probably binary search this, or maintain an incrementing
// counter // counter
sortedObjectPositions sortedObjectPositions |> Array.tryFindIndex (fun pos -> pos > offset)
|> Array.tryFindIndex (fun pos -> pos > offset)
// Account for the case where the index file contains garbage // Account for the case where the index file contains garbage
let startingIndex = let startingIndex =
@@ -334,12 +318,10 @@ module PackFile =
| Some 0 -> uint64 stream.Position | Some 0 -> uint64 stream.Position
| Some i -> sortedObjectPositions.[i - 1] | Some i -> sortedObjectPositions.[i - 1]
stream.Seek (int64 startingIndex, SeekOrigin.Begin) stream.Seek (int64 startingIndex, SeekOrigin.Begin) |> ignore
|> ignore
let nextObjectPosition = let nextObjectPosition =
nextObjectIndex nextObjectIndex |> Option.map (fun i -> sortedObjectPositions.[i])
|> Option.map (fun i -> sortedObjectPositions.[i])
Hash.ofBytes name, startingIndex, parseObject nextObjectPosition crc stream Hash.ofBytes name, startingIndex, parseObject nextObjectPosition crc stream
) )
@@ -389,13 +371,10 @@ module PackFile =
let remainingBytes = Stream.consume s 3 let remainingBytes = Stream.consume s 3
if firstByte >= 128uy then if firstByte >= 128uy then
toUint remainingBytes toUint remainingBytes + ((uint32 (firstByte % 128uy)) <<< 24)
+ ((uint32 (firstByte % 128uy)) <<< 24)
|> PackIndexOffset.LayerFiveEntry |> PackIndexOffset.LayerFiveEntry
else else
toUint remainingBytes toUint remainingBytes + ((uint32 firstByte) <<< 24) |> PackIndexOffset.RawOffset
+ ((uint32 firstByte) <<< 24)
|> PackIndexOffset.RawOffset
let readIndex (file : IFileInfo) : PackIndex = let readIndex (file : IFileInfo) : PackIndex =
use s = file.OpenRead () use s = file.OpenRead ()
@@ -523,16 +502,14 @@ module PackFile =
if nameLookup = 0uy then if nameLookup = 0uy then
0L, Stream.consume packIndex 4 |> toUint |> int64 0L, Stream.consume packIndex 4 |> toUint |> int64
else else
packIndex.Seek ((int64 (nameLookup - 1uy)) * 4L, SeekOrigin.Current) packIndex.Seek ((int64 (nameLookup - 1uy)) * 4L, SeekOrigin.Current) |> ignore
|> ignore
let before = Stream.consume packIndex 4 |> toUint |> int64 let before = Stream.consume packIndex 4 |> toUint |> int64
let after = Stream.consume packIndex 4 |> toUint |> int64 let after = Stream.consume packIndex 4 |> toUint |> int64
before, after before, after
let totalCount = let totalCount =
packIndex.Seek (4L + 4L + 255L * 4L, SeekOrigin.Begin) packIndex.Seek (4L + 4L + 255L * 4L, SeekOrigin.Begin) |> ignore
|> ignore
Stream.consume packIndex 4 |> toUint |> int64 Stream.consume packIndex 4 |> toUint |> int64
@@ -566,14 +543,7 @@ module PackFile =
| None -> None | None -> None
| Some location -> | Some location ->
packIndex.Seek ( packIndex.Seek (4L + 4L + 256L * 4L + totalCount * 24L + location * 4L, SeekOrigin.Begin)
4L
+ 4L
+ 256L * 4L
+ totalCount * 24L
+ location * 4L,
SeekOrigin.Begin
)
|> ignore |> ignore
let index = consumeOffset packIndex let index = consumeOffset packIndex
@@ -582,14 +552,7 @@ module PackFile =
match index with match index with
| PackIndexOffset.RawOffset i -> int64 i | PackIndexOffset.RawOffset i -> int64 i
| PackIndexOffset.LayerFiveEntry entry -> | PackIndexOffset.LayerFiveEntry entry ->
packIndex.Seek ( packIndex.Seek (4L + 4L + 256L * 4L + totalCount * 28L + (int64 entry) * 8L, SeekOrigin.Begin)
4L
+ 4L
+ 256L * 4L
+ totalCount * 28L
+ (int64 entry) * 8L,
SeekOrigin.Begin
)
|> ignore |> ignore
Stream.consume packIndex 8 |> toUint64 |> int64 Stream.consume packIndex 8 |> toUint64 |> int64
@@ -610,16 +573,27 @@ module PackFile =
match subObject with match subObject with
| None -> failwithf "Failed to find sub-object with name %s" (Hash.toString name) | None -> failwithf "Failed to find sub-object with name %s" (Hash.toString name)
| Some subObject -> | Some subObject -> (subObject, data, hash, metadata) |> PackObject.Delta |> Some
(subObject, data, hash, metadata) | Preamble.Offset offset -> (failwith "", data, hash, metadata) |> PackObject.Delta |> Some
|> PackObject.Delta
|> Some
| Preamble.Offset offset ->
(failwith "", data, hash, metadata)
|> PackObject.Delta
|> Some
let locateObject (h : Hash) (packIndex : IFileInfo) (packFile : IFileInfo) : PackObject option = let locateObject (h : Hash) (packIndex : IFileInfo) (packFile : IFileInfo) : PackObject option =
use index = packIndex.OpenRead () use index = packIndex.OpenRead ()
use file = packFile.OpenRead () use file = packFile.OpenRead ()
locateObjectInStream h index file locateObjectInStream h index file
/// Get all the pack files in this repository.
/// The resulting hash is a hash that can be interpolated into e.g. `id-%s.pack`,
/// or passed straight into `VerifyPack.verify`.
let allPacks (repo : Repository) : (Hash * IFileInfo) seq =
let gitDir = Repository.gitDir repo
let fs = repo.Fs
fs.Path.Combine (gitDir.FullName, "objects", "pack")
|> fs.DirectoryInfo.FromDirectoryName
|> fun di -> di.EnumerateFiles "*.pack"
|> Seq.map (fun fi ->
let hash =
fi.FullName |> String.chopStart "id-" |> String.chopEnd ".pack" |> Hash.ofString
hash, fi
)

View File

@@ -6,7 +6,7 @@ open System.IO
open System.Text open System.Text
open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames
type OneOf = OneOf of string list type OneOf = | OneOf of string list
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Parse = module Parse =
@@ -53,9 +53,7 @@ module Parse =
w, h w, h
let consumePerson (id : string) (s : Stream) = let consumePerson (id : string) (s : Stream) =
let name = let name = Stream.consumeTo s (byte '<') |> Option.map Encoding.UTF8.GetString
Stream.consumeTo s (byte '<')
|> Option.map Encoding.UTF8.GetString
match name with match name with
| None -> failwithf "No %s name present in object." id | None -> failwithf "No %s name present in object." id
@@ -66,9 +64,7 @@ module Parse =
let name = name.Substring (0, name.Length - 1) let name = name.Substring (0, name.Length - 1)
let email = let email = Stream.consumeTo s (byte '>') |> Option.map Encoding.UTF8.GetString
Stream.consumeTo s (byte '>')
|> Option.map Encoding.UTF8.GetString
match email with match email with
| None -> failwithf "No %s email present in object." id | None -> failwithf "No %s email present in object." id
@@ -90,9 +86,7 @@ module Parse =
let timestamp = parseInt timestamp * 1<second> let timestamp = parseInt timestamp * 1<second>
let offset = let offset = Stream.consumeTo s 10uy |> Option.map Encoding.UTF8.GetString
Stream.consumeTo s 10uy
|> Option.map Encoding.UTF8.GetString
match offset with match offset with
| None -> failwithf "Commit object ended before %s timezone" id | None -> failwithf "Commit object ended before %s timezone" id

View File

@@ -1,7 +1,14 @@
namespace Git namespace Git
open System
open System.IO
type ReferenceUpdate = { Was : Hash option ; Now : Hash }
type ReferenceUpdate =
{
Was : Hash option
Now : Hash
}
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Reference = module Reference =
@@ -11,29 +18,40 @@ module Reference =
|> r.Fs.FileInfo.FromFileName |> r.Fs.FileInfo.FromFileName
let was = let was =
if refFile.Exists then try
r.Fs.File.ReadAllText refFile.FullName r.Fs.File.ReadAllText refFile.FullName |> Some
|> Hash.ofString with :? FileNotFoundException ->
|> Some
else
do
use _v = refFile.Create ()
()
None None
|> Option.map Hash.ofString
r.Fs.File.WriteAllText (refFile.FullName, hash.ToString ()) r.Fs.File.WriteAllText (refFile.FullName, hash.ToString ())
{ Was = was ; Now = hash }
{
Was = was
Now = hash
}
let lookup (r : Repository) (name : string) : Hash option = let lookup (r : Repository) (name : string) : Hash option =
let refFile = let lookup (name : string) =
r.Fs.Path.Combine ((Repository.refDir r).FullName, "heads", name) let refFile =
|> r.Fs.FileInfo.FromFileName r.Fs.Path.Combine ((Repository.gitDir r).FullName, name)
|> r.Fs.FileInfo.FromFileName
if refFile.Exists then try
Some (
r.Fs.File.ReadAllText refFile.FullName r.Fs.File.ReadAllText refFile.FullName
|> String.chopEnd "\n"
|> Hash.ofString |> Hash.ofString
) |> Some
else with
None | :? FileNotFoundException
| :? DirectoryNotFoundException -> None
seq {
yield name
yield r.Fs.Path.Combine ("refs", name)
yield r.Fs.Path.Combine ("refs", "tags", name)
yield r.Fs.Path.Combine ("refs", "heads", name)
yield r.Fs.Path.Combine ("refs", "remotes", name)
yield r.Fs.Path.Combine ("refs", "remotes", name, "HEAD")
}
|> Seq.tryPick lookup

65
Git/RevParse.fs Normal file
View File

@@ -0,0 +1,65 @@
namespace Git
open System
open System.IO
[<RequireQualifiedAccess>]
module RevParse =
/// Get the object hashes which match this start, from among the loose objects.
let disambiguateLooseHash (r : Repository) (startOfHash : string) : Hash list =
let objectDir = Repository.objectDir r
match startOfHash.Length with
| 0 -> objectDir.EnumerateFiles ("*", SearchOption.AllDirectories)
| 1 ->
if r.IsCaseSensitive then
objectDir.EnumerateDirectories ("*", SearchOption.AllDirectories)
|> Seq.filter (fun dir -> dir.Name.[0] = startOfHash.[0])
|> Seq.collect (fun dir -> dir.EnumerateFiles "*")
else
objectDir.EnumerateDirectories (sprintf "%c*" startOfHash.[0], SearchOption.AllDirectories)
|> Seq.collect (fun dir -> dir.EnumerateFiles "*")
| 2 ->
let subDir =
r.Fs.Path.Combine (objectDir.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
let subDir =
r.Fs.Path.Combine (objectDir.FullName, prefix)
|> r.Fs.DirectoryInfo.FromDirectoryName
if subDir.Exists then
if r.IsCaseSensitive then
subDir.EnumerateFiles ()
|> Seq.filter (fun i -> i.Name.StartsWith (suffix, StringComparison.Ordinal))
else
subDir.EnumerateFiles ()
|> Seq.filter (fun i -> i.Name.StartsWith (suffix, StringComparison.OrdinalIgnoreCase))
else
Seq.empty
|> Seq.map (fun i -> sprintf "%s%s" i.Directory.Name i.Name)
|> Seq.map Hash.ofString
|> List.ofSeq
let rec parse (repo : Repository) (s : string) : Hash list =
match s with
| "@" -> parse repo "HEAD"
| _ ->
let fromBranchName = Reference.lookup repo s
let fromHash = disambiguateLooseHash repo s
Option.toList fromBranchName @ fromHash
//let disambiguatePackedHash (r : Repository) (startOfHash : string) : Hash list =
// let packs = PackFile.allPacks r

18
Git/String.fs Normal file
View File

@@ -0,0 +1,18 @@
namespace Git
open System
[<RequireQualifiedAccess>]
module internal String =
let chopStart (toChop : string) (s : string) =
if s.StartsWith (toChop, StringComparison.Ordinal) then
s.Substring toChop.Length
else
s
let chopEnd (toChop : string) (s : string) =
if s.EndsWith (toChop, StringComparison.Ordinal) then
s.Substring (0, s.Length - toChop.Length)
else
s

View File

@@ -1,9 +1,11 @@
namespace Git namespace Git
open System
open System.IO
open System.IO.Abstractions open System.IO.Abstractions
/// The target of a symbolic reference, e.g. "refs/heads/blah". /// The target of a symbolic reference, e.g. "refs/heads/blah".
type SymbolicRefTarget = SymbolicRefTarget of string type SymbolicRefTarget = | SymbolicRefTarget of string
type SymbolicRef = type SymbolicRef =
| CherryPickHead | CherryPickHead
@@ -34,8 +36,21 @@ module SymbolicRef =
|> r.Fs.FileInfo.FromFileName |> r.Fs.FileInfo.FromFileName
type SymbolicRefLookupError = type SymbolicRefLookupError =
| RefDidNotExist | RefDidNotExist of SymbolicRef
| MalformedRef of string | MalformedRef of SymbolicRef * string
override this.ToString () =
match this with
| SymbolicRefLookupError.RefDidNotExist s -> sprintf "Symbolic ref %s did not exist" (string<SymbolicRef> s)
| SymbolicRefLookupError.MalformedRef (ref, contents) ->
sprintf "Symbolic ref %s had malformed contents: %s" (string<SymbolicRef> ref) contents
type SymbolicRefWriteError =
| PointingOutsideRefs of SymbolicRef
override this.ToString () =
match this with
| SymbolicRefWriteError.PointingOutsideRefs ref -> sprintf "refusing to point %O outside of refs/" ref
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module SymbolicReference = module SymbolicReference =
@@ -44,20 +59,32 @@ module SymbolicReference =
let lookup (r : Repository) (name : SymbolicRef) : Result<SymbolicRefTarget, SymbolicRefLookupError> = let lookup (r : Repository) (name : SymbolicRef) : Result<SymbolicRefTarget, SymbolicRefLookupError> =
let f = SymbolicRef.getFile r name let f = SymbolicRef.getFile r name
if not <| f.Exists then let text =
Error RefDidNotExist try
r.Fs.File.ReadAllText f.FullName |> Ok
with :? FileNotFoundException ->
Error (RefDidNotExist name)
text
|> Result.bind (fun contents ->
if not (contents.StartsWith ("ref: ", StringComparison.Ordinal)) then
Error (MalformedRef (name, contents))
elif not (contents.EndsWith ("\n", StringComparison.Ordinal)) then
Error (MalformedRef (name, contents))
else
// Omit the trailing newline
contents.Substring (5, contents.Length - 6) |> SymbolicRefTarget |> Ok
)
let write (r : Repository) (name : SymbolicRef) (contents : string) : Result<unit, SymbolicRefWriteError> =
if not <| contents.StartsWith ("refs/", StringComparison.Ordinal) then
Error (SymbolicRefWriteError.PointingOutsideRefs name)
else else
r.Fs.File.ReadAllText f.FullName
|> fun contents ->
if contents.Substring (0, 5) = "ref: " then
contents.Substring 5 |> SymbolicRefTarget |> Ok
else
Error (MalformedRef contents)
let write (r : Repository) (name : SymbolicRef) (contents : string) : unit = r.Fs.File.WriteAllText ((SymbolicRef.getFile r name).FullName, sprintf "ref: %s\n" contents)
if not <| contents.StartsWith "refs/" then Ok ()
failwithf "refusing to point %O outside of refs/" name
r.Fs.File.WriteAllText ((SymbolicRef.getFile r name).FullName, sprintf "ref: %s" contents) let delete (r : Repository) (name : SymbolicRef) : unit =
let underlyingFile = SymbolicRef.getFile r name
let delete (r : Repository) (name : SymbolicRef) : unit = (SymbolicRef.getFile r name).Delete () underlyingFile.Delete ()

View File

@@ -38,35 +38,28 @@ module Tag =
use ms = new MemoryStream (file) use ms = new MemoryStream (file)
let objectHash = let objectHash =
Parse.consumeWord "tag" (OneOf [ "object" ]) ms Parse.consumeWord "tag" (OneOf [ "object" ]) ms |> ignore
|> ignore
match Stream.consumeTo ms (byte '\n') with match Stream.consumeTo ms (byte '\n') with
| None -> failwith "Tag object should have had a newline in" | None -> failwith "Tag object should have had a newline in"
| Some h -> h |> Hash.ofSpelling | Some h -> h |> Hash.ofSpelling
let typeReferredTo = let typeReferredTo =
Parse.consumeWord "tag" (OneOf [ "type" ]) ms Parse.consumeWord "tag" (OneOf [ "type" ]) ms |> ignore
|> ignore
match Stream.consumeTo ms (byte '\n') with match Stream.consumeTo ms (byte '\n') with
| None -> failwith "Tag type should have had a newline in" | None -> failwith "Tag type should have had a newline in"
| Some h -> | Some h -> h |> Encoding.ASCII.GetString |> TaggedObjectType.Parse
h
|> Encoding.ASCII.GetString
|> TaggedObjectType.Parse
let tagName = let tagName =
Parse.consumeWord "tag" (OneOf [ "tag" ]) ms Parse.consumeWord "tag" (OneOf [ "tag" ]) ms |> ignore
|> ignore
match Stream.consumeTo ms (byte '\n') with match Stream.consumeTo ms (byte '\n') with
| None -> failwith "Tag name should have had a newline in" | None -> failwith "Tag name should have had a newline in"
| Some t -> t |> Encoding.ASCII.GetString | Some t -> t |> Encoding.ASCII.GetString
let tagger = let tagger =
Parse.consumeWord "tag" (OneOf [ "tagger" ]) ms Parse.consumeWord "tag" (OneOf [ "tagger" ]) ms |> ignore
|> ignore
Parse.consumePerson "tagger" ms Parse.consumePerson "tagger" ms

View File

@@ -11,10 +11,7 @@ type PackVerificationLine =
let typeString = string<ObjectType> this.Type let typeString = string<ObjectType> this.Type
let padding = let padding =
Array.create Array.create (ObjectType.Commit.ToString().Length - typeString.Length) " "
(ObjectType.Commit.ToString().Length
- typeString.Length)
" "
|> String.concat "" |> String.concat ""
sprintf "%s %s%s %s" (Hash.toString this.Object) typeString padding (string<PackObjectMetadata> this.Metadata) sprintf "%s %s%s %s" (Hash.toString this.Object) typeString padding (string<PackObjectMetadata> this.Metadata)
@@ -77,16 +74,12 @@ module VerifyPack =
let id = Hash.toString idHash let id = Hash.toString idHash
let index = let index =
fs.Path.Combine (packDir, sprintf "pack-%s.idx" id) fs.Path.Combine (packDir, sprintf "pack-%s.idx" id) |> fs.FileInfo.FromFileName
|> fs.FileInfo.FromFileName
let packFile = let packFile =
fs.Path.Combine (packDir, sprintf "pack-%s.pack" id) fs.Path.Combine (packDir, sprintf "pack-%s.pack" id) |> fs.FileInfo.FromFileName
|> fs.FileInfo.FromFileName
let allPacks = let allPacks = PackFile.readIndex index |> PackFile.readAll packFile
PackFile.readIndex index
|> PackFile.readAll packFile
let rec baseObject (o : PackObject) = let rec baseObject (o : PackObject) =
match o with match o with
@@ -151,9 +144,7 @@ module VerifyPack =
let maxChainLength = chainCounts |> Map.toSeq |> Seq.last |> fst let maxChainLength = chainCounts |> Map.toSeq |> Seq.last |> fst
let chainCounts = let chainCounts =
fun length -> fun length -> Map.tryFind length chainCounts |> Option.defaultValue 0
Map.tryFind length chainCounts
|> Option.defaultValue 0
|> Array.init (maxChainLength + 1) // for the 0 index |> Array.init (maxChainLength + 1) // for the 0 index
{ {

View File

@@ -1,4 +1,4 @@
{ {
"version": "6.0.300", "version": "7.0.300",
"rollForward": "latestPatch" "rollForward": "latestFeature"
} }

View File

@@ -3,10 +3,10 @@
import subprocess import subprocess
def check_fantomas(): def check_fantomas():
result = subprocess.run(["dotnet", "tool", "run", "fantomas", "--check", "-r", "."]) result = subprocess.run(["dotnet", "fantomas", "--check", "."])
if result.returncode != 0: if result.returncode != 0:
print(result.stdout) print(result.stdout)
raise Exception(f"Formatting incomplete (return code: {result.returncode}). Consider running `dotnet tool run fantomas -r .`") raise Exception(f"Formatting incomplete (return code: {result.returncode}). Consider running `dotnet fantomas .`")
def main(): def main():