Repository: libgit2/git2go
Pull request #909 (2022-02-25 03:21:52)

Add refspec bindings (#898)
This add support for the parse, access, and transform functions for
refspec objects.

(cherry picked from commit eae00773cce87d5282a8ac7c10b5c1961ee6f9cb)


Patch:
diff --git a/refspec.go b/refspec.go
new file mode 100644
index 0000000..450e5af
--- /dev/null
+++ b/refspec.go
@@ -0,0 +1,149 @@
+package git
+
+/*
+#include <git2.h>
+*/
+import "C"
+import (
+	"runtime"
+	"unsafe"
+)
+
+type Refspec struct {
+	doNotCompare
+	ptr *C.git_refspec
+}
+
+// ParseRefspec parses a given refspec string
+func ParseRefspec(input string, isFetch bool) (*Refspec, error) {
+	var ptr *C.git_refspec
+
+	cinput := C.CString(input)
+	defer C.free(unsafe.Pointer(cinput))
+
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	ret := C.git_refspec_parse(&ptr, cinput, cbool(isFetch))
+	if ret < 0 {
+		return nil, MakeGitError(ret)
+	}
+
+	spec := &Refspec{ptr: ptr}
+	runtime.SetFinalizer(spec, (*Refspec).Free)
+	return spec, nil
+}
+
+// Free releases a refspec object which has been created by ParseRefspec
+func (s *Refspec) Free() {
+	runtime.SetFinalizer(s, nil)
+	C.git_refspec_free(s.ptr)
+}
+
+// Direction returns the refspec's direction
+func (s *Refspec) Direction() ConnectDirection {
+	direction := C.git_refspec_direction(s.ptr)
+	return ConnectDirection(direction)
+}
+
+// Src returns the refspec's source specifier
+func (s *Refspec) Src() string {
+	var ret string
+	cstr := C.git_refspec_src(s.ptr)
+
+	if cstr != nil {
+		ret = C.GoString(cstr)
+	}
+
+	runtime.KeepAlive(s)
+	return ret
+}
+
+// Dst returns the refspec's destination specifier
+func (s *Refspec) Dst() string {
+	var ret string
+	cstr := C.git_refspec_dst(s.ptr)
+
+	if cstr != nil {
+		ret = C.GoString(cstr)
+	}
+
+	runtime.KeepAlive(s)
+	return ret
+}
+
+// Force returns the refspec's force-update setting
+func (s *Refspec) Force() bool {
+	force := C.git_refspec_force(s.ptr)
+	return force != 0
+}
+
+// String returns the refspec's string representation
+func (s *Refspec) String() string {
+	var ret string
+	cstr := C.git_refspec_string(s.ptr)
+
+	if cstr != nil {
+		ret = C.GoString(cstr)
+	}
+
+	runtime.KeepAlive(s)
+	return ret
+}
+
+// SrcMatches checks if a refspec's source descriptor matches a reference
+func (s *Refspec) SrcMatches(refname string) bool {
+	cname := C.CString(refname)
+	defer C.free(unsafe.Pointer(cname))
+
+	matches := C.git_refspec_src_matches(s.ptr, cname)
+	return matches != 0
+}
+
+// SrcMatches checks if a refspec's destination descriptor matches a reference
+func (s *Refspec) DstMatches(refname string) bool {
+	cname := C.CString(refname)
+	defer C.free(unsafe.Pointer(cname))
+
+	matches := C.git_refspec_dst_matches(s.ptr, cname)
+	return matches != 0
+}
+
+// Transform a reference to its target following the refspec's rules
+func (s *Refspec) Transform(refname string) (string, error) {
+	buf := C.git_buf{}
+
+	cname := C.CString(refname)
+	defer C.free(unsafe.Pointer(cname))
+
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	ret := C.git_refspec_transform(&buf, s.ptr, cname)
+	if ret < 0 {
+		return "", MakeGitError(ret)
+	}
+	defer C.git_buf_dispose(&buf)
+
+	return C.GoString(buf.ptr), nil
+}
+
+// Rtransform converts a target reference to its source reference following the
+// refspec's rules
+func (s *Refspec) Rtransform(refname string) (string, error) {
+	buf := C.git_buf{}
+
+	cname := C.CString(refname)
+	defer C.free(unsafe.Pointer(cname))
+
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	ret := C.git_refspec_rtransform(&buf, s.ptr, cname)
+	if ret < 0 {
+		return "", MakeGitError(ret)
+	}
+	defer C.git_buf_dispose(&buf)
+
+	return C.GoString(buf.ptr), nil
+}


Test patch:
diff --git a/refspec_test.go b/refspec_test.go
new file mode 100644
index 0000000..7064198
--- /dev/null
+++ b/refspec_test.go
@@ -0,0 +1,75 @@
+package git
+
+import (
+	"testing"
+)
+
+func TestRefspec(t *testing.T) {
+	t.Parallel()
+
+	const (
+		input      = "+refs/heads/*:refs/remotes/origin/*"
+		mainLocal  = "refs/heads/main"
+		mainRemote = "refs/remotes/origin/main"
+	)
+
+	refspec, err := ParseRefspec(input, true)
+	checkFatal(t, err)
+
+	// Accessors
+
+	s := refspec.String()
+	if s != input {
+		t.Errorf("expected string %q, got %q", input, s)
+	}
+
+	if d := refspec.Direction(); d != ConnectDirectionFetch {
+		t.Errorf("expected fetch refspec, got direction %v", d)
+	}
+
+	if pat, expected := refspec.Src(), "refs/heads/*"; pat != expected {
+		t.Errorf("expected refspec src %q, got %q", expected, pat)
+	}
+
+	if pat, expected := refspec.Dst(), "refs/remotes/origin/*"; pat != expected {
+		t.Errorf("expected refspec dst %q, got %q", expected, pat)
+	}
+
+	if !refspec.Force() {
+		t.Error("expected refspec force flag")
+	}
+
+	// SrcMatches
+
+	if !refspec.SrcMatches(mainLocal) {
+		t.Errorf("refspec source did not match %q", mainLocal)
+	}
+
+	if refspec.SrcMatches("refs/tags/v1.0") {
+		t.Error("refspec source matched under refs/tags")
+	}
+
+	// DstMatches
+
+	if !refspec.DstMatches(mainRemote) {
+		t.Errorf("refspec destination did not match %q", mainRemote)
+	}
+
+	if refspec.DstMatches("refs/tags/v1.0") {
+		t.Error("refspec destination matched under refs/tags")
+	}
+
+	// Transforms
+
+	fromLocal, err := refspec.Transform(mainLocal)
+	checkFatal(t, err)
+	if fromLocal != mainRemote {
+		t.Errorf("transform by refspec returned %s; expected %s", fromLocal, mainRemote)
+	}
+
+	fromRemote, err := refspec.Rtransform(mainRemote)
+	checkFatal(t, err)
+	if fromRemote != mainLocal {
+		t.Errorf("rtransform by refspec returned %s; expected %s", fromRemote, mainLocal)
+	}
+}