feat(repo): validate create templates, publish local projects, verify edits

repo create --license NOPE returned Gitea's HTTP 500 internals. License,
.gitignore and label templates are now checked against the server's
lists, case-insensitively, and complete with Tab. --source DIR adds the
new repository as a remote of a local Git repository and --push pushes
its current branch; --public is the explicit opposite of --private, and
-d/-g/-l and the new alias follow gh. --trust-model now offers the names
Gitea accepts. A prompted name is confirmed with the visibility, owner
and host, and creation reports "✓ Created repository ..." and the URL.

repo edit --default-branch nope exited 0 without changing anything; the
branch must now exist. Edit gains --visibility, --enable-issues/-wiki/
-pull-requests/-projects, --allow-merge-commit/-squash/-rebase,
--delete-branch-on-merge, --default-merge-style and --add-topic/
--remove-topic, and reports which settings it changed.

repo migrate names the repository after the source URL, validates
--service before prompting, and reports the import with the URL.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:51:49 +02:00
1 parent 368de47c40
commit 682c9f66a2
9 files changed
+742 -308

No files matched your search

+152 -67
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"net/url"
"path/filepath"
"strconv"
"strings"
"gitea.dev/tea/internal/clierr"
@@ -16,12 +17,9 @@ import (
"gitea.dev/tea/internal/settings"
"gitea.dev/tea/internal/terminal"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func repositoryRecord(item services.Repository) terminal.Record {
return terminal.Record{Heading: []terminal.Cell{{Text: item.FullName, Bold: true}}, Lines: []string{item.URL}}
}
func cloneURLForProtocol(cloneURL, sshURL, protocol string) (remote string, ssh bool, err error) {
remote = cloneURL
if protocol == "ssh" {
@@ -73,72 +71,159 @@ func mirrorSourceURL(item services.Repository) string {
}
func (a *application) addRepositoryMutations(group *cobra.Command) {
group.AddCommand(a.repoCreateCommand(), a.repoMigrateCommand(), a.repoForkCommand(), a.repoCloneCommand())
var description, repoName, website, defaultBranch string
var private, template, repoArchived, actionsEnabled bool
edit := &cobra.Command{Use: "edit [repository]", Short: "Edit repository settings", Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
if err := a.repositoryArgument(args); err != nil {
return err
}
options := services.RepositoryEdit{}
if cmd.Flags().Changed("name") {
if strings.TrimSpace(repoName) == "" {
return clierr.Wrap(clierr.Usage, errors.New("repository name cannot be empty"))
}
options.Name = &repoName
}
if cmd.Flags().Changed("website") {
options.Website = &website
}
if cmd.Flags().Changed("default-branch") {
if strings.TrimSpace(defaultBranch) == "" {
return clierr.Wrap(clierr.Usage, errors.New("default branch cannot be empty"))
}
options.DefaultBranch = &defaultBranch
}
if cmd.Flags().Changed("archived") {
options.Archived = &repoArchived
}
group.AddCommand(a.repoCreateCommand(), a.repoMigrateCommand(), a.repoForkCommand(), a.repoCloneCommand(), a.repoEditCommand())
group.AddCommand(a.repoArchiveCommand(true), a.repoArchiveCommand(false), a.repoRenameCommand(), a.repoDeleteCommand())
}
if cmd.Flags().Changed("description") {
options.Description = &description
}
if cmd.Flags().Changed("private") {
options.Private = &private
}
if cmd.Flags().Changed("as-template") {
options.Template = &template
}
if cmd.Flags().Changed("actions") {
options.ActionsEnabled = &actionsEnabled
}
if options == (services.RepositoryEdit{}) {
return clierr.Wrap(clierr.Usage, errors.New("supply an edit flag; see tea repo edit --help"))
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
if repoArchived {
if err := a.confirm(cmd, "Archive repository "+t.Repo); err != nil {
// repositorySwitches are the on/off settings of repo edit: each flag sets one field, and =false
// turns the setting off.
var repositorySwitches = []struct {
flag, usage string
field func(*services.RepositoryEdit) **bool
}{
{"private", "Make the repository private (=false makes it public)", func(e *services.RepositoryEdit) **bool { return &e.Private }},
{"as-template", "Make the repository a template", func(e *services.RepositoryEdit) **bool { return &e.Template }},
{"enable-issues", "Enable issues", func(e *services.RepositoryEdit) **bool { return &e.Issues }},
{"enable-wiki", "Enable the wiki", func(e *services.RepositoryEdit) **bool { return &e.Wiki }},
{"enable-pull-requests", "Enable pull requests", func(e *services.RepositoryEdit) **bool { return &e.PullRequests }},
{"enable-projects", "Enable projects", func(e *services.RepositoryEdit) **bool { return &e.Projects }},
{"actions", "Enable Actions", func(e *services.RepositoryEdit) **bool { return &e.ActionsEnabled }},
{"allow-merge-commit", "Allow merging pull requests with a merge commit", func(e *services.RepositoryEdit) **bool { return &e.MergeCommits }},
{"allow-squash", "Allow squash merging pull requests", func(e *services.RepositoryEdit) **bool { return &e.Squash }},
{"allow-rebase", "Allow rebase merging pull requests", func(e *services.RepositoryEdit) **bool { return &e.Rebase }},
{"delete-branch-on-merge", "Delete head branches after merging pull requests", func(e *services.RepositoryEdit) **bool { return &e.DeleteBranch }},
{"archived", "Archive the repository (=false unarchives it)", func(e *services.RepositoryEdit) **bool { return &e.Archived }},
}
func (a *application) repoEditCommand() *cobra.Command {
var name, website, defaultBranch, description, visibility, mergeStyle string
var addTopics, removeTopics []string
switches := make([]bool, len(repositorySwitches))
command := &cobra.Command{
Use: "edit [<repository>]",
Short: "Edit repository settings",
Long: `Edit the settings of a repository. Only the flags you pass change; on/off settings such as
--enable-wiki take =false to turn them off.`,
Example: `# Change the description and the website
$ tea repo edit --description "Command line tool" --website https://example.com
# Make a repository private and disable its wiki
$ tea repo edit alice/project --visibility private --enable-wiki=false
# Squash pull requests by default and tag the repository
$ tea repo edit --default-merge-style squash --add-topic cli --add-topic go`,
Annotations: map[string]string{"tea.arguments": repositoryArguments},
Args: usageArgs(cobra.MaximumNArgs(1)),
RunE: func(cmd *cobra.Command, args []string) error {
if err := a.repositoryArgument(args); err != nil {
return err
}
}
item, err := (services.Repositories{Client: t.client}).Edit(cmd.Context(), t.Repo, options)
if err != nil {
return err
}
return writeRecord(a, cmd, item, repositoryRecord)
}}
edit.Flags().StringVar(&repoName, "name", "", "Rename the repository")
edit.Flags().StringVar(&website, "website", "", "Repository website; empty clears")
edit.Flags().StringVar(&defaultBranch, "default-branch", "", "Default branch name")
edit.Flags().BoolVar(&repoArchived, "archived", false, "Archive repository; =false restores it")
edit.Flags().StringVar(&description, "description", "", "Repository description")
edit.Flags().BoolVar(&private, "private", false, "Make repository private (use --private=false to make public)")
edit.Flags().BoolVar(&template, "as-template", false, "Make repository a template (use =false to disable)")
edit.Flags().BoolVar(&actionsEnabled, "actions", false, "Enable Actions (use --actions=false to disable)")
group.AddCommand(edit, a.repoArchiveCommand(true), a.repoArchiveCommand(false), a.repoRenameCommand(), a.repoDeleteCommand())
changed := cmd.Flags().Changed
options := services.RepositoryEdit{}
for i, toggle := range repositorySwitches {
if changed(toggle.flag) {
*toggle.field(&options) = &switches[i]
}
}
for _, field := range []struct {
flag string
value *string
into **string
}{{"name", &name, &options.Name}, {"default-branch", &defaultBranch, &options.DefaultBranch}, {"default-merge-style", &mergeStyle, &options.MergeStyle}} {
if changed(field.flag) {
if strings.TrimSpace(*field.value) == "" {
return clierr.Wrap(clierr.Usage, fmt.Errorf("--%s cannot be empty", field.flag))
}
*field.into = field.value
}
}
if changed("website") {
options.Website = &website
}
if changed("description") {
options.Description = &description
}
if changed("visibility") {
if changed("private") {
return clierr.Wrap(clierr.Usage, errors.New("--visibility and --private cannot be combined"))
}
private := visibility == "private"
options.Private = &private
}
if err := requireNames("topic", "", addTopics, removeTopics); err != nil {
return err
}
if options == (services.RepositoryEdit{}) && len(addTopics)+len(removeTopics) == 0 {
return clierr.Wrap(clierr.Usage, errors.New("supply a setting to change; see tea repo edit --help"))
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
service := services.Repositories{Client: t.client}
if options.DefaultBranch != nil {
if _, err := (services.Branches{Client: t.client}).View(cmd.Context(), t.Repo, defaultBranch); err != nil {
return services.Classify(err, "branch", strconv.Quote(defaultBranch), t.Repo)
}
}
if options.Archived != nil && *options.Archived {
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Archive " + t.Repo + "? It becomes read-only.", NonTTY: "archive " + t.Repo}); err != nil {
return err
}
}
repo := t.Repo
if options != (services.RepositoryEdit{}) {
item, err := service.Edit(cmd.Context(), t.Repo, options)
if err != nil {
return err
}
repo = item.FullName
}
for _, topic := range addTopics {
if err := service.AddTopic(cmd.Context(), repo, topic); err != nil {
return fmt.Errorf("add topic %q: %w", topic, err)
}
}
for _, topic := range removeTopics {
if err := service.RemoveTopic(cmd.Context(), repo, topic); err != nil {
return fmt.Errorf("remove topic %q: %w", topic, err)
}
}
var edited []string
local := cmd.LocalFlags()
cmd.Flags().Visit(func(flag *pflag.Flag) {
if local.Lookup(flag.Name) != nil {
edited = append(edited, flag.Name)
}
})
a.done(cmd, "Edited repository %s (%s)", repo, strings.Join(edited, ", "))
if a.options.json == "" {
return nil
}
item, err := service.View(cmd.Context(), repo)
if err != nil {
return err
}
return a.writeJSON(cmd, item)
},
}
flags := command.Flags()
flags.StringVar(&name, "name", "", "Rename the repository")
_ = flags.MarkHidden("name")
flags.StringVarP(&description, "description", "d", "", "Repository description")
flags.StringVar(&website, "website", "", "Repository website (empty clears it)")
flags.StringVar(&defaultBranch, "default-branch", "", "Default branch; it must exist")
enumFlag(command, &visibility, "visibility", "", "", "Repository visibility", "public", "private")
enumFlag(command, &mergeStyle, "default-merge-style", "", "", "Default merge style for pull requests", "merge", "rebase", "rebase-merge", "squash", "fast-forward-only")
flags.StringSliceVar(&addTopics, "add-topic", nil, "Add topics (repeatable or comma-separated)")
flags.StringSliceVar(&removeTopics, "remove-topic", nil, "Remove topics (repeatable or comma-separated)")
for i, toggle := range repositorySwitches {
flags.BoolVar(&switches[i], toggle.flag, false, toggle.usage)
}
_ = flags.MarkHidden("archived")
_ = command.RegisterFlagCompletionFunc("default-branch", a.completeBranches())
command.ValidArgsFunction = a.completeRepos()
return command
}
func (a *application) repoCloneCommand() *cobra.Command {