using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Compat; using LibGit2Sharp.Core.Handles; namespace LibGit2Sharp { /// /// The Index is a staging area between the Working directory and the Repository. /// It's used to prepare and aggregate the changes that will be part of the next commit. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class Index : IEnumerable { private readonly IndexSafeHandle handle; private readonly Repository repo; private readonly ConflictCollection conflicts; /// /// Needed for mocking purposes. /// protected Index() { } internal Index(Repository repo) { this.repo = repo; handle = Proxy.git_repository_index(repo.Handle); conflicts = new ConflictCollection(repo); repo.RegisterForCleanup(handle); } internal Index(Repository repo, string indexPath) { this.repo = repo; handle = Proxy.git_index_open(indexPath); Proxy.git_repository_set_index(repo.Handle, handle); conflicts = new ConflictCollection(repo); repo.RegisterForCleanup(handle); } internal IndexSafeHandle Handle { get { return handle; } } /// /// Gets the number of in the index. /// public virtual int Count { get { return Proxy.git_index_entrycount(handle); } } /// /// Determines if the index is free from conflicts. /// public virtual bool IsFullyMerged { get { return !Proxy.git_index_has_conflicts(handle); } } /// /// Gets the with the specified relative path. /// public virtual IndexEntry this[string path] { get { Ensure.ArgumentNotNullOrEmptyString(path, "path"); IndexEntrySafeHandle entryHandle = Proxy.git_index_get_bypath(handle, path, 0); return IndexEntry.BuildFromPtr(repo, entryHandle); } } private IndexEntry this[int index] { get { IndexEntrySafeHandle entryHandle = Proxy.git_index_get_byindex(handle, (UIntPtr)index); return IndexEntry.BuildFromPtr(repo, entryHandle); } } #region IEnumerable Members private List AllIndexEntries() { var entryCount = Count; var list = new List(entryCount); for (int i = 0; i < entryCount; i++) { list.Add(this[i]); } return list; } /// /// Returns an enumerator that iterates through the collection. /// /// An object that can be used to iterate through the collection. public virtual IEnumerator GetEnumerator() { return AllIndexEntries().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(); } #endregion /// /// Promotes to the staging area the latest modifications of a file in the working directory (addition, updation or removal). /// /// The path of the file within the working directory. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// public virtual void Stage(string path, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); Stage(new[] { path }, explicitPathsOptions); } /// /// Promotes to the staging area the latest modifications of a collection of files in the working directory (addition, updation or removal). /// /// The collection of paths of the files within the working directory. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// public virtual void Stage(IEnumerable paths, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(paths, "paths"); TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUntracked | DiffOptions.IncludeIgnored, paths, explicitPathsOptions); foreach (var treeEntryChanges in changes) { switch (treeEntryChanges.Status) { case ChangeKind.Unmodified: continue; case ChangeKind.Deleted: RemoveFromIndex(treeEntryChanges.Path); continue; case ChangeKind.Added: /* Fall through */ case ChangeKind.Modified: AddToIndex(treeEntryChanges.Path); continue; default: throw new InvalidOperationException( string.Format(CultureInfo.InvariantCulture, "Entry '{0}' bears an unexpected ChangeKind '{1}'", treeEntryChanges.Path, treeEntryChanges.Status)); } } UpdatePhysicalIndex(); } /// /// Removes from the staging area all the modifications of a file since the latest commit (addition, updation or removal). /// /// The path of the file within the working directory. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// public virtual void Unstage(string path, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); Unstage(new[] { path }, explicitPathsOptions); } /// /// Removes from the staging area all the modifications of a collection of file since the latest commit (addition, updation or removal). /// /// The collection of paths of the files within the working directory. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// public virtual void Unstage(IEnumerable paths, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(paths, "paths"); if (repo.Info.IsHeadOrphaned) { TreeChanges changes = repo.Diff.Compare(null, DiffTargets.Index, paths, explicitPathsOptions); Reset(changes); } else { repo.Reset("HEAD", paths, explicitPathsOptions); } } /// /// Moves and/or renames a file in the working directory and promotes the change to the staging area. /// /// The path of the file within the working directory which has to be moved/renamed. /// The target path of the file within the working directory. public virtual void Move(string sourcePath, string destinationPath) { Move(new[] { sourcePath }, new[] { destinationPath }); } /// /// Moves and/or renames a collection of files in the working directory and promotes the changes to the staging area. /// /// The paths of the files within the working directory which have to be moved/renamed. /// The target paths of the files within the working directory. public virtual void Move(IEnumerable sourcePaths, IEnumerable destinationPaths) { Ensure.ArgumentNotNull(sourcePaths, "sourcePaths"); Ensure.ArgumentNotNull(destinationPaths, "destinationPaths"); //TODO: Move() should support following use cases: // - Moving a file under a directory ('file' and 'dir' -> 'dir/file') // - Moving a directory (and its content) under another directory ('dir1' and 'dir2' -> 'dir2/dir1/*') //TODO: Move() should throw when: // - Moving a directory under a file IDictionary, Tuple> batch = PrepareBatch(sourcePaths, destinationPaths); if (batch.Count == 0) { throw new ArgumentNullException("sourcePaths"); } foreach (KeyValuePair, Tuple> keyValuePair in batch) { string sourcePath = keyValuePair.Key.Item1; string destPath = keyValuePair.Value.Item1; if (Directory.Exists(sourcePath) || Directory.Exists(destPath)) { throw new NotImplementedException(); } FileStatus sourceStatus = keyValuePair.Key.Item2; if (sourceStatus.HasAny(new Enum[] { FileStatus.Nonexistent, FileStatus.Removed, FileStatus.Untracked, FileStatus.Missing })) { throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unable to move file '{0}'. Its current status is '{1}'.", sourcePath, sourceStatus)); } FileStatus desStatus = keyValuePair.Value.Item2; if (desStatus.HasAny(new Enum[] { FileStatus.Nonexistent, FileStatus.Missing })) { continue; } throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unable to overwrite file '{0}'. Its current status is '{1}'.", destPath, desStatus)); } string wd = repo.Info.WorkingDirectory; foreach (KeyValuePair, Tuple> keyValuePair in batch) { string from = keyValuePair.Key.Item1; string to = keyValuePair.Value.Item1; RemoveFromIndex(from); File.Move(Path.Combine(wd, from), Path.Combine(wd, to)); AddToIndex(to); } UpdatePhysicalIndex(); } /// /// Removes a file from the staging area, and optionally removes it from the working directory as well. /// /// If the file has already been deleted from the working directory, this method will only deal /// with promoting the removal to the staging area. /// /// /// The default behavior is to remove the file from the working directory as well. /// /// /// When not passing a , the passed path will be treated as /// a pathspec. You can for example use it to pass the relative path to a folder inside the working directory, /// so that all files beneath this folders, and the folder itself, will be removed. /// /// /// The path of the file within the working directory. /// True to remove the file from the working directory, False otherwise. /// /// If set, the passed will be treated as an explicit path. /// Use these options to determine how unmatched explicit paths should be handled. /// public virtual void Remove(string path, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); Remove(new[] { path }, removeFromWorkingDirectory, explicitPathsOptions); } /// /// Removes a collection of fileS from the staging, and optionally removes them from the working directory as well. /// /// If a file has already been deleted from the working directory, this method will only deal /// with promoting the removal to the staging area. /// /// /// The default behavior is to remove the files from the working directory as well. /// /// /// When not passing a , the passed paths will be treated as /// a pathspec. You can for example use it to pass the relative paths to folders inside the working directory, /// so that all files beneath these folders, and the folders themselves, will be removed. /// /// /// The collection of paths of the files within the working directory. /// True to remove the files from the working directory, False otherwise. /// /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// public virtual void Remove(IEnumerable paths, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { var pathsList = paths.ToList(); TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUnmodified | DiffOptions.IncludeUntracked, pathsList, explicitPathsOptions); var pathsTodelete = pathsList.Where(p => Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, p))).ToList(); foreach (var treeEntryChanges in changes) { var status = repo.Index.RetrieveStatus(treeEntryChanges.Path); switch (treeEntryChanges.Status) { case ChangeKind.Added: case ChangeKind.Deleted: pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); break; case ChangeKind.Unmodified: if (removeFromWorkingDirectory && ( status.HasFlag(FileStatus.Staged) || status.HasFlag(FileStatus.Added))) { throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}', as it has changes staged in the index. You can call the Remove() method with removeFromWorkingDirectory=false if you want to remove it from the index only.", treeEntryChanges.Path)); } pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); continue; case ChangeKind.Modified: if (status.HasFlag(FileStatus.Modified) && status.HasFlag(FileStatus.Staged)) { throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}', as it has staged content different from both the working directory and the HEAD.", treeEntryChanges.Path)); } if (removeFromWorkingDirectory) { throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}', as it has local modifications. You can call the Remove() method with removeFromWorkingDirectory=false if you want to remove it from the index only.", treeEntryChanges.Path)); } pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); continue; default: throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", treeEntryChanges.Path, treeEntryChanges.Status)); } } if (removeFromWorkingDirectory) { RemoveFilesAndFolders(pathsTodelete); } UpdatePhysicalIndex(); } private void RemoveFilesAndFolders(IEnumerable pathsList) { string wd = repo.Info.WorkingDirectory; foreach (string path in pathsList) { string fileName = Path.Combine(wd, path); if (Directory.Exists(fileName)) { Directory.Delete(fileName, true); continue; } if (!File.Exists(fileName)) { continue; } File.Delete(fileName); } } private IDictionary, Tuple> PrepareBatch(IEnumerable leftPaths, IEnumerable rightPaths) { IDictionary, Tuple> dic = new Dictionary, Tuple>(); IEnumerator leftEnum = leftPaths.GetEnumerator(); IEnumerator rightEnum = rightPaths.GetEnumerator(); while (Enumerate(leftEnum, rightEnum)) { Tuple from = BuildFrom(leftEnum.Current); Tuple to = BuildFrom(rightEnum.Current); dic.Add(from, to); } return dic; } private Tuple BuildFrom(string path) { string relativePath = repo.BuildRelativePathFrom(path); return new Tuple(relativePath, RetrieveStatus(relativePath)); } private static bool Enumerate(IEnumerator leftEnum, IEnumerator rightEnum) { bool isLeftEoF = leftEnum.MoveNext(); bool isRightEoF = rightEnum.MoveNext(); if (isLeftEoF == isRightEoF) { return isLeftEoF; } throw new ArgumentException("The collection of paths are of different lengths."); } private void AddToIndex(string relativePath) { if (!repo.Submodules.TryStage(relativePath, true)) { Proxy.git_index_add_bypath(handle, relativePath); } } private string RemoveFromIndex(string relativePath) { Proxy.git_index_remove_bypath(handle, relativePath); return relativePath; } private void UpdatePhysicalIndex() { Proxy.git_index_write(handle); } /// /// Retrieves the state of a file in the working directory, comparing it against the staging area and the latest commmit. /// /// The relative path within the working directory to the file. /// A representing the state of the parameter. public virtual FileStatus RetrieveStatus(string filePath) { Ensure.ArgumentNotNullOrEmptyString(filePath, "filePath"); string relativePath = repo.BuildRelativePathFrom(filePath); return Proxy.git_status_file(repo.Handle, relativePath); } /// /// Retrieves the state of all files in the working directory, comparing them against the staging area and the latest commmit. /// /// A holding the state of all the files. public virtual RepositoryStatus RetrieveStatus() { return new RepositoryStatus(repo); } internal void Reset(TreeChanges changes) { foreach (TreeEntryChanges treeEntryChanges in changes) { switch (treeEntryChanges.Status) { case ChangeKind.Unmodified: continue; case ChangeKind.Added: RemoveFromIndex(treeEntryChanges.Path); continue; case ChangeKind.Deleted: /* Fall through */ case ChangeKind.Modified: ReplaceIndexEntryWith(treeEntryChanges); continue; default: throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Entry '{0}' bears an unexpected ChangeKind '{1}'", treeEntryChanges.Path, treeEntryChanges.Status)); } } UpdatePhysicalIndex(); } /// /// Gets the conflicts that exist. /// public virtual ConflictCollection Conflicts { get { return conflicts; } } private void ReplaceIndexEntryWith(TreeEntryChanges treeEntryChanges) { var indexEntry = new GitIndexEntry { Mode = (uint)treeEntryChanges.OldMode, oid = treeEntryChanges.OldOid.Oid, Path = FilePathMarshaler.FromManaged(treeEntryChanges.OldPath), }; Proxy.git_index_add(handle, indexEntry); Marshal.FreeHGlobal(indexEntry.Path); } private string DebuggerDisplay { get { return string.Format(CultureInfo.InvariantCulture, "Count = {0}", Count); } } } }