diff --git a/Lib/NativeBinaries/amd64/git2-e0902fb.dll b/Lib/NativeBinaries/amd64/git2-e0902fb.dll deleted file mode 100644 index f4931dbdd..000000000 Binary files a/Lib/NativeBinaries/amd64/git2-e0902fb.dll and /dev/null differ diff --git a/Lib/NativeBinaries/amd64/git2-fb2f3a7.dll b/Lib/NativeBinaries/amd64/git2-fb2f3a7.dll new file mode 100644 index 000000000..787433c54 Binary files /dev/null and b/Lib/NativeBinaries/amd64/git2-fb2f3a7.dll differ diff --git a/Lib/NativeBinaries/amd64/git2-e0902fb.pdb b/Lib/NativeBinaries/amd64/git2-fb2f3a7.pdb similarity index 50% rename from Lib/NativeBinaries/amd64/git2-e0902fb.pdb rename to Lib/NativeBinaries/amd64/git2-fb2f3a7.pdb index bdef63b09..510be3e21 100644 Binary files a/Lib/NativeBinaries/amd64/git2-e0902fb.pdb and b/Lib/NativeBinaries/amd64/git2-fb2f3a7.pdb differ diff --git a/Lib/NativeBinaries/x86/git2-e0902fb.dll b/Lib/NativeBinaries/x86/git2-e0902fb.dll deleted file mode 100644 index b0c55e6cf..000000000 Binary files a/Lib/NativeBinaries/x86/git2-e0902fb.dll and /dev/null differ diff --git a/Lib/NativeBinaries/x86/git2-fb2f3a7.dll b/Lib/NativeBinaries/x86/git2-fb2f3a7.dll new file mode 100644 index 000000000..3a32d8c6c Binary files /dev/null and b/Lib/NativeBinaries/x86/git2-fb2f3a7.dll differ diff --git a/Lib/NativeBinaries/x86/git2-e0902fb.pdb b/Lib/NativeBinaries/x86/git2-fb2f3a7.pdb similarity index 51% rename from Lib/NativeBinaries/x86/git2-e0902fb.pdb rename to Lib/NativeBinaries/x86/git2-fb2f3a7.pdb index d466f239a..3cb578c30 100644 Binary files a/Lib/NativeBinaries/x86/git2-e0902fb.pdb and b/Lib/NativeBinaries/x86/git2-fb2f3a7.pdb differ diff --git a/Lib/NativeBinaries_979.zip b/Lib/NativeBinaries_979.zip new file mode 100644 index 000000000..f677d9ec0 Binary files /dev/null and b/Lib/NativeBinaries_979.zip differ diff --git a/LibGit2Sharp.Tests/FilterFixture.cs b/LibGit2Sharp.Tests/FilterFixture.cs new file mode 100644 index 000000000..fee05398c --- /dev/null +++ b/LibGit2Sharp.Tests/FilterFixture.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.IO; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class FilterFixture : BaseFixture + { + private const int GitPassThrough = -30; + + readonly Func successCallback = (reader, writer) => 0; + + private const string FilterName = "the-filter"; + readonly List attributes = new List { new FilterAttributeEntry("test") }; + + [Fact] + public void CanRegisterFilterWithSingleAttribute() + { + var filter = new EmptyFilter(FilterName, attributes); + Assert.Equal( attributes , filter.Attributes); + } + + [Fact] + public void CanRegisterAndUnregisterTheSameFilter() + { + var filter = new EmptyFilter(FilterName + 1, attributes); + + var registration = GlobalSettings.RegisterFilter(filter); + GlobalSettings.DeregisterFilter(registration); + + var secondRegistration = GlobalSettings.RegisterFilter(filter); + GlobalSettings.DeregisterFilter(secondRegistration); + } + + [Fact] + public void CanRegisterAndDeregisterAfterGarbageCollection() + { + var filter = new EmptyFilter(FilterName + 2, attributes); + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + GC.Collect(); + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Fact] + public void SameFilterIsEqual() + { + var filter = new EmptyFilter(FilterName + 3, attributes); + Assert.Equal(filter, filter); + } + + [Fact] + public void InitCallbackNotMadeWhenFilterNeverUsed() + { + bool called = false; + Func initializeCallback = () => + { + called = true; + return 0; + }; + + var filter = new FakeFilter(FilterName + 11, attributes, + successCallback, + successCallback, + initializeCallback); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + Assert.False(called); + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Fact] + public void InitCallbackMadeWhenUsingTheFilter() + { + bool called = false; + Func initializeCallback = () => + { + called = true; + return 0; + }; + + var filter = new FakeFilter(FilterName + 12, attributes, + successCallback, + successCallback, + initializeCallback); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + Assert.False(called); + + string repoPath = InitNewRepository(); + using (var repo = CreateTestRepository(repoPath)) + { + StageNewFile(repo); + Assert.True(called); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Fact] + public void WhenStagingFileApplyIsCalledWithCleanForCorrectPath() + { + string repoPath = InitNewRepository(); + bool called = false; + + Func clean = (reader, writer) => + { + called = true; + return GitPassThrough; + }; + var filter = new FakeFilter(FilterName + 15, attributes, clean); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + using (var repo = CreateTestRepository(repoPath)) + { + StageNewFile(repo); + Assert.True(called); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Fact] + public void CleanFilterWritesOutputToObjectTree() + { + const string decodedInput = "This is a substitution cipher"; + const string encodedInput = "Guvf vf n fhofgvghgvba pvcure"; + + string repoPath = InitNewRepository(); + + Func cleanCallback = SubstitutionCipherFilter.RotateByThirteenPlaces; + + var filter = new FakeFilter(FilterName + 16, attributes, cleanCallback); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + using (var repo = CreateTestRepository(repoPath)) + { + FileInfo expectedFile = StageNewFile(repo, decodedInput); + var commit = repo.Commit("Clean that file"); + + var blob = (Blob)commit.Tree[expectedFile.Name].Target; + + var textDetected = blob.GetContentText(); + Assert.Equal(encodedInput, textDetected); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + + [Fact] + public void WhenCheckingOutAFileFileSmudgeWritesCorrectFileToWorkingDirectory() + { + const string decodedInput = "This is a substitution cipher"; + const string encodedInput = "Guvf vf n fhofgvghgvba pvcure"; + + const string branchName = "branch"; + string repoPath = InitNewRepository(); + + Func smudgeCallback = SubstitutionCipherFilter.RotateByThirteenPlaces; + + var filter = new FakeFilter(FilterName + 17, attributes, null, smudgeCallback); + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + FileInfo expectedFile = CheckoutFileForSmudge(repoPath, branchName, encodedInput); + + string combine = Path.Combine(repoPath, "..", expectedFile.Name); + string readAllText = File.ReadAllText(combine); + Assert.Equal(decodedInput, readAllText); + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Fact] + public void FilterStreamsAreCoherent() + { + string repoPath = InitNewRepository(); + + bool? inputCanWrite = null, inputCanRead = null, inputCanSeek = null; + bool? outputCanWrite = null, outputCanRead = null, outputCanSeek = null; + + Func assertor = (input, output) => + { + inputCanRead = input.CanRead; + inputCanWrite = input.CanWrite; + inputCanSeek = input.CanSeek; + + outputCanRead = output.CanRead; + outputCanWrite = output.CanWrite; + outputCanSeek = output.CanSeek; + + return GitPassThrough; + }; + + var filter = new FakeFilter(FilterName + 18, attributes, assertor, assertor); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + using (var repo = CreateTestRepository(repoPath)) + { + StageNewFile(repo); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + + Assert.True(inputCanRead.HasValue); + Assert.True(inputCanWrite.HasValue); + Assert.True(inputCanSeek.HasValue); + Assert.True(outputCanRead.HasValue); + Assert.True(outputCanWrite.HasValue); + Assert.True(outputCanSeek.HasValue); + + Assert.True(inputCanRead.Value); + Assert.False(inputCanWrite.Value); + Assert.False(inputCanSeek.Value); + + Assert.False(outputCanRead.Value); + Assert.True(outputCanWrite.Value); + Assert.False(outputCanSeek.Value); + } + + private FileInfo CheckoutFileForSmudge(string repoPath, string branchName, string content) + { + FileInfo expectedPath; + using (var repo = CreateTestRepository(repoPath)) + { + StageNewFile(repo, content); + + repo.Commit("Initial commit"); + + expectedPath = CommitFileOnBranch(repo, branchName, content); + + repo.Checkout("master"); + + repo.Checkout(branchName); + } + return expectedPath; + } + + private static FileInfo CommitFileOnBranch(Repository repo, string branchName, String content) + { + var branch = repo.CreateBranch(branchName); + repo.Checkout(branch.Name); + + FileInfo expectedPath = StageNewFile(repo, content); + repo.Commit("Commit"); + return expectedPath; + } + + private static FileInfo StageNewFile(IRepository repo, string contents = "null") + { + string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", contents); + var stageNewFile = new FileInfo(newFilePath); + repo.Stage(newFilePath); + return stageNewFile; + } + + private Repository CreateTestRepository(string path) + { + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + var repository = new Repository(path, repositoryOptions); + CreateAttributesFile(repository, "* filter=test"); + return repository; + } + + private static void CreateAttributesFile(IRepository repo, string attributeEntry) + { + Touch(repo.Info.WorkingDirectory, ".gitattributes", attributeEntry); + } + + class EmptyFilter : Filter + { + public EmptyFilter(string name, IEnumerable attributes) + : base(name, attributes) + { } + } + + class FakeFilter : Filter + { + private readonly Func cleanCallback; + private readonly Func smudgeCallback; + private readonly Func initCallback; + + public FakeFilter(string name, IEnumerable attributes, + Func cleanCallback = null, + Func smudgeCallback = null, + Func initCallback = null) + : base(name, attributes) + { + this.cleanCallback = cleanCallback; + this.smudgeCallback = smudgeCallback; + this.initCallback = initCallback; + } + + protected override int Clean(string path, Stream input, Stream output) + { + return cleanCallback != null ? cleanCallback(input, output) : base.Clean(path, input, output); + } + + protected override int Smudge(string path, Stream input, Stream output) + { + return smudgeCallback != null ? smudgeCallback(input, output) : base.Smudge(path, input, output); + } + + protected override int Initialize() + { + return initCallback != null ? initCallback() : base.Initialize(); + } + } + } +} diff --git a/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs b/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs new file mode 100644 index 000000000..5f429d001 --- /dev/null +++ b/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.IO; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; +using Xunit.Extensions; + +namespace LibGit2Sharp.Tests +{ + public class FilterSubstitutionCipherFixture : BaseFixture + { + [Fact] + public void SmugdeIsNotCalledForFileWhichDoesNotMatchAnAttributeEntry() + { + const string decodedInput = "This is a substitution cipher"; + const string encodedInput = "Guvf vf n fhofgvghgvba pvcure"; + + var attributes = new List { new FilterAttributeEntry("filter=rot13") }; + var filter = new SubstitutionCipherFilter("cipher-filter", attributes); + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + string repoPath = InitNewRepository(); + string fileName = Guid.NewGuid() + ".rot13"; + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + using (var repo = new Repository(repoPath, repositoryOptions)) + { + CreateAttributesFile(repo, "*.rot13 filter=rot13"); + + var blob = CommitOnBranchAndReturnDatabaseBlob(repo, fileName, decodedInput); + var textDetected = blob.GetContentText(); + + Assert.Equal(encodedInput, textDetected); + Assert.Equal(1, filter.CleanCalledCount); + Assert.Equal(0, filter.SmudgeCalledCount); + + var branch = repo.CreateBranch("delete-files"); + repo.Checkout(branch.Name); + + DeleteFile(repo, fileName); + + repo.Checkout("master"); + + var fileContents = ReadTextFromFile(repo, fileName); + Assert.Equal(1, filter.SmudgeCalledCount); + Assert.Equal(decodedInput, fileContents); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Fact] + public void CorrectlyEncodesAndDecodesInput() + { + const string decodedInput = "This is a substitution cipher"; + const string encodedInput = "Guvf vf n fhofgvghgvba pvcure"; + + var attributes = new List { new FilterAttributeEntry("filter=rot13") }; + var filter = new SubstitutionCipherFilter("cipher-filter", attributes); + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + string repoPath = InitNewRepository(); + string fileName = Guid.NewGuid() + ".rot13"; + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + using (var repo = new Repository(repoPath, repositoryOptions)) + { + CreateAttributesFile(repo, "*.rot13 filter=rot13"); + + var blob = CommitOnBranchAndReturnDatabaseBlob(repo, fileName, decodedInput); + var textDetected = blob.GetContentText(); + + Assert.Equal(encodedInput, textDetected); + Assert.Equal(1, filter.CleanCalledCount); + Assert.Equal(0, filter.SmudgeCalledCount); + + var branch = repo.CreateBranch("delete-files"); + repo.Checkout(branch.Name); + + DeleteFile(repo, fileName); + + repo.Checkout("master"); + + var fileContents = ReadTextFromFile(repo, fileName); + Assert.Equal(1, filter.SmudgeCalledCount); + Assert.Equal(decodedInput, fileContents); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Theory] + [InlineData("*.txt", ".bat", 0, 0)] + [InlineData("*.txt", ".txt", 1, 0)] + public void WhenStagedFileDoesNotMatchPathSpecFileIsNotFiltered(string pathSpec, string fileExtension, int cleanCount, int smudgeCount) + { + const string filterName = "filter=rot13"; + const string decodedInput = "This is a substitution cipher"; + string attributeFileEntry = string.Format("{0} {1}", pathSpec, filterName); + + var filterForAttributes = new List { new FilterAttributeEntry(filterName) }; + var filter = new SubstitutionCipherFilter("cipher-filter", filterForAttributes); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + string repoPath = InitNewRepository(); + string fileName = Guid.NewGuid() + fileExtension; + + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + using (var repo = new Repository(repoPath, repositoryOptions)) + { + CreateAttributesFile(repo, attributeFileEntry); + + CommitOnBranchAndReturnDatabaseBlob(repo, fileName, decodedInput); + + Assert.Equal(cleanCount, filter.CleanCalledCount); + Assert.Equal(smudgeCount, filter.SmudgeCalledCount); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Theory] + [InlineData("filter=rot13", "*.txt filter=rot13", 1)] + [InlineData("filter=rot13", "*.txt filter=fake", 0)] + [InlineData("filter=rot13", "*.bat filter=rot13", 0)] + [InlineData("rot13", "*.txt filter=rot13", 1)] + [InlineData("rot13", "*.txt filter=fake", 0)] + [InlineData("fake", "*.txt filter=fake", 1)] + [InlineData("filter=fake", "*.txt filter=fake", 1)] + [InlineData("filter=fake", "*.bat filter=fake", 0)] + [InlineData("filter=rot13", "*.txt filter=rot13 -crlf", 1)] + public void CleanIsCalledIfAttributeEntryMatches(string filterAttribute, string attributeEntry, int cleanCount) + { + const string decodedInput = "This is a substitution cipher"; + + var filterForAttributes = new List { new FilterAttributeEntry(filterAttribute) }; + var filter = new SubstitutionCipherFilter("cipher-filter", filterForAttributes); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + string repoPath = InitNewRepository(); + string fileName = Guid.NewGuid() + ".txt"; + + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + using (var repo = new Repository(repoPath, repositoryOptions)) + { + CreateAttributesFile(repo, attributeEntry); + + CommitOnBranchAndReturnDatabaseBlob(repo, fileName, decodedInput); + + Assert.Equal(cleanCount, filter.CleanCalledCount); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + } + + [Theory] + [InlineData("filter=rot13", "*.txt filter=rot13", 1)] + [InlineData("filter=rot13", "*.txt filter=fake", 0)] + [InlineData("filter=rot13", "*.bat filter=rot13", 0)] + [InlineData("rot13", "*.txt filter=rot13", 1)] + [InlineData("rot13", "*.txt filter=fake", 0)] + [InlineData("filter=rot13", "*.txt filter=rot13 -crlf", 1)] + public void SmudgeIsCalledIfAttributeEntryMatches(string filterAttribute, string attributeEntry, int smudgeCount) + { + const string decodedInput = "This is a substitution cipher"; + + var filterForAttributes = new List { new FilterAttributeEntry(filterAttribute) }; + var filter = new SubstitutionCipherFilter("cipher-filter", filterForAttributes); + + var filterRegistration = GlobalSettings.RegisterFilter(filter); + + string repoPath = InitNewRepository(); + string fileName = Guid.NewGuid() + ".txt"; + + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + using (var repo = new Repository(repoPath, repositoryOptions)) + { + CreateAttributesFile(repo, attributeEntry); + + CommitOnBranchAndReturnDatabaseBlob(repo, fileName, decodedInput); + + var branch = repo.CreateBranch("delete-files"); + repo.Checkout(branch.Name); + + DeleteFile(repo, fileName); + + repo.Checkout("master"); + + Assert.Equal(smudgeCount, filter.SmudgeCalledCount); + } + + GlobalSettings.DeregisterFilter(filterRegistration); + + } + + private static string ReadTextFromFile(Repository repo, string fileName) + { + return File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, fileName)); + } + + private static void DeleteFile(Repository repo, string fileName) + { + File.Delete(Path.Combine(repo.Info.WorkingDirectory, fileName)); + repo.Stage(fileName); + repo.Commit("remove file"); + } + + private static Blob CommitOnBranchAndReturnDatabaseBlob(Repository repo, string fileName, string input) + { + Touch(repo.Info.WorkingDirectory, fileName, input); + repo.Stage(fileName); + + var commit = repo.Commit("new file"); + + var blob = (Blob)commit.Tree[fileName].Target; + return blob; + } + + private static void CreateAttributesFile(IRepository repo, string attributeEntry) + { + Touch(repo.Info.WorkingDirectory, ".gitattributes", attributeEntry); + } + } +} diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index f93a24f30..1f545cb3f 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -56,6 +56,7 @@ + @@ -98,6 +99,7 @@ + @@ -113,6 +115,7 @@ + diff --git a/LibGit2Sharp.Tests/TestHelpers/SubstitutionCipherFilter.cs b/LibGit2Sharp.Tests/TestHelpers/SubstitutionCipherFilter.cs new file mode 100644 index 000000000..ab29b0d77 --- /dev/null +++ b/LibGit2Sharp.Tests/TestHelpers/SubstitutionCipherFilter.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; + +namespace LibGit2Sharp.Tests.TestHelpers +{ + public class SubstitutionCipherFilter : Filter + { + public int CleanCalledCount = 0; + public int SmudgeCalledCount = 0; + + public SubstitutionCipherFilter(string name, IEnumerable attributes) + : base(name, attributes) + { + } + + protected override int Clean(string path, Stream input, Stream output) + { + CleanCalledCount++; + return RotateByThirteenPlaces(input, output); + } + + protected override int Smudge(string path, Stream input, Stream output) + { + SmudgeCalledCount++; + return RotateByThirteenPlaces(input, output); + } + + public static int RotateByThirteenPlaces(Stream input, Stream output) + { + int value; + + while ((value = input.ReadByte()) != -1) + { + if ((value >= 'a' && value <= 'm') || (value >= 'A' && value <= 'M')) + { + value += 13; + } + else if ((value >= 'n' && value <= 'z') || (value >= 'N' && value <= 'Z')) + { + value -= 13; + } + + output.WriteByte((byte)value); + } + + return 0; + } + } +} diff --git a/LibGit2Sharp/Core/GitBufReadStream.cs b/LibGit2Sharp/Core/GitBufReadStream.cs new file mode 100644 index 000000000..a8b36d16b --- /dev/null +++ b/LibGit2Sharp/Core/GitBufReadStream.cs @@ -0,0 +1,48 @@ +using System; +using System.Globalization; +using System.IO; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp.Core +{ + /// + /// Reads data from a pointer + /// + internal class GitBufReadStream : UnmanagedMemoryStream + { + internal GitBufReadStream(IntPtr gitBufPointer) + : this(gitBufPointer.MarshalAs()) + { } + + private unsafe GitBufReadStream(GitBuf gitBuf) + : base((byte*)gitBuf.ptr, + ConvertToLong(gitBuf.size), + ConvertToLong(gitBuf.asize), + FileAccess.Read) + { } + + private static long ConvertToLong(UIntPtr len) + { + if (len.ToUInt64() > long.MaxValue) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Provided length ({0}) exceeds long.MaxValue ({1}).", + len.ToUInt64(), long.MaxValue)); + } + + return (long)len.ToUInt64(); + } + + public override long Seek(long offset, SeekOrigin loc) + { + throw new NotSupportedException(); + } + + public override bool CanSeek + { + get { return false; } + } + } +} diff --git a/LibGit2Sharp/Core/GitBufWriteStream.cs b/LibGit2Sharp/Core/GitBufWriteStream.cs new file mode 100644 index 000000000..7cc281f22 --- /dev/null +++ b/LibGit2Sharp/Core/GitBufWriteStream.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp.Core +{ + internal class GitBufWriteStream : Stream + { + private readonly IntPtr gitBufPointer; + + internal GitBufWriteStream(IntPtr gitBufPointer) + { + this.gitBufPointer = gitBufPointer; + + //Preallocate the buffer + Proxy.git_buf_grow(gitBufPointer, 4096); + } + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + Proxy.git_buf_put(gitBufPointer, buffer, offset, count); + } + + public override bool CanRead + { + get { return false; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + } +} diff --git a/LibGit2Sharp/Core/GitFilter.cs b/LibGit2Sharp/Core/GitFilter.cs new file mode 100644 index 000000000..06e3ad8e0 --- /dev/null +++ b/LibGit2Sharp/Core/GitFilter.cs @@ -0,0 +1,103 @@ +using System; +using System.Runtime.InteropServices; +namespace LibGit2Sharp.Core +{ + /// + /// A git filter + /// + [StructLayout(LayoutKind.Sequential)] + internal class GitFilter + { + public uint version = 1; + + public IntPtr attributes; + + [MarshalAs(UnmanagedType.FunctionPtr)] + public git_filter_init_fn init; + + [MarshalAs(UnmanagedType.FunctionPtr)] + public git_filter_shutdown_fn shutdown; + + [MarshalAs(UnmanagedType.FunctionPtr)] + public git_filter_check_fn check; + + [MarshalAs(UnmanagedType.FunctionPtr)] + public git_filter_apply_fn apply; + + [MarshalAs(UnmanagedType.FunctionPtr)] + public git_filter_cleanup_fn cleanup; + + /* The libgit2 structure definition ends here. Subsequent fields are for libgit2sharp bookkeeping. */ + + /// + /// Initialize callback on filter + /// + /// Specified as `filter.initialize`, this is an optional callback invoked + /// before a filter is first used. It will be called once at most. + /// + /// If non-NULL, the filter's `initialize` callback will be invoked right + /// before the first use of the filter, so you can defer expensive + /// initialization operations (in case libgit2 is being used in a way that doesn't need the filter). + /// + public delegate int git_filter_init_fn(IntPtr filter); + + /// + /// Shutdown callback on filter + /// + /// Specified as `filter.shutdown`, this is an optional callback invoked + /// when the filter is unregistered or when libgit2 is shutting down. It + /// will be called once at most and should release resources as needed. + /// Typically this function will free the `git_filter` object itself. + /// + public delegate void git_filter_shutdown_fn(IntPtr filter); + + /// + /// Callback to decide if a given source needs this filter + /// Specified as `filter.check`, this is an optional callback that checks if filtering is needed for a given source. + /// + /// It should return 0 if the filter should be applied (i.e. success), GIT_PASSTHROUGH if the filter should + /// not be applied, or an error code to fail out of the filter processing pipeline and return to the caller. + /// + /// The `attr_values` will be set to the values of any attributes given in the filter definition. See `git_filter` below for more detail. + /// + /// The `payload` will be a pointer to a reference payload for the filter. This will start as NULL, but `check` can assign to this + /// pointer for later use by the `apply` callback. Note that the value should be heap allocated (not stack), so that it doesn't go + /// away before the `apply` callback can use it. If a filter allocates and assigns a value to the `payload`, it will need a `cleanup` + /// callback to free the payload. + /// + public delegate int git_filter_check_fn( + GitFilter gitFilter, IntPtr payload, IntPtr filterSource, IntPtr attributeValues); + + /// + /// Callback to actually perform the data filtering + /// + /// Specified as `filter.apply`, this is the callback that actually filters data. + /// If it successfully writes the output, it should return 0. Like `check`, + /// it can return GIT_PASSTHROUGH to indicate that the filter doesn't want to run. + /// Other error codes will stop filter processing and return to the caller. + /// + /// The `payload` value will refer to any payload that was set by the `check` callback. It may be read from or written to as needed. + /// + public delegate int git_filter_apply_fn( + GitFilter gitFilter, IntPtr payload, IntPtr gitBufTo, IntPtr gitBufFrom, IntPtr filterSource); + + /// + /// Callback to clean up after filtering has been applied. Specified as `filter.cleanup`, this is an optional callback invoked + /// after the filter has been applied. If the `check` or `apply` callbacks allocated a `payload` + /// to keep per-source filter state, use this callback to free that payload and release resources as required. + /// + public delegate void git_filter_cleanup_fn(IntPtr gitFilter, IntPtr payload); + } + /// + /// The file source being filtered + /// + [StructLayout(LayoutKind.Sequential)] + internal class GitFilterSource + { + public IntPtr repository; + + public IntPtr path; + + public GitOid oid; + } +} diff --git a/LibGit2Sharp/Core/NativeDllName.cs b/LibGit2Sharp/Core/NativeDllName.cs index 5127c4e4d..1d547e80e 100644 --- a/LibGit2Sharp/Core/NativeDllName.cs +++ b/LibGit2Sharp/Core/NativeDllName.cs @@ -2,6 +2,6 @@ namespace LibGit2Sharp.Core { internal static class NativeDllName { - public const string Name = "git2-e0902fb"; + public const string Name = "git2-fb2f3a7"; } } diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index a3e8cd095..ef33fe02c 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -218,6 +218,12 @@ internal static extern int git_branch_remote_name( RepositorySafeHandle repo, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string canonical_branch_name); + [DllImport(libgit2)] + internal static extern int git_buf_grow(IntPtr buffer, UIntPtr targetSize); + + [DllImport(libgit2)] + internal static extern int git_buf_put(IntPtr buffer, IntPtr data, UIntPtr len); + [DllImport(libgit2)] internal static extern int git_remote_rename( ref GitStrArray problems, @@ -229,7 +235,6 @@ internal delegate int git_remote_rename_problem_cb( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] string problematic_refspec, IntPtr payload); - [DllImport(libgit2)] internal static extern int git_branch_upstream_name( GitBuf buf, @@ -522,6 +527,21 @@ internal static extern int git_diff_find_similar( [DllImport(libgit2)] internal static extern IntPtr git_diff_get_delta(DiffSafeHandle diff, UIntPtr idx); + [DllImport(libgit2)] + internal static extern int git_filter_register( + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, + IntPtr gitFilter, int priority); + + [DllImport(libgit2)] + internal static extern int git_filter_unregister( + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))]string name); + + [DllImport(libgit2)] + internal static extern int git_filter_source_mode(IntPtr source); + + [DllImport(libgit2)] + internal static extern void git_filter_free(IntPtr filterSafeHandle); + [DllImport(libgit2)] internal static extern int git_libgit2_features(); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 5d6dd60cc..8a1c8ee10 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -241,6 +241,33 @@ public static string git_branch_upstream_name(RepositorySafeHandle handle, strin #region git_buf_ + public static void git_buf_grow(IntPtr gitBufPointer, ulong target_size) + { + using (ThreadAffinity()) + { + var res = NativeMethods.git_buf_grow(gitBufPointer, (UIntPtr)target_size); + Ensure.ZeroResult(res); + } + } + + public static void git_buf_put(IntPtr gitBufPointer, byte[] data, int offset, int count) + { + using (ThreadAffinity()) + { + unsafe + { + int res; + + fixed (byte* ptr = data) + { + res = NativeMethods.git_buf_put(gitBufPointer, (IntPtr)ptr, (UIntPtr)count); + } + + Ensure.ZeroResult(res); + } + } + } + public static void git_buf_free(GitBuf buf) { NativeMethods.git_buf_free(buf); @@ -807,6 +834,47 @@ public static GitDiffDelta git_diff_get_delta(DiffSafeHandle diff, int idx) #endregion + #region git_filter_ + + public static void git_filter_register(string name, IntPtr filter, int priority) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_filter_register(name, filter, priority); + if (res == (int)GitErrorCode.Exists) + { + var message = string.Format("A filter with the name '{0}' is already registered", name); + throw new EntryExistsException(message); + } + Ensure.ZeroResult(res); + } + } + + public static void git_filter_unregister(string name) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_filter_unregister(name); + Ensure.ZeroResult(res); + } + } + + public static FilterMode git_filter_source_mode(IntPtr filterSource) + { + var res = NativeMethods.git_filter_source_mode(filterSource); + return (FilterMode)res; + } + + public static void git_filter_free(IntPtr gitFilter) + { + using (ThreadAffinity()) + { + NativeMethods.git_filter_free(gitFilter); + } + } + + #endregion + #region git_graph_ public static Tuple git_graph_ahead_behind(RepositorySafeHandle repo, Commit first, Commit second) diff --git a/LibGit2Sharp/Filter.cs b/LibGit2Sharp/Filter.cs new file mode 100644 index 000000000..082df8581 --- /dev/null +++ b/LibGit2Sharp/Filter.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// A filter is a way to execute code against a file as it moves to and from the git + /// repository and into the working directory. + /// + public abstract class Filter : IEquatable + { + private static readonly LambdaEqualityHelper equalityHelper = + new LambdaEqualityHelper(x => x.Name, x => x.Attributes); + + private readonly string name; + private readonly IEnumerable attributes; + + private readonly GitFilter gitFilter; + + /// + /// Initializes a new instance of the class. + /// And allocates the filter natively. + /// The unique name with which this filtered is registered with + /// A list of attributes which this filter applies to + /// + protected Filter(string name, IEnumerable attributes) + { + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNull(attributes, "attributes"); + + this.name = name; + this.attributes = attributes; + var attributesAsString = string.Join(",", this.attributes.Select(attr => attr.FilterDefinition)); + + gitFilter = new GitFilter + { + attributes = EncodingMarshaler.FromManaged(Encoding.UTF8, attributesAsString), + init = InitializeCallback, + apply = ApplyCallback + }; + } + + /// + /// The name that this filter was registered with + /// + public string Name + { + get { return name; } + } + + /// + /// The filter filterForAttributes. + /// + public IEnumerable Attributes + { + get { return attributes; } + } + + /// + /// The marshalled filter + /// + internal GitFilter GitFilter + { + get { return gitFilter; } + } + + /// + /// Initialize callback on filter + /// + /// Specified as `filter.initialize`, this is an optional callback invoked + /// before a filter is first used. It will be called once at most. + /// + /// If non-NULL, the filter's `initialize` callback will be invoked right + /// before the first use of the filter, so you can defer expensive + /// initialization operations (in case the library is being used in a way + /// that doesn't need the filter. + /// + protected virtual int Initialize() + { + return 0; + } + + /// + /// Clean the input stream and write to the output stream. + /// + /// The path of the file being filtered + /// The git buf input reader + /// The git buf output writer + /// 0 if successful and to skip and pass through + protected virtual int Clean(string path, Stream input, Stream output) + { + return (int)GitErrorCode.PassThrough; + } + + /// + /// Smudge the input stream and write to the output stream. + /// + /// The path of the file being filtered + /// The git buf input reader + /// The git buf output writer + /// 0 if successful and to skip and pass through + protected virtual int Smudge(string path, Stream input, Stream output) + { + return (int)GitErrorCode.PassThrough; + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public override bool Equals(object obj) + { + return Equals(obj as Filter); + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public bool Equals(Filter other) + { + return equalityHelper.Equals(this, other); + } + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return equalityHelper.GetHashCode(this); + } + + /// + /// Tests if two are equal. + /// + /// First to compare. + /// Second to compare. + /// True if the two objects are equal; false otherwise. + public static bool operator ==(Filter left, Filter right) + { + return Equals(left, right); + } + + /// + /// Tests if two are different. + /// + /// First to compare. + /// Second to compare. + /// True if the two objects are different; false otherwise. + public static bool operator !=(Filter left, Filter right) + { + return !Equals(left, right); + } + + /// + /// Initialize callback on filter + /// + /// Specified as `filter.initialize`, this is an optional callback invoked + /// before a filter is first used. It will be called once at most. + /// + /// If non-NULL, the filter's `initialize` callback will be invoked right + /// before the first use of the filter, so you can defer expensive + /// initialization operations (in case libgit2 is being used in a way that doesn't need the filter). + /// + int InitializeCallback(IntPtr filterPointer) + { + return Initialize(); + } + + /// + /// Callback to actually perform the data filtering + /// + /// Specified as `filter.apply`, this is the callback that actually filters data. + /// If it successfully writes the output, it should return 0. Like `check`, + /// it can return GIT_PASSTHROUGH to indicate that the filter doesn't want to run. + /// Other error codes will stop filter processing and return to the caller. + /// + /// The `payload` value will refer to any payload that was set by the `check` callback. It may be read from or written to as needed. + /// + int ApplyCallback(GitFilter filter, IntPtr payload, + IntPtr gitBufferToPtr, IntPtr gitBufferFromPtr, IntPtr filterSourcePtr) + { + var filterSource = FilterSource.FromNativePtr(filterSourcePtr); + using (var reader = new GitBufReadStream(gitBufferFromPtr)) + using (var writer = new GitBufWriteStream(gitBufferToPtr)) + using (var bufferedWriter = new BufferedStream(writer)) + { + return filterSource.SourceMode == FilterMode.Clean ? + Clean(filterSource.Path, reader, bufferedWriter) : + Smudge(filterSource.Path, reader, bufferedWriter); + } + } + } +} diff --git a/LibGit2Sharp/FilterAttributeEntry.cs b/LibGit2Sharp/FilterAttributeEntry.cs new file mode 100644 index 000000000..bd5d434ce --- /dev/null +++ b/LibGit2Sharp/FilterAttributeEntry.cs @@ -0,0 +1,47 @@ +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// The definition for a given filter found in the .gitattributes file. + /// The filter definition will result as 'filter=filterName' + /// + /// In the .gitattributes file a filter will be matched to a pathspec like so + /// '*.txt filter=filterName' + /// + public class FilterAttributeEntry + { + private const string AttributeFilterDefinition = "filter="; + + private readonly string filterDefinition; + + /// + /// For testing purposes + /// + protected FilterAttributeEntry() { } + + /// + /// The name of the filter found in a .gitattributes file + /// + /// The name of the filter + public FilterAttributeEntry(string filterName) + { + Ensure.ArgumentNotNullOrEmptyString(filterName, "filterName"); + + if (!filterName.Contains(AttributeFilterDefinition)) + { + filterName = string.Format("{0}{1}", AttributeFilterDefinition, filterName); + } + + this.filterDefinition = filterName; + } + + /// + /// The filter name in the form of 'filter=filterName' + /// + public virtual string FilterDefinition + { + get { return filterDefinition; } + } + } +} diff --git a/LibGit2Sharp/FilterMode.cs b/LibGit2Sharp/FilterMode.cs new file mode 100644 index 000000000..bed687966 --- /dev/null +++ b/LibGit2Sharp/FilterMode.cs @@ -0,0 +1,20 @@ +namespace LibGit2Sharp +{ + /// + /// These values control which direction of change is with which which a filter is being applied. + /// + public enum FilterMode + { + /// + /// Smudge occurs when exporting a file from the Git object database to the working directory. + /// For example, a file would be smudged during a checkout operation. + /// + Smudge = 0, + + /// + /// Clean occurs when importing a file from the working directory to the Git object database. + /// For example, a file would be cleaned when staging a file. + /// + Clean = (1 << 0), + } +} \ No newline at end of file diff --git a/LibGit2Sharp/FilterRegistration.cs b/LibGit2Sharp/FilterRegistration.cs new file mode 100644 index 000000000..9b4ea4e0f --- /dev/null +++ b/LibGit2Sharp/FilterRegistration.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// An object representing the registration of a Filter type with libgit2 + /// + public sealed class FilterRegistration + { + internal FilterRegistration(Filter filter) + { + Ensure.ArgumentNotNull(filter, "filter"); + Name = filter.Name; + + FilterPointer = Marshal.AllocHGlobal(Marshal.SizeOf(filter.GitFilter)); + Marshal.StructureToPtr(filter.GitFilter, FilterPointer, false); + } + + /// + /// The name of the filter in the libgit2 registry + /// + public string Name { get; private set; } + + internal IntPtr FilterPointer { get; private set; } + + internal void Free() + { + Marshal.FreeHGlobal(FilterPointer); + FilterPointer = IntPtr.Zero; + } + } +} diff --git a/LibGit2Sharp/FilterSource.cs b/LibGit2Sharp/FilterSource.cs new file mode 100644 index 000000000..ee6b2f0b3 --- /dev/null +++ b/LibGit2Sharp/FilterSource.cs @@ -0,0 +1,51 @@ +using System; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// A filter source - describes the direction of filtering and the file being filtered. + /// + public class FilterSource + { + /// + /// Needed for mocking purposes + /// + protected FilterSource() { } + + internal FilterSource(FilePath path, FilterMode mode, GitFilterSource source) + { + SourceMode = mode; + ObjectId = new ObjectId(source.oid); + Path = path.Native; + } + + /// + /// Take an unmanaged pointer and convert it to filter source callback paramater + /// + /// + /// + internal static FilterSource FromNativePtr(IntPtr ptr) + { + var source = ptr.MarshalAs(); + FilePath path = LaxFilePathMarshaler.FromNative(source.path) ?? FilePath.Empty; + FilterMode gitFilterSourceMode = Proxy.git_filter_source_mode(ptr); + return new FilterSource(path, gitFilterSourceMode, source); + } + + /// + /// The filter mode for current file being filtered + /// + public virtual FilterMode SourceMode { get; private set; } + + /// + /// The relative path to the file + /// + public virtual string Path { get; private set; } + + /// + /// The blob id + /// + public virtual ObjectId ObjectId { get; private set; } + } +} diff --git a/LibGit2Sharp/GlobalSettings.cs b/LibGit2Sharp/GlobalSettings.cs index b6ea82454..5501b0769 100644 --- a/LibGit2Sharp/GlobalSettings.cs +++ b/LibGit2Sharp/GlobalSettings.cs @@ -1,5 +1,6 @@ using System; using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; namespace LibGit2Sharp { @@ -108,5 +109,38 @@ public static LogConfiguration LogConfiguration return logConfiguration; } } + + /// + /// Register a filter globally with a default priority of 200 allowing the custom filter + /// to imitate a core Git filter driver. It will be run last on checkout and first on checkin. + /// + public static FilterRegistration RegisterFilter(Filter filter) + { + return RegisterFilter(filter, 200); + } + /// + /// Register a filter globally with given priority for execution. + /// A filter with the priority of 200 will be run last on checkout and first on checkin. + /// A filter with the priority of 0 will be run first on checkout and last on checkin. + /// + public static FilterRegistration RegisterFilter(Filter filter, int priority) + { + var registration = new FilterRegistration(filter); + + Proxy.git_filter_register(filter.Name, registration.FilterPointer, priority); + + return registration; + } + + /// + /// Remove the filter from the registry, and frees the native heap allocation. + /// + public static void DeregisterFilter(FilterRegistration registration) + { + Ensure.ArgumentNotNull(registration, "registration"); + + Proxy.git_filter_unregister(registration.Name); + registration.Free(); + } } } diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index a2e6e616d..a2932b69d 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -68,17 +68,25 @@ + + + + - + + + + + diff --git a/LibGit2Sharp/libgit2_hash.txt b/LibGit2Sharp/libgit2_hash.txt index 4a8fde8aa..9ff5518f6 100644 --- a/LibGit2Sharp/libgit2_hash.txt +++ b/LibGit2Sharp/libgit2_hash.txt @@ -1 +1 @@ -e0902fbce7d14631bd02091c1c70cde3e68f78ab +fb2f3a76aaf9d76b947ca7527797e0eb0b5d057f diff --git a/libgit2 b/libgit2 index e0902fbce..fb2f3a76a 160000 --- a/libgit2 +++ b/libgit2 @@ -1 +1 @@ -Subproject commit e0902fbce7d14631bd02091c1c70cde3e68f78ab +Subproject commit fb2f3a76aaf9d76b947ca7527797e0eb0b5d057f diff --git a/nuget.package/build/LibGit2Sharp.props b/nuget.package/build/LibGit2Sharp.props index f9460e24a..b5de8c573 100644 --- a/nuget.package/build/LibGit2Sharp.props +++ b/nuget.package/build/LibGit2Sharp.props @@ -1,20 +1,20 @@  - - NativeBinaries\amd64\git2-e0902fb.dll + + NativeBinaries\amd64\git2-fb2f3a7.dll PreserveNewest - - NativeBinaries\amd64\git2-e0902fb.pdb + + NativeBinaries\amd64\git2-fb2f3a7.pdb PreserveNewest - - NativeBinaries\x86\git2-e0902fb.dll + + NativeBinaries\x86\git2-fb2f3a7.dll PreserveNewest - - NativeBinaries\x86\git2-e0902fb.pdb + + NativeBinaries\x86\git2-fb2f3a7.pdb PreserveNewest