Skip to content

Commit c462df3

Browse files
Implement git log --follow
This commit basically implements the git log --follow <path> command. It adds the following two methods to the IQueryableCommitLog interface: IEnumerable<LogEntry> QueryBy(string path); IEnumerable<LogEntry> QueryBy(string path, FollowFilter filter); The corresponding implementations are added to the CommitLog class. The actual functionality is implemented by the FileHistory class that is part of the LibGit2Sharp.Core namespace. Related to topics #893 and #89
1 parent cad89d0 commit c462df3

8 files changed

+689
-2
lines changed

LibGit2Sharp.Tests/FileHistoryFixture.cs

Lines changed: 396 additions & 0 deletions
Large diffs are not rendered by default.

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>

LibGit2Sharp/CommitLog.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,32 @@ public ICommitLog QueryBy(CommitFilter filter)
8080
return new CommitLog(repo, filter);
8181
}
8282

83+
/// <summary>
84+
/// Returns the list of commits of the repository representing the history of a file beyond renames.
85+
/// </summary>
86+
/// <param name="path">The file's path.</param>
87+
/// <returns>A list of file history entries, ready to be enumerated.</returns>
88+
public IEnumerable<LogEntry> QueryBy(string path)
89+
{
90+
Ensure.ArgumentNotNull(path, "path");
91+
92+
return new FileHistory(repo, path);
93+
}
94+
95+
/// <summary>
96+
/// Returns the list of commits of the repository representing the history of a file beyond renames.
97+
/// </summary>
98+
/// <param name="path">The file's path.</param>
99+
/// <param name="filter">The options used to control which commits will be returned.</param>
100+
/// <returns>A list of file history entries, ready to be enumerated.</returns>
101+
public IEnumerable<LogEntry> QueryBy(string path, FollowFilter filter)
102+
{
103+
Ensure.ArgumentNotNull(path, "path");
104+
Ensure.ArgumentNotNull(filter, "filter");
105+
106+
return new FileHistory(repo, path, new CommitFilter {SortBy = filter.SortBy});
107+
}
108+
83109
/// <summary>
84110
/// Find the best possible merge base given two <see cref="Commit"/>s.
85111
/// </summary>
@@ -206,7 +232,6 @@ private void FirstParentOnly(bool firstParent)
206232
}
207233
}
208234
}
209-
210235
}
211236

212237
/// <summary>

LibGit2Sharp/Core/FileHistory.cs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace LibGit2Sharp.Core
7+
{
8+
/// <summary>
9+
/// Represents a file-related log of commits beyond renames.
10+
/// </summary>
11+
internal class FileHistory : IEnumerable<LogEntry>
12+
{
13+
#region Fields
14+
15+
/// <summary>
16+
/// The allowed commit sort strategies.
17+
/// </summary>
18+
private static readonly List<CommitSortStrategies> AllowedSortStrategies = new List<CommitSortStrategies>
19+
{
20+
CommitSortStrategies.Topological,
21+
CommitSortStrategies.Time,
22+
CommitSortStrategies.Topological | CommitSortStrategies.Time
23+
};
24+
25+
/// <summary>
26+
/// The repository.
27+
/// </summary>
28+
private readonly Repository _repo;
29+
30+
/// <summary>
31+
/// The file's path relative to the repository's root.
32+
/// </summary>
33+
private readonly string _path;
34+
35+
/// <summary>
36+
/// The filter to be used in querying the commit log.
37+
/// </summary>
38+
private readonly CommitFilter _queryFilter;
39+
40+
#endregion
41+
42+
#region Constructors
43+
44+
/// <summary>
45+
/// Initializes a new instance of the <see cref="FileHistory"/> class.
46+
/// The commits will be enumerated in reverse chronological order.
47+
/// </summary>
48+
/// <param name="repo">The repository.</param>
49+
/// <param name="path">The file's path relative to the repository's root.</param>
50+
/// <exception cref="ArgumentNullException">If any of the parameters is null.</exception>
51+
internal FileHistory(Repository repo, string path)
52+
: this(repo, path, new CommitFilter())
53+
{ }
54+
55+
/// <summary>
56+
/// Initializes a new instance of the <see cref="FileHistory"/> class.
57+
/// The given <see cref="CommitFilter"/> instance specifies the commit
58+
/// sort strategies and range of commits to be considered.
59+
/// Only the time (corresponding to <code>--date-order</code>) and topological
60+
/// (coresponding to <code>--topo-order</code>) sort strategies are supported.
61+
/// </summary>
62+
/// <param name="repo">The repository.</param>
63+
/// <param name="path">The file's path relative to the repository's root.</param>
64+
/// <param name="queryFilter">The filter to be used in querying the commit log.</param>
65+
/// <exception cref="ArgumentNullException">If any of the parameters is null.</exception>
66+
/// <exception cref="ArgumentException">When an unsupported commit sort strategy is specified.</exception>
67+
internal FileHistory(Repository repo, string path, CommitFilter queryFilter)
68+
{
69+
Ensure.ArgumentNotNull(repo, "repo");
70+
Ensure.ArgumentNotNull(path, "path");
71+
Ensure.ArgumentNotNull(queryFilter, "queryFilter");
72+
73+
// Ensure the commit sort strategy makes sense.
74+
if (!AllowedSortStrategies.Contains(queryFilter.SortBy))
75+
throw new ArgumentException(
76+
"Unsupported sort strategy. Only 'Topological', 'Time', or 'Topological | Time' are allowed.",
77+
"queryFilter");
78+
79+
_repo = repo;
80+
_path = path;
81+
_queryFilter = queryFilter;
82+
}
83+
84+
#endregion
85+
86+
#region IEnumerable<LogEntry> Members
87+
88+
/// <summary>
89+
/// Gets the <see cref="IEnumerator{LogEntry}"/> that enumerates the
90+
/// <see cref="LogEntry"/> instances representing the file's history,
91+
/// including renames (as in <code>git log --follow</code>).
92+
/// </summary>
93+
/// <returns>A <see cref="IEnumerator{LogEntry}"/>.</returns>
94+
public IEnumerator<LogEntry> GetEnumerator()
95+
{
96+
return FullHistory(_repo, _path, _queryFilter).GetEnumerator();
97+
}
98+
99+
IEnumerator IEnumerable.GetEnumerator()
100+
{
101+
return GetEnumerator();
102+
}
103+
104+
#endregion
105+
106+
/// <summary>
107+
/// Gets the relevant commits in which the given file was created, changed, or renamed.
108+
/// </summary>
109+
/// <param name="repo">The repository.</param>
110+
/// <param name="path">The file's path relative to the repository's root.</param>
111+
/// <param name="filter">The filter to be used in querying the commits log.</param>
112+
/// <returns>A collection of <see cref="LogEntry"/> instances.</returns>
113+
private static IEnumerable<LogEntry> FullHistory(IRepository repo, string path, CommitFilter filter)
114+
{
115+
var map = new Dictionary<Commit, string>();
116+
117+
foreach (var currentCommit in repo.Commits.QueryBy(filter))
118+
{
119+
var currentPath = map.Keys.Count > 0 ? map[currentCommit] : path;
120+
var currentTreeEntry = currentCommit.Tree[currentPath];
121+
122+
if (currentTreeEntry == null)
123+
{
124+
yield break;
125+
}
126+
127+
var parentCount = currentCommit.Parents.Count();
128+
if (parentCount == 0)
129+
{
130+
yield return new LogEntry { Path = currentPath, Commit = currentCommit };
131+
}
132+
else
133+
{
134+
DetermineParentPaths(repo, currentCommit, currentPath, map);
135+
136+
if (parentCount != 1)
137+
{
138+
continue;
139+
}
140+
141+
var parentCommit = currentCommit.Parents.Single();
142+
var parentPath = map[parentCommit];
143+
var parentTreeEntry = parentCommit.Tree[parentPath];
144+
145+
if (parentTreeEntry == null ||
146+
parentTreeEntry.Target.Id != currentTreeEntry.Target.Id ||
147+
parentPath != currentPath)
148+
{
149+
yield return new LogEntry { Path = currentPath, Commit = currentCommit };
150+
}
151+
}
152+
}
153+
}
154+
155+
private static void DetermineParentPaths(IRepository repo, Commit currentCommit, string currentPath, IDictionary<Commit, string> map)
156+
{
157+
foreach (var parentCommit in currentCommit.Parents.Where(parentCommit => !map.ContainsKey(parentCommit)))
158+
{
159+
map.Add(parentCommit, ParentPath(repo, currentCommit, currentPath, parentCommit));
160+
}
161+
}
162+
163+
private static string ParentPath(IRepository repo, Commit currentCommit, string currentPath, Commit parentCommit)
164+
{
165+
var treeChanges = repo.Diff.Compare<TreeChanges>(parentCommit.Tree, currentCommit.Tree);
166+
var treeEntryChanges = treeChanges.FirstOrDefault(c => c.Path == currentPath);
167+
return treeEntryChanges != null && treeEntryChanges.Status == ChangeKind.Renamed
168+
? treeEntryChanges.OldPath
169+
: currentPath;
170+
}
171+
}
172+
}

LibGit2Sharp/FollowFilter.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace LibGit2Sharp
5+
{
6+
/// <summary>
7+
/// Criteria used to order the commits of the repository when querying its history.
8+
/// <para>
9+
/// The commits will be enumerated from the current HEAD of the repository.
10+
/// </para>
11+
/// </summary>
12+
public sealed class FollowFilter
13+
{
14+
private static readonly List<CommitSortStrategies> AllowedSortStrategies = new List<CommitSortStrategies>
15+
{
16+
CommitSortStrategies.Topological,
17+
CommitSortStrategies.Time,
18+
CommitSortStrategies.Topological | CommitSortStrategies.Time
19+
};
20+
21+
private CommitSortStrategies _sortBy;
22+
23+
/// <summary>
24+
/// Initializes a new instance of <see cref="FollowFilter" />.
25+
/// </summary>
26+
public FollowFilter()
27+
{
28+
SortBy = CommitSortStrategies.Time;
29+
}
30+
31+
/// <summary>
32+
/// The ordering strategy to use.
33+
/// <para>
34+
/// By default, the commits are shown in reverse chronological order.
35+
/// </para>
36+
/// <para>
37+
/// Only 'Topological', 'Time', or 'Topological | Time' are allowed.
38+
/// </para>
39+
/// </summary>
40+
public CommitSortStrategies SortBy
41+
{
42+
get { return _sortBy; }
43+
44+
set
45+
{
46+
if (!AllowedSortStrategies.Contains(value))
47+
{
48+
throw new ArgumentException(
49+
"Unsupported sort strategy. Only 'Topological', 'Time', or 'Topological | Time' are allowed.",
50+
"value");
51+
}
52+
53+
_sortBy = value;
54+
}
55+
}
56+
}
57+
}

LibGit2Sharp/IQueryableCommitLog.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ public interface IQueryableCommitLog : ICommitLog
1515
/// <returns>A list of commits, ready to be enumerated.</returns>
1616
ICommitLog QueryBy(CommitFilter filter);
1717

18+
/// <summary>
19+
/// Returns the list of commits of the repository representing the history of a file beyond renames.
20+
/// </summary>
21+
/// <param name="path">The file's path.</param>
22+
/// <returns>A list of file history entries, ready to be enumerated.</returns>
23+
IEnumerable<LogEntry> QueryBy(string path);
24+
25+
/// <summary>
26+
/// Returns the list of commits of the repository representing the history of a file beyond renames.
27+
/// </summary>
28+
/// <param name="path">The file's path.</param>
29+
/// <param name="filter">The options used to control which commits will be returned.</param>
30+
/// <returns>A list of file history entries, ready to be enumerated.</returns>
31+
IEnumerable<LogEntry> QueryBy(string path, FollowFilter filter);
32+
1833
/// <summary>
1934
/// Find the best possible merge base given two <see cref="Commit"/>s.
2035
/// </summary>

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<Compile Include="CommitOptions.cs" />
6969
<Compile Include="CommitSortStrategies.cs" />
7070
<Compile Include="CompareOptions.cs" />
71+
<Compile Include="Core\FileHistory.cs" />
7172
<Compile Include="Core\Platform.cs" />
7273
<Compile Include="Core\Handles\ConflictIteratorSafeHandle.cs" />
7374
<Compile Include="DescribeOptions.cs" />
@@ -80,6 +81,8 @@
8081
<Compile Include="Core\Handles\IndexReucEntrySafeHandle.cs" />
8182
<Compile Include="EntryExistsException.cs" />
8283
<Compile Include="FetchOptionsBase.cs" />
84+
<Compile Include="LogEntry.cs" />
85+
<Compile Include="FollowFilter.cs" />
8386
<Compile Include="IBelongToARepository.cs" />
8487
<Compile Include="Identity.cs" />
8588
<Compile Include="IndexNameEntryCollection.cs" />

LibGit2Sharp/LogEntry.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace LibGit2Sharp
2+
{
3+
/// <summary>
4+
/// An entry in a file's commit history.
5+
/// </summary>
6+
public sealed class LogEntry
7+
{
8+
/// <summary>
9+
/// The file's path relative to the repository's root.
10+
/// </summary>
11+
public string Path { get; internal set; }
12+
13+
/// <summary>
14+
/// The commit in which the file was created or changed.
15+
/// </summary>
16+
public Commit Commit { get; internal set; }
17+
}
18+
}

0 commit comments

Comments
 (0)