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
diff --git a/LibGitTest/FileRepoSource.cs b/LibGitTest/FileRepoSource.cs
new file mode 100644
index 0000000..09ec836
--- /dev/null
+++ b/LibGitTest/FileRepoSource.cs
@@ -0,0 +1,18 @@
+using LibGit.Interfaces;
+
+namespace LibGitTest;
+
+public class FileRepoSource : IRepoSource
+{
+ public FileRepoSource(string basePath)
+ {
+ BasePath = basePath;
+ }
+
+ public string BasePath { get; set; }
+
+ public async Task<Stream> GetFileStream(string path)
+ {
+ return File.OpenRead(Path.Join(BasePath, path));
+ }
+}
\ No newline at end of file
diff --git a/LibGitTest/LibGitTest.csproj b/LibGitTest/LibGitTest.csproj
new file mode 100644
index 0000000..ebad2df
--- /dev/null
+++ b/LibGitTest/LibGitTest.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibGit\LibGit.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/LibGitTest/Program.cs b/LibGitTest/Program.cs
new file mode 100644
index 0000000..3d87631
--- /dev/null
+++ b/LibGitTest/Program.cs
@@ -0,0 +1,11 @@
+// See https://aka.ms/new-console-template for more information
+
+using LibGit;
+using LibGit.Extensions;
+using LibGitTest;
+
+Console.WriteLine("Hello, World!");
+
+//await Test1.Run();
+//await Test2.Run();
+await Test3.Run();
\ No newline at end of file
diff --git a/LibGitTest/Test1.cs b/LibGitTest/Test1.cs
new file mode 100644
index 0000000..ac88cf9
--- /dev/null
+++ b/LibGitTest/Test1.cs
@@ -0,0 +1,36 @@
+using LibGit;
+
+namespace LibGitTest;
+
+public class Test1
+{
+ public static async Task Run()
+ {
+ var repo = new GitRepo(new FileRepoSource(@"/home/root@Rory/tmpgit/MatrixRoomUtils.git"));
+// var repo = new GitRepo(new WebRepoSource("https://git.rory.gay/MatrixRoomUtils.git/"));
+ var commit = await repo.GetCommit("HEAD");
+
+ while (commit.ParentIds.Count > 0)
+ {
+ Console.WriteLine($"{commit.CommitId[..7]} | {commit.AuthorName.PadRight(16)} | {commit.Message.PadRight(32)[..32]} | {commit.TreeId}");
+ var tree = await commit.GetTreeAsync();
+ await PrintTreeRecursive(tree);
+
+ commit = await repo.GetCommit(commit.ParentIds.First());
+ }
+
+ async Task PrintTreeRecursive(TreeObject tree, int indent = 0)
+ {
+ foreach (var (key, value) in tree.Entries.Where(x => x.Value.Mode.StartsWith("1")))
+ {
+ Console.WriteLine($"{value.Mode.PadLeft(6)} {value.Hash}{"".PadRight(indent)} {key}");
+ }
+
+ foreach (var (key, value) in tree.Entries.Where(x => !x.Value.Mode.StartsWith("1")))
+ {
+ Console.WriteLine($"{value.Mode.PadLeft(6)}{"".PadRight(indent + 41)} {key + "/"}");
+ await PrintTreeRecursive(new TreeObject(tree.RepoSource, value.Hash).ReadFromZlibCompressedObjFile(await tree.RepoSource.GetObjectStreamById(value.Hash)), indent + 2);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/LibGitTest/Test2.cs b/LibGitTest/Test2.cs
new file mode 100644
index 0000000..c25f9b0
--- /dev/null
+++ b/LibGitTest/Test2.cs
@@ -0,0 +1,67 @@
+using LibGit;
+
+namespace LibGitTest;
+
+public class Test2
+{
+ public static async Task Run()
+ {
+ List<CommitObject> commits = new();
+ List<GitRef> heads = new();
+ var repo = new GitRepo(new WebRepoSource("https://git.rory.gay/.fosscord/fosscord-server.git/")
+ {
+ });
+
+ var ss = new SemaphoreSlim(12,12);
+
+ var _heads = repo.GetRefs().GetAsyncEnumerator();
+ while (await _heads.MoveNextAsync())
+ {
+ heads.Add(_heads.Current);
+ var isCached = await ((WebRepoSource)repo.RepoSource).HasObjectCached(_heads.Current.CommitId);
+ Console.WriteLine(_heads.Current.Name+ " - cache miss: " + !isCached);
+ if (!isCached)
+ {
+
+ var _c = _heads.Current.CommitId;
+#pragma warning disable CS4014
+ Task.Run(async () =>
+#pragma warning restore CS4014
+ {
+ await ss.WaitAsync();
+ Console.WriteLine("hi from task");
+ var a = new GitRepo(new WebRepoSource("https://git.rory.gay/.fosscord/fosscord-server.git/")
+ {
+ }).GetCommits(_c).GetAsyncEnumerator();
+ while (
+ await a.MoveNextAsync()
+ && !await ((WebRepoSource)repo.RepoSource)
+ .HasObjectCached(a.Current.CommitId)
+ ) Console.WriteLine($"Prefetched commit {a.Current.CommitId} with {a.Current.ParentIds.Count()} parents");
+ Console.WriteLine($"Reached already-cached log: {a.Current.CommitId}");
+ ss.Release();
+ });
+ }
+ }
+
+
+ var log = repo.GetCommits(heads.First(x=>x.Name == "refs/heads/master").CommitId).GetAsyncEnumerator();
+ while (await log.MoveNextAsync())
+ {
+ commits.Add(log.Current);
+ if (commits.Count % 50 == 0)
+ {
+ // StateHasChanged();
+ await Task.Delay(1);
+ }
+
+ Console.WriteLine($"Fetched in-log commit {log.Current.CommitId}, {12-ss.CurrentCount} tasks running");
+
+ if (ss.CurrentCount == 12)
+ {
+ Console.WriteLine("All tasks finished");
+ return;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/LibGitTest/Test3.cs b/LibGitTest/Test3.cs
new file mode 100644
index 0000000..be7275f
--- /dev/null
+++ b/LibGitTest/Test3.cs
@@ -0,0 +1,17 @@
+using LibGit;
+using LibGit.Extensions;
+
+namespace LibGitTest;
+
+public class Test3
+{
+ public static async Task Run()
+ {
+ var repo = new GitRepo(new FileRepoSource(@"/home/root@Rory/tmpgit/fosscord-server.git"));
+ var packs = repo.GetPacks().GetAsyncEnumerator();
+ while(await packs.MoveNextAsync())
+ {
+ Console.WriteLine(packs.Current.ToJson());
+ }
+ }
+}
\ No newline at end of file
diff --git a/LibGitTest/WebRepoSource.cs b/LibGitTest/WebRepoSource.cs
new file mode 100644
index 0000000..39d9b79
--- /dev/null
+++ b/LibGitTest/WebRepoSource.cs
@@ -0,0 +1,28 @@
+using LibGit.Interfaces;
+
+namespace LibGitTest;
+
+public class WebRepoSource : IRepoSource
+{
+ private const bool _debug = false;
+ public WebRepoSource(string basePath)
+ {
+ BasePath = basePath;
+ }
+
+ public string BasePath { get; set; }
+
+ public async Task<Stream> GetFileStream(string path)
+ {
+ var client = new HttpClient();
+ if(_debug)Console.WriteLine("Fetching file: " + Path.Join(BasePath, path));
+ var response = await client.GetAsync(Path.Join(BasePath, path));
+ if(!response.IsSuccessStatusCode) throw new Exception("Failed to fetch file: " + Path.Join(BasePath, path));
+ return await response.Content.ReadAsStreamAsync();
+ }
+
+ public async Task<bool> HasObjectCached(string currentCommitId)
+ {
+ return Random.Shared.Next(0, 2) == 1;
+ }
+}
\ No newline at end of file
|