Skip to content

Commit 64d1929

Browse files
author
Edward Thomson
committed
Introduce renames in status
Renames in status require more detailed data than simply paths, in order to collect old path, new path and similarity data. Use git_status_list to collect this data and use a new StatusEntry to store it.
1 parent 439ee14 commit 64d1929

16 files changed

+625
-73
lines changed

LibGit2Sharp.Tests/IgnoreFixture.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ public void TemporaryRulesShouldApplyUntilCleared()
1616
{
1717
Touch(repo.Info.WorkingDirectory, "Foo.cs", "Bar");
1818

19-
Assert.True(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs"));
19+
Assert.True(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs"));
2020

2121
repo.Ignore.AddTemporaryRules(new[] { "*.cs" });
2222

23-
Assert.False(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs"));
23+
Assert.False(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs"));
2424

2525
repo.Ignore.ResetAllTemporaryRules();
2626

27-
Assert.True(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs"));
27+
Assert.True(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs"));
2828
}
2929
}
3030

LibGit2Sharp.Tests/ResetIndexFixture.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void CanResetTheIndexToTheContentOfACommitWithCommittishAsArgument()
9393
"deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt" };
9494

9595
Assert.Equal(expected.Length, newStatus.Where(IsStaged).Count());
96-
Assert.Equal(expected, newStatus.Removed);
96+
Assert.Equal(expected, newStatus.Removed.Select(s => s.FilePath));
9797
}
9898
}
9999

@@ -111,7 +111,7 @@ public void CanResetTheIndexToTheContentOfACommitWithCommitAsArgument()
111111
"deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt" };
112112

113113
Assert.Equal(expected.Length, newStatus.Where(IsStaged).Count());
114-
Assert.Equal(expected, newStatus.Removed);
114+
Assert.Equal(expected, newStatus.Removed.Select(s => s.FilePath));
115115
}
116116
}
117117

LibGit2Sharp.Tests/StatusFixture.cs

Lines changed: 119 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -75,37 +75,132 @@ public void CanRetrieveTheStatusOfTheWholeWorkingDirectory()
7575

7676
RepositoryStatus status = repo.Index.RetrieveStatus();
7777

78-
Assert.Equal(FileStatus.Staged, status[file]);
78+
Assert.Equal(FileStatus.Staged, status[file].State);
7979

8080
Assert.NotNull(status);
8181
Assert.Equal(6, status.Count());
8282
Assert.True(status.IsDirty);
8383

84-
Assert.Equal("new_untracked_file.txt", status.Untracked.Single());
85-
Assert.Equal("modified_unstaged_file.txt", status.Modified.Single());
86-
Assert.Equal("deleted_unstaged_file.txt", status.Missing.Single());
87-
Assert.Equal("new_tracked_file.txt", status.Added.Single());
88-
Assert.Equal(file, status.Staged.Single());
89-
Assert.Equal("deleted_staged_file.txt", status.Removed.Single());
84+
Assert.Equal("new_untracked_file.txt", status.Untracked.Select(s => s.FilePath).Single());
85+
Assert.Equal("modified_unstaged_file.txt", status.Modified.Select(s => s.FilePath).Single());
86+
Assert.Equal("deleted_unstaged_file.txt", status.Missing.Select(s => s.FilePath).Single());
87+
Assert.Equal("new_tracked_file.txt", status.Added.Select(s => s.FilePath).Single());
88+
Assert.Equal(file, status.Staged.Select(s => s.FilePath).Single());
89+
Assert.Equal("deleted_staged_file.txt", status.Removed.Select(s => s.FilePath).Single());
9090

9191
File.AppendAllText(Path.Combine(repo.Info.WorkingDirectory, file),
9292
"Tclem's favorite commit message: boom");
9393

9494
Assert.Equal(FileStatus.Staged | FileStatus.Modified, repo.Index.RetrieveStatus(file));
9595

9696
RepositoryStatus status2 = repo.Index.RetrieveStatus();
97-
Assert.Equal(FileStatus.Staged | FileStatus.Modified, status2[file]);
97+
Assert.Equal(FileStatus.Staged | FileStatus.Modified, status2[file].State);
9898

9999
Assert.NotNull(status2);
100100
Assert.Equal(6, status2.Count());
101101
Assert.True(status2.IsDirty);
102102

103-
Assert.Equal("new_untracked_file.txt", status2.Untracked.Single());
104-
Assert.Equal(new[] { file, "modified_unstaged_file.txt" }, status2.Modified);
105-
Assert.Equal("deleted_unstaged_file.txt", status2.Missing.Single());
106-
Assert.Equal("new_tracked_file.txt", status2.Added.Single());
107-
Assert.Equal(file, status2.Staged.Single());
108-
Assert.Equal("deleted_staged_file.txt", status2.Removed.Single());
103+
Assert.Equal("new_untracked_file.txt", status2.Untracked.Select(s => s.FilePath).Single());
104+
Assert.Equal(new[] { file, "modified_unstaged_file.txt" }, status2.Modified.Select(s => s.FilePath));
105+
Assert.Equal("deleted_unstaged_file.txt", status2.Missing.Select(s => s.FilePath).Single());
106+
Assert.Equal("new_tracked_file.txt", status2.Added.Select(s => s.FilePath).Single());
107+
Assert.Equal(file, status2.Staged.Select(s => s.FilePath).Single());
108+
Assert.Equal("deleted_staged_file.txt", status2.Removed.Select(s => s.FilePath).Single());
109+
}
110+
}
111+
112+
[Fact]
113+
public void CanRetrieveTheStatusOfRenamedFilesInWorkdir()
114+
{
115+
string path = CloneStandardTestRepo();
116+
using (var repo = new Repository(path))
117+
{
118+
Touch(repo.Info.WorkingDirectory, "old_name.txt",
119+
"This is a file with enough data to trigger similarity matching.\r\n" +
120+
"This is a file with enough data to trigger similarity matching.\r\n" +
121+
"This is a file with enough data to trigger similarity matching.\r\n" +
122+
"This is a file with enough data to trigger similarity matching.\r\n");
123+
124+
repo.Index.Stage("old_name.txt");
125+
126+
File.Move(Path.Combine(repo.Info.WorkingDirectory, "old_name.txt"),
127+
Path.Combine(repo.Info.WorkingDirectory, "rename_target.txt"));
128+
129+
RepositoryStatus status = repo.Index.RetrieveStatus(
130+
new StatusOptions()
131+
{
132+
DetectRenamesInIndex = true,
133+
DetectRenamesInWorkdir = true
134+
});
135+
136+
Assert.Equal(FileStatus.Added | FileStatus.RenamedInWorkdir, status["rename_target.txt"].State);
137+
Assert.Equal(100, status["rename_target.txt"].IndexToWorkdirRenameDetails.Similarity);
138+
}
139+
}
140+
141+
[Fact]
142+
public void CanRetrieveTheStatusOfRenamedFilesInIndex()
143+
{
144+
string path = CloneStandardTestRepo();
145+
using (var repo = new Repository(path))
146+
{
147+
File.Move(
148+
Path.Combine(repo.Info.WorkingDirectory, "1.txt"),
149+
Path.Combine(repo.Info.WorkingDirectory, "rename_target.txt"));
150+
151+
repo.Index.Stage("1.txt");
152+
repo.Index.Stage("rename_target.txt");
153+
154+
RepositoryStatus status = repo.Index.RetrieveStatus();
155+
156+
Assert.Equal(FileStatus.RenamedInIndex, status["rename_target.txt"].State);
157+
Assert.Equal(100, status["rename_target.txt"].HeadToIndexRenameDetails.Similarity);
158+
}
159+
}
160+
161+
[Fact]
162+
public void CanDetectedVariousKindsOfRenaming()
163+
{
164+
string path = InitNewRepository();
165+
using (var repo = new Repository(path))
166+
{
167+
Touch(repo.Info.WorkingDirectory, "file.txt",
168+
"This is a file with enough data to trigger similarity matching.\r\n" +
169+
"This is a file with enough data to trigger similarity matching.\r\n" +
170+
"This is a file with enough data to trigger similarity matching.\r\n" +
171+
"This is a file with enough data to trigger similarity matching.\r\n");
172+
173+
repo.Index.Stage("file.txt");
174+
repo.Commit("Initial commit", Constants.Signature, Constants.Signature);
175+
176+
File.Move(Path.Combine(repo.Info.WorkingDirectory, "file.txt"),
177+
Path.Combine(repo.Info.WorkingDirectory, "renamed.txt"));
178+
179+
var opts = new StatusOptions
180+
{
181+
DetectRenamesInIndex = true,
182+
DetectRenamesInWorkdir = true
183+
};
184+
185+
RepositoryStatus status = repo.Index.RetrieveStatus(opts);
186+
187+
// This passes as expected
188+
Assert.Equal(FileStatus.RenamedInWorkdir, status.Single().State);
189+
190+
repo.Index.Stage("file.txt");
191+
repo.Index.Stage("renamed.txt");
192+
193+
status = repo.Index.RetrieveStatus(opts);
194+
195+
Assert.Equal(FileStatus.RenamedInIndex, status.Single().State);
196+
197+
File.Move(Path.Combine(repo.Info.WorkingDirectory, "renamed.txt"),
198+
Path.Combine(repo.Info.WorkingDirectory, "renamed_again.txt"));
199+
200+
status = repo.Index.RetrieveStatus(opts);
201+
202+
Assert.Equal(FileStatus.RenamedInWorkdir | FileStatus.RenamedInIndex,
203+
status.Single().State);
109204
}
110205
}
111206

@@ -154,7 +249,7 @@ public void RetrievingTheStatusOfARepositoryReturnNativeFilePaths()
154249

155250
Assert.Equal(relFilePath, statusEntry.FilePath);
156251

157-
Assert.Equal(statusEntry.FilePath, repoStatus.Added.Single());
252+
Assert.Equal(statusEntry.FilePath, repoStatus.Added.Select(s => s.FilePath).Single());
158253
}
159254
}
160255

@@ -169,15 +264,15 @@ public void RetrievingTheStatusOfAnEmptyRepositoryHonorsTheGitIgnoreDirectives()
169264
Touch(repo.Info.WorkingDirectory, relativePath, "I'm going to be ignored!");
170265

171266
RepositoryStatus status = repo.Index.RetrieveStatus();
172-
Assert.Equal(new[] { relativePath }, status.Untracked);
267+
Assert.Equal(new[] { relativePath }, status.Untracked.Select(s => s.FilePath));
173268

174269
Touch(repo.Info.WorkingDirectory, ".gitignore", "*.txt" + Environment.NewLine);
175270

176271
RepositoryStatus newStatus = repo.Index.RetrieveStatus();
177-
Assert.Equal(".gitignore", newStatus.Untracked.Single());
272+
Assert.Equal(".gitignore", newStatus.Untracked.Select(s => s.FilePath).Single());
178273

179274
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath));
180-
Assert.Equal(new[] { relativePath }, newStatus.Ignored);
275+
Assert.Equal(new[] { relativePath }, newStatus.Ignored.Select(s => s.FilePath));
181276
}
182277
}
183278

@@ -223,7 +318,7 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives()
223318

224319
RepositoryStatus status = repo.Index.RetrieveStatus();
225320

226-
Assert.Equal(new[]{relativePath, "new_untracked_file.txt"}, status.Untracked);
321+
Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, status.Untracked.Select(s => s.FilePath));
227322

228323
Touch(repo.Info.WorkingDirectory, ".gitignore", "*.txt" + Environment.NewLine);
229324

@@ -263,10 +358,10 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives()
263358
*/
264359

265360
RepositoryStatus newStatus = repo.Index.RetrieveStatus();
266-
Assert.Equal(".gitignore", newStatus.Untracked.Single());
361+
Assert.Equal(".gitignore", newStatus.Untracked.Select(s => s.FilePath).Single());
267362

268363
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath));
269-
Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored);
364+
Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored.Select(s => s.FilePath));
270365
}
271366
}
272367

@@ -354,7 +449,7 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroug
354449
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus("bin/what-about-me.txt"));
355450

356451
RepositoryStatus newStatus = repo.Index.RetrieveStatus();
357-
Assert.Equal(new[] { "bin" + dirSep }, newStatus.Ignored);
452+
Assert.Equal(new[] { "bin" + dirSep }, newStatus.Ignored.Select(s => s.FilePath));
358453

359454
var sb = new StringBuilder();
360455
sb.AppendLine("bin/*");
@@ -366,8 +461,8 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroug
366461

367462
newStatus = repo.Index.RetrieveStatus();
368463

369-
Assert.Equal(new[] { "bin" + dirSep + "look-ma.txt" }, newStatus.Ignored);
370-
Assert.True(newStatus.Untracked.Contains("bin" + dirSep + "what-about-me.txt" ));
464+
Assert.Equal(new[] { "bin" + dirSep + "look-ma.txt" }, newStatus.Ignored.Select(s => s.FilePath));
465+
Assert.True(newStatus.Untracked.Select(s => s.FilePath).Contains("bin" + dirSep + "what-about-me.txt"));
371466
}
372467
}
373468
}

LibGit2Sharp/Core/GitStatusEntry.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.InteropServices;
5+
using System.Text;
6+
7+
namespace LibGit2Sharp.Core
8+
{
9+
/// <summary>
10+
/// A status entry from libgit2.
11+
/// </summary>
12+
[StructLayout(LayoutKind.Sequential)]
13+
internal class GitStatusEntry
14+
{
15+
/// <summary>
16+
/// Calculated status of a filepath in the working directory considering the current <see cref = "Repository.Index" /> and the <see cref="Repository.Head" />.
17+
/// </summary>
18+
public FileStatus Status;
19+
20+
/// <summary>
21+
/// The difference between the <see cref="Repository.Head" /> and <see cref = "Repository.Index" />.
22+
/// </summary>
23+
public IntPtr HeadToIndexPtr;
24+
25+
/// <summary>
26+
/// The difference between the <see cref = "Repository.Index" /> and the working directory.
27+
/// </summary>
28+
public IntPtr IndexToWorkdirPtr;
29+
}
30+
}

LibGit2Sharp/Core/GitStatusOptions.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.InteropServices;
5+
using System.Text;
6+
7+
namespace LibGit2Sharp.Core
8+
{
9+
[StructLayout(LayoutKind.Sequential)]
10+
internal class GitStatusOptions : IDisposable
11+
{
12+
public uint Version = 1;
13+
14+
public GitStatusShow Show;
15+
public GitStatusOptionFlags Flags;
16+
17+
GitStrArrayIn PathSpec;
18+
19+
public void Dispose()
20+
{
21+
if (PathSpec == null)
22+
{
23+
return;
24+
}
25+
26+
PathSpec.Dispose();
27+
}
28+
}
29+
30+
internal enum GitStatusShow
31+
{
32+
IndexAndWorkdir = 0,
33+
IndexOnly = 1,
34+
WorkdirOnly = 2,
35+
}
36+
37+
[Flags]
38+
internal enum GitStatusOptionFlags
39+
{
40+
IncludeUntracked = (1 << 0),
41+
IncludeIgnored = (1 << 1),
42+
IncludeUnmodified = (1 << 2),
43+
ExcludeSubmodules = (1 << 3),
44+
RecurseUntrackedDirs = (1 << 4),
45+
DisablePathspecMatch = (1 << 5),
46+
RecurseIgnoredDirs = (1 << 6),
47+
RenamesHeadToIndex = (1 << 7),
48+
RenamesIndexToWorkdir = (1 << 8),
49+
SortCaseSensitively = (1 << 9),
50+
SortCaseInsensitively = (1 << 10),
51+
RenamesFromRewrites = (1 << 11),
52+
}
53+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.InteropServices;
5+
using System.Text;
6+
7+
namespace LibGit2Sharp.Core.Handles
8+
{
9+
internal class StatusEntrySafeHandle : NotOwnedSafeHandleBase
10+
{
11+
public StatusEntrySafeHandle()
12+
: base()
13+
{
14+
}
15+
16+
public StatusEntrySafeHandle(IntPtr handle)
17+
: base()
18+
{
19+
this.SetHandle(handle);
20+
}
21+
22+
public GitStatusEntry MarshalAsGitStatusEntry()
23+
{
24+
return (GitStatusEntry)Marshal.PtrToStructure(handle, typeof(GitStatusEntry));
25+
}
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace LibGit2Sharp.Core.Handles
7+
{
8+
internal class StatusListSafeHandle : SafeHandleBase
9+
{
10+
protected override bool ReleaseHandleImpl()
11+
{
12+
Proxy.git_status_list_free(handle);
13+
return true;
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)