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