summary refs log tree commit diff
path: root/LibGit
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2023-06-05 03:25:53 +0200
committerTheArcaneBrony <myrainbowdash949@gmail.com>2023-06-05 03:25:53 +0200
commit51d820e22a4517dbb06d38a4f07f7c48522ef811 (patch)
tree4a7749cf77223dff2414fd4b73cb17df43d7449e /LibGit
downloadGitTools-master.tar.xz
Initial commit HEAD master
Diffstat (limited to 'LibGit')
-rw-r--r--LibGit/BlobObject.cs71
-rw-r--r--LibGit/CommitObject.cs171
-rw-r--r--LibGit/Extensions/DictionaryExtensions.cs15
-rw-r--r--LibGit/Extensions/HttpClientExtensions.cs19
-rw-r--r--LibGit/Extensions/IEnumerableExtensions.cs45
-rw-r--r--LibGit/Extensions/JsonElementExtensions.cs22
-rw-r--r--LibGit/Extensions/ObjectExtensions.cs15
-rw-r--r--LibGit/Extensions/QueueExtensions.cs26
-rw-r--r--LibGit/Extensions/StreamExtensions.cs211
-rw-r--r--LibGit/Extensions/StringExtensions.cs8
-rw-r--r--LibGit/GitPack.cs135
-rw-r--r--LibGit/GitRepo.cs141
-rw-r--r--LibGit/Interfaces/IRepoSource.cs9
-rw-r--r--LibGit/LibGit.csproj17
-rw-r--r--LibGit/Static.cs6
-rw-r--r--LibGit/TreeObject.cs101
16 files changed, 1012 insertions, 0 deletions
diff --git a/LibGit/BlobObject.cs b/LibGit/BlobObject.cs
new file mode 100644
index 0000000..93fd5f3
--- /dev/null
+++ b/LibGit/BlobObject.cs
@@ -0,0 +1,71 @@
+using System.IO.Compression;
+using System.Text.Json.Serialization;
+using LibGit.Extensions;
+using LibGit.Interfaces;
+
+namespace LibGit;
+
+public class BlobObject
+{
+    [JsonIgnore]
+    public IRepoSource RepoSource { get; }
+    public string ObjectId { get; }
+
+    public BlobObject(IRepoSource repoSource, string objectId)
+    {
+        RepoSource = repoSource;
+        ObjectId = objectId;
+    }
+
+    private const bool _debug = false;
+
+    public string Length { get; set; }
+    public IEnumerable<byte> Data { get; set; }
+
+    public BlobObject ReadFromZlibCompressedObjFile(Stream bytes)
+    {
+        if(_debug) Console.WriteLine($"Decompressing {this.GetType().Name}");
+        using ZLibStream stream = new ZLibStream(bytes, CompressionMode.Decompress);
+        using var result = new MemoryStream();
+        stream.CopyTo(result);
+        stream.Flush();
+        stream.Close();
+        return ReadFromDecompressedObjFile(result);
+    }
+
+    public BlobObject ReadFromDecompressedObjFile(Stream data)
+    {
+        if(_debug) Console.WriteLine("Parsing commit object");
+        // var data = new Queue<byte>(bytes.ToArray());
+        int iters = 0;
+        data.Seek(0, SeekOrigin.Begin);
+        if(_debug)Console.WriteLine($"Iteration {iters}: starting pos: {data.Position}/+{data.Remaining()}/{data.Length}");
+        while (data.Position < data.Length)
+        {
+            if(_debug) Console.WriteLine($"Iteration {iters} ({data.Position}/+{data.Remaining()}/{data.Length})");
+            if (data.StartsWith("tree "))
+                Length = data.ReadNullTerminatedField(asciiPrefix: "tree ").AsString();
+
+            Data = data.ReadBytes(data.Remaining());
+
+            if (data.Remaining() > 0)
+            {
+                Console.WriteLine($"--Unparsed data after {++iters} iteration(s) of parsing CommitObject--");
+                Console.WriteLine(this.ToJson());
+                Console.WriteLine("--HexDump of remaining data--");
+                data.Peek(data.Remaining()).HexDump();
+                //Console.WriteLine($"Unparsed data: {Encoding.UTF8.GetString(data.ToArray())}");
+            }
+            if(iters > 100) throw new Exception("Too many iterations");
+        }
+
+        data.Close();
+        return this;
+    }
+
+    public class TreeObjectEntry
+    {
+        public string Mode { get; set; }
+        public string Hash { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/LibGit/CommitObject.cs b/LibGit/CommitObject.cs
new file mode 100644
index 0000000..c2107df
--- /dev/null
+++ b/LibGit/CommitObject.cs
@@ -0,0 +1,171 @@
+using System.IO.Compression;
+using System.Text.Json.Serialization;
+using LibGit.Extensions;
+using LibGit.Interfaces;
+
+namespace LibGit;
+
+public class CommitObject
+{
+    [JsonIgnore]
+    public IRepoSource RepoSource { get; }
+    public string CommitId { get; }
+
+    public CommitObject(IRepoSource repoSource, string commitId)
+    {
+        RepoSource = repoSource;
+        CommitId = commitId;
+    }
+
+    private const bool _debug = false;
+
+    public string Length { get; set; }
+    public string TreeId { get; set; }
+    public List<string> ParentIds { get; set; } = new();
+
+    public string Author { get; set; }
+
+    //The Arcane Brony <myrainbowdash949@gmail.com> 1682901812 +0200
+    public string Committer { get; set; }
+    public string GpgSignature { get; set; }
+    public string Message { get; set; }
+    public string Description { get; set; }
+
+    public List<CommitObject> Children { get; set; } = new();
+
+    [JsonIgnore]
+    public DateTime CommitDate => new DateTime(long.Parse(Committer.Split(" ").TakeLast(2).First())*TimeSpan.TicksPerSecond)
+        .AddHours(long.Parse(Committer.Split(" ").TakeLast(1).First()[1..3]))
+        .AddMinutes(long.Parse(Committer.Split(" ").TakeLast(1).First()[3..]))
+        .AddYears(1970);
+
+    [JsonIgnore]
+    public DateTime AuthorDate => new DateTime(long.Parse(Author.Split(" ").TakeLast(2).First())*TimeSpan.TicksPerSecond)
+        .AddHours(long.Parse(Committer.Split(" ").TakeLast(1).First()[1..3]))
+        .AddMinutes(long.Parse(Committer.Split(" ").TakeLast(1).First()[3..]))
+        .AddYears(1970);
+
+    [JsonIgnore]
+    public string AuthorEmail => Author.Split(" <").Last().Split("> ").First();
+
+    [JsonIgnore]
+    public string AuthorName => Author.Split(" <").First();
+
+    [JsonIgnore]
+    public string CommitterEmail => Committer.Split(" <").Last().Split("> ").First();
+
+    [JsonIgnore]
+    public string CommitterName => Committer.Split(" <").First();
+
+    public async Task<TreeObject> GetTreeAsync() => new TreeObject(RepoSource, TreeId).ReadFromZlibCompressedObjFile(await RepoSource.GetObjectStreamById(TreeId));
+
+    public CommitObject ReadFromZlibCompressedObjFile(Stream bytes)
+    {
+        if (_debug)
+        {
+            Console.WriteLine($"Decompressing {this.GetType().Name}");
+            Console.WriteLine($"File header:");
+            // bytes.Peek(64).HexDump(16);
+        }
+        using ZLibStream stream = new ZLibStream(bytes, CompressionMode.Decompress);
+        using var result = new MemoryStream();
+        stream.CopyTo(result);
+        stream.Flush();
+        stream.Close();
+        return ReadFromDecompressedObjFile(result);
+    }
+
+    public CommitObject ReadFromDecompressedObjFile(Stream data)
+    {
+        if(_debug) Console.WriteLine("Parsing commit object");
+        // var data = new Queue<byte>(bytes.ToArray());
+        int iters = 0;
+        data.Seek(0, SeekOrigin.Begin);
+        if(_debug)Console.WriteLine($"Iteration {iters}: starting pos: {data.Position}/+{data.Remaining()}/{data.Length}");
+        Length = data.ReadNullTerminatedField(asciiPrefix: "commit ").AsString();
+        while (data.Position < data.Length)
+        {
+            if(_debug) Console.WriteLine($"Iteration {iters} ({data.Position}/+{data.Remaining()}/{data.Length})");
+            if (data.StartsWith("tree "))
+                TreeId = data.ReadTerminatedField((byte)'\n', asciiPrefix: "tree ").AsString();
+            while (data.StartsWith("parent "))
+                ParentIds.Add(data.ReadTerminatedField((byte)'\n', asciiPrefix: "parent ").AsString());
+            if (data.StartsWith("author "))
+                Author = data.ReadTerminatedField((byte)'\n', asciiPrefix: "author ").AsString();
+            if (data.StartsWith("committer "))
+                Committer = data.ReadTerminatedField((byte)'\n', asciiPrefix: "committer ").AsString();;
+            if (data.StartsWith("gpgsig "))
+                GpgSignature = GetGpgSignature(data);
+            if(data.Remaining() >= 1 && (data.Peek() == (byte)'\n' || data.Peek() == (byte)'\r'))
+                data.Skip(1);
+            if(data.Peek() != (byte)'\n')
+                Message = GetMessage(data);
+            while(data.Remaining() >= 1 && (data.Peek() == (byte)'\n' || data.Peek() == (byte)'\r'))
+                data.Skip(1);
+            if(data.Remaining() >= 1 && data.Peek() != (byte)'\n')
+                Description = GetDescription(data);
+            
+            if (data.Remaining() > 0)
+            {
+                Console.WriteLine($"--Unparsed data after {++iters} iteration(s) of parsing CommitObject--");
+                Console.WriteLine(this.ToJson());
+                Console.WriteLine("--HexDump of remaining data--");
+                data.Peek(data.Remaining()).HexDump();
+                //Console.WriteLine($"Unparsed data: {Encoding.UTF8.GetString(data.ToArray())}");
+            }
+            if(iters > 100) throw new Exception("Too many iterations");
+        }
+        
+        data.Close();
+        return this;
+    }
+
+    //parsing
+    private string GetMessage(Stream data)
+    {
+        if(_debug) Console.WriteLine($"--commit.GetMessage--");
+        var message = "";
+        while (data.Remaining() > 0 && data.Peek() != (byte)'\n')
+        {
+            if(_debug) Console.WriteLine($"Commit.GetMessage -- pos: {data.Position}/+{data.Remaining()}/{data.Length} | next: {(char)data.Peek()} | Message: {message}");
+            message += (char)data.ReadByte();
+            
+        }
+
+        // if(data.Count > 0 && data.Peek() == (byte)'\n')
+        // data.Dequeue();
+        return message.Trim();
+    }
+
+    private string GetDescription(Stream data)
+    {
+        if(_debug) Console.WriteLine($"--commit.GetDescription--");
+        var message = "";
+        while (data.Remaining() > 0)
+        {
+            message += (char)data.ReadByte();
+        }
+
+        // if(data.Count > 0 && data.Peek() == (byte)'\n')
+        // data.Dequeue();
+        return message.Trim();
+    }
+
+    private string GetGpgSignature(Stream data)
+    {
+        if(_debug) Console.WriteLine($"--commit.GetGpgSignature--");
+        data.Seek(7, SeekOrigin.Current);
+        var signature = "";
+        while (
+            !signature.EndsWith("-----END PGP SIGNATURE-----\n\n") 
+               && !signature.EndsWith("-----END PGP SIGNATURE-----\n \n")
+               && !signature.EndsWith("-----END SSH SIGNATURE-----\n\n")
+            )
+        {
+            // Console.Write((char)data.Peek());
+            signature += (char)data.ReadByte();
+        }
+
+        return signature;
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/DictionaryExtensions.cs b/LibGit/Extensions/DictionaryExtensions.cs
new file mode 100644
index 0000000..0e6df06
--- /dev/null
+++ b/LibGit/Extensions/DictionaryExtensions.cs
@@ -0,0 +1,15 @@
+namespace LibGit.Extensions;
+
+public static class DictionaryExtensions
+{
+    public static bool ChangeKey<TKey, TValue>(this IDictionary<TKey, TValue> dict, 
+        TKey oldKey, TKey newKey)
+    {
+        TValue value;
+        if (!dict.Remove(oldKey, out value))
+            return false;
+
+        dict[newKey] = value;  // or dict.Add(newKey, value) depending on ur comfort
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/HttpClientExtensions.cs b/LibGit/Extensions/HttpClientExtensions.cs
new file mode 100644
index 0000000..79e3e5f
--- /dev/null
+++ b/LibGit/Extensions/HttpClientExtensions.cs
@@ -0,0 +1,19 @@
+namespace LibGit.Extensions;
+
+public static class HttpClientExtensions
+{
+    public static async Task<bool> CheckSuccessStatus(this HttpClient hc, string url)
+    {
+        //cors causes failure, try to catch
+        try
+        {
+            var resp = await hc.GetAsync(url);
+            return resp.IsSuccessStatusCode;
+        }
+        catch (Exception e)
+        {
+            Console.WriteLine($"Failed to check success status: {e.Message}");
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/IEnumerableExtensions.cs b/LibGit/Extensions/IEnumerableExtensions.cs
new file mode 100644
index 0000000..d8fc54d
--- /dev/null
+++ b/LibGit/Extensions/IEnumerableExtensions.cs
@@ -0,0 +1,45 @@
+using System.IO.Compression;
+using System.Text;
+
+namespace LibGit.Extensions;
+
+public static class IEnumerableExtensions
+{
+    public static bool StartsWith<T>(this IEnumerable<T> list, IEnumerable<T> prefix)
+    {
+        return prefix.SequenceEqual(list.Take(prefix.Count()));
+    }
+
+    public static void HexDump(this IEnumerable<byte> bytes, int width = 32)
+    {
+        var data = new Queue<(string hex, char utf8)>(bytes.ToArray().Select(x => ($"{x:X2}", (char)x)).ToArray());
+        while (data.Count > 0)
+        {
+            var line = data.Dequeue(Math.Min(width, data.Count)).ToArray();
+            Console.WriteLine(
+                string.Join(" ", line.Select(x => x.hex)).PadRight(width * 3)
+                + " | "
+                + string.Join("", line.Select(x => x.utf8))
+                    .Replace('\n', '.')
+                    .Replace('\r', '.')
+                    .Replace('\0', '.')
+                );
+        }
+    }
+
+    public static string AsHexString(this IEnumerable<byte> bytes) => string.Join(' ', bytes.Select(x => $"{x:X2}"));
+    public static string AsString(this IEnumerable<byte> bytes) => Encoding.UTF8.GetString(bytes.ToArray());
+    
+    
+    //zlib decompress 
+    public static byte[] ZlibDecompress(this IEnumerable<byte> bytes)
+    {
+        var inStream = new MemoryStream(bytes.ToArray());
+        using ZLibStream stream = new ZLibStream(inStream, CompressionMode.Decompress);
+        using var result = new MemoryStream();
+        stream.CopyTo(result);
+        stream.Flush();
+        stream.Close();
+        return result.ToArray();
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/JsonElementExtensions.cs b/LibGit/Extensions/JsonElementExtensions.cs
new file mode 100644
index 0000000..dd97013
--- /dev/null
+++ b/LibGit/Extensions/JsonElementExtensions.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace LibGit.Extensions;
+
+public static class JsonElementExtensions
+{
+    public static void FindExtraJsonFields([DisallowNull] this JsonElement? res, Type t)
+    {
+        var props = t.GetProperties();
+        var unknownPropertyFound = false;
+        foreach (var field in res.Value.EnumerateObject())
+        {
+            if (props.Any(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name == field.Name)) continue;
+            Console.WriteLine($"[!!] Unknown property {field.Name} in {t.Name}!");
+            unknownPropertyFound = true;
+        }
+        if(unknownPropertyFound) Console.WriteLine(res.Value.ToJson());
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/ObjectExtensions.cs b/LibGit/Extensions/ObjectExtensions.cs
new file mode 100644
index 0000000..2cdeccd
--- /dev/null
+++ b/LibGit/Extensions/ObjectExtensions.cs
@@ -0,0 +1,15 @@
+using System.Text.Json;
+
+namespace LibGit.Extensions;
+
+public static class ObjectExtensions
+{
+    public static string ToJson(this object obj, bool indent = true, bool ignoreNull = false, bool unsafeContent = false)
+    {
+        var jso = new JsonSerializerOptions();
+        if(indent) jso.WriteIndented = true;
+        if(ignoreNull) jso.IgnoreNullValues = true;
+        if(unsafeContent) jso.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
+        return JsonSerializer.Serialize(obj, jso);
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/QueueExtensions.cs b/LibGit/Extensions/QueueExtensions.cs
new file mode 100644
index 0000000..45b8093
--- /dev/null
+++ b/LibGit/Extensions/QueueExtensions.cs
@@ -0,0 +1,26 @@
+namespace LibGit.Extensions;
+
+public static class QueueExtensions
+{
+public static IEnumerable<T> Dequeue<T>(this Queue<T> queue, int count)
+{
+    for (int i = 0; i < count; i++)
+    {
+        yield return queue.Dequeue();
+    }
+}
+    public static IEnumerable<T> Peek<T>(this Queue<T> queue, int count)
+    {
+        for (int i = 0; i < count; i++)
+        {
+            yield return queue.ElementAt(i);
+        }
+    }
+    public static void Drop<T>(this Queue<T> queue, int count)
+    {
+        for (int i = 0; i < count; i++)
+        {
+            queue.Dequeue();
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/StreamExtensions.cs b/LibGit/Extensions/StreamExtensions.cs
new file mode 100644
index 0000000..6555783
--- /dev/null
+++ b/LibGit/Extensions/StreamExtensions.cs
@@ -0,0 +1,211 @@
+using System.IO.Compression;
+
+namespace LibGit.Extensions;
+
+public static class StreamExtensions
+{
+    private const bool _debug = false;
+
+    public static long Remaining(this Stream stream)
+    {
+        //if (_debug) Console.WriteLine($"stream pos: {stream.Position}, stream len: {stream.Length}, stream rem: {stream.Length - stream.Position}");
+        return stream.Length - stream.Position;
+    }
+
+    public static int Peek(this Stream stream)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't peek a non-readable stream");
+        if (!stream.CanSeek)
+            throw new InvalidOperationException("Can't peek a non-seekable stream");
+
+        int peek = stream.ReadByte();
+        if (peek != -1)
+            stream.Seek(-1, SeekOrigin.Current);
+
+        return peek;
+    }
+
+    public static IEnumerable<byte> Peek(this Stream stream, long count)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't peek a non-readable stream");
+        if (!stream.CanSeek)
+            throw new InvalidOperationException("Can't peek a non-seekable stream");
+        long i;
+        for (i = 0; i < count; i++)
+        {
+            int peek = stream.ReadByte();
+            if (peek == -1)
+            {
+                if(_debug) Console.WriteLine($"Can't peek {count} bytes, only {i} bytes remaining");
+                stream.Seek(-i, SeekOrigin.Current);
+                yield break;
+            }
+
+            yield return (byte)peek;
+        }
+        stream.Seek(-i, SeekOrigin.Current);
+    }
+
+    public static IEnumerable<byte> ReadBytes(this Stream stream, long count)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+        if (!stream.CanSeek)
+            throw new InvalidOperationException("Can't read a non-seekable stream");
+
+        for (long i = 0; i < count; i++)
+        {
+            int read = stream.ReadByte();
+            if (read == -1)
+                yield break;
+            yield return (byte)read;
+        }
+    }
+
+    public static bool StartsWith(this Stream stream, IEnumerable<byte> sequence)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+        if (!stream.CanSeek)
+            throw new InvalidOperationException("Can't read a non-seekable stream");
+
+        if (_debug)
+        {
+            Console.WriteLine($"Expected: {sequence.AsHexString()} ({sequence.AsString()})");
+            Console.WriteLine($"Actual:   {stream.Peek(sequence.Count()).AsHexString()} ({stream.Peek(sequence.Count()).AsString()})");
+        }
+        
+        int readCount = 0;
+        foreach (int b in sequence)
+        {
+            int read = stream.ReadByte();
+            readCount++;
+            if (read == -1)
+            {
+                stream.Seek(-readCount, SeekOrigin.Current);
+                return false;
+            }
+            
+            if (read != b)
+            {
+                if (_debug)
+                {
+                    Console.ForegroundColor = ConsoleColor.Red;
+                    Console.WriteLine("^^".PadLeft(readCount * 3 + 9));
+                    Console.ResetColor();
+                }
+
+                stream.Seek(-readCount, SeekOrigin.Current);
+                return false;
+            }
+        }
+        stream.Seek(-readCount, SeekOrigin.Current);
+        
+        return true;
+    }
+
+    public static bool StartsWith(this Stream stream, string ascii_seq)
+    {
+        return stream.StartsWith(ascii_seq.Select(x => (byte)x));
+    }
+
+    public static Stream Skip(this Stream stream, long count = 1)
+    {
+        if (!stream.CanSeek)
+            throw new InvalidOperationException("Can't skip a non-seekable stream");
+        stream.Seek(count, SeekOrigin.Current);
+        return stream;
+    }
+
+    public static IEnumerable<byte> ReadNullTerminatedField(this Stream stream, IEnumerable<byte>? binaryPrefix = null, string? asciiPrefix = null) => ReadTerminatedField(stream: stream, terminator: 0x00, binaryPrefix: binaryPrefix, asciiPrefix: asciiPrefix);
+    public static IEnumerable<byte> ReadSpaceTerminatedField(this Stream stream, IEnumerable<byte>? binaryPrefix = null, string? asciiPrefix = null) => ReadTerminatedField(stream: stream, terminator: 0x20, binaryPrefix: binaryPrefix, asciiPrefix: asciiPrefix);
+    public static IEnumerable<byte> ReadTerminatedField(this Stream stream, byte terminator, IEnumerable<byte>? binaryPrefix = null, string? asciiPrefix = null)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+        if (binaryPrefix != null)
+            if (!stream.StartsWith(binaryPrefix))
+                throw new InvalidDataException($"Binary prefix {stream.Peek(binaryPrefix.Count()).AsHexString()} does not match expected value of {binaryPrefix.AsHexString()}!");
+            else stream.Skip(binaryPrefix.Count());
+        else if (asciiPrefix != null)
+            if (!stream.StartsWith(asciiPrefix))
+                throw new InvalidDataException($"Text prefix {stream.Peek(asciiPrefix.Length).AsHexString()} ({stream.Peek(asciiPrefix.Length).AsString()}) does not match expected value of {asciiPrefix.AsBytes().AsHexString()} ({asciiPrefix})!");
+            else stream.Skip(asciiPrefix.Length);
+
+        var read = 0;
+        while (stream.Peek() != terminator)
+        {
+            if (_debug) Console.WriteLine($"ReadTerminatedField -- pos: {stream.Position}/+{stream.Remaining()}/{stream.Length} | next: {(char)stream.Peek()} | Length: {read}");
+            if (stream.Peek() == -1)
+            {
+                Console.WriteLine($"Warning: Reached end of stream while reading null-terminated field");
+                yield break;
+            }
+
+            read++;
+            yield return (byte)stream.ReadByte();
+        }
+
+        if (stream.Peek() == terminator) stream.Skip();
+    }
+    
+    public static IEnumerable<byte> ReadToEnd(this Stream stream)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+
+        while (stream.Peek() != -1)
+            yield return (byte)stream.ReadByte();
+    }
+    
+    public static int ReadInt32BE(this Stream stream)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+
+        var bytes = stream.ReadBytes(4).ToArray();
+        
+        if (BitConverter.IsLittleEndian)
+            Array.Reverse(bytes);
+        
+        Console.WriteLine("ReadInt32BE: " + bytes.AsHexString() + " => " + BitConverter.ToInt32(bytes));
+        return BitConverter.ToInt32(bytes);
+    }
+    
+    //read variable length number
+    public static int ReadVLQ(this Stream stream)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+
+        int result = 0;
+        int shift = 0;
+        byte b;
+        do
+        {
+            b = (byte)stream.ReadByte();
+            result |= (b & 0b0111_1111) << shift;
+            shift += 7;
+        } while ((b & 0b1000_0000) != 0);
+
+        return result;
+    }
+    public static int ReadVLQBigEndian(this Stream stream)
+    {
+        if (!stream.CanRead)
+            throw new InvalidOperationException("Can't read a non-readable stream");
+
+        int result = 0;
+        int shift = 0;
+        byte b;
+        do
+        {
+            b = (byte)stream.ReadByte();
+            result = (result << 7) | (b & 0b0111_1111);
+        } while ((b & 0b1000_0000) != 0);
+
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Extensions/StringExtensions.cs b/LibGit/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..f33bd4e
--- /dev/null
+++ b/LibGit/Extensions/StringExtensions.cs
@@ -0,0 +1,8 @@
+using System.Text;
+
+namespace LibGit.Extensions;
+
+public static class StringExtensions
+{
+    public static IEnumerable<byte> AsBytes(this string str) => Encoding.UTF8.GetBytes(str);
+}
\ No newline at end of file
diff --git a/LibGit/GitPack.cs b/LibGit/GitPack.cs
new file mode 100644
index 0000000..58c6edc
--- /dev/null
+++ b/LibGit/GitPack.cs
@@ -0,0 +1,135 @@
+using LibGit.Extensions;
+
+namespace LibGit;
+
+public class GitPack
+{
+    public string PackId { get; set; }
+    public GitRepo Repo { get; set; }
+    
+    public int Version { get; set; }
+    public int ObjectCount { get; set; }
+    public List<GitPackObject> Objects { get; set; } = new List<GitPackObject>();
+    
+    public GitPack Read(Stream stream)
+    {
+        stream.Peek(12).HexDump(16);
+        
+        Console.Write(" Header: "); stream.Peek(04).ToArray()[0..].HexDump(4);
+        Console.Write("Version: "); stream.Peek(08).ToArray()[4..].HexDump(4);
+        Console.Write(" ObjCnt: "); stream.Peek(12).ToArray()[8..].HexDump(4);
+        
+        if(!stream.StartsWith("PACK"))
+            throw new Exception("Invalid pack file header");
+
+        stream.Skip(4);
+        
+        Version = stream.ReadInt32BE();
+        ObjectCount = stream.ReadInt32BE();
+        Console.WriteLine($"Got git v{Version} pack with {ObjectCount} objects");
+        for (int i = 0; i < ObjectCount; i++)
+        {
+            Objects.Add(new GitPackObject().Read(stream));
+        }
+
+        return this;
+    }
+    
+    public GitPack(string packId, GitRepo repo)
+    {
+        PackId = packId;
+        Repo = repo;
+    }
+}
+
+public class GitPackIndex
+{
+    public int Version { get; set; }
+    public int[] fanOutTable = new int[256];
+    public GitPackIndex Read(Stream stream)
+    {
+        if(!stream.StartsWith(new byte[]{0xff,0x74,0x4f,0x63}))
+            throw new Exception("Invalid pack index file header or pack is v1");
+
+        stream.Skip(4);
+        Version = stream.ReadInt32BE();
+        Console.WriteLine($"Got git v{Version} pack index");
+        
+        //fan-out table
+        for (int i = 0; i < 256; i++)
+        {
+            fanOutTable[i] = stream.ReadInt32BE();
+        }
+        
+
+        
+        
+        return this;
+    }
+}
+
+
+public class GitPackObject
+{
+    private const bool _debug = true; 
+    public GitPackObject Read(Stream stream)
+    {
+        stream.Peek(64).HexDump(32);
+        var header = stream.ReadBytes(4).ToArray();
+        ObjType = (GitObjectType)((header[0] & 0b0111_0000) >> 4);
+        if(ObjType == 0 || (int)ObjType == 5 || (int)ObjType > 7)
+            throw new Exception($"Invalid object type: {(int)ObjType}");
+        Size = header[0] & 0b0000_1111;
+        
+        Offset = 0;
+        for (int i = 1; i < 4; i++)
+        {
+            Offset <<= 8;
+            Offset |= header[i];
+        }
+        
+        if ((Size & 0b0000_1000) != 0)
+        {
+            Size <<= 4;
+            Size |= stream.ReadVLQ();
+        }
+
+        // ObjType = Type switch
+        // {
+        //     1 => GitObjectType.Commit,
+        //     2 => GitObjectType.Tree,
+        //     3 => GitObjectType.Blob,
+        //     4 => GitObjectType.Tag,
+        //     5 => GitObjectType.Invalid, 
+        //     6 => GitObjectType.OffsDelta,
+        //     7 => GitObjectType.RefDelta,
+        //     _ => throw new Exception($"Invalid object type {Type}")
+        // };
+
+        if(_debug) Console.WriteLine($"pack obj type: {ObjType} ({(int)ObjType}), size: {Size}, offset: {Offset}, sizeBytes: {SizeBytes}");
+        Console.WriteLine("Data: ");
+        stream.Peek(Size).Take(16).ToArray().HexDump(16);
+        stream.ReadBytes(Size).ZlibDecompress().Take(16).HexDump(16);
+        
+        return this;
+    }
+
+    public GitObjectType ObjType { get; set; }
+
+    public int SizeBytes { get; set; }
+
+    public int Size { get; set; }
+
+    public int Offset { get; set; }
+}
+
+public enum GitObjectType
+{
+    Commit = 1,
+    Tree,
+    Blob,
+    Tag,
+    Invalid, // Reserved for future expansion, see https://git-scm.com/docs/pack-format#_object_types
+    OffsDelta,
+    RefDelta
+}
\ No newline at end of file
diff --git a/LibGit/GitRepo.cs b/LibGit/GitRepo.cs
new file mode 100644
index 0000000..a58bc38
--- /dev/null
+++ b/LibGit/GitRepo.cs
@@ -0,0 +1,141 @@
+using LibGit.Extensions;
+using LibGit.Interfaces;
+
+namespace LibGit;
+
+public class GitRepo
+{
+    public IRepoSource RepoSource;
+
+    public string Name { get; set; }
+    // public string Description
+    // {
+    // get => File.ReadAllText(Path.Join(RepoPath, "description"));
+    // set => File.WriteAllText(Path.Join(RepoPath, "description"), value);
+    // }
+
+    public async Task<CommitObject> GetCommit(string commitId)
+    {
+        if (!commitId.Length.Equals(40)) commitId = await ResolveRef(commitId);
+        commitId = commitId.Trim(' ', '\n', '\r', '\t');
+        string path = Path.Join("objects", commitId[..2], commitId[2..]);
+        var commit = new CommitObject(RepoSource, commitId).ReadFromZlibCompressedObjFile(await RepoSource.GetFileStream(path));
+        return commit;
+    }
+
+    public async IAsyncEnumerable<CommitObject> GetCommits(string commitId, int limit = Int32.MaxValue)
+    {
+        if (!commitId.Length.Equals(40)) commitId = await ResolveRef(commitId);
+        commitId = commitId.Trim(' ', '\n', '\r', '\t');
+        var commit = await GetCommit(commitId);
+        int i = 0;
+        while (commit.ParentIds.Count > 0 && i++ < limit)
+        {
+            yield return commit;
+            commit = await GetCommit(commit.ParentIds[0]);
+        }
+
+        yield return commit;
+        Console.WriteLine($"Reached last commit: {commit.CommitId} ({commit.Message})");
+    }
+
+    public async IAsyncEnumerable<GitRef> GetRefs()
+    {
+        var fs = await RepoSource.GetFileStream("info/refs");
+        while (fs.Remaining() > 0)
+        {
+            yield return new GitRef(
+                commitId: fs.ReadTerminatedField((byte)'\t').AsString(),
+                name: fs.ReadTerminatedField((byte)'\n').AsString(),
+                repo: this
+            );
+        }
+    }
+
+    public async IAsyncEnumerable<GitPack> GetPacks()
+    {
+        var fs = await RepoSource.GetFileStream("objects/info/packs");
+        Console.WriteLine("Found packs file:");
+        fs.Peek(32).HexDump(32);
+        if (fs.Length <= 1)
+        {
+            Console.WriteLine("WARNING: No packs found!");
+            yield break;
+        }
+        while (fs.Remaining() > 0 && fs.Peek() != 0x0A)
+        {
+            //example: P pack-24bd1c46d657f74f40629503d8e5083a9ad36a67.pack
+            var line = fs.ReadTerminatedField((byte)'\n').AsString();
+            if (line.StartsWith("P "))
+            {
+                new GitPackIndex().Read(await RepoSource.GetFileStream($"objects/pack/{line[2..].Replace(".pack", ".idx")}"));
+                yield return new GitPack(
+                    packId: line[2..],
+                    repo: this
+                ).Read(await RepoSource.GetFileStream($"objects/pack/{line[2..]}"));
+            }
+            else
+            {
+                Console.WriteLine($"WARNING: Unknown pack line: {line}");
+            }
+        }
+    }
+
+
+    //utilities
+    public async Task<string> ResolveRef(string commitRef)
+    {
+        if (commitRef == "HEAD")
+        {
+            var head = (await RepoSource.GetFileStream("HEAD")).ReadToEnd().AsString();
+            if (head.StartsWith("ref: "))
+            {
+                return await ResolveRef(head[5..].Trim(' ', '\n', '\r', '\t'));
+            }
+            else
+            {
+                Console.WriteLine($"Found unknown HEAD style: {head}");
+                return head;
+            }
+        }
+
+        if (commitRef.StartsWith("refs/"))
+        {
+            return (await RepoSource.GetFileStream(commitRef)).ReadToEnd().AsString().Trim(' ', '\n', '\r', '\t');
+        }
+
+        throw new ArgumentException($"Unknown commit ref: {commitRef}");
+    }
+
+    public GitRepo(IRepoSource repoSource)
+    {
+        RepoSource = repoSource;
+    }
+}
+
+public class GitRef
+{
+    public string Name { get; set; }
+    public string CommitId { get; set; }
+    public GitRepo Repo { get; set; }
+
+    public GitRef(string name, string commitId, GitRepo repo)
+    {
+        Name = name;
+        CommitId = commitId;
+        Repo = repo;
+    }
+
+    public async Task<CommitObject> GetCommit()
+    {
+        return await Repo.GetCommit(CommitId);
+    }
+
+    public async IAsyncEnumerable<CommitObject> GetCommits(int limit = Int32.MaxValue)
+    {
+        await foreach (var commit in Repo.GetCommits(CommitId, limit))
+        {
+            yield return commit;
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibGit/Interfaces/IRepoSource.cs b/LibGit/Interfaces/IRepoSource.cs
new file mode 100644
index 0000000..e276f60
--- /dev/null
+++ b/LibGit/Interfaces/IRepoSource.cs
@@ -0,0 +1,9 @@
+namespace LibGit.Interfaces;
+
+public interface IRepoSource
+{
+    public string BasePath { get; set; }
+    public Task<Stream> GetFileStream(string path);
+
+    public Task<Stream> GetObjectStreamById(string objectId) => GetFileStream(Path.Join("objects", objectId[..2], objectId[2..]));
+}
\ No newline at end of file
diff --git a/LibGit/LibGit.csproj b/LibGit/LibGit.csproj
new file mode 100644
index 0000000..ac74b55
--- /dev/null
+++ b/LibGit/LibGit.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+    
+    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+        <DebugType>none</DebugType>
+        <DebugSymbols>false</DebugSymbols>
+        <Optimize>true</Optimize>
+        <OutputPath>bin\Release\</OutputPath>
+        <WholeProgramOptimization>true</WholeProgramOptimization>
+    </PropertyGroup>
+
+</Project>
diff --git a/LibGit/Static.cs b/LibGit/Static.cs
new file mode 100644
index 0000000..ec67750
--- /dev/null
+++ b/LibGit/Static.cs
@@ -0,0 +1,6 @@
+namespace LibGit;
+
+public class Static
+{
+    public const bool Debug = false;
+}
\ No newline at end of file
diff --git a/LibGit/TreeObject.cs b/LibGit/TreeObject.cs
new file mode 100644
index 0000000..7d5eb36
--- /dev/null
+++ b/LibGit/TreeObject.cs
@@ -0,0 +1,101 @@
+using System.IO.Compression;
+using System.Text.Json.Serialization;
+using LibGit.Extensions;
+using LibGit.Interfaces;
+
+namespace LibGit;
+
+public class TreeObject
+{
+    [JsonIgnore] public IRepoSource RepoSource { get; }
+    public string ObjectId { get; }
+
+    public TreeObject(IRepoSource repoSource, string objectId)
+    {
+        RepoSource = repoSource;
+        ObjectId = objectId;
+    }
+
+    private const bool _debug = false;
+
+    public string Length { get; set; }
+    public Dictionary<string, TreeObjectEntry> Entries { get; set; } = new();
+
+    public TreeObject ReadFromZlibCompressedObjFile(Stream bytes)
+    {
+        if (_debug) Console.WriteLine($"Decompressing {GetType().Name}");
+        using ZLibStream stream = new ZLibStream(bytes, CompressionMode.Decompress);
+        using var result = new MemoryStream();
+        stream.CopyTo(result);
+        stream.Flush();
+        stream.Close();
+        return ReadFromDecompressedObjFile(result);
+    }
+
+    public TreeObject ReadFromDecompressedObjFile(Stream data)
+    {
+        if (_debug) Console.WriteLine("Parsing tree object");
+        // var data = new Queue<byte>(bytes.ToArray());
+        int iters = 0;
+        data.Seek(0, SeekOrigin.Begin);
+        if (_debug) Console.WriteLine($"Iteration {iters}: starting pos: {data.Position}/+{data.Remaining()}/{data.Length}");
+        Length = data.ReadNullTerminatedField(asciiPrefix: "tree ").AsString();
+        while (data.Remaining() > 20)
+        {
+            if (_debug) Console.WriteLine($"readTree.Iteration {iters} ({data.Position}/+{data.Remaining()}/{data.Length})");
+
+            var entry = ReadFileEntry(data);
+            Entries.Add(entry.Key, entry.Value);
+        }
+
+        if (data.Remaining() > 0)
+        {
+            Console.WriteLine($"--parseTree: Unparsed data after {iters} iteration(s) of parsing TreeObject--");
+            Console.WriteLine(this.ToJson());
+            Console.WriteLine("--HexDump of remaining data--");
+            data.Peek(data.Remaining()).HexDump();
+            //Console.WriteLine($"Unparsed data: {Encoding.UTF8.GetString(data.ToArray())}");
+        }
+
+        data.Close();
+
+        if (_debug)
+        {
+            Console.WriteLine($"-- Read tree object of size {Length} --");
+            foreach (var x in Entries.ToList())
+            {
+                Console.WriteLine($"{x.Value.Mode} {x.Value.Hash} {x.Key}");
+            }
+
+            foreach (var x in Entries.ToList())
+            {
+                Console.WriteLine($"Path: {x.Key}");
+                Console.WriteLine($"Mode: {x.Value.Mode}");
+                Console.WriteLine($"Hash: {x.Value.Hash}");
+            }
+        }
+
+        return this;
+    }
+
+    //parsing
+
+    private static KeyValuePair<string, TreeObjectEntry> ReadFileEntry(Stream data)
+    {
+        if (_debug) Console.WriteLine($"--tree.ReadFileEntry--");
+        var path = "";
+        TreeObjectEntry entry = new();
+        //entry format: <mode> <path>\0<hash>
+        entry.Mode = data.ReadSpaceTerminatedField().AsString();
+        path = data.ReadNullTerminatedField().AsString();
+        entry.Hash = string.Join("", data.ReadBytes(20).Select(x => $"{x:x2}"));
+
+        return new KeyValuePair<string, TreeObjectEntry>(path, entry);
+    }
+
+    public class TreeObjectEntry
+    {
+        public string Mode { get; set; }
+        public string Hash { get; set; }
+    }
+}
\ No newline at end of file