From 31291febe2322e10059e54c353fffb3ec7fe4c6d Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 15 Oct 2013 15:27:16 -0400 Subject: [PATCH] Introduce blob FilteringOptions for content stream Get the content of a blob as it would be checked out to the working directory via the filters. --- LibGit2Sharp.Tests/BlobFixture.cs | 87 ++++++++++++++++++++++++++- LibGit2Sharp/Blob.cs | 18 ++++-- LibGit2Sharp/BlobExtensions.cs | 23 ++++++- LibGit2Sharp/Core/GitBuf.cs | 18 ++++++ LibGit2Sharp/Core/NativeMethods.cs | 10 +++ LibGit2Sharp/Core/Proxy.cs | 26 +++++++- LibGit2Sharp/Core/RawContentStream.cs | 58 +++++++++++++++--- LibGit2Sharp/FilteringOptions.cs | 27 +++++++++ LibGit2Sharp/LibGit2Sharp.csproj | 2 + 9 files changed, 249 insertions(+), 20 deletions(-) create mode 100644 LibGit2Sharp/Core/GitBuf.cs create mode 100644 LibGit2Sharp/FilteringOptions.cs diff --git a/LibGit2Sharp.Tests/BlobFixture.cs b/LibGit2Sharp.Tests/BlobFixture.cs index 6d97a588d..8661c871d 100644 --- a/LibGit2Sharp.Tests/BlobFixture.cs +++ b/LibGit2Sharp.Tests/BlobFixture.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Text; using LibGit2Sharp.Tests.TestHelpers; @@ -22,6 +23,28 @@ public void CanGetBlobAsText() } } + [Fact] + public void CanGetBlobAsFilteredText() + { + using (var repo = new Repository(BareTestRepoPath)) + { + var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); + + var text = blob.ContentAsText(new FilteringOptions("foo.txt")); + + ConfigurationEntry autocrlf = repo.Config.Get("core.autocrlf"); + + if (autocrlf != null && autocrlf.Value) + { + Assert.Equal("hey there\r\n", text); + } + else + { + Assert.Equal("hey there\n", text); + } + } + } + [Theory] [InlineData("ascii", 4, "31 32 33 34")] [InlineData("utf-7", 4, "31 32 33 34")] @@ -99,7 +122,7 @@ public void CanReadBlobStream() { var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); - using (var tr = new StreamReader(blob.ContentStream, Encoding.UTF8)) + using (var tr = new StreamReader(blob.ContentStream(), Encoding.UTF8)) { string content = tr.ReadToEnd(); Assert.Equal("hey there\n", content); @@ -107,6 +130,51 @@ public void CanReadBlobStream() } } + [Fact] + public void CanReadBlobFilteredStream() + { + using (var repo = new Repository(BareTestRepoPath)) + { + var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); + + using (var tr = new StreamReader(blob.ContentStream(new FilteringOptions("foo.txt")), Encoding.UTF8)) + { + string content = tr.ReadToEnd(); + + ConfigurationEntry autocrlf = repo.Config.Get("core.autocrlf"); + + if (autocrlf != null && autocrlf.Value) + { + Assert.Equal("hey there\r\n", content); + } + else + { + Assert.Equal("hey there\n", content); + } + } + } + } + + [Fact] + public void CanReadBlobFilteredStreamOfUnmodifiedBinary() + { + var binaryContent = new byte[] { 0, 1, 2, 3, 4, 5 }; + + string path = CloneBareTestRepo(); + using (var repo = new Repository(path)) + { + using (var stream = new MemoryStream(binaryContent)) + { + Blob blob = repo.ObjectDatabase.CreateBlob(stream); + + using (var filtered = blob.ContentStream(new FilteringOptions("foo.txt"))) + { + Assert.True(StreamEquals(stream, filtered)); + } + } + } + } + public static void CopyStream(Stream input, Stream output) { // Reused from the following Stack Overflow post with permission @@ -120,6 +188,19 @@ public static void CopyStream(Stream input, Stream output) } } + public static bool StreamEquals(Stream one, Stream two) + { + int onebyte, twobyte; + + while ((onebyte = one.ReadByte()) >= 0 && (twobyte = two.ReadByte()) >= 0) + { + if (onebyte != twobyte) + return false; + } + + return true; + } + [Fact] public void CanStageAFileGeneratedFromABlobContentStream() { @@ -143,7 +224,7 @@ public void CanStageAFileGeneratedFromABlobContentStream() var blob = repo.Lookup(entry.Id.Sha); - using (Stream stream = blob.ContentStream) + using (Stream stream = blob.ContentStream()) using (Stream file = File.OpenWrite(Path.Combine(repo.Info.WorkingDirectory, "small.fromblob.txt"))) { CopyStream(stream, file); diff --git a/LibGit2Sharp/Blob.cs b/LibGit2Sharp/Blob.cs index fd17d203c..86134173f 100644 --- a/LibGit2Sharp/Blob.cs +++ b/LibGit2Sharp/Blob.cs @@ -49,12 +49,20 @@ public virtual byte[] Content /// /// Gets the blob content in a . /// - public virtual Stream ContentStream + public virtual Stream ContentStream() { - get - { - return Proxy.git_blob_rawcontent_stream(repo.Handle, Id, Size); - } + return Proxy.git_blob_rawcontent_stream(repo.Handle, Id, Size); + } + + /// + /// Gets the blob content in a as it would be + /// checked out to the working directory. + /// Parameter controlling content filtering behavior + /// + public virtual Stream ContentStream(FilteringOptions filteringOptions) + { + Ensure.ArgumentNotNull(filteringOptions, "filteringOptions"); + return Proxy.git_blob_filtered_content_stream(repo.Handle, Id, filteringOptions.HintPath, false); } } } diff --git a/LibGit2Sharp/BlobExtensions.cs b/LibGit2Sharp/BlobExtensions.cs index a00208ec6..e1964ba91 100644 --- a/LibGit2Sharp/BlobExtensions.cs +++ b/LibGit2Sharp/BlobExtensions.cs @@ -22,7 +22,28 @@ public static string ContentAsText(this Blob blob, Encoding encoding = null) { Ensure.ArgumentNotNull(blob, "blob"); - using (var reader = new StreamReader(blob.ContentStream, encoding ?? Encoding.UTF8, encoding == null)) + using (var reader = new StreamReader(blob.ContentStream(), encoding ?? Encoding.UTF8, encoding == null)) + { + return reader.ReadToEnd(); + } + } + + /// + /// Gets the blob content as it would be checked out to the + /// working directory, decoded with the specified encoding, + /// or according to byte order marks, with UTF8 as fallback, + /// if is null. + /// + /// The blob for which the content will be returned. + /// Parameter controlling content filtering behavior + /// The encoding of the text. (default: detected or UTF8) + /// Blob content as text. + public static string ContentAsText(this Blob blob, FilteringOptions filteringOptions, Encoding encoding = null) + { + Ensure.ArgumentNotNull(blob, "blob"); + Ensure.ArgumentNotNull(filteringOptions, "filteringOptions"); + + using(var reader = new StreamReader(blob.ContentStream(filteringOptions), encoding ?? Encoding.UTF8, encoding == null)) { return reader.ReadToEnd(); } diff --git a/LibGit2Sharp/Core/GitBuf.cs b/LibGit2Sharp/Core/GitBuf.cs new file mode 100644 index 000000000..09860fdc3 --- /dev/null +++ b/LibGit2Sharp/Core/GitBuf.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core.Handles +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitBuf : IDisposable + { + public IntPtr ptr; + public UIntPtr asize; + public UIntPtr size; + + public void Dispose() + { + Proxy.git_buf_free(this); + } + } +} diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 30ce01c48..c84c99110 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -131,6 +131,13 @@ internal static extern int git_blob_create_fromchunks( source_callback fileCallback, IntPtr data); + [DllImport(libgit2)] + internal static extern int git_blob_filtered_content( + GitBuf buf, + GitObjectSafeHandle blob, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath as_path, + [MarshalAs(UnmanagedType.Bool)] bool check_for_binary_data); + [DllImport(libgit2)] internal static extern IntPtr git_blob_rawcontent(GitObjectSafeHandle blob); @@ -182,6 +189,9 @@ internal static extern int git_branch_upstream_name( RepositorySafeHandle repo, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string referenceName); + [DllImport(libgit2)] + internal static extern void git_buf_free(GitBuf buf); + [DllImport(libgit2)] internal static extern int git_checkout_tree( RepositorySafeHandle repo, diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index aeefea824..5fdd63906 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -73,6 +73,20 @@ public static ObjectId git_blob_create_fromfile(RepositorySafeHandle repo, FileP } } + public static UnmanagedMemoryStream git_blob_filtered_content_stream(RepositorySafeHandle repo, ObjectId id, FilePath path, bool check_for_binary_data) + { + var buf = new GitBuf(); + var handle = new ObjectSafeWrapper(id, repo).ObjectPtr; + + return new RawContentStream(handle, h => + { + Ensure.ZeroResult(NativeMethods.git_blob_filtered_content(buf, h, path, check_for_binary_data)); + return buf.ptr; + }, + h => (long)buf.size, + new[] { buf }); + } + public static byte[] git_blob_rawcontent(RepositorySafeHandle repo, ObjectId id, int size) { using (var obj = new ObjectSafeWrapper(id, repo)) @@ -85,7 +99,8 @@ public static byte[] git_blob_rawcontent(RepositorySafeHandle repo, ObjectId id, public static UnmanagedMemoryStream git_blob_rawcontent_stream(RepositorySafeHandle repo, ObjectId id, Int64 size) { - return new RawContentStream(id, repo, NativeMethods.git_blob_rawcontent, size); + var handle = new ObjectSafeWrapper(id, repo).ObjectPtr; + return new RawContentStream(handle, NativeMethods.git_blob_rawcontent, h => size); } public static Int64 git_blob_rawsize(GitObjectSafeHandle obj) @@ -188,6 +203,15 @@ public static string git_branch_upstream_name(RepositorySafeHandle handle, strin #endregion + #region git_buf_ + + public static void git_buf_free(GitBuf buf) + { + NativeMethods.git_buf_free(buf); + } + + #endregion + #region git_checkout_ public static void git_checkout_tree( diff --git a/LibGit2Sharp/Core/RawContentStream.cs b/LibGit2Sharp/Core/RawContentStream.cs index 825b09763..d02fe8b81 100644 --- a/LibGit2Sharp/Core/RawContentStream.cs +++ b/LibGit2Sharp/Core/RawContentStream.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using LibGit2Sharp.Core.Handles; @@ -6,25 +7,62 @@ namespace LibGit2Sharp.Core { internal class RawContentStream : UnmanagedMemoryStream { - private readonly ObjectSafeWrapper wrapper; + private readonly GitObjectSafeHandle handle; + private readonly ICollection linkedResources; - internal RawContentStream(ObjectId id, RepositorySafeHandle repo, - Func bytePtrProvider, long length) - : this(new ObjectSafeWrapper(id, repo), bytePtrProvider, length) + internal unsafe RawContentStream( + GitObjectSafeHandle handle, + Func bytePtrProvider, + Func sizeProvider, + ICollection linkedResources = null) + : base((byte*)Wrap(handle, bytePtrProvider, linkedResources).ToPointer(), + Wrap(handle, sizeProvider, linkedResources)) { + this.handle = handle; + this.linkedResources = linkedResources; } - unsafe RawContentStream(ObjectSafeWrapper wrapper, - Func bytePtrProvider, long length) - : base((byte*)bytePtrProvider(wrapper.ObjectPtr).ToPointer(), length) + private static T Wrap( + GitObjectSafeHandle handle, + Func provider, + IEnumerable linkedResources) { - this.wrapper = wrapper; + T value; + + try + { + value = provider(handle); + } + catch + { + Dispose(handle, linkedResources); + throw; + } + + return value; + } + + private static void Dispose( + GitObjectSafeHandle handle, + IEnumerable linkedResources) + { + handle.SafeDispose(); + + if (linkedResources == null) + { + return; + } + + foreach (IDisposable linkedResource in linkedResources) + { + linkedResource.Dispose(); + } } protected override void Dispose(bool disposing) { base.Dispose(disposing); - wrapper.SafeDispose(); + Dispose(handle, linkedResources); } - } + } } diff --git a/LibGit2Sharp/FilteringOptions.cs b/LibGit2Sharp/FilteringOptions.cs new file mode 100644 index 000000000..f06d5a2a4 --- /dev/null +++ b/LibGit2Sharp/FilteringOptions.cs @@ -0,0 +1,27 @@ +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Allows callers to specify how blob content filters will be applied. + /// + public sealed class FilteringOptions + { + /// + /// Initializes a new instance of the class. + /// + /// The path that a file would be checked out as + public FilteringOptions(string hintPath) + { + Ensure.ArgumentNotNull(hintPath, "hintPath"); + + this.HintPath = hintPath; + } + + /// + /// The path to "hint" to the filters will be used to apply + /// attributes. + /// + public string HintPath { get; private set; } + } +} diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 2d568a804..620847f10 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -73,6 +73,8 @@ + +