mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-07 21:38:21 +00:00
feat(gitstore): honor configured branch and follow live remote default
This commit is contained in:
@@ -60,6 +60,8 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB
|
||||
|
||||
CLIProxyAPI Guides: [https://help.router-for.me/](https://help.router-for.me/)
|
||||
|
||||
For the optional git-backed config store, `GITSTORE_GIT_BRANCH` is optional. Leave it empty or unset to follow the remote repository's default branch, and set it only when you want to force a specific branch.
|
||||
|
||||
## Management API
|
||||
|
||||
see [MANAGEMENT_API.md](https://help.router-for.me/management/api)
|
||||
|
||||
@@ -60,6 +60,8 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元
|
||||
|
||||
CLIProxyAPI 用户手册: [https://help.router-for.me/](https://help.router-for.me/cn/)
|
||||
|
||||
对于可选的 git 存储配置,`GITSTORE_GIT_BRANCH` 是可选项。留空或不设置时会跟随远程仓库的默认分支,只有在你需要强制指定分支时才设置它。
|
||||
|
||||
## 管理 API 文档
|
||||
|
||||
请参见 [MANAGEMENT_API_CN.md](https://help.router-for.me/cn/management/api)
|
||||
|
||||
@@ -60,6 +60,8 @@ GLM CODING PLANを10%割引で取得:https://z.ai/subscribe?ic=8JVLJQFSKB
|
||||
|
||||
CLIProxyAPIガイド:[https://help.router-for.me/ja/](https://help.router-for.me/ja/)
|
||||
|
||||
オプションのgitバックアップ設定ストアでは、`GITSTORE_GIT_BRANCH` は任意です。空のままにするか未設定にすると、リモートリポジトリのデフォルトブランチに従います。特定のブランチを強制したい場合のみ設定してください。
|
||||
|
||||
## 管理API
|
||||
|
||||
[MANAGEMENT_API.md](https://help.router-for.me/ja/management/api)を参照
|
||||
|
||||
@@ -140,6 +140,7 @@ func main() {
|
||||
gitStoreRemoteURL string
|
||||
gitStoreUser string
|
||||
gitStorePassword string
|
||||
gitStoreBranch string
|
||||
gitStoreLocalPath string
|
||||
gitStoreInst *store.GitTokenStore
|
||||
gitStoreRoot string
|
||||
@@ -209,6 +210,9 @@ func main() {
|
||||
if value, ok := lookupEnv("GITSTORE_LOCAL_PATH", "gitstore_local_path"); ok {
|
||||
gitStoreLocalPath = value
|
||||
}
|
||||
if value, ok := lookupEnv("GITSTORE_GIT_BRANCH", "gitstore_git_branch"); ok {
|
||||
gitStoreBranch = value
|
||||
}
|
||||
if value, ok := lookupEnv("OBJECTSTORE_ENDPOINT", "objectstore_endpoint"); ok {
|
||||
useObjectStore = true
|
||||
objectStoreEndpoint = value
|
||||
@@ -343,7 +347,7 @@ func main() {
|
||||
}
|
||||
gitStoreRoot = filepath.Join(gitStoreLocalPath, "gitstore")
|
||||
authDir := filepath.Join(gitStoreRoot, "auths")
|
||||
gitStoreInst = store.NewGitTokenStore(gitStoreRemoteURL, gitStoreUser, gitStorePassword)
|
||||
gitStoreInst = store.NewGitTokenStore(gitStoreRemoteURL, gitStoreUser, gitStorePassword, gitStoreBranch)
|
||||
gitStoreInst.SetBaseDir(authDir)
|
||||
if errRepo := gitStoreInst.EnsureRepository(); errRepo != nil {
|
||||
log.Errorf("failed to prepare git token store: %v", errRepo)
|
||||
|
||||
@@ -32,16 +32,24 @@ type GitTokenStore struct {
|
||||
repoDir string
|
||||
configDir string
|
||||
remote string
|
||||
branch string
|
||||
username string
|
||||
password string
|
||||
lastGC time.Time
|
||||
}
|
||||
|
||||
type resolvedRemoteBranch struct {
|
||||
name plumbing.ReferenceName
|
||||
hash plumbing.Hash
|
||||
}
|
||||
|
||||
// NewGitTokenStore creates a token store that saves credentials to disk through the
|
||||
// TokenStorage implementation embedded in the token record.
|
||||
func NewGitTokenStore(remote, username, password string) *GitTokenStore {
|
||||
// When branch is non-empty, clone/pull/push operations target that branch instead of the remote default.
|
||||
func NewGitTokenStore(remote, username, password, branch string) *GitTokenStore {
|
||||
return &GitTokenStore{
|
||||
remote: remote,
|
||||
branch: strings.TrimSpace(branch),
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
@@ -120,7 +128,11 @@ func (s *GitTokenStore) EnsureRepository() error {
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: create repo dir: %w", errMk)
|
||||
}
|
||||
if _, errClone := git.PlainClone(repoDir, &git.CloneOptions{Auth: authMethod, URL: s.remote}); errClone != nil {
|
||||
cloneOpts := &git.CloneOptions{Auth: authMethod, URL: s.remote}
|
||||
if s.branch != "" {
|
||||
cloneOpts.ReferenceName = plumbing.NewBranchReferenceName(s.branch)
|
||||
}
|
||||
if _, errClone := git.PlainClone(repoDir, cloneOpts); errClone != nil {
|
||||
if errors.Is(errClone, transport.ErrEmptyRemoteRepository) {
|
||||
_ = os.RemoveAll(gitDir)
|
||||
repo, errInit := git.PlainInit(repoDir, false)
|
||||
@@ -128,6 +140,13 @@ func (s *GitTokenStore) EnsureRepository() error {
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: init empty repo: %w", errInit)
|
||||
}
|
||||
if s.branch != "" {
|
||||
headRef := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(s.branch))
|
||||
if errHead := repo.Storer.SetReference(headRef); errHead != nil {
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: set head to branch %s: %w", s.branch, errHead)
|
||||
}
|
||||
}
|
||||
if _, errRemote := repo.Remote("origin"); errRemote != nil {
|
||||
if _, errCreate := repo.CreateRemote(&config.RemoteConfig{
|
||||
Name: "origin",
|
||||
@@ -176,16 +195,39 @@ func (s *GitTokenStore) EnsureRepository() error {
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: worktree: %w", errWorktree)
|
||||
}
|
||||
if errPull := worktree.Pull(&git.PullOptions{Auth: authMethod, RemoteName: "origin"}); errPull != nil {
|
||||
if s.branch != "" {
|
||||
if errCheckout := s.checkoutConfiguredBranch(repo, worktree, authMethod); errCheckout != nil {
|
||||
s.dirLock.Unlock()
|
||||
return errCheckout
|
||||
}
|
||||
} else {
|
||||
// When branch is unset, ensure the working tree follows the remote default branch
|
||||
if err := checkoutRemoteDefaultBranch(repo, worktree, authMethod); err != nil {
|
||||
if !shouldFallbackToCurrentBranch(repo, err) {
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: checkout remote default: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
pullOpts := &git.PullOptions{Auth: authMethod, RemoteName: "origin"}
|
||||
if s.branch != "" {
|
||||
pullOpts.ReferenceName = plumbing.NewBranchReferenceName(s.branch)
|
||||
}
|
||||
if errPull := worktree.Pull(pullOpts); errPull != nil {
|
||||
switch {
|
||||
case errors.Is(errPull, git.NoErrAlreadyUpToDate),
|
||||
errors.Is(errPull, git.ErrUnstagedChanges),
|
||||
errors.Is(errPull, git.ErrNonFastForwardUpdate):
|
||||
// Ignore clean syncs, local edits, and remote divergence—local changes win.
|
||||
case errors.Is(errPull, transport.ErrAuthenticationRequired),
|
||||
errors.Is(errPull, plumbing.ErrReferenceNotFound),
|
||||
errors.Is(errPull, transport.ErrEmptyRemoteRepository):
|
||||
// Ignore authentication prompts and empty remote references on initial sync.
|
||||
case errors.Is(errPull, plumbing.ErrReferenceNotFound):
|
||||
if s.branch != "" {
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: pull: %w", errPull)
|
||||
}
|
||||
// Ignore missing references only when following the remote default branch.
|
||||
default:
|
||||
s.dirLock.Unlock()
|
||||
return fmt.Errorf("git token store: pull: %w", errPull)
|
||||
@@ -553,6 +595,192 @@ func (s *GitTokenStore) relativeToRepo(path string) (string, error) {
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func (s *GitTokenStore) checkoutConfiguredBranch(repo *git.Repository, worktree *git.Worktree, authMethod transport.AuthMethod) error {
|
||||
branchRefName := plumbing.NewBranchReferenceName(s.branch)
|
||||
headRef, errHead := repo.Head()
|
||||
switch {
|
||||
case errHead == nil && headRef.Name() == branchRefName:
|
||||
return nil
|
||||
case errHead != nil && !errors.Is(errHead, plumbing.ErrReferenceNotFound):
|
||||
return fmt.Errorf("git token store: get head: %w", errHead)
|
||||
}
|
||||
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: branchRefName}); err == nil {
|
||||
return nil
|
||||
} else if _, errRef := repo.Reference(branchRefName, true); errRef == nil {
|
||||
return fmt.Errorf("git token store: checkout branch %s: %w", s.branch, err)
|
||||
} else if !errors.Is(errRef, plumbing.ErrReferenceNotFound) {
|
||||
return fmt.Errorf("git token store: inspect branch %s: %w", s.branch, errRef)
|
||||
} else if err := s.checkoutConfiguredRemoteTrackingBranch(repo, worktree, branchRefName, authMethod); err != nil {
|
||||
return fmt.Errorf("git token store: checkout branch %s: %w", s.branch, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GitTokenStore) checkoutConfiguredRemoteTrackingBranch(repo *git.Repository, worktree *git.Worktree, branchRefName plumbing.ReferenceName, authMethod transport.AuthMethod) error {
|
||||
remoteRefName := plumbing.ReferenceName("refs/remotes/origin/" + s.branch)
|
||||
remoteRef, err := repo.Reference(remoteRefName, true)
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
if errSync := syncRemoteReferences(repo, authMethod); errSync != nil {
|
||||
return fmt.Errorf("sync remote refs: %w", errSync)
|
||||
}
|
||||
remoteRef, err = repo.Reference(remoteRefName, true)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: branchRefName, Create: true, Hash: remoteRef.Hash()}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := repo.Config()
|
||||
if err != nil {
|
||||
return fmt.Errorf("git token store: repo config: %w", err)
|
||||
}
|
||||
if _, ok := cfg.Branches[s.branch]; !ok {
|
||||
cfg.Branches[s.branch] = &config.Branch{Name: s.branch}
|
||||
}
|
||||
cfg.Branches[s.branch].Remote = "origin"
|
||||
cfg.Branches[s.branch].Merge = branchRefName
|
||||
if err := repo.SetConfig(cfg); err != nil {
|
||||
return fmt.Errorf("git token store: set branch config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncRemoteReferences(repo *git.Repository, authMethod transport.AuthMethod) error {
|
||||
if err := repo.Fetch(&git.FetchOptions{Auth: authMethod, RemoteName: "origin"}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRemoteDefaultBranch queries the origin remote to determine the remote's default branch
|
||||
// (the target of HEAD) and returns the corresponding local branch reference name (e.g. refs/heads/master).
|
||||
func resolveRemoteDefaultBranch(repo *git.Repository, authMethod transport.AuthMethod) (resolvedRemoteBranch, error) {
|
||||
if err := syncRemoteReferences(repo, authMethod); err != nil {
|
||||
return resolvedRemoteBranch{}, fmt.Errorf("resolve remote default: sync remote refs: %w", err)
|
||||
}
|
||||
remote, err := repo.Remote("origin")
|
||||
if err != nil {
|
||||
return resolvedRemoteBranch{}, fmt.Errorf("resolve remote default: get remote: %w", err)
|
||||
}
|
||||
refs, err := remote.List(&git.ListOptions{Auth: authMethod})
|
||||
if err != nil {
|
||||
if resolved, ok := resolveRemoteDefaultBranchFromLocal(repo); ok {
|
||||
return resolved, nil
|
||||
}
|
||||
return resolvedRemoteBranch{}, fmt.Errorf("resolve remote default: list remote refs: %w", err)
|
||||
}
|
||||
for _, r := range refs {
|
||||
if r.Name() == plumbing.HEAD {
|
||||
if r.Type() == plumbing.SymbolicReference {
|
||||
if target, ok := normalizeRemoteBranchReference(r.Target()); ok {
|
||||
return resolvedRemoteBranch{name: target}, nil
|
||||
}
|
||||
}
|
||||
s := r.String()
|
||||
if idx := strings.Index(s, "->"); idx != -1 {
|
||||
if target, ok := normalizeRemoteBranchReference(plumbing.ReferenceName(strings.TrimSpace(s[idx+2:]))); ok {
|
||||
return resolvedRemoteBranch{name: target}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if resolved, ok := resolveRemoteDefaultBranchFromLocal(repo); ok {
|
||||
return resolved, nil
|
||||
}
|
||||
for _, r := range refs {
|
||||
if normalized, ok := normalizeRemoteBranchReference(r.Name()); ok {
|
||||
return resolvedRemoteBranch{name: normalized, hash: r.Hash()}, nil
|
||||
}
|
||||
}
|
||||
return resolvedRemoteBranch{}, fmt.Errorf("resolve remote default: remote default branch not found")
|
||||
}
|
||||
|
||||
func resolveRemoteDefaultBranchFromLocal(repo *git.Repository) (resolvedRemoteBranch, bool) {
|
||||
ref, err := repo.Reference(plumbing.ReferenceName("refs/remotes/origin/HEAD"), true)
|
||||
if err != nil || ref.Type() != plumbing.SymbolicReference {
|
||||
return resolvedRemoteBranch{}, false
|
||||
}
|
||||
target, ok := normalizeRemoteBranchReference(ref.Target())
|
||||
if !ok {
|
||||
return resolvedRemoteBranch{}, false
|
||||
}
|
||||
return resolvedRemoteBranch{name: target}, true
|
||||
}
|
||||
|
||||
func normalizeRemoteBranchReference(name plumbing.ReferenceName) (plumbing.ReferenceName, bool) {
|
||||
switch {
|
||||
case strings.HasPrefix(name.String(), "refs/heads/"):
|
||||
return name, true
|
||||
case strings.HasPrefix(name.String(), "refs/remotes/origin/"):
|
||||
return plumbing.NewBranchReferenceName(strings.TrimPrefix(name.String(), "refs/remotes/origin/")), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func shouldFallbackToCurrentBranch(repo *git.Repository, err error) bool {
|
||||
if !errors.Is(err, transport.ErrAuthenticationRequired) && !errors.Is(err, transport.ErrEmptyRemoteRepository) {
|
||||
return false
|
||||
}
|
||||
_, headErr := repo.Head()
|
||||
return headErr == nil
|
||||
}
|
||||
|
||||
// checkoutRemoteDefaultBranch ensures the working tree is checked out to the remote's default branch
|
||||
// (the branch target of origin/HEAD). If the local branch does not exist it will be created to track
|
||||
// the remote branch.
|
||||
func checkoutRemoteDefaultBranch(repo *git.Repository, worktree *git.Worktree, authMethod transport.AuthMethod) error {
|
||||
resolved, err := resolveRemoteDefaultBranch(repo, authMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
branchRefName := resolved.name
|
||||
// If HEAD already points to the desired branch, nothing to do.
|
||||
headRef, errHead := repo.Head()
|
||||
if errHead == nil && headRef.Name() == branchRefName {
|
||||
return nil
|
||||
}
|
||||
// If local branch exists, attempt a checkout
|
||||
if _, err := repo.Reference(branchRefName, true); err == nil {
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: branchRefName}); err != nil {
|
||||
return fmt.Errorf("checkout branch %s: %w", branchRefName.String(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Try to find the corresponding remote tracking ref (refs/remotes/origin/<name>)
|
||||
branchShort := strings.TrimPrefix(branchRefName.String(), "refs/heads/")
|
||||
remoteRefName := plumbing.ReferenceName("refs/remotes/origin/" + branchShort)
|
||||
hash := resolved.hash
|
||||
if remoteRef, err := repo.Reference(remoteRefName, true); err == nil {
|
||||
hash = remoteRef.Hash()
|
||||
} else if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
return fmt.Errorf("checkout remote default: remote ref %s: %w", remoteRefName.String(), err)
|
||||
}
|
||||
if hash == plumbing.ZeroHash {
|
||||
return fmt.Errorf("checkout remote default: remote ref %s not found", remoteRefName.String())
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: branchRefName, Create: true, Hash: hash}); err != nil {
|
||||
return fmt.Errorf("checkout create branch %s: %w", branchRefName.String(), err)
|
||||
}
|
||||
cfg, err := repo.Config()
|
||||
if err != nil {
|
||||
return fmt.Errorf("git token store: repo config: %w", err)
|
||||
}
|
||||
if _, ok := cfg.Branches[branchShort]; !ok {
|
||||
cfg.Branches[branchShort] = &config.Branch{Name: branchShort}
|
||||
}
|
||||
cfg.Branches[branchShort].Remote = "origin"
|
||||
cfg.Branches[branchShort].Merge = branchRefName
|
||||
if err := repo.SetConfig(cfg); err != nil {
|
||||
return fmt.Errorf("git token store: set branch config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GitTokenStore) commitAndPushLocked(message string, relPaths ...string) error {
|
||||
repoDir := s.repoDirSnapshot()
|
||||
if repoDir == "" {
|
||||
@@ -618,7 +846,16 @@ func (s *GitTokenStore) commitAndPushLocked(message string, relPaths ...string)
|
||||
return errRewrite
|
||||
}
|
||||
s.maybeRunGC(repo)
|
||||
if err = repo.Push(&git.PushOptions{Auth: s.gitAuth(), Force: true}); err != nil {
|
||||
pushOpts := &git.PushOptions{Auth: s.gitAuth(), Force: true}
|
||||
if s.branch != "" {
|
||||
pushOpts.RefSpecs = []config.RefSpec{config.RefSpec("refs/heads/" + s.branch + ":refs/heads/" + s.branch)}
|
||||
} else {
|
||||
// When branch is unset, pin push to the currently checked-out branch.
|
||||
if headRef, err := repo.Head(); err == nil {
|
||||
pushOpts.RefSpecs = []config.RefSpec{config.RefSpec(headRef.Name().String() + ":" + headRef.Name().String())}
|
||||
}
|
||||
}
|
||||
if err = repo.Push(pushOpts); err != nil {
|
||||
if errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||
return nil
|
||||
}
|
||||
|
||||
585
internal/store/gitstore_test.go
Normal file
585
internal/store/gitstore_test.go
Normal file
@@ -0,0 +1,585 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v6"
|
||||
gitconfig "github.com/go-git/go-git/v6/config"
|
||||
"github.com/go-git/go-git/v6/plumbing"
|
||||
"github.com/go-git/go-git/v6/plumbing/object"
|
||||
)
|
||||
|
||||
type testBranchSpec struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryUsesRemoteDefaultBranchWhenBranchNotConfigured(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "trunk",
|
||||
testBranchSpec{name: "trunk", contents: "remote default branch\n"},
|
||||
testBranchSpec{name: "release/2026", contents: "release branch\n"},
|
||||
)
|
||||
|
||||
store := NewGitTokenStore(remoteDir, "", "", "")
|
||||
store.SetBaseDir(filepath.Join(root, "workspace", "auths"))
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository: %v", err)
|
||||
}
|
||||
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "trunk", "remote default branch\n")
|
||||
advanceRemoteBranch(t, filepath.Join(root, "seed"), remoteDir, "trunk", "remote default branch updated\n", "advance trunk")
|
||||
advanceRemoteBranch(t, filepath.Join(root, "seed"), remoteDir, "release/2026", "release branch updated\n", "advance release")
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository second call: %v", err)
|
||||
}
|
||||
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "trunk", "remote default branch updated\n")
|
||||
assertRemoteHeadBranch(t, remoteDir, "trunk")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryUsesConfiguredBranchWhenExplicitlySet(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "trunk",
|
||||
testBranchSpec{name: "trunk", contents: "remote default branch\n"},
|
||||
testBranchSpec{name: "release/2026", contents: "release branch\n"},
|
||||
)
|
||||
|
||||
store := NewGitTokenStore(remoteDir, "", "", "release/2026")
|
||||
store.SetBaseDir(filepath.Join(root, "workspace", "auths"))
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository: %v", err)
|
||||
}
|
||||
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "release/2026", "release branch\n")
|
||||
advanceRemoteBranch(t, filepath.Join(root, "seed"), remoteDir, "trunk", "remote default branch updated\n", "advance trunk")
|
||||
advanceRemoteBranch(t, filepath.Join(root, "seed"), remoteDir, "release/2026", "release branch updated\n", "advance release")
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository second call: %v", err)
|
||||
}
|
||||
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "release/2026", "release branch updated\n")
|
||||
assertRemoteHeadBranch(t, remoteDir, "trunk")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryReturnsErrorForMissingConfiguredBranch(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "trunk",
|
||||
testBranchSpec{name: "trunk", contents: "remote default branch\n"},
|
||||
)
|
||||
|
||||
store := NewGitTokenStore(remoteDir, "", "", "missing-branch")
|
||||
store.SetBaseDir(filepath.Join(root, "workspace", "auths"))
|
||||
|
||||
err := store.EnsureRepository()
|
||||
if err == nil {
|
||||
t.Fatal("EnsureRepository succeeded, want error for nonexistent configured branch")
|
||||
}
|
||||
assertRemoteHeadBranch(t, remoteDir, "trunk")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryReturnsErrorForMissingConfiguredBranchOnExistingRepositoryPull(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "trunk",
|
||||
testBranchSpec{name: "trunk", contents: "remote default branch\n"},
|
||||
)
|
||||
|
||||
baseDir := filepath.Join(root, "workspace", "auths")
|
||||
store := NewGitTokenStore(remoteDir, "", "", "")
|
||||
store.SetBaseDir(baseDir)
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository initial clone: %v", err)
|
||||
}
|
||||
|
||||
reopened := NewGitTokenStore(remoteDir, "", "", "missing-branch")
|
||||
reopened.SetBaseDir(baseDir)
|
||||
|
||||
err := reopened.EnsureRepository()
|
||||
if err == nil {
|
||||
t.Fatal("EnsureRepository succeeded on reopen, want error for nonexistent configured branch")
|
||||
}
|
||||
assertRepositoryHeadBranch(t, filepath.Join(root, "workspace"), "trunk")
|
||||
assertRemoteHeadBranch(t, remoteDir, "trunk")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryInitializesEmptyRemoteUsingConfiguredBranch(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := filepath.Join(root, "remote.git")
|
||||
if _, err := git.PlainInit(remoteDir, true); err != nil {
|
||||
t.Fatalf("init bare remote: %v", err)
|
||||
}
|
||||
|
||||
branch := "feature/gemini-fix"
|
||||
store := NewGitTokenStore(remoteDir, "", "", branch)
|
||||
store.SetBaseDir(filepath.Join(root, "workspace", "auths"))
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository: %v", err)
|
||||
}
|
||||
|
||||
assertRepositoryHeadBranch(t, filepath.Join(root, "workspace"), branch)
|
||||
assertRemoteBranchExistsWithCommit(t, remoteDir, branch)
|
||||
assertRemoteBranchDoesNotExist(t, remoteDir, "master")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryExistingRepoSwitchesToConfiguredBranch(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "master",
|
||||
testBranchSpec{name: "master", contents: "remote master branch\n"},
|
||||
testBranchSpec{name: "develop", contents: "remote develop branch\n"},
|
||||
)
|
||||
|
||||
baseDir := filepath.Join(root, "workspace", "auths")
|
||||
store := NewGitTokenStore(remoteDir, "", "", "")
|
||||
store.SetBaseDir(baseDir)
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository initial clone: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "master", "remote master branch\n")
|
||||
|
||||
reopened := NewGitTokenStore(remoteDir, "", "", "develop")
|
||||
reopened.SetBaseDir(baseDir)
|
||||
|
||||
if err := reopened.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository reopen: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "develop", "remote develop branch\n")
|
||||
|
||||
workspaceDir := filepath.Join(root, "workspace")
|
||||
if err := os.WriteFile(filepath.Join(workspaceDir, "branch.txt"), []byte("local develop update\n"), 0o600); err != nil {
|
||||
t.Fatalf("write local branch marker: %v", err)
|
||||
}
|
||||
|
||||
reopened.mu.Lock()
|
||||
err := reopened.commitAndPushLocked("Update develop branch marker", "branch.txt")
|
||||
reopened.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatalf("commitAndPushLocked: %v", err)
|
||||
}
|
||||
|
||||
assertRepositoryHeadBranch(t, workspaceDir, "develop")
|
||||
assertRemoteBranchContents(t, remoteDir, "develop", "local develop update\n")
|
||||
assertRemoteBranchContents(t, remoteDir, "master", "remote master branch\n")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryExistingRepoSwitchesToConfiguredBranchCreatedAfterClone(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "master",
|
||||
testBranchSpec{name: "master", contents: "remote master branch\n"},
|
||||
)
|
||||
|
||||
baseDir := filepath.Join(root, "workspace", "auths")
|
||||
store := NewGitTokenStore(remoteDir, "", "", "")
|
||||
store.SetBaseDir(baseDir)
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository initial clone: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "master", "remote master branch\n")
|
||||
|
||||
advanceRemoteBranchFromNewBranch(t, filepath.Join(root, "seed"), remoteDir, "release/2026", "release branch\n", "create release")
|
||||
|
||||
reopened := NewGitTokenStore(remoteDir, "", "", "release/2026")
|
||||
reopened.SetBaseDir(baseDir)
|
||||
|
||||
if err := reopened.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository reopen: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "release/2026", "release branch\n")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryResetsToRemoteDefaultWhenBranchUnset(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "master",
|
||||
testBranchSpec{name: "master", contents: "remote master branch\n"},
|
||||
testBranchSpec{name: "develop", contents: "remote develop branch\n"},
|
||||
)
|
||||
|
||||
baseDir := filepath.Join(root, "workspace", "auths")
|
||||
// First store pins to develop and prepares local workspace
|
||||
storePinned := NewGitTokenStore(remoteDir, "", "", "develop")
|
||||
storePinned.SetBaseDir(baseDir)
|
||||
if err := storePinned.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository pinned: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "develop", "remote develop branch\n")
|
||||
|
||||
// Second store has branch unset and should reset local workspace to remote default (master)
|
||||
storeDefault := NewGitTokenStore(remoteDir, "", "", "")
|
||||
storeDefault.SetBaseDir(baseDir)
|
||||
if err := storeDefault.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository default: %v", err)
|
||||
}
|
||||
// Local HEAD should now follow remote default (master)
|
||||
assertRepositoryHeadBranch(t, filepath.Join(root, "workspace"), "master")
|
||||
|
||||
// Make a local change and push using the store with branch unset; push should update remote master
|
||||
workspaceDir := filepath.Join(root, "workspace")
|
||||
if err := os.WriteFile(filepath.Join(workspaceDir, "branch.txt"), []byte("local master update\n"), 0o600); err != nil {
|
||||
t.Fatalf("write local master marker: %v", err)
|
||||
}
|
||||
storeDefault.mu.Lock()
|
||||
if err := storeDefault.commitAndPushLocked("Update master marker", "branch.txt"); err != nil {
|
||||
storeDefault.mu.Unlock()
|
||||
t.Fatalf("commitAndPushLocked: %v", err)
|
||||
}
|
||||
storeDefault.mu.Unlock()
|
||||
|
||||
assertRemoteBranchContents(t, remoteDir, "master", "local master update\n")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryFollowsRenamedRemoteDefaultBranchWhenAvailable(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "master",
|
||||
testBranchSpec{name: "master", contents: "remote master branch\n"},
|
||||
testBranchSpec{name: "main", contents: "remote main branch\n"},
|
||||
)
|
||||
|
||||
baseDir := filepath.Join(root, "workspace", "auths")
|
||||
store := NewGitTokenStore(remoteDir, "", "", "")
|
||||
store.SetBaseDir(baseDir)
|
||||
|
||||
if err := store.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository initial clone: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "master", "remote master branch\n")
|
||||
|
||||
setRemoteHeadBranch(t, remoteDir, "main")
|
||||
advanceRemoteBranch(t, filepath.Join(root, "seed"), remoteDir, "main", "remote main branch updated\n", "advance main")
|
||||
|
||||
reopened := NewGitTokenStore(remoteDir, "", "", "")
|
||||
reopened.SetBaseDir(baseDir)
|
||||
|
||||
if err := reopened.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository after remote default rename: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "main", "remote main branch updated\n")
|
||||
assertRemoteHeadBranch(t, remoteDir, "main")
|
||||
}
|
||||
|
||||
func TestEnsureRepositoryKeepsCurrentBranchWhenRemoteDefaultCannotBeResolved(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
remoteDir := setupGitRemoteRepository(t, root, "master",
|
||||
testBranchSpec{name: "master", contents: "remote master branch\n"},
|
||||
testBranchSpec{name: "develop", contents: "remote develop branch\n"},
|
||||
)
|
||||
|
||||
baseDir := filepath.Join(root, "workspace", "auths")
|
||||
pinned := NewGitTokenStore(remoteDir, "", "", "develop")
|
||||
pinned.SetBaseDir(baseDir)
|
||||
if err := pinned.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository pinned: %v", err)
|
||||
}
|
||||
assertRepositoryBranchAndContents(t, filepath.Join(root, "workspace"), "develop", "remote develop branch\n")
|
||||
|
||||
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="git"`)
|
||||
http.Error(w, "auth required", http.StatusUnauthorized)
|
||||
}))
|
||||
defer authServer.Close()
|
||||
|
||||
repo, err := git.PlainOpen(filepath.Join(root, "workspace"))
|
||||
if err != nil {
|
||||
t.Fatalf("open workspace repo: %v", err)
|
||||
}
|
||||
cfg, err := repo.Config()
|
||||
if err != nil {
|
||||
t.Fatalf("read repo config: %v", err)
|
||||
}
|
||||
cfg.Remotes["origin"].URLs = []string{authServer.URL}
|
||||
if err := repo.SetConfig(cfg); err != nil {
|
||||
t.Fatalf("set repo config: %v", err)
|
||||
}
|
||||
|
||||
reopened := NewGitTokenStore(remoteDir, "", "", "")
|
||||
reopened.SetBaseDir(baseDir)
|
||||
|
||||
if err := reopened.EnsureRepository(); err != nil {
|
||||
t.Fatalf("EnsureRepository default branch fallback: %v", err)
|
||||
}
|
||||
assertRepositoryHeadBranch(t, filepath.Join(root, "workspace"), "develop")
|
||||
}
|
||||
|
||||
func setupGitRemoteRepository(t *testing.T, root, defaultBranch string, branches ...testBranchSpec) string {
|
||||
t.Helper()
|
||||
|
||||
remoteDir := filepath.Join(root, "remote.git")
|
||||
if _, err := git.PlainInit(remoteDir, true); err != nil {
|
||||
t.Fatalf("init bare remote: %v", err)
|
||||
}
|
||||
|
||||
seedDir := filepath.Join(root, "seed")
|
||||
seedRepo, err := git.PlainInit(seedDir, false)
|
||||
if err != nil {
|
||||
t.Fatalf("init seed repo: %v", err)
|
||||
}
|
||||
if err := seedRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(defaultBranch))); err != nil {
|
||||
t.Fatalf("set seed HEAD: %v", err)
|
||||
}
|
||||
|
||||
worktree, err := seedRepo.Worktree()
|
||||
if err != nil {
|
||||
t.Fatalf("open seed worktree: %v", err)
|
||||
}
|
||||
|
||||
defaultSpec, ok := findBranchSpec(branches, defaultBranch)
|
||||
if !ok {
|
||||
t.Fatalf("missing default branch spec for %q", defaultBranch)
|
||||
}
|
||||
commitBranchMarker(t, seedDir, worktree, defaultSpec, "seed default branch")
|
||||
|
||||
for _, branch := range branches {
|
||||
if branch.name == defaultBranch {
|
||||
continue
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(defaultBranch)}); err != nil {
|
||||
t.Fatalf("checkout default branch %s: %v", defaultBranch, err)
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch.name), Create: true}); err != nil {
|
||||
t.Fatalf("create branch %s: %v", branch.name, err)
|
||||
}
|
||||
commitBranchMarker(t, seedDir, worktree, branch, "seed branch "+branch.name)
|
||||
}
|
||||
|
||||
if _, err := seedRepo.CreateRemote(&gitconfig.RemoteConfig{Name: "origin", URLs: []string{remoteDir}}); err != nil {
|
||||
t.Fatalf("create origin remote: %v", err)
|
||||
}
|
||||
if err := seedRepo.Push(&git.PushOptions{
|
||||
RemoteName: "origin",
|
||||
RefSpecs: []gitconfig.RefSpec{gitconfig.RefSpec("refs/heads/*:refs/heads/*")},
|
||||
}); err != nil {
|
||||
t.Fatalf("push seed branches: %v", err)
|
||||
}
|
||||
|
||||
remoteRepo, err := git.PlainOpen(remoteDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open remote repo: %v", err)
|
||||
}
|
||||
if err := remoteRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(defaultBranch))); err != nil {
|
||||
t.Fatalf("set remote HEAD: %v", err)
|
||||
}
|
||||
|
||||
return remoteDir
|
||||
}
|
||||
|
||||
func commitBranchMarker(t *testing.T, seedDir string, worktree *git.Worktree, branch testBranchSpec, message string) {
|
||||
t.Helper()
|
||||
|
||||
if err := os.WriteFile(filepath.Join(seedDir, "branch.txt"), []byte(branch.contents), 0o600); err != nil {
|
||||
t.Fatalf("write branch marker for %s: %v", branch.name, err)
|
||||
}
|
||||
if _, err := worktree.Add("branch.txt"); err != nil {
|
||||
t.Fatalf("add branch marker for %s: %v", branch.name, err)
|
||||
}
|
||||
if _, err := worktree.Commit(message, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "CLIProxyAPI",
|
||||
Email: "cliproxy@local",
|
||||
When: time.Unix(1711929600, 0),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("commit branch marker for %s: %v", branch.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func advanceRemoteBranch(t *testing.T, seedDir, remoteDir, branch, contents, message string) {
|
||||
t.Helper()
|
||||
|
||||
seedRepo, err := git.PlainOpen(seedDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open seed repo: %v", err)
|
||||
}
|
||||
worktree, err := seedRepo.Worktree()
|
||||
if err != nil {
|
||||
t.Fatalf("open seed worktree: %v", err)
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch)}); err != nil {
|
||||
t.Fatalf("checkout branch %s: %v", branch, err)
|
||||
}
|
||||
commitBranchMarker(t, seedDir, worktree, testBranchSpec{name: branch, contents: contents}, message)
|
||||
if err := seedRepo.Push(&git.PushOptions{
|
||||
RemoteName: "origin",
|
||||
RefSpecs: []gitconfig.RefSpec{
|
||||
gitconfig.RefSpec(plumbing.NewBranchReferenceName(branch).String() + ":" + plumbing.NewBranchReferenceName(branch).String()),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("push branch %s update to %s: %v", branch, remoteDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
func advanceRemoteBranchFromNewBranch(t *testing.T, seedDir, remoteDir, branch, contents, message string) {
|
||||
t.Helper()
|
||||
|
||||
seedRepo, err := git.PlainOpen(seedDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open seed repo: %v", err)
|
||||
}
|
||||
worktree, err := seedRepo.Worktree()
|
||||
if err != nil {
|
||||
t.Fatalf("open seed worktree: %v", err)
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName("master")}); err != nil {
|
||||
t.Fatalf("checkout master before creating %s: %v", branch, err)
|
||||
}
|
||||
if err := worktree.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(branch), Create: true}); err != nil {
|
||||
t.Fatalf("create branch %s: %v", branch, err)
|
||||
}
|
||||
commitBranchMarker(t, seedDir, worktree, testBranchSpec{name: branch, contents: contents}, message)
|
||||
if err := seedRepo.Push(&git.PushOptions{
|
||||
RemoteName: "origin",
|
||||
RefSpecs: []gitconfig.RefSpec{
|
||||
gitconfig.RefSpec(plumbing.NewBranchReferenceName(branch).String() + ":" + plumbing.NewBranchReferenceName(branch).String()),
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("push new branch %s update to %s: %v", branch, remoteDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
func findBranchSpec(branches []testBranchSpec, name string) (testBranchSpec, bool) {
|
||||
for _, branch := range branches {
|
||||
if branch.name == name {
|
||||
return branch, true
|
||||
}
|
||||
}
|
||||
return testBranchSpec{}, false
|
||||
}
|
||||
|
||||
func assertRepositoryBranchAndContents(t *testing.T, repoDir, branch, wantContents string) {
|
||||
t.Helper()
|
||||
|
||||
repo, err := git.PlainOpen(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open local repo: %v", err)
|
||||
}
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
t.Fatalf("local repo head: %v", err)
|
||||
}
|
||||
if got, want := head.Name(), plumbing.NewBranchReferenceName(branch); got != want {
|
||||
t.Fatalf("local head branch = %s, want %s", got, want)
|
||||
}
|
||||
contents, err := os.ReadFile(filepath.Join(repoDir, "branch.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("read branch marker: %v", err)
|
||||
}
|
||||
if got := string(contents); got != wantContents {
|
||||
t.Fatalf("branch marker contents = %q, want %q", got, wantContents)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRepositoryHeadBranch(t *testing.T, repoDir, branch string) {
|
||||
t.Helper()
|
||||
|
||||
repo, err := git.PlainOpen(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open local repo: %v", err)
|
||||
}
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
t.Fatalf("local repo head: %v", err)
|
||||
}
|
||||
if got, want := head.Name(), plumbing.NewBranchReferenceName(branch); got != want {
|
||||
t.Fatalf("local head branch = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRemoteHeadBranch(t *testing.T, remoteDir, branch string) {
|
||||
t.Helper()
|
||||
|
||||
remoteRepo, err := git.PlainOpen(remoteDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open remote repo: %v", err)
|
||||
}
|
||||
head, err := remoteRepo.Reference(plumbing.HEAD, false)
|
||||
if err != nil {
|
||||
t.Fatalf("read remote HEAD: %v", err)
|
||||
}
|
||||
if got, want := head.Target(), plumbing.NewBranchReferenceName(branch); got != want {
|
||||
t.Fatalf("remote HEAD target = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func setRemoteHeadBranch(t *testing.T, remoteDir, branch string) {
|
||||
t.Helper()
|
||||
|
||||
remoteRepo, err := git.PlainOpen(remoteDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open remote repo: %v", err)
|
||||
}
|
||||
if err := remoteRepo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))); err != nil {
|
||||
t.Fatalf("set remote HEAD to %s: %v", branch, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRemoteBranchExistsWithCommit(t *testing.T, remoteDir, branch string) {
|
||||
t.Helper()
|
||||
|
||||
remoteRepo, err := git.PlainOpen(remoteDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open remote repo: %v", err)
|
||||
}
|
||||
ref, err := remoteRepo.Reference(plumbing.NewBranchReferenceName(branch), false)
|
||||
if err != nil {
|
||||
t.Fatalf("read remote branch %s: %v", branch, err)
|
||||
}
|
||||
if got := ref.Hash(); got == plumbing.ZeroHash {
|
||||
t.Fatalf("remote branch %s hash = %s, want non-zero hash", branch, got)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRemoteBranchDoesNotExist(t *testing.T, remoteDir, branch string) {
|
||||
t.Helper()
|
||||
|
||||
remoteRepo, err := git.PlainOpen(remoteDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open remote repo: %v", err)
|
||||
}
|
||||
if _, err := remoteRepo.Reference(plumbing.NewBranchReferenceName(branch), false); err == nil {
|
||||
t.Fatalf("remote branch %s exists, want missing", branch)
|
||||
} else if err != plumbing.ErrReferenceNotFound {
|
||||
t.Fatalf("read remote branch %s: %v", branch, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRemoteBranchContents(t *testing.T, remoteDir, branch, wantContents string) {
|
||||
t.Helper()
|
||||
|
||||
remoteRepo, err := git.PlainOpen(remoteDir)
|
||||
if err != nil {
|
||||
t.Fatalf("open remote repo: %v", err)
|
||||
}
|
||||
ref, err := remoteRepo.Reference(plumbing.NewBranchReferenceName(branch), false)
|
||||
if err != nil {
|
||||
t.Fatalf("read remote branch %s: %v", branch, err)
|
||||
}
|
||||
commit, err := remoteRepo.CommitObject(ref.Hash())
|
||||
if err != nil {
|
||||
t.Fatalf("read remote branch %s commit: %v", branch, err)
|
||||
}
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
t.Fatalf("read remote branch %s tree: %v", branch, err)
|
||||
}
|
||||
file, err := tree.File("branch.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("read remote branch %s file: %v", branch, err)
|
||||
}
|
||||
contents, err := file.Contents()
|
||||
if err != nil {
|
||||
t.Fatalf("read remote branch %s contents: %v", branch, err)
|
||||
}
|
||||
if contents != wantContents {
|
||||
t.Fatalf("remote branch %s contents = %q, want %q", branch, contents, wantContents)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user