using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; namespace LibGit2Sharp { /// /// Holds the result of the determination of the state of the working directory. /// Only files that differ from the current index and/or commit will be considered. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class RepositoryStatus : IEnumerable { private readonly ICollection statusEntries; private readonly List added = new List(); private readonly List staged = new List(); private readonly List removed = new List(); private readonly List missing = new List(); private readonly List modified = new List(); private readonly List untracked = new List(); private readonly List ignored = new List(); private readonly List renamedInIndex = new List(); private readonly List renamedInWorkDir = new List(); private readonly List unaltered = new List(); private readonly bool isDirty; private readonly IDictionary> dispatcher = Build(); private static IDictionary> Build() { return new Dictionary> { { FileStatus.NewInWorkdir, (rs, s) => rs.untracked.Add(s) }, { FileStatus.ModifiedInWorkdir, (rs, s) => rs.modified.Add(s) }, { FileStatus.DeletedFromWorkdir, (rs, s) => rs.missing.Add(s) }, { FileStatus.NewInIndex, (rs, s) => rs.added.Add(s) }, { FileStatus.ModifiedInIndex, (rs, s) => rs.staged.Add(s) }, { FileStatus.DeletedFromIndex, (rs, s) => rs.removed.Add(s) }, { FileStatus.RenamedInIndex, (rs, s) => rs.renamedInIndex.Add(s) }, { FileStatus.Ignored, (rs, s) => rs.ignored.Add(s) }, { FileStatus.RenamedInWorkdir, (rs, s) => rs.renamedInWorkDir.Add(s) }, }; } /// /// Needed for mocking purposes. /// protected RepositoryStatus() { } internal RepositoryStatus(Repository repo, StatusOptions options) { statusEntries = new List(); using (GitStatusOptions coreOptions = CreateStatusOptions(options ?? new StatusOptions())) using (StatusListSafeHandle list = Proxy.git_status_list_new(repo.Handle, coreOptions)) { int count = Proxy.git_status_list_entrycount(list); for (int i = 0; i < count; i++) { StatusEntrySafeHandle e = Proxy.git_status_byindex(list, i); GitStatusEntry entry = e.MarshalAsGitStatusEntry(); GitDiffDelta deltaHeadToIndex = null; GitDiffDelta deltaIndexToWorkDir = null; if (entry.HeadToIndexPtr != IntPtr.Zero) { deltaHeadToIndex = entry.HeadToIndexPtr.MarshalAs(); } if (entry.IndexToWorkDirPtr != IntPtr.Zero) { deltaIndexToWorkDir = entry.IndexToWorkDirPtr.MarshalAs(); } AddStatusEntryForDelta(entry.Status, deltaHeadToIndex, deltaIndexToWorkDir); } isDirty = statusEntries.Any(entry => entry.State != FileStatus.Ignored && entry.State != FileStatus.Unaltered); } } private static GitStatusOptions CreateStatusOptions(StatusOptions options) { var coreOptions = new GitStatusOptions { Version = 1, Show = (GitStatusShow)options.Show, Flags = GitStatusOptionFlags.IncludeIgnored | GitStatusOptionFlags.IncludeUntracked | GitStatusOptionFlags.RecurseUntrackedDirs, }; if (options.DetectRenamesInIndex) { coreOptions.Flags |= GitStatusOptionFlags.RenamesHeadToIndex | GitStatusOptionFlags.RenamesFromRewrites; } if (options.DetectRenamesInWorkDir) { coreOptions.Flags |= GitStatusOptionFlags.RenamesIndexToWorkDir | GitStatusOptionFlags.RenamesFromRewrites; } if (options.ExcludeSubmodules) { coreOptions.Flags |= GitStatusOptionFlags.ExcludeSubmodules; } if (options.RecurseIgnoredDirs) { coreOptions.Flags |= GitStatusOptionFlags.RecurseIgnoredDirs; } if (options.PathSpec != null) { coreOptions.PathSpec = GitStrArrayManaged.BuildFrom(options.PathSpec); } if (options.DisablePathSpecMatch) { coreOptions.Flags |= GitStatusOptionFlags.DisablePathspecMatch; } if (options.IncludeUnaltered) { coreOptions.Flags |= GitStatusOptionFlags.IncludeUnmodified; } return coreOptions; } private void AddStatusEntryForDelta(FileStatus gitStatus, GitDiffDelta deltaHeadToIndex, GitDiffDelta deltaIndexToWorkDir) { RenameDetails headToIndexRenameDetails = null; RenameDetails indexToWorkDirRenameDetails = null; if ((gitStatus & FileStatus.RenamedInIndex) == FileStatus.RenamedInIndex) { headToIndexRenameDetails = new RenameDetails( LaxFilePathMarshaler.FromNative(deltaHeadToIndex.OldFile.Path).Native, LaxFilePathMarshaler.FromNative(deltaHeadToIndex.NewFile.Path).Native, (int)deltaHeadToIndex.Similarity); } if ((gitStatus & FileStatus.RenamedInWorkdir) == FileStatus.RenamedInWorkdir) { indexToWorkDirRenameDetails = new RenameDetails( LaxFilePathMarshaler.FromNative(deltaIndexToWorkDir.OldFile.Path).Native, LaxFilePathMarshaler.FromNative(deltaIndexToWorkDir.NewFile.Path).Native, (int)deltaIndexToWorkDir.Similarity); } var filePath = (deltaIndexToWorkDir != null) ? LaxFilePathMarshaler.FromNative(deltaIndexToWorkDir.NewFile.Path).Native : LaxFilePathMarshaler.FromNative(deltaHeadToIndex.NewFile.Path).Native; StatusEntry statusEntry = new StatusEntry(filePath, gitStatus, headToIndexRenameDetails, indexToWorkDirRenameDetails); if (gitStatus == FileStatus.Unaltered) { unaltered.Add(statusEntry); } else { foreach (KeyValuePair> kvp in dispatcher) { if (!gitStatus.HasFlag(kvp.Key)) { continue; } kvp.Value(this, statusEntry); } } statusEntries.Add(statusEntry); } /// /// Gets the for the specified relative path. /// public virtual StatusEntry this[string path] { get { Ensure.ArgumentNotNullOrEmptyString(path, "path"); var entries = statusEntries.Where(e => string.Equals(e.FilePath, path, StringComparison.Ordinal)).ToList(); Debug.Assert(!(entries.Count > 1)); if (entries.Count == 0) { return new StatusEntry(path, FileStatus.Nonexistent); } return entries.Single(); } } /// /// Returns an enumerator that iterates through the collection. /// /// An object that can be used to iterate through the collection. public virtual IEnumerator GetEnumerator() { return statusEntries.GetEnumerator(); } /// /// Returns an enumerator that iterates through the collection. /// /// An object that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// List of files added to the index, which are not in the current commit /// public virtual IEnumerable Added { get { return added; } } /// /// List of files added to the index, which are already in the current commit with different content /// public virtual IEnumerable Staged { get { return staged; } } /// /// List of files removed from the index but are existent in the current commit /// public virtual IEnumerable Removed { get { return removed; } } /// /// List of files existent in the index but are missing in the working directory /// public virtual IEnumerable Missing { get { return missing; } } /// /// List of files with unstaged modifications. A file may be modified and staged at the same time if it has been modified after adding. /// public virtual IEnumerable Modified { get { return modified; } } /// /// List of files existing in the working directory but are neither tracked in the index nor in the current commit. /// public virtual IEnumerable Untracked { get { return untracked; } } /// /// List of files existing in the working directory that are ignored. /// public virtual IEnumerable Ignored { get { return ignored; } } /// /// List of files that were renamed and staged. /// public virtual IEnumerable RenamedInIndex { get { return renamedInIndex; } } /// /// List of files that were renamed in the working directory but have not been staged. /// public virtual IEnumerable RenamedInWorkDir { get { return renamedInWorkDir; } } /// /// List of files that were unmodified in the working directory. /// public virtual IEnumerable Unaltered { get { return unaltered; } } /// /// True if the index or the working directory has been altered since the last commit. False otherwise. /// public virtual bool IsDirty { get { return isDirty; } } private string DebuggerDisplay { get { return string.Format( CultureInfo.InvariantCulture, "+{0} ~{1} -{2} | +{3} ~{4} -{5} | i{6}", Added.Count(), Staged.Count(), Removed.Count(), Untracked.Count(), Modified.Count(), Missing.Count(), Ignored.Count()); } } } }