Skip to content

Commit 8a303f2

Browse files
author
Edward Thomson
committed
Introduce EmptyCommitException
By default, we now throw an exception when trying to commit an "empty commit" - that is, one that does not change any items. This check only applies when doing a normal commit, or an amend of a normal commit; it does not apply when performing a merge or an amend of a merge commit. This check can optionally be overridden.
1 parent aa94e9d commit 8a303f2

File tree

8 files changed

+192
-4
lines changed

8 files changed

+192
-4
lines changed

LibGit2Sharp.Tests/BranchFixture.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ public void CanListBranchesWithRemoteAndLocalBranchWithSameShortName()
300300

301301
var expectedWdBranches = new[]
302302
{
303-
"diff-test-cases", "i-do-numbers", "logo", "master", "origin/master", "track-local",
303+
"diff-test-cases", "i-do-numbers", "logo", "master", "origin/master", "track-local", "treesame_as_32eab"
304304
};
305305

306306
Assert.Equal(expectedWdBranches,
@@ -315,7 +315,7 @@ public void CanListAllBranchesWhenGivenWorkingDir()
315315
{
316316
var expectedWdBranches = new[]
317317
{
318-
"diff-test-cases", "i-do-numbers", "logo", "master", "track-local",
318+
"diff-test-cases", "i-do-numbers", "logo", "master", "track-local", "treesame_as_32eab",
319319
"origin/HEAD", "origin/br2", "origin/master", "origin/packed-test",
320320
"origin/test"
321321
};
@@ -336,6 +336,7 @@ public void CanListAllBranchesIncludingRemoteRefs()
336336
new { Name = "logo", Sha = "a447ba2ca8fffd46dece72f7db6faf324afb8fcd", IsRemote = false },
337337
new { Name = "master", Sha = "32eab9cb1f450b5fe7ab663462b77d7f4b703344", IsRemote = false },
338338
new { Name = "track-local", Sha = "580c2111be43802dab11328176d94c391f1deae9", IsRemote = false },
339+
new { Name = "treesame_as_32eab", Sha = "f705abffe7015f2beacf2abe7a36583ebee3487e", IsRemote = false },
339340
new { Name = "origin/HEAD", Sha = "580c2111be43802dab11328176d94c391f1deae9", IsRemote = true },
340341
new { Name = "origin/br2", Sha = "a4a7dce85cf63874e984719f4fdd239f5145052f", IsRemote = true },
341342
new { Name = "origin/master", Sha = "580c2111be43802dab11328176d94c391f1deae9", IsRemote = true },

LibGit2Sharp.Tests/CommitFixture.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,5 +885,124 @@ public void CanCommitOnOrphanedBranch()
885885
Assert.Equal(1, repo.Head.Commits.Count());
886886
}
887887
}
888+
889+
[Fact]
890+
public void CanNotCommitAnEmptyCommit()
891+
{
892+
string path = CloneStandardTestRepo();
893+
using (var repo = new Repository(path))
894+
{
895+
repo.Reset(ResetMode.Hard);
896+
repo.RemoveUntrackedFiles();
897+
898+
Assert.Throws<EmptyCommitException>(() => repo.Commit("Empty commit!", Constants.Signature, Constants.Signature));
899+
}
900+
}
901+
902+
[Fact]
903+
public void CanCommitAnEmptyCommitWhenForced()
904+
{
905+
string path = CloneStandardTestRepo();
906+
using (var repo = new Repository(path))
907+
{
908+
repo.Reset(ResetMode.Hard);
909+
repo.RemoveUntrackedFiles();
910+
911+
repo.Commit("Empty commit!", Constants.Signature, Constants.Signature, false, true);
912+
}
913+
}
914+
915+
[Fact]
916+
public void CanNotAmendAnEmptyCommit()
917+
{
918+
string path = CloneStandardTestRepo();
919+
using (var repo = new Repository(path))
920+
{
921+
repo.Reset(ResetMode.Hard);
922+
repo.RemoveUntrackedFiles();
923+
924+
repo.Commit("Empty commit!", Constants.Signature, Constants.Signature, false, true);
925+
926+
Assert.Throws<EmptyCommitException>(() => repo.Commit("Empty commit!", Constants.Signature, Constants.Signature, true, false));
927+
}
928+
}
929+
930+
[Fact]
931+
public void CanAmendAnEmptyCommitWhenForced()
932+
{
933+
string path = CloneStandardTestRepo();
934+
using (var repo = new Repository(path))
935+
{
936+
repo.Reset(ResetMode.Hard);
937+
repo.RemoveUntrackedFiles();
938+
939+
Commit emptyCommit = repo.Commit("Empty commit!", Constants.Signature, Constants.Signature, false, true);
940+
941+
Commit amendedCommit = repo.Commit("I'm rewriting the history!", Constants.Signature, Constants.Signature, true, true);
942+
AssertCommitHasBeenAmended(repo, amendedCommit, emptyCommit);
943+
}
944+
}
945+
946+
[Fact]
947+
public void CanCommitAnEmptyCommitWhenMerging()
948+
{
949+
string path = CloneStandardTestRepo();
950+
using (var repo = new Repository(path))
951+
{
952+
repo.Reset(ResetMode.Hard);
953+
repo.RemoveUntrackedFiles();
954+
955+
Touch(repo.Info.Path, "MERGE_HEAD", "f705abffe7015f2beacf2abe7a36583ebee3487e\n");
956+
957+
Assert.Equal(CurrentOperation.Merge, repo.Info.CurrentOperation);
958+
959+
Commit newMergedCommit = repo.Commit("Merge commit", Constants.Signature, Constants.Signature);
960+
961+
Assert.Equal(2, newMergedCommit.Parents.Count());
962+
Assert.Equal(newMergedCommit.Parents.First().Sha, "32eab9cb1f450b5fe7ab663462b77d7f4b703344");
963+
Assert.Equal(newMergedCommit.Parents.Skip(1).First().Sha, "f705abffe7015f2beacf2abe7a36583ebee3487e");
964+
}
965+
}
966+
967+
[Fact]
968+
public void CanAmendAnEmptyMergeCommit()
969+
{
970+
string path = CloneStandardTestRepo();
971+
using (var repo = new Repository(path))
972+
{
973+
repo.Reset(ResetMode.Hard);
974+
repo.RemoveUntrackedFiles();
975+
976+
Touch(repo.Info.Path, "MERGE_HEAD", "f705abffe7015f2beacf2abe7a36583ebee3487e\n");
977+
Commit newMergedCommit = repo.Commit("Merge commit", Constants.Signature, Constants.Signature);
978+
979+
Commit amendedCommit = repo.Commit("I'm rewriting the history!", Constants.Signature, Constants.Signature, true);
980+
AssertCommitHasBeenAmended(repo, amendedCommit, newMergedCommit);
981+
}
982+
}
983+
984+
[Fact]
985+
public void CanNotAmendACommitInAWayThatWouldLeadTheNewCommitToBecomeEmpty()
986+
{
987+
string repoPath = InitNewRepository();
988+
989+
using (var repo = new Repository(repoPath))
990+
{
991+
Touch(repo.Info.WorkingDirectory, "test.txt", "test\n");
992+
repo.Index.Stage("test.txt");
993+
994+
repo.Commit("Initial commit", Constants.Signature, Constants.Signature);
995+
996+
Touch(repo.Info.WorkingDirectory, "new.txt", "content\n");
997+
repo.Index.Stage("new.txt");
998+
999+
repo.Commit("One commit", Constants.Signature, Constants.Signature);
1000+
1001+
repo.Index.Remove("new.txt");
1002+
1003+
Assert.Throws<EmptyCommitException>(() => repo.Commit("Oops", Constants.Signature, Constants.Signature,
1004+
new CommitOptions { AmendPreviousCommit = true }));
1005+
}
1006+
}
8881007
}
8891008
}

LibGit2Sharp.Tests/Resources/testrepo_wd/dot_git/objects/f7/05abffe7015f2beacf2abe7a36583ebee3487e

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
x���B!D=Sh؅�Bb�;�X�G��c�|�/��K�d��z-�FѲDXy) Y�1��������X4��z�.�����r�dv�4Mb�st+Ҍ���������S/�zkuk}�I�\�����q�VOl�m ����Q�ΣC��Pp���1�J�
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
f705abffe7015f2beacf2abe7a36583ebee3487e

LibGit2Sharp/EmptyCommitException.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
namespace LibGit2Sharp
5+
{
6+
/// <summary>
7+
/// The exception that is thrown when a commit would create an "empty"
8+
/// commit that is treesame to its parent without an explicit override.
9+
/// </summary>
10+
[Serializable]
11+
public class EmptyCommitException : LibGit2SharpException
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="EmptyCommitException"/> class.
15+
/// </summary>
16+
public EmptyCommitException()
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="EmptyCommitException"/> class with a specified error message.
22+
/// </summary>
23+
/// <param name="message">A message that describes the error.</param>
24+
public EmptyCommitException(string message)
25+
: base(message)
26+
{
27+
}
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="EmptyCommitException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
31+
/// </summary>
32+
/// <param name="message">The error message that explains the reason for the exception.</param>
33+
/// <param name="innerException">The exception that is the cause of the current exception. If the <paramref name="innerException"/> parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
34+
public EmptyCommitException(string message, Exception innerException)
35+
: base(message, innerException)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="EmptyCommitException"/> class with a serialized data.
41+
/// </summary>
42+
/// <param name="info">The <see cref="SerializationInfo "/> that holds the serialized object data about the exception being thrown.</param>
43+
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
44+
protected EmptyCommitException(SerializationInfo info, StreamingContext context)
45+
: base(info, context)
46+
{
47+
}
48+
}
49+
}

LibGit2Sharp/IRepository.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,9 @@ public interface IRepository : IDisposable
164164
/// <param name="author">The <see cref="Signature"/> of who made the change.</param>
165165
/// <param name="committer">The <see cref="Signature"/> of who added the change to the repository.</param>
166166
/// <param name="amendPreviousCommit">True to amend the current <see cref="Commit"/> pointed at by <see cref="Repository.Head"/>, false otherwise.</param>
167+
/// <param name="allowEmptyCommit">True to allow creation of an empty <see cref="Commit"/>, false otherwise.</param>
167168
/// <returns>The generated <see cref="Commit"/>.</returns>
168-
Commit Commit(string message, Signature author, Signature committer, bool amendPreviousCommit = false);
169+
Commit Commit(string message, Signature author, Signature committer, bool amendPreviousCommit = false, bool allowEmptyCommit = false);
169170

170171
/// <summary>
171172
/// Sets the current <see cref="Head"/> to the specified commit and optionally resets the <see cref="Index"/> and

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
<Compile Include="Core\Handles\PatchSafeHandle.cs" />
8282
<Compile Include="Core\IntPtrExtensions.cs" />
8383
<Compile Include="DefaultCredentials.cs" />
84+
<Compile Include="EmptyCommitException.cs" />
8485
<Compile Include="FetchOptions.cs" />
8586
<Compile Include="MergeResult.cs" />
8687
<Compile Include="PatchStats.cs" />

LibGit2Sharp/Repository.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,8 +894,9 @@ public void Reset(Commit commit, IEnumerable<string> paths = null, ExplicitPaths
894894
/// <param name="author">The <see cref="Signature"/> of who made the change.</param>
895895
/// <param name="committer">The <see cref="Signature"/> of who added the change to the repository.</param>
896896
/// <param name="amendPreviousCommit">True to amend the current <see cref="Commit"/> pointed at by <see cref="Repository.Head"/>, false otherwise.</param>
897+
/// <param name="allowEmptyCommit">True to allow creation of an empty <see cref="Commit"/>, false otherwise.</param>
897898
/// <returns>The generated <see cref="Commit"/>.</returns>
898-
public Commit Commit(string message, Signature author, Signature committer, bool amendPreviousCommit = false)
899+
public Commit Commit(string message, Signature author, Signature committer, bool amendPreviousCommit = false, bool allowEmptyCommit = false)
899900
{
900901
bool isHeadOrphaned = Info.IsHeadUnborn;
901902

@@ -909,6 +910,20 @@ public Commit Commit(string message, Signature author, Signature committer, bool
909910

910911
var parents = RetrieveParentsOfTheCommitBeingCreated(amendPreviousCommit).ToList();
911912

913+
if (parents.Count == 1 && !allowEmptyCommit)
914+
{
915+
var treesame = parents[0].Tree.Id.Equals(treeId);
916+
var amendMergeCommit = amendPreviousCommit && !isHeadOrphaned && Head.Tip.Parents.Count() > 1;
917+
918+
if (treesame && !amendMergeCommit)
919+
{
920+
throw new EmptyCommitException(
921+
options.AmendPreviousCommit ?
922+
String.Format("Amending this commit would produce a commit that is identical to its parent (id = {0})", parents[0].Id) :
923+
"No changes; nothing to commit.");
924+
}
925+
}
926+
912927
Commit result = ObjectDatabase.CreateCommit(author, committer, message, true, tree, parents);
913928

914929
Proxy.git_repository_state_cleanup(handle);

0 commit comments

Comments
 (0)