Merge pull request #1 from coveord/feature/DT-5224-worktree-api

Added the bindings for the Worktree API
for @wtrep
This commit is contained in:
Philippe Ballandras 2022-10-27 09:21:57 -04:00 committed by GitHub
commit 9232cbaf8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 500 additions and 0 deletions

View File

@ -36,6 +36,9 @@ type Repository struct {
// Stashes represents the collection of stashes and can be used to // Stashes represents the collection of stashes and can be used to
// save, apply and iterate over stash states in this repository. // save, apply and iterate over stash states in this repository.
Stashes StashCollection Stashes StashCollection
// Worktrees represents the collection of worktrees and can be used to
// add, list and remove worktrees for this repository
Worktrees WorktreeCollection
// weak indicates that a repository is a weak pointer and should not be // weak indicates that a repository is a weak pointer and should not be
// freed. // freed.
@ -52,6 +55,7 @@ func newRepositoryFromC(ptr *C.git_repository) *Repository {
repo.Notes.repo = repo repo.Notes.repo = repo
repo.Tags.repo = repo repo.Tags.repo = repo
repo.Stashes.repo = repo repo.Stashes.repo = repo
repo.Worktrees.repo = repo
runtime.SetFinalizer(repo, (*Repository).Free) runtime.SetFinalizer(repo, (*Repository).Free)

271
worktree.go Normal file
View File

@ -0,0 +1,271 @@
package git
/*
#include <git2.h>
*/
import "C"
import (
"runtime"
"unsafe"
)
type WorktreeCollection struct {
doNotCompare
repo *Repository
}
type Worktree struct {
doNotCompare
ptr *C.git_worktree
}
type AddWorktreeOptions struct {
// Lock the newly created worktree
Lock bool
// Reference to use for the new worktree
Reference *Reference
// CheckoutOptions is used for configuring the checkout for the newly created worktree
CheckoutOptions CheckoutOptions
}
// Add adds a new working tree for the given repository
func (c *WorktreeCollection) Add(name string, path string, options *AddWorktreeOptions) (*Worktree, error) {
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
var err error
cOptions := populateAddWorktreeOptions(&C.git_worktree_add_options{}, options, &err)
defer freeAddWorktreeOptions(cOptions)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var ptr *C.git_worktree
ret := C.git_worktree_add(&ptr, c.repo.ptr, cName, cPath, cOptions)
runtime.KeepAlive(c)
if options != nil && options.Reference != nil {
runtime.KeepAlive(options.Reference)
}
if ret == C.int(ErrorCodeUser) && err != nil {
return nil, err
} else if ret < 0 {
return nil, MakeGitError(ret)
}
return newWorktreeFromC(ptr), nil
}
// List lists names of linked working trees for the given repository
func (c *WorktreeCollection) List() ([]string, error) {
var strC C.git_strarray
defer C.git_strarray_dispose(&strC)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_list(&strC, c.repo.ptr)
runtime.KeepAlive(c)
if ret < 0 {
return nil, MakeGitError(ret)
}
w := makeStringsFromCStrings(strC.strings, int(strC.count))
return w, nil
}
// Lookup gets a working tree by its name for the given repository
func (c *WorktreeCollection) Lookup(name string) (*Worktree, error) {
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var ptr *C.git_worktree
ret := C.git_worktree_lookup(&ptr, c.repo.ptr, cname)
runtime.KeepAlive(c)
if ret < 0 {
return nil, MakeGitError(ret)
} else if ptr == nil {
return nil, nil
}
return newWorktreeFromC(ptr), nil
}
// OpenFromRepository retrieves a worktree for the given repository
func (c *WorktreeCollection) OpenFromRepository() (*Worktree, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var ptr *C.git_worktree
ret := C.git_worktree_open_from_repository(&ptr, c.repo.ptr)
runtime.KeepAlive(c)
if ret < 0 {
return nil, MakeGitError(ret)
}
return newWorktreeFromC(ptr), nil
}
func newWorktreeFromC(ptr *C.git_worktree) *Worktree {
worktree := &Worktree{ptr: ptr}
runtime.SetFinalizer(worktree, (*Worktree).Free)
return worktree
}
func freeAddWorktreeOptions(cOptions *C.git_worktree_add_options) {
if cOptions == nil {
return
}
freeCheckoutOptions(&cOptions.checkout_options)
}
func populateAddWorktreeOptions(cOptions *C.git_worktree_add_options, options *AddWorktreeOptions, errorTarget *error) *C.git_worktree_add_options {
C.git_worktree_add_options_init(cOptions, C.GIT_WORKTREE_ADD_OPTIONS_VERSION)
if options == nil {
return nil
}
populateCheckoutOptions(&cOptions.checkout_options, &options.CheckoutOptions, errorTarget)
cOptions.lock = cbool(options.Lock)
if options.Reference != nil {
cOptions.ref = options.Reference.ptr
}
return cOptions
}
// Free a previously allocated worktree
func (w *Worktree) Free() {
runtime.SetFinalizer(w, nil)
C.git_worktree_free(w.ptr)
}
// IsLocked checks if the given worktree is locked
func (w *Worktree) IsLocked() (locked bool, reason string, err error) {
buf := C.git_buf{}
defer C.git_buf_dispose(&buf)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_is_locked(&buf, w.ptr)
runtime.KeepAlive(w)
if ret < 0 {
return false, "", MakeGitError(ret)
}
return ret != 0, C.GoString(buf.ptr), nil
}
type WorktreePruneFlag uint32
const (
// WorktreePruneValid means prune working tree even if working tree is valid
WorktreePruneValid WorktreePruneFlag = C.GIT_WORKTREE_PRUNE_VALID
// WorktreePruneLocked means prune working tree even if it is locked
WorktreePruneLocked WorktreePruneFlag = C.GIT_WORKTREE_PRUNE_LOCKED
// WorktreePruneWorkingTree means prune checked out working tree
WorktreePruneWorkingTree WorktreePruneFlag = C.GIT_WORKTREE_PRUNE_WORKING_TREE
)
// IsPrunable checks that the worktree is prunable with the given flags
func (w *Worktree) IsPrunable(flags WorktreePruneFlag) (bool, error) {
cOptions := C.git_worktree_prune_options{}
C.git_worktree_prune_options_init(&cOptions, C.GIT_WORKTREE_PRUNE_OPTIONS_VERSION)
cOptions.flags = C.uint32_t(flags)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_is_prunable(w.ptr, &cOptions)
runtime.KeepAlive(w)
if ret < 0 {
return false, MakeGitError(ret)
}
return ret != 0, nil
}
// Lock locks the worktree if not already locked
func (w *Worktree) Lock(reason string) error {
var cReason *C.char
if reason != "" {
cReason = C.CString(reason)
defer C.free(unsafe.Pointer(cReason))
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_lock(w.ptr, cReason)
runtime.KeepAlive(w)
if ret < 0 {
return MakeGitError(ret)
}
return nil
}
// Name retrieves the name of the worktree
func (w *Worktree) Name() string {
s := C.GoString(C.git_worktree_name(w.ptr))
runtime.KeepAlive(w)
return s
}
// Path retrieves the path of the worktree
func (w *Worktree) Path() string {
s := C.GoString(C.git_worktree_path(w.ptr))
runtime.KeepAlive(w)
return s
}
// Prune the worktree with the provided flags
func (w *Worktree) Prune(flags WorktreePruneFlag) error {
cOptions := C.git_worktree_prune_options{}
C.git_worktree_prune_options_init(&cOptions, C.GIT_WORKTREE_PRUNE_OPTIONS_VERSION)
cOptions.flags = C.uint32_t(flags)
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_prune(w.ptr, &cOptions)
runtime.KeepAlive(w)
if ret < 0 {
return MakeGitError(ret)
}
return nil
}
// Unlock a locked worktree
func (w *Worktree) Unlock() (notLocked bool, err error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_unlock(w.ptr)
runtime.KeepAlive(w)
if ret < 0 {
return false, MakeGitError(ret)
}
return ret != 0, nil
}
// Validate checks if the given worktree is valid
func (w *Worktree) Validate() error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ret := C.git_worktree_validate(w.ptr)
runtime.KeepAlive(w)
if ret < 0 {
return MakeGitError(ret)
}
return nil
}

225
worktree_test.go Normal file
View File

@ -0,0 +1,225 @@
package git
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func assertSamePath(t *testing.T, expected string, actual string) {
var err error
expected, err = filepath.EvalSymlinks(expected)
checkFatal(t, err)
actual, err = filepath.EvalSymlinks(actual)
checkFatal(t, err)
if expected != actual {
t.Fatalf("wrong path (expected %s, got %s)", expected, actual)
}
}
func TestAddWorkspace(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktreeName := "testWorktree"
worktreePath := filepath.Join(worktreeTemporaryPath, "worktree")
worktree, err := repo.Worktrees.Add(worktreeName, worktreePath, &AddWorktreeOptions{
Lock: true, CheckoutOptions: CheckoutOptions{Strategy: CheckoutForce},
})
checkFatal(t, err)
if name := worktree.Name(); name != worktreeName {
t.Fatalf("wrong worktree name: %s != %s", worktreeName, name)
}
locked, _, err := worktree.IsLocked()
checkFatal(t, err)
if locked != true {
t.Fatal("worktree isn't locked")
}
assertSamePath(t, worktreePath, worktree.Path())
checkFatal(t, worktree.Validate())
}
func TestAddWorkspaceWithoutOptions(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktreeName := "testWorktree"
worktreePath := filepath.Join(worktreeTemporaryPath, "worktree")
worktree, err := repo.Worktrees.Add(worktreeName, worktreePath, nil)
checkFatal(t, err)
if name := worktree.Name(); name != worktreeName {
t.Fatalf("wrong worktree name: %s != %s", worktreeName, name)
}
locked, _, err := worktree.IsLocked()
checkFatal(t, err)
if locked != false {
t.Fatal("worktree is locked")
}
assertSamePath(t, worktreePath, worktree.Path())
checkFatal(t, worktree.Validate())
}
func TestLookupWorkspace(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktreeName := "testWorktree"
worktree, err := repo.Worktrees.Add(worktreeName, filepath.Join(worktreeTemporaryPath, "worktree"), nil)
checkFatal(t, err)
retrievedWorktree, err := repo.Worktrees.Lookup(worktreeName)
checkFatal(t, err)
assertSamePath(t, worktree.Path(), retrievedWorktree.Path())
}
func TestListWorkspaces(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktreeNames := []string{"worktree1", "worktree2", "worktree3"}
for _, name := range worktreeNames {
_, err = repo.Worktrees.Add(name, filepath.Join(worktreeTemporaryPath, name), nil)
checkFatal(t, err)
}
listedWorktree, err := repo.Worktrees.List()
checkFatal(t, err)
if len(worktreeNames) != len(listedWorktree) {
t.Fatalf("len(worktreeNames) != len(listedWorktree) as %d != %d", len(worktreeNames), len(listedWorktree))
}
for _, name := range worktreeNames {
found := false
for _, nameToMatch := range listedWorktree {
if name == nameToMatch {
found = true
break
}
}
if !found {
t.Fatalf("worktree %s is missing", name)
}
}
}
func TestOpenWorkspaceFromRepository(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktree, err := repo.Worktrees.Add("testWorktree", filepath.Join(worktreeTemporaryPath, "worktree"), nil)
checkFatal(t, err)
worktreeRepo, err := OpenRepository(worktree.Path())
checkFatal(t, err)
worktreeFromRepo, err := worktreeRepo.Worktrees.OpenFromRepository()
checkFatal(t, err)
if worktreeFromRepo.Name() != worktree.Name() {
t.Fatalf("wrong name (expected %s, got %s)", worktreeFromRepo.Name(), worktree.Name())
}
}
func TestWorktreeIsPrunable(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktree, err := repo.Worktrees.Add("testWorktree", filepath.Join(worktreeTemporaryPath, "worktree"), nil)
checkFatal(t, err)
err = worktree.Lock("test")
checkFatal(t, err)
isPrunableWithoutLockedFlag, err := worktree.IsPrunable(WorktreePruneValid)
checkFatal(t, err)
if isPrunableWithoutLockedFlag {
t.Fatal("worktree shouldn't be prunable without the WorktreePruneLocked flag")
}
isPrunableWithLockedFlag, err := worktree.IsPrunable(WorktreePruneValid | WorktreePruneLocked)
checkFatal(t, err)
if !isPrunableWithLockedFlag {
t.Fatal("worktree should be prunable with the WorktreePruneLocked flag")
}
err = worktree.Prune(WorktreePruneValid | WorktreePruneLocked)
checkFatal(t, err)
}
func TestWorktreeCanBeLockedAndUnlocked(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
worktreeTemporaryPath, err := ioutil.TempDir("", "git2go")
checkFatal(t, err)
defer func() { checkFatal(t, os.RemoveAll(worktreeTemporaryPath)) }()
worktree, err := repo.Worktrees.Add("testWorktree", filepath.Join(worktreeTemporaryPath, "worktree"), nil)
checkFatal(t, err)
notLocked, err := worktree.Unlock()
checkFatal(t, err)
if !notLocked {
t.Fatal("worktree should be unlocked by default")
}
expectedReason := "toTestIt"
err = worktree.Lock(expectedReason)
checkFatal(t, err)
isLocked, reason, err := worktree.IsLocked()
checkFatal(t, err)
if !isLocked {
t.Fatal("worktree should be locked after the locking operation")
}
if expectedReason != reason {
t.Fatalf("locked reason doesn't match: %s != %s", expectedReason, reason)
}
notLocked, err = worktree.Unlock()
checkFatal(t, err)
if notLocked {
t.Fatal("worktree was lock before so notLocked should be false")
}
isLocked, _, err = worktree.IsLocked()
checkFatal(t, err)
if isLocked {
t.Fatal("worktree should be unlocked after the Unlock() call")
}
}