Skip to content

Commit 431e9c6

Browse files
committed
Merge pull request #1030 from whoisj/streaming-filter-support
Streaming Filter Support
2 parents 8008e2d + 71dc2c1 commit 431e9c6

18 files changed

+1580
-6
lines changed

LibGit2Sharp.Tests/FilterFixture.cs

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using LibGit2Sharp.Tests.TestHelpers;
5+
using Xunit;
6+
7+
namespace LibGit2Sharp.Tests
8+
{
9+
public class FilterFixture : BaseFixture
10+
{
11+
readonly Action<Stream, Stream> successCallback = (reader, writer) =>
12+
{
13+
reader.CopyTo(writer);
14+
};
15+
16+
private const string FilterName = "the-filter";
17+
readonly List<FilterAttributeEntry> attributes = new List<FilterAttributeEntry> { new FilterAttributeEntry("test") };
18+
19+
[Fact]
20+
public void CanRegisterFilterWithSingleAttribute()
21+
{
22+
var filter = new EmptyFilter(FilterName, attributes);
23+
Assert.Equal(attributes, filter.Attributes);
24+
}
25+
26+
[Fact]
27+
public void CanRegisterAndUnregisterTheSameFilter()
28+
{
29+
var filter = new EmptyFilter(FilterName + 1, attributes);
30+
31+
var registration = GlobalSettings.RegisterFilter(filter);
32+
GlobalSettings.DeregisterFilter(registration);
33+
34+
var secondRegistration = GlobalSettings.RegisterFilter(filter);
35+
GlobalSettings.DeregisterFilter(secondRegistration);
36+
}
37+
38+
[Fact]
39+
public void CanRegisterAndDeregisterAfterGarbageCollection()
40+
{
41+
var filter = new EmptyFilter(FilterName + 2, attributes);
42+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
43+
44+
GC.Collect();
45+
46+
GlobalSettings.DeregisterFilter(filterRegistration);
47+
}
48+
49+
[Fact]
50+
public void SameFilterIsEqual()
51+
{
52+
var filter = new EmptyFilter(FilterName + 3, attributes);
53+
Assert.Equal(filter, filter);
54+
}
55+
56+
[Fact]
57+
public void InitCallbackNotMadeWhenFilterNeverUsed()
58+
{
59+
bool called = false;
60+
Action initializeCallback = () =>
61+
{
62+
called = true;
63+
};
64+
65+
var filter = new FakeFilter(FilterName + 11, attributes,
66+
successCallback,
67+
successCallback,
68+
initializeCallback);
69+
70+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
71+
72+
Assert.False(called);
73+
74+
GlobalSettings.DeregisterFilter(filterRegistration);
75+
}
76+
77+
[Fact]
78+
public void InitCallbackMadeWhenUsingTheFilter()
79+
{
80+
bool called = false;
81+
Action initializeCallback = () =>
82+
{
83+
called = true;
84+
};
85+
86+
var filter = new FakeFilter(FilterName + 12, attributes,
87+
successCallback,
88+
successCallback,
89+
initializeCallback);
90+
91+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
92+
Assert.False(called);
93+
94+
string repoPath = InitNewRepository();
95+
using (var repo = CreateTestRepository(repoPath))
96+
{
97+
StageNewFile(repo);
98+
Assert.True(called);
99+
}
100+
101+
GlobalSettings.DeregisterFilter(filterRegistration);
102+
}
103+
104+
[Fact]
105+
public void WhenStagingFileApplyIsCalledWithCleanForCorrectPath()
106+
{
107+
string repoPath = InitNewRepository();
108+
bool called = false;
109+
110+
Action<Stream, Stream> clean = (reader, writer) =>
111+
{
112+
called = true;
113+
reader.CopyTo(writer);
114+
};
115+
var filter = new FakeFilter(FilterName + 15, attributes, clean);
116+
117+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
118+
119+
using (var repo = CreateTestRepository(repoPath))
120+
{
121+
StageNewFile(repo);
122+
Assert.True(called);
123+
}
124+
125+
GlobalSettings.DeregisterFilter(filterRegistration);
126+
}
127+
128+
[Fact]
129+
public void CleanFilterWritesOutputToObjectTree()
130+
{
131+
const string decodedInput = "This is a substitution cipher";
132+
const string encodedInput = "Guvf vf n fhofgvghgvba pvcure";
133+
134+
string repoPath = InitNewRepository();
135+
136+
Action<Stream, Stream> cleanCallback = SubstitutionCipherFilter.RotateByThirteenPlaces;
137+
138+
var filter = new FakeFilter(FilterName + 16, attributes, cleanCallback);
139+
140+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
141+
142+
using (var repo = CreateTestRepository(repoPath))
143+
{
144+
FileInfo expectedFile = StageNewFile(repo, decodedInput);
145+
var commit = repo.Commit("Clean that file");
146+
147+
var blob = (Blob)commit.Tree[expectedFile.Name].Target;
148+
149+
var textDetected = blob.GetContentText();
150+
Assert.Equal(encodedInput, textDetected);
151+
}
152+
153+
GlobalSettings.DeregisterFilter(filterRegistration);
154+
}
155+
156+
[Fact]
157+
public void WhenCheckingOutAFileFileSmudgeWritesCorrectFileToWorkingDirectory()
158+
{
159+
const string decodedInput = "This is a substitution cipher";
160+
const string encodedInput = "Guvf vf n fhofgvghgvba pvcure";
161+
162+
const string branchName = "branch";
163+
string repoPath = InitNewRepository();
164+
165+
Action<Stream, Stream> smudgeCallback = SubstitutionCipherFilter.RotateByThirteenPlaces;
166+
167+
var filter = new FakeFilter(FilterName + 17, attributes, null, smudgeCallback);
168+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
169+
170+
FileInfo expectedFile = CheckoutFileForSmudge(repoPath, branchName, encodedInput);
171+
172+
string combine = Path.Combine(repoPath, "..", expectedFile.Name);
173+
string readAllText = File.ReadAllText(combine);
174+
Assert.Equal(decodedInput, readAllText);
175+
176+
GlobalSettings.DeregisterFilter(filterRegistration);
177+
}
178+
179+
[Fact]
180+
public void CanFilterLargeFiles()
181+
{
182+
const int ContentLength = 128 * 1024 * 1024;
183+
const char ContentValue = 'x';
184+
185+
char[] content = (new string(ContentValue, 1024)).ToCharArray();
186+
187+
string repoPath = InitNewRepository();
188+
189+
var filter = new FileExportFilter("exportFilter", attributes);
190+
var filterRegistration = GlobalSettings.RegisterFilter(filter);
191+
192+
string filePath = Path.Combine(Directory.GetParent(repoPath).Parent.FullName, Guid.NewGuid().ToString() + ".blob");
193+
FileInfo contentFile = new FileInfo(filePath);
194+
using (var writer = new StreamWriter(contentFile.OpenWrite()) { AutoFlush = true })
195+
{
196+
for (int i = 0; i < ContentLength / content.Length; i++)
197+
{
198+
writer.Write(content);
199+
}
200+
}
201+
202+
string attributesPath = Path.Combine(Directory.GetParent(repoPath).Parent.FullName, ".gitattributes");
203+
FileInfo attributesFile = new FileInfo(attributesPath);
204+
205+
string configPath = CreateConfigurationWithDummyUser(Constants.Signature);
206+
var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath };
207+
208+
using (Repository repo = new Repository(repoPath, repositoryOptions))
209+
{
210+
File.WriteAllText(attributesPath, "*.blob filter=test");
211+
repo.Stage(attributesFile.Name);
212+
repo.Stage(contentFile.Name);
213+
repo.Commit("test");
214+
contentFile.Delete();
215+
repo.Checkout("HEAD", new CheckoutOptions() { CheckoutModifiers = CheckoutModifiers.Force });
216+
}
217+
218+
contentFile = new FileInfo(filePath);
219+
Assert.True(contentFile.Exists, "Contents not restored correctly by forced checkout.");
220+
using (StreamReader reader = contentFile.OpenText())
221+
{
222+
int totalRead = 0;
223+
char[] block = new char[1024];
224+
int read;
225+
while ((read = reader.Read(block, 0, block.Length)) > 0)
226+
{
227+
Assert.True(CharArrayAreEqual(block, content, read));
228+
totalRead += read;
229+
}
230+
231+
Assert.Equal(ContentLength, totalRead);
232+
}
233+
234+
contentFile.Delete();
235+
236+
GlobalSettings.DeregisterFilter(filterRegistration);
237+
}
238+
239+
private unsafe bool CharArrayAreEqual(char[] array1, char[] array2, int count)
240+
{
241+
if (Object.ReferenceEquals(array1, array2))
242+
{
243+
return true;
244+
}
245+
if (Object.ReferenceEquals(array1, null) || Object.ReferenceEquals(null, array2))
246+
{
247+
return false;
248+
}
249+
if (array1.Length < count || array2.Length < count)
250+
{
251+
return false;
252+
}
253+
254+
int len = count * sizeof(char);
255+
int cnt = len / sizeof(long);
256+
257+
fixed (char* c1 = array1, c2 = array2)
258+
{
259+
long* p1 = (long*)c1,
260+
p2 = (long*)c2;
261+
262+
for (int i = 0; i < cnt; i++)
263+
{
264+
if (p1[i] != p2[i])
265+
{
266+
return false;
267+
}
268+
}
269+
270+
byte* b1 = (byte*)c1,
271+
b2 = (byte*)c2;
272+
273+
for (int i = len * sizeof(long); i < len; i++)
274+
{
275+
if (b1[i] != b2[i])
276+
{
277+
return false;
278+
}
279+
}
280+
}
281+
282+
return true;
283+
}
284+
285+
286+
private FileInfo CheckoutFileForSmudge(string repoPath, string branchName, string content)
287+
{
288+
FileInfo expectedPath;
289+
using (var repo = CreateTestRepository(repoPath))
290+
{
291+
StageNewFile(repo, content);
292+
293+
repo.Commit("Initial commit");
294+
295+
expectedPath = CommitFileOnBranch(repo, branchName, content);
296+
297+
repo.Checkout("master");
298+
299+
repo.Checkout(branchName);
300+
}
301+
return expectedPath;
302+
}
303+
304+
private static FileInfo CommitFileOnBranch(Repository repo, string branchName, String content)
305+
{
306+
var branch = repo.CreateBranch(branchName);
307+
repo.Checkout(branch.Name);
308+
309+
FileInfo expectedPath = StageNewFile(repo, content);
310+
repo.Commit("Commit");
311+
return expectedPath;
312+
}
313+
314+
private static FileInfo StageNewFile(IRepository repo, string contents = "null")
315+
{
316+
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", contents);
317+
var stageNewFile = new FileInfo(newFilePath);
318+
repo.Stage(newFilePath);
319+
return stageNewFile;
320+
}
321+
322+
private Repository CreateTestRepository(string path)
323+
{
324+
string configPath = CreateConfigurationWithDummyUser(Constants.Signature);
325+
var repositoryOptions = new RepositoryOptions { GlobalConfigurationLocation = configPath };
326+
var repository = new Repository(path, repositoryOptions);
327+
CreateAttributesFile(repository, "* filter=test");
328+
return repository;
329+
}
330+
331+
private static void CreateAttributesFile(IRepository repo, string attributeEntry)
332+
{
333+
Touch(repo.Info.WorkingDirectory, ".gitattributes", attributeEntry);
334+
}
335+
336+
class EmptyFilter : Filter
337+
{
338+
public EmptyFilter(string name, IEnumerable<FilterAttributeEntry> attributes)
339+
: base(name, attributes)
340+
{ }
341+
}
342+
343+
class FakeFilter : Filter
344+
{
345+
private readonly Action<Stream, Stream> cleanCallback;
346+
private readonly Action<Stream, Stream> smudgeCallback;
347+
private readonly Action initCallback;
348+
349+
public FakeFilter(string name, IEnumerable<FilterAttributeEntry> attributes,
350+
Action<Stream, Stream> cleanCallback = null,
351+
Action<Stream, Stream> smudgeCallback = null,
352+
Action initCallback = null)
353+
: base(name, attributes)
354+
{
355+
this.cleanCallback = cleanCallback;
356+
this.smudgeCallback = smudgeCallback;
357+
this.initCallback = initCallback;
358+
}
359+
360+
protected override void Clean(string path, string root, Stream input, Stream output)
361+
{
362+
if (cleanCallback == null)
363+
{
364+
base.Clean(path, root, input, output);
365+
}
366+
else
367+
{
368+
cleanCallback(input, output);
369+
}
370+
}
371+
372+
protected override void Smudge(string path, string root, Stream input, Stream output)
373+
{
374+
if (smudgeCallback == null)
375+
{
376+
base.Smudge(path, root, input, output);
377+
}
378+
else
379+
{
380+
smudgeCallback(input, output);
381+
}
382+
}
383+
384+
protected override void Initialize()
385+
{
386+
if (initCallback == null)
387+
{
388+
base.Initialize();
389+
}
390+
else
391+
{
392+
initCallback();
393+
}
394+
}
395+
}
396+
}
397+
}

0 commit comments

Comments
 (0)