diff --git a/LibGit2Sharp.Tests/FilterFixture.cs b/LibGit2Sharp.Tests/FilterFixture.cs index 2a86e29a2..5e178d0d1 100644 --- a/LibGit2Sharp.Tests/FilterFixture.cs +++ b/LibGit2Sharp.Tests/FilterFixture.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using LibGit2Sharp.Tests.TestHelpers; using Xunit; @@ -11,12 +10,10 @@ public class FilterFixture : BaseFixture { private const int GitPassThrough = -30; - readonly Func, int> checkPassThrough = (source, attr) => GitPassThrough; readonly Func successCallback = (reader, writer) => 0; - readonly Func, int> checkSuccess = (source, attr) => 0; private const string FilterName = "the-filter"; - readonly List attributes = new List{"test"}; + readonly List attributes = new List { new FilterAttribute("test") }; [Fact] public void CanRegisterFilterWithSingleAttribute() @@ -55,101 +52,6 @@ public void SameFilterIsEqual() Assert.Equal(filter, filter); } - [Fact] - public void CheckCallbackNotMadeWhenFileStagedAndFilterNotRegistered() - { - bool called = false; - Func, int> callback = (source, attr) => - { - called = true; - return GitPassThrough; - }; - - string repoPath = InitNewRepository(); - - new FakeFilter(FilterName + 4, attributes, callback); - - using (var repo = CreateTestRepository(repoPath)) - { - StageNewFile(repo); - } - - Assert.False(called); - } - - [Fact] - public void CheckCallbackMadeWhenFileStaged() - { - bool called = false; - Func, int> checkCallBack = (source, attr) => - { - called = true; - return GitPassThrough; - }; - string repoPath = InitNewRepository(); - - var filter = new FakeFilter(FilterName + 5, attributes, checkCallBack); - - var filterRegistration = GlobalSettings.RegisterFilter(filter); - using (var repo = CreateTestRepository(repoPath)) - { - StageNewFile(repo); - Assert.True(called); - } - - GlobalSettings.DeregisterFilter(filterRegistration); - } - - [Fact] - public void ApplyCallbackMadeWhenCheckCallbackReturnsZero() - { - bool called = false; - - Func applyCallback = (reader, writer) => - { - called = true; - return 0; //successCallback - }; - - string repoPath = InitNewRepository(); - var filter = new FakeFilter(FilterName + 6, attributes, checkSuccess, applyCallback); - - var filterRegistration = GlobalSettings.RegisterFilter(filter); - using (var repo = CreateTestRepository(repoPath)) - { - StageNewFile(repo); - } - - GlobalSettings.DeregisterFilter(filterRegistration); - - Assert.True(called); - } - - [Fact] - public void ApplyCallbackNotMadeWhenCheckCallbackReturnsPassThrough() - { - bool called = false; - - Func applyCallback = (reader, writer) => - { - called = true; - return 0; - }; - - string repoPath = InitNewRepository(); - var filter = new FakeFilter(FilterName + 7, attributes, checkPassThrough, applyCallback); - - var filterRegistration = GlobalSettings.RegisterFilter(filter); - using (var repo = CreateTestRepository(repoPath)) - { - StageNewFile(repo); - } - - GlobalSettings.DeregisterFilter(filterRegistration); - - Assert.False(called); - } - [Fact] public void InitCallbackNotMadeWhenFilterNeverUsed() { @@ -161,7 +63,6 @@ public void InitCallbackNotMadeWhenFilterNeverUsed() }; var filter = new FakeFilter(FilterName + 11, attributes, - checkSuccess, successCallback, successCallback, initializeCallback); @@ -184,7 +85,6 @@ public void InitCallbackMadeWhenUsingTheFilter() }; var filter = new FakeFilter(FilterName + 12, attributes, - checkSuccess, successCallback, successCallback, initializeCallback); @@ -202,68 +102,6 @@ public void InitCallbackMadeWhenUsingTheFilter() GlobalSettings.DeregisterFilter(filterRegistration); } - [Fact] - public void WhenStagingFileCheckIsCalledWithCleanForCorrectPath() - { - string repoPath = InitNewRepository(); - - var calledWithMode = FilterMode.Smudge; - string actualPath = string.Empty; - IEnumerable actualAttributes = Enumerable.Empty(); - Func, int> callback = (source, attr) => - { - calledWithMode = source.SourceMode; - actualPath = source.Path; - actualAttributes = attr; - return GitPassThrough; - }; - - var filter = new FakeFilter(FilterName + 13, attributes, callback); - - var filterRegistration = GlobalSettings.RegisterFilter(filter); - - using (var repo = CreateTestRepository(repoPath)) - { - FileInfo expectedFile = StageNewFile(repo); - - Assert.Equal(FilterMode.Clean, calledWithMode); - Assert.Equal(expectedFile.Name, actualPath); - Assert.Equal(attributes, actualAttributes); - } - - GlobalSettings.DeregisterFilter(filterRegistration); - } - - - [Fact] - public void WhenCheckingOutAFileFileCheckIsCalledWithSmudgeForCorrectPath() - { - const string branchName = "branch"; - string repoPath = InitNewRepository(); - - var calledWithMode = FilterMode.Clean; - string actualPath = string.Empty; - IEnumerable actualAttributes = Enumerable.Empty(); - Func, int> callback = (source, attr) => - { - calledWithMode = source.SourceMode; - actualPath = source.Path; - actualAttributes = attr; - return GitPassThrough; - }; - - var filter = new FakeFilter(FilterName + 14, attributes, callback); - - var filterRegistration = GlobalSettings.RegisterFilter(filter); - - FileInfo expectedFile = CheckoutFileForSmudge(repoPath, branchName, "hello"); - Assert.Equal(FilterMode.Smudge, calledWithMode); - Assert.Equal(expectedFile.FullName, actualPath); - Assert.Equal(attributes, actualAttributes); - - GlobalSettings.DeregisterFilter(filterRegistration); - } - [Fact] public void WhenStagingFileApplyIsCalledWithCleanForCorrectPath() { @@ -275,7 +113,7 @@ public void WhenStagingFileApplyIsCalledWithCleanForCorrectPath() called = true; return GitPassThrough; }; - var filter = new FakeFilter(FilterName + 15, attributes, checkSuccess, clean); + var filter = new FakeFilter(FilterName + 15, attributes, clean); var filterRegistration = GlobalSettings.RegisterFilter(filter); @@ -298,7 +136,7 @@ public void CleanFilterWritesOutputToObjectTree() Func cleanCallback = SubstitutionCipherFilter.RotateByThirteenPlaces; - var filter = new FakeFilter(FilterName + 16, attributes, checkSuccess, cleanCallback); + var filter = new FakeFilter(FilterName + 16, attributes, cleanCallback); var filterRegistration = GlobalSettings.RegisterFilter(filter); @@ -328,7 +166,7 @@ public void WhenCheckingOutAFileFileSmudgeWritesCorrectFileToWorkingDirectory() Func smudgeCallback = SubstitutionCipherFilter.RotateByThirteenPlaces; - var filter = new FakeFilter(FilterName + 17, attributes, checkSuccess, null, smudgeCallback); + var filter = new FakeFilter(FilterName + 17, attributes, null, smudgeCallback); var filterRegistration = GlobalSettings.RegisterFilter(filter); FileInfo expectedFile = CheckoutFileForSmudge(repoPath, branchName, encodedInput); @@ -361,7 +199,7 @@ public void FilterStreamsAreCoherent() return GitPassThrough; }; - var filter = new FakeFilter(FilterName + 18, attributes, checkSuccess, assertor, assertor); + var filter = new FakeFilter(FilterName + 18, attributes, assertor, assertor); var filterRegistration = GlobalSettings.RegisterFilter(filter); @@ -433,36 +271,28 @@ private Repository CreateTestRepository(string path) class EmptyFilter : Filter { - public EmptyFilter(string name, IEnumerable attributes) + public EmptyFilter(string name, IEnumerable attributes) : base(name, attributes) { } } class FakeFilter : Filter { - private readonly Func, int> checkCallBack; private readonly Func cleanCallback; private readonly Func smudgeCallback; private readonly Func initCallback; - public FakeFilter(string name, IEnumerable attributes, - Func, int> checkCallBack = null, + public FakeFilter(string name, IEnumerable attributes, Func cleanCallback = null, Func smudgeCallback = null, Func initCallback = null) : base(name, attributes) { - this.checkCallBack = checkCallBack; this.cleanCallback = cleanCallback; this.smudgeCallback = smudgeCallback; this.initCallback = initCallback; } - protected override int Check(IEnumerable attributes, FilterSource filterSource) - { - return checkCallBack != null ? checkCallBack(filterSource, attributes) : base.Check(attributes, filterSource); - } - protected override int Clean(string path, Stream input, Stream output) { return cleanCallback != null ? cleanCallback(input, output) : base.Clean(path, input, output); diff --git a/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs b/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs index f2fc14b66..f88909b84 100644 --- a/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs +++ b/LibGit2Sharp.Tests/FilterSubstitutionCipherFixture.cs @@ -3,19 +3,20 @@ using System.IO; using LibGit2Sharp.Tests.TestHelpers; using Xunit; +using Xunit.Extensions; namespace LibGit2Sharp.Tests { public class FilterSubstitutionCipherFixture : BaseFixture { [Fact] - public void CorrectlyEncodesAndDecodesInput() + public void SmugdeIsNotCalledForFileWhichDoesNotMatchAnAttributeEntry() { const string decodedInput = "This is a substitution cipher"; const string encodedInput = "Guvf vf n fhofgvghgvba pvcure"; - var attributes = new List { ".rot13" }; - var filter = new SubstitutionCipherFilter("ROT13", attributes); + var attributes = new List { new FilterAttribute("filter=rot13") }; + var filter = new SubstitutionCipherFilter("cipher-filter", attributes); var filterRegistration = GlobalSettings.RegisterFilter(filter); string repoPath = InitNewRepository(); @@ -23,8 +24,9 @@ public void CorrectlyEncodesAndDecodesInput() 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(); @@ -48,31 +50,154 @@ public void CorrectlyEncodesAndDecodesInput() } [Fact] - public void WhenAttributesDoNotMatchFileIsNotFilterd() + public void CorrectlyEncodesAndDecodesInput() { const string decodedInput = "This is a substitution cipher"; + const string encodedInput = "Guvf vf n fhofgvghgvba pvcure"; - var attributes = new List { ".rot13" }; - var filter = new SubstitutionCipherFilter("ROT13", attributes); + var attributes = new List { new FilterAttribute("filter=rot13") }; + var filter = new SubstitutionCipherFilter("cipher-filter", attributes); var filterRegistration = GlobalSettings.RegisterFilter(filter); string repoPath = InitNewRepository(); - string fileName = Guid.NewGuid() + ".rot131"; + 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 FilterAttribute(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(0, filter.CleanCalledCount); - Assert.Equal(0, filter.SmudgeCalledCount); - Assert.Equal(1, filter.CheckCalledCount); + 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 FilterAttribute(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 FilterAttribute(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)); @@ -95,5 +220,10 @@ private static Blob CommitOnBranchAndReturnDatabaseBlob(Repository repo, string 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/TestHelpers/SubstitutionCipherFilter.cs b/LibGit2Sharp.Tests/TestHelpers/SubstitutionCipherFilter.cs index 184ca526e..c3a83b137 100644 --- a/LibGit2Sharp.Tests/TestHelpers/SubstitutionCipherFilter.cs +++ b/LibGit2Sharp.Tests/TestHelpers/SubstitutionCipherFilter.cs @@ -5,21 +5,14 @@ namespace LibGit2Sharp.Tests.TestHelpers { public class SubstitutionCipherFilter : Filter { - public int CheckCalledCount = 0; public int CleanCalledCount = 0; public int SmudgeCalledCount = 0; - public SubstitutionCipherFilter(string name, IEnumerable attributes) + public SubstitutionCipherFilter(string name, IEnumerable attributes) : base(name, attributes) { } - protected override int Check(IEnumerable attributes, FilterSource filterSource) - { - CheckCalledCount++; - return base.Check(attributes, filterSource); - } - protected override int Clean(string path, Stream input, Stream output) { CleanCalledCount++; diff --git a/LibGit2Sharp/Filter.cs b/LibGit2Sharp/Filter.cs index a304d28f6..29c173ab0 100644 --- a/LibGit2Sharp/Filter.cs +++ b/LibGit2Sharp/Filter.cs @@ -17,7 +17,7 @@ public abstract class Filter : IEquatable new LambdaEqualityHelper(x => x.Name, x => x.Attributes); private readonly string name; - private readonly string attributes; + private readonly IEnumerable attributes; private readonly GitFilter gitFilter; @@ -25,32 +25,22 @@ public abstract class Filter : IEquatable /// 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 filterForAttributes which this filter applies to + /// A list of attributes which this filter applies to /// - protected Filter(string name, IEnumerable attributes) - : this(name, string.Join(",", attributes)) - { } - - /// - /// Initializes a new instance of the class. - /// And allocates the filter natively. - /// The unique name with which this filtered is registered with - /// Either a single attribute, or a comma separated list of filterForAttributes for which this filter applies to - /// - private Filter(string name, string attributes) + protected Filter(string name, IEnumerable attributes) { Ensure.ArgumentNotNullOrEmptyString(name, "name"); - Ensure.ArgumentNotNullOrEmptyEnumerable(attributes, "attributes"); + 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, attributes), + attributes = EncodingMarshaler.FromManaged(Encoding.UTF8, attributesAsString), init = InitializeCallback, - apply = ApplyCallback, - check = CheckCallback + apply = ApplyCallback }; } @@ -65,9 +55,9 @@ public string Name /// /// The filter filterForAttributes. /// - public IEnumerable Attributes + public IEnumerable Attributes { - get { return attributes.Split(','); } + get { return attributes; } } /// @@ -94,20 +84,6 @@ protected virtual int Initialize() return 0; } - /// - /// Decides if a given source needs to be filtered by checking if the filter - /// matches the current file extension. - /// - /// The filterForAttributes that this filter was created for. - /// The source of the filter - /// 0 if successful and to skip and pass through - protected virtual int Check(IEnumerable filterForAttributes, FilterSource filterSource) - { - var fileInfo = new FileInfo(filterSource.Path); - var matches = filterForAttributes.Any(currentExtension => string.Equals(fileInfo.Extension, currentExtension, StringComparison.Ordinal)); - return matches ? 0 : (int)GitErrorCode.PassThrough; - } - /// /// Clean the input stream and write to the output stream. /// @@ -198,29 +174,6 @@ int InitializeCallback(IntPtr filterPointer) return Initialize(); } - /// - /// 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 filterForAttributes 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. - /// - /// - int CheckCallback(GitFilter filter, IntPtr payload, IntPtr filterSourcePtr, IntPtr attributeValues) - { - string filterForAttributes = EncodingMarshaler.FromNative(Encoding.UTF8, filter.attributes); - var filterSource = FilterSource.FromNativePtr(filterSourcePtr); - return Check(filterForAttributes.Split(','), filterSource); - } - - /// /// Callback to actually perform the data filtering /// diff --git a/LibGit2Sharp/FilterAttribute.cs b/LibGit2Sharp/FilterAttribute.cs new file mode 100644 index 000000000..07d7a7481 --- /dev/null +++ b/LibGit2Sharp/FilterAttribute.cs @@ -0,0 +1,42 @@ +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 FilterAttribute + { + private const string AttributeFilterDefinition = "filter="; + + private readonly string filterDefinition; + + /// + /// The name of the filter found in a .gitattributes file + /// + /// The name of the filter + public FilterAttribute(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 string FilterDefinition + { + get { return filterDefinition; } + } + } +} \ No newline at end of file diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 5f626b6fb..3986ba38c 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -81,6 +81,7 @@ +