mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
fix: address verified review findings (branches-checkout-status-1)
Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
27 files changed
+281
-65
No files matched your search
+15
-3
@@ -29,13 +29,25 @@ type BranchConfig struct {
|
||||
// CurrentBranchConfig reads the configuration of the checked-out branch, failing with
|
||||
// ErrNotOnBranch when HEAD is detached or dir is not a checkout.
|
||||
func CurrentBranchConfig(ctx context.Context, runner Runner, dir string) (BranchConfig, error) {
|
||||
name, err := runner.Output(ctx, dir, false, "symbolic-ref", "--short", "HEAD")
|
||||
if err != nil || name == "" {
|
||||
return BranchConfig{}, ErrNotOnBranch
|
||||
name, err := CurrentBranch(ctx, runner, dir)
|
||||
if err != nil {
|
||||
return BranchConfig{}, err
|
||||
}
|
||||
return ReadBranchConfig(ctx, runner, dir, name), nil
|
||||
}
|
||||
|
||||
// CurrentBranch names the checked-out branch, failing with ErrNotOnBranch when HEAD is detached
|
||||
// or dir is not a checkout. It reads the full ref because --short answers heads/NAME for a branch
|
||||
// that shares its name with a tag.
|
||||
func CurrentBranch(ctx context.Context, runner Runner, dir string) (string, error) {
|
||||
ref, err := runner.Output(ctx, dir, false, "symbolic-ref", "--quiet", "HEAD")
|
||||
name := strings.TrimPrefix(ref, "refs/heads/")
|
||||
if err != nil || name == "" {
|
||||
return "", ErrNotOnBranch
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// ReadBranchConfig reads branch.<name>.remote, .merge and the keys pr checkout records.
|
||||
// Missing or malformed keys stay empty.
|
||||
func ReadBranchConfig(ctx context.Context, runner Runner, dir, name string) BranchConfig {
|
||||
|
||||
@@ -5,13 +5,14 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCurrentBranchConfig(t *testing.T) {
|
||||
const head = "symbolic-ref --short HEAD"
|
||||
const head = "symbolic-ref --quiet HEAD"
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
runner discoverRunner
|
||||
@@ -39,3 +40,18 @@ func TestCurrentBranchConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentBranchIgnoresATagWithTheSameName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, args := range [][]string{
|
||||
{"init", "--quiet", "--initial-branch=rel-2"},
|
||||
{"-c", "user.name=t", "-c", "user.email=t@example.test", "commit", "--quiet", "--allow-empty", "--message=x"},
|
||||
{"tag", "rel-2"},
|
||||
} {
|
||||
out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
}
|
||||
name, err := CurrentBranch(context.Background(), System{}, dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "rel-2", name)
|
||||
}
|
||||
+14
-6
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -34,6 +35,9 @@ type FindCleanBranchOptions struct {
|
||||
DefaultBranch string
|
||||
PreferredNames, MatchingRemotes []string
|
||||
IgnoreSHA bool
|
||||
// PullNumber is the pull request being cleaned; a branch that pr checkout recorded for it
|
||||
// wins over branches that merely share the head branch's name.
|
||||
PullNumber int64
|
||||
}
|
||||
|
||||
// FindCleanBranch locates one local branch for PR cleanup without mutating Git state.
|
||||
@@ -49,8 +53,8 @@ func FindCleanBranch(ctx context.Context, runner Runner, opts FindCleanBranchOpt
|
||||
return "", "", err
|
||||
}
|
||||
type candidate struct {
|
||||
name, remote string
|
||||
matched bool
|
||||
name, remote string
|
||||
matched, recorded bool
|
||||
}
|
||||
setOf := func(names []string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
@@ -80,8 +84,9 @@ func FindCleanBranch(ctx context.Context, runner Runner, opts FindCleanBranchOpt
|
||||
if len(parts) == 3 {
|
||||
upstream = parts[2]
|
||||
}
|
||||
recorded := opts.PullNumber > 0 && ReadBranchConfig(ctx, runner, opts.Directory, name).PullNumber == opts.PullNumber
|
||||
if opts.IgnoreSHA {
|
||||
if name != opts.HeadBranch && !preferred[name] {
|
||||
if name != opts.HeadBranch && !preferred[name] && !recorded {
|
||||
continue
|
||||
}
|
||||
} else if opts.HeadSHA == "" || sha != opts.HeadSHA {
|
||||
@@ -107,7 +112,7 @@ func FindCleanBranch(ctx context.Context, runner Runner, opts FindCleanBranchOpt
|
||||
remoteName = remote
|
||||
}
|
||||
}
|
||||
candidates = append(candidates, candidate{name: name, remote: remoteName, matched: matched})
|
||||
candidates = append(candidates, candidate{name: name, remote: remoteName, matched: matched, recorded: recorded})
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
if opts.IgnoreSHA {
|
||||
@@ -119,13 +124,16 @@ Retry with --ignore-sha to select by branch name`, opts.HeadSHA)
|
||||
}
|
||||
selected := []candidate{}
|
||||
for _, item := range candidates {
|
||||
if item.matched || preferred[item.name] || item.name == opts.HeadBranch {
|
||||
if item.matched || item.recorded || preferred[item.name] || item.name == opts.HeadBranch {
|
||||
selected = append(selected, item)
|
||||
}
|
||||
}
|
||||
if len(selected) == 0 {
|
||||
selected = candidates
|
||||
}
|
||||
if recorded := slices.DeleteFunc(slices.Clone(selected), func(item candidate) bool { return !item.recorded }); len(recorded) > 0 {
|
||||
selected = recorded
|
||||
}
|
||||
if len(selected) > 1 {
|
||||
strong := make([]candidate, 0, len(selected))
|
||||
for _, item := range selected {
|
||||
@@ -183,7 +191,7 @@ func CleanPull(ctx context.Context, runner Runner, opts CleanOptions) (CleanResu
|
||||
if _, err := run("rev-parse", "--verify", "refs/heads/"+opts.DefaultBranch+"^{commit}"); err != nil {
|
||||
return CleanResult{}, fmt.Errorf("default branch is unavailable; create or fetch it before cleaning: %w", err)
|
||||
}
|
||||
current, err := run("symbolic-ref", "--short", "HEAD")
|
||||
current, err := CurrentBranch(ctx, runner, opts.Directory)
|
||||
if err == nil && current == opts.LocalBranch {
|
||||
if _, err := run("switch", "--quiet", "--", opts.DefaultBranch); err != nil {
|
||||
return CleanResult{}, fmt.Errorf("could not switch to %s before deleting %s: %w", opts.DefaultBranch, opts.LocalBranch, err)
|
||||
|
||||
@@ -15,7 +15,7 @@ type cleanRunner struct {
|
||||
calls []string
|
||||
dirty, missingLocal, missingDefault, onFeature bool
|
||||
branches string
|
||||
remoteTracking map[string]string
|
||||
remoteTracking, config map[string]string
|
||||
}
|
||||
|
||||
func (r *cleanRunner) Output(_ context.Context, dir string, prompt bool, args ...string) (string, error) {
|
||||
@@ -44,6 +44,11 @@ func (r *cleanRunner) Output(_ context.Context, dir string, prompt bool, args ..
|
||||
}
|
||||
return "", &CommandError{ExitCode: 1}
|
||||
}
|
||||
case "config":
|
||||
if value, ok := r.config[args[2]]; ok {
|
||||
return value, nil
|
||||
}
|
||||
return "", &CommandError{ExitCode: 1}
|
||||
case "status":
|
||||
if r.dirty {
|
||||
return " M file", nil
|
||||
@@ -81,6 +86,24 @@ func TestFindCleanBranchIgnoreSHAUsesName(t *testing.T) {
|
||||
require.Equal(t, "origin", remote)
|
||||
}
|
||||
|
||||
func TestFindCleanBranchIgnoreSHAPrefersTheBranchRecordedForThePull(t *testing.T) {
|
||||
runner := &cleanRunner{
|
||||
branches: "aaa111\tfeature\t\nbbb222\tbob/feature\t\n",
|
||||
config: map[string]string{"branch.bob/feature.tea-pr": "1"},
|
||||
}
|
||||
options := FindCleanBranchOptions{
|
||||
Directory: "/work", HeadSHA: "abc123", HeadBranch: "feature", IgnoreSHA: true, PullNumber: 1, MatchingRemotes: []string{"origin"},
|
||||
}
|
||||
branch, _, err := FindCleanBranch(context.Background(), runner, options)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "bob/feature", branch)
|
||||
// Without the record, OWNER/BRANCH is only preferred by name and the unrelated branch stays a candidate.
|
||||
runner.config = nil
|
||||
options.PreferredNames = []string{"bob/feature"}
|
||||
_, _, err = FindCleanBranch(context.Background(), runner, options)
|
||||
require.ErrorContains(t, err, "multiple local branches")
|
||||
}
|
||||
|
||||
func TestFindCleanBranchRejectsAmbiguity(t *testing.T) {
|
||||
runner := &cleanRunner{branches: "abc123\tone\t\nabc123\ttwo\t\n"}
|
||||
_, _, err := FindCleanBranch(context.Background(), runner, FindCleanBranchOptions{
|
||||
|
||||
@@ -92,7 +92,7 @@ func DiscoverRemote(ctx context.Context, runner Runner, dir, preferred string, r
|
||||
// CurrentUpstreamRemote names the checked-out branch and the remote it tracks; each is "" when
|
||||
// HEAD is detached or the branch tracks no remote.
|
||||
func CurrentUpstreamRemote(ctx context.Context, runner Runner, dir string) (branch, remote string) {
|
||||
branch, err := runner.Output(ctx, dir, false, "symbolic-ref", "--short", "HEAD")
|
||||
branch, err := CurrentBranch(ctx, runner, dir)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (r discoverRunner) Output(_ context.Context, dir string, prompt bool, args
|
||||
}
|
||||
|
||||
func TestDiscoverRemote(t *testing.T) {
|
||||
const head, tracking, base = "symbolic-ref --short HEAD", "config --get branch.main.remote", "config --get branch.main.tea-base-remote"
|
||||
const head, tracking, base = "symbolic-ref --quiet HEAD", "config --get branch.main.remote", "config --get branch.main.tea-base-remote"
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
runner discoverRunner
|
||||
@@ -55,7 +55,7 @@ func TestDiscoverRemote(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDiscoverRemoteSkipsUnusableRemotes(t *testing.T) {
|
||||
const head, tracking, base = "symbolic-ref --short HEAD", "config --get branch.main.remote", "config --get branch.main.tea-base-remote"
|
||||
const head, tracking, base = "symbolic-ref --quiet HEAD", "config --get branch.main.remote", "config --get branch.main.tea-base-remote"
|
||||
// gitea makes each named remote usable; a name=repository pair names its repository.
|
||||
gitea := func(names ...string) func(string) string {
|
||||
return func(remote string) string {
|
||||
|
||||
Reference in new issue
Block a user