Skip to content

Commit

Permalink
Reflog support (libgit2#467)
Browse files Browse the repository at this point in the history
  • Loading branch information
pokstad committed Jan 18, 2021
1 parent 4b2ac7c commit ce4dd16
Show file tree
Hide file tree
Showing 2 changed files with 337 additions and 0 deletions.
175 changes: 175 additions & 0 deletions reflog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package git

/*
#include <git2.h>
*/
import "C"
import (
"runtime"
"unsafe"
)

// Reflog is a log of changes for a reference
type Reflog struct {
ptr *C.git_reflog
repo *Repository
name string
}

func newRefLogFromC(ptr *C.git_reflog, repo *Repository, name string) *Reflog {
l := &Reflog{
ptr: ptr,
repo: repo,
name: name,
}
runtime.SetFinalizer(l, (*Reflog).Free)
return l
}

func (repo *Repository) ReadReflog(name string) (*Reflog, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))

var ptr *C.git_reflog

ecode := C.git_reflog_read(&ptr, repo.ptr, cname)
runtime.KeepAlive(repo)
if ecode < 0 {
return nil, MakeGitError(ecode)
}

return newRefLogFromC(ptr, repo, name), nil
}

func (repo *Repository) DeleteReflog(name string) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))

ecode := C.git_reflog_delete(repo.ptr, cname)
runtime.KeepAlive(repo)
if ecode < 0 {
return MakeGitError(ecode)
}

return nil
}

func (repo *Repository) RenameReflog(oldName, newName string) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

cOldName := C.CString(oldName)
defer C.free(unsafe.Pointer(cOldName))

cNewName := C.CString(newName)
defer C.free(unsafe.Pointer(cNewName))

ecode := C.git_reflog_rename(repo.ptr, cOldName, cNewName)
runtime.KeepAlive(repo)
if ecode < 0 {
return MakeGitError(ecode)
}

return nil
}

func (l *Reflog) Write() error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

ecode := C.git_reflog_write(l.ptr)
runtime.KeepAlive(l)
if ecode < 0 {
return MakeGitError(ecode)
}
return nil
}

func (l *Reflog) EntryCount() uint {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

count := C.git_reflog_entrycount(l.ptr)
runtime.KeepAlive(l)
return uint(count)
}

// ReflogEntry specifies a reference change
type ReflogEntry struct {
Old *Oid
New *Oid
Committer *Signature
Message string // may be empty
}

func newReflogEntry(entry *C.git_reflog_entry) *ReflogEntry {
return &ReflogEntry{
New: newOidFromC(C.git_reflog_entry_id_new(entry)),
Old: newOidFromC(C.git_reflog_entry_id_old(entry)),
Committer: newSignatureFromC(C.git_reflog_entry_committer(entry)),
Message: C.GoString(C.git_reflog_entry_message(entry)),
}
}

func (l *Reflog) EntryByIndex(index uint) *ReflogEntry {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

entry := C.git_reflog_entry_byindex(l.ptr, C.size_t(index))
if entry == nil {
return nil
}

goEntry := newReflogEntry(entry)
runtime.KeepAlive(l)

return goEntry
}

func (l *Reflog) DropEntry(index uint, rewriteHistory bool) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

var rewriteHistoryInt int
if rewriteHistory {
rewriteHistoryInt = 1
}

ecode := C.git_reflog_drop(l.ptr, C.size_t(index), C.int(rewriteHistoryInt))
runtime.KeepAlive(l)
if ecode < 0 {
return MakeGitError(ecode)
}

return nil
}

func (l *Reflog) AppendEntry(oid *Oid, committer *Signature, message string) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

cSignature, err := committer.toC()
if err != nil {
return err
}
defer C.git_signature_free(cSignature)

cMsg := C.CString(message)
defer C.free(unsafe.Pointer(cMsg))

C.git_reflog_append(l.ptr, oid.toC(), cSignature, cMsg)
runtime.KeepAlive(l)

return nil
}

func (l *Reflog) Free() {
runtime.SetFinalizer(l, nil)
C.git_reflog_free(l.ptr)
}
162 changes: 162 additions & 0 deletions reflog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package git

import (
"fmt"
"reflect"
"testing"
"time"
)

func allReflogEntries(t *testing.T, repo *Repository, refName string) (entries []*ReflogEntry) {
rl, err := repo.ReadReflog(refName)
checkFatal(t, err)
defer rl.Free()

for i := uint(0); i < rl.EntryCount(); i++ {
entries = append(entries, rl.EntryByIndex(i))
}
return entries
}

// assertEntriesEqual will assert that the reflogs match with the exception of
// the signature time (it is not reliably deterministic to predict the
// signature time during many reference updates)
func assertEntriesEqual(t *testing.T, got, want []*ReflogEntry) {
if len(got) != len(want) {
t.Fatalf("got %d length, wanted %d length", len(got), len(want))
}

for i := 0; i < len(got); i++ {
gi := got[i]
wi := want[i]
// remove the signature time to make the results deterministic
gi.Committer.When = time.Time{}
wi.Committer.When = time.Time{}
// check committer separately to print results clearly
if !reflect.DeepEqual(gi.Committer, wi.Committer) {
t.Fatalf("got committer %v, want committer %v",
gi.Committer, wi.Committer)
}
if !reflect.DeepEqual(gi, wi) {
t.Fatalf("got %v, want %v", gi, wi)
}
}
}

func TestReflog(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)

commitID, treeId := seedTestRepo(t, repo)

testRefName := "refs/heads/test"

// configure committer for deterministic reflog entries
cfg, err := repo.Config()
checkFatal(t, err)

sig := &Signature{
Name: "Rand Om Hacker",
Email: "[email protected]",
}

checkFatal(t, cfg.SetString("user.name", sig.Name))
checkFatal(t, cfg.SetString("user.email", sig.Email))

checkFatal(t, repo.References.EnsureLog(testRefName))
_, err = repo.References.Create(testRefName, commitID, true, "first update")
checkFatal(t, err)
got := allReflogEntries(t, repo, testRefName)
want := []*ReflogEntry{
&ReflogEntry{
New: commitID,
Old: &Oid{},
Committer: sig,
Message: "first update",
},
}

// create additional commits and verify they are added to reflog
tree, err := repo.LookupTree(treeId)
checkFatal(t, err)
for i := 0; i < 10; i++ {
nextEntry := &ReflogEntry{
Old: commitID,
Committer: sig,
Message: fmt.Sprintf("commit: %d", i),
}

commit, err := repo.LookupCommit(commitID)
checkFatal(t, err)

commitID, err = repo.CreateCommit(testRefName, sig, sig, fmt.Sprint(i), tree, commit)
checkFatal(t, err)

nextEntry.New = commitID

want = append([]*ReflogEntry{nextEntry}, want...)
}

t.Run("ReadReflog", func(t *testing.T) {
got = allReflogEntries(t, repo, testRefName)
assertEntriesEqual(t, got, want)
})

t.Run("DropEntry", func(t *testing.T) {
rl, err := repo.ReadReflog(testRefName)
checkFatal(t, err)
defer rl.Free()

gotBefore := allReflogEntries(t, repo, testRefName)

checkFatal(t, rl.DropEntry(0, false))
checkFatal(t, rl.Write())

gotAfter := allReflogEntries(t, repo, testRefName)

assertEntriesEqual(t, gotAfter, gotBefore[1:])
})

t.Run("AppendEntry", func(t *testing.T) {
logs := allReflogEntries(t, repo, testRefName)

rl, err := repo.ReadReflog(testRefName)
checkFatal(t, err)
defer rl.Free()

newOID := NewOidFromBytes([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
checkFatal(t, rl.AppendEntry(newOID, sig, "synthetic"))
checkFatal(t, rl.Write())

want := append([]*ReflogEntry{
&ReflogEntry{
New: newOID,
Old: logs[0].New,
Committer: sig,
Message: "synthetic",
},
}, logs...)
got := allReflogEntries(t, repo, testRefName)
assertEntriesEqual(t, got, want)
})

t.Run("RenameReflog", func(t *testing.T) {
logs := allReflogEntries(t, repo, testRefName)
newRefName := "refs/heads/new"

checkFatal(t, repo.RenameReflog(testRefName, newRefName))
assertEntriesEqual(t, allReflogEntries(t, repo, testRefName), nil)
assertEntriesEqual(t, allReflogEntries(t, repo, newRefName), logs)

checkFatal(t, repo.RenameReflog(newRefName, testRefName))
assertEntriesEqual(t, allReflogEntries(t, repo, testRefName), logs)
assertEntriesEqual(t, allReflogEntries(t, repo, newRefName), nil)
})

t.Run("DeleteReflog", func(t *testing.T) {
checkFatal(t, repo.DeleteReflog(testRefName))
assertEntriesEqual(t, allReflogEntries(t, repo, testRefName), nil)
})

}

0 comments on commit ce4dd16

Please sign in to comment.