Files
wehub-resource-sync 26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

447 lines
15 KiB
Go

package git
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
func TestWorktreeAddAndRemove(t *testing.T) {
// create a bare repo with at least one commit by pushing from a regular repo
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "bare.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
run(t, bare, "git", "config", "core.autocrlf", "false")
run(t, src, "git", "remote", "add", "bare", bare)
run(t, src, "git", "push", "bare", "HEAD:refs/heads/main")
sha := run(t, src, "git", "rev-parse", "HEAD")
// add worktree
wtDir := filepath.Join(t.TempDir(), "worktree")
if err := WorktreeAdd(ctx, bare, wtDir, sha); err != nil {
t.Fatalf("WorktreeAdd failed: %v", err)
}
// verify worktree has the file
content, err := os.ReadFile(filepath.Join(wtDir, "README.md"))
if err != nil {
t.Fatalf("worktree missing README.md: %v", err)
}
if string(content) != "# test\n" {
t.Fatalf("unexpected content: %q", content)
}
// remove worktree
if err := WorktreeRemove(ctx, bare, wtDir); err != nil {
t.Fatalf("WorktreeRemove failed: %v", err)
}
// verify worktree directory is gone
if _, err := os.Stat(wtDir); !os.IsNotExist(err) {
t.Fatal("worktree directory should not exist after removal")
}
}
func TestFindMainRepoRoot(t *testing.T) {
ctx := context.Background()
mainRepo := initTestRepo(t)
// For a normal repo, FindMainRepoRoot should return the same as FindGitRoot.
mainRoot, err := FindMainRepoRoot(mainRepo)
if err != nil {
t.Fatalf("FindMainRepoRoot failed for main repo: %v", err)
}
expectedMain, _ := filepath.EvalSymlinks(mainRepo)
gotMain, _ := filepath.EvalSymlinks(mainRoot)
if gotMain != expectedMain {
t.Fatalf("expected %q, got %q", expectedMain, gotMain)
}
// Create a worktree and verify FindMainRepoRoot returns the main repo root.
run(t, mainRepo, "git", "checkout", "-b", "wt-branch")
run(t, mainRepo, "git", "checkout", "-") // back to original branch
wtDir := filepath.Join(t.TempDir(), "worktree")
if err := WorktreeAdd(ctx, mainRepo, wtDir, "wt-branch"); err != nil {
t.Fatalf("WorktreeAdd failed: %v", err)
}
t.Cleanup(func() { WorktreeRemove(ctx, mainRepo, wtDir) })
// FindGitRoot from worktree returns the worktree path.
wtRoot, err := FindGitRoot(wtDir)
if err != nil {
t.Fatalf("FindGitRoot from worktree failed: %v", err)
}
resolvedWt, _ := filepath.EvalSymlinks(wtDir)
gotWt, _ := filepath.EvalSymlinks(wtRoot)
if gotWt != resolvedWt {
t.Fatalf("FindGitRoot should return worktree path %q, got %q", resolvedWt, gotWt)
}
// FindMainRepoRoot from worktree should return the main repo root.
mainFromWt, err := FindMainRepoRoot(wtDir)
if err != nil {
t.Fatalf("FindMainRepoRoot from worktree failed: %v", err)
}
gotFromWt, _ := filepath.EvalSymlinks(mainFromWt)
if gotFromWt != expectedMain {
t.Fatalf("FindMainRepoRoot from worktree: expected %q, got %q", expectedMain, gotFromWt)
}
}
func TestFindMainRepoRootNotFound(t *testing.T) {
_, err := FindMainRepoRoot(t.TempDir())
if err == nil {
t.Fatal("expected error for non-git directory")
}
}
// addSubmodule wires up an absorbed submodule at <super>/<name> pointing at
// the repo at <remoteDir>, commits the new submodule, and returns the
// submodule's working tree. The path layout matches `git submodule add`:
// - <super>/<name> working tree
// - <super>/.git/modules/<name> absorbed git dir
// - core.worktree = ../../../../<name> relative inside the absorbed git dir
func addSubmodule(t *testing.T, superDir, name, remoteDir string) string {
t.Helper()
// Use an absolute path so the clone does not depend on the superproject
// having an "origin" remote (which would change the relative path's
// resolution base).
absRemote, err := filepath.Abs(remoteDir)
if err != nil {
t.Fatalf("abs remote: %v", err)
}
// superDir may itself be a submodule checkout (nested case), which never
// had a commit identity configured; set one so the commit below does not
// fail with "empty ident name" on runners without an ambient git identity.
run(t, superDir, "git", "config", "user.email", "test@test.com")
run(t, superDir, "git", "config", "user.name", "Test")
run(t, superDir, "git", "-c", "protocol.file.allow=always", "submodule", "add", absRemote, name)
run(t, superDir, "git", "-c", "protocol.file.allow=always", "submodule", "init", name)
run(t, superDir, "git", "-c", "protocol.file.allow=always", "submodule", "update", name)
run(t, superDir, "git", "add", ".gitmodules", name)
run(t, superDir, "git", "commit", "-m", "add "+name+" submodule")
return filepath.Join(superDir, name)
}
// TestFindMainRepoRootSubmodule reproduces issue #328: in an absorbed
// submodule checkout, the git common dir is <super>/.git/modules/<name>, so
// the historical "parent of commonDir" heuristic points at the superproject's
// modules directory. The function must instead resolve to the submodule's
// own working tree (via core.worktree) so callers read the submodule's
// remotes, not the superproject's.
func TestFindMainRepoRootSubmodule(t *testing.T) {
ctx := context.Background()
superDir := initTestRepo(t)
// Give the superproject its own distinct origin so the assertion below
// can tell the two apart — the bug is that the gate ends up reading
// the superproject's origin instead of the submodule's.
if err := AddRemote(ctx, superDir, "origin", "https://github.com/example/superproject.git"); err != nil {
t.Fatalf("add super origin: %v", err)
}
subRemote := initTestRepo(t)
subWorktree := addSubmodule(t, superDir, "sub", subRemote)
// Set the submodule's own origin on its working tree (the helper above
// clones subRemote, so without this the cloned submodule's "origin"
// would be subRemote's path, not the URL the gate should record).
// `git submodule add` already created an "origin" remote pointing at
// subRemote, so use EnsureRemote to overwrite it idempotently.
if err := EnsureRemote(ctx, subWorktree, "origin", "https://github.com/example/submodule.git"); err != nil {
t.Fatalf("add submodule origin: %v", err)
}
got, err := FindMainRepoRoot(subWorktree)
if err != nil {
t.Fatalf("FindMainRepoRoot from submodule: %v", err)
}
wantResolved, _ := filepath.EvalSymlinks(subWorktree)
gotResolved, _ := filepath.EvalSymlinks(got)
if gotResolved != wantResolved {
t.Fatalf("FindMainRepoRoot from submodule = %q, want %q (superproject's .git/modules would be wrong)", gotResolved, wantResolved)
}
// The dangerous half: callers (init, eject) read remotes by running git
// with cwd set to the returned root. If that root resolves to the
// superproject instead, the gate records the superproject's origin
// instead of the submodule's.
url, err := Run(ctx, got, "remote", "get-url", "origin")
if err != nil {
t.Fatalf("read origin from returned root: %v", err)
}
if url != "https://github.com/example/submodule.git" {
t.Fatalf("origin read from returned root = %q, want the submodule's origin", url)
}
}
// TestFindMainRepoRootNestedSubmodule is the same fix one level deeper:
// a submodule inside a submodule has its git dir at
// <super>/.git/modules/a/modules/b. The function must still resolve to the
// innermost submodule's working tree.
func TestFindMainRepoRootNestedSubmodule(t *testing.T) {
ctx := context.Background()
superDir := initTestRepo(t)
outerRemote := initTestRepo(t)
innerRemote := initTestRepo(t)
outerWorktree := addSubmodule(t, superDir, "outer", outerRemote)
innerWorktree := addSubmodule(t, outerWorktree, "inner", innerRemote)
if err := AddRemote(ctx, innerRemote, "origin", "https://github.com/example/inner.git"); err != nil {
t.Fatalf("add inner origin: %v", err)
}
got, err := FindMainRepoRoot(innerWorktree)
if err != nil {
t.Fatalf("FindMainRepoRoot from nested submodule: %v", err)
}
wantResolved, _ := filepath.EvalSymlinks(innerWorktree)
gotResolved, _ := filepath.EvalSymlinks(got)
if gotResolved != wantResolved {
t.Fatalf("FindMainRepoRoot from nested submodule = %q, want %q", gotResolved, wantResolved)
}
}
// TestFindMainRepoRootFromWorktreeStillResolvesToMain is the linked-worktree
// regression guard for the three-way fix. Linked worktrees keep their git
// common dir at <main>/.git, so the historical "parent of commonDir" answer
// stays correct and must not regress.
func TestFindMainRepoRootFromWorktreeStillResolvesToMain(t *testing.T) {
ctx := context.Background()
mainRepo := initTestRepo(t)
run(t, mainRepo, "git", "checkout", "-b", "wt-branch")
run(t, mainRepo, "git", "checkout", "-")
wtDir := filepath.Join(t.TempDir(), "worktree")
if err := WorktreeAdd(ctx, mainRepo, wtDir, "wt-branch"); err != nil {
t.Fatalf("WorktreeAdd: %v", err)
}
t.Cleanup(func() { WorktreeRemove(ctx, mainRepo, wtDir) })
got, err := FindMainRepoRoot(wtDir)
if err != nil {
t.Fatalf("FindMainRepoRoot from worktree: %v", err)
}
want, _ := filepath.EvalSymlinks(mainRepo)
gotResolved, _ := filepath.EvalSymlinks(got)
if gotResolved != want {
t.Fatalf("FindMainRepoRoot from worktree = %q, want %q", gotResolved, want)
}
}
func TestPush(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "dest.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
run(t, src, "git", "remote", "add", "dest", bare)
// push main branch
if err := Push(ctx, src, "dest", "refs/heads/main", "", false); err != nil {
t.Fatalf("Push failed: %v", err)
}
// verify ref exists in bare repo
out, err := Run(ctx, bare, "rev-parse", "refs/heads/main")
if err != nil {
t.Fatalf("ref not found in dest: %v", err)
}
expected := run(t, src, "git", "rev-parse", "HEAD")
if out != expected {
t.Fatalf("expected %q, got %q", expected, out)
}
}
func TestPushWithOptionsForwardsPushOptions(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "dest.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
run(t, bare, "git", "config", "receive.advertisePushOptions", "true")
run(t, src, "git", "remote", "add", "dest", bare)
marker := filepath.Join(t.TempDir(), "push-options.txt")
hook := "#!/bin/sh\nprintf '%s:%s\n' \"$GIT_PUSH_OPTION_COUNT\" \"$GIT_PUSH_OPTION_0\" > " + shellSingleQuote(marker) + "\n"
if err := os.WriteFile(filepath.Join(bare, "hooks", "post-receive"), []byte(hook), 0o755); err != nil {
t.Fatal(err)
}
if err := PushWithOptions(ctx, src, "dest", "refs/heads/main", "", false, []string{"no-mistakes.skip=test,lint"}); err != nil {
t.Fatalf("PushWithOptions failed: %v", err)
}
data, err := os.ReadFile(marker)
if err != nil {
t.Fatal(err)
}
if got, want := strings.TrimSpace(string(data)), "1:no-mistakes.skip=test,lint"; got != want {
t.Fatalf("push options marker = %q, want %q", got, want)
}
}
func TestPushForceWithLease(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "dest.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
run(t, src, "git", "remote", "add", "dest", bare)
run(t, src, "git", "push", "dest", "HEAD:refs/heads/main")
expectedSHA := run(t, src, "git", "rev-parse", "HEAD")
// make a new commit (simulating rebase)
writeFile(t, filepath.Join(src, "new.txt"), "new\n")
run(t, src, "git", "add", ".")
run(t, src, "git", "commit", "-m", "new commit")
// force-with-lease should succeed with correct expected SHA
if err := Push(ctx, src, "dest", "refs/heads/main", expectedSHA, true); err != nil {
t.Fatalf("PushForceWithLease failed: %v", err)
}
// verify new SHA
out, err := Run(ctx, bare, "rev-parse", "refs/heads/main")
if err != nil {
t.Fatal(err)
}
newSHA := run(t, src, "git", "rev-parse", "HEAD")
if out != newSHA {
t.Fatalf("expected %q, got %q", newSHA, out)
}
}
func TestLsRemote(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "dest.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
run(t, src, "git", "remote", "add", "dest", bare)
run(t, src, "git", "push", "dest", "HEAD:refs/heads/main")
expectedSHA := run(t, src, "git", "rev-parse", "HEAD")
// Query existing ref
sha, err := LsRemote(ctx, src, bare, "refs/heads/main")
if err != nil {
t.Fatalf("LsRemote failed: %v", err)
}
if sha != expectedSHA {
t.Fatalf("expected %q, got %q", expectedSHA, sha)
}
}
func TestLsRemoteNotFound(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "dest.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
// Query a ref that doesn't exist
sha, err := LsRemote(ctx, src, bare, "refs/heads/nonexistent")
if err != nil {
t.Fatalf("LsRemote should not error for missing ref: %v", err)
}
if sha != "" {
t.Fatalf("expected empty string for missing ref, got %q", sha)
}
}
func TestDefaultBranch(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "upstream.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
// Push to bare repo so HEAD ref exists.
run(t, src, "git", "remote", "add", "upstream", bare)
run(t, src, "git", "push", "upstream", "HEAD:refs/heads/main")
branch := DefaultBranch(ctx, src, "upstream")
if branch != "main" {
t.Fatalf("expected 'main', got %q", branch)
}
}
func TestDefaultBranchNonMain(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "upstream.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
// Set bare repo HEAD to develop.
run(t, bare, "git", "symbolic-ref", "HEAD", "refs/heads/develop")
// Push a develop branch.
run(t, src, "git", "remote", "add", "upstream", bare)
run(t, src, "git", "push", "upstream", "HEAD:refs/heads/develop")
branch := DefaultBranch(ctx, src, "upstream")
if branch != "develop" {
t.Fatalf("expected 'develop', got %q", branch)
}
}
func TestDefaultBranchFallback(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
// Remote doesn't exist — should fall back to "main".
branch := DefaultBranch(ctx, src, "nonexistent")
if branch != "main" {
t.Fatalf("expected 'main' fallback, got %q", branch)
}
}
func TestDefaultBranchEmptyRemote(t *testing.T) {
ctx := context.Background()
src := initTestRepo(t)
bare := filepath.Join(t.TempDir(), "empty.git")
if err := InitBare(ctx, bare); err != nil {
t.Fatal(err)
}
// Empty bare repo (no refs) — HEAD symref exists but target doesn't.
// ls-remote returns nothing for HEAD, so should fall back to "main".
run(t, src, "git", "remote", "add", "empty", bare)
branch := DefaultBranch(ctx, src, "empty")
if branch != "main" {
t.Fatalf("expected 'main' fallback for empty remote, got %q", branch)
}
}
func TestCurrentBranch(t *testing.T) {
dir := initTestRepo(t)
ctx := context.Background()
// default branch after init — could be main or master depending on git config
branch, err := CurrentBranch(ctx, dir)
if err != nil {
t.Fatalf("CurrentBranch failed: %v", err)
}
if branch == "" {
t.Fatal("expected non-empty branch name")
}
// create and checkout a new branch
run(t, dir, "git", "checkout", "-b", "feature")
branch, err = CurrentBranch(ctx, dir)
if err != nil {
t.Fatalf("CurrentBranch on feature failed: %v", err)
}
if branch != "feature" {
t.Fatalf("expected 'feature', got %q", branch)
}
}