using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace LibGit2Sharp.Core
{
///
/// Represents a file-related log of commits beyond renames.
///
internal class FileHistory : IEnumerable
{
#region Fields
///
/// The allowed commit sort strategies.
///
private static readonly List AllowedSortStrategies = new List
{
CommitSortStrategies.Topological,
CommitSortStrategies.Time,
CommitSortStrategies.Topological | CommitSortStrategies.Time
};
///
/// The repository.
///
private readonly Repository _repo;
///
/// The file's path relative to the repository's root.
///
private readonly string _path;
///
/// The filter to be used in querying the commit log.
///
private readonly CommitFilter _queryFilter;
#endregion
#region Constructors
///
/// Initializes a new instance of the class.
/// The commits will be enumerated in reverse chronological order.
///
/// The repository.
/// The file's path relative to the repository's root.
/// If any of the parameters is null.
internal FileHistory(Repository repo, string path)
: this(repo, path, new CommitFilter())
{ }
///
/// Initializes a new instance of the class.
/// The given instance specifies the commit
/// sort strategies and range of commits to be considered.
/// Only the time (corresponding to --date-order) and topological
/// (coresponding to --topo-order) sort strategies are supported.
///
/// The repository.
/// The file's path relative to the repository's root.
/// The filter to be used in querying the commit log.
/// If any of the parameters is null.
/// When an unsupported commit sort strategy is specified.
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 Members
///
/// Gets the that enumerates the
/// instances representing the file's history,
/// including renames (as in git log --follow).
///
/// A .
public IEnumerator GetEnumerator()
{
return FullHistory(_repo, _path, _queryFilter).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
///
/// Gets the relevant commits in which the given file was created, changed, or renamed.
///
/// The repository.
/// The file's path relative to the repository's root.
/// The filter to be used in querying the commits log.
/// A collection of instances.
private static IEnumerable FullHistory(IRepository repo, string path, CommitFilter filter)
{
var map = new Dictionary();
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 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(parentCommit.Tree, currentCommit.Tree);
var treeEntryChanges = treeChanges.FirstOrDefault(c => c.Path == currentPath);
return treeEntryChanges != null && treeEntryChanges.Status == ChangeKind.Renamed
? treeEntryChanges.OldPath
: currentPath;
}
}
}