185 lines
6.1 KiB
Go
185 lines
6.1 KiB
Go
package main
|
|
|
|
// deleteremote.go — the `deleteremote` command: remove the active project's
|
|
// repository from a public mirror server via that server's REST API.
|
|
//
|
|
// This is the only mgsh command that destroys something on a server it does not
|
|
// own, and nothing here can undo it. Four things keep it in check:
|
|
//
|
|
// - the server has to be named. `pushremote` without one means "every
|
|
// configured target", which is convenient there and catastrophic here.
|
|
// - the repository is looked up first, so a typo comes back as "not on that
|
|
// server" instead of as a DELETE nobody meant to send.
|
|
// - every deletion is confirmed on its own, spelling out owner/repo and the
|
|
// server it lives on, and the default answer is no.
|
|
// - the local clone is never touched. Only the git remote `pushremote`
|
|
// created is dropped, and only while it still points at what was deleted.
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// deleteRepo removes owner/repo from the server.
|
|
//
|
|
// The providers disagree on what success looks like: Gitea and GitHub answer
|
|
// 204, GitLab 202 — there the deletion is merely scheduled, and depending on
|
|
// the plan the project stays visible until its retention period is over.
|
|
func (r *remoteAPI) deleteRepo(owner, repo string) error {
|
|
code, data, err := r.do("DELETE", r.repoPath(owner, repo), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch code {
|
|
case 200, 202, 204:
|
|
return nil
|
|
case 403:
|
|
// the common failure by far: the token can push and create, but was
|
|
// never given the separate permission a deletion needs
|
|
return fmt.Errorf("not allowed to delete %s/%s — the token needs %s (HTTP 403): %s",
|
|
owner, repo, r.deletePermission(), firstLine(data))
|
|
default:
|
|
return fmt.Errorf("deleting repository failed (HTTP %d): %s", code, firstLine(data))
|
|
}
|
|
}
|
|
|
|
// deletePermission names what a provider wants from a token before it will
|
|
// delete a repository, because a bare "403" sends people to the wrong setting.
|
|
func (r *remoteAPI) deletePermission() string {
|
|
switch r.kind {
|
|
case kindGitHub:
|
|
return "the 'delete_repo' scope"
|
|
case kindGitLab:
|
|
return "the 'api' scope and the Owner role"
|
|
default: // Gitea
|
|
return "the 'write:repository' scope"
|
|
}
|
|
}
|
|
|
|
// parseRemoteSelectors splits `deleteremote <name|host> ...` into the targets
|
|
// it names. The '@' that `pushremote` requires is accepted but optional here:
|
|
// the command takes nothing but selectors, so there is no description a bare
|
|
// word could be mistaken for.
|
|
func parseRemoteSelectors(args string) []string {
|
|
var out []string
|
|
for _, f := range strings.Fields(args) {
|
|
if s := strings.TrimPrefix(f, "@"); s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// handleDeleteRemote implements `deleteremote <@name|host> ...`: it deletes the
|
|
// active project's repository on each named mirror server.
|
|
func handleDeleteRemote(args string) {
|
|
if !requireProject() {
|
|
return
|
|
}
|
|
targets, incomplete := cfg.mirrorTargets()
|
|
for _, n := range incomplete {
|
|
errorln("remote " + n + ": url or key missing — skipped")
|
|
}
|
|
if len(targets) == 0 {
|
|
errorln("deleteremote needs a 'remote.<name>.url' and 'remote.<name>.key' in " + configFile())
|
|
return
|
|
}
|
|
|
|
// no selector is never "all of them" — that is the whole point of the
|
|
// command taking one
|
|
sels := parseRemoteSelectors(args)
|
|
if len(sels) == 0 {
|
|
errorln("deleteremote needs the server to delete from: " + remoteChoices(targets))
|
|
return
|
|
}
|
|
|
|
targets = pickRemotes(targets, sels)
|
|
if len(targets) == 0 {
|
|
return // pickRemotes already named the selectors it did not recognise
|
|
}
|
|
|
|
repo := PRJ // like pushremote: the project names the repository
|
|
done := 0
|
|
for _, t := range targets {
|
|
if deleteOnRemote(t, repo) {
|
|
done++
|
|
}
|
|
}
|
|
if len(targets) > 1 {
|
|
fmt.Printf("%s %d/%d repositories deleted\n", col(cDark, "deleteremote:"), done, len(targets))
|
|
}
|
|
}
|
|
|
|
// remoteChoices lists the configured targets the way they may be selected, for
|
|
// the message a `deleteremote` without a target earns.
|
|
func remoteChoices(targets []RemoteTarget) string {
|
|
out := make([]string, 0, len(targets))
|
|
for _, t := range targets {
|
|
if h := remoteHost(t.URL); h != "" && !strings.EqualFold(h, t.Name) {
|
|
out = append(out, "@"+t.Name+" ("+h+")")
|
|
continue
|
|
}
|
|
out = append(out, "@"+t.Name)
|
|
}
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
// deleteOnRemote deletes repo on one target, after asking. It reports whether
|
|
// something was actually deleted — a repository that is not there, and a
|
|
// question answered with no, are not failures, and neither stops the remaining
|
|
// targets.
|
|
func deleteOnRemote(t RemoteTarget, repo string) bool {
|
|
api := newRemoteAPI(t.URL, t.Key, t.Type)
|
|
|
|
owner, err := api.authUser()
|
|
if err != nil {
|
|
errorln(t.Name + ": " + err.Error())
|
|
return false
|
|
}
|
|
fmt.Printf("%s %s %s (as %s)\n",
|
|
col(cDark, "remote"), col(cYellow, t.Name), col(cBlue, api.url), col(cGreen, owner))
|
|
|
|
exists, err := api.repoExists(owner, repo)
|
|
if err != nil {
|
|
errorln(t.Name + ": " + err.Error())
|
|
return false
|
|
}
|
|
if !exists {
|
|
fmt.Println(col(cDark, "no repository "+owner+"/"+repo+" there — nothing to delete"))
|
|
return false
|
|
}
|
|
|
|
fmt.Println(col(cOrange, "this deletes "+api.repoWebURL(owner, repo)+
|
|
" with its issues, releases and history, and cannot be undone"))
|
|
if !yesno("delete "+owner+"/"+repo+" on "+t.Name+"?", false) {
|
|
fmt.Println(col(cDark, "kept"))
|
|
return false
|
|
}
|
|
|
|
if err := api.deleteRepo(owner, repo); err != nil {
|
|
errorln(t.Name + ": " + err.Error())
|
|
return false
|
|
}
|
|
fmt.Println(col(cRed, "deleted ") + col(cWhite, owner+"/"+repo) + col(cDark, " on "+t.Name))
|
|
|
|
dropGitRemote(t.Name, api.repoWebURL(owner, repo))
|
|
return true
|
|
}
|
|
|
|
// dropGitRemote removes the local git remote named name, but only while it
|
|
// still points at web — the repository just deleted. A remote the user has
|
|
// since re-aimed somewhere else is theirs, not ours, and a `git push` failing
|
|
// against a repository that no longer exists is worse than no remote at all.
|
|
func dropGitRemote(name, web string) {
|
|
if !isDir(DIR + "/.git") {
|
|
return
|
|
}
|
|
url, err := gitCapture(DIR, "remote", "get-url", name)
|
|
if err != nil || strings.TrimSpace(url) != web {
|
|
return
|
|
}
|
|
if gitOK(DIR, "remote", "remove", name) {
|
|
fmt.Println(col(cDark, "removed git remote "+name))
|
|
}
|
|
}
|