diff --git a/LibGit2Sharp.Tests/IgnoreFixture.cs b/LibGit2Sharp.Tests/IgnoreFixture.cs
index ef91bd179..0ad15a3f4 100644
--- a/LibGit2Sharp.Tests/IgnoreFixture.cs
+++ b/LibGit2Sharp.Tests/IgnoreFixture.cs
@@ -16,15 +16,15 @@ public void TemporaryRulesShouldApplyUntilCleared()
{
Touch(repo.Info.WorkingDirectory, "Foo.cs", "Bar");
- Assert.True(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs"));
+ Assert.True(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs"));
repo.Ignore.AddTemporaryRules(new[] { "*.cs" });
- Assert.False(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs"));
+ Assert.False(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs"));
repo.Ignore.ResetAllTemporaryRules();
- Assert.True(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs"));
+ Assert.True(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs"));
}
}
diff --git a/LibGit2Sharp.Tests/ResetIndexFixture.cs b/LibGit2Sharp.Tests/ResetIndexFixture.cs
index 121da8c74..23e6a1cc4 100644
--- a/LibGit2Sharp.Tests/ResetIndexFixture.cs
+++ b/LibGit2Sharp.Tests/ResetIndexFixture.cs
@@ -93,7 +93,7 @@ public void CanResetTheIndexToTheContentOfACommitWithCommittishAsArgument()
"deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt" };
Assert.Equal(expected.Length, newStatus.Where(IsStaged).Count());
- Assert.Equal(expected, newStatus.Removed);
+ Assert.Equal(expected, newStatus.Removed.Select(s => s.FilePath));
}
}
@@ -111,7 +111,7 @@ public void CanResetTheIndexToTheContentOfACommitWithCommitAsArgument()
"deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt" };
Assert.Equal(expected.Length, newStatus.Where(IsStaged).Count());
- Assert.Equal(expected, newStatus.Removed);
+ Assert.Equal(expected, newStatus.Removed.Select(s => s.FilePath));
}
}
diff --git a/LibGit2Sharp.Tests/StatusFixture.cs b/LibGit2Sharp.Tests/StatusFixture.cs
index f9a1c487e..b70ff0ae0 100644
--- a/LibGit2Sharp.Tests/StatusFixture.cs
+++ b/LibGit2Sharp.Tests/StatusFixture.cs
@@ -20,6 +20,42 @@ public void CanRetrieveTheStatusOfAFile()
}
}
+ [Theory]
+ [InlineData(StatusShowOption.IndexAndWorkDir, FileStatus.Untracked)]
+ [InlineData(StatusShowOption.WorkDirOnly, FileStatus.Untracked)]
+ [InlineData(StatusShowOption.IndexOnly, FileStatus.Nonexistent)]
+ public void CanLimitStatusToWorkDirOnly(StatusShowOption show, FileStatus expected)
+ {
+ var clone = CloneStandardTestRepo();
+
+ using (var repo = new Repository(clone))
+ {
+ Touch(repo.Info.WorkingDirectory, "file.txt", "content");
+
+ RepositoryStatus status = repo.Index.RetrieveStatus(new StatusOptions() { Show = show });
+ Assert.Equal(expected, status["file.txt"].State);
+ }
+ }
+
+ [Theory]
+ [InlineData(StatusShowOption.IndexAndWorkDir, FileStatus.Added)]
+ [InlineData(StatusShowOption.WorkDirOnly, FileStatus.Nonexistent)]
+ [InlineData(StatusShowOption.IndexOnly, FileStatus.Added)]
+ public void CanLimitStatusToIndexOnly(StatusShowOption show, FileStatus expected)
+ {
+ var clone = CloneStandardTestRepo();
+
+ using (var repo = new Repository(clone))
+ {
+ Touch(repo.Info.WorkingDirectory, "file.txt", "content");
+ repo.Index.Stage("file.txt");
+
+ RepositoryStatus status = repo.Index.RetrieveStatus(new StatusOptions() { Show = show });
+ Assert.Equal(expected, status["file.txt"].State);
+ }
+ }
+
+
[Theory]
[InlineData("file")]
[InlineData("file.txt")]
@@ -75,18 +111,18 @@ public void CanRetrieveTheStatusOfTheWholeWorkingDirectory()
RepositoryStatus status = repo.Index.RetrieveStatus();
- Assert.Equal(FileStatus.Staged, status[file]);
+ Assert.Equal(FileStatus.Staged, status[file].State);
Assert.NotNull(status);
Assert.Equal(6, status.Count());
Assert.True(status.IsDirty);
- Assert.Equal("new_untracked_file.txt", status.Untracked.Single());
- Assert.Equal("modified_unstaged_file.txt", status.Modified.Single());
- Assert.Equal("deleted_unstaged_file.txt", status.Missing.Single());
- Assert.Equal("new_tracked_file.txt", status.Added.Single());
- Assert.Equal(file, status.Staged.Single());
- Assert.Equal("deleted_staged_file.txt", status.Removed.Single());
+ Assert.Equal("new_untracked_file.txt", status.Untracked.Select(s => s.FilePath).Single());
+ Assert.Equal("modified_unstaged_file.txt", status.Modified.Select(s => s.FilePath).Single());
+ Assert.Equal("deleted_unstaged_file.txt", status.Missing.Select(s => s.FilePath).Single());
+ Assert.Equal("new_tracked_file.txt", status.Added.Select(s => s.FilePath).Single());
+ Assert.Equal(file, status.Staged.Select(s => s.FilePath).Single());
+ Assert.Equal("deleted_staged_file.txt", status.Removed.Select(s => s.FilePath).Single());
File.AppendAllText(Path.Combine(repo.Info.WorkingDirectory, file),
"Tclem's favorite commit message: boom");
@@ -94,18 +130,113 @@ public void CanRetrieveTheStatusOfTheWholeWorkingDirectory()
Assert.Equal(FileStatus.Staged | FileStatus.Modified, repo.Index.RetrieveStatus(file));
RepositoryStatus status2 = repo.Index.RetrieveStatus();
- Assert.Equal(FileStatus.Staged | FileStatus.Modified, status2[file]);
+ Assert.Equal(FileStatus.Staged | FileStatus.Modified, status2[file].State);
Assert.NotNull(status2);
Assert.Equal(6, status2.Count());
Assert.True(status2.IsDirty);
- Assert.Equal("new_untracked_file.txt", status2.Untracked.Single());
- Assert.Equal(new[] { file, "modified_unstaged_file.txt" }, status2.Modified);
- Assert.Equal("deleted_unstaged_file.txt", status2.Missing.Single());
- Assert.Equal("new_tracked_file.txt", status2.Added.Single());
- Assert.Equal(file, status2.Staged.Single());
- Assert.Equal("deleted_staged_file.txt", status2.Removed.Single());
+ Assert.Equal("new_untracked_file.txt", status2.Untracked.Select(s => s.FilePath).Single());
+ Assert.Equal(new[] { file, "modified_unstaged_file.txt" }, status2.Modified.Select(s => s.FilePath));
+ Assert.Equal("deleted_unstaged_file.txt", status2.Missing.Select(s => s.FilePath).Single());
+ Assert.Equal("new_tracked_file.txt", status2.Added.Select(s => s.FilePath).Single());
+ Assert.Equal(file, status2.Staged.Select(s => s.FilePath).Single());
+ Assert.Equal("deleted_staged_file.txt", status2.Removed.Select(s => s.FilePath).Single());
+ }
+ }
+
+ [Fact]
+ public void CanRetrieveTheStatusOfRenamedFilesInWorkDir()
+ {
+ string path = CloneStandardTestRepo();
+ using (var repo = new Repository(path))
+ {
+ Touch(repo.Info.WorkingDirectory, "old_name.txt",
+ "This is a file with enough data to trigger similarity matching.\r\n" +
+ "This is a file with enough data to trigger similarity matching.\r\n" +
+ "This is a file with enough data to trigger similarity matching.\r\n" +
+ "This is a file with enough data to trigger similarity matching.\r\n");
+
+ repo.Index.Stage("old_name.txt");
+
+ File.Move(Path.Combine(repo.Info.WorkingDirectory, "old_name.txt"),
+ Path.Combine(repo.Info.WorkingDirectory, "rename_target.txt"));
+
+ RepositoryStatus status = repo.Index.RetrieveStatus(
+ new StatusOptions()
+ {
+ DetectRenamesInIndex = true,
+ DetectRenamesInWorkDir = true
+ });
+
+ Assert.Equal(FileStatus.Added | FileStatus.RenamedInWorkDir, status["rename_target.txt"].State);
+ Assert.Equal(100, status["rename_target.txt"].IndexToWorkDirRenameDetails.Similarity);
+ }
+ }
+
+ [Fact]
+ public void CanRetrieveTheStatusOfRenamedFilesInIndex()
+ {
+ string path = CloneStandardTestRepo();
+ using (var repo = new Repository(path))
+ {
+ File.Move(
+ Path.Combine(repo.Info.WorkingDirectory, "1.txt"),
+ Path.Combine(repo.Info.WorkingDirectory, "rename_target.txt"));
+
+ repo.Index.Stage("1.txt");
+ repo.Index.Stage("rename_target.txt");
+
+ RepositoryStatus status = repo.Index.RetrieveStatus();
+
+ Assert.Equal(FileStatus.RenamedInIndex, status["rename_target.txt"].State);
+ Assert.Equal(100, status["rename_target.txt"].HeadToIndexRenameDetails.Similarity);
+ }
+ }
+
+ [Fact]
+ public void CanDetectedVariousKindsOfRenaming()
+ {
+ string path = InitNewRepository();
+ using (var repo = new Repository(path))
+ {
+ Touch(repo.Info.WorkingDirectory, "file.txt",
+ "This is a file with enough data to trigger similarity matching.\r\n" +
+ "This is a file with enough data to trigger similarity matching.\r\n" +
+ "This is a file with enough data to trigger similarity matching.\r\n" +
+ "This is a file with enough data to trigger similarity matching.\r\n");
+
+ repo.Index.Stage("file.txt");
+ repo.Commit("Initial commit", Constants.Signature, Constants.Signature);
+
+ File.Move(Path.Combine(repo.Info.WorkingDirectory, "file.txt"),
+ Path.Combine(repo.Info.WorkingDirectory, "renamed.txt"));
+
+ var opts = new StatusOptions
+ {
+ DetectRenamesInIndex = true,
+ DetectRenamesInWorkDir = true
+ };
+
+ RepositoryStatus status = repo.Index.RetrieveStatus(opts);
+
+ // This passes as expected
+ Assert.Equal(FileStatus.RenamedInWorkDir, status.Single().State);
+
+ repo.Index.Stage("file.txt");
+ repo.Index.Stage("renamed.txt");
+
+ status = repo.Index.RetrieveStatus(opts);
+
+ Assert.Equal(FileStatus.RenamedInIndex, status.Single().State);
+
+ File.Move(Path.Combine(repo.Info.WorkingDirectory, "renamed.txt"),
+ Path.Combine(repo.Info.WorkingDirectory, "renamed_again.txt"));
+
+ status = repo.Index.RetrieveStatus(opts);
+
+ Assert.Equal(FileStatus.RenamedInWorkDir | FileStatus.RenamedInIndex,
+ status.Single().State);
}
}
@@ -154,7 +285,7 @@ public void RetrievingTheStatusOfARepositoryReturnNativeFilePaths()
Assert.Equal(relFilePath, statusEntry.FilePath);
- Assert.Equal(statusEntry.FilePath, repoStatus.Added.Single());
+ Assert.Equal(statusEntry.FilePath, repoStatus.Added.Select(s => s.FilePath).Single());
}
}
@@ -169,15 +300,15 @@ public void RetrievingTheStatusOfAnEmptyRepositoryHonorsTheGitIgnoreDirectives()
Touch(repo.Info.WorkingDirectory, relativePath, "I'm going to be ignored!");
RepositoryStatus status = repo.Index.RetrieveStatus();
- Assert.Equal(new[] { relativePath }, status.Untracked);
+ Assert.Equal(new[] { relativePath }, status.Untracked.Select(s => s.FilePath));
Touch(repo.Info.WorkingDirectory, ".gitignore", "*.txt" + Environment.NewLine);
RepositoryStatus newStatus = repo.Index.RetrieveStatus();
- Assert.Equal(".gitignore", newStatus.Untracked.Single());
+ Assert.Equal(".gitignore", newStatus.Untracked.Select(s => s.FilePath).Single());
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath));
- Assert.Equal(new[] { relativePath }, newStatus.Ignored);
+ Assert.Equal(new[] { relativePath }, newStatus.Ignored.Select(s => s.FilePath));
}
}
@@ -223,7 +354,7 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives()
RepositoryStatus status = repo.Index.RetrieveStatus();
- Assert.Equal(new[]{relativePath, "new_untracked_file.txt"}, status.Untracked);
+ Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, status.Untracked.Select(s => s.FilePath));
Touch(repo.Info.WorkingDirectory, ".gitignore", "*.txt" + Environment.NewLine);
@@ -263,10 +394,10 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives()
*/
RepositoryStatus newStatus = repo.Index.RetrieveStatus();
- Assert.Equal(".gitignore", newStatus.Untracked.Single());
+ Assert.Equal(".gitignore", newStatus.Untracked.Select(s => s.FilePath).Single());
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath));
- Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored);
+ Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored.Select(s => s.FilePath));
}
}
@@ -354,7 +485,7 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroug
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus("bin/what-about-me.txt"));
RepositoryStatus newStatus = repo.Index.RetrieveStatus();
- Assert.Equal(new[] { "bin" + dirSep }, newStatus.Ignored);
+ Assert.Equal(new[] { "bin" + dirSep }, newStatus.Ignored.Select(s => s.FilePath));
var sb = new StringBuilder();
sb.AppendLine("bin/*");
@@ -366,8 +497,43 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroug
newStatus = repo.Index.RetrieveStatus();
- Assert.Equal(new[] { "bin" + dirSep + "look-ma.txt" }, newStatus.Ignored);
- Assert.True(newStatus.Untracked.Contains("bin" + dirSep + "what-about-me.txt" ));
+ Assert.Equal(new[] { "bin" + dirSep + "look-ma.txt" }, newStatus.Ignored.Select(s => s.FilePath));
+ Assert.True(newStatus.Untracked.Select(s => s.FilePath).Contains("bin" + dirSep + "what-about-me.txt"));
+ }
+ }
+
+ [Fact]
+ public void CanRetrieveStatusOfFilesInSubmodule()
+ {
+ var path = CloneSubmoduleTestRepo();
+ using (var repo = new Repository(path))
+ {
+ string[] expected = new string[] {
+ ".gitmodules",
+ "sm_changed_file",
+ "sm_changed_head",
+ "sm_changed_index",
+ "sm_changed_untracked_file",
+ "sm_missing_commits"
+ };
+
+ RepositoryStatus status = repo.Index.RetrieveStatus();
+ Assert.Equal(expected, status.Modified.Select(x => x.FilePath).ToArray());
+ }
+ }
+
+ [Fact]
+ public void CanExcludeStatusOfFilesInSubmodule()
+ {
+ var path = CloneSubmoduleTestRepo();
+ using (var repo = new Repository(path))
+ {
+ string[] expected = new string[] {
+ ".gitmodules",
+ };
+
+ RepositoryStatus status = repo.Index.RetrieveStatus(new StatusOptions() { ExcludeSubmodules = true });
+ Assert.Equal(expected, status.Modified.Select(x => x.FilePath).ToArray());
}
}
}
diff --git a/LibGit2Sharp/Core/GitStatusEntry.cs b/LibGit2Sharp/Core/GitStatusEntry.cs
new file mode 100644
index 000000000..2e13a186a
--- /dev/null
+++ b/LibGit2Sharp/Core/GitStatusEntry.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace LibGit2Sharp.Core
+{
+ ///
+ /// A status entry from libgit2.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal class GitStatusEntry
+ {
+ ///
+ /// Calculated status of a filepath in the working directory considering the current and the .
+ ///
+ public FileStatus Status;
+
+ ///
+ /// The difference between the and .
+ ///
+ public IntPtr HeadToIndexPtr;
+
+ ///
+ /// The difference between the and the working directory.
+ ///
+ public IntPtr IndexToWorkDirPtr;
+ }
+}
diff --git a/LibGit2Sharp/Core/GitStatusOptions.cs b/LibGit2Sharp/Core/GitStatusOptions.cs
new file mode 100644
index 000000000..5d02ebfe9
--- /dev/null
+++ b/LibGit2Sharp/Core/GitStatusOptions.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace LibGit2Sharp.Core
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal class GitStatusOptions : IDisposable
+ {
+ public uint Version = 1;
+
+ public GitStatusShow Show;
+ public GitStatusOptionFlags Flags;
+
+ GitStrArrayIn PathSpec;
+
+ public void Dispose()
+ {
+ if (PathSpec == null)
+ {
+ return;
+ }
+
+ PathSpec.Dispose();
+ }
+ }
+
+ internal enum GitStatusShow
+ {
+ IndexAndWorkDir = 0,
+ IndexOnly = 1,
+ WorkDirOnly = 2,
+ }
+
+ [Flags]
+ internal enum GitStatusOptionFlags
+ {
+ IncludeUntracked = (1 << 0),
+ IncludeIgnored = (1 << 1),
+ IncludeUnmodified = (1 << 2),
+ ExcludeSubmodules = (1 << 3),
+ RecurseUntrackedDirs = (1 << 4),
+ DisablePathspecMatch = (1 << 5),
+ RecurseIgnoredDirs = (1 << 6),
+ RenamesHeadToIndex = (1 << 7),
+ RenamesIndexToWorkDir = (1 << 8),
+ SortCaseSensitively = (1 << 9),
+ SortCaseInsensitively = (1 << 10),
+ RenamesFromRewrites = (1 << 11),
+ }
+}
diff --git a/LibGit2Sharp/Core/Handles/StatusEntrySafeHandle.cs b/LibGit2Sharp/Core/Handles/StatusEntrySafeHandle.cs
new file mode 100644
index 000000000..ef8433529
--- /dev/null
+++ b/LibGit2Sharp/Core/Handles/StatusEntrySafeHandle.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace LibGit2Sharp.Core.Handles
+{
+ internal class StatusEntrySafeHandle : NotOwnedSafeHandleBase
+ {
+ public StatusEntrySafeHandle()
+ : base()
+ {
+ }
+
+ public StatusEntrySafeHandle(IntPtr handle)
+ : base()
+ {
+ this.SetHandle(handle);
+ }
+
+ public GitStatusEntry MarshalAsGitStatusEntry()
+ {
+ return (GitStatusEntry)Marshal.PtrToStructure(handle, typeof(GitStatusEntry));
+ }
+ }
+}
diff --git a/LibGit2Sharp/Core/Handles/StatusListSafeHandle.cs b/LibGit2Sharp/Core/Handles/StatusListSafeHandle.cs
new file mode 100644
index 000000000..74316a3fa
--- /dev/null
+++ b/LibGit2Sharp/Core/Handles/StatusListSafeHandle.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace LibGit2Sharp.Core.Handles
+{
+ internal class StatusListSafeHandle : SafeHandleBase
+ {
+ protected override bool ReleaseHandleImpl()
+ {
+ Proxy.git_status_list_free(handle);
+ return true;
+ }
+ }
+}
diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs
index 93e0ff595..2c6a2a6ea 100644
--- a/LibGit2Sharp/Core/NativeMethods.cs
+++ b/LibGit2Sharp/Core/NativeMethods.cs
@@ -1127,13 +1127,25 @@ internal static extern int git_status_file(
RepositorySafeHandle repo,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath filepath);
- internal delegate int git_status_cb(
- IntPtr path,
- uint statusflags,
- IntPtr payload);
[DllImport(libgit2)]
- internal static extern int git_status_foreach(RepositorySafeHandle repo, git_status_cb cb, IntPtr payload);
+ internal static extern int git_status_list_new(
+ out StatusListSafeHandle git_status_list,
+ RepositorySafeHandle repo,
+ GitStatusOptions options);
+
+ [DllImport(libgit2)]
+ internal static extern int git_status_list_entrycount(
+ StatusListSafeHandle statusList);
+
+ [DllImport(libgit2)]
+ internal static extern StatusEntrySafeHandle git_status_byindex(
+ StatusListSafeHandle list,
+ UIntPtr idx);
+
+ [DllImport(libgit2)]
+ internal static extern void git_status_list_free(
+ IntPtr statusList);
[DllImport(libgit2)]
internal static extern int git_submodule_lookup(
diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs
index fd8f6fda7..8fa771656 100644
--- a/LibGit2Sharp/Core/Proxy.cs
+++ b/LibGit2Sharp/Core/Proxy.cs
@@ -2178,9 +2178,35 @@ public static FileStatus git_status_file(RepositorySafeHandle repo, FilePath pat
}
}
- public static ICollection git_status_foreach(RepositorySafeHandle repo, Func resultSelector)
+ public static StatusListSafeHandle git_status_list_new(RepositorySafeHandle repo, GitStatusOptions options)
{
- return git_foreach(resultSelector, c => NativeMethods.git_status_foreach(repo, (x, y, p) => c(x, y, p), IntPtr.Zero));
+ using (ThreadAffinity())
+ {
+ StatusListSafeHandle handle;
+ int res = NativeMethods.git_status_list_new(out handle, repo, options);
+ Ensure.ZeroResult(res);
+ return handle;
+ }
+ }
+
+ public static int git_status_list_entrycount(StatusListSafeHandle list)
+ {
+ using (ThreadAffinity())
+ {
+ int res = NativeMethods.git_status_list_entrycount(list);
+ Ensure.Int32Result(res);
+ return res;
+ }
+ }
+
+ public static StatusEntrySafeHandle git_status_byindex(StatusListSafeHandle list, long idx)
+ {
+ return NativeMethods.git_status_byindex(list, (UIntPtr)idx);
+ }
+
+ public static void git_status_list_free(IntPtr statusList)
+ {
+ NativeMethods.git_status_list_free(statusList);
}
#endregion
diff --git a/LibGit2Sharp/FileStatus.cs b/LibGit2Sharp/FileStatus.cs
index bf6975d0d..08fa73f60 100644
--- a/LibGit2Sharp/FileStatus.cs
+++ b/LibGit2Sharp/FileStatus.cs
@@ -36,7 +36,7 @@ public enum FileStatus
///
/// The renaming of a file has been promoted from the working directory to the Index. A previous version exists in the Head.
///
- Renamed = (1 << 3), /* GIT_STATUS_INDEX_RENAMED */
+ RenamedInIndex = (1 << 3), /* GIT_STATUS_INDEX_RENAMED */
///
/// A change in type for a file has been promoted from the working directory to the Index. A previous version exists in the Head.
@@ -63,6 +63,11 @@ public enum FileStatus
///
TypeChanged = (1 << 10), /* GIT_STATUS_WT_TYPECHANGE */
+ ///
+ /// The file has been renamed in the working directory. The previous version at the previous name exists in the Index.
+ ///
+ RenamedInWorkDir = (1 << 11), /* GIT_STATUS_WT_RENAMED */
+
///
/// The file is but its name and/or path matches an exclude pattern in a gitignore file.
///
diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs
index 992c0f703..1e7031e60 100644
--- a/LibGit2Sharp/Index.cs
+++ b/LibGit2Sharp/Index.cs
@@ -498,11 +498,13 @@ public virtual FileStatus RetrieveStatus(string filePath)
///
/// Retrieves the state of all files in the working directory, comparing them against the staging area and the latest commmit.
///
+ /// If set, the options that control the status investigation.
/// A holding the state of all the files.
- public virtual RepositoryStatus RetrieveStatus()
+ public virtual RepositoryStatus RetrieveStatus(StatusOptions options = null)
{
ReloadFromDisk();
- return new RepositoryStatus(repo);
+
+ return new RepositoryStatus(repo, options);
}
internal void Reset(TreeChanges changes)
diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj
index 3832ac559..e0a30a56b 100644
--- a/LibGit2Sharp/LibGit2Sharp.csproj
+++ b/LibGit2Sharp/LibGit2Sharp.csproj
@@ -89,6 +89,12 @@
+
+
+
+
+
+
diff --git a/LibGit2Sharp/RenameDetails.cs b/LibGit2Sharp/RenameDetails.cs
new file mode 100644
index 000000000..bfc470589
--- /dev/null
+++ b/LibGit2Sharp/RenameDetails.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Diagnostics;
+using LibGit2Sharp.Core;
+
+namespace LibGit2Sharp
+{
+ ///
+ /// Holds the rename details of a particular file.
+ ///
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ public class RenameDetails : IEquatable
+ {
+ private readonly string oldFilePath;
+ private readonly string newFilePath;
+ private readonly int similarity;
+
+ private static readonly LambdaEqualityHelper equalityHelper =
+ new LambdaEqualityHelper(x => x.OldFilePath, x => x.NewFilePath, x => x.Similarity);
+
+ ///
+ /// Needed for mocking purposes.
+ ///
+ protected RenameDetails()
+ { }
+
+ internal RenameDetails(string oldFilePath, string newFilePath, int similarity)
+ {
+ this.oldFilePath = oldFilePath;
+ this.newFilePath = newFilePath;
+ this.similarity = similarity;
+ }
+
+ ///
+ /// Gets the relative filepath to the working directory of the old file (the rename source).
+ ///
+ public virtual string OldFilePath
+ {
+ get { return oldFilePath; }
+ }
+
+ ///
+ /// Gets the relative filepath to the working directory of the new file (the rename target).
+ ///
+ public virtual string NewFilePath
+ {
+ get { return newFilePath; }
+ }
+
+ ///
+ /// Gets the similarity between the old file an the new file (0-100).
+ ///
+ public virtual int Similarity
+ {
+ get { return similarity; }
+ }
+
+ ///
+ /// Determines whether the specified is equal to the current .
+ ///
+ /// The to compare with the current .
+ /// True if the specified is equal to the current ; otherwise, false.
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as RenameDetails);
+ }
+
+ ///
+ /// Determines whether the specified is equal to the current .
+ ///
+ /// The to compare with the current .
+ /// True if the specified is equal to the current ; otherwise, false.
+ public bool Equals(RenameDetails other)
+ {
+ return equalityHelper.Equals(this, other);
+ }
+
+ ///
+ /// Returns the hash code for this instance.
+ ///
+ /// A 32-bit signed integer hash code.
+ public override int GetHashCode()
+ {
+ return equalityHelper.GetHashCode(this);
+ }
+
+ ///
+ /// Tests if two are equal.
+ ///
+ /// First to compare.
+ /// Second to compare.
+ /// True if the two objects are equal; false otherwise.
+ public static bool operator ==(RenameDetails left, RenameDetails right)
+ {
+ return Equals(left, right);
+ }
+
+ ///
+ /// Tests if two are different.
+ ///
+ /// First to compare.
+ /// Second to compare.
+ /// True if the two objects are different; false otherwise.
+ public static bool operator !=(RenameDetails left, RenameDetails right)
+ {
+ return !Equals(left, right);
+ }
+
+ private string DebuggerDisplay
+ {
+ get
+ {
+ return string.Format("{0} -> {1} [{2}%]", OldFilePath, NewFilePath, Similarity);
+ }
+ }
+ }
+}
diff --git a/LibGit2Sharp/RepositoryStatus.cs b/LibGit2Sharp/RepositoryStatus.cs
index f025ab0a4..ddb8f58db 100644
--- a/LibGit2Sharp/RepositoryStatus.cs
+++ b/LibGit2Sharp/RepositoryStatus.cs
@@ -4,8 +4,10 @@
using System.Diagnostics;
using System.Globalization;
using System.Linq;
+using System.Runtime.InteropServices;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Compat;
+using LibGit2Sharp.Core.Handles;
namespace LibGit2Sharp
{
@@ -17,20 +19,22 @@ namespace LibGit2Sharp
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 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 bool isDirty;
- private readonly IDictionary> dispatcher = Build();
+ private readonly IDictionary> dispatcher = Build();
- private static IDictionary> Build()
+ private static IDictionary> Build()
{
- return new Dictionary>
+ return new Dictionary>
{
{ FileStatus.Untracked, (rs, s) => rs.untracked.Add(s) },
{ FileStatus.Modified, (rs, s) => rs.modified.Add(s) },
@@ -38,7 +42,9 @@ private static IDictionary> Build()
{ FileStatus.Added, (rs, s) => rs.added.Add(s) },
{ FileStatus.Staged, (rs, s) => rs.staged.Add(s) },
{ FileStatus.Removed, (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) }
};
}
@@ -48,34 +54,118 @@ private static IDictionary> Build()
protected RepositoryStatus()
{ }
- internal RepositoryStatus(Repository repo)
+ internal RepositoryStatus(Repository repo, StatusOptions options)
{
- statusEntries = Proxy.git_status_foreach(repo.Handle, StateChanged);
- isDirty = statusEntries.Any(entry => entry.State != FileStatus.Ignored);
+ statusEntries = new List();
+
+ using (GitStatusOptions coreOptions = CreateStatusOptions(options ?? new StatusOptions()))
+ {
+ 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 = (GitDiffDelta)Marshal.PtrToStructure(entry.HeadToIndexPtr, typeof(GitDiffDelta));
+ }
+ if (entry.IndexToWorkDirPtr != IntPtr.Zero)
+ {
+ deltaIndexToWorkDir = (GitDiffDelta)Marshal.PtrToStructure(entry.IndexToWorkDirPtr, typeof(GitDiffDelta));
+ }
+
+ AddStatusEntryForDelta(entry.Status, deltaHeadToIndex, deltaIndexToWorkDir);
+ }
+
+ isDirty = statusEntries.Any(entry => entry.State != FileStatus.Ignored);
+ }
+ }
+
+ 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;
+ }
+
+ return coreOptions;
}
- private StatusEntry StateChanged(IntPtr filePathPtr, uint state)
+ private void AddStatusEntryForDelta(FileStatus gitStatus, GitDiffDelta deltaHeadToIndex, GitDiffDelta deltaIndexToWorkDir)
{
- FilePath filePath = LaxFilePathMarshaler.FromNative(filePathPtr);
- var gitStatus = (FileStatus)state;
+ 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);
- foreach (KeyValuePair> kvp in dispatcher)
+ foreach (KeyValuePair> kvp in dispatcher)
{
if (!gitStatus.HasFlag(kvp.Key))
{
continue;
}
- kvp.Value(this, filePath.Native);
+ kvp.Value(this, statusEntry);
}
- return new StatusEntry(filePath.Native, gitStatus);
+ statusEntries.Add(statusEntry);
}
///
- /// Gets the for the specified relative path.
+ /// Gets the for the specified relative path.
///
- public virtual FileStatus this[string path]
+ public virtual StatusEntry this[string path]
{
get
{
@@ -87,10 +177,10 @@ public virtual FileStatus this[string path]
if (entries.Count == 0)
{
- return FileStatus.Nonexistent;
+ return new StatusEntry(path, FileStatus.Nonexistent);
}
- return entries.Single().State;
+ return entries.Single();
}
}
@@ -115,7 +205,7 @@ IEnumerator IEnumerable.GetEnumerator()
///
/// List of files added to the index, which are not in the current commit
///
- public virtual IEnumerable Added
+ public virtual IEnumerable Added
{
get { return added; }
}
@@ -123,7 +213,7 @@ public virtual IEnumerable Added
///
/// List of files added to the index, which are already in the current commit with different content
///
- public virtual IEnumerable Staged
+ public virtual IEnumerable Staged
{
get { return staged; }
}
@@ -131,7 +221,7 @@ public virtual IEnumerable Staged
///
/// List of files removed from the index but are existent in the current commit
///
- public virtual IEnumerable Removed
+ public virtual IEnumerable Removed
{
get { return removed; }
}
@@ -139,7 +229,7 @@ public virtual IEnumerable Removed
///
/// List of files existent in the index but are missing in the working directory
///
- public virtual IEnumerable Missing
+ public virtual IEnumerable Missing
{
get { return missing; }
}
@@ -147,7 +237,7 @@ public virtual IEnumerable 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
+ public virtual IEnumerable Modified
{
get { return modified; }
}
@@ -155,7 +245,7 @@ public virtual IEnumerable 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
+ public virtual IEnumerable Untracked
{
get { return untracked; }
}
@@ -163,11 +253,27 @@ public virtual IEnumerable Untracked
///
/// List of files existing in the working directory that are ignored.
///
- public virtual IEnumerable 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; }
+ }
+
///
/// True if the index or the working directory has been altered since the last commit. False otherwise.
///
diff --git a/LibGit2Sharp/StatusEntry.cs b/LibGit2Sharp/StatusEntry.cs
index a11d57be2..a049d3164 100644
--- a/LibGit2Sharp/StatusEntry.cs
+++ b/LibGit2Sharp/StatusEntry.cs
@@ -12,9 +12,11 @@ public class StatusEntry : IEquatable
{
private readonly string filePath;
private readonly FileStatus state;
+ private readonly RenameDetails headToIndexRenameDetails;
+ private readonly RenameDetails indexToWorkDirRenameDetails;
private static readonly LambdaEqualityHelper equalityHelper =
- new LambdaEqualityHelper(x => x.FilePath, x => x.State);
+ new LambdaEqualityHelper(x => x.FilePath, x => x.State, x => x.HeadToIndexRenameDetails, x => x.IndexToWorkDirRenameDetails);
///
/// Needed for mocking purposes.
@@ -22,10 +24,12 @@ public class StatusEntry : IEquatable
protected StatusEntry()
{ }
- internal StatusEntry(string filePath, FileStatus state)
+ internal StatusEntry(string filePath, FileStatus state, RenameDetails headToIndexRenameDetails = null, RenameDetails indexToWorkDirRenameDetails = null)
{
this.filePath = filePath;
this.state = state;
+ this.headToIndexRenameDetails = headToIndexRenameDetails;
+ this.indexToWorkDirRenameDetails = indexToWorkDirRenameDetails;
}
///
@@ -37,13 +41,29 @@ public virtual FileStatus State
}
///
- /// Gets the relative filepath to the working directory of the file.
+ /// Gets the relative new filepath to the working directory of the file.
///
public virtual string FilePath
{
get { return filePath; }
}
+ ///
+ /// Gets the rename details from the HEAD to the Index, if this contains
+ ///
+ public virtual RenameDetails HeadToIndexRenameDetails
+ {
+ get { return headToIndexRenameDetails; }
+ }
+
+ ///
+ /// Gets the rename details from the Index to the working directory, if this contains
+ ///
+ public virtual RenameDetails IndexToWorkDirRenameDetails
+ {
+ get { return indexToWorkDirRenameDetails; }
+ }
+
///
/// Determines whether the specified is equal to the current .
///
@@ -97,7 +117,19 @@ public override int GetHashCode()
private string DebuggerDisplay
{
- get { return string.Format("{0}: {1}", State, FilePath); }
+ get
+ {
+ if ((State & FileStatus.RenamedInIndex) == FileStatus.RenamedInIndex ||
+ (State & FileStatus.RenamedInWorkDir) == FileStatus.RenamedInWorkDir)
+ {
+ string oldFilePath = ((State & FileStatus.RenamedInIndex) == FileStatus.RenamedInIndex) ?
+ HeadToIndexRenameDetails.OldFilePath : IndexToWorkDirRenameDetails.OldFilePath;
+
+ return string.Format("{0}: {1} -> {2}", State, oldFilePath, FilePath);
+ }
+
+ return string.Format("{0}: {1}", State, FilePath);
+ }
}
}
}
diff --git a/LibGit2Sharp/StatusOptions.cs b/LibGit2Sharp/StatusOptions.cs
new file mode 100644
index 000000000..13963711a
--- /dev/null
+++ b/LibGit2Sharp/StatusOptions.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace LibGit2Sharp
+{
+ ///
+ /// Flags controlling what files are reported by status.
+ ///
+ public enum StatusShowOption
+ {
+ ///
+ /// Both the index and working directory are examined for changes
+ ///
+ IndexAndWorkDir = 0,
+
+ ///
+ /// Only the index is examined for changes
+ ///
+ IndexOnly = 1,
+
+ ///
+ /// Only the working directory is examined for changes
+ ///
+ WorkDirOnly = 2
+ }
+
+ ///
+ /// Options controlling the status behavior.
+ ///
+ public sealed class StatusOptions
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// By default, both the index and the working directory will be scanned
+ /// for status, and renames will be detected from changes staged in the
+ /// index only.
+ ///
+ public StatusOptions()
+ {
+ DetectRenamesInIndex = true;
+ }
+
+ ///
+ /// Which files should be scanned and returned
+ ///
+ public StatusShowOption Show { get; set; }
+
+ ///
+ /// Examine the staged changes for renames.
+ ///
+ public bool DetectRenamesInIndex { get; set; }
+
+ ///
+ /// Examine unstaged changes in the working directory for renames.
+ ///
+ public bool DetectRenamesInWorkDir { get; set; }
+
+ ///
+ /// Exclude submodules from being scanned for status
+ ///
+ public bool ExcludeSubmodules { get; set; }
+ }
+}