using System; using System.Collections.Generic; using System.Linq; using System.Text; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Compat; using LibGit2Sharp.Core.Handles; using Environment = System.Environment; namespace LibGit2Sharp { /// /// Show changes between the working tree and the index or a tree, changes between the index and a tree, changes between two trees, or changes between two files on disk. /// /// Copied and renamed files currently cannot be detected, as the feature is not supported by libgit2 yet. /// These files will be shown as a pair of Deleted/Added files. /// public class Diff { private readonly Repository repo; private static GitDiffOptions BuildOptions(DiffOptions diffOptions, FilePath[] filePaths = null, MatchedPathsAggregator matchedPathsAggregator = null, CompareOptions compareOptions = null) { var options = new GitDiffOptions(); options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_TYPECHANGE; compareOptions = compareOptions ?? new CompareOptions(); options.ContextLines = (ushort)compareOptions.ContextLines; options.InterhunkLines = (ushort)compareOptions.InterhunkLines; if (diffOptions.HasFlag(DiffOptions.IncludeUntracked)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED | GitDiffOptionFlags.GIT_DIFF_RECURSE_UNTRACKED_DIRS | GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED_CONTENT; } if (diffOptions.HasFlag(DiffOptions.IncludeIgnored)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_IGNORED; } if (diffOptions.HasFlag(DiffOptions.IncludeUnmodified)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED; } if (diffOptions.HasFlag(DiffOptions.DisablePathspecMatch)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_DISABLE_PATHSPEC_MATCH; } if (matchedPathsAggregator != null) { options.NotifyCallback = matchedPathsAggregator.OnGitDiffNotify; } if (filePaths == null) { return options; } options.PathSpec = GitStrArrayIn.BuildFrom(filePaths); return options; } private static FilePath[] ToFilePaths(Repository repo, IEnumerable paths) { if (paths == null) { return null; } var filePaths = new List(); foreach (string path in paths) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException("At least one provided path is either null or empty.", "paths"); } filePaths.Add(repo.BuildRelativePathFrom(path)); } if (filePaths.Count == 0) { throw new ArgumentException("No path has been provided.", "paths"); } return filePaths.ToArray(); } /// /// Needed for mocking purposes. /// protected Diff() { } internal Diff(Repository repo) { this.repo = repo; } /// /// Show changes between two s. /// /// The you want to compare from. /// The you want to compare to. /// The list of paths (either files or directories) that should be compared. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// /// Additional options to define comparison behavior. /// A containing the changes between the and the . public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) { var comparer = TreeToTree(repo); ObjectId oldTreeId = oldTree != null ? oldTree.Id : null; ObjectId newTreeId = newTree != null ? newTree.Id : null; var diffOptions = DiffOptions.None; if (explicitPathsOptions != null) { diffOptions |= DiffOptions.DisablePathspecMatch; if (explicitPathsOptions.ShouldFailOnUnmatchedPath || explicitPathsOptions.OnUnmatchedPath != null) { diffOptions |= DiffOptions.IncludeUnmodified; } } return BuildTreeChangesFromComparer(oldTreeId, newTreeId, comparer, diffOptions, paths, explicitPathsOptions, compareOptions); } /// /// Show changes between two s. /// /// The you want to compare from. /// The you want to compare to. /// Additional options to define comparison behavior. /// A containing the changes between the and the . public virtual ContentChanges Compare(Blob oldBlob, Blob newBlob, CompareOptions compareOptions = null) { using (GitDiffOptions options = BuildOptions(DiffOptions.None, compareOptions: compareOptions)) { return new ContentChanges(repo, oldBlob, newBlob, options); } } private readonly IDictionary> handleRetrieverDispatcher = BuildHandleRetrieverDispatcher(); private static IDictionary> BuildHandleRetrieverDispatcher() { return new Dictionary> { { DiffTargets.Index, IndexToTree }, { DiffTargets.WorkingDirectory, WorkdirToTree }, { DiffTargets.Index | DiffTargets.WorkingDirectory, WorkdirAndIndexToTree }, }; } /// /// Show changes between a and the Index, the Working Directory, or both. /// /// The to compare from. /// The targets to compare to. /// The list of paths (either files or directories) that should be compared. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// /// Additional options to define comparison behavior. /// A containing the changes between the and the selected target. public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) { var comparer = handleRetrieverDispatcher[diffTargets](repo); ObjectId oldTreeId = oldTree != null ? oldTree.Id : null; DiffOptions diffOptions = diffTargets.HasFlag(DiffTargets.WorkingDirectory) ? DiffOptions.IncludeUntracked : DiffOptions.None; if (explicitPathsOptions != null) { diffOptions |= DiffOptions.DisablePathspecMatch; if (explicitPathsOptions.ShouldFailOnUnmatchedPath || explicitPathsOptions.OnUnmatchedPath != null) { diffOptions |= DiffOptions.IncludeUnmodified; } } return BuildTreeChangesFromComparer(oldTreeId, null, comparer, diffOptions, paths, explicitPathsOptions, compareOptions); } /// /// Show changes between the working directory and the index. /// /// The list of paths (either files or directories) that should be compared. /// If true, include untracked files from the working dir as additions. Otherwise ignore them. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// /// Additional options to define comparison behavior. /// A containing the changes between the working directory and the index. public virtual TreeChanges Compare(IEnumerable paths = null, bool includeUntracked = false, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) { return Compare(includeUntracked ? DiffOptions.IncludeUntracked : DiffOptions.None, paths, explicitPathsOptions, compareOptions); } internal virtual TreeChanges Compare(DiffOptions diffOptions, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) { var comparer = WorkdirToIndex(repo); if (explicitPathsOptions != null) { diffOptions |= DiffOptions.DisablePathspecMatch; if (explicitPathsOptions.ShouldFailOnUnmatchedPath || explicitPathsOptions.OnUnmatchedPath != null) { diffOptions |= DiffOptions.IncludeUnmodified; } } return BuildTreeChangesFromComparer(null, null, comparer, diffOptions, paths, explicitPathsOptions, compareOptions); } private delegate DiffListSafeHandle TreeComparisonHandleRetriever(ObjectId oldTreeId, ObjectId newTreeId, GitDiffOptions options); private static TreeComparisonHandleRetriever TreeToTree(Repository repo) { return (oh, nh, o) => Proxy.git_diff_tree_to_tree(repo.Handle, oh, nh, o); } private static TreeComparisonHandleRetriever WorkdirToIndex(Repository repo) { return (oh, nh, o) => Proxy.git_diff_index_to_workdir(repo.Handle, repo.Index.Handle, o); } private static TreeComparisonHandleRetriever WorkdirToTree(Repository repo) { return (oh, nh, o) => Proxy.git_diff_tree_to_workdir(repo.Handle, oh, o); } private static TreeComparisonHandleRetriever WorkdirAndIndexToTree(Repository repo) { TreeComparisonHandleRetriever comparisonHandleRetriever = (oh, nh, o) => { DiffListSafeHandle diff = null, diff2 = null; try { diff = Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, oh, o); diff2 = Proxy.git_diff_index_to_workdir(repo.Handle, repo.Index.Handle, o); Proxy.git_diff_merge(diff, diff2); } catch { diff.SafeDispose(); throw; } finally { diff2.SafeDispose(); } return diff; }; return comparisonHandleRetriever; } private static TreeComparisonHandleRetriever IndexToTree(Repository repo) { return (oh, nh, o) => Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, oh, o); } private TreeChanges BuildTreeChangesFromComparer( ObjectId oldTreeId, ObjectId newTreeId, TreeComparisonHandleRetriever comparisonHandleRetriever, DiffOptions diffOptions, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) { var matchedPaths = new MatchedPathsAggregator(); var filePaths = ToFilePaths(repo, paths); using (GitDiffOptions options = BuildOptions(diffOptions, filePaths, matchedPaths, compareOptions)) using (DiffListSafeHandle diffList = comparisonHandleRetriever(oldTreeId, newTreeId, options)) { if (explicitPathsOptions != null) { DispatchUnmatchedPaths(explicitPathsOptions, filePaths, matchedPaths); } return new TreeChanges(diffList); } } private static void DispatchUnmatchedPaths(ExplicitPathsOptions explicitPathsOptions, IEnumerable filePaths, IEnumerable matchedPaths) { List unmatchedPaths = (filePaths != null ? filePaths.Except(matchedPaths) : Enumerable.Empty()).ToList(); if (!unmatchedPaths.Any()) { return; } if (explicitPathsOptions.OnUnmatchedPath != null) { unmatchedPaths.ForEach(filePath => explicitPathsOptions.OnUnmatchedPath(filePath.Native)); } if (explicitPathsOptions.ShouldFailOnUnmatchedPath) { throw new UnmatchedPathException(BuildUnmatchedPathsMessage(unmatchedPaths)); } } private static string BuildUnmatchedPathsMessage(List unmatchedPaths) { var message = new StringBuilder("There were some unmatched paths:" + Environment.NewLine); unmatchedPaths.ForEach(filePath => message.AppendFormat("- {0}{1}", filePath.Native, Environment.NewLine)); return message.ToString(); } } }