-
Notifications
You must be signed in to change notification settings - Fork 899
FileHistory Feature #963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FileHistory Feature #963
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace LibGit2Sharp.Core | ||
{ | ||
/// <summary> | ||
/// Represents a file-related log of commits beyond renames. | ||
/// </summary> | ||
internal class FileHistory : IEnumerable<LogEntry> | ||
{ | ||
#region Fields | ||
|
||
/// <summary> | ||
/// The allowed commit sort strategies. | ||
/// </summary> | ||
private static readonly List<CommitSortStrategies> AllowedSortStrategies = new List<CommitSortStrategies> | ||
{ | ||
CommitSortStrategies.Topological, | ||
CommitSortStrategies.Time, | ||
CommitSortStrategies.Topological | CommitSortStrategies.Time | ||
}; | ||
|
||
/// <summary> | ||
/// The repository. | ||
/// </summary> | ||
private readonly Repository _repo; | ||
|
||
/// <summary> | ||
/// The file's path relative to the repository's root. | ||
/// </summary> | ||
private readonly string _path; | ||
|
||
/// <summary> | ||
/// The filter to be used in querying the commit log. | ||
/// </summary> | ||
private readonly CommitFilter _queryFilter; | ||
|
||
#endregion | ||
|
||
#region Constructors | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="FileHistory"/> class. | ||
/// The commits will be enumerated in reverse chronological order. | ||
/// </summary> | ||
/// <param name="repo">The repository.</param> | ||
/// <param name="path">The file's path relative to the repository's root.</param> | ||
/// <exception cref="ArgumentNullException">If any of the parameters is null.</exception> | ||
internal FileHistory(Repository repo, string path) | ||
: this(repo, path, new CommitFilter()) | ||
{ } | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="FileHistory"/> class. | ||
/// The given <see cref="CommitFilter"/> instance specifies the commit | ||
/// sort strategies and range of commits to be considered. | ||
/// Only the time (corresponding to <code>--date-order</code>) and topological | ||
/// (coresponding to <code>--topo-order</code>) sort strategies are supported. | ||
/// </summary> | ||
/// <param name="repo">The repository.</param> | ||
/// <param name="path">The file's path relative to the repository's root.</param> | ||
/// <param name="queryFilter">The filter to be used in querying the commit log.</param> | ||
/// <exception cref="ArgumentNullException">If any of the parameters is null.</exception> | ||
/// <exception cref="ArgumentException">When an unsupported commit sort strategy is specified.</exception> | ||
internal FileHistory(Repository repo, string path, CommitFilter queryFilter) | ||
{ | ||
Ensure.ArgumentNotNull(repo, "repo"); | ||
Ensure.ArgumentNotNull(path, "path"); | ||
Ensure.ArgumentNotNull(queryFilter, "queryFilter"); | ||
|
||
// Ensure the commit sort strategy makes sense. | ||
if (!AllowedSortStrategies.Contains(queryFilter.SortBy)) | ||
throw new ArgumentException( | ||
"Unsupported sort strategy. Only 'Topological', 'Time', or 'Topological | Time' are allowed.", | ||
"queryFilter"); | ||
|
||
_repo = repo; | ||
_path = path; | ||
_queryFilter = queryFilter; | ||
} | ||
|
||
#endregion | ||
|
||
#region IEnumerable<LogEntry> Members | ||
|
||
/// <summary> | ||
/// Gets the <see cref="IEnumerator{LogEntry}"/> that enumerates the | ||
/// <see cref="LogEntry"/> instances representing the file's history, | ||
/// including renames (as in <code>git log --follow</code>). | ||
/// </summary> | ||
/// <returns>A <see cref="IEnumerator{LogEntry}"/>.</returns> | ||
public IEnumerator<LogEntry> GetEnumerator() | ||
{ | ||
return FullHistory(_repo, _path, _queryFilter).GetEnumerator(); | ||
} | ||
|
||
IEnumerator IEnumerable.GetEnumerator() | ||
{ | ||
return GetEnumerator(); | ||
} | ||
|
||
#endregion | ||
|
||
/// <summary> | ||
/// Gets the relevant commits in which the given file was created, changed, or renamed. | ||
/// </summary> | ||
/// <param name="repo">The repository.</param> | ||
/// <param name="path">The file's path relative to the repository's root.</param> | ||
/// <param name="filter">The filter to be used in querying the commits log.</param> | ||
/// <returns>A collection of <see cref="LogEntry"/> instances.</returns> | ||
private static IEnumerable<LogEntry> FullHistory(IRepository repo, string path, CommitFilter filter) | ||
{ | ||
var map = new Dictionary<Commit, string>(); | ||
|
||
foreach (var currentCommit in repo.Commits.QueryBy(filter)) | ||
{ | ||
var currentPath = map.Keys.Count > 0 ? map[currentCommit] : path; | ||
var currentTreeEntry = currentCommit.Tree[currentPath]; | ||
|
||
if (currentTreeEntry == null) | ||
{ | ||
yield break; | ||
} | ||
|
||
var parentCount = currentCommit.Parents.Count(); | ||
if (parentCount == 0) | ||
{ | ||
yield return new LogEntry { Path = currentPath, Commit = currentCommit }; | ||
} | ||
else | ||
{ | ||
DetermineParentPaths(repo, currentCommit, currentPath, map); | ||
|
||
if (parentCount != 1) | ||
{ | ||
continue; | ||
} | ||
|
||
var parentCommit = currentCommit.Parents.Single(); | ||
var parentPath = map[parentCommit]; | ||
var parentTreeEntry = parentCommit.Tree[parentPath]; | ||
|
||
if (parentTreeEntry == null || | ||
parentTreeEntry.Target.Id != currentTreeEntry.Target.Id || | ||
parentPath != currentPath) | ||
{ | ||
yield return new LogEntry { Path = currentPath, Commit = currentCommit }; | ||
} | ||
} | ||
} | ||
} | ||
|
||
private static void DetermineParentPaths(IRepository repo, Commit currentCommit, string currentPath, IDictionary<Commit, string> map) | ||
{ | ||
foreach (var parentCommit in currentCommit.Parents.Where(parentCommit => !map.ContainsKey(parentCommit))) | ||
{ | ||
map.Add(parentCommit, ParentPath(repo, currentCommit, currentPath, parentCommit)); | ||
} | ||
} | ||
|
||
private static string ParentPath(IRepository repo, Commit currentCommit, string currentPath, Commit parentCommit) | ||
{ | ||
var treeChanges = repo.Diff.Compare<TreeChanges>(parentCommit.Tree, currentCommit.Tree); | ||
var treeEntryChanges = treeChanges.FirstOrDefault(c => c.Path == currentPath); | ||
return treeEntryChanges != null && treeEntryChanges.Status == ChangeKind.Renamed | ||
? treeEntryChanges.OldPath | ||
: currentPath; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
namespace LibGit2Sharp | ||
{ | ||
/// <summary> | ||
/// Criteria used to order the commits of the repository when querying its history. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we don't expose
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add. |
||
/// <para> | ||
/// The commits will be enumerated from the current HEAD of the repository. | ||
/// </para> | ||
/// </summary> | ||
public sealed class FollowFilter | ||
{ | ||
private static readonly List<CommitSortStrategies> AllowedSortStrategies = new List<CommitSortStrategies> | ||
{ | ||
CommitSortStrategies.Topological, | ||
CommitSortStrategies.Time, | ||
CommitSortStrategies.Topological | CommitSortStrategies.Time | ||
}; | ||
|
||
private CommitSortStrategies _sortBy; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="FollowFilter" />. | ||
/// </summary> | ||
public FollowFilter() | ||
{ | ||
SortBy = CommitSortStrategies.Time; | ||
} | ||
|
||
/// <summary> | ||
/// The ordering strategy to use. | ||
/// <para> | ||
/// By default, the commits are shown in reverse chronological order. | ||
/// </para> | ||
/// <para> | ||
/// Only 'Topological', 'Time', or 'Topological | Time' are allowed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Nice touch! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. |
||
/// </para> | ||
/// </summary> | ||
public CommitSortStrategies SortBy | ||
{ | ||
get { return _sortBy; } | ||
|
||
set | ||
{ | ||
if (!AllowedSortStrategies.Contains(value)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd rather see this test moved in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But that would mean that you could create an invalid If we moved the test, I wouldn't see the need for What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And by the way, I would be with you if we talked about using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm. It looks like my memory was wrong. The Framework design guidelines doesn't specify anything about avoid throwing from setters. Let's keep it this way, then. Sorry for the noise. FWIW, the sole purpose of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, no problem. |
||
{ | ||
throw new ArgumentException( | ||
"Unsupported sort strategy. Only 'Topological', 'Time', or 'Topological | Time' are allowed.", | ||
"value"); | ||
} | ||
|
||
_sortBy = value; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace LibGit2Sharp | ||
{ | ||
/// <summary> | ||
/// An entry in a file's commit history. | ||
/// </summary> | ||
public sealed class LogEntry | ||
{ | ||
/// <summary> | ||
/// The file's path relative to the repository's root. | ||
/// </summary> | ||
public string Path { get; internal set; } | ||
|
||
/// <summary> | ||
/// The commit in which the file was created or changed. | ||
/// </summary> | ||
public Commit Commit { get; internal set; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to give in to the fact that VS removes the trailing new line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What can you do about that in .csproj files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ThomasBarnekow It's usually recommended to have all text files ending with a final newline, but VS keeps on removing them from .csproj. One way to work around this is through partial staging. One would only stage the interesting chunks and reset the removal of the newline in the workdir. But don't bother about this, @jamill's right. This is useless ceremony. Let's keep the file as it is.
@jamill I'm surrendering to VisualStudio trimming frenzy 😉