diff --git a/reflog.go b/reflog.go new file mode 100644 index 00000000..24bafd97 --- /dev/null +++ b/reflog.go @@ -0,0 +1,175 @@ +package git + +/* +#include +*/ +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) +} diff --git a/reflog_test.go b/reflog_test.go new file mode 100644 index 00000000..4612c96f --- /dev/null +++ b/reflog_test.go @@ -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) + }) + +}