Skip to content

Commit 6842909

Browse files
committed
Reflog support (libgit2#467)
1 parent 4b2ac7c commit 6842909

File tree

2 files changed

+337
-0
lines changed

2 files changed

+337
-0
lines changed

reflog.go

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package git
2+
3+
/*
4+
#include <git2.h>
5+
*/
6+
import "C"
7+
import (
8+
"runtime"
9+
"unsafe"
10+
)
11+
12+
// RefLog is a log of changes for a reference
13+
type RefLog struct {
14+
ptr *C.git_reflog
15+
repo *Repository
16+
name string
17+
}
18+
19+
func newRefLogFromC(ptr *C.git_reflog, repo *Repository, name string) *RefLog {
20+
l := &RefLog{
21+
ptr: ptr,
22+
repo: repo,
23+
name: name,
24+
}
25+
runtime.SetFinalizer(l, (*RefLog).Free)
26+
return l
27+
}
28+
29+
func (repo *Repository) ReadRefLog(name string) (*RefLog, error) {
30+
runtime.LockOSThread()
31+
defer runtime.UnlockOSThread()
32+
33+
cname := C.CString(name)
34+
defer C.free(unsafe.Pointer(cname))
35+
36+
var ptr *C.git_reflog
37+
38+
ecode := C.git_reflog_read(&ptr, repo.ptr, cname)
39+
runtime.KeepAlive(repo)
40+
if ecode < 0 {
41+
return nil, MakeGitError(ecode)
42+
}
43+
44+
return newRefLogFromC(ptr, repo, name), nil
45+
}
46+
47+
func (repo *Repository) DeleteReflog(name string) error {
48+
runtime.LockOSThread()
49+
defer runtime.UnlockOSThread()
50+
51+
cname := C.CString(name)
52+
defer C.free(unsafe.Pointer(cname))
53+
54+
ecode := C.git_reflog_delete(repo.ptr, cname)
55+
runtime.KeepAlive(repo)
56+
if ecode < 0 {
57+
return MakeGitError(ecode)
58+
}
59+
60+
return nil
61+
}
62+
63+
func (repo *Repository) RenameReflog(oldName, newName string) error {
64+
runtime.LockOSThread()
65+
defer runtime.UnlockOSThread()
66+
67+
cOldName := C.CString(oldName)
68+
defer C.free(unsafe.Pointer(cOldName))
69+
70+
cNewName := C.CString(newName)
71+
defer C.free(unsafe.Pointer(cNewName))
72+
73+
ecode := C.git_reflog_rename(repo.ptr, cOldName, cNewName)
74+
runtime.KeepAlive(repo)
75+
if ecode < 0 {
76+
return MakeGitError(ecode)
77+
}
78+
79+
return nil
80+
}
81+
82+
func (l *RefLog) Write() error {
83+
runtime.LockOSThread()
84+
defer runtime.UnlockOSThread()
85+
86+
ecode := C.git_reflog_write(l.ptr)
87+
runtime.KeepAlive(l)
88+
if ecode < 0 {
89+
return MakeGitError(ecode)
90+
}
91+
return nil
92+
}
93+
94+
func (l *RefLog) EntryCount() uint {
95+
runtime.LockOSThread()
96+
defer runtime.UnlockOSThread()
97+
98+
count := C.git_reflog_entrycount(l.ptr)
99+
runtime.KeepAlive(l)
100+
return uint(count)
101+
}
102+
103+
// RefLogEntry specifies a reference change
104+
type RefLogEntry struct {
105+
Old *Oid
106+
New *Oid
107+
Committer *Signature
108+
Message string // may be empty
109+
}
110+
111+
func newRefLogEntry(entry *C.git_reflog_entry) *RefLogEntry {
112+
return &RefLogEntry{
113+
New: newOidFromC(C.git_reflog_entry_id_new(entry)),
114+
Old: newOidFromC(C.git_reflog_entry_id_old(entry)),
115+
Committer: newSignatureFromC(C.git_reflog_entry_committer(entry)),
116+
Message: C.GoString(C.git_reflog_entry_message(entry)),
117+
}
118+
}
119+
120+
func (l *RefLog) EntryByIndex(index uint) *RefLogEntry {
121+
runtime.LockOSThread()
122+
defer runtime.UnlockOSThread()
123+
124+
entry := C.git_reflog_entry_byindex(l.ptr, C.size_t(index))
125+
if entry == nil {
126+
return nil
127+
}
128+
129+
goEntry := newRefLogEntry(entry)
130+
runtime.KeepAlive(l)
131+
132+
return goEntry
133+
}
134+
135+
func (l *RefLog) DropEntry(index uint, rewriteHistory bool) error {
136+
runtime.LockOSThread()
137+
defer runtime.UnlockOSThread()
138+
139+
var rewriteHistoryInt int
140+
if rewriteHistory {
141+
rewriteHistoryInt = 1
142+
}
143+
144+
ecode := C.git_reflog_drop(l.ptr, C.size_t(index), C.int(rewriteHistoryInt))
145+
runtime.KeepAlive(l)
146+
if ecode < 0 {
147+
return MakeGitError(ecode)
148+
}
149+
150+
return nil
151+
}
152+
153+
func (l *RefLog) AppendEntry(oid *Oid, committer *Signature, message string) error {
154+
runtime.LockOSThread()
155+
defer runtime.UnlockOSThread()
156+
157+
cSignature, err := committer.toC()
158+
if err != nil {
159+
return err
160+
}
161+
defer C.git_signature_free(cSignature)
162+
163+
cMsg := C.CString(message)
164+
defer C.free(unsafe.Pointer(cMsg))
165+
166+
C.git_reflog_append(l.ptr, oid.toC(), cSignature, cMsg)
167+
runtime.KeepAlive(l)
168+
169+
return nil
170+
}
171+
172+
func (l *RefLog) Free() {
173+
runtime.SetFinalizer(l, nil)
174+
C.git_reflog_free(l.ptr)
175+
}

reflog_test.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package git
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"testing"
7+
"time"
8+
)
9+
10+
func allReflogEntries(t *testing.T, repo *Repository, refName string) (entries []*RefLogEntry) {
11+
rl, err := repo.ReadRefLog(refName)
12+
checkFatal(t, err)
13+
defer rl.Free()
14+
15+
for i := uint(0); i < rl.EntryCount(); i++ {
16+
entries = append(entries, rl.EntryByIndex(i))
17+
}
18+
return entries
19+
}
20+
21+
// assertEntriesEqual will assert that the reflogs match with the exception of
22+
// the signature time (it is not reliably deterministic to predict the
23+
// signature time during many reference updates)
24+
func assertEntriesEqual(t *testing.T, got, want []*RefLogEntry) {
25+
if len(got) != len(want) {
26+
t.Fatalf("got %d length, wanted %d length", len(got), len(want))
27+
}
28+
29+
for i := 0; i < len(got); i++ {
30+
gi := got[i]
31+
wi := want[i]
32+
// remove the signature time to make the results deterministic
33+
gi.Committer.When = time.Time{}
34+
wi.Committer.When = time.Time{}
35+
// check committer separately to print results clearly
36+
if !reflect.DeepEqual(gi.Committer, wi.Committer) {
37+
t.Fatalf("got committer %v, want committer %v",
38+
gi.Committer, wi.Committer)
39+
}
40+
if !reflect.DeepEqual(gi, wi) {
41+
t.Fatalf("got %v, want %v", gi, wi)
42+
}
43+
}
44+
}
45+
46+
func TestReflog(t *testing.T) {
47+
t.Parallel()
48+
repo := createTestRepo(t)
49+
defer cleanupTestRepo(t, repo)
50+
51+
commitID, treeId := seedTestRepo(t, repo)
52+
53+
testRefName := "refs/heads/test"
54+
55+
// configure committer for deterministic reflog entries
56+
cfg, err := repo.Config()
57+
checkFatal(t, err)
58+
59+
sig := &Signature{
60+
Name: "Rand Om Hacker",
61+
62+
}
63+
64+
checkFatal(t, cfg.SetString("user.name", sig.Name))
65+
checkFatal(t, cfg.SetString("user.email", sig.Email))
66+
67+
checkFatal(t, repo.References.EnsureLog(testRefName))
68+
_, err = repo.References.Create(testRefName, commitID, true, "first update")
69+
checkFatal(t, err)
70+
got := allReflogEntries(t, repo, testRefName)
71+
want := []*RefLogEntry{
72+
&RefLogEntry{
73+
New: commitID,
74+
Old: &Oid{},
75+
Committer: sig,
76+
Message: "first update",
77+
},
78+
}
79+
80+
// create additional commits and verify they are added to reflog
81+
tree, err := repo.LookupTree(treeId)
82+
checkFatal(t, err)
83+
for i := 0; i < 10; i++ {
84+
nextEntry := &RefLogEntry{
85+
Old: commitID,
86+
Committer: sig,
87+
Message: fmt.Sprintf("commit: %d", i),
88+
}
89+
90+
commit, err := repo.LookupCommit(commitID)
91+
checkFatal(t, err)
92+
93+
commitID, err = repo.CreateCommit(testRefName, sig, sig, fmt.Sprint(i), tree, commit)
94+
checkFatal(t, err)
95+
96+
nextEntry.New = commitID
97+
98+
want = append([]*RefLogEntry{nextEntry}, want...)
99+
}
100+
got = allReflogEntries(t, repo, testRefName)
101+
102+
t.Run("ReadReflog", func(t *testing.T) {
103+
assertEntriesEqual(t, got, want)
104+
})
105+
106+
t.Run("DropEntry", func(t *testing.T) {
107+
rl, err := repo.ReadRefLog(testRefName)
108+
checkFatal(t, err)
109+
defer rl.Free()
110+
111+
gotBefore := allReflogEntries(t, repo, testRefName)
112+
113+
checkFatal(t, rl.DropEntry(0, false))
114+
checkFatal(t, rl.Write())
115+
116+
gotAfter := allReflogEntries(t, repo, testRefName)
117+
118+
assertEntriesEqual(t, gotAfter, gotBefore[1:])
119+
})
120+
121+
t.Run("AppendEntry", func(t *testing.T) {
122+
logs := allReflogEntries(t, repo, testRefName)
123+
124+
rl, err := repo.ReadRefLog(testRefName)
125+
checkFatal(t, err)
126+
defer rl.Free()
127+
128+
newOID := NewOidFromBytes([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
129+
checkFatal(t, rl.AppendEntry(newOID, sig, "synthetic"))
130+
checkFatal(t, rl.Write())
131+
132+
want := append([]*RefLogEntry{
133+
&RefLogEntry{
134+
New: newOID,
135+
Old: logs[0].New,
136+
Committer: sig,
137+
Message: "synthetic",
138+
},
139+
}, logs...)
140+
got := allReflogEntries(t, repo, testRefName)
141+
assertEntriesEqual(t, got, want)
142+
})
143+
144+
t.Run("RenameReflog", func(t *testing.T) {
145+
logs := allReflogEntries(t, repo, testRefName)
146+
newRefName := "refs/heads/new"
147+
148+
checkFatal(t, repo.RenameReflog(testRefName, newRefName))
149+
assertEntriesEqual(t, allReflogEntries(t, repo, testRefName), nil)
150+
assertEntriesEqual(t, allReflogEntries(t, repo, newRefName), logs)
151+
152+
checkFatal(t, repo.RenameReflog(newRefName, testRefName))
153+
assertEntriesEqual(t, allReflogEntries(t, repo, testRefName), logs)
154+
assertEntriesEqual(t, allReflogEntries(t, repo, newRefName), nil)
155+
})
156+
157+
t.Run("DeleteReflog", func(t *testing.T) {
158+
checkFatal(t, repo.DeleteReflog(testRefName))
159+
assertEntriesEqual(t, allReflogEntries(t, repo, testRefName), nil)
160+
})
161+
162+
}

0 commit comments

Comments
 (0)