Skip to content

Commit bccacb9

Browse files
committed
Teach Checkout() to append to the reflog
When no user information exists in the configuration, a dummy signature is created to value the reflog entry.
1 parent 0efaf0d commit bccacb9

File tree

4 files changed

+200
-6
lines changed

4 files changed

+200
-6
lines changed

LibGit2Sharp.Tests/CheckoutFixture.cs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ public void CanCheckoutAnExistingBranch(string branchName)
4444

4545
// Working directory should not be dirty
4646
Assert.False(repo.Index.RetrieveStatus().IsDirty);
47+
48+
// Assert reflog entry is created
49+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
50+
Assert.Equal(master.Tip.Id, reflogEntry.From);
51+
Assert.Equal(branch.Tip.Id, reflogEntry.To);
52+
Assert.NotNull(reflogEntry.Commiter.Email);
53+
Assert.NotNull(reflogEntry.Commiter.Name);
54+
Assert.Equal(string.Format("checkout: moving from master to {0}", branchName), reflogEntry.Message);
4755
}
4856
}
4957

@@ -74,6 +82,14 @@ public void CanCheckoutAnExistingBranchByName(string branchName)
7482

7583
// Working directory should not be dirty
7684
Assert.False(repo.Index.RetrieveStatus().IsDirty);
85+
86+
// Assert reflog entry is created
87+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
88+
Assert.Equal(master.Tip.Id, reflogEntry.From);
89+
Assert.Equal(repo.Branches[branchName].Tip.Id, reflogEntry.To);
90+
Assert.NotNull(reflogEntry.Commiter.Email);
91+
Assert.NotNull(reflogEntry.Commiter.Name);
92+
Assert.Equal(string.Format("checkout: moving from master to {0}", branchName), reflogEntry.Message);
7793
}
7894
}
7995

@@ -111,6 +127,14 @@ public void CanCheckoutAnArbitraryCommit(string commitPointer)
111127
Assert.Equal("(no branch)", detachedHead.CanonicalName);
112128

113129
Assert.False(master.IsCurrentRepositoryHead);
130+
131+
// Assert reflog entry is created
132+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
133+
Assert.Equal(master.Tip.Id, reflogEntry.From);
134+
Assert.Equal(commitSha, reflogEntry.To.Sha);
135+
Assert.NotNull(reflogEntry.Commiter.Email);
136+
Assert.NotNull(reflogEntry.Commiter.Name);
137+
Assert.Equal(string.Format("checkout: moving from master to {0}", commitPointer), reflogEntry.Message);
114138
}
115139
}
116140

@@ -572,6 +596,14 @@ public void CheckingOutRemoteBranchResultsInDetachedHead()
572596
// Verify that HEAD is detached.
573597
Assert.Equal(repo.Refs["HEAD"].TargetIdentifier, repo.Branches["origin/master"].Tip.Sha);
574598
Assert.True(repo.Info.IsHeadDetached);
599+
600+
// Assert reflog entry is created
601+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
602+
Assert.Equal(master.Tip.Id, reflogEntry.From);
603+
Assert.Equal(repo.Branches["origin/master"].Tip.Id, reflogEntry.To);
604+
Assert.NotNull(reflogEntry.Commiter.Email);
605+
Assert.NotNull(reflogEntry.Commiter.Name);
606+
Assert.Equal("checkout: moving from master to origin/master", reflogEntry.Message);
575607
}
576608
}
577609

@@ -601,6 +633,136 @@ public void CheckingOutABranchDoesNotAlterBinaryFiles()
601633
}
602634
}
603635

636+
[Theory]
637+
[InlineData("a447ba2ca8")]
638+
[InlineData("refs/tags/lw")]
639+
[InlineData("e90810^{}")]
640+
public void CheckoutFromDetachedHead(string commitPointer)
641+
{
642+
string path = CloneStandardTestRepo();
643+
using (var repo = new Repository(path))
644+
{
645+
// Set the working directory to the current head
646+
ResetAndCleanWorkingDirectory(repo);
647+
Assert.False(repo.Index.RetrieveStatus().IsDirty);
648+
649+
var commitSha = repo.Lookup(commitPointer).Sha;
650+
651+
Branch initialHead = repo.Checkout("6dcf9bf");
652+
653+
repo.Checkout(commitPointer);
654+
655+
// Assert reflog entry is created
656+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
657+
Assert.Equal(initialHead.Tip.Id, reflogEntry.From);
658+
Assert.Equal(commitSha, reflogEntry.To.Sha);
659+
Assert.NotNull(reflogEntry.Commiter.Email);
660+
Assert.NotNull(reflogEntry.Commiter.Name);
661+
Assert.Equal(string.Format("checkout: moving from {0} to {1}", initialHead.Tip.Sha, commitPointer), reflogEntry.Message);
662+
}
663+
}
664+
665+
[Fact]
666+
public void CheckoutBranchFromDetachedHead()
667+
{
668+
string path = CloneStandardTestRepo();
669+
using (var repo = new Repository(path))
670+
{
671+
// Set the working directory to the current head
672+
ResetAndCleanWorkingDirectory(repo);
673+
Assert.False(repo.Index.RetrieveStatus().IsDirty);
674+
675+
Branch initialHead = repo.Checkout("6dcf9bf");
676+
677+
Assert.True(repo.Info.IsHeadDetached);
678+
679+
Branch newHead = repo.Checkout(repo.Branches["master"]);
680+
681+
// Assert reflog entry is created
682+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
683+
Assert.Equal(initialHead.Tip.Id, reflogEntry.From);
684+
Assert.Equal(newHead.Tip.Id, reflogEntry.To);
685+
Assert.NotNull(reflogEntry.Commiter.Email);
686+
Assert.NotNull(reflogEntry.Commiter.Name);
687+
Assert.Equal(string.Format("checkout: moving from {0} to {1}", initialHead.Tip.Sha, newHead.Name), reflogEntry.Message);
688+
}
689+
}
690+
691+
692+
[Fact(Skip = "Current libgit2 revparse implementation only returns the object being pointed at, not the reference pointing at it.")]
693+
public void CheckoutPreviousCheckedOutBranch()
694+
{
695+
string path = CloneStandardTestRepo();
696+
using (var repo = new Repository(path))
697+
{
698+
// Set the working directory to the current head
699+
ResetAndCleanWorkingDirectory(repo);
700+
Assert.False(repo.Index.RetrieveStatus().IsDirty);
701+
702+
Branch previousHead = repo.Checkout("i-do-numbers");
703+
Branch newHead = repo.Checkout("diff-test-cases");
704+
705+
//Go back to previous branch checked out
706+
repo.Checkout(@"@{-1}");
707+
708+
// Assert reflog entry is created
709+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
710+
Assert.Equal(newHead.Tip.Id, reflogEntry.From);
711+
Assert.Equal(previousHead.Tip.Id, reflogEntry.To);
712+
Assert.NotNull(reflogEntry.Commiter.Email);
713+
Assert.NotNull(reflogEntry.Commiter.Name);
714+
Assert.Equal("checkout: moving from diff-test-cases to i-do-numbers", reflogEntry.Message);
715+
}
716+
}
717+
718+
[Fact]
719+
public void CheckoutCurrentReference()
720+
{
721+
string path = CloneStandardTestRepo();
722+
using (var repo = new Repository(path))
723+
{
724+
Branch master = repo.Branches["master"];
725+
Assert.True(master.IsCurrentRepositoryHead);
726+
727+
ResetAndCleanWorkingDirectory(repo);
728+
Assert.False(repo.Index.RetrieveStatus().IsDirty);
729+
730+
var reflogEntriesCount = repo.Refs.Log(repo.Refs.Head).Count();
731+
732+
// Checkout branch
733+
repo.Checkout(master);
734+
735+
Assert.Equal(reflogEntriesCount, repo.Refs.Log(repo.Refs.Head).Count());
736+
737+
// Checkout in detached mode
738+
repo.Checkout(master.Tip.Sha);
739+
740+
Assert.True(repo.Info.IsHeadDetached);
741+
var reflogEntry = repo.Refs.Log(repo.Refs.Head).First();
742+
Assert.True(reflogEntry.To == reflogEntry.From);
743+
Assert.Equal(string.Format("checkout: moving from master to {0}", master.Tip.Sha), reflogEntry.Message);
744+
745+
// Checkout detached "HEAD" => nothing should happen
746+
reflogEntriesCount = repo.Refs.Log(repo.Refs.Head).Count();
747+
748+
repo.Checkout(repo.Head);
749+
750+
Assert.Equal(reflogEntriesCount, repo.Refs.Log(repo.Refs.Head).Count());
751+
752+
// Checkout attached "HEAD" => nothing should happen
753+
repo.Checkout("master");
754+
reflogEntriesCount = repo.Refs.Log(repo.Refs.Head).Count();
755+
756+
repo.Checkout(repo.Head);
757+
758+
Assert.Equal(reflogEntriesCount, repo.Refs.Log(repo.Refs.Head).Count());
759+
760+
repo.Checkout("HEAD");
761+
762+
Assert.Equal(reflogEntriesCount, repo.Refs.Log(repo.Refs.Head).Count());
763+
}
764+
}
765+
604766
[Fact]
605767
public void CheckoutLowerCasedHeadThrows()
606768
{

LibGit2Sharp/Configuration.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,17 +288,20 @@ private IEnumerable<ConfigurationEntry<string>> BuildConfigEntries()
288288
});
289289
}
290290

291-
internal Signature BuildSignatureFromGlobalConfiguration(DateTimeOffset now)
291+
internal Signature BuildSignatureFromGlobalConfiguration(DateTimeOffset now, bool shouldThrowIfNotFound)
292292
{
293293
var name = Get<string>("user.name");
294294
var email = Get<string>("user.email");
295295

296-
if ((name == null) || (email == null))
296+
if (shouldThrowIfNotFound && ((name == null) || (email == null)))
297297
{
298-
throw new LibGit2SharpException("Can not find Name and Email settings of the current user in Git configuration.");
298+
throw new LibGit2SharpException("Can not find Name or Email setting of the current user in Git configuration.");
299299
}
300300

301-
return new Signature(name.Value, email.Value, now);
301+
return new Signature(
302+
name != null ? name.Value : "unknown",
303+
email != null ? email.Value : string.Format("{0}@{1}", Environment.UserName, Environment.UserDomainName),
304+
now);
302305
}
303306
}
304307
}

LibGit2Sharp/Repository.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,11 +586,17 @@ public Branch Checkout(string committishOrBranchSpec, CheckoutOptions checkoutOp
586586
return Checkout(branch, checkoutOptions, onCheckoutProgress);
587587
}
588588

589+
var previousHeadName = Info.IsHeadDetached ? Head.Tip.Sha : Head.Name;
590+
589591
Commit commit = LookupCommit(committishOrBranchSpec);
590592
CheckoutTree(commit.Tree, checkoutOptions, onCheckoutProgress);
591593

592594
// Update HEAD.
593595
Refs.UpdateTarget("HEAD", commit.Id.Sha);
596+
if (committishOrBranchSpec != "HEAD")
597+
{
598+
LogCheckout(previousHeadName, commit.Id, committishOrBranchSpec);
599+
}
594600

595601
return Head;
596602
}
@@ -633,6 +639,9 @@ public Branch Checkout(Branch branch, CheckoutOptions checkoutOptions, CheckoutP
633639
"The tip of branch '{0}' is null. There's nothing to checkout.", branch.Name));
634640
}
635641

642+
var branchIsCurrentRepositoryHead = branch.IsCurrentRepositoryHead;
643+
var previousHeadName = Info.IsHeadDetached ? Head.Tip.Sha : Head.Name;
644+
636645
CheckoutTree(branch.Tip.Tree, checkoutOptions, onCheckoutProgress);
637646

638647
// Update HEAD.
@@ -647,9 +656,29 @@ public Branch Checkout(Branch branch, CheckoutOptions checkoutOptions, CheckoutP
647656
Refs.UpdateTarget("HEAD", branch.Tip.Id.Sha);
648657
}
649658

659+
if (!branchIsCurrentRepositoryHead)
660+
{
661+
LogCheckout(previousHeadName, branch);
662+
}
663+
650664
return Head;
651665
}
652666

667+
private void LogCheckout(string previousHeadName, Branch newHead)
668+
{
669+
LogCheckout(previousHeadName, newHead.Tip.Id, newHead.Name);
670+
}
671+
672+
private void LogCheckout(string previousHeadName, ObjectId newHeadTip, string newHeadSpec)
673+
{
674+
// Compute reflog message
675+
string reflogMessage = string.Format("checkout: moving from {0} to {1}", previousHeadName, newHeadSpec);
676+
677+
// Log checkout
678+
Signature author = Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now, false);
679+
Refs.Log(Refs.Head).Append(newHeadTip, author, reflogMessage);
680+
}
681+
653682
/// <summary>
654683
/// Internal implementation of Checkout that expects the ID of the checkout target
655684
/// to already be in the form of a canonical branch name or a commit ID.

LibGit2Sharp/RepositoryExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ private static Commit LookUpCommit(IRepository repository, string committish)
173173
/// <returns>The generated <see cref = "LibGit2Sharp.Commit" />.</returns>
174174
public static Commit Commit(this IRepository repository, string message, bool amendPreviousCommit = false)
175175
{
176-
Signature author = repository.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now);
176+
Signature author = repository.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now, true);
177177

178178
return repository.Commit(message, author, amendPreviousCommit);
179179
}
@@ -191,7 +191,7 @@ public static Commit Commit(this IRepository repository, string message, bool am
191191
/// <returns>The generated <see cref = "LibGit2Sharp.Commit" />.</returns>
192192
public static Commit Commit(this IRepository repository, string message, Signature author, bool amendPreviousCommit = false)
193193
{
194-
Signature committer = repository.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now);
194+
Signature committer = repository.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now, true);
195195

196196
return repository.Commit(message, author, committer, amendPreviousCommit);
197197
}

0 commit comments

Comments
 (0)