# HG changeset patch
# Node ID 72d08f29fe73
# Parent  2d7cd54a5666
diff --git a/access/access.go b/access/access.go
--- a/access/access.go
+++ b/access/access.go
@@ -118,3 +118,7 @@ func CanWrite(user, repo string) bool {
 func CanInit(user, repo string) bool {
 	return check(user, repo, "init")
 }
+
+func CanRemove(user, repo string) bool {
+	return check(user, repo, "remove")
+}
diff --git a/access/hgweb.go b/access/hgweb.go
--- a/access/hgweb.go
+++ b/access/hgweb.go
@@ -8,6 +8,14 @@ import (
 	"strings"
 )
 
+var (
+	repositories map[string]bool
+)
+
+func IsExistingRepo(repo string) bool {
+	return repositories[repo]
+}
+
 func refreshHgWeb() error {
 	fp, err := os.OpenFile(hgwebConfigPath, os.O_WRONLY|os.O_TRUNC, 0644)
 	if err != nil {
@@ -22,6 +30,8 @@ func refreshHgWeb() error {
 		return err
 	}
 
+	repositories = make(map[string]bool)
+
 	// walk the reposPath, looking for .hg directories, when one is found,
 	// check if it is publicly readable, and if so, add it to the config file.
 	filepath.WalkDir(absReposPath, func(filename string, entry os.DirEntry, err error) error {
@@ -40,6 +50,7 @@ func refreshHgWeb() error {
 		// include the parent directory.
 		repoPath := path.Dir(filename)
 		relativeRepoPath := strings.TrimPrefix(repoPath, absReposPath)
+		repositories[strings.Trim(relativeRepoPath, "/")] = true
 
 		// check if it's publicly readable
 		if CanRead("public", relativeRepoPath) {
diff --git a/hg/parser.go b/hg/parser.go
--- a/hg/parser.go
+++ b/hg/parser.go
@@ -15,6 +15,10 @@ type CommandArguments struct {
 			Repo string `kong:"arg"`
 		} `kong:"cmd"`
 	} `kong:"cmd"`
+
+	Rm struct {
+		Repo string `kong:"arg"`
+	} `kong:"cmd"`
 }
 
 func ParseCommandArguments(cmd string) (string, CommandArguments, error) {
diff --git a/setup/resources/policy.csv.template b/setup/resources/policy.csv.template
--- a/setup/resources/policy.csv.template
+++ b/setup/resources/policy.csv.template
@@ -21,8 +21,10 @@
 # More than one policy can match, but if any of the matching policies is a deny,
 # then the deny is honored and the user is denied permission.
 
-# give users in the admins group the ability to create repositories everywhere.
+# give users in the admins group the ability to create and remove
+# repositories everywhere. The admin repository /hgkeeper cannot be removed.
 p, admins, /*, init, allow
+p, admins, /*, remove, allow
 
 # deny authenticated, but not explicitly defined users read access to the
 # {{.AdminRepo}} repo
@@ -53,5 +55,8 @@ g, {{.AdminUsername}}, admins
 # give the write action read permission
 g2, read, write
 
+# give the remove action read permission
+g2, read, remove
+
 # give the init action write permission (which has read)
 g2, write, init
diff --git a/ssh/commands/commands.go b/ssh/commands/commands.go
--- a/ssh/commands/commands.go
+++ b/ssh/commands/commands.go
@@ -38,6 +38,8 @@ func Find(cmd, reposPath string) (Comman
 		return normalize(reposPath, values.Hg.Repo, NewServe)
 	case "hg init <repo>":
 		return normalize(reposPath, values.Hg.Init.Repo, NewInit)
+	case "rm <repo>":
+		return normalize(reposPath, values.Rm.Repo, NewRemove)
 	default:
 		return nil, fmt.Errorf("unknown command %s", cmd)
 	}
diff --git a/ssh/commands/rm.go b/ssh/commands/rm.go
new file mode 100644
--- /dev/null
+++ b/ssh/commands/rm.go
@@ -0,0 +1,61 @@
+package commands
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/gliderlabs/ssh"
+	"go.uber.org/zap"
+
+	"keep.imfreedom.org/grim/hgkeeper/access"
+)
+
+type Remove struct {
+	repoPath string
+	repoName string
+}
+
+func NewRemove(reposPath, repoName string) Command {
+	return &Remove{
+		repoPath: filepath.Join(reposPath, repoName),
+		repoName: repoName,
+	}
+}
+
+func (i *Remove) Run(session ssh.Session, username string) error {
+	if !access.CanRemove(username, "/"+i.repoName) {
+		return fmt.Errorf("access denied")
+	}
+
+	err := error(nil)
+	if access.IsExistingRepo(i.repoName) {
+		err = i.removePath(session)
+	} else {
+		err = fmt.Errorf("repository not found: %s", i.repoName)
+	}
+
+	if err != nil {
+		sendResponse(err.Error(), session)
+	}
+	return err
+}
+
+func (i *Remove) removePath(session ssh.Session) error {
+	if i.repoName == access.AdminRepo() {
+		return fmt.Errorf("repository protected: %s", i.repoName)
+	}
+
+	err := os.RemoveAll(i.repoPath)
+	if err == nil {
+		sendResponse("repository removed: "+i.repoName, session)
+		zap.S().Info("refreshing access control for removed repo")
+		return access.Refresh()
+	}
+	return err
+}
+
+func sendResponse(content string, session ssh.Session) {
+	session.Write([]byte(content))
+	session.Write([]byte("\n"))
+}
