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; } } }