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) } }