Files
simplegit/cmd/gitctl/actions.go
T
2026-07-29 06:51:43 -04:00

325 lines
8.8 KiB
Go

package gitctl
import (
"context"
"fmt"
"strings"
"time"
"connectrpc.com/connect"
v1 "simplegit/gitrpc/v1"
)
type field struct {
name string
hint string
secret bool
numeric bool
optional bool
}
type action struct {
label string
confirm string
fields []field
hasPerm bool
destructive bool
immediate bool
run func(ctx context.Context, b *backend, vals map[string]string, perm string) (string, error)
}
var perms = []string{"read", "write", "admin"}
func nextPerm(p string) string {
switch p {
case "read":
return "write"
case "write":
return "admin"
default:
return "read"
}
}
func opStatus() action {
return action{
label: "Server status", immediate: true,
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
st, err := b.manage.Status(ctx)
if err != nil {
return "", err
}
return formatStatus(st), nil
},
}
}
func opCreateRepo() action {
return action{
label: "Create repo",
fields: []field{{name: "ns", hint: "owner (may be nested: org/team)"}, {name: "name", hint: "repo name"}},
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
if err := b.manage.UpsertRepo(ctx, v["ns"], v["name"], false); err != nil {
return "", err
}
return fmt.Sprintf("created %s/%s", v["ns"], v["name"]), nil
},
}
}
func opCreateNS() action {
return action{
label: "New namespace",
fields: []field{{name: "ns", hint: "namespace name"}},
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
if err := b.manage.CreateNS(ctx, v["ns"]); err != nil {
return "", err
}
return fmt.Sprintf("created namespace %s", v["ns"]), nil
},
}
}
func opChangePassword(ns string) action {
return action{
label: "Change password",
fields: []field{{name: "password", hint: "new password (empty clears)", secret: true, optional: true}},
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
if err := b.manage.UserPasswordUpsert(ctx, ns, v["password"]); err != nil {
return "", err
}
return fmt.Sprintf("updated password for %s", ns), nil
},
}
}
func opRemovePassword(ns string) action {
return action{
label: "Remove password", immediate: true, destructive: true,
confirm: fmt.Sprintf("remove password login from namespace %s", ns),
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
if err := b.manage.UserDelete(ctx, ns); err != nil {
return "", err
}
return fmt.Sprintf("removed password from %s", ns), nil
},
}
}
func opDeleteNS(ns string) action {
return action{
label: "Delete namespace", immediate: true, destructive: true,
confirm: fmt.Sprintf("delete empty namespace %s and its namespace-level grants", ns),
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
if err := b.manage.DeleteNS(ctx, ns); err != nil {
return "", err
}
return fmt.Sprintf("deleted namespace %s", ns), nil
},
}
}
func opDeleteRepo(ns, name string) action {
return action{
label: "Delete repo", immediate: true, destructive: true,
confirm: fmt.Sprintf("delete repo %s/%s (and its repo-level grants, and the on-disk bare repo)", ns, name),
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
if err := b.manage.DeleteRepo(ctx, ns, name); err != nil {
return "", err
}
return fmt.Sprintf("deleted %s/%s", ns, name), nil
},
}
}
func opToggleRepoVisibility(repo repoRow) action {
next := !repo.IsPrivate
visibility := "public"
if next {
visibility = "private"
}
return action{
label: "Set repo " + visibility, immediate: true,
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
if err := b.manage.UpsertRepo(ctx, repo.Ns, repo.Name, next); err != nil {
return "", err
}
return fmt.Sprintf("set %s/%s %s", repo.Ns, repo.Name, visibility), nil
},
}
}
func opMoveRepo(src string) action {
return action{
label: "Move / rename repo",
fields: []field{{name: "dst", hint: "ns/name (new)"}},
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
if err := b.manage.MoveRepo(ctx, src, v["dst"]); err != nil {
return "", err
}
return fmt.Sprintf("moved %s -> %s", src, v["dst"]), nil
},
}
}
func opMoveNS(src string) action {
return action{
label: "Move / rename namespace",
fields: []field{{name: "dst", hint: "namespace (new)"}},
run: func(ctx context.Context, b *backend, v map[string]string, _ string) (string, error) {
if err := b.manage.MoveNS(ctx, src, v["dst"]); err != nil {
return "", err
}
return fmt.Sprintf("moved namespace %s -> %s", src, v["dst"]), nil
},
}
}
func opGrantSSHKey() action {
return action{
label: "Grant SSH key",
fields: []field{{name: "target", hint: "ns/name (repo) or ns (namespace; nested ok)"}, {name: "key", hint: "ssh-ed25519 AAAA... comment"}},
hasPerm: true,
run: func(ctx context.Context, b *backend, v map[string]string, perm string) (string, error) {
return grantSSHOn(ctx, b, v["target"], v["key"], perm)
},
}
}
func opGrantPAT() action {
return action{
label: "Grant PAT",
fields: []field{{name: "target", hint: "ns/name (repo) or ns (namespace; nested ok)"}, {name: "pat", hint: "plaintext PAT", secret: true}},
hasPerm: true,
run: func(ctx context.Context, b *backend, v map[string]string, perm string) (string, error) {
return grantPATOn(ctx, b, v["target"], v["pat"], perm)
},
}
}
func grantSSHOn(ctx context.Context, b *backend, tgt, key, perm string) (string, error) {
if strings.Contains(tgt, "/") {
id, err := b.manage.ACLUpsertSSHKeyOnRepo(ctx, tgt, key, perm)
if err == nil {
return fmt.Sprintf("granted %s on repo %s (ssh, acl_id=%d)", perm, tgt, id), nil
}
if connect.CodeOf(err) != connect.CodeNotFound {
return "", err
}
}
id, err := b.manage.ACLUpsertSSHKeyOnNS(ctx, tgt, key, perm)
if err != nil {
return "", err
}
return fmt.Sprintf("granted %s on namespace %s (ssh, acl_id=%d)", perm, tgt, id), nil
}
func grantPATOn(ctx context.Context, b *backend, tgt, pat, perm string) (string, error) {
if strings.Contains(tgt, "/") {
id, err := b.manage.ACLUpsertPATOnRepo(ctx, tgt, pat, perm)
if err == nil {
return fmt.Sprintf("granted %s on repo %s (pat, acl_id=%d)", perm, tgt, id), nil
}
if connect.CodeOf(err) != connect.CodeNotFound {
return "", err
}
}
id, err := b.manage.ACLUpsertPATOnNS(ctx, tgt, pat, perm)
if err != nil {
return "", err
}
return fmt.Sprintf("granted %s on namespace %s (pat, acl_id=%d)", perm, tgt, id), nil
}
func opACLDelete(id int64) action {
return action{
label: "Delete ACL grant", immediate: true, destructive: true,
confirm: fmt.Sprintf("delete ACL grant #%d", id),
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
if err := b.manage.ACLDelete(ctx, id); err != nil {
return "", err
}
return fmt.Sprintf("deleted ACL grant %d", id), nil
},
}
}
func opRotatePerm(g grantRow) action {
return action{
label: "Rotate grant perm", immediate: true,
run: func(ctx context.Context, b *backend, _ map[string]string, _ string) (string, error) {
next := nextPerm(g.Perm)
if err := b.manage.ACLSetPerm(ctx, g.AclID, next); err != nil {
return "", err
}
return fmt.Sprintf("rotated %s grant on %s: %s -> %s", g.CredType, g.TargetLabel, g.Perm, next), nil
},
}
}
func formatStatus(st *v1.StatusResponse) string {
started := "-"
if st.StartedAt != nil {
started = fmtTime(st.StartedAt.AsTime())
}
var b strings.Builder
fmt.Fprintln(&b, "simplegit daemon")
fmt.Fprintf(&b, " root %s\n", st.Root)
fmt.Fprintf(&b, " http %s\n", orDash(st.RpcAddr))
fmt.Fprintf(&b, " ssh %s\n", orDash(st.SshAddr))
fmt.Fprintf(&b, " gitrpc %s\n", orDash(st.ManageAddr))
fmt.Fprintf(&b, " started %s (up %s)", started, fmtDuration(st.UptimeSeconds))
return b.String()
}
func grantIdentity(g grantRow) string {
switch g.CredType {
case "pat":
s := g.PatPrefix
if g.PatName != "" {
s += " (" + g.PatName + ")"
}
return s
case "ssh":
s := g.SshKeyType + " " + g.SshFingerprint
if g.SshComment != "" {
s += " (" + g.SshComment + ")"
}
return s
}
return ""
}
func orDash(s string) string {
if s == "" {
return "-"
}
return s
}
func fmtTime(t time.Time) string {
if t.IsZero() {
return "-"
}
return t.Format("2006-01-02 15:04")
}
func fmtDuration(secs int64) string {
d := time.Duration(secs) * time.Second
if d < 0 {
d = 0
}
h := int(d.Hours())
m := int(d.Minutes()) % 60
s := int(d.Seconds()) % 60
switch {
case h > 0:
return fmt.Sprintf("%dh%dm", h, m)
case m > 0:
return fmt.Sprintf("%dm%ds", m, s)
default:
return fmt.Sprintf("%ds", s)
}
}