feat(repo): sync forks on the server, wire forks into clones, validate set-default

repo sync OWNER/REPO, run where no Git remote points to the repository,
now updates it on the server: a fork's branch fast-forwards to its
parent's through merge-upstream, and a pull mirror fetches its source.

repo fork reports an existing fork ("! bob/x already exists") instead of
a raw HTTP 409, refuses to fork a repository into its own owner, and
gains --fork-name, --clone (Git flags after --) and --remote, which adds
the fork as origin and renames the old origin to upstream.

repo clone passes Git flags after --, and takes --upstream-remote-name
and --no-upstream for forks. Clones report what they set up.

repo set-default checks that the repository exists before saving it,
prints the saved repository with --view, removes it with --unset, and
reports the change.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:51:47 +02:00
1 parent 7cd13ef64e
commit 368de47c40
13 files changed
+599 -98

No files matched your search

+73 -12
View File
@@ -51,22 +51,83 @@ func (a *application) repoCommand() *cobra.Command {
Long: `Work with Gitea repositories: list and view them, create, fork, clone and migrate them, keep
local branches and forks in sync, and change their settings.`,
}
group.AddCommand(a.repoListCommand(), a.repoViewCommand(), a.repositorySyncCommand())
group.AddCommand(&cobra.Command{Use: "set-default [repository]", Short: "Save the selected repository in local Git configuration", Annotations: textOutput("set-default configures Git"), Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
if err := a.repositoryArgument(args); err != nil {
return err
}
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
_, err = a.git(cmd.Context(), "config", "--local", "tea.repository", target.Host+"/"+target.Repo)
return err
}})
group.AddCommand(a.repoListCommand(), a.repoViewCommand(), a.repositorySyncCommand(), a.repoSetDefaultCommand())
a.addRepositoryMutations(group)
return group
}
func (a *application) repoSetDefaultCommand() *cobra.Command {
var view, unset bool
command := &cobra.Command{
Use: "set-default [<repository>]",
Short: "Set the default repository for this clone",
Long: `Set the repository that tea commands use in the current Git clone, instead of the one found through
its Git remotes. It is saved as tea.repository in the clone's Git configuration.
Without an argument, the repository that tea selects now is saved.`,
Example: `# Make tea commands in this clone use the fork's parent
$ tea repo set-default alice/project
# Show the saved repository
$ tea repo set-default --view
# Go back to the repository of the Git remotes
$ tea repo set-default --unset`,
Annotations: map[string]string{"tea.arguments": repositoryArguments, "tea.text": "set-default configures Git"},
Args: usageArgs(cobra.MaximumNArgs(1)),
RunE: func(cmd *cobra.Command, args []string) error {
switch {
case view && unset:
return clierr.Wrap(clierr.Usage, errors.New("--view and --unset cannot be combined"))
case (view || unset) && len(args) > 0:
return clierr.Wrap(clierr.Usage, errors.New("--view and --unset take no repository"))
}
ctx := cmd.Context()
if inside, _ := a.gitProbe().Output(ctx, a.options.cwd, false, "rev-parse", "--is-inside-work-tree"); inside != "true" {
return clierr.Wrap(clierr.Usage, errors.New("set-default must run inside a Git clone"))
}
saved, _ := a.gitProbe().Output(ctx, a.options.cwd, false, "config", "--local", "--get", "tea.repository")
switch {
case view && saved == "":
_, err := fmt.Fprintln(cmd.ErrOrStderr(), "no default repository is set for this clone")
return err
case view:
_, err := fmt.Fprintln(cmd.OutOrStdout(), terminal.SafeLine(saved))
return err
case unset && saved == "":
a.warn(cmd, "No default repository is set for this clone")
return nil
case unset:
if _, err := a.git(ctx, "config", "--local", "--unset", "tea.repository"); err != nil {
return err
}
a.done(cmd, "Removed the default repository %s from this clone", saved)
return nil
}
if err := a.repositoryArgument(args); err != nil {
return err
}
t, err := a.resolve(ctx, true)
if err != nil {
return err
}
item, err := (services.Repositories{Client: t.client}).View(ctx, t.Repo)
if err != nil {
return err
}
if _, err := a.git(ctx, "config", "--local", "tea.repository", t.Host+"/"+item.FullName); err != nil {
return err
}
a.done(cmd, "Set %s as the default repository for this clone", item.FullName)
return nil
},
}
command.Flags().BoolVarP(&view, "view", "v", false, "Print the saved default repository")
command.Flags().BoolVarP(&unset, "unset", "u", false, "Remove the saved default repository")
command.ValidArgsFunction = a.completeRepos()
return command
}
func (a *application) repoListCommand() *cobra.Command {
var opts services.RepositoryListOptions
list := &cobra.Command{