test(pulls): cover local branch deletion with a git fixture

The other tests chdir into an empty temp dir, so RepoForWorkdir fails and
PullDeleteBranch's branch-matching and checkout path never runs.

A real repository fixture works offline: the git backend shells out to the
git binary, GetRemote matches a remote by parsed host and path rather than
contacting it, and a remote-tracking ref can be written with update-ref
instead of fetched.

Covers a branch that is not checked out, one that is, a diverged local
branch being left alone, no matching local branch, and a missing base
branch being reported rather than returned.

The fixture pins --initial-branch=main and isolates GIT_CONFIG_GLOBAL and
commit.gpgsign (see #1072). These tests call t.Chdir, so they cannot run
in parallel.

Signed-off-by: Jan Baer <jan.s.baer@googlemail.com>
This commit is contained in:
Jan Baer 2026-09-01 09:00:22 +02:00
parent 5aedf30e7b
commit 1472589a93

View file

@ -0,0 +1,162 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package task
import (
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// headCloneURL is never contacted: GetRemote matches on parsed host and path.
const headCloneURL = "https://example.invalid/contributor/tea.git"
func git(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
out, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git %v failed: %s", args, out)
return strings.TrimSpace(string(out))
}
func commit(t *testing.T, dir, name string) string {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(name), 0o644))
git(t, dir, "add", name)
git(t, dir, "commit", "-m", name)
return git(t, dir, "rev-parse", "HEAD")
}
// gitFixture builds a repo with 'main' and 'feature-x' plus a remote-tracking
// ref, and returns the path and the feature-x commit. HEAD is left on main.
func gitFixture(t *testing.T) (dir, sha string) {
t.Helper()
// A global commit.gpgsign or init.defaultBranch would break the fixture (#1072).
t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(t.TempDir(), ".gitconfig"))
dir = filepath.Join(t.TempDir(), "repo")
require.NoError(t, os.MkdirAll(dir, 0o755))
// The default name varies by git version, and the PR names 'main' as base.
git(t, dir, "init", "--initial-branch=main")
git(t, dir, "config", "user.email", "test@example.com")
git(t, dir, "config", "user.name", "Test User")
git(t, dir, "config", "commit.gpgsign", "false")
commit(t, dir, "base")
git(t, dir, "checkout", "-b", "feature-x")
sha = commit(t, dir, "work")
git(t, dir, "remote", "add", "origin", headCloneURL)
git(t, dir, "update-ref", "refs/remotes/origin/feature-x", sha)
git(t, dir, "checkout", "main")
return dir, sha
}
func branchExists(t *testing.T, dir, branch string) bool {
t.Helper()
return exec.Command("git", "-C", dir, "rev-parse", "--verify", "--quiet",
"refs/heads/"+branch).Run() == nil
}
func currentBranch(t *testing.T, dir string) string {
t.Helper()
return git(t, dir, "rev-parse", "--abbrev-ref", "HEAD")
}
// mergedPR serves the PR for a gitFixture repo and accepts the branch DELETE.
func mergedPR(t *testing.T, sha string) *httptest.Server {
t.Helper()
var ignored string
return deleteBranchServer(t, prJSON("contributor", "tea", "feature-x", sha, true), &ignored)
}
func deleteFixtureBranch(t *testing.T, server *httptest.Server) error {
t.Helper()
return PullDeleteBranch(t.Context(), &config.Login{
Name: "test",
URL: server.URL,
Token: "secret-token",
VersionCheck: false,
}, "owner", "repo", 7, nil)
}
// These tests change the working directory, so they cannot run in parallel.
func TestPullDeleteBranchRemovesLocalBranch(t *testing.T) {
dir, sha := gitFixture(t)
server := mergedPR(t, sha)
defer server.Close()
t.Chdir(dir)
require.NoError(t, deleteFixtureBranch(t, server))
assert.False(t, branchExists(t, dir, "feature-x"), "local branch should be gone")
assert.Equal(t, "main", currentBranch(t, dir), "HEAD should not have moved")
}
func TestPullDeleteBranchChecksOutBaseWhenOnTheBranch(t *testing.T) {
dir, sha := gitFixture(t)
git(t, dir, "checkout", "feature-x")
server := mergedPR(t, sha)
defer server.Close()
t.Chdir(dir)
require.NoError(t, deleteFixtureBranch(t, server))
// git won't delete the checked-out branch, so base comes first.
assert.Equal(t, "main", currentBranch(t, dir))
assert.False(t, branchExists(t, dir, "feature-x"))
}
// The guard that matters: unpushed commits must survive.
func TestPullDeleteBranchKeepsDivergedLocalBranch(t *testing.T) {
dir, sha := gitFixture(t)
git(t, dir, "checkout", "feature-x")
local := commit(t, dir, "unpushed")
git(t, dir, "checkout", "main")
require.NotEqual(t, sha, local)
server := mergedPR(t, sha)
defer server.Close()
t.Chdir(dir)
require.NoError(t, deleteFixtureBranch(t, server))
require.True(t, branchExists(t, dir, "feature-x"), "diverged branch must not be deleted")
assert.Equal(t, local, git(t, dir, "rev-parse", "feature-x"), "unpushed commit must survive")
}
func TestPullDeleteBranchWithoutMatchingLocalBranch(t *testing.T) {
dir, sha := gitFixture(t)
git(t, dir, "branch", "-D", "feature-x")
server := mergedPR(t, sha)
defer server.Close()
t.Chdir(dir)
assert.NoError(t, deleteFixtureBranch(t, server))
}
// The remote is already gone by then, so a failed checkout is reported, not returned.
func TestPullDeleteBranchSurvivesMissingBaseBranch(t *testing.T) {
dir, sha := gitFixture(t)
git(t, dir, "checkout", "feature-x")
git(t, dir, "branch", "-D", "main")
server := mergedPR(t, sha)
defer server.Close()
t.Chdir(dir)
assert.NoError(t, deleteFixtureBranch(t, server))
assert.True(t, branchExists(t, dir, "feature-x"), "branch stays when the checkout fails")
}