diff --git a/LibGit2Sharp.Tests/CommitFixture.cs b/LibGit2Sharp.Tests/CommitFixture.cs
index 4b4db8ec7..1d302023c 100644
--- a/LibGit2Sharp.Tests/CommitFixture.cs
+++ b/LibGit2Sharp.Tests/CommitFixture.cs
@@ -614,7 +614,8 @@ public void CanCommitALittleBit()
Assert.Null(repo.Head[relativeFilepath]);
var author = DummySignature;
- Commit commit = repo.Commit("Initial egotistic commit", author, author);
+ const string commitMessage = "Initial egotistic commit";
+ Commit commit = repo.Commit(commitMessage, author, author);
AssertBlobContent(repo.Head[relativeFilepath], "nulltoken\n");
AssertBlobContent(commit[relativeFilepath], "nulltoken\n");
@@ -622,6 +623,14 @@ public void CanCommitALittleBit()
Assert.Equal(0, commit.Parents.Count());
Assert.False(repo.Info.IsHeadOrphaned);
+ // Assert a reflog entry is created
+ Assert.Equal(1, repo.Refs.Log("HEAD").Count());
+ var reflogEntry = repo.Refs.Log("HEAD").First();
+ Assert.Equal(author, reflogEntry.Commiter);
+ Assert.Equal(commit.Id, reflogEntry.To);
+ Assert.Equal(ObjectId.Zero, reflogEntry.From);
+ Assert.Equal(string.Format("commit (initial): {0}", commitMessage), reflogEntry.Message);
+
File.WriteAllText(filePath, "nulltoken commits!\n");
repo.Index.Stage(relativeFilepath);
@@ -634,6 +643,10 @@ public void CanCommitALittleBit()
Assert.Equal(1, commit2.Parents.Count());
Assert.Equal(commit.Id, commit2.Parents.First().Id);
+ // Assert the reflog is shifted
+ Assert.Equal(2, repo.Refs.Log("HEAD").Count());
+ Assert.Equal(reflogEntry.To, repo.Refs.Log("HEAD").First().From);
+
Branch firstCommitBranch = repo.CreateBranch("davidfowl-rules", commit);
repo.Checkout(firstCommitBranch);
@@ -731,10 +744,17 @@ public void CanAmendACommitWithMoreThanOneParent()
repo.Reset(ResetOptions.Soft, mergedCommit.Sha);
CreateAndStageANewFile(repo);
+ const string commitMessage = "I'm rewriting the history!";
- Commit amendedCommit = repo.Commit("I'm rewriting the history!", DummySignature, DummySignature, true);
+ Commit amendedCommit = repo.Commit(commitMessage, DummySignature, DummySignature, true);
AssertCommitHasBeenAmended(repo, amendedCommit, mergedCommit);
+
+ // Assert a reflog entry is created
+ var reflogEntry = repo.Refs.Log("HEAD").First();
+ Assert.Equal(amendedCommit.Committer, reflogEntry.Commiter);
+ Assert.Equal(amendedCommit.Id, reflogEntry.To);
+ Assert.Equal(string.Format("commit (amend): {0}", commitMessage), reflogEntry.Message);
}
}
diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
index f2568f2cd..a6b9b20fc 100644
--- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
+++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
@@ -61,6 +61,7 @@
+
diff --git a/LibGit2Sharp.Tests/ReferenceFixture.cs b/LibGit2Sharp.Tests/ReferenceFixture.cs
index 38a0d9b54..860cec7ca 100644
--- a/LibGit2Sharp.Tests/ReferenceFixture.cs
+++ b/LibGit2Sharp.Tests/ReferenceFixture.cs
@@ -686,6 +686,21 @@ public void CanTellIfAReferenceIsValid(string refname, bool expectedResult)
}
}
+ [Fact]
+ public void CanUpdateTheTargetOfASymbolicReferenceWithAnotherSymbolicReference()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+
+ using (var repo = Repository.Init(scd.DirectoryPath))
+ {
+ Reference symbolicRef = repo.Refs.Add("refs/heads/unit_test", "refs/heads/master");
+
+ Reference newHead = repo.Refs.UpdateTarget(repo.Refs.Head, symbolicRef);
+ Assert.IsType(newHead);
+ Assert.Equal(symbolicRef.CanonicalName, newHead.TargetIdentifier);
+ }
+ }
+
private static T[] SortedRefs(IRepository repo, Func selector)
{
return repo.Refs.OrderBy(r => r.CanonicalName, StringComparer.Ordinal).Select(selector).ToArray();
diff --git a/LibGit2Sharp.Tests/ReflogFixture.cs b/LibGit2Sharp.Tests/ReflogFixture.cs
new file mode 100644
index 000000000..a5f287f4c
--- /dev/null
+++ b/LibGit2Sharp.Tests/ReflogFixture.cs
@@ -0,0 +1,106 @@
+using System.IO;
+using LibGit2Sharp.Tests.TestHelpers;
+using System.Linq;
+using Xunit;
+
+namespace LibGit2Sharp.Tests
+{
+ public class ReflogFixture : BaseFixture
+ {
+ [Fact]
+ public void CanReadReflog()
+ {
+ const int expectedReflogEntriesCount = 3;
+
+
+ using (var repo = new Repository(StandardTestRepoWorkingDirPath))
+ {
+ var reflog = repo.Refs.Log(repo.Refs.Head);
+
+ Assert.Equal(expectedReflogEntriesCount, reflog.Count());
+
+ // Initial commit assertions
+ Assert.Equal("timothy.clem@gmail.com", reflog.Last().Commiter.Email);
+ Assert.True(reflog.Last().Message.StartsWith("clone: from"));
+ Assert.Equal(ObjectId.Zero, reflog.Last().From);
+
+ // second commit assertions
+ Assert.Equal("4c062a6361ae6959e06292c1fa5e2822d9c96345", reflog.ElementAt(expectedReflogEntriesCount - 2).From.Sha);
+ Assert.Equal("592d3c869dbc4127fc57c189cb94f2794fa84e7e", reflog.ElementAt(expectedReflogEntriesCount - 2).To.Sha);
+ }
+ }
+
+ [Fact]
+ public void CannotReadReflogOnUnknownReference()
+ {
+ using (var repo = new Repository(StandardTestRepoWorkingDirPath))
+ {
+ Assert.Throws(() => repo.Refs.Log("toto").Count());
+ }
+ }
+
+ [Fact]
+ public void CommitShouldCreateReflogEntryOnHeadandOnTargetedDirectReference()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+
+ using (var repo = Repository.Init(scd.DirectoryPath))
+ {
+ // setup refs as HEAD => unit_test => master
+ var newRef = repo.Refs.Add("refs/heads/unit_test", "refs/heads/master");
+ Assert.NotNull(newRef);
+ repo.Refs.UpdateTarget(repo.Refs.Head, newRef);
+
+ const string relativeFilepath = "new.txt";
+ string filePath = Path.Combine(repo.Info.WorkingDirectory, relativeFilepath);
+
+ File.WriteAllText(filePath, "content\n");
+ repo.Index.Stage(relativeFilepath);
+
+ var author = DummySignature;
+ const string commitMessage = "Hope reflog behaves as it should";
+ Commit commit = repo.Commit(commitMessage, author, author);
+
+ // Assert a reflog entry is created on HEAD
+ Assert.Equal(1, repo.Refs.Log("HEAD").Count());
+ var reflogEntry = repo.Refs.Log("HEAD").First();
+ Assert.Equal(author, reflogEntry.Commiter);
+ Assert.Equal(commit.Id, reflogEntry.To);
+ Assert.Equal(ObjectId.Zero, reflogEntry.From);
+
+ // Assert the same reflog entry is created on refs/heads/master
+ Assert.Equal(1, repo.Refs.Log("refs/heads/master").Count());
+ reflogEntry = repo.Refs.Log("HEAD").First();
+ Assert.Equal(author, reflogEntry.Commiter);
+ Assert.Equal(commit.Id, reflogEntry.To);
+ Assert.Equal(ObjectId.Zero, reflogEntry.From);
+
+ // Assert no reflog entry is created on refs/heads/unit_test
+ Assert.Equal(0, repo.Refs.Log("refs/heads/unit_test").Count());
+ }
+ }
+
+ [Fact]
+ public void CommitOnUnbornReferenceShouldCreateReflogEntryWithInitialTag()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+
+ using (var repo = Repository.Init(scd.DirectoryPath))
+ {
+ const string relativeFilepath = "new.txt";
+ string filePath = Path.Combine(repo.Info.WorkingDirectory, relativeFilepath);
+
+ File.WriteAllText(filePath, "content\n");
+ repo.Index.Stage(relativeFilepath);
+
+ var author = DummySignature;
+ const string commitMessage = "First commit should be logged as initial";
+ repo.Commit(commitMessage, author, author);
+
+ // Assert the reflog entry message is correct
+ Assert.Equal(1, repo.Refs.Log("HEAD").Count());
+ Assert.Equal(string.Format("commit (initial): {0}", commitMessage), repo.Refs.Log("HEAD").First().Message);
+ }
+ }
+ }
+}
diff --git a/LibGit2Sharp/Core/Handles/ReflogEntrySafeHandle.cs b/LibGit2Sharp/Core/Handles/ReflogEntrySafeHandle.cs
new file mode 100644
index 000000000..1739ccac3
--- /dev/null
+++ b/LibGit2Sharp/Core/Handles/ReflogEntrySafeHandle.cs
@@ -0,0 +1,6 @@
+namespace LibGit2Sharp.Core.Handles
+{
+ internal class ReflogEntrySafeHandle : NotOwnedSafeHandleBase
+ {
+ }
+}
diff --git a/LibGit2Sharp/Core/Handles/ReflogSafeHandle.cs b/LibGit2Sharp/Core/Handles/ReflogSafeHandle.cs
new file mode 100644
index 000000000..a75deabea
--- /dev/null
+++ b/LibGit2Sharp/Core/Handles/ReflogSafeHandle.cs
@@ -0,0 +1,11 @@
+namespace LibGit2Sharp.Core.Handles
+{
+ internal class ReflogSafeHandle : SafeHandleBase
+ {
+ protected override bool ReleaseHandleImpl()
+ {
+ Proxy.git_reflog_free(handle);
+ return true;
+ }
+ }
+}
diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs
index d6ffd2891..227f22e4b 100644
--- a/LibGit2Sharp/Core/NativeMethods.cs
+++ b/LibGit2Sharp/Core/NativeMethods.cs
@@ -699,6 +699,50 @@ internal static extern int git_reference_symbolic_set_target(
[DllImport(libgit2)]
internal static extern GitReferenceType git_reference_type(ReferenceSafeHandle reference);
+ [DllImport(libgit2)]
+ internal static extern void git_reflog_free(
+ IntPtr reflog);
+
+ [DllImport(libgit2)]
+ internal static extern int git_reflog_read(
+ out ReflogSafeHandle ref_out,
+ ReferenceSafeHandle reference);
+
+ [DllImport(libgit2)]
+ internal static extern UIntPtr git_reflog_entrycount
+ (ReflogSafeHandle reflog);
+
+ [DllImport(libgit2)]
+ internal static extern ReflogEntrySafeHandle git_reflog_entry_byindex(
+ ReflogSafeHandle reflog,
+ UIntPtr idx);
+
+ [DllImport(libgit2)]
+ internal static extern OidSafeHandle git_reflog_entry_id_old(
+ SafeHandle entry);
+
+ [DllImport(libgit2)]
+ internal static extern OidSafeHandle git_reflog_entry_id_new(
+ SafeHandle entry);
+
+ [DllImport(libgit2)]
+ internal static extern IntPtr git_reflog_entry_committer(
+ SafeHandle entry);
+
+ [DllImport(libgit2)]
+ internal static extern int git_reflog_append(
+ ReflogSafeHandle reflog,
+ ref GitOid id,
+ SignatureSafeHandle committer,
+ [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8Marshaler))] string msg);
+
+ [DllImport(libgit2)]
+ internal static extern int git_reflog_write(ReflogSafeHandle reflog);
+
+ [DllImport(libgit2)]
+ [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))]
+ internal static extern string git_reflog_entry_message(SafeHandle entry);
+
[DllImport(libgit2)]
internal static extern int git_refspec_rtransform(
byte[] target,
diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs
index 68226c20f..c6c2975e1 100644
--- a/LibGit2Sharp/Core/Proxy.cs
+++ b/LibGit2Sharp/Core/Proxy.cs
@@ -1267,6 +1267,73 @@ public static GitReferenceType git_reference_type(ReferenceSafeHandle reference)
#endregion
+ #region git_reflog_
+
+ public static void git_reflog_free(IntPtr reflog)
+ {
+ NativeMethods.git_reflog_free(reflog);
+ }
+
+ public static ReflogSafeHandle git_reflog_read(ReferenceSafeHandle reference)
+ {
+ using (ThreadAffinity())
+ {
+ ReflogSafeHandle reflog_out;
+
+ int res = NativeMethods.git_reflog_read(out reflog_out, reference);
+ Ensure.ZeroResult(res);
+
+ return reflog_out;
+ }
+ }
+
+ public static int git_reflog_entrycount(ReflogSafeHandle reflog)
+ {
+ return (int)NativeMethods.git_reflog_entrycount(reflog);
+ }
+
+ public static ReflogEntrySafeHandle git_reflog_entry_byindex(ReflogSafeHandle reflog, int idx)
+ {
+ return NativeMethods.git_reflog_entry_byindex(reflog, (UIntPtr)idx);
+ }
+
+ public static ObjectId git_reflog_entry_id_old(SafeHandle entry)
+ {
+ return NativeMethods.git_reflog_entry_id_old(entry).MarshalAsObjectId();
+ }
+
+ public static ObjectId git_reflog_entry_id_new(SafeHandle entry)
+ {
+ return NativeMethods.git_reflog_entry_id_new(entry).MarshalAsObjectId();
+ }
+
+ public static Signature git_reflog_entry_committer(SafeHandle entry)
+ {
+ return new Signature(NativeMethods.git_reflog_entry_committer(entry));
+ }
+
+ public static string git_reflog_entry_message(SafeHandle entry)
+ {
+ return NativeMethods.git_reflog_entry_message(entry);
+ }
+
+ public static void git_reflog_append(ReflogSafeHandle reflog, ObjectId commit_id, Signature committer, string message)
+ {
+ using (ThreadAffinity())
+ using (SignatureSafeHandle committerHandle = committer.BuildHandle())
+ {
+ var oid = commit_id.Oid;
+
+ int res = NativeMethods.git_reflog_append(reflog, ref oid, committerHandle, message);
+ Ensure.ZeroResult(res);
+
+ res = NativeMethods.git_reflog_write(reflog);
+ Ensure.ZeroResult(res);
+ }
+ }
+
+ #endregion
+
#region git_refspec
public static string git_refspec_rtransform(GitRefSpecHandle refSpecPtr, string name)
diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj
index 28fee9413..b88853e12 100644
--- a/LibGit2Sharp/LibGit2Sharp.csproj
+++ b/LibGit2Sharp/LibGit2Sharp.csproj
@@ -71,10 +71,14 @@
+
+
+
+
diff --git a/LibGit2Sharp/ReferenceCollection.cs b/LibGit2Sharp/ReferenceCollection.cs
index 3c19e02b2..43a2e4b2c 100644
--- a/LibGit2Sharp/ReferenceCollection.cs
+++ b/LibGit2Sharp/ReferenceCollection.cs
@@ -191,6 +191,11 @@ private Reference UpdateTarget(Reference reference, T target, Func
+ /// Returns as a the reflog of the named
+ ///
+ /// The canonical name of the reference
+ /// a , enumerable of
+ public virtual ReflogCollection Log(string canonicalName)
+ {
+ Ensure.ArgumentNotNullOrEmptyString(canonicalName, "canonicalName");
+
+ return new ReflogCollection(repo, canonicalName);
+ }
+
+ ///
+ /// Returns as a the reflog of the
+ ///
+ /// The reference
+ /// a , enumerable of
+ public virtual ReflogCollection Log(Reference reference)
+ {
+ Ensure.ArgumentNotNull(reference, "reference");
+
+ return new ReflogCollection(repo, reference.CanonicalName);
+ }
}
}
diff --git a/LibGit2Sharp/ReflogCollection.cs b/LibGit2Sharp/ReflogCollection.cs
new file mode 100644
index 000000000..1cd50af1b
--- /dev/null
+++ b/LibGit2Sharp/ReflogCollection.cs
@@ -0,0 +1,107 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using LibGit2Sharp.Core;
+using LibGit2Sharp.Core.Handles;
+
+namespace LibGit2Sharp
+{
+ ///
+ /// The is the reflog of a given , as a enumerable of .
+ /// Reflog is a mechanism to record when the tip of a is updated.
+ ///
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ public class ReflogCollection : IEnumerable
+ {
+ internal readonly Repository repo;
+
+ private readonly string canonicalName;
+
+ ///
+ /// Needed for mocking purposes.
+ ///
+ protected ReflogCollection()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The repo.
+ /// the canonical name of the to retrieve reflog entries on.
+ internal ReflogCollection(Repository repo, string canonicalName)
+ {
+ Ensure.ArgumentNotNullOrEmptyString(canonicalName, "canonicalName");
+ Ensure.ArgumentNotNull(repo, "repo");
+
+ this.repo = repo;
+ this.canonicalName = canonicalName;
+ }
+
+ #region Implementation of IEnumerable
+
+ ///
+ /// Returns an enumerator that iterates through the collection.
+ ///
+ /// The enumerator returns the by descending order (last reflog entry is returned first).
+ ///
+ ///
+ /// An object that can be used to iterate through the collection.
+ public IEnumerator GetEnumerator()
+ {
+ var entries = new List();
+
+ using (ReferenceSafeHandle reference = Proxy.git_reference_lookup(repo.Handle, canonicalName, true))
+ using (ReflogSafeHandle reflog = Proxy.git_reflog_read(reference))
+ {
+ var entriesCount = Proxy.git_reflog_entrycount(reflog);
+
+ for (int i = 0; i < entriesCount; i++)
+ {
+ ReflogEntrySafeHandle handle = Proxy.git_reflog_entry_byindex(reflog, i);
+ entries.Add(new ReflogEntry(handle));
+ }
+ }
+
+ return entries.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
+
+ private string DebuggerDisplay
+ {
+ get
+ {
+ return string.Format(CultureInfo.InvariantCulture,
+ "Count = {0}", this.Count());
+ }
+ }
+
+ ///
+ /// Add a new to the current . It will be created as first item of the collection
+ /// The native reflog object will be saved right after inserting the entry.
+ ///
+ /// the of the new commit the will point out.
+ /// of the author of the new commit.
+ /// the message associated with the new .
+ internal virtual void Append(ObjectId objectId, Signature committer, string message)
+ {
+ using (ReferenceSafeHandle reference = Proxy.git_reference_lookup(repo.Handle, canonicalName, true))
+ using (ReflogSafeHandle reflog = Proxy.git_reflog_read(reference))
+ {
+ string prettifiedMessage = Proxy.git_message_prettify(message);
+ Proxy.git_reflog_append(reflog, objectId, committer, prettifiedMessage);
+ }
+ }
+ }
+}
diff --git a/LibGit2Sharp/ReflogEntry.cs b/LibGit2Sharp/ReflogEntry.cs
new file mode 100644
index 000000000..d9f53170f
--- /dev/null
+++ b/LibGit2Sharp/ReflogEntry.cs
@@ -0,0 +1,67 @@
+using System.Runtime.InteropServices;
+using LibGit2Sharp.Core;
+
+namespace LibGit2Sharp
+{
+ ///
+ /// As single entry of a
+ /// a describes one single update on a particular reference
+ ///
+ public class ReflogEntry
+ {
+ private readonly ObjectId _from;
+ private readonly ObjectId _to;
+ private readonly Signature _commiter;
+ private readonly string message;
+
+ ///
+ /// Needed for mocking purposes.
+ ///
+ protected ReflogEntry()
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// a to the reflog entry
+ public ReflogEntry(SafeHandle entryHandle)
+ {
+ _from = Proxy.git_reflog_entry_id_old(entryHandle);
+ _to = Proxy.git_reflog_entry_id_new(entryHandle);
+ _commiter = Proxy.git_reflog_entry_committer(entryHandle);
+ message = Proxy.git_reflog_entry_message(entryHandle);
+ }
+
+ ///
+ /// targeted before the reference update described by this
+ ///
+ public virtual ObjectId From
+ {
+ get { return _from; }
+ }
+
+ ///
+ /// targeted after the reference update described by this
+ ///
+ public virtual ObjectId To
+ {
+ get { return _to; }
+ }
+
+ ///
+ /// of the commiter of this reference update
+ ///
+ public virtual Signature Commiter
+ {
+ get { return _commiter; }
+ }
+
+ ///
+ /// the message assiocated to this reference update
+ ///
+ public virtual string Message
+ {
+ get { return message; }
+ }
+ }
+}
diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs
index 32768a470..b6532070d 100644
--- a/LibGit2Sharp/Repository.cs
+++ b/LibGit2Sharp/Repository.cs
@@ -684,9 +684,39 @@ public Commit Commit(string message, Signature author, Signature committer, bool
Proxy.git_repository_merge_cleanup(handle);
+ // Insert reflog entry
+ LogCommit(result, amendPreviousCommit, parents.Count() == 0);
+
return result;
}
+ private void LogCommit(Commit commit, bool amendPreviousCommit, bool isInitialCommit)
+ {
+ // Compute reflog message
+ string reflogMessage = "commit";
+ if (isInitialCommit)
+ {
+ reflogMessage += " (initial)";
+ }
+ else if(amendPreviousCommit)
+ {
+ reflogMessage += " (amend)";
+ }
+
+ reflogMessage = string.Format("{0}: {1}", reflogMessage, commit.Message);
+
+ var headRef = Refs.Head;
+
+ // in case HEAD targets a symbolic reference, log commit on the targeted direct reference
+ if(headRef is SymbolicReference)
+ {
+ Refs.Log(headRef.ResolveToDirectReference()).Append(commit.Id, commit.Committer, reflogMessage);
+ }
+
+ // Log commit on HEAD
+ Refs.Log(headRef).Append(commit.Id, commit.Committer, reflogMessage);
+ }
+
private IEnumerable RetrieveParentsOfTheCommitBeingCreated(bool amendPreviousCommit)
{
if (amendPreviousCommit)