mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
feat(repo): add repo unarchive and repo rename, and report archive and delete
repo unarchive restores an archived repository, and repo rename renames one, moving the Git remote or the saved default repository that selected it to the new name. The archived-repository hint now points to tea repo unarchive. repo archive asks "Archive alice/x? It becomes read-only." and warns "! alice/x is already archived" without a request when there is nothing to do. Archive, unarchive, rename and delete confirm with a ✓ line on a terminal, and --json returns the repository. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
6 files changed
+327
-46
No files matched your search
+223
-40
@@ -154,46 +154,7 @@ func (a *application) addRepositoryMutations(group *cobra.Command) {
|
||||
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)
|
||||
for _, verb := range []string{"archive", "delete"} {
|
||||
command := &cobra.Command{Use: verb + " [repository]", Short: titleCase(verb) + " the selected repository", Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := a.repositoryArgument(args); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inferred := ""
|
||||
if t.RepoSource != "--repo" {
|
||||
source := strings.Replace(t.RepoSource, "remote ", "git remote ", 1)
|
||||
if a.options.yes {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("--yes requires an explicit repository, not one inferred from %s: tea repo %s %s --yes", source, verb, t.Repo))
|
||||
}
|
||||
inferred = " (from " + source + ")"
|
||||
}
|
||||
service := services.Repositories{Client: t.client}
|
||||
if verb == "delete" {
|
||||
if err := a.confirmTyped(cmd, "Delete "+t.Repo+inferred+"? This cannot be undone.", t.Repo); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.Delete(cmd.Context(), t.Repo)
|
||||
}
|
||||
if err := a.confirm(cmd, "Archive "+t.Repo+inferred+"?"); err != nil {
|
||||
return err
|
||||
}
|
||||
archived := true
|
||||
item, err := service.Edit(cmd.Context(), t.Repo, services.RepositoryEdit{Archived: &archived})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeRecord(a, cmd, item, repositoryRecord)
|
||||
}}
|
||||
if verb == "delete" {
|
||||
command.Annotations = textOutput("repository deletion does not return a resource")
|
||||
}
|
||||
group.AddCommand(command)
|
||||
}
|
||||
group.AddCommand(edit, a.repoArchiveCommand(true), a.repoArchiveCommand(false), a.repoRenameCommand(), a.repoDeleteCommand())
|
||||
var depth int
|
||||
clone := &cobra.Command{Use: "clone <repository> [directory]", Short: "Clone a repository using the configured Git protocol", Args: usageArgs(cobra.RangeArgs(1, 2)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if depth < 0 {
|
||||
@@ -295,3 +256,225 @@ func (a *application) cloneRepository(cmd *cobra.Command, t target, item service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inferredRepository words where a repository that no argument or --repo named came from, as in
|
||||
// " (from git remote upstream)", for confirmations. Irreversible commands refuse --yes for it.
|
||||
func (a *application) inferredRepository(t target, verb string, refuseYes bool) (string, error) {
|
||||
if t.RepoSource == "--repo" {
|
||||
return "", nil
|
||||
}
|
||||
source := describeRepoSource(t.RepoSource)
|
||||
if refuseYes && a.options.yes {
|
||||
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("--yes requires an explicit repository, not one inferred from %s: tea repo %s %s --yes", source, verb, t.Repo))
|
||||
}
|
||||
return " (from " + source + ")", nil
|
||||
}
|
||||
|
||||
// describeRepoSource words a repository source for people: git remote upstream, TEA_REPO, ...
|
||||
func describeRepoSource(source string) string {
|
||||
return strings.Replace(source, "remote ", "git remote ", 1)
|
||||
}
|
||||
|
||||
// repoArchiveCommand builds repo archive, or repo unarchive.
|
||||
func (a *application) repoArchiveCommand(archive bool) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "archive [<repository>]",
|
||||
Short: "Archive a repository",
|
||||
Long: `Archive a repository. An archived repository is read-only: nobody can push to it or change its
|
||||
issues, pull requests, releases or wiki until it is unarchived with tea repo unarchive.`,
|
||||
Example: `# Archive a repository
|
||||
$ tea repo archive alice/old-project
|
||||
|
||||
# Archive without a prompt; scripts must name the repository
|
||||
$ tea repo archive alice/old-project --yes`,
|
||||
Annotations: map[string]string{"tea.arguments": repositoryArguments, "tea.confirm": "true"},
|
||||
Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||
}
|
||||
if !archive {
|
||||
command.Use, command.Short = "unarchive [<repository>]", "Unarchive a repository"
|
||||
command.Long = "Unarchive an archived repository, so that it accepts pushes and changes again."
|
||||
command.Example = "$ tea repo unarchive alice/old-project"
|
||||
delete(command.Annotations, "tea.confirm")
|
||||
}
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
if err := a.repositoryArgument(args); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inferred, err := a.inferredRepository(t, "archive", archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Repositories{Client: t.client}
|
||||
item, err := service.View(cmd.Context(), t.Repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if item.Archived == archive {
|
||||
if archive {
|
||||
a.warn(cmd, "%s is already archived", item.FullName)
|
||||
} else {
|
||||
a.warn(cmd, "%s is not archived", item.FullName)
|
||||
}
|
||||
return a.writeMutationJSON(cmd, item)
|
||||
}
|
||||
if archive {
|
||||
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Archive " + t.Repo + inferred + "? It becomes read-only.", NonTTY: "archive " + t.Repo}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
item, err = service.Edit(cmd.Context(), t.Repo, services.RepositoryEdit{Archived: &archive})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if archive {
|
||||
a.done(cmd, "Archived repository %s", item.FullName)
|
||||
} else {
|
||||
a.done(cmd, "Unarchived repository %s", item.FullName)
|
||||
}
|
||||
return a.writeMutationJSON(cmd, item)
|
||||
}
|
||||
command.ValidArgsFunction = a.completeRepos()
|
||||
return command
|
||||
}
|
||||
|
||||
// writeMutationJSON writes the result of a mutation with --json; people get done lines instead.
|
||||
func (a *application) writeMutationJSON(cmd *cobra.Command, item any) error {
|
||||
if a.options.json == "" {
|
||||
return nil
|
||||
}
|
||||
return a.writeJSON(cmd, item)
|
||||
}
|
||||
|
||||
func (a *application) repoRenameCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "rename <new-name>",
|
||||
Short: "Rename a repository",
|
||||
Long: `Rename a repository; its owner stays the same. Gitea redirects the old name to the new one. When
|
||||
the repository was found through a Git remote of the current clone, or through tea repo set-default,
|
||||
that remote or setting moves to the new name.`,
|
||||
Example: `# Rename the repository of the current clone
|
||||
$ tea repo rename new-name
|
||||
|
||||
# Rename another repository
|
||||
$ tea repo rename new-name -R alice/old-name`,
|
||||
Annotations: map[string]string{"tea.confirm": "true"},
|
||||
Args: argsExact(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := strings.TrimSpace(args[0])
|
||||
if err := validateRepositoryName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owner, current, _ := strings.Cut(t.Repo, "/")
|
||||
if current == name {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("%s is already named %s", t.Repo, name))
|
||||
}
|
||||
inferred, err := a.inferredRepository(t, "rename", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inferred != "" {
|
||||
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Rename " + t.Repo + inferred + " to " + owner + "/" + name + "?", DefaultYes: true, NonTTY: "rename " + t.Repo}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
item, err := (services.Repositories{Client: t.client}).Edit(cmd.Context(), t.Repo, services.RepositoryEdit{Name: &name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.done(cmd, "Renamed repository %s to %s", t.Repo, item.FullName)
|
||||
if err := a.followRename(cmd, t, current, item); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.writeMutationJSON(cmd, item)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// followRename points the Git remote or the saved default repository that selected a renamed
|
||||
// repository, previously named old, at its new name.
|
||||
func (a *application) followRename(cmd *cobra.Command, t target, old string, item services.Repository) error {
|
||||
if remote, ok := strings.CutPrefix(t.RepoSource, "remote "); ok {
|
||||
raw, err := a.git(cmd.Context(), "remote", "get-url", remote)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renamed %s, but could not read Git remote %s: %w", item.FullName, remote, err)
|
||||
}
|
||||
renamed, ok := renamedURL(raw, old, item.Name)
|
||||
if !ok {
|
||||
a.warn(cmd, "Git remote %s still points to %s; update it with git remote set-url", remote, t.Repo)
|
||||
return nil
|
||||
}
|
||||
if _, err := a.git(cmd.Context(), "remote", "set-url", remote, renamed); err != nil {
|
||||
return fmt.Errorf("renamed %s, but could not update Git remote %s: %w", item.FullName, remote, err)
|
||||
}
|
||||
a.done(cmd, "Updated Git remote %s to %s", remote, item.FullName)
|
||||
}
|
||||
if t.RepoSource == "git config tea.repository" {
|
||||
if _, err := a.git(cmd.Context(), "config", "--local", "tea.repository", t.Host+"/"+item.FullName); err != nil {
|
||||
return fmt.Errorf("renamed %s, but could not update the default repository: %w", item.FullName, err)
|
||||
}
|
||||
a.done(cmd, "Set %s as the default repository for this clone", item.FullName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renamedURL replaces the repository name old at the end of a remote URL with name, keeping the
|
||||
// rest of the URL, such as an SSH host alias, and its .git suffix.
|
||||
func renamedURL(raw, old, name string) (string, bool) {
|
||||
base, suffix := strings.TrimSuffix(raw, "/"), ""
|
||||
if trimmed, ok := strings.CutSuffix(base, ".git"); ok {
|
||||
base, suffix = trimmed, ".git"
|
||||
}
|
||||
if len(base) <= len(old) || !strings.EqualFold(base[len(base)-len(old):], old) || !strings.ContainsRune("/:", rune(base[len(base)-len(old)-1])) {
|
||||
return "", false
|
||||
}
|
||||
return base[:len(base)-len(old)] + name + suffix, true
|
||||
}
|
||||
|
||||
func (a *application) repoDeleteCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "delete [<repository>]",
|
||||
Short: "Delete a repository",
|
||||
Long: `Delete a repository with its issues, pull requests, releases and wiki. This cannot be undone.
|
||||
|
||||
Type the repository's name to confirm. With --yes, name the repository explicitly.`,
|
||||
Example: `# Delete a repository after typing its name
|
||||
$ tea repo delete alice/old-project
|
||||
|
||||
# Delete without a prompt
|
||||
$ tea repo delete alice/old-project --yes`,
|
||||
Annotations: map[string]string{"tea.arguments": repositoryArguments, "tea.confirm": "true", "tea.text": "repository deletion does not return a resource"},
|
||||
Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := a.repositoryArgument(args); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inferred, err := a.inferredRepository(t, "delete", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + t.Repo + inferred + "? This cannot be undone.", Typed: t.Repo, NonTTY: "delete " + t.Repo}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := (services.Repositories{Client: t.client}).Delete(cmd.Context(), t.Repo); err != nil {
|
||||
return err
|
||||
}
|
||||
a.done(cmd, "Deleted repository %s", t.Repo)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
command.ValidArgsFunction = a.completeRepos()
|
||||
return command
|
||||
}
|
||||
Reference in new issue
Block a user