From aabb68096820ed966be66f29bb147a643ec52ca9 Mon Sep 17 00:00:00 2001 From: aaronggao Date: Tue, 31 Mar 2026 20:26:04 +0800 Subject: [PATCH] feat: upgrade git2go bindings to libgit2 v1.9 Breaking API changes: - git_blame_get_hunk_byindex -> git_blame_hunk_byindex - git_blame_get_hunk_byline -> git_blame_hunk_byline - git_blame_get_hunk_count -> git_blame_hunkcount - git_indexer_hash -> git_indexer_name (returns string instead of git_oid) - Add #include for git_error_set_str New bindings: - HomeDir / SetHomeDir - ServerConnectTimeout / SetServerConnectTimeout - ServerTimeout / SetServerTimeout - Mempack.ObjectCount / Mempack.WriteThinPack - ConfigEntry.BackendType / ConfigEntry.OriginPath - Repository.CreateCommitFromStage with CommitCreateOptions - Repository.CommitParents - Repository.DefaultSignatureFromEnv - StashCollection.SaveWithOptions with pathspec support - CheckoutDryRun / CheckoutConflictStyleZdiff3 Bug fixes: - Fix recursive fmt.Sprintf in CredentialType.String() Test updates: - Replace hardcoded "master" branch name with dynamic defaultBranchName() helper to support systems where default branch is "main" - Add tests for all new bindings in corresponding test files - Split new API tests into settings_test.go, mempack_test.go, config_test.go, commit_test.go, repository_test.go, stash_test.go, and signature_test.go --- Build_bundled_static.go | 4 +- Build_system_dynamic.go | 4 +- Build_system_static.go | 4 +- README.md | 14 +++--- blame.go | 12 +++-- blame_test.go | 6 +++ branch_test.go | 10 +++-- checkout.go | 6 ++- clone_test.go | 5 ++- commit.go | 68 ++++++++++++++++++++++++++++ commit_test.go | 76 +++++++++++++++++++++++++++++++ config.go | 21 ++++++--- config_test.go | 24 ++++++++++ credentials.go | 2 +- deprecated.go | 28 ++++++++++++ describe_test.go | 3 +- features.go | 24 ++++++++++ git.go | 15 +++++++ git_test.go | 12 +++++ go.mod | 7 +-- indexer.go | 11 +++-- mempack.go | 32 +++++++++++++ mempack_test.go | 55 +++++++++++++++++++++++ merge_test.go | 11 +++-- push_test.go | 7 +-- reference_test.go | 6 ++- refspec.go | 7 +++ remote.go | 77 +++++++++++++++++++++++++++++--- repository.go | 38 ++++++++++++++++ repository_test.go | 28 ++++++++++++ revparse_test.go | 5 ++- script/build-libgit2.sh | 4 +- settings.go | 99 +++++++++++++++++++++++++++++++++++++++++ settings_test.go | 67 ++++++++++++++++++++++++++++ signature.go | 35 +++++++++++++++ signature_test.go | 37 +++++++++++++++ stash.go | 56 +++++++++++++++++++++++ stash_test.go | 41 +++++++++++++++-- vendor/libgit2 | 1 - wrapper.c | 23 +++++++++- 40 files changed, 923 insertions(+), 62 deletions(-) create mode 100644 commit_test.go create mode 100644 signature_test.go delete mode 160000 vendor/libgit2 diff --git a/Build_bundled_static.go b/Build_bundled_static.go index 9af2d9543..c5dc7b9e7 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 < 5 || LIBGIT2_VER_MINOR > 5 -# error "Invalid libgit2 version; this git2go supports libgit2 between v1.5.0 and v1.5.0" +#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 9 || LIBGIT2_VER_MINOR > 9 +# error "Invalid libgit2 version; this git2go supports libgit2 between v1.9.0 and v1.9.x" #endif */ import "C" diff --git a/Build_system_dynamic.go b/Build_system_dynamic.go index bc423b594..537c6db08 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 < 5 || LIBGIT2_VER_MINOR > 5 -# error "Invalid libgit2 version; this git2go supports libgit2 between v1.5.0 and v1.5.0" +#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 9 || LIBGIT2_VER_MINOR > 9 +# error "Invalid libgit2 version; this git2go supports libgit2 between v1.9.0 and v1.9.x" #endif */ import "C" diff --git a/Build_system_static.go b/Build_system_static.go index 7038e93d9..4b92d7a38 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 < 5 || LIBGIT2_VER_MINOR > 5 -# error "Invalid libgit2 version; this git2go supports libgit2 between v1.5.0 and v1.5.0" +#if LIBGIT2_VER_MAJOR != 1 || LIBGIT2_VER_MINOR < 9 || LIBGIT2_VER_MINOR > 9 +# error "Invalid libgit2 version; this git2go supports libgit2 between v1.9.0 and v1.9.x" #endif */ import "C" diff --git a/README.md b/README.md index 28165902f..df55a2b77 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/v34) [![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/v35) [![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/). @@ -10,7 +10,7 @@ Due to the fact that Go 1.11 module versions have semantic meaning and don't nec | libgit2 | git2go | |---------|---------------| -| main | (will be v35) | +| 1.9 | v35 | | 1.5 | v34 | | 1.3 | v33 | | 1.2 | v32 | @@ -20,13 +20,13 @@ Due to the fact that Go 1.11 module versions have semantic meaning and don't nec | 0.28 | v28 | | 0.27 | v27 | -You can import them in your project with the version's major number as a suffix. For example, if you have libgit2 v1.2 installed, you'd import git2go v34 with: +You can import them in your project with the version's major number as a suffix. For example, if you have libgit2 v1.9 installed, you'd import git2go v35 with: ```sh -go get github.com/libgit2/git2go/v34 +go get github.com/libgit2/git2go/v35 ``` ```go -import "github.com/libgit2/git2go/v34" +import "github.com/libgit2/git2go/v35" ``` which will ensure there are no sudden changes to the API. @@ -50,7 +50,7 @@ This project wraps the functionality provided by libgit2. If you're using a vers When linking dynamically against a released version of libgit2, install it via your system's package manager. CGo will take care of finding its pkg-config file and set up the linking. Import via Go modules, e.g. to work against libgit2 v1.2 ```go -import "github.com/libgit2/git2go/v34" +import "github.com/libgit2/git2go/v35" ``` ### Versioned branch, static linking @@ -80,7 +80,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/v34 => ../../libgit2/git2go + replace github.com/libgit2/git2go/v35 => ../../libgit2/git2go Parallelism and network operations ---------------------------------- diff --git a/blame.go b/blame.go index bed29b5f3..ab9b99067 100644 --- a/blame.go +++ b/blame.go @@ -92,14 +92,14 @@ type Blame struct { } func (blame *Blame) HunkCount() int { - ret := int(C.git_blame_get_hunk_count(blame.ptr)) + ret := int(C.git_blame_hunkcount(blame.ptr)) runtime.KeepAlive(blame) return ret } func (blame *Blame) HunkByIndex(index int) (BlameHunk, error) { - ptr := C.git_blame_get_hunk_byindex(blame.ptr, C.uint32_t(index)) + ptr := C.git_blame_hunk_byindex(blame.ptr, C.size_t(index)) runtime.KeepAlive(blame) if ptr == nil { return BlameHunk{}, ErrInvalid @@ -108,7 +108,7 @@ func (blame *Blame) HunkByIndex(index int) (BlameHunk, error) { } func (blame *Blame) HunkByLine(lineno int) (BlameHunk, error) { - ptr := C.git_blame_get_hunk_byline(blame.ptr, C.size_t(lineno)) + ptr := C.git_blame_hunk_byline(blame.ptr, C.size_t(lineno)) runtime.KeepAlive(blame) if ptr == nil { return BlameHunk{}, ErrInvalid @@ -144,10 +144,13 @@ type BlameHunk struct { FinalCommitId *Oid FinalStartLineNumber uint16 FinalSignature *Signature + FinalCommitter *Signature // The committer of final_commit_id (v1.9+) OrigCommitId *Oid OrigPath string OrigStartLineNumber uint16 OrigSignature *Signature + OrigCommitter *Signature // The committer of orig_commit_id (v1.9+) + Summary string // The commit summary (v1.9+) Boundary bool } @@ -157,10 +160,13 @@ func blameHunkFromC(hunk *C.git_blame_hunk) BlameHunk { FinalCommitId: newOidFromC(&hunk.final_commit_id), FinalStartLineNumber: uint16(hunk.final_start_line_number), FinalSignature: newSignatureFromC(hunk.final_signature), + FinalCommitter: newSignatureFromC(hunk.final_committer), OrigCommitId: newOidFromC(&hunk.orig_commit_id), OrigPath: C.GoString(hunk.orig_path), OrigStartLineNumber: uint16(hunk.orig_start_line_number), OrigSignature: newSignatureFromC(hunk.orig_signature), + OrigCommitter: newSignatureFromC(hunk.orig_committer), + Summary: C.GoString(hunk.summary), Boundary: hunk.boundary == 1, } } diff --git a/blame_test.go b/blame_test.go index ec96af778..0da4743dd 100644 --- a/blame_test.go +++ b/blame_test.go @@ -34,6 +34,7 @@ func TestBlame(t *testing.T) { OrigPath: "README", OrigStartLineNumber: 1, Boundary: true, + Summary: "This is a commit", } wantHunk2 := BlameHunk{ LinesInHunk: 2, @@ -43,6 +44,7 @@ func TestBlame(t *testing.T) { OrigPath: "README", OrigStartLineNumber: 2, Boundary: false, + Summary: "This is a commit", } hunk1, err := blame.HunkByIndex(0) @@ -67,6 +69,10 @@ func checkHunk(t *testing.T, label string, hunk, want BlameHunk) { want.FinalSignature = nil hunk.OrigSignature = nil want.OrigSignature = nil + hunk.FinalCommitter = nil + want.FinalCommitter = nil + hunk.OrigCommitter = nil + want.OrigCommitter = nil if !reflect.DeepEqual(hunk, want) { t.Fatalf("%s: got hunk %+v, want %+v", label, hunk, want) } diff --git a/branch_test.go b/branch_test.go index 56795a4f5..c26bd2094 100644 --- a/branch_test.go +++ b/branch_test.go @@ -16,8 +16,9 @@ func TestBranchIterator(t *testing.T) { b, bt, err := i.Next() checkFatal(t, err) - if name, _ := b.Name(); name != "master" { - t.Fatalf("expected master") + branchName := defaultBranchName(t, repo) + if name, _ := b.Name(); name != branchName { + t.Fatalf("expected %s, got %s", branchName, name) } else if bt != BranchLocal { t.Fatalf("expected BranchLocal, not %v", t) } @@ -57,7 +58,8 @@ func TestBranchIteratorEach(t *testing.T) { t.Fatalf("expect 1 branch, but it was %d\n", len(names)) } - if names[0] != "master" { - t.Fatalf("expect branch master, but it was %s\n", names[0]) + branchName := defaultBranchName(t, repo) + if names[0] != branchName { + t.Fatalf("expect branch %s, but it was %s\n", branchName, names[0]) } } diff --git a/checkout.go b/checkout.go index 89841a8a0..bd9e35891 100644 --- a/checkout.go +++ b/checkout.go @@ -24,8 +24,8 @@ const ( CheckoutNotifyIgnored CheckoutNotifyType = C.GIT_CHECKOUT_NOTIFY_IGNORED CheckoutNotifyAll CheckoutNotifyType = C.GIT_CHECKOUT_NOTIFY_ALL - CheckoutNone CheckoutStrategy = C.GIT_CHECKOUT_NONE // Dry run, no actual updates - CheckoutSafe CheckoutStrategy = C.GIT_CHECKOUT_SAFE // Allow safe updates that cannot overwrite uncommitted data + CheckoutNone CheckoutStrategy = C.GIT_CHECKOUT_NONE // Do not do a checkout and do not fire callbacks + CheckoutSafe CheckoutStrategy = C.GIT_CHECKOUT_SAFE // Allow safe updates that cannot overwrite uncommitted data (default) CheckoutForce CheckoutStrategy = C.GIT_CHECKOUT_FORCE // Allow all updates to force working directory to look like index CheckoutRecreateMissing CheckoutStrategy = C.GIT_CHECKOUT_RECREATE_MISSING // Allow checkout to recreate missing files CheckoutAllowConflicts CheckoutStrategy = C.GIT_CHECKOUT_ALLOW_CONFLICTS // Allow checkout to make safe updates even if conflicts are found @@ -44,6 +44,8 @@ const ( CheckoutConflictStyleDiff3 CheckoutStrategy = C.GIT_CHECKOUT_CONFLICT_STYLE_DIFF3 // Include common ancestor data in diff3 format files for conflicts CheckoutDontRemoveExisting CheckoutStrategy = C.GIT_CHECKOUT_DONT_REMOVE_EXISTING // Don't overwrite existing files or folders CheckoutDontWriteIndex CheckoutStrategy = C.GIT_CHECKOUT_DONT_WRITE_INDEX // Normally checkout writes the index upon completion; this prevents that + CheckoutDryRun CheckoutStrategy = C.GIT_CHECKOUT_DRY_RUN // Dry run: check for conflicts but don't make changes + CheckoutConflictStyleZdiff3 CheckoutStrategy = C.GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3 // Include common ancestor data in zdiff3 format for conflicts CheckoutUpdateSubmodules CheckoutStrategy = C.GIT_CHECKOUT_UPDATE_SUBMODULES // Recursively checkout submodules with same options (NOT IMPLEMENTED) CheckoutUpdateSubmodulesIfChanged CheckoutStrategy = C.GIT_CHECKOUT_UPDATE_SUBMODULES_IF_CHANGED // Recursively checkout submodules if HEAD moved in super repo (NOT IMPLEMENTED) ) diff --git a/clone_test.go b/clone_test.go index 9ff03e673..a50079d59 100644 --- a/clone_test.go +++ b/clone_test.go @@ -20,7 +20,8 @@ func TestClone(t *testing.T) { path, err := ioutil.TempDir("", "git2go") checkFatal(t, err) - ref, err := repo.References.Lookup("refs/heads/master") + branchName := defaultBranchName(t, repo) + ref, err := repo.References.Lookup("refs/heads/" + branchName) checkFatal(t, err) repo2, err := Clone(repo.Path(), path, &CloneOptions{Bare: true}) @@ -28,7 +29,7 @@ func TestClone(t *testing.T) { checkFatal(t, err) - ref2, err := repo2.References.Lookup("refs/heads/master") + ref2, err := repo2.References.Lookup("refs/heads/" + branchName) checkFatal(t, err) if ref.Cmp(ref2) != 0 { diff --git a/commit.go b/commit.go index 9a09fe775..446899b1b 100644 --- a/commit.go +++ b/commit.go @@ -4,6 +4,10 @@ package git #include extern int _go_git_treewalk(git_tree *tree, git_treewalk_mode mode, void *ptr); + +static void _go_git_commit_create_options_set_allow_empty(git_commit_create_options *opts, int allow) { + opts->allow_empty_commit = allow ? 1 : 0; +} */ import "C" @@ -227,3 +231,67 @@ func (c *Commit) Amend(refname string, author, committer *Signature, message str return oid, nil } + +// CommitCreateOptions contains options for creating a commit from stage. +type CommitCreateOptions struct { + // If set, allow an empty commit (no changes from parent). + AllowEmptyCommit bool + // The commit author, or nil for the default. + Author *Signature + // The committer, or nil for the default. + Committer *Signature + // Encoding for the commit message; leave empty for default (UTF-8). + MessageEncoding string +} + +// CreateCommitFromStage commits the staged changes in the repository. +// This is a near analog to `git commit -m message`. +// By default, empty commits are not allowed. +func (v *Repository) CreateCommitFromStage(message string, opts *CommitCreateOptions) (*Oid, error) { + oid := new(Oid) + + cmsg := C.CString(message) + defer C.free(unsafe.Pointer(cmsg)) + + var copts C.git_commit_create_options + copts.version = C.GIT_COMMIT_CREATE_OPTIONS_VERSION + + var authorSig *C.git_signature + var committerSig *C.git_signature + + if opts != nil { + if opts.AllowEmptyCommit { + C._go_git_commit_create_options_set_allow_empty(&copts, 1) + } + if opts.Author != nil { + authorSig, _ = opts.Author.toC() + if authorSig != nil { + defer C.git_signature_free(authorSig) + copts.author = authorSig + } + } + if opts.Committer != nil { + committerSig, _ = opts.Committer.toC() + if committerSig != nil { + defer C.git_signature_free(committerSig) + copts.committer = committerSig + } + } + if opts.MessageEncoding != "" { + cenc := C.CString(opts.MessageEncoding) + defer C.free(unsafe.Pointer(cenc)) + copts.message_encoding = cenc + } + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_commit_create_from_stage(oid.toC(), v.ptr, cmsg, &copts) + runtime.KeepAlive(v) + if ret < 0 { + return nil, MakeGitError(ret) + } + + return oid, nil +} diff --git a/commit_test.go b/commit_test.go new file mode 100644 index 000000000..7207b8106 --- /dev/null +++ b/commit_test.go @@ -0,0 +1,76 @@ +package git + +import ( + "testing" +) + +func TestCreateCommitFromStage(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + // Configure user for the repo + cfg, err := repo.Config() + checkFatal(t, err) + defer cfg.Free() + err = cfg.SetString("user.name", "Test User") + checkFatal(t, err) + err = cfg.SetString("user.email", "test@example.com") + checkFatal(t, err) + + // Stage a file + idx, err := repo.Index() + checkFatal(t, err) + err = idx.AddByPath("README") + checkFatal(t, err) + err = idx.Write() + checkFatal(t, err) + + // Create commit from stage + oid, err := repo.CreateCommitFromStage("initial commit from stage", nil) + checkFatal(t, err) + + if oid == nil || oid.IsZero() { + t.Fatal("expected a valid commit OID") + } + + // Verify the commit + commit, err := repo.LookupCommit(oid) + checkFatal(t, err) + defer commit.Free() + + if commit.Message() != "initial commit from stage" { + t.Fatalf("expected 'initial commit from stage', got %q", commit.Message()) + } +} + +func TestCreateCommitFromStageAllowEmpty(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + cfg, err := repo.Config() + checkFatal(t, err) + defer cfg.Free() + err = cfg.SetString("user.name", "Test User") + checkFatal(t, err) + err = cfg.SetString("user.email", "test@example.com") + checkFatal(t, err) + + seedTestRepo(t, repo) + + // Try empty commit without AllowEmptyCommit — should fail + _, err = repo.CreateCommitFromStage("empty commit", nil) + if err == nil { + t.Fatal("expected error for empty commit without AllowEmptyCommit") + } + + // Try with AllowEmptyCommit + oid, err := repo.CreateCommitFromStage("empty commit", &CommitCreateOptions{ + AllowEmptyCommit: true, + }) + checkFatal(t, err) + if oid == nil || oid.IsZero() { + t.Fatal("expected a valid commit OID for empty commit") + } +} diff --git a/config.go b/config.go index bca4d180b..3147ad3f9 100644 --- a/config.go +++ b/config.go @@ -29,6 +29,9 @@ const ( // non-bare repos ConfigLevelLocal ConfigLevel = C.GIT_CONFIG_LEVEL_LOCAL + // Worktree-specific configuration; typically $GIT_DIR/config.worktree + ConfigLevelWorktree ConfigLevel = C.GIT_CONFIG_LEVEL_WORKTREE + // Application specific configuration file; freely defined by applications ConfigLevelApp ConfigLevel = C.GIT_CONFIG_LEVEL_APP @@ -38,16 +41,20 @@ const ( ) type ConfigEntry struct { - Name string - Value string - Level ConfigLevel + Name string + Value string + Level ConfigLevel + BackendType string // The type of backend that this entry exists in (e.g. "file") + OriginPath string // The path to the origin of this entry } func newConfigEntryFromC(centry *C.git_config_entry) *ConfigEntry { return &ConfigEntry{ - Name: C.GoString(centry.name), - Value: C.GoString(centry.value), - Level: ConfigLevel(centry.level), + Name: C.GoString(centry.name), + Value: C.GoString(centry.value), + Level: ConfigLevel(centry.level), + BackendType: C.GoString(centry.backend_type), + OriginPath: C.GoString(centry.origin_path), } } @@ -387,7 +394,7 @@ func (iter *ConfigIterator) Next() (*ConfigEntry, error) { func (iter *ConfigIterator) Free() { runtime.SetFinalizer(iter, nil) - C.free(unsafe.Pointer(iter.ptr)) + C.git_config_iterator_free(iter.ptr) } func ConfigFindGlobal() (string, error) { diff --git a/config_test.go b/config_test.go index f942dcbbc..760be4100 100644 --- a/config_test.go +++ b/config_test.go @@ -117,3 +117,27 @@ func TestOpenDefault(t *testing.T) { } defer c.Free() } + +func TestConfigEntryBackendType(t *testing.T) { + t.Parallel() + + cfg, err := setupConfig() + defer cleanupConfig() + checkFatal(t, err) + defer cfg.Free() + + iter, err := cfg.NewIterator() + checkFatal(t, err) + + entry, err := iter.Next() + checkFatal(t, err) + + // A file-backed config should have backend_type = "file" + if entry.BackendType != "file" { + t.Errorf("expected BackendType 'file', got %q", entry.BackendType) + } + + if entry.OriginPath == "" { + t.Error("expected non-empty OriginPath for file-backed config") + } +} diff --git a/credentials.go b/credentials.go index 2fb65d54b..4c8f09231 100644 --- a/credentials.go +++ b/credentials.go @@ -73,7 +73,7 @@ func (t CredentialType) String() string { } if t != 0 { - parts = append(parts, fmt.Sprintf("CredentialType(%#x)", t)) + parts = append(parts, fmt.Sprintf("CredentialType(%#x)", uint(t))) } return strings.Join(parts, "|") diff --git a/deprecated.go b/deprecated.go index 01253eecb..c52a491e7 100644 --- a/deprecated.go +++ b/deprecated.go @@ -269,3 +269,31 @@ func CallbackGitTreeWalk(_root *C.char, entry *C.git_tree_entry, ptr unsafe.Poin } return C.int(ErrorCodeOK) } + +// git.go (v35 deprecations) + +const ( + // Deprecated: ErrClassWorktree is a deprecated alias of ErrorClassWorktree. + ErrClassWorktree = ErrorClassWorktree + // Deprecated: ErrClassSHA is a deprecated alias of ErrorClassSHA. + ErrClassSHA = ErrorClassSHA + // Deprecated: ErrClassHTTP is a deprecated alias of ErrorClassHTTP. + ErrClassHTTP = ErrorClassHTTP + // Deprecated: ErrClassInternal is a deprecated alias of ErrorClassInternal. + ErrClassInternal = ErrorClassInternal + // Deprecated: ErrClassGrafts is a deprecated alias of ErrorClassGrafts. + ErrClassGrafts = ErrorClassGrafts +) + +const ( + // Deprecated: ErrOwner is a deprecated alias of ErrorCodeOwner. + ErrOwner = ErrorCodeOwner + // Deprecated: ErrTimeout is a deprecated alias of ErrorCodeTimeout. + ErrTimeout = ErrorCodeTimeout + // Deprecated: ErrUnchanged is a deprecated alias of ErrorCodeUnchanged. + ErrUnchanged = ErrorCodeUnchanged + // Deprecated: ErrNotSupported is a deprecated alias of ErrorCodeNotSupported. + ErrNotSupported = ErrorCodeNotSupported + // Deprecated: ErrReadOnly is a deprecated alias of ErrorCodeReadOnly. + ErrReadOnly = ErrorCodeReadOnly +) diff --git a/describe_test.go b/describe_test.go index 3181f230d..18f7af8c1 100644 --- a/describe_test.go +++ b/describe_test.go @@ -78,7 +78,8 @@ func TestDescribeCommit(t *testing.T) { checkFatal(t, err) resultStr, err = result.Format(&formatOpts) checkFatal(t, err) - compareStrings(t, "heads/master", resultStr) + branchName := defaultBranchName(t, repo) + compareStrings(t, "heads/"+branchName, resultStr) repo.CreateBranch("hotfix", commit, false) diff --git a/features.go b/features.go index a9f81a12b..47d99c377 100644 --- a/features.go +++ b/features.go @@ -19,6 +19,30 @@ const ( // libgit2 was built with nanosecond support for files FeatureNSec Feature = C.GIT_FEATURE_NSEC + + // HTTP parsing; always available + FeatureHTTPParser Feature = C.GIT_FEATURE_HTTP_PARSER + + // Regular expression support; always available + FeatureRegex Feature = C.GIT_FEATURE_REGEX + + // Internationalization support for filename translation + FeatureI18N Feature = C.GIT_FEATURE_I18N + + // NTLM support over HTTPS + FeatureAuthNTLM Feature = C.GIT_FEATURE_AUTH_NTLM + + // Kerberos (SPNEGO) authentication support over HTTPS + FeatureAuthNegotiate Feature = C.GIT_FEATURE_AUTH_NEGOTIATE + + // zlib support; always available + FeatureCompression Feature = C.GIT_FEATURE_COMPRESSION + + // SHA1 object support; always available + FeatureSHA1 Feature = C.GIT_FEATURE_SHA1 + + // SHA256 object support + FeatureSHA256 Feature = C.GIT_FEATURE_SHA256 ) // Features returns a bit-flag of Feature values indicating which features the diff --git a/git.go b/git.go index b7c8b3c7d..8755aa646 100644 --- a/git.go +++ b/git.go @@ -47,6 +47,11 @@ const ( ErrorClassCallback ErrorClass = C.GIT_ERROR_CALLBACK ErrorClassRebase ErrorClass = C.GIT_ERROR_REBASE ErrorClassPatch ErrorClass = C.GIT_ERROR_PATCH + ErrorClassWorktree ErrorClass = C.GIT_ERROR_WORKTREE + ErrorClassSHA ErrorClass = C.GIT_ERROR_SHA + ErrorClassHTTP ErrorClass = C.GIT_ERROR_HTTP + ErrorClassInternal ErrorClass = C.GIT_ERROR_INTERNAL + ErrorClassGrafts ErrorClass = C.GIT_ERROR_GRAFTS ) //go:generate stringer -type ErrorCode -trimprefix ErrorCode -tags static @@ -119,6 +124,16 @@ const ( ErrorCodeIndexDirty ErrorCode = C.GIT_EINDEXDIRTY // ErrorCodeApplyFail represents that a patch application failed. ErrorCodeApplyFail ErrorCode = C.GIT_EAPPLYFAIL + // ErrorCodeOwner represents that the object is not owned by the current user. + ErrorCodeOwner ErrorCode = C.GIT_EOWNER + // ErrorCodeTimeout represents that the operation timed out. + ErrorCodeTimeout ErrorCode = C.GIT_TIMEOUT + // ErrorCodeUnchanged represents that there were no changes. + ErrorCodeUnchanged ErrorCode = C.GIT_EUNCHANGED + // ErrorCodeNotSupported represents that an option is not supported. + ErrorCodeNotSupported ErrorCode = C.GIT_ENOTSUPPORTED + // ErrorCodeReadOnly represents that the subject is read-only. + ErrorCodeReadOnly ErrorCode = C.GIT_EREADONLY ) var ( diff --git a/git_test.go b/git_test.go index 592e06fe5..ac11b1401 100644 --- a/git_test.go +++ b/git_test.go @@ -197,3 +197,15 @@ func TestEmptyOid(t *testing.T) { t.Fatal("Should have returned invalid error") } } + +// defaultBranchName returns the default branch name for the given repo +// (typically "master" or "main" depending on git config). +func defaultBranchName(t *testing.T, repo *Repository) string { + head, err := repo.Head() + checkFatal(t, err) + defer head.Free() + + branch, err := head.Branch().Name() + checkFatal(t, err) + return branch +} diff --git a/go.mod b/go.mod index ed6d20cce..aabaa7061 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ -module github.com/libgit2/git2go/v34 +module github.com/libgit2/git2go/v35 -go 1.13 +go 1.18 require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c - golang.org/x/sys v0.0.0-20201204225414-ed752295db88 // indirect ) + +require golang.org/x/sys v0.0.0-20201204225414-ed752295db88 // indirect diff --git a/indexer.go b/indexer.go index 53ac420c4..9a03d91d3 100644 --- a/indexer.go +++ b/indexer.go @@ -3,7 +3,7 @@ package git /* #include -extern const git_oid * git_indexer_hash(const git_indexer *idx); +extern const char * git_indexer_name(const git_indexer *idx); extern int git_indexer_append(git_indexer *idx, const void *data, size_t size, git_transfer_progress *stats); extern int git_indexer_commit(git_indexer *idx, git_transfer_progress *stats); extern int _go_git_indexer_new(git_indexer **out, const char *path, unsigned int mode, git_odb *odb, void *progress_cb_payload); @@ -84,9 +84,14 @@ func (indexer *Indexer) Commit() (*Oid, error) { return nil, MakeGitError(ret) } - id := newOidFromC(C.git_indexer_hash(indexer.ptr)) + name := C.GoString(C.git_indexer_name(indexer.ptr)) runtime.KeepAlive(indexer) - return id, nil + + oid, err := NewOid(name) + if err != nil { + return nil, err + } + return oid, nil } // Free frees the indexer and its resources. diff --git a/mempack.go b/mempack.go index 14898f987..6f3a6819d 100644 --- a/mempack.go +++ b/mempack.go @@ -3,6 +3,7 @@ package git /* #include #include +#include extern int git_mempack_new(git_odb_backend **out); extern int git_mempack_dump(git_buf *pack, git_repository *repo, git_odb_backend *backend); @@ -90,3 +91,34 @@ func (mempack *Mempack) Reset() error { } return nil } + +// WriteThinPack writes a thin packfile with the objects in the memory store +// into the given packbuilder. A thin packfile does not contain its transitive +// closure of references. This does not reset the in-memory object database. +func (mempack *Mempack) WriteThinPack(pb *Packbuilder) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_mempack_write_thin_pack(mempack.ptr, pb.ptr) + runtime.KeepAlive(mempack) + runtime.KeepAlive(pb) + if ret < 0 { + return MakeGitError(ret) + } + return nil +} + +// ObjectCount returns the total number of objects in the mempack. +func (mempack *Mempack) ObjectCount() (uint, error) { + var count C.size_t + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_mempack_object_count(&count, mempack.ptr) + runtime.KeepAlive(mempack) + if ret < 0 { + return 0, MakeGitError(ret) + } + return uint(count), nil +} diff --git a/mempack_test.go b/mempack_test.go index 9bbfea248..e22b04507 100644 --- a/mempack_test.go +++ b/mempack_test.go @@ -58,3 +58,58 @@ func TestMempack(t *testing.T) { } } } + +func TestMempackObjectCount(t *testing.T) { + t.Parallel() + + odb, err := NewOdb() + checkFatal(t, err) + + mempack, err := NewMempack(odb) + checkFatal(t, err) + + count, err := mempack.ObjectCount() + checkFatal(t, err) + if count != 0 { + t.Fatalf("expected 0 objects, got %d", count) + } + + _, err = odb.Write([]byte("hello, world!"), ObjectBlob) + checkFatal(t, err) + + count, err = mempack.ObjectCount() + checkFatal(t, err) + if count != 1 { + t.Fatalf("expected 1 object, got %d", count) + } +} + +func TestMempackWriteThinPack(t *testing.T) { + t.Parallel() + + odb, err := NewOdb() + checkFatal(t, err) + + repo, err := NewRepositoryWrapOdb(odb) + checkFatal(t, err) + + mempack, err := NewMempack(odb) + checkFatal(t, err) + + blobId, err := odb.Write([]byte("thin pack content"), ObjectBlob) + checkFatal(t, err) + + pb, err := repo.NewPackbuilder() + checkFatal(t, err) + defer pb.Free() + + err = pb.Insert(blobId, "") + checkFatal(t, err) + + err = mempack.WriteThinPack(pb) + checkFatal(t, err) + + if pb.ObjectCount() == 0 { + t.Fatal("expected some objects in the packbuilder") + } +} diff --git a/merge_test.go b/merge_test.go index 72b1a263a..5a94cdfcd 100644 --- a/merge_test.go +++ b/merge_test.go @@ -12,7 +12,8 @@ func TestAnnotatedCommitFromRevspec(t *testing.T) { seedTestRepo(t, repo) - mergeHead, err := repo.AnnotatedCommitFromRevspec("refs/heads/master") + branchName := defaultBranchName(t, repo) + mergeHead, err := repo.AnnotatedCommitFromRevspec("refs/heads/" + branchName) checkFatal(t, err) expectedId := "473bf778b67b6d53e2ab289e0f1a2e8addef2fc2" @@ -28,7 +29,8 @@ func TestMergeWithSelf(t *testing.T) { seedTestRepo(t, repo) - master, err := repo.References.Lookup("refs/heads/master") + branchName := defaultBranchName(t, repo) + master, err := repo.References.Lookup("refs/heads/" + branchName) checkFatal(t, err) mergeHead, err := repo.AnnotatedCommitFromRef(master) @@ -47,7 +49,7 @@ func TestMergeWithSelf(t *testing.T) { mergeMessage, err := repo.Message() checkFatal(t, err) - expectedMessage := "Merge branch 'master'\n" + expectedMessage := "Merge branch '" + branchName + "'\n" if mergeMessage != expectedMessage { t.Errorf("merge Message = %v, want %v", mergeMessage, expectedMessage) } @@ -60,7 +62,8 @@ func TestMergeAnalysisWithSelf(t *testing.T) { seedTestRepo(t, repo) - master, err := repo.References.Lookup("refs/heads/master") + branchName := defaultBranchName(t, repo) + master, err := repo.References.Lookup("refs/heads/" + branchName) checkFatal(t, err) mergeHead, err := repo.AnnotatedCommitFromRef(master) diff --git a/push_test.go b/push_test.go index 311b86d14..17be6ae23 100644 --- a/push_test.go +++ b/push_test.go @@ -18,14 +18,15 @@ func TestRemotePush(t *testing.T) { seedTestRepo(t, localRepo) - err = remote.Push([]string{"refs/heads/master"}, nil) + branchName := defaultBranchName(t, localRepo) + err = remote.Push([]string{"refs/heads/" + branchName}, nil) checkFatal(t, err) - ref, err := localRepo.References.Lookup("refs/remotes/test_push/master") + ref, err := localRepo.References.Lookup("refs/remotes/test_push/" + branchName) checkFatal(t, err) defer ref.Free() - ref, err = repo.References.Lookup("refs/heads/master") + ref, err = repo.References.Lookup("refs/heads/" + branchName) checkFatal(t, err) defer ref.Free() } diff --git a/reference_test.go b/reference_test.go index 46c9e7ff0..3486160fc 100644 --- a/reference_test.go +++ b/reference_test.go @@ -92,8 +92,12 @@ func TestReferenceIterator(t *testing.T) { checkFatal(t, err) var list []string + headRef, err := repo.Head() + checkFatal(t, err) + defer headRef.Free() + headBranchName := headRef.Name() expected := []string{ - "refs/heads/master", + headBranchName, "refs/heads/one", "refs/heads/three", "refs/heads/two", diff --git a/refspec.go b/refspec.go index 450e5afc9..da3675971 100644 --- a/refspec.go +++ b/refspec.go @@ -14,6 +14,13 @@ type Refspec struct { ptr *C.git_refspec } +func newRefspecFromC(ptr *C.git_refspec) *Refspec { + if ptr == nil { + return nil + } + return &Refspec{ptr: ptr} +} + // ParseRefspec parses a given refspec string func ParseRefspec(input string, isFetch bool) (*Refspec, error) { var ptr *C.git_refspec diff --git a/remote.go b/remote.go index a294ca230..c55742aea 100644 --- a/remote.go +++ b/remote.go @@ -4,7 +4,7 @@ package git #include #include -#include +#include extern void _go_git_populate_remote_callbacks(git_remote_callbacks *callbacks); */ @@ -73,18 +73,37 @@ type TransportMessageCallback func(str string) error type CompletionCallback func(RemoteCompletion) error type CredentialsCallback func(url string, username_from_url string, allowed_types CredentialType) (*Credential, error) type TransferProgressCallback func(stats TransferProgress) error + +// Deprecated: UpdateTipsCallback is deprecated. Use UpdateRefsCallback instead. type UpdateTipsCallback func(refname string, a *Oid, b *Oid) error + +// UpdateRefsCallback is called for each updated reference on fetch/push. +// It provides more information than UpdateTipsCallback, including the refspec. +type UpdateRefsCallback func(refname string, a *Oid, b *Oid, spec *Refspec) error + type CertificateCheckCallback func(cert *Certificate, valid bool, hostname string) error type PackbuilderProgressCallback func(stage int32, current, total uint32) error type PushTransferProgressCallback func(current, total uint32, bytes uint) error type PushUpdateReferenceCallback func(refname, status string) error +// RemoteUpdateFlags controls how reference updates are handled. +type RemoteUpdateFlags uint + +const ( + // RemoteUpdateFetchhead writes the fetch results to FETCH_HEAD. + RemoteUpdateFetchhead RemoteUpdateFlags = C.GIT_REMOTE_UPDATE_FETCHHEAD + // RemoteUpdateReportUnchanged reports unchanged tips in the update_refs callback. + RemoteUpdateReportUnchanged RemoteUpdateFlags = C.GIT_REMOTE_UPDATE_REPORT_UNCHANGED +) + type RemoteCallbacks struct { SidebandProgressCallback TransportMessageCallback CompletionCallback CredentialsCallback TransferProgressCallback + // Deprecated: Use UpdateRefsCallback instead. UpdateTipsCallback + UpdateRefsCallback CertificateCheckCallback PackProgressCallback PackbuilderProgressCallback PushTransferProgressCallback @@ -128,9 +147,9 @@ type FetchOptions struct { RemoteCallbacks RemoteCallbacks // Whether to perform a prune after the fetch Prune FetchPrune - // Whether to write the results to FETCH_HEAD. Defaults to - // on. Leave this default in order to behave like git. - UpdateFetchhead bool + // How to handle reference updates; see RemoteUpdateFlags. + // Defaults to RemoteUpdateFetchhead. + UpdateFetchhead RemoteUpdateFlags // Determines how to behave regarding tags on the remote, such // as auto-downloading tags for objects we're downloading or @@ -144,6 +163,9 @@ type FetchOptions struct { // Proxy options to use for this fetch operation ProxyOptions ProxyOptions + + // Depth of the fetch to perform, or 0 for full history. + Depth int } type RemoteConnectOptions struct { @@ -305,6 +327,9 @@ type PushOptions struct { // Proxy options to use for this push operation ProxyOptions ProxyOptions + + // "Push options" to deliver to the remote. + RemotePushOptions []string } type RemoteHead struct { @@ -445,6 +470,36 @@ func updateTipsCallback( return C.int(ErrorCodeOK) } +//export updateRefsCallback +func updateRefsCallback( + errorMessage **C.char, + _refname *C.char, + _a *C.git_oid, + _b *C.git_oid, + _spec *C.git_refspec, + handle unsafe.Pointer, +) C.int { + data := pointerHandles.Get(handle).(*remoteCallbacksData) + if data.callbacks.UpdateRefsCallback == nil { + return C.int(ErrorCodeOK) + } + refname := C.GoString(_refname) + a := newOidFromC(_a) + b := newOidFromC(_b) + var spec *Refspec + if _spec != nil { + spec = newRefspecFromC(_spec) + } + err := data.callbacks.UpdateRefsCallback(refname, a, b, spec) + if err != nil { + if data.errorTarget != nil { + *data.errorTarget = err + } + return setCallbackError(errorMessage, err) + } + return C.int(ErrorCodeOK) +} + //export certificateCheckCallback func certificateCheckCallback( errorMessage **C.char, @@ -685,7 +740,7 @@ func (c *RemoteCollection) Create(name string, url string) (*Remote, error) { return remote, nil } -//CreateWithOptions Creates a repository object with extended options. +// CreateWithOptions Creates a repository object with extended options. func (c *RemoteCollection) CreateWithOptions(url string, option *RemoteCreateOptions) (*Remote, error) { remote := &Remote{repo: c.repo} @@ -983,8 +1038,9 @@ func populateFetchOptions(copts *C.git_fetch_options, opts *FetchOptions, errorT } populateRemoteCallbacks(&copts.callbacks, &opts.RemoteCallbacks, errorTarget) copts.prune = C.git_fetch_prune_t(opts.Prune) - copts.update_fetchhead = cbool(opts.UpdateFetchhead) + copts.update_fetchhead = C.uint(opts.UpdateFetchhead) copts.download_tags = C.git_remote_autotag_option_t(opts.DownloadTags) + copts.depth = C.int(opts.Depth) copts.custom_headers = C.git_strarray{ count: C.size_t(len(opts.Headers)), @@ -1014,6 +1070,12 @@ func populatePushOptions(copts *C.git_push_options, opts *PushOptions, errorTarg count: C.size_t(len(opts.Headers)), strings: makeCStringsFromStrings(opts.Headers), } + if len(opts.RemotePushOptions) > 0 { + copts.remote_push_options = C.git_strarray{ + count: C.size_t(len(opts.RemotePushOptions)), + strings: makeCStringsFromStrings(opts.RemotePushOptions), + } + } populateRemoteCallbacks(&copts.callbacks, &opts.RemoteCallbacks, errorTarget) populateProxyOptions(&copts.proxy_opts, &opts.ProxyOptions) return copts @@ -1025,6 +1087,9 @@ func freePushOptions(copts *C.git_push_options) { } untrackCallbacksPayload(&copts.callbacks) freeStrarray(&copts.custom_headers) + if copts.remote_push_options.count > 0 { + freeStrarray(&copts.remote_push_options) + } freeProxyOptions(&copts.proxy_opts) } diff --git a/repository.go b/repository.go index 5bdaacded..b201f5cfa 100644 --- a/repository.go +++ b/repository.go @@ -5,6 +5,10 @@ package git #include #include #include + +static git_commit *_go_git_commitarray_get(git_commitarray *array, size_t idx) { + return array->commits[idx]; +} */ import "C" import ( @@ -874,3 +878,37 @@ func (r *Repository) ItemPath(item RepositoryItem) (string, error) { } return C.GoString(c_buf.ptr), nil } + +// CommitParents gets the parents of the next commit, given the current +// repository state. Generally, this is the HEAD commit, except when +// performing a merge, in which case it is two or more commits. +func (v *Repository) CommitParents() ([]*Commit, error) { + var carray C.git_commitarray + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_repository_commit_parents(&carray, v.ptr) + runtime.KeepAlive(v) + if ret < 0 { + return nil, MakeGitError(ret) + } + defer C.git_commitarray_dispose(&carray) + + count := int(carray.count) + commits := make([]*Commit, count) + for i := 0; i < count; i++ { + ccommit := C._go_git_commitarray_get(&carray, C.size_t(i)) + var dup *C.git_commit + cErr := C.git_commit_dup(&dup, ccommit) + if cErr < 0 { + for j := 0; j < i; j++ { + commits[j].Free() + } + return nil, MakeGitError(cErr) + } + commits[i] = allocCommit(dup, v) + } + + return commits, nil +} diff --git a/repository_test.go b/repository_test.go index 60758c153..82bab97b8 100644 --- a/repository_test.go +++ b/repository_test.go @@ -157,3 +157,31 @@ func TestRepositoryItemPath(t *testing.T) { t.Error("expected not empty gitDir") } } + +func TestCommitParents(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + seedTestRepo(t, repo) + + parents, err := repo.CommitParents() + checkFatal(t, err) + + if len(parents) != 1 { + t.Fatalf("expected 1 parent, got %d", len(parents)) + } + + // The parent should be HEAD + head, err := repo.Head() + checkFatal(t, err) + defer head.Free() + + if !parents[0].Id().Equal(head.Target()) { + t.Fatalf("expected parent to be HEAD (%s), got %s", head.Target(), parents[0].Id()) + } + + for _, p := range parents { + p.Free() + } +} diff --git a/revparse_test.go b/revparse_test.go index 283543430..66bb0d020 100644 --- a/revparse_test.go +++ b/revparse_test.go @@ -37,10 +37,11 @@ func TestRevparseExt(t *testing.T) { _, treeId := seedTestRepo(t, repo) - ref, err := repo.References.Create("refs/heads/master", treeId, true, "") + branchName := defaultBranchName(t, repo) + ref, err := repo.References.Create("refs/heads/"+branchName, treeId, true, "") checkFatal(t, err) - obj, ref, err := repo.RevparseExt("master") + obj, ref, err := repo.RevparseExt(branchName) checkFatal(t, err) checkObject(t, obj, treeId) diff --git a/script/build-libgit2.sh b/script/build-libgit2.sh index dbc19e422..d691563c5 100755 --- a/script/build-libgit2.sh +++ b/script/build-libgit2.sh @@ -69,7 +69,7 @@ fi mkdir -p "${BUILD_PATH}/build" && cd "${BUILD_PATH}/build" && cmake -DTHREADSAFE=ON \ - -DBUILD_CLAR=OFF \ + -DBUILD_TESTS=OFF \ -DBUILD_SHARED_LIBS"=${BUILD_SHARED_LIBS}" \ -DREGEX_BACKEND=builtin \ -DUSE_BUNDLED_ZLIB="${USE_BUNDLED_ZLIB}" \ @@ -79,7 +79,7 @@ cmake -DTHREADSAFE=ON \ -DCMAKE_BUILD_TYPE="RelWithDebInfo" \ -DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \ -DCMAKE_INSTALL_LIBDIR="lib" \ - -DDEPRECATE_HARD="${BUILD_DEPRECATE_HARD}" \ + -DDEPRECATE_HARD="${BUILD_DEPRECATED_HARD}" \ "${VENDORED_PATH}" if which make nproc >/dev/null && [ -f Makefile ]; then diff --git a/settings.go b/settings.go index 7f9b5b125..c72e35970 100644 --- a/settings.go +++ b/settings.go @@ -32,6 +32,26 @@ int _go_git_opts_get_size_t_size_t(int opt, size_t *val1, size_t *val2) { return git_libgit2_opts(opt, val1, val2); } + +int _go_git_opts_get_homedir(git_buf *buf) +{ + return git_libgit2_opts(GIT_OPT_GET_HOMEDIR, buf); +} + +int _go_git_opts_set_homedir(const char *path) +{ + return git_libgit2_opts(GIT_OPT_SET_HOMEDIR, path); +} + +int _go_git_opts_get_int(int opt, int *val) +{ + return git_libgit2_opts(opt, val); +} + +int _go_git_opts_set_int(int opt, int val) +{ + return git_libgit2_opts(opt, val); +} */ import "C" import ( @@ -172,3 +192,82 @@ func setSizet(opt C.int, val int) error { return nil } + +// HomeDir returns the home directory used by libgit2 for file lookups. +func HomeDir() (string, error) { + var buf C.git_buf + defer C.git_buf_dispose(&buf) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := C._go_git_opts_get_homedir(&buf) + if err < 0 { + return "", MakeGitError(err) + } + + return C.GoString(buf.ptr), nil +} + +// SetHomeDir sets the directory used as the current user's home directory. +func SetHomeDir(path string) error { + cpath := C.CString(path) + defer C.free(unsafe.Pointer(cpath)) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := C._go_git_opts_set_homedir(cpath) + if err < 0 { + return MakeGitError(err) + } + + return nil +} + +func getInt(opt C.int) (int, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var val C.int + err := C._go_git_opts_get_int(opt, &val) + if err < 0 { + return 0, MakeGitError(err) + } + return int(val), nil +} + +func setInt(opt C.int, val int) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := C._go_git_opts_set_int(opt, C.int(val)) + if err < 0 { + return MakeGitError(err) + } + return nil +} + +// ServerConnectTimeout returns the timeout (in milliseconds) to attempt +// connections to a remote server. +func ServerConnectTimeout() (int, error) { + return getInt(C.GIT_OPT_GET_SERVER_CONNECT_TIMEOUT) +} + +// SetServerConnectTimeout sets the timeout (in milliseconds) to attempt +// connections to a remote server. Set to 0 to use the system default. +func SetServerConnectTimeout(timeout int) error { + return setInt(C.GIT_OPT_SET_SERVER_CONNECT_TIMEOUT, timeout) +} + +// ServerTimeout returns the timeout (in milliseconds) for reading from +// and writing to a remote server. +func ServerTimeout() (int, error) { + return getInt(C.GIT_OPT_GET_SERVER_TIMEOUT) +} + +// SetServerTimeout sets the timeout (in milliseconds) for reading from +// and writing to a remote server. Set to 0 to use the system default. +func SetServerTimeout(timeout int) error { + return setInt(C.GIT_OPT_SET_SERVER_TIMEOUT, timeout) +} diff --git a/settings_test.go b/settings_test.go index e3761d459..2700b2350 100644 --- a/settings_test.go +++ b/settings_test.go @@ -1,6 +1,8 @@ package git import ( + "io/ioutil" + "os" "testing" ) @@ -97,3 +99,68 @@ func TestSetCacheMaxSize(t *testing.T) { err = SetCacheMaxSize(256 * 1024 * 1024) checkFatal(t, err) } + +func TestHomeDir(t *testing.T) { + t.Parallel() + dir, err := HomeDir() + checkFatal(t, err) + + if dir == "" { + t.Fatal("HomeDir returned empty string") + } +} + +func TestSetHomeDir(t *testing.T) { + t.Parallel() + original, err := HomeDir() + checkFatal(t, err) + + tmpDir, err := ioutil.TempDir("", "git2go-homedir") + checkFatal(t, err) + defer os.RemoveAll(tmpDir) + + err = SetHomeDir(tmpDir) + checkFatal(t, err) + + actual, err := HomeDir() + checkFatal(t, err) + if actual != tmpDir { + t.Fatalf("expected %q, got %q", tmpDir, actual) + } + + // Restore original + err = SetHomeDir(original) + checkFatal(t, err) +} + +func TestServerConnectTimeout(t *testing.T) { + t.Parallel() + err := SetServerConnectTimeout(5000) + checkFatal(t, err) + + val, err := ServerConnectTimeout() + checkFatal(t, err) + if val != 5000 { + t.Fatalf("expected 5000, got %d", val) + } + + // Reset to default + err = SetServerConnectTimeout(0) + checkFatal(t, err) +} + +func TestServerTimeout(t *testing.T) { + t.Parallel() + err := SetServerTimeout(10000) + checkFatal(t, err) + + val, err := ServerTimeout() + checkFatal(t, err) + if val != 10000 { + t.Fatalf("expected 10000, got %d", val) + } + + // Reset to default + err = SetServerTimeout(0) + checkFatal(t, err) +} diff --git a/signature.go b/signature.go index dfd97189c..b51d89945 100644 --- a/signature.go +++ b/signature.go @@ -76,3 +76,38 @@ func (repo *Repository) DefaultSignature() (*Signature, error) { return newSignatureFromC(out), nil } + +// DefaultSignatureFromEnv creates default author and/or committer signatures +// using environment variables and configuration. +// +// Environment variables GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, +// GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL are honored, falling back +// to user.name and user.email configuration. For timestamps, +// GIT_AUTHOR_DATE and GIT_COMMITTER_DATE are used if set. +// +// Returns (author, committer, error). Either author or committer may be nil +// if not requested (pass false for the corresponding parameter). +func (repo *Repository) DefaultSignatureFromEnv() (author *Signature, committer *Signature, err error) { + var authorOut *C.git_signature + var committerOut *C.git_signature + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cErr := C.git_signature_default_from_env(&authorOut, &committerOut, repo.ptr) + runtime.KeepAlive(repo) + if cErr < 0 { + return nil, nil, MakeGitError(cErr) + } + + if authorOut != nil { + defer C.git_signature_free(authorOut) + author = newSignatureFromC(authorOut) + } + if committerOut != nil { + defer C.git_signature_free(committerOut) + committer = newSignatureFromC(committerOut) + } + + return author, committer, nil +} diff --git a/signature_test.go b/signature_test.go new file mode 100644 index 000000000..c492a9630 --- /dev/null +++ b/signature_test.go @@ -0,0 +1,37 @@ +package git + +import ( + "testing" +) + +func TestDefaultSignatureFromEnv(t *testing.T) { + t.Parallel() + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + // Configure user for the repo + cfg, err := repo.Config() + checkFatal(t, err) + defer cfg.Free() + err = cfg.SetString("user.name", "Test Author") + checkFatal(t, err) + err = cfg.SetString("user.email", "author@example.com") + checkFatal(t, err) + + author, committer, err := repo.DefaultSignatureFromEnv() + checkFatal(t, err) + + if author == nil { + t.Fatal("expected non-nil author") + } + if committer == nil { + t.Fatal("expected non-nil committer") + } + + if author.Name != "Test Author" { + t.Errorf("expected author name 'Test Author', got %q", author.Name) + } + if author.Email != "author@example.com" { + t.Errorf("expected author email 'author@example.com', got %q", author.Email) + } +} diff --git a/stash.go b/stash.go index 865ea91ea..f45e1b307 100644 --- a/stash.go +++ b/stash.go @@ -30,6 +30,10 @@ const ( // StashIncludeIgnored means all ignored files are also // stashed and then cleaned up from the working directory. StashIncludeIgnored StashFlag = C.GIT_STASH_INCLUDE_IGNORED + + // StashKeepAll means all changes in the index and working + // directory are left intact. + StashKeepAll StashFlag = C.GIT_STASH_KEEP_ALL ) // StashCollection represents the possible operations that can be @@ -71,6 +75,58 @@ func (c *StashCollection) Save( return oid, nil } +// StashSaveOptions contains options for stash save with options. +type StashSaveOptions struct { + Flags StashFlag + Stasher *Signature + Message string + Paths []string +} + +// SaveWithOptions saves the local modifications to a new stash with extended options. +// This allows stashing specific files using the Paths field. +func (c *StashCollection) SaveWithOptions(opts *StashSaveOptions) (*Oid, error) { + oid := new(Oid) + + var copts C.git_stash_save_options + C.git_stash_save_options_init(&copts, C.GIT_STASH_SAVE_OPTIONS_VERSION) + + if opts != nil { + copts.flags = C.uint32_t(opts.Flags) + + if opts.Stasher != nil { + stasherC, err := opts.Stasher.toC() + if err != nil { + return nil, err + } + defer C.git_signature_free(stasherC) + copts.stasher = stasherC + } + + if opts.Message != "" { + cmsg := C.CString(opts.Message) + defer C.free(unsafe.Pointer(cmsg)) + copts.message = cmsg + } + + if len(opts.Paths) > 0 { + copts.paths.strings = makeCStringsFromStrings(opts.Paths) + copts.paths.count = C.size_t(len(opts.Paths)) + defer freeStrarray(&copts.paths) + } + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ret := C.git_stash_save_with_opts(oid.toC(), c.repo.ptr, &copts) + runtime.KeepAlive(c) + if ret < 0 { + return nil, MakeGitError(ret) + } + return oid, nil +} + // StashApplyFlag are flags that affect the stash apply operation. type StashApplyFlag int diff --git a/stash_test.go b/stash_test.go index 62990b139..1dd336bb7 100644 --- a/stash_test.go +++ b/stash_test.go @@ -89,9 +89,10 @@ func TestStash(t *testing.T) { // Stash foreach + branchName := defaultBranchName(t, repo) expected := []stash{ - {0, "On master: Second stash", stash2.String()}, - {1, "On master: First stash", stash1.String()}, + {0, "On " + branchName + ": Second stash", stash2.String()}, + {1, "On " + branchName + ": First stash", stash1.String()}, } checkStashes(t, repo, expected) @@ -108,7 +109,7 @@ func TestStash(t *testing.T) { } expected = []stash{ - {0, "On master: Second stash", stash2.String()}, + {0, "On " + branchName + ": Second stash", stash2.String()}, } checkStashes(t, repo, expected) @@ -196,3 +197,37 @@ func fileExistsInRepo(repo *Repository, name string) bool { } return true } + +func TestStashSaveWithOptions(t *testing.T) { + repo := createTestRepo(t) + defer cleanupTestRepo(t, repo) + + prepareStashRepo(t, repo) + + sig := &Signature{ + Name: "Rand Om Hacker", + Email: "random@hacker.com", + When: time.Now(), + } + + // Stash only the README using pathspec + stashId, err := repo.Stashes.SaveWithOptions(&StashSaveOptions{ + Flags: StashDefault, + Stasher: sig, + Message: "Pathspec stash", + Paths: []string{"README"}, + }) + checkFatal(t, err) + + if stashId == nil || stashId.IsZero() { + t.Fatal("expected a valid stash OID") + } + + _, err = repo.LookupCommit(stashId) + checkFatal(t, err) + + // Untracked file should still exist since we only stashed README + if !fileExistsInRepo(repo, "untracked.txt") { + t.Error("untracked.txt should still exist when stashing by path") + } +} diff --git a/vendor/libgit2 b/vendor/libgit2 deleted file mode 160000 index fbea439d4..000000000 --- a/vendor/libgit2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fbea439d4b6fc91c6b619d01b85ab3b7746e4c19 diff --git a/wrapper.c b/wrapper.c index e999136dd..cacd1cf41 100644 --- a/wrapper.c +++ b/wrapper.c @@ -3,7 +3,8 @@ #include #include #include -#include +#include +#include // There are two ways in which to declare a callback: // @@ -296,6 +297,25 @@ static int update_tips_callback(const char *refname, const git_oid *a, const git return set_callback_error(error_message, ret); } +static int update_refs_callback( + const char *refname, + const git_oid *a, + const git_oid *b, + git_refspec *spec, + void *data) +{ + char *error_message = NULL; + const int ret = updateRefsCallback( + &error_message, + (char *)refname, + (git_oid *)a, + (git_oid *)b, + spec, + data + ); + return set_callback_error(error_message, ret); +} + static int certificate_check_callback(git_cert *cert, int valid, const char *host, void *data) { char *error_message = NULL; @@ -362,6 +382,7 @@ void _go_git_populate_remote_callbacks(git_remote_callbacks *callbacks) callbacks->pack_progress = pack_progress_callback; callbacks->push_transfer_progress = push_transfer_progress_callback; callbacks->push_update_reference = push_update_reference_callback; + callbacks->update_refs = update_refs_callback; } int _go_git_index_add_all(git_index *index, const git_strarray *pathspec, unsigned int flags, void *callback)