Skip to content

Commit abd2ff7

Browse files
Implement git log --follow
This commit basically implements the git log --follow command. To do that, it implements the FileHistory, FileHistoryEntry, and FileHistoryExtensions classes. The associated FileHistoryFixture implements a number of tests, covering the essential cases (e.g., following renames, dealing with branches). Related to topics libgit2#893 and libgit2#89
1 parent e1228da commit abd2ff7

File tree

4 files changed

+622
-1
lines changed

4 files changed

+622
-1
lines changed
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using LibGit2Sharp.Tests.TestHelpers;
6+
using Xunit;
7+
using Xunit.Extensions;
8+
9+
namespace LibGit2Sharp.Tests
10+
{
11+
public class FileHistoryFixture : BaseFixture
12+
{
13+
[Fact]
14+
public void EmptyRepositoryHasNoHistory()
15+
{
16+
string repoPath = CreateEmptyRepository();
17+
18+
using (Repository repo = new Repository(repoPath))
19+
{
20+
IEnumerable<FileHistoryEntry> history = repo.Follow("Test.txt");
21+
Assert.Equal(0, history.Count());
22+
Assert.Equal(0, history.ChangedBlobs().Count());
23+
}
24+
}
25+
26+
[Fact]
27+
public void CanTellSingleCommitHistory()
28+
{
29+
string repoPath = CreateEmptyRepository();
30+
31+
using (Repository repo = new Repository(repoPath))
32+
{
33+
// Set up repository.
34+
string path = "Test.txt";
35+
Commit commit = MakeAndCommitChange(repo, repoPath, path, "Hello World");
36+
37+
// Perform tests.
38+
IEnumerable<FileHistoryEntry> history = repo.Follow(path);
39+
IEnumerable<Blob> changedBlobs = history.ChangedBlobs();
40+
41+
Assert.Equal(1, history.Count());
42+
Assert.Equal(1, changedBlobs.Count());
43+
44+
Assert.Equal(path, history.First().Path);
45+
Assert.Equal(commit, history.First().Commit);
46+
}
47+
}
48+
49+
[Fact]
50+
public void CanTellSimpleCommitHistory()
51+
{
52+
string repoPath = CreateEmptyRepository();
53+
string path1 = "Test1.txt";
54+
string path2 = "Test2.txt";
55+
56+
using (Repository repo = new Repository(repoPath))
57+
{
58+
// Set up repository.
59+
Commit commit1 = MakeAndCommitChange(repo, repoPath, path1, "Hello World");
60+
Commit commit2 = MakeAndCommitChange(repo, repoPath, path2, "Second file's contents");
61+
Commit commit3 = MakeAndCommitChange(repo, repoPath, path1, "Hello World again");
62+
63+
// Perform tests.
64+
IEnumerable<FileHistoryEntry> history = repo.Follow(path1);
65+
IEnumerable<Blob> changedBlobs = history.ChangedBlobs();
66+
67+
Assert.Equal(2, history.Count());
68+
Assert.Equal(2, changedBlobs.Count());
69+
70+
Assert.Equal(commit3, history.ElementAt(0).Commit);
71+
Assert.Equal(commit1, history.ElementAt(1).Commit);
72+
}
73+
}
74+
75+
[Fact]
76+
public void CanTellComplexCommitHistory()
77+
{
78+
string repoPath = CreateEmptyRepository();
79+
string path1 = "Test1.txt";
80+
string path2 = "Test2.txt";
81+
82+
using (Repository repo = new Repository(repoPath))
83+
{
84+
// Make initial changes.
85+
Commit commit1 = MakeAndCommitChange(repo, repoPath, path1, "Hello World");
86+
MakeAndCommitChange(repo, repoPath, path2, "Second file's contents");
87+
Commit commit2 = MakeAndCommitChange(repo, repoPath, path1, "Hello World again");
88+
89+
// Move the first file to a new directory.
90+
string newPath1 = Path.Combine(subFolderPath1, path1);
91+
repo.Move(path1, newPath1);
92+
Commit commit3 = repo.Commit("Moved " + path1 + " to " + newPath1,
93+
Constants.Signature, Constants.Signature);
94+
95+
// Make further changes.
96+
MakeAndCommitChange(repo, repoPath, path2, "Changed second file's contents");
97+
Commit commit4 = MakeAndCommitChange(repo, repoPath, newPath1, "I have done it again!");
98+
99+
// Perform tests.
100+
List<FileHistoryEntry> fileHistoryEntries = repo.Follow(newPath1).ToList();
101+
List<Blob> changedBlobs = fileHistoryEntries.ChangedBlobs().ToList();
102+
103+
Assert.Equal(4, fileHistoryEntries.Count());
104+
Assert.Equal(3, changedBlobs.Count());
105+
106+
Assert.Equal(2, fileHistoryEntries.Where(e => e.Path == newPath1).Count());
107+
Assert.Equal(2, fileHistoryEntries.Where(e => e.Path == path1).Count());
108+
109+
Assert.Equal(commit4, fileHistoryEntries[0].Commit);
110+
Assert.Equal(commit3, fileHistoryEntries[1].Commit);
111+
Assert.Equal(commit2, fileHistoryEntries[2].Commit);
112+
Assert.Equal(commit1, fileHistoryEntries[3].Commit);
113+
114+
Assert.Equal(commit4.Tree[newPath1].Target, changedBlobs[0]);
115+
Assert.Equal(commit2.Tree[path1].Target, changedBlobs[1]);
116+
Assert.Equal(commit1.Tree[path1].Target, changedBlobs[2]);
117+
}
118+
}
119+
120+
[Theory]
121+
[InlineData("https://github.com/nulltoken/follow-test.git")]
122+
public void CanDealWithFollowTest(string url)
123+
{
124+
var scd = BuildSelfCleaningDirectory();
125+
string clonedRepoPath = Repository.Clone(url, scd.DirectoryPath);
126+
127+
using (Repository repo = new Repository(clonedRepoPath))
128+
{
129+
// $ git log --follow --format=oneline so-renamed.txt
130+
// 88f91835062161febb46fb270ef4188f54c09767 Update not-yet-renamed.txt AND rename into so-renamed.txt
131+
// ef7cb6a63e32595fffb092cb1ae9a32310e58850 Add not-yet-renamed.txt
132+
List<FileHistoryEntry> fileHistoryEntries = repo.Follow("so-renamed.txt").ToList();
133+
Assert.Equal(2, fileHistoryEntries.Count());
134+
Assert.Equal(1, fileHistoryEntries.ExcludeRenames().Count());
135+
Assert.Equal("88f91835062161febb46fb270ef4188f54c09767", fileHistoryEntries[0].Commit.Sha);
136+
Assert.Equal("ef7cb6a63e32595fffb092cb1ae9a32310e58850", fileHistoryEntries[1].Commit.Sha);
137+
138+
// $ git log --follow --format=oneline untouched.txt
139+
// c10c1d5f74b76f20386d18674bf63fbee6995061 Initial commit
140+
fileHistoryEntries = repo.Follow("untouched.txt").ToList();
141+
Assert.Equal(1, fileHistoryEntries.Count());
142+
Assert.Equal(1, fileHistoryEntries.ExcludeRenames().Count());
143+
Assert.Equal("c10c1d5f74b76f20386d18674bf63fbee6995061", fileHistoryEntries[0].Commit.Sha);
144+
145+
// $ git log --follow --format=oneline under-test.txt
146+
// 0b5b18f2feb917dee98df1210315b2b2b23c5bec Rename file renamed.txt into under-test.txt
147+
// 49921d463420a892c9547a326632ef6a9ba3b225 Update file renamed.txt
148+
// 70f636e8c64bbc2dfef3735a562bb7e195d8019f Rename file under-test.txt into renamed.txt
149+
// d3868d57a6aaf2ae6ed4887d805ae4bc91d8ce4d Updated file under test
150+
// 9da10ef7e139c49604a12caa866aae141f38b861 Updated file under test
151+
// 599a5d821fb2c0a25855b4233e26d475c2fbeb34 Updated file under test
152+
// 678b086b44753000567aa64344aa0d8034fa0083 Updated file under test
153+
// 8f7d9520f306771340a7c79faea019ad18e4fa1f Updated file under test
154+
// bd5f8ee279924d33be8ccbde82e7f10b9d9ff237 Updated file under test
155+
// c10c1d5f74b76f20386d18674bf63fbee6995061 Initial commit
156+
fileHistoryEntries = repo.Follow("under-test.txt").ToList();
157+
Assert.Equal(10, fileHistoryEntries.Count());
158+
Assert.Equal(1, fileHistoryEntries.ExcludeRenames().Count());
159+
Assert.Equal("0b5b18f2feb917dee98df1210315b2b2b23c5bec", fileHistoryEntries[0].Commit.Sha);
160+
Assert.Equal("49921d463420a892c9547a326632ef6a9ba3b225", fileHistoryEntries[1].Commit.Sha);
161+
Assert.Equal("70f636e8c64bbc2dfef3735a562bb7e195d8019f", fileHistoryEntries[2].Commit.Sha);
162+
Assert.Equal("d3868d57a6aaf2ae6ed4887d805ae4bc91d8ce4d", fileHistoryEntries[3].Commit.Sha);
163+
Assert.Equal("9da10ef7e139c49604a12caa866aae141f38b861", fileHistoryEntries[4].Commit.Sha);
164+
Assert.Equal("599a5d821fb2c0a25855b4233e26d475c2fbeb34", fileHistoryEntries[5].Commit.Sha);
165+
Assert.Equal("678b086b44753000567aa64344aa0d8034fa0083", fileHistoryEntries[6].Commit.Sha);
166+
Assert.Equal("8f7d9520f306771340a7c79faea019ad18e4fa1f", fileHistoryEntries[7].Commit.Sha);
167+
Assert.Equal("bd5f8ee279924d33be8ccbde82e7f10b9d9ff237", fileHistoryEntries[8].Commit.Sha);
168+
Assert.Equal("c10c1d5f74b76f20386d18674bf63fbee6995061", fileHistoryEntries[9].Commit.Sha);
169+
}
170+
}
171+
172+
[Theory]
173+
[InlineData(null)]
174+
public void CanFollowBranches(string specificRepoPath)
175+
{
176+
string repoPath = specificRepoPath ?? CreateEmptyRepository();
177+
string path = "Test.txt";
178+
179+
using (Repository repo = new Repository(repoPath))
180+
{
181+
List<Commit> commits = new List<Commit>();
182+
183+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Before merge", "0. Initial commit for this test"));
184+
185+
Branch fixBranch = repo.CreateBranch("fix", GetNextSignature());
186+
187+
repo.Checkout("fix");
188+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Change on fix branch", "1. Changed on fix"));
189+
190+
repo.Checkout("master");
191+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Independent change on master branch", "2. Changed on master"));
192+
193+
repo.Checkout("fix");
194+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Another change on fix branch", "3. Changed on fix"));
195+
196+
repo.Checkout("master");
197+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Another independent change on master branch", "4. Changed on master"));
198+
199+
MergeResult mergeResult = repo.Merge("fix", GetNextSignature());
200+
if (mergeResult.Status == MergeStatus.Conflicts)
201+
{
202+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Manual resolution of merge conflict", "5. Merged fix into master"));
203+
}
204+
205+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Change after merge", "6. Changed on master"));
206+
207+
repo.CreateBranch("next-fix", GetNextSignature());
208+
209+
repo.Checkout("next-fix");
210+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Change on next-fix branch", "7. Changed on next-fix"));
211+
212+
repo.Checkout("master");
213+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Some arbitrary change on master branch", "8. Changed on master"));
214+
215+
mergeResult = repo.Merge("next-fix", GetNextSignature());
216+
if (mergeResult.Status == MergeStatus.Conflicts)
217+
{
218+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "Another manual resolution of merge conflict", "9. Merged next-fix into master"));
219+
}
220+
221+
commits.Add(MakeAndCommitChange(repo, repoPath, path, "A change on master after merging", "10. Changed on master"));
222+
223+
// Test --date-order.
224+
IEnumerable<FileHistoryEntry> timeHistory = repo.Follow(path, new CommitFilter { SortBy = CommitSortStrategies.Time });
225+
List<Commit> timeCommits = new List<Commit>
226+
{
227+
commits[10], // master
228+
229+
commits[8], // master
230+
commits[7], // next-fix
231+
commits[6], // master
232+
233+
commits[4], // master
234+
commits[3], // fix
235+
commits[2], // master
236+
commits[1], // fix
237+
commits[0] // master (initial commit)
238+
};
239+
Assert.Equal<Commit>(timeCommits, timeHistory.Select(e => e.Commit));
240+
Assert.Equal(timeHistory.Count(), timeHistory.ExcludeRenames().Count());
241+
242+
// Test --topo-order.
243+
IEnumerable<FileHistoryEntry> topoHistory = repo.Follow(path, new CommitFilter { SortBy = CommitSortStrategies.Topological });
244+
List<Commit> topoCommits = new List<Commit>
245+
{
246+
commits[10], // master
247+
248+
commits[7], // next-fix
249+
commits[8], // master
250+
commits[6], // master
251+
252+
commits[3], // fix
253+
commits[1], // fix
254+
commits[4], // master
255+
commits[2], // master
256+
commits[0] // master (initial commit)
257+
};
258+
Assert.Equal<Commit>(topoCommits, topoHistory.Select(e => e.Commit));
259+
Assert.Equal(topoHistory.Count(), topoHistory.ExcludeRenames().Count());
260+
}
261+
}
262+
263+
#region Helpers
264+
265+
protected Signature signature = Constants.Signature;
266+
protected string subFolderPath1 = "SubFolder1";
267+
268+
protected Signature GetNextSignature()
269+
{
270+
signature = signature.TimeShift(TimeSpan.FromMinutes(1));
271+
return signature;
272+
}
273+
274+
protected string CreateEmptyRepository()
275+
{
276+
// Create a new empty directory with subfolders.
277+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
278+
Directory.CreateDirectory(Path.Combine(scd.DirectoryPath, subFolderPath1));
279+
280+
// Initialize a GIT repository in that directory.
281+
Repository.Init(scd.DirectoryPath, false);
282+
using (Repository repo = new Repository(scd.DirectoryPath))
283+
{
284+
repo.Config.Set("user.name", signature.Name);
285+
repo.Config.Set("user.email", signature.Email);
286+
}
287+
288+
// Done.
289+
return scd.DirectoryPath;
290+
}
291+
292+
protected Commit MakeAndCommitChange(Repository repo, string repoPath, string path, string text, string message = null)
293+
{
294+
Touch(repoPath, path, text);
295+
repo.Stage(path);
296+
297+
Signature commitSignature = GetNextSignature();
298+
return repo.Commit(message ?? "Changed " + path, commitSignature, commitSignature);
299+
}
300+
301+
#endregion
302+
}
303+
}

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
<Compile Include="CheckoutFixture.cs" />
5858
<Compile Include="CherryPickFixture.cs" />
5959
<Compile Include="DescribeFixture.cs" />
60+
<Compile Include="FileHistoryFixture.cs" />
6061
<Compile Include="GlobalSettingsFixture.cs" />
6162
<Compile Include="PatchStatsFixture.cs" />
6263
<Compile Include="RefSpecFixture.cs" />
@@ -157,4 +158,4 @@
157158
<Target Name="AfterBuild">
158159
</Target>
159160
-->
160-
</Project>
161+
</Project>

0 commit comments

Comments
 (0)