// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package app import ( "errors" "strings" "gitea.dev/tea/internal/clierr" "gitea.dev/tea/internal/git" "gitea.dev/tea/internal/services" "gitea.dev/tea/internal/settings" "github.com/spf13/cobra" ) func (a *application) repositorySyncCommand() *cobra.Command { var branch string command := &cobra.Command{ Use: "sync []", Short: "Sync a local branch with its remote", Long: `Fast-forward a local branch to the branch of the same name on the repository's Git remote. The branch defaults to the repository's default branch. It is created when it is missing, and it is never checked out: the branch you are on stays checked out. When the branch is checked out, the working tree must be clean. Branches that have diverged from the remote are left unchanged.`, Example: `# Fast-forward the default branch from the repository's remote $ tea repo sync # Fast-forward another branch $ tea repo sync --branch release/1.0`, Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("branch") && strings.TrimSpace(branch) == "" { return clierr.Wrap(clierr.Usage, errors.New("--branch cannot be empty")) } if err := a.repositoryArgument(args); err != nil { return err } target, err := a.resolve(cmd.Context(), true) if err != nil { return err } if inside, _ := a.gitProbe().Output(cmd.Context(), a.options.cwd, false, "rev-parse", "--is-inside-work-tree"); inside != "true" { return clierr.Wrap(clierr.Usage, errors.New("repo sync must run inside a Git clone of "+target.Repo)) } snapshot, err := a.deps.Settings.Load() if err != nil { return err } remote, err := a.checkoutRemote(cmd.Context(), target, snapshot) if errors.Is(err, errNoMatchingRemote) { return clierr.Wrap(clierr.Usage, errors.New("no Git remote of this clone points to "+target.Repo+"; add one with git remote add, or pass --remote")) } if err != nil { return clierr.Wrap(clierr.Usage, err) } if branch == "" { repository, err := (services.Repositories{Client: target.client}).View(cmd.Context(), target.Repo) if err != nil { return err } branch = repository.DefaultBranch } raw, err := a.git(cmd.Context(), "remote", "get-url", remote) if err != nil { return err } if !target.ownsRemote(raw) { return clierr.Wrap(clierr.Usage, errors.New("selected Git remote changed; resolve the repository again before sync")) } credentials, err := a.gitTransportArguments(target, settings.RemoteIsSSH(raw)) if err != nil { return err } result, err := git.SyncBranch(cmd.Context(), a.deps.Git, git.SyncOptions{Directory: a.options.cwd, Remote: remote, Branch: branch, CredentialArgs: credentials, Prompt: a.policy(snapshot).CanPrompt(a.deps.Streams)}) if err != nil { return err } if a.options.json != "" { return a.writeJSON(cmd, result) } a.reportSync(cmd, result, remote+"/"+result.Branch) return nil }, } command.Flags().StringVarP(&branch, "branch", "b", "", "Branch to sync (default: the repository's default branch)") _ = command.RegisterFlagCompletionFunc("branch", a.completeBranches()) command.ValidArgsFunction = a.completeRepos() return command } // reportSync tells how a sync from source, such as origin/main, changed the branch. func (a *application) reportSync(cmd *cobra.Command, result git.SyncResult, source string) { switch result.Previous { case result.Commit: a.done(cmd, "%s is already up to date with %s", result.Branch, source) case "": a.done(cmd, "Created %s from %s (%s)", result.Branch, source, shortSHA(result.Commit)) default: a.done(cmd, "Synced %s from %s (fast-forward %s..%s)", result.Branch, source, shortSHA(result.Previous), shortSHA(result.Commit)) } } // shortSHA abbreviates a commit ID to seven characters, as Gitea and Git show them. func shortSHA(sha string) string { return sha[:min(len(sha), 7)] }