fix(repo): sync a branch without switching to it

tea repo sync ran git switch on the synced branch, so syncing main while
on experiment left the user on main. A branch that is not checked out now
moves with update-ref, guarded by its old value; the checked-out branch
merges with --ff-only and needs a clean working tree; a missing branch is
created and set to track the remote. Diverged branches stay untouched.

Sync reports "✓ Synced main from origin/main (fast-forward a..b)" on a
terminal, "already up to date" when nothing changed, and the JSON result
gains the previous commit. Outside a clone, or without a remote for the
repository, the errors now name repo sync instead of checkout.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:51:43 +02:00
1 parent 33f6406a1e
commit ea2e9aadbb
6 files changed
+387 -188

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"errors"
"net/http"
"strings"
"testing"
"gitea.dev/tea/internal/clierr"
"github.com/stretchr/testify/require"
)
// syncGit answers the Git commands of a local repo sync in a clone whose origin is url, with
// experiment checked out and main one commit behind origin.
type syncGit struct {
url, inside string
calls []string
}
func (g *syncGit) Output(_ context.Context, _ string, _ bool, args ...string) (string, error) {
for len(args) > 2 && args[0] == "-c" {
args = args[2:]
}
call := strings.Join(args, " ")
g.calls = append(g.calls, call)
switch call {
case "rev-parse --is-inside-work-tree":
if g.inside == "" {
return "", errors.New("not a git repository")
}
return g.inside, nil
case "remote get-url origin":
return g.url, nil
case "symbolic-ref --quiet HEAD":
return "refs/heads/experiment", nil
case "rev-parse --verify --quiet refs/heads/main^{commit}":
return "1111111aaaa", nil
case "rev-parse --verify FETCH_HEAD^{commit}":
return "2222222bbbb", nil
}
return "", nil
}
func TestRepoSyncFastForwardsWithoutSwitchingBranches(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
deps, out, diagnostics := repoTTYDeps(t, server.URL)
runner := &syncGit{url: server.URL + "/alice/project.git", inside: "true"}
deps.Git = runner
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "sync", "-R", "alice/project", "--remote", "origin", "-b", "main"}))
require.Empty(t, out.String())
require.Equal(t, "✓ Synced main from origin/main (fast-forward 1111111..2222222)\n", diagnostics.String())
require.Contains(t, runner.calls, "update-ref -m tea repo sync refs/heads/main 2222222bbbb 1111111aaaa")
for _, call := range runner.calls {
require.False(t, strings.HasPrefix(call, "switch") || strings.HasPrefix(call, "checkout") || strings.HasPrefix(call, "merge "), call)
}
}
func TestRepoSyncOutsideACloneExplainsWhatItNeeds(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
deps, _, _ := serverDeps(t, server.URL)
deps.Git = &syncGit{}
err := Execute(context.Background(), deps, []string{"repo", "sync", "-R", "alice/project", "-b", "main"})
require.Equal(t, clierr.Usage, clierr.ExitCode(err), "%v", err)
require.ErrorContains(t, err, "repo sync must run inside a Git clone of alice/project")
}