From 1472589a93ef2d194c2c36aadee21a63892061b4 Mon Sep 17 00:00:00 2001 From: Jan Baer Date: Tue, 1 Sep 2026 09:00:22 +0200 Subject: [PATCH] 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 --- modules/task/pull_delete_branch_local_test.go | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 modules/task/pull_delete_branch_local_test.go diff --git a/modules/task/pull_delete_branch_local_test.go b/modules/task/pull_delete_branch_local_test.go new file mode 100644 index 00000000..991273ef --- /dev/null +++ b/modules/task/pull_delete_branch_local_test.go @@ -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") +}