Files
ReleaseKit/internal/git/remote_test.go

224 lines
8.1 KiB
Go

package git
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestResolveUsesPushURLAndQueriesWithoutMutation(t *testing.T) {
repo := newRemoteFixture(t)
configBefore := gitText(t, repo.work, "config", "--get-all", "remote.origin.url") + "\x00" + gitText(t, repo.work, "config", "--get-all", "remote.origin.pushurl")
refsBefore := gitText(t, repo.bare, "show-ref", "--heads", "--tags")
endpoint, err := Resolve(context.Background(), repo.work, "origin")
if err != nil {
t.Fatal(err)
}
if endpoint.PushURL != repo.bare {
t.Fatalf("push URL = %q, want %q", endpoint.PushURL, repo.bare)
}
if err := endpoint.Recheck(context.Background(), repo.work); err != nil {
t.Fatalf("Recheck() error = %v", err)
}
snapshot, err := endpoint.RequireCandidate(context.Background(), repo.work, "v1.2.3", repo.commit)
if err != nil {
t.Fatalf("RequireCandidate() error = %v", err)
}
if snapshot.Main.State != RefPresent || snapshot.Main.Object != repo.commit || snapshot.Tag.State != RefAbsent {
t.Fatalf("snapshot = %#v", snapshot)
}
configAfter := gitText(t, repo.work, "config", "--get-all", "remote.origin.url") + "\x00" + gitText(t, repo.work, "config", "--get-all", "remote.origin.pushurl")
refsAfter := gitText(t, repo.bare, "show-ref", "--heads", "--tags")
if configAfter != configBefore {
t.Fatalf("persistent remote config changed: before %q, after %q", configBefore, configAfter)
}
if refsAfter != refsBefore {
t.Fatalf("remote refs changed: before %q, after %q", refsBefore, refsAfter)
}
}
func TestResolveAppliesGitURLRewrites(t *testing.T) {
for _, kind := range []string{"insteadOf", "pushInsteadOf"} {
t.Run(kind, func(t *testing.T) {
repo := newRemoteFixture(t)
alias := "releasekit-test:"
if kind == "insteadOf" {
gitRun(t, repo.work, "config", "url."+repo.bare+".insteadOf", alias)
gitRun(t, repo.work, "remote", "set-url", "origin", alias)
gitRun(t, repo.work, "config", "--unset-all", "remote.origin.pushurl")
} else {
gitRun(t, repo.work, "config", "url."+repo.bare+".pushInsteadOf", alias)
gitRun(t, repo.work, "remote", "set-url", "origin", alias)
gitRun(t, repo.work, "config", "--unset-all", "remote.origin.pushurl")
}
endpoint, err := Resolve(context.Background(), repo.work, "origin")
if err != nil {
t.Fatal(err)
}
if endpoint.PushURL != repo.bare {
t.Fatalf("effective push URL = %q, want %q", endpoint.PushURL, repo.bare)
}
})
}
}
func TestResolveRejectsMultipleDistinctPushDestinations(t *testing.T) {
repo := newRemoteFixture(t)
second := filepath.Join(t.TempDir(), "second.git")
gitRun(t, t.TempDir(), "init", "--bare", second)
gitRun(t, repo.work, "config", "--add", "remote.origin.pushurl", second)
_, err := Resolve(context.Background(), repo.work, "origin")
if !errors.Is(err, ErrMultiplePushURLs) {
t.Fatalf("Resolve() error = %v, want ErrMultiplePushURLs", err)
}
}
func TestRequireCandidateDistinguishesMissingMainTagConflictAndMainMismatch(t *testing.T) {
t.Run("missing main", func(t *testing.T) {
repo := newRemoteFixture(t)
gitRun(t, repo.bare, "update-ref", "-d", "refs/heads/main")
endpoint := resolveFixture(t, repo)
snapshot, err := endpoint.RequireCandidate(context.Background(), repo.work, "v1.2.3", repo.commit)
if !errors.Is(err, ErrRemoteMainMissing) || snapshot.Main.State != RefAbsent {
t.Fatalf("snapshot = %#v, error = %v", snapshot, err)
}
})
t.Run("existing tag", func(t *testing.T) {
repo := newRemoteFixture(t)
gitRun(t, repo.work, "tag", "v1.2.3", repo.commit)
gitRun(t, repo.work, "push", repo.bare, "refs/tags/v1.2.3:refs/tags/v1.2.3")
endpoint := resolveFixture(t, repo)
snapshot, err := endpoint.RequireCandidate(context.Background(), repo.work, "v1.2.3", repo.commit)
if !errors.Is(err, ErrRemoteTagConflict) || snapshot.Tag.State != RefConflict {
t.Fatalf("snapshot = %#v, error = %v", snapshot, err)
}
})
for _, relation := range []string{"ahead", "behind"} {
t.Run("main "+relation, func(t *testing.T) {
repo := newRemoteFixture(t)
candidate := repo.commit
remoteMain := repo.commit
if relation == "ahead" {
writeRepoFile(t, repo.work, "ahead.txt", "ahead")
gitRun(t, repo.work, "add", "ahead.txt")
gitRun(t, repo.work, "commit", "-m", "Advance main")
remoteMain = gitText(t, repo.work, "rev-parse", "HEAD")
gitRun(t, repo.work, "push", repo.bare, "HEAD:refs/heads/main")
} else {
writeRepoFile(t, repo.work, "behind.txt", "candidate is ahead")
gitRun(t, repo.work, "add", "behind.txt")
gitRun(t, repo.work, "commit", "-m", "Candidate ahead of main")
candidate = gitText(t, repo.work, "rev-parse", "HEAD")
}
if relation == "behind" {
gitRun(t, repo.bare, "update-ref", "refs/heads/main", remoteMain)
}
endpoint := resolveFixture(t, repo)
snapshot, err := endpoint.RequireCandidate(context.Background(), repo.work, "v1.2.3", candidate)
if !errors.Is(err, ErrRemoteMainConflict) || snapshot.Main.State != RefConflict {
t.Fatalf("snapshot = %#v, error = %v", snapshot, err)
}
})
}
}
func TestQueryFailureIsUnreadableAndEndpointChangesAreDetected(t *testing.T) {
repo := newRemoteFixture(t)
endpoint := resolveFixture(t, repo)
missing := filepath.Join(t.TempDir(), "missing.git")
gitRun(t, repo.work, "remote", "set-url", "--push", "origin", missing)
if err := endpoint.Recheck(context.Background(), repo.work); !errors.Is(err, ErrEndpointChanged) {
t.Fatalf("Recheck() error = %v, want ErrEndpointChanged", err)
}
gitRun(t, repo.work, "remote", "set-url", "--push", "origin", missing)
broken, err := Resolve(context.Background(), repo.work, "origin")
if err != nil {
t.Fatalf("Resolve(missing bare path) error = %v", err)
}
snapshot, err := broken.Query(context.Background(), repo.work, "v1.2.3")
if !errors.Is(err, ErrRemoteStateUnreadable) || snapshot.Main.State != RefUnreadable || snapshot.Tag.State != RefUnreadable {
t.Fatalf("snapshot = %#v, error = %v", snapshot, err)
}
}
func TestMergeRefMarksDuplicateExactResultsConflicting(t *testing.T) {
first := strings.Repeat("a", 40)
second := strings.Repeat("b", 40)
ref := mergeRef(RemoteRef{State: RefAbsent}, first, false)
ref = mergeRef(ref, second, false)
if ref.State != RefConflict {
t.Fatalf("state = %v, want RefConflict", ref.State)
}
}
type remoteFixture struct {
work string
bare string
commit string
}
func newRemoteFixture(t testing.TB) remoteFixture {
t.Helper()
base := t.TempDir()
work := filepath.Join(base, "work")
bare := filepath.Join(base, "authoritative.git")
decoy := filepath.Join(base, "fetch-only.git")
gitRun(t, base, "init", "-b", "main", work)
gitRun(t, work, "config", "user.name", "ReleaseKit Test")
gitRun(t, work, "config", "user.email", "releasekit@example.invalid")
gitRun(t, work, "init", "--bare", bare)
gitRun(t, work, "init", "--bare", decoy)
writeRepoFile(t, work, "source.txt", "candidate")
gitRun(t, work, "add", "source.txt")
gitRun(t, work, "commit", "-m", "Candidate")
commit := gitText(t, work, "rev-parse", "HEAD")
gitRun(t, work, "remote", "add", "origin", decoy)
gitRun(t, work, "config", "--add", "remote.origin.pushurl", bare)
gitRun(t, work, "push", bare, "HEAD:refs/heads/main")
return remoteFixture{work: work, bare: bare, commit: commit}
}
func resolveFixture(t *testing.T, repo remoteFixture) Endpoint {
t.Helper()
endpoint, err := Resolve(context.Background(), repo.work, "origin")
if err != nil {
t.Fatal(err)
}
return endpoint
}
func gitRun(t testing.TB, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
}
}
func gitText(t testing.TB, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
outputBytes, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, outputBytes)
}
return strings.TrimSuffix(string(outputBytes), "\n")
}
func writeRepoFile(t testing.TB, repoDir, name, contents string) {
t.Helper()
path := filepath.Join(repoDir, name)
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
t.Fatal(err)
}
}