using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; 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(DiffModifiers 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(DiffModifiers.IncludeUntracked)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED | GitDiffOptionFlags.GIT_DIFF_RECURSE_UNTRACKED_DIRS | GitDiffOptionFlags.GIT_DIFF_SHOW_UNTRACKED_CONTENT; } if (diffOptions.HasFlag(DiffModifiers.IncludeIgnored)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_IGNORED | GitDiffOptionFlags.GIT_DIFF_RECURSE_IGNORED_DIRS; } if (diffOptions.HasFlag(DiffModifiers.IncludeUnmodified) || compareOptions.IncludeUnmodified || (compareOptions.Similarity != null && (compareOptions.Similarity.RenameDetectionMode == RenameDetectionMode.CopiesHarder || compareOptions.Similarity.RenameDetectionMode == RenameDetectionMode.Exact))) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED; } if (compareOptions.Algorithm == DiffAlgorithm.Patience) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_PATIENCE; } else if (compareOptions.Algorithm == DiffAlgorithm.Minimal) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_MINIMAL; } if (diffOptions.HasFlag(DiffModifiers.DisablePathspecMatch)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_DISABLE_PATHSPEC_MATCH; } if (matchedPathsAggregator != null) { options.NotifyCallback = matchedPathsAggregator.OnGitDiffNotify; } if (filePaths != null) { options.PathSpec = GitStrArrayManaged.BuildFrom(filePaths); } return options; } /// /// Needed for mocking purposes. /// protected Diff() { } internal Diff(Repository repo) { this.repo = repo; } private static readonly IDictionary> HandleRetrieverDispatcher = BuildHandleRetrieverDispatcher(); private static IDictionary> BuildHandleRetrieverDispatcher() { return new Dictionary> { { DiffTargets.Index, IndexToTree }, { DiffTargets.WorkingDirectory, WorkdirToTree }, { DiffTargets.Index | DiffTargets.WorkingDirectory, WorkdirAndIndexToTree }, }; } private static readonly IDictionary> ChangesBuilders = new Dictionary> { { typeof(Patch), diff => new Patch(diff) }, { typeof(TreeChanges), diff => new TreeChanges(diff) }, { typeof(PatchStats), diff => new PatchStats(diff) }, }; private static T BuildDiffResult(DiffHandle diff) where T : class, IDiffResult { Func builder; if (!ChangesBuilders.TryGetValue(typeof(T), out builder)) { throw new LibGit2SharpException("User-defined types passed to Compare are not supported. Supported values are: {0}", string.Join(", ", ChangesBuilders.Keys.Select(x => x.Name))); } return (T)builder(diff); } /// /// Show changes between two s. /// /// The you want to compare from. /// The you want to compare to. /// A containing the changes between the and the . public virtual ContentChanges Compare(Blob oldBlob, Blob newBlob) { return Compare(oldBlob, newBlob, null); } /// /// 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) { using (GitDiffOptions options = BuildOptions(DiffModifiers.None, compareOptions: compareOptions)) { return new ContentChanges(repo, oldBlob, newBlob, options); } } /// /// Show changes between two s. /// /// The you want to compare from. /// The you want to compare to. /// A containing the changes between the and the . public virtual T Compare(Tree oldTree, Tree newTree) where T : class, IDiffResult { return Compare(oldTree, newTree, null, null, null); } /// /// 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. /// A containing the changes between the and the . public virtual T Compare(Tree oldTree, Tree newTree, IEnumerable paths) where T : class, IDiffResult { return Compare(oldTree, newTree, paths, null, null); } /// /// 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. /// /// A containing the changes between the and the . public virtual T Compare(Tree oldTree, Tree newTree, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions) where T : class, IDiffResult { return Compare(oldTree, newTree, paths, explicitPathsOptions, null); } /// /// 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. /// Additional options to define patch generation behavior. /// A containing the changes between the and the . public virtual T Compare(Tree oldTree, Tree newTree, IEnumerable paths, CompareOptions compareOptions) where T : class, IDiffResult { return Compare(oldTree, newTree, paths, null, compareOptions); } /// /// Show changes between two s. /// /// The you want to compare from. /// The you want to compare to. /// Additional options to define patch generation behavior. /// A containing the changes between the and the . public virtual T Compare(Tree oldTree, Tree newTree, CompareOptions compareOptions) where T : class, IDiffResult { return Compare(oldTree, newTree, null, null, compareOptions); } /// /// 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 patch generation behavior. /// A containing the changes between the and the . public virtual T Compare(Tree oldTree, Tree newTree, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions, CompareOptions compareOptions) where T : class, IDiffResult { var comparer = TreeToTree(repo); ObjectId oldTreeId = oldTree != null ? oldTree.Id : null; ObjectId newTreeId = newTree != null ? newTree.Id : null; var diffOptions = DiffModifiers.None; if (explicitPathsOptions != null) { diffOptions |= DiffModifiers.DisablePathspecMatch; if (explicitPathsOptions.ShouldFailOnUnmatchedPath || explicitPathsOptions.OnUnmatchedPath != null) { diffOptions |= DiffModifiers.IncludeUnmodified; } } using (DiffHandle diff = BuildDiffList(oldTreeId, newTreeId, comparer, diffOptions, paths, explicitPathsOptions, compareOptions)) { return BuildDiffResult(diff); } } /// /// Show changes between a and the Index, the Working Directory, or both. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// The to compare from. /// The targets to compare to. /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the and the selected target. public virtual T Compare(Tree oldTree, DiffTargets diffTargets) where T : class, IDiffResult { return Compare(oldTree, diffTargets, null, null, null); } /// /// Show changes between a and the Index, the Working Directory, or both. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// The to compare from. /// The targets to compare to. /// The list of paths (either files or directories) that should be compared. /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the and the selected target. public virtual T Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths) where T : class, IDiffResult { return Compare(oldTree, diffTargets, paths, null, null); } /// /// Show changes between a and the Index, the Working Directory, or both. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// 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. /// /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the and the selected target. public virtual T Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions) where T : class, IDiffResult { return Compare(oldTree, diffTargets, paths, explicitPathsOptions, null); } /// /// Show changes between a and the Index, the Working Directory, or both. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// 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 patch generation behavior. /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the and the selected target. public virtual T Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions, CompareOptions compareOptions) where T : class, IDiffResult { var comparer = HandleRetrieverDispatcher[diffTargets](repo); ObjectId oldTreeId = oldTree != null ? oldTree.Id : null; DiffModifiers diffOptions = diffTargets.HasFlag(DiffTargets.WorkingDirectory) ? DiffModifiers.IncludeUntracked : DiffModifiers.None; if (explicitPathsOptions != null) { diffOptions |= DiffModifiers.DisablePathspecMatch; if (explicitPathsOptions.ShouldFailOnUnmatchedPath || explicitPathsOptions.OnUnmatchedPath != null) { diffOptions |= DiffModifiers.IncludeUnmodified; } } using (DiffHandle diff = BuildDiffList(oldTreeId, null, comparer, diffOptions, paths, explicitPathsOptions, compareOptions)) { return BuildDiffResult(diff); } } /// /// Show changes between the working directory and the index. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the working directory and the index. public virtual T Compare() where T : class, IDiffResult { return Compare(DiffModifiers.None); } /// /// Show changes between the working directory and the index. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// The list of paths (either files or directories) that should be compared. /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the working directory and the index. public virtual T Compare(IEnumerable paths) where T : class, IDiffResult { return Compare(DiffModifiers.None, paths); } /// /// Show changes between the working directory and the index. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// 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. /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the working directory and the index. public virtual T Compare(IEnumerable paths, bool includeUntracked) where T : class, IDiffResult { return Compare(includeUntracked ? DiffModifiers.IncludeUntracked : DiffModifiers.None, paths); } /// /// Show changes between the working directory and the index. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// 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. /// /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the working directory and the index. public virtual T Compare(IEnumerable paths, bool includeUntracked, ExplicitPathsOptions explicitPathsOptions) where T : class, IDiffResult { return Compare(includeUntracked ? DiffModifiers.IncludeUntracked : DiffModifiers.None, paths, explicitPathsOptions); } /// /// Show changes between the working directory and the index. /// /// The level of diff performed can be specified by passing either a /// or type as the generic parameter. /// /// /// 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 patch generation behavior. /// Can be either a if you are only interested in the list of files modified, added, ..., or /// a if you want the actual patch content for the whole diff and for individual files. /// A containing the changes between the working directory and the index. public virtual T Compare( IEnumerable paths, bool includeUntracked, ExplicitPathsOptions explicitPathsOptions, CompareOptions compareOptions) where T : class, IDiffResult { return Compare(includeUntracked ? DiffModifiers.IncludeUntracked : DiffModifiers.None, paths, explicitPathsOptions, compareOptions); } internal virtual T Compare( DiffModifiers diffOptions, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) where T : class, IDiffResult { var comparer = WorkdirToIndex(repo); if (explicitPathsOptions != null) { diffOptions |= DiffModifiers.DisablePathspecMatch; if (explicitPathsOptions.ShouldFailOnUnmatchedPath || explicitPathsOptions.OnUnmatchedPath != null) { diffOptions |= DiffModifiers.IncludeUnmodified; } } using (DiffHandle diff = BuildDiffList(null, null, comparer, diffOptions, paths, explicitPathsOptions, compareOptions)) { return BuildDiffResult(diff); } } internal delegate DiffHandle 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) => { DiffHandle diff = Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, oh, o); using (DiffHandle diff2 = Proxy.git_diff_index_to_workdir(repo.Handle, repo.Index.Handle, o)) { Proxy.git_diff_merge(diff, diff2); } 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 DiffHandle BuildDiffList( ObjectId oldTreeId, ObjectId newTreeId, TreeComparisonHandleRetriever comparisonHandleRetriever, DiffModifiers diffOptions, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions, CompareOptions compareOptions) { var matchedPaths = new MatchedPathsAggregator(); var filePaths = repo.ToFilePaths(paths); using (GitDiffOptions options = BuildOptions(diffOptions, filePaths, matchedPaths, compareOptions)) { var diffList = comparisonHandleRetriever(oldTreeId, newTreeId, options); if (explicitPathsOptions != null) { try { DispatchUnmatchedPaths(explicitPathsOptions, filePaths, matchedPaths); } catch { diffList.Dispose(); throw; } } DetectRenames(diffList, compareOptions); return diffList; } } private static void DetectRenames(DiffHandle diffList, CompareOptions compareOptions) { var similarityOptions = (compareOptions == null) ? null : compareOptions.Similarity; if (similarityOptions == null || similarityOptions.RenameDetectionMode == RenameDetectionMode.Default) { Proxy.git_diff_find_similar(diffList, null); return; } if (similarityOptions.RenameDetectionMode == RenameDetectionMode.None) { return; } var opts = new GitDiffFindOptions { RenameThreshold = (ushort)similarityOptions.RenameThreshold, RenameFromRewriteThreshold = (ushort)similarityOptions.RenameFromRewriteThreshold, CopyThreshold = (ushort)similarityOptions.CopyThreshold, BreakRewriteThreshold = (ushort)similarityOptions.BreakRewriteThreshold, RenameLimit = (UIntPtr)similarityOptions.RenameLimit, }; switch (similarityOptions.RenameDetectionMode) { case RenameDetectionMode.Exact: opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_EXACT_MATCH_ONLY | GitDiffFindFlags.GIT_DIFF_FIND_RENAMES | GitDiffFindFlags.GIT_DIFF_FIND_COPIES | GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; break; case RenameDetectionMode.Renames: opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES; break; case RenameDetectionMode.Copies: opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES | GitDiffFindFlags.GIT_DIFF_FIND_COPIES; break; case RenameDetectionMode.CopiesHarder: opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES | GitDiffFindFlags.GIT_DIFF_FIND_COPIES | GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; break; } if (!compareOptions.IncludeUnmodified) { opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_REMOVE_UNMODIFIED; } switch (similarityOptions.WhitespaceMode) { case WhitespaceMode.DontIgnoreWhitespace: opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE; break; case WhitespaceMode.IgnoreLeadingWhitespace: opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE; break; case WhitespaceMode.IgnoreAllWhitespace: opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_IGNORE_WHITESPACE; break; } Proxy.git_diff_find_similar(diffList, opts); } 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(); } } }