Reflog support (#467)
This commit is contained in:
parent
4b2ac7c998
commit
ce4dd16b1f
|
@ -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)
|
||||
}
|
|
@ -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: "random@hacker.com",
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
Loading…
Reference in New Issue