From 37d4d3ddf8d734bbfcacff4a2b88f89801421eb8 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Sat, 16 Oct 2021 04:50:06 -0700 Subject: [PATCH 01/10] Declare forward-compatibility with libgit2 v1.3.0 #minor (#843) This commit marks this (Golang) version of git2go as being compatible with libgit2 v1.3.0. --- .github/workflows/ci.yml | 1 + Build_bundled_static.go | 4 ++-- Build_system_dynamic.go | 4 ++-- Build_system_static.go | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50e5e14c..dcf980bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,7 @@ jobs: matrix: libgit2: - '109b4c887ffb63962c7017a66fc4a1f48becb48e' # v1.2.0 with a fixed symbol + - 'v1.3.0' name: Go (system-wide, dynamic) runs-on: ubuntu-20.04 diff --git a/Build_bundled_static.go b/Build_bundled_static.go index 09ed0f5b..fc652c86 100644 --- a/Build_bundled_static.go +++ b/Build_bundled_static.go @@ -10,8 +10,8 @@ package git #cgo CFLAGS: -DLIBGIT2_STATIC #include -#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 2 || LIBGIT2_VER_MINOR > 2 -# error "Invalid libgit2 version; this git2go supports libgit2 between v1.2.0 and v1.2.0" +#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 2 || LIBGIT2_VER_MINOR > 3 +# error "Invalid libgit2 version; this git2go supports libgit2 between v1.2.0 and v1.3.0" #endif */ import "C" diff --git a/Build_system_dynamic.go b/Build_system_dynamic.go index 95001885..566202da 100644 --- a/Build_system_dynamic.go +++ b/Build_system_dynamic.go @@ -8,8 +8,8 @@ package git #cgo CFLAGS: -DLIBGIT2_DYNAMIC #include -#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 2 || LIBGIT2_VER_MINOR > 2 -# error "Invalid libgit2 version; this git2go supports libgit2 between v1.2.0 and v1.2.0" +#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 2 || LIBGIT2_VER_MINOR > 3 +# error "Invalid libgit2 version; this git2go supports libgit2 between v1.2.0 and v1.3.0" #endif */ import "C" diff --git a/Build_system_static.go b/Build_system_static.go index 309369d9..2e17ba1c 100644 --- a/Build_system_static.go +++ b/Build_system_static.go @@ -8,8 +8,8 @@ package git #cgo CFLAGS: -DLIBGIT2_STATIC #include -#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 2 || LIBGIT2_VER_MINOR > 2 -# error "Invalid libgit2 version; this git2go supports libgit2 between v1.2.0 and v1.2.0" +#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 2 || LIBGIT2_VER_MINOR > 3 +# error "Invalid libgit2 version; this git2go supports libgit2 between v1.2.0 and v1.3.0" #endif */ import "C" From bacde673363f8559a954bcde8ecd5013de8dba79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Nov 2021 06:23:02 -0800 Subject: [PATCH 02/10] bugfix: HTTPS Clone fails with remote pointer not found using Go transport (#836) (#842) (#849) Fixes: #836 Changes: * adding a weak bool param for Remote * create a new remote in the smartTransportCallback incase one is not found (cherry picked from commit 0e8009f00a65034d196c67b1cdd82af6f12c34d3) Co-authored-by: Yashodhan Ghadge Co-authored-by: lhchavez --- clone_test.go | 15 +++++++++++++++ remote.go | 15 +++++++++++++++ transport.go | 7 ++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/clone_test.go b/clone_test.go index 8814dd00..9ff03e67 100644 --- a/clone_test.go +++ b/clone_test.go @@ -2,6 +2,7 @@ package git import ( "io/ioutil" + "os" "testing" ) @@ -70,3 +71,17 @@ func TestCloneWithCallback(t *testing.T) { } defer remote.Free() } + +// TestCloneWithExternalHTTPUrl +func TestCloneWithExternalHTTPUrl(t *testing.T) { + + path, err := ioutil.TempDir("", "git2go") + defer os.RemoveAll(path) + + // clone the repo + url := "https://github.com/libgit2/TestGitRepository" + _, err = Clone(url, path, &CloneOptions{}) + if err != nil { + t.Fatal("cannot clone remote repo via https, error: ", err) + } +} diff --git a/remote.go b/remote.go index 7abc461a..f4424fc2 100644 --- a/remote.go +++ b/remote.go @@ -182,6 +182,9 @@ type Remote struct { ptr *C.git_remote callbacks RemoteCallbacks repo *Repository + // weak indicates that a remote is a weak pointer and should not be + // freed. + weak bool } type remotePointerList struct { @@ -602,6 +605,9 @@ func (r *Remote) free() { // Free releases the resources of the Remote. func (r *Remote) Free() { r.repo.Remotes.untrackRemote(r) + if r.weak { + return + } r.free() } @@ -1231,3 +1237,12 @@ func freeRemoteCreateOptions(ptr *C.git_remote_create_options) { C.free(unsafe.Pointer(ptr.name)) C.free(unsafe.Pointer(ptr.fetchspec)) } + +// createNewEmptyRemote used to get a new empty object of *Remote +func createNewEmptyRemote() *Remote { + return &Remote{ + callbacks: RemoteCallbacks{}, + repo: nil, + weak: false, + } +} diff --git a/transport.go b/transport.go index 1afa6f4c..23514b47 100644 --- a/transport.go +++ b/transport.go @@ -22,7 +22,6 @@ void _go_git_setup_smart_subtransport_stream(_go_managed_smart_subtransport_stre */ import "C" import ( - "errors" "fmt" "io" "reflect" @@ -306,8 +305,10 @@ func smartTransportCallback( registeredSmartTransport := pointerHandles.Get(handle).(*RegisteredSmartTransport) remote, ok := remotePointers.get(owner) if !ok { - err := errors.New("remote pointer not found") - return setCallbackError(errorMessage, err) + // create a new empty remote and set it + // as a weak pointer, so that control stays in golang + remote = createNewEmptyRemote() + remote.weak = true } managed := &managedSmartSubtransport{ From 303350587982ea4a27b56c20f53ed96fc0bef81e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Nov 2021 06:27:24 -0800 Subject: [PATCH 03/10] Make ssh commands used in the git smart transport compatible with libgit2 (#852) (#856) * Fix ssh commands used in go SmartSubtransport Before the fix, the commands sent were of the form: ``` git-upload-pack "/bar/test-reponame" ``` This resulted in the git server returning error: `error parsing command: invalid git command` This change replaces the double quotes with single quotes: ``` git-upload-pack '/bar/test-reponame' ``` * Update ssh.go Co-authored-by: lhchavez (cherry picked from commit 6cea7a7a59f44e0e72ca577fbea65a042b3fb26b) Co-authored-by: Sunny Co-authored-by: lhchavez --- ssh.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ssh.go b/ssh.go index dd2725e1..0581ca09 100644 --- a/ssh.go +++ b/ssh.go @@ -17,6 +17,7 @@ import ( "net" "net/url" "runtime" + "strings" "unsafe" "golang.org/x/crypto/ssh" @@ -74,6 +75,13 @@ func (t *sshSmartSubtransport) Action(urlString string, action SmartServiceActio return nil, err } + // Escape \ and '. + uPath := strings.Replace(u.Path, `\`, `\\`, -1) + uPath = strings.Replace(uPath, `'`, `\'`, -1) + + // TODO: Add percentage decode similar to libgit2. + // Refer: https://github.com/libgit2/libgit2/blob/358a60e1b46000ea99ef10b4dd709e92f75ff74b/src/str.c#L455-L481 + var cmd string switch action { case SmartServiceActionUploadpackLs, SmartServiceActionUploadpack: @@ -83,7 +91,7 @@ func (t *sshSmartSubtransport) Action(urlString string, action SmartServiceActio } t.Close() } - cmd = fmt.Sprintf("git-upload-pack %q", u.Path) + cmd = fmt.Sprintf("git-upload-pack '%s'", uPath) case SmartServiceActionReceivepackLs, SmartServiceActionReceivepack: if t.currentStream != nil { @@ -92,7 +100,7 @@ func (t *sshSmartSubtransport) Action(urlString string, action SmartServiceActio } t.Close() } - cmd = fmt.Sprintf("git-receive-pack %q", u.Path) + cmd = fmt.Sprintf("git-receive-pack '%s'", uPath) default: return nil, fmt.Errorf("unexpected action: %v", action) From a6446ac047eed61ba362f9ff1fe6fa23b8b9ab3b Mon Sep 17 00:00:00 2001 From: lhchavez Date: Tue, 9 Nov 2021 06:36:00 -0800 Subject: [PATCH 04/10] Fix replace statement example in README.md (#859) (#860) (cherry picked from commit 533c82f2707b8ad2f0f667867b3ea91ec08667aa) Co-authored-by: Ignacio Taranto --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cc36eb2..64cd74f8 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ In order to let Go pass the correct flags to `pkg-config`, `-tags static` needs One thing to take into account is that since Go expects the `pkg-config` file to be within the same directory where `make install-static` was called, so the `go.mod` file may need to have a [`replace` directive](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) so that the correct setup is achieved. So if `git2go` is checked out at `$GOPATH/src/github.com/libgit2/git2go` and your project at `$GOPATH/src/github.com/my/project`, the `go.mod` file of `github.com/my/project` might need to have a line like - replace github.com/libgit2/git2go/v32 ../../libgit2/git2go + replace github.com/libgit2/git2go/v32 => ../../libgit2/git2go Parallelism and network operations ---------------------------------- From a96457545387aed49185667740ba809761af33b5 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Tue, 9 Nov 2021 06:46:28 -0800 Subject: [PATCH 05/10] Generate stringer files automatically (#841) (#865) Added `stringer` annotations to `git.go` for `ErrorClass` and `ErrorCode`. Added `generate` rule for `Makefile` to generate string representations for these types (first building cgo files in `_obj` dir to get C constants). Finally, updated `ci` actions workflow to check that generated files are up to date. Fixes: #543 (cherry picked from commit 5e35338d58b589939b599c98ec9e6b44f94de20a) Co-authored-by: Kirill --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ Makefile | 4 ++++ git.go | 2 ++ 3 files changed, 29 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcf980bf..c3d01de4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,3 +106,26 @@ jobs: sudo ./script/build-libgit2.sh --static --system - name: Test run: go test --count=1 --tags "static,system_libgit2" ./... + + check-generate: + name: Check generated files were not modified + runs-on: ubuntu-20.04 + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: '1.17' + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Install libgit2 build dependencies + run: | + git submodule update --init + sudo apt-get install -y --no-install-recommends libssh2-1-dev + go install golang.org/x/tools/cmd/stringer@latest + - name: Generate files + run: | + export PATH=$(go env GOPATH)/bin:$PATH + make generate + - name: Check nothing changed + run: git diff --quiet --exit-code || (echo "detected changes after generate" ; git status ; exit 1) diff --git a/Makefile b/Makefile index 84262f4d..71c5ee06 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,10 @@ TEST_ARGS ?= --count=1 default: test + +generate: static-build/install/lib/libgit2.a + go generate --tags "static" ./... + # System library # ============== # This uses whatever version of libgit2 can be found in the system. diff --git a/git.go b/git.go index 383c4926..b7c8b3c7 100644 --- a/git.go +++ b/git.go @@ -14,6 +14,7 @@ import ( "unsafe" ) +//go:generate stringer -type ErrorClass -trimprefix ErrorClass -tags static type ErrorClass int const ( @@ -48,6 +49,7 @@ const ( ErrorClassPatch ErrorClass = C.GIT_ERROR_PATCH ) +//go:generate stringer -type ErrorCode -trimprefix ErrorCode -tags static type ErrorCode int const ( From a3f32b93cd02f3c2f8ea60b3e57b5b7945a7f874 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 19:00:12 -0800 Subject: [PATCH 06/10] Add EnableFsyncGitDir to enable synchronized writes to the gitdir (#874) (#877) This adds support for the GIT_OPT_ENABLE_FSYNC_GITDIR option in libgit2. Co-authored-by: James Fargher (cherry picked from commit 1fcc9d87430e41cc333ce5b2431df7058e03bbb7) Co-authored-by: James Fargher --- settings.go | 8 ++++++++ settings_test.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/settings.go b/settings.go index 93256181..7f9b5b12 100644 --- a/settings.go +++ b/settings.go @@ -101,6 +101,14 @@ func EnableStrictHashVerification(enabled bool) error { } } +func EnableFsyncGitDir(enabled bool) error { + if enabled { + return setSizet(C.GIT_OPT_ENABLE_FSYNC_GITDIR, 1) + } else { + return setSizet(C.GIT_OPT_ENABLE_FSYNC_GITDIR, 0) + } +} + func CachedMemory() (current int, allowed int, err error) { return getSizetSizet(C.GIT_OPT_GET_CACHED_MEMORY) } diff --git a/settings_test.go b/settings_test.go index 47eb7116..e3761d45 100644 --- a/settings_test.go +++ b/settings_test.go @@ -65,6 +65,14 @@ func TestEnableStrictHashVerification(t *testing.T) { checkFatal(t, err) } +func TestEnableFsyncGitDir(t *testing.T) { + err := EnableFsyncGitDir(false) + checkFatal(t, err) + + err = EnableFsyncGitDir(true) + checkFatal(t, err) +} + func TestCachedMemory(t *testing.T) { current, allowed, err := CachedMemory() checkFatal(t, err) From 9dfdab765a5e1a3b5e4497cb217f34ed2d5e932d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 19:06:48 -0800 Subject: [PATCH 07/10] Add ProxyOptions for push operations (#872) (#882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Analog to #623 but for push operations rather than fetch. (cherry picked from commit 5eca48cda937e67c1f8d68a55c2db22a79d83d0c) Co-authored-by: Aurélien <6292584+au2001@users.noreply.github.com> --- remote.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/remote.go b/remote.go index f4424fc2..cde92b94 100644 --- a/remote.go +++ b/remote.go @@ -291,6 +291,9 @@ type PushOptions struct { // Headers are extra headers for the push operation. Headers []string + + // Proxy options to use for this push operation + ProxyOptions ProxyOptions } type RemoteHead struct { @@ -1001,6 +1004,7 @@ func populatePushOptions(copts *C.git_push_options, opts *PushOptions, errorTarg strings: makeCStringsFromStrings(opts.Headers), } populateRemoteCallbacks(&copts.callbacks, &opts.RemoteCallbacks, errorTarget) + populateProxyOptions(&copts.proxy_opts, &opts.ProxyOptions) return copts } @@ -1010,6 +1014,7 @@ func freePushOptions(copts *C.git_push_options) { } untrackCallbacksPayload(&copts.callbacks) freeStrarray(&copts.custom_headers) + freeProxyOptions(&copts.proxy_opts) } // Fetch performs a fetch operation. refspecs specifies which refspecs From 8230ed934e4cdbab5fb8771206202d07d2a22961 Mon Sep 17 00:00:00 2001 From: Dylan Richardson Date: Sat, 22 Jan 2022 21:04:16 -0600 Subject: [PATCH 08/10] readme: link to godoc for current main branch (#892) This is a release-specific change corresponding to #886 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64cd74f8..a574458b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ git2go ====== -[![GoDoc](https://godoc.org/github.com/libgit2/git2go?status.svg)](http://godoc.org/github.com/libgit2/git2go) [![Build Status](https://travis-ci.org/libgit2/git2go.svg?branch=main)](https://travis-ci.org/libgit2/git2go) +[![GoDoc](https://godoc.org/github.com/libgit2/git2go?status.svg)](http://godoc.org/github.com/libgit2/git2go/v32) [![Build Status](https://travis-ci.org/libgit2/git2go.svg?branch=main)](https://travis-ci.org/libgit2/git2go) Go bindings for [libgit2](http://libgit2.github.com/). From a7dc7d41b9941f5429342ff148694f765e449f5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Feb 2022 19:20:37 -0800 Subject: [PATCH 09/10] rebase: Add wrapper for `git_rebase_inmemory_index()` (#900) (#904) * rebase: Fix missing initialization of the repo pointer While the `Rebase` structure has a pointer to the repository the rebase is creatde in, this pointer isn't ever initialized. Fix this. * rebase: Add wrapper for `git_rebase_inmemory_index()` Add a new wrapper for `git_rebase_inmemory_index()`, which can be used to retrieve the index for an in-memory rebase. Co-authored-by: Patrick Steinhardt (cherry picked from commit e7d1b2b69fbe476c75a00cf8dcda284337facb50) Co-authored-by: Patrick Steinhardt --- rebase.go | 29 +++++++++++++++++--- rebase_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/rebase.go b/rebase.go index 58c0b6f9..b9c94fd8 100644 --- a/rebase.go +++ b/rebase.go @@ -318,7 +318,7 @@ func (r *Repository) InitRebase(branch *AnnotatedCommit, upstream *AnnotatedComm return nil, MakeGitError(ret) } - return newRebaseFromC(ptr, cOpts), nil + return newRebaseFromC(ptr, r, cOpts), nil } // OpenRebase opens an existing rebase that was previously started by either an invocation of InitRebase or by another client. @@ -340,7 +340,7 @@ func (r *Repository) OpenRebase(opts *RebaseOptions) (*Rebase, error) { return nil, MakeGitError(ret) } - return newRebaseFromC(ptr, cOpts), nil + return newRebaseFromC(ptr, r, cOpts), nil } // OperationAt gets the rebase operation specified by the given index. @@ -392,6 +392,27 @@ func (rebase *Rebase) Next() (*RebaseOperation, error) { return newRebaseOperationFromC(ptr), nil } +// InmemoryIndex gets the index produced by the last operation, which is the +// result of `Next()` and which will be committed by the next invocation of +// `Commit()`. This is useful for resolving conflicts in an in-memory rebase +// before committing them. +// +// This is only applicable for in-memory rebases; for rebases within a working +// directory, the changes were applied to the repository's index. +func (rebase *Rebase) InmemoryIndex() (*Index, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var ptr *C.git_index + err := C.git_rebase_inmemory_index(&ptr, rebase.ptr) + runtime.KeepAlive(rebase) + if err < 0 { + return nil, MakeGitError(err) + } + + return newIndexFromC(ptr, rebase.r), nil +} + // Commit commits the current patch. // You must have resolved any conflicts that were introduced during the patch application from the Next() invocation. func (rebase *Rebase) Commit(ID *Oid, author, committer *Signature, message string) error { @@ -457,8 +478,8 @@ func (r *Rebase) Free() { freeRebaseOptions(r.options) } -func newRebaseFromC(ptr *C.git_rebase, opts *C.git_rebase_options) *Rebase { - rebase := &Rebase{ptr: ptr, options: opts} +func newRebaseFromC(ptr *C.git_rebase, repo *Repository, opts *C.git_rebase_options) *Rebase { + rebase := &Rebase{ptr: ptr, r: repo, options: opts} runtime.SetFinalizer(rebase, (*Rebase).Free) return rebase } diff --git a/rebase_test.go b/rebase_test.go index 65df5222..779e1a55 100644 --- a/rebase_test.go +++ b/rebase_test.go @@ -14,6 +14,80 @@ import ( // Tests +func TestRebaseInMemoryWithConflict(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + seedTestRepo(t, repo) + + // Create two branches with common history, where both modify "common-file" + // in a conflicting way. + _, err := commitSomething(repo, "common-file", "a\nb\nc\n", commitOptions{}) + checkFatal(t, err) + checkFatal(t, createBranch(repo, "branch-a")) + checkFatal(t, createBranch(repo, "branch-b")) + + checkFatal(t, repo.SetHead("refs/heads/branch-a")) + _, err = commitSomething(repo, "common-file", "1\nb\nc\n", commitOptions{}) + checkFatal(t, err) + + checkFatal(t, repo.SetHead("refs/heads/branch-b")) + _, err = commitSomething(repo, "common-file", "x\nb\nc\n", commitOptions{}) + checkFatal(t, err) + + branchA, err := repo.LookupBranch("branch-a", BranchLocal) + checkFatal(t, err) + onto, err := repo.AnnotatedCommitFromRef(branchA.Reference) + checkFatal(t, err) + + // We then rebase "branch-b" onto "branch-a" in-memory, which should result + // in a conflict. + rebase, err := repo.InitRebase(nil, nil, onto, &RebaseOptions{InMemory: 1}) + checkFatal(t, err) + + _, err = rebase.Next() + checkFatal(t, err) + + index, err := rebase.InmemoryIndex() + checkFatal(t, err) + + // We simply resolve the conflict and commit the rebase. + if !index.HasConflicts() { + t.Fatal("expected index to have conflicts") + } + + conflict, err := index.Conflict("common-file") + checkFatal(t, err) + + resolvedBlobID, err := repo.CreateBlobFromBuffer([]byte("resolved contents")) + checkFatal(t, err) + + resolvedEntry := *conflict.Our + resolvedEntry.Id = resolvedBlobID + checkFatal(t, index.Add(&resolvedEntry)) + checkFatal(t, index.RemoveConflict("common-file")) + + var commitID Oid + checkFatal(t, rebase.Commit(&commitID, signature(), signature(), "rebased message")) + checkFatal(t, rebase.Finish()) + + // And then assert that we can look up the new merge commit, and that the + // "common-file" has the expected contents. + commit, err := repo.LookupCommit(&commitID) + checkFatal(t, err) + if commit.Message() != "rebased message" { + t.Fatalf("unexpected commit message %q", commit.Message()) + } + + tree, err := commit.Tree() + checkFatal(t, err) + + blob, err := repo.LookupBlob(tree.EntryByName("common-file").Id) + checkFatal(t, err) + if string(blob.Contents()) != "resolved contents" { + t.Fatalf("unexpected resolved blob contents %q", string(blob.Contents())) + } +} + func TestRebaseAbort(t *testing.T) { // TEST DATA From a53cd8b51277b3a30153ee407d23c58258478ff2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Feb 2022 19:31:34 -0800 Subject: [PATCH 10/10] Add refspec bindings (#898) (#907) This add support for the parse, access, and transform functions for refspec objects. (cherry picked from commit eae00773cce87d5282a8ac7c10b5c1961ee6f9cb) Co-authored-by: William Bain --- refspec.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ refspec_test.go | 75 ++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 refspec.go create mode 100644 refspec_test.go diff --git a/refspec.go b/refspec.go new file mode 100644 index 00000000..450e5afc --- /dev/null +++ b/refspec.go @@ -0,0 +1,149 @@ +package git + +/* +#include +*/ +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 +} diff --git a/refspec_test.go b/refspec_test.go new file mode 100644 index 00000000..70641989 --- /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) + } +}