diff --git a/config/auth.go b/config/auth.go index f977e410046d93b286d9122e5225dc467d047a1b..1e444226a424cd41756335b80447eb5fc4db34c7 100644 --- a/config/auth.go +++ b/config/auth.go @@ -4,7 +4,7 @@ import ( "log" "strings" - gm "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) @@ -39,40 +39,40 @@ func (cfg *Config) Fetch(repo string, pk ssh.PublicKey) { } // AuthRepo grants repo authorization to the given key. -func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel { +func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) proto.AccessLevel { return cfg.accessForKey(repo, pk) } // PasswordHandler returns whether or not password access is allowed. func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool { - return (cfg.AnonAccess != gm.NoAccess.String()) && cfg.AllowKeyless + return (cfg.AnonAccess != proto.NoAccess.String()) && cfg.AllowKeyless } // KeyboardInteractiveHandler returns whether or not keyboard interactive is allowed. func (cfg *Config) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool { - return (cfg.AnonAccess != gm.NoAccess.String()) && cfg.AllowKeyless + return (cfg.AnonAccess != proto.NoAccess.String()) && cfg.AllowKeyless } // PublicKeyHandler returns whether or not the given public key may access the // repo. func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool { - return cfg.accessForKey("", pk) != gm.NoAccess + return cfg.accessForKey("", pk) != proto.NoAccess } -func (cfg *Config) anonAccessLevel() gm.AccessLevel { +func (cfg *Config) anonAccessLevel() proto.AccessLevel { cfg.mtx.RLock() defer cfg.mtx.RUnlock() switch cfg.AnonAccess { case "no-access": - return gm.NoAccess + return proto.NoAccess case "read-only": - return gm.ReadOnlyAccess + return proto.ReadOnlyAccess case "read-write": - return gm.ReadWriteAccess + return proto.ReadWriteAccess case "admin-access": - return gm.AdminAccess + return proto.AdminAccess default: - return gm.NoAccess + return proto.NoAccess } } @@ -82,7 +82,7 @@ func (cfg *Config) anonAccessLevel() gm.AccessLevel { // config.AnonAccess. // If repo exists, and private, then admins and collabs are allowed access. // If repo exists, and not private, then access is based on config.AnonAccess. -func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel { +func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) proto.AccessLevel { anon := cfg.anonAccessLevel() private := cfg.isPrivate(repo) // Find user @@ -92,24 +92,24 @@ func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel { apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k))) if err != nil { log.Printf("error: malformed authorized key: '%s'", k) - return gm.NoAccess + return proto.NoAccess } if ssh.KeysEqual(pk, apk) { if user.Admin { - return gm.AdminAccess + return proto.AdminAccess } u := user if cfg.isCollab(repo, &u) { - if anon > gm.ReadWriteAccess { + if anon > proto.ReadWriteAccess { return anon } - return gm.ReadWriteAccess + return proto.ReadWriteAccess } if !private { - if anon > gm.ReadOnlyAccess { + if anon > proto.ReadOnlyAccess { return anon } - return gm.ReadOnlyAccess + return proto.ReadOnlyAccess } } } @@ -118,7 +118,7 @@ func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel { // Don't restrict access to private repos if no users are configured. // Return anon access level. if private && len(cfg.Users) > 0 { - return gm.NoAccess + return proto.NoAccess } return anon } diff --git a/config/auth_test.go b/config/auth_test.go index a3be357e8b4157097108db0c9b09ef2bf0445343..626ff0e8a180bc4db47378268b84c6c32216c0fc 100644 --- a/config/auth_test.go +++ b/config/auth_test.go @@ -3,7 +3,7 @@ package config import ( "testing" - "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/gliderlabs/ssh" "github.com/matryer/is" ) @@ -18,12 +18,12 @@ func TestAuth(t *testing.T) { cfg Config repo string key ssh.PublicKey - access git.AccessLevel + access proto.AccessLevel }{ // Repo access { name: "anon access: no-access, anonymous user", - access: git.NoAccess, + access: proto.NoAccess, repo: "foo", cfg: Config{ AnonAccess: "no-access", @@ -36,7 +36,7 @@ func TestAuth(t *testing.T) { }, { name: "anon access: no-access, anonymous user with admin user", - access: git.NoAccess, + access: proto.NoAccess, repo: "foo", cfg: Config{ AnonAccess: "no-access", @@ -59,7 +59,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, authd user", key: dummyPk, repo: "foo", - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "no-access", Repos: []RepoConfig{ @@ -80,7 +80,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, anonymous user with admin user", key: dummyPk, repo: "foo", - access: git.NoAccess, + access: proto.NoAccess, cfg: Config{ AnonAccess: "no-access", Repos: []RepoConfig{ @@ -102,7 +102,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, admin user", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "no-access", Repos: []RepoConfig{ @@ -123,7 +123,7 @@ func TestAuth(t *testing.T) { { name: "anon access: read-only, anonymous user", repo: "foo", - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "read-only", Repos: []RepoConfig{ @@ -137,7 +137,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-only, authd user", repo: "foo", key: dummyPk, - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "read-only", Repos: []RepoConfig{ @@ -158,7 +158,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-only, admin user", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "read-only", Repos: []RepoConfig{ @@ -179,7 +179,7 @@ func TestAuth(t *testing.T) { { name: "anon access: read-write, anonymous user", repo: "foo", - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "read-write", Repos: []RepoConfig{ @@ -193,7 +193,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-write, authd user", repo: "foo", key: dummyPk, - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "read-write", Repos: []RepoConfig{ @@ -213,7 +213,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-write, admin user", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "read-write", Repos: []RepoConfig{ @@ -234,7 +234,7 @@ func TestAuth(t *testing.T) { { name: "anon access: admin-access, anonymous user", repo: "foo", - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Repos: []RepoConfig{ @@ -248,7 +248,7 @@ func TestAuth(t *testing.T) { name: "anon access: admin-access, authd user", repo: "foo", key: dummyPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Repos: []RepoConfig{ @@ -268,7 +268,7 @@ func TestAuth(t *testing.T) { name: "anon access: admin-access, admin user", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Repos: []RepoConfig{ @@ -292,7 +292,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, authd user, collab", key: dummyPk, repo: "foo", - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "no-access", Repos: []RepoConfig{ @@ -317,7 +317,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, authd user, collab, private repo", key: dummyPk, repo: "foo", - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "no-access", Repos: []RepoConfig{ @@ -343,7 +343,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, admin user, collab, private repo", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "no-access", Repos: []RepoConfig{ @@ -370,7 +370,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-only, authd user, collab, private repo", repo: "foo", key: dummyPk, - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "read-only", Repos: []RepoConfig{ @@ -395,7 +395,7 @@ func TestAuth(t *testing.T) { { name: "anon access: admin-access, anonymous user, collab", repo: "foo", - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Repos: []RepoConfig{ @@ -412,7 +412,7 @@ func TestAuth(t *testing.T) { name: "anon access: admin-access, authd user, collab", repo: "foo", key: dummyPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Repos: []RepoConfig{ @@ -436,7 +436,7 @@ func TestAuth(t *testing.T) { name: "anon access: admin-access, admin user, collab", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Repos: []RepoConfig{ @@ -462,7 +462,7 @@ func TestAuth(t *testing.T) { // New repo { name: "anon access: no-access, anonymous user, new repo", - access: git.NoAccess, + access: proto.NoAccess, repo: "foo", cfg: Config{ AnonAccess: "no-access", @@ -472,7 +472,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, authd user, new repo", key: dummyPk, repo: "foo", - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "no-access", Users: []User{ @@ -488,7 +488,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, authd user, new repo, with user", key: dummyPk, repo: "foo", - access: git.NoAccess, + access: proto.NoAccess, cfg: Config{ AnonAccess: "no-access", Users: []User{ @@ -504,7 +504,7 @@ func TestAuth(t *testing.T) { name: "anon access: no-access, admin user, new repo", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "no-access", Users: []User{ @@ -520,7 +520,7 @@ func TestAuth(t *testing.T) { { name: "anon access: read-only, anonymous user, new repo", repo: "foo", - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "read-only", }, @@ -529,7 +529,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-only, authd user, new repo", repo: "foo", key: dummyPk, - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "read-only", Users: []User{ @@ -545,7 +545,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-only, admin user, new repo", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "read-only", Users: []User{ @@ -561,7 +561,7 @@ func TestAuth(t *testing.T) { { name: "anon access: read-write, anonymous user, new repo", repo: "foo", - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "read-write", }, @@ -570,7 +570,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-write, authd user, new repo", repo: "foo", key: dummyPk, - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "read-write", Users: []User{ @@ -586,7 +586,7 @@ func TestAuth(t *testing.T) { name: "anon access: read-write, admin user, new repo", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "read-write", Users: []User{ @@ -602,7 +602,7 @@ func TestAuth(t *testing.T) { { name: "anon access: admin-access, anonymous user, new repo", repo: "foo", - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", }, @@ -611,7 +611,7 @@ func TestAuth(t *testing.T) { name: "anon access: admin-access, authd user, new repo", repo: "foo", key: dummyPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Users: []User{ @@ -627,7 +627,7 @@ func TestAuth(t *testing.T) { name: "anon access: admin-access, admin user, new repo", repo: "foo", key: adminPk, - access: git.AdminAccess, + access: proto.AdminAccess, cfg: Config{ AnonAccess: "admin-access", Users: []User{ @@ -645,7 +645,7 @@ func TestAuth(t *testing.T) { { name: "anon access: read-only, no users", repo: "foo", - access: git.ReadOnlyAccess, + access: proto.ReadOnlyAccess, cfg: Config{ AnonAccess: "read-only", }, @@ -653,7 +653,7 @@ func TestAuth(t *testing.T) { { name: "anon access: read-write, no users", repo: "foo", - access: git.ReadWriteAccess, + access: proto.ReadWriteAccess, cfg: Config{ AnonAccess: "read-write", }, diff --git a/config/config.go b/config/config.go index de96808c3b2a8133a2044dacfbf6602c4df6dc97..fea3deeac3b53b720cc0e46989f9dd59469216af 100644 --- a/config/config.go +++ b/config/config.go @@ -19,8 +19,8 @@ import ( "os" "github.com/charmbracelet/soft-serve/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/server/config" - gm "github.com/charmbracelet/soft-serve/server/git" "github.com/go-git/go-billy/v5/memfs" ggit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" @@ -103,9 +103,9 @@ func NewConfig(cfg *config.Config) (*Config, error) { c.Source = rs // Grant read-write access when no keys are provided. if len(pks) == 0 { - anonAccess = gm.ReadWriteAccess.String() + anonAccess = proto.ReadWriteAccess.String() } else { - anonAccess = gm.ReadOnlyAccess.String() + anonAccess = proto.ReadOnlyAccess.String() } if host == "" { displayHost = "localhost" diff --git a/git/config.go b/git/config.go new file mode 100644 index 0000000000000000000000000000000000000000..f4fec12889389c876af4b7a54158b09cdde51820 --- /dev/null +++ b/git/config.go @@ -0,0 +1,44 @@ +package git + +// ConfigOptions are options for Config. +type ConfigOptions struct { + File string + All bool + Add bool +} + +// Config gets a git configuration. +func Config(key string, opts ...ConfigOptions) (string, error) { + var opt ConfigOptions + if len(opts) > 0 { + opt = opts[0] + } + cmd := NewCommand("config") + if opt.File != "" { + cmd.AddArgs("--file", opt.File) + } + if opt.All { + cmd.AddArgs("--get-all") + } + cmd.AddArgs(key) + bts, err := cmd.Run() + if err != nil { + return "", err + } + return string(bts), nil +} + +// SetConfig sets a git configuration. +func SetConfig(key string, value string, opts ...ConfigOptions) error { + var opt ConfigOptions + if len(opts) > 0 { + opt = opts[0] + } + cmd := NewCommand("config") + if opt.File != "" { + cmd.AddArgs("--file", opt.File) + } + cmd.AddArgs(key, value) + _, err := cmd.Run() + return err +} diff --git a/git/repo.go b/git/repo.go index d1b896e689861633dfb7b719c055e0c017e83f97..c2408b19dbd4836e99b4d888129ab7baccd91ffd 100644 --- a/git/repo.go +++ b/git/repo.go @@ -207,3 +207,31 @@ func (r *Repository) UpdateServerInfo() error { _, err := cmd.RunInDir(r.Path) return err } + +// Config returns the config value for the given key. +func (r *Repository) Config(key string, opts ...ConfigOptions) (string, error) { + dir, err := gitDir(r.Repository) + if err != nil { + return "", err + } + var opt ConfigOptions + if len(opts) > 0 { + opt = opts[0] + } + opt.File = filepath.Join(dir, "config") + return Config(key, opt) +} + +// SetConfig sets the config value for the given key. +func (r *Repository) SetConfig(key, value string, opts ...ConfigOptions) error { + dir, err := gitDir(r.Repository) + if err != nil { + return err + } + var opt ConfigOptions + if len(opts) > 0 { + opt = opts[0] + } + opt.File = filepath.Join(dir, "config") + return SetConfig(key, value, opt) +} diff --git a/go.mod b/go.mod index 5a0c129a0a3d36b3ec2481baa581c74340c23d5d..ca8dc670099bb4aa403cf857df16fff8dce67c77 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/spf13/cobra v1.6.1 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.19.5 ) require ( @@ -47,10 +48,12 @@ require ( github.com/dlclark/regexp2 v1.4.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -64,18 +67,31 @@ require ( github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/yuin/goldmark v1.5.2 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect + golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.21.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.4.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect ) // see https://github.com/sergi/go-diff/issues/123 diff --git a/go.sum b/go.sum index ca9bde11b918811ed71ec9aa3b164e7ab3f16d8c..d5bcaed27dd7faf6f0287c398dc2deec71e27a74 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,9 @@ github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87ini github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= github.com/charmbracelet/wish v0.7.0 h1:rdfacCWaKCQpCMPbOKfi68GYqsb+9CnUzN1Ov/INZJ0= github.com/charmbracelet/wish v0.7.0/go.mod h1:16EQz7k3hEgPkPENghcpEddvlrmucIudE0jnczKr+k4= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -76,11 +79,18 @@ github.com/gogs/git-module v1.7.1/go.mod h1:Y3rsSqtFZEbn7lp+3gWf42GKIY1eNTtLt7Jr github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -89,6 +99,8 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o= github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= @@ -120,6 +132,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk= github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= @@ -156,6 +170,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -191,26 +207,39 @@ github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5ta github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -222,6 +251,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= @@ -230,13 +260,21 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -252,3 +290,44 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= +modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= +modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA= +modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0= +modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI= +modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.19.5 h1:E3iHL55c1Vw1knqIeU9N7B0fSjuiOjHZo7iVMsO6U5U= +modernc.org/sqlite v1.19.5/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34= +modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= +modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= diff --git a/proto/access.go b/proto/access.go new file mode 100644 index 0000000000000000000000000000000000000000..f41ea07e2176ab7cd44bbb10b4426b7653e1c0f2 --- /dev/null +++ b/proto/access.go @@ -0,0 +1,34 @@ +package proto + +// AccessLevel is the level of access allowed to a repo. +type AccessLevel int + +const ( + // NoAccess does not allow access to the repo. + NoAccess AccessLevel = iota + + // ReadOnlyAccess allows read-only access to the repo. + ReadOnlyAccess + + // ReadWriteAccess allows read and write access to the repo. + ReadWriteAccess + + // AdminAccess allows read, write, and admin access to the repo. + AdminAccess +) + +// String implements the Stringer interface for AccessLevel. +func (a AccessLevel) String() string { + switch a { + case NoAccess: + return "no-access" + case ReadOnlyAccess: + return "read-only" + case ReadWriteAccess: + return "read-write" + case AdminAccess: + return "admin-access" + default: + return "" + } +} diff --git a/proto/provider.go b/proto/provider.go new file mode 100644 index 0000000000000000000000000000000000000000..629da8d89a3990c69e2123e0fcfa5cdd15c212e4 --- /dev/null +++ b/proto/provider.go @@ -0,0 +1,7 @@ +package proto + +// Provider is a repository provider. +type Provider interface { + // Open opens a repository. + Open(name string) (RepositoryService, error) +} diff --git a/proto/repo.go b/proto/repo.go new file mode 100644 index 0000000000000000000000000000000000000000..715b8bffbfc198d53e2d717cd73d6af170252e7d --- /dev/null +++ b/proto/repo.go @@ -0,0 +1,17 @@ +package proto + +// Repository is Git repository. +type Repository interface { + Name() string + ProjectName() string + Description() string + IsPrivate() bool +} + +// RepositoryService is a service for managing repositories metadata. +type RepositoryService interface { + Repository + SetProjectName(string) error + SetDescription(string) error + SetPrivate(bool) error +} diff --git a/server/cmd/cat.go b/server/cmd/cat.go index 3fb38a970dd0837275a9517e18de7aca12214977..6dd8221dd60d6c00aed2588b3364ca88a9b3c6a2 100644 --- a/server/cmd/cat.go +++ b/server/cmd/cat.go @@ -8,7 +8,7 @@ import ( gansi "github.com/charmbracelet/glamour/ansi" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/config" - gm "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/ui/common" "github.com/muesli/termenv" "github.com/spf13/cobra" @@ -37,7 +37,7 @@ func CatCommand() *cobra.Command { rn := ps[0] fp := strings.Join(ps[1:], "/") auth := ac.AuthRepo(rn, s.PublicKey()) - if auth < gm.ReadOnlyAccess { + if auth < proto.ReadOnlyAccess { return ErrUnauthorized } var repo *config.Repo diff --git a/server/cmd/git.go b/server/cmd/git.go index 7c426edc1b4035dcae8dafb8d65d608e6be120c4..d9fb44b8cb7ea1d4eb10121402fede5955148deb 100644 --- a/server/cmd/git.go +++ b/server/cmd/git.go @@ -5,7 +5,7 @@ import ( "os/exec" "github.com/charmbracelet/soft-serve/config" - gm "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/spf13/cobra" ) @@ -17,7 +17,7 @@ func GitCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ac, s := fromContext(cmd) auth := ac.AuthRepo("config", s.PublicKey()) - if auth < gm.AdminAccess { + if auth < proto.AdminAccess { return ErrUnauthorized } if len(args) < 1 { diff --git a/server/cmd/list.go b/server/cmd/list.go index 38e56677d876d4344b957c8aea7496b694c038de..01aabda5cdf1fd888fe1d195976a26f84093dedc 100644 --- a/server/cmd/list.go +++ b/server/cmd/list.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/charmbracelet/soft-serve/git" - gm "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/spf13/cobra" ) @@ -27,13 +27,13 @@ func ListCommand() *cobra.Command { ps = strings.Split(path, "/") rn = ps[0] auth := ac.AuthRepo(rn, s.PublicKey()) - if auth < gm.ReadOnlyAccess { + if auth < proto.ReadOnlyAccess { return ErrUnauthorized } } if path == "" || path == "." || path == "/" { for _, r := range ac.Source.AllRepos() { - if ac.AuthRepo(r.Repo(), s.PublicKey()) >= gm.ReadOnlyAccess { + if ac.AuthRepo(r.Repo(), s.PublicKey()) >= proto.ReadOnlyAccess { fmt.Fprintln(s, r.Repo()) } } diff --git a/server/cmd/reload.go b/server/cmd/reload.go index 568f5df5c77e5498ecf67dd68accb4251c26bc94..dda55c13a5dfcca4efc23949b20bf3b528426240 100644 --- a/server/cmd/reload.go +++ b/server/cmd/reload.go @@ -1,7 +1,7 @@ package cmd import ( - gm "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/spf13/cobra" ) @@ -13,7 +13,7 @@ func ReloadCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ac, s := fromContext(cmd) auth := ac.AuthRepo("config", s.PublicKey()) - if auth < gm.AdminAccess { + if auth < proto.AdminAccess { return ErrUnauthorized } return ac.Reload() diff --git a/server/db/db.go b/server/db/db.go new file mode 100644 index 0000000000000000000000000000000000000000..e2b261451ee53314f9ada94cb39aa0f96f75fbcc --- /dev/null +++ b/server/db/db.go @@ -0,0 +1,74 @@ +package db + +import ( + "github.com/charmbracelet/soft-serve/server/db/types" +) + +// ConfigStore is a configuration database storage. +type ConfigStore interface { + // Config + GetConfig() (*types.Config, error) + SetConfigName(string) error + SetConfigHost(string) error + SetConfigPort(int) error + SetConfigAnonAccess(string) error + SetConfigAllowKeyless(bool) error +} + +// UserStore is a user database storage. +type UserStore interface { + // Users + AddUser(name, login, email, password string, isAdmin bool) error + DeleteUser(int) error + GetUser(int) (*types.User, error) + GetUserByLogin(string) (*types.User, error) + GetUserByEmail(string) (*types.User, error) + GetUserByPublicKey(string) (*types.User, error) + SetUserName(*types.User, string) error + SetUserLogin(*types.User, string) error + SetUserEmail(*types.User, string) error + SetUserPassword(*types.User, string) error + SetUserAdmin(*types.User, bool) error +} + +// PublicKeyStore is a public key database storage. +type PublicKeyStore interface { + // Public keys + AddUserPublicKey(*types.User, string) error + DeleteUserPublicKey(int) error + GetUserPublicKeys(*types.User) ([]*types.PublicKey, error) +} + +// RepoStore is a repository database storage. +type RepoStore interface { + // Repos + AddRepo(name, projectName, description string, isPrivate bool) error + DeleteRepo(string) error + GetRepo(string) (*types.Repo, error) + SetRepoProjectName(string, string) error + SetRepoDescription(string, string) error + SetRepoPrivate(string, bool) error +} + +// CollabStore is a collaborator database storage. +type CollabStore interface { + // Collaborators + AddRepoCollab(*types.Repo, *types.User) error + DeleteRepoCollab(int, int) error + ListRepoCollabs(*types.Repo) ([]*types.User, error) +} + +// DB is a database. +type DB interface { + ConfigStore + UserStore + PublicKeyStore + RepoStore + CollabStore + + // CreateDB creates the database. + CreateDB() error + + // Close closes the database. + Close() error +} diff --git a/server/db/sqlite/repo.go b/server/db/sqlite/repo.go new file mode 100644 index 0000000000000000000000000000000000000000..ad61f8ae4eb1a927f861229adfe7a70f795bc091 --- /dev/null +++ b/server/db/sqlite/repo.go @@ -0,0 +1,58 @@ +package sqlite + +import ( + "github.com/charmbracelet/soft-serve/proto" + "github.com/charmbracelet/soft-serve/server/db/types" +) + +// Open opens a repository. +func (d *Sqlite) Open(name string) (proto.RepositoryService, error) { + r, err := d.GetRepo(name) + if err != nil { + return nil, err + } + return &repository{ + repo: r, + db: d, + }, nil +} + +type repository struct { + repo *types.Repo + db *Sqlite +} + +// Name returns the repository's name. +func (r *repository) Name() string { + return r.repo.Name +} + +// ProjectName returns the repository's project name. +func (r *repository) ProjectName() string { + return r.repo.ProjectName +} + +// SetProjectName sets the repository's project name. +func (r *repository) SetProjectName(name string) error { + return r.db.SetRepoProjectName(r.repo.Name, name) +} + +// Description returns the repository's description. +func (r *repository) Description() string { + return r.repo.Description +} + +// SetDescription sets the repository's description. +func (r *repository) SetDescription(desc string) error { + return r.db.SetRepoDescription(r.repo.Name, desc) +} + +// IsPrivate returns whether the repository is private. +func (r *repository) IsPrivate() bool { + return r.repo.Private +} + +// SetPrivate sets whether the repository is private. +func (r *repository) SetPrivate(p bool) error { + return r.db.SetRepoPrivate(r.repo.Name, p) +} diff --git a/server/db/sqlite/sql.go b/server/db/sqlite/sql.go new file mode 100644 index 0000000000000000000000000000000000000000..fd3d96f73e20243d6a6859e9341b77ac00afda92 --- /dev/null +++ b/server/db/sqlite/sql.go @@ -0,0 +1,107 @@ +package sqlite + +var ( + sqlCreateConfigTable = `CREATE TABLE IF NOT EXISTS config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + host TEXT NOT NULL, + port INTEGER NOT NULL, + anon_access TEXT NOT NULL, + allow_keyless BOOLEAN NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL + ); + ` + + sqlCreateUserTable = `CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + login TEXT UNIQUE, + email TEXT UNIQUE, + password TEXT, + admin BOOLEAN NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL + );` + + sqlCreatePublicKeyTable = `CREATE TABLE IF NOT EXISTS public_key ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + public_key TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL, + UNIQUE (user_id, public_key), + CONSTRAINT user_id_fk + FOREIGN KEY(user_id) REFERENCES user(id) + ON DELETE CASCADE + ON UPDATE CASCADE + );` + + sqlCreateRepoTable = `CREATE TABLE IF NOT EXISTS repo ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + project_name TEXT NOT NULL, + description TEXT NOT NULL, + private BOOLEAN NOT NULL, + create_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL, + );` + + sqlCreateCollabTable = `CREATE TABLE IF NOT EXISTS collab ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + repo_id INTEGER NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL, + UNIQUE (user_id, repo_id), + CONSTRAINT user_id_fk + FOREIGN KEY(user_id) REFERENCES user(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT repo_id_fk + FOREIGN KEY(repo_id) REFERENCES repo(id) + ON DELETE CASCADE + ON UPDATE CASCADE + );` + + // Config. + sqlInsertConfig = `INSERT INTO config (name, host, port, anon_access, allow_keyless, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP);` + sqlSelectConfig = `SELECT id, name, host, port, anon_access, allow_keyless, created_at, updated_at FROM config WHERE id = ?;` + sqlUpdateConfigName = `UPDATE config SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateConfigHost = `UPDATE config SET host = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateConfigPort = `UPDATE config SET port = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateConfigAnon = `UPDATE config SET anon_access = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateConfigKeyless = `UPDATE config SET allow_keyless = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + + // User. + sqlInsertUser = `INSERT INTO user (name, login, email, password, admin, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP);` + sqlDeleteUser = `DELETE FROM user WHERE id = ?;` + sqlSelectUser = `SELECT id, name, login, email, password, admin, created_at, updated_at FROM user WHERE id = ?;` + sqlSelectUserByLogin = `SELECT id, name, login, email, password, admin, created_at, updated_at FROM user WHERE login = ?;` + sqlSelectUserByEmail = `SELECT id, name, login, email, password, admin, created_at, updated_at FROM user WHERE email = ?;` + sqlSelectUserByPublicKey = `SELECT u.id, u.name, u.login, u.email, u.password, u.admin, u.created_at, u.updated_at FROM user u INNER JOIN public_key pk ON u.id = pk.user_id WHERE pk.public_key = ?;` + sqlUpdateUserName = `UPDATE user SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateUserLogin = `UPDATE user SET login = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateUserEmail = `UPDATE user SET email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateUserPassword = `UPDATE user SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + sqlUpdateUserAdmin = `UPDATE user SET admin = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;` + + // Public Key. + sqlInsertPublicKey = `INSERT INTO public_key (user_id, public_key, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP);` + sqlDeletePublicKey = `DELETE FROM public_key WHERE id = ?;` + sqlSelectUserPublicKeys = `SELECT id, user_id, public_key, created_at, updated_at FROM public_key WHERE user_id = ?;` + + // Repo. + sqlInsertRepo = `INSERT INTO repo (name, project_name, description, private, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP);` + sqlDeleteRepo = `DELETE FROM repo WHERE id = ?;` + sqlDeleteRepoWithName = `DELETE FROM repo WHERE name = ?;` + sqlSelectRepoByName = `SELECT id, name, project_name, description, private, created_at, updated_at FROM repo WHERE name = ?;` + sqlUpdateRepoProjectNameByName = `UPDATE repo SET project_name = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?;` + sqlUpdateRepoDescriptionByName = `UPDATE repo SET description = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?;` + sqlUpdateRepoPrivateByName = `UPDATE repo SET private = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?;` + + // Collab. + sqlInsertCollab = `INSERT INTO collab (user_id, repo_id, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP);` + sqlDeleteCollab = `DELETE FROM collab WHERE user_id = ? AND repo_id;` + sqlSelectRepoCollabs = `SELECT user.id, user.name, user.login, user.email, user.admin, user.created_at, user.updated_at FROM user INNER JOIN collab ON user.id = collab.user_id WHERE collab.repo_id = ?;` +) diff --git a/server/db/sqlite/sqlite.go b/server/db/sqlite/sqlite.go new file mode 100644 index 0000000000000000000000000000000000000000..d5b4e92fbb99a14fc4f4f201c6bff86fad7d80e5 --- /dev/null +++ b/server/db/sqlite/sqlite.go @@ -0,0 +1,452 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + "log" + "strings" + "time" + + "github.com/charmbracelet/soft-serve/server/db" + "github.com/charmbracelet/soft-serve/server/db/types" + "modernc.org/sqlite" + sqlitelib "modernc.org/sqlite/lib" +) + +var _ db.DB = &Sqlite{} + +// Sqlite is a SQLite database. +type Sqlite struct { + path string + db *sql.DB +} + +// New creates a new DB in the given path. +func New(path string) (*Sqlite, error) { + var err error + log.Printf("Opening SQLite db: %s\n", path) + db, err := sql.Open("sqlite", path+ + "?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)") + if err != nil { + return nil, err + } + d := &Sqlite{ + db: db, + path: path, + } + if err = d.CreateDB(); err != nil { + return nil, err + } + return d, d.db.Ping() +} + +// Close closes the database. +func (d *Sqlite) Close() error { + return d.db.Close() +} + +// CreateDB creates the database and tables. +func (d *Sqlite) CreateDB() error { + return d.wrapTransaction(func(tx *sql.Tx) error { + if _, err := tx.Exec(sqlInsertConfig); err != nil { + return err + } + if _, err := tx.Exec(sqlCreateUserTable); err != nil { + return err + } + if _, err := tx.Exec(sqlCreatePublicKeyTable); err != nil { + return err + } + if _, err := tx.Exec(sqlCreateRepoTable); err != nil { + return err + } + if _, err := tx.Exec(sqlCreateCollabTable); err != nil { + return err + } + return nil + }) +} + +const defaultConfigID = 1 + +// GetConfig returns the server config. +func (d *Sqlite) GetConfig() (*types.Config, error) { + var c types.Config + if err := d.wrapTransaction(func(tx *sql.Tx) error { + r := tx.QueryRow(sqlSelectConfig, defaultConfigID) + if err := r.Scan(&c.ID, &c.Name, &c.Host, &c.Port, &c.AnonAccess, &c.AllowKeyless, &c.CreatedAt, &c.UpdatedAt); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + return &c, nil +} + +// SetConfigName sets the server config name. +func (d *Sqlite) SetConfigName(name string) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateConfigName, name, defaultConfigID) + return err + }) +} + +// SetConfigHost sets the server config host. +func (d *Sqlite) SetConfigHost(host string) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateConfigHost, host, defaultConfigID) + return err + }) +} + +// SetConfigPort sets the server config port. +func (d *Sqlite) SetConfigPort(port int) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateConfigPort, port, defaultConfigID) + return err + }) +} + +// SetConfigAnonAccess sets the server config anon access. +func (d *Sqlite) SetConfigAnonAccess(access string) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateConfigAnon, access, defaultConfigID) + return err + }) +} + +// SetConfigAllowKeyless sets the server config allow keyless. +func (d *Sqlite) SetConfigAllowKeyless(allow bool) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateConfigKeyless, allow, defaultConfigID) + return err + }) +} + +// AddUser adds a new user. +func (d *Sqlite) AddUser(name, login, email, password string, isAdmin bool) error { + var l *string + var e *string + var p *string + if login != "" { + login = strings.ToLower(login) + l = &login + } + if email != "" { + email = strings.ToLower(email) + e = &email + } + if password != "" { + p = &password + } + if err := d.wrapTransaction(func(tx *sql.Tx) error { + if _, err := tx.Exec(sqlInsertUser, name, l, e, p, isAdmin); err != nil { + return err + } + return nil + }); err != nil { + return err + } + return nil +} + +// DeleteUser deletes a user. +func (d *Sqlite) DeleteUser(id int) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlDeleteUser, id) + return err + }) +} + +// GetUser returns a user by ID. +func (d *Sqlite) GetUser(id int) (*types.User, error) { + var u types.User + if err := d.wrapTransaction(func(tx *sql.Tx) error { + r := tx.QueryRow(sqlSelectUser, id) + if err := r.Scan(&u.ID, &u.Name, &u.Login, &u.Email, &u.Password, &u.Admin, &u.CreatedAt, &u.UpdatedAt); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + return &u, nil +} + +// GetUserByLogin returns a user by login. +func (d *Sqlite) GetUserByLogin(login string) (*types.User, error) { + login = strings.ToLower(login) + var u types.User + if err := d.wrapTransaction(func(tx *sql.Tx) error { + r := tx.QueryRow(sqlSelectUserByLogin, login) + if err := r.Scan(&u.ID, &u.Name, &u.Login, &u.Email, &u.Password, &u.Admin, &u.CreatedAt, &u.UpdatedAt); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + return &u, nil +} + +// GetUserByLogin returns a user by login. +func (d *Sqlite) GetUserByEmail(email string) (*types.User, error) { + email = strings.ToLower(email) + var u types.User + if err := d.wrapTransaction(func(tx *sql.Tx) error { + r := tx.QueryRow(sqlSelectUserByEmail, email) + if err := r.Scan(&u.ID, &u.Name, &u.Login, &u.Email, &u.Password, &u.Admin, &u.CreatedAt, &u.UpdatedAt); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + return &u, nil +} + +// GetUserByPublicKey returns a user by public key. +func (d *Sqlite) GetUserByPublicKey(key string) (*types.User, error) { + var u types.User + if err := d.wrapTransaction(func(tx *sql.Tx) error { + r := tx.QueryRow(sqlSelectUserByPublicKey, key) + if err := r.Scan(&u.ID, &u.Name, &u.Login, &u.Email, &u.Password, &u.Admin, &u.CreatedAt, &u.UpdatedAt); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + return &u, nil +} + +// SetUserName sets the user name. +func (d *Sqlite) SetUserName(user *types.User, name string) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateUserName, name, user.ID) + return err + }) +} + +// SetUserLogin sets the user login. +func (d *Sqlite) SetUserLogin(user *types.User, login string) error { + if login == "" { + return fmt.Errorf("login cannot be empty") + } + login = strings.ToLower(login) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateUserLogin, login, user.ID) + return err + }) +} + +// SetUserEmail sets the user email. +func (d *Sqlite) SetUserEmail(user *types.User, email string) error { + if email == "" { + return fmt.Errorf("email cannot be empty") + } + email = strings.ToLower(email) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateUserEmail, email, user.ID) + return err + }) +} + +// SetUserPassword sets the user password. +func (d *Sqlite) SetUserPassword(user *types.User, password string) error { + if password == "" { + return fmt.Errorf("password cannot be empty") + } + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateUserPassword, password, user.ID) + return err + }) +} + +// SetUserAdmin sets the user admin. +func (d *Sqlite) SetUserAdmin(user *types.User, admin bool) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateUserAdmin, admin, user.ID) + return err + }) +} + +// AddUserPublicKey adds a new user public key. +func (d *Sqlite) AddUserPublicKey(user *types.User, key string) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlInsertPublicKey, user.ID, key) + return err + }) +} + +// DeleteUserPublicKey deletes a user public key. +func (d *Sqlite) DeleteUserPublicKey(id int) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlDeletePublicKey, id) + return err + }) +} + +// GetUserPublicKeys returns the user public keys. +func (d *Sqlite) GetUserPublicKeys(user *types.User) ([]*types.PublicKey, error) { + keys := make([]*types.PublicKey, 0) + if err := d.wrapTransaction(func(tx *sql.Tx) error { + rows, err := tx.Query(sqlSelectUserPublicKeys, user.ID) + if err != nil { + return err + } + if err := rows.Err(); err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var k types.PublicKey + if err := rows.Scan(&k.ID, &k.UserID, &k.PublicKey, &k.CreatedAt, &k.UpdatedAt); err != nil { + return err + } + keys = append(keys, &k) + } + return nil + }); err != nil { + return nil, err + } + return keys, nil +} + +// AddRepo adds a new repo. +func (d *Sqlite) AddRepo(name, projectName, description string, isPrivate bool) error { + name = strings.ToLower(name) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlInsertRepo, name, projectName, description, isPrivate) + return err + }) +} + +// DeleteRepo deletes a repo. +func (d *Sqlite) DeleteRepo(name string) error { + name = strings.ToLower(name) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlDeleteRepoWithName, name) + return err + }) +} + +// GetRepo returns a repo by name. +func (d *Sqlite) GetRepo(name string) (*types.Repo, error) { + name = strings.ToLower(name) + var r types.Repo + if err := d.wrapTransaction(func(tx *sql.Tx) error { + rows := tx.QueryRow(sqlSelectRepoByName, name) + if err := rows.Scan(&r.ID, &r.Name, &r.ProjectName, &r.Description, &r.Private, &r.CreatedAt, &r.UpdatedAt); err != nil { + return err + } + if err := rows.Err(); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + return &r, nil +} + +// SetRepoProjectName sets the repo project name. +func (d *Sqlite) SetRepoProjectName(name string, projectName string) error { + name = strings.ToLower(name) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateRepoProjectNameByName, projectName, name) + return err + }) +} + +// SetRepoDescription sets the repo description. +func (d *Sqlite) SetRepoDescription(name string, description string) error { + name = strings.ToLower(name) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateRepoDescriptionByName, description, + name) + return err + }) +} + +// SetRepoPrivate sets the repo private. +func (d *Sqlite) SetRepoPrivate(name string, private bool) error { + name = strings.ToLower(name) + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlUpdateRepoPrivateByName, private, name) + return err + }) +} + +// AddRepoCollab adds a new repo collaborator. +func (d *Sqlite) AddRepoCollab(repo *types.Repo, user *types.User) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlInsertCollab, repo.ID, user.ID) + return err + }) +} + +// DeleteRepoCollab deletes a repo collaborator. +func (d *Sqlite) DeleteRepoCollab(userID int, repoID int) error { + return d.wrapTransaction(func(tx *sql.Tx) error { + _, err := tx.Exec(sqlDeleteCollab, repoID, userID) + return err + }) +} + +// ListRepoCollabs returns a list of repo collaborators. +func (d *Sqlite) ListRepoCollabs(repo *types.Repo) ([]*types.User, error) { + collabs := make([]*types.User, 0) + if err := d.wrapTransaction(func(tx *sql.Tx) error { + rows, err := tx.Query(sqlSelectRepoCollabs, repo.ID) + if err != nil { + return err + } + if err := rows.Err(); err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var c types.User + if err := rows.Scan(&c.ID, &c.Name, &c.Login, &c.Email, &c.Admin, &c.CreatedAt, &c.UpdatedAt); err != nil { + return err + } + collabs = append(collabs, &c) + } + return nil + }); err != nil { + return nil, err + } + return collabs, nil +} + +// WrapTransaction runs the given function within a transaction. +func (d *Sqlite) wrapTransaction(f func(tx *sql.Tx) error) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + tx, err := d.db.BeginTx(ctx, nil) + if err != nil { + log.Printf("error starting transaction: %s", err) + return err + } + for { + err = f(tx) + if err != nil { + serr, ok := err.(*sqlite.Error) + if ok && serr.Code() == sqlitelib.SQLITE_BUSY { + continue + } + log.Printf("error in transaction: %s", err) + return err + } + err = tx.Commit() + if err != nil { + log.Printf("error committing transaction: %s", err) + return err + } + break + } + return nil +} diff --git a/server/db/types/config.go b/server/db/types/config.go new file mode 100644 index 0000000000000000000000000000000000000000..f764591955c4c5e4db5947e8ca11310ad020f3eb --- /dev/null +++ b/server/db/types/config.go @@ -0,0 +1,15 @@ +package types + +import "time" + +// Config is the Soft Serve application configuration. +type Config struct { + ID int + Name string + Host string + Port int + AnonAccess string + AllowKeyless bool + CreatedAt *time.Time + UpdatedAt *time.Time +} diff --git a/server/db/types/publickey.go b/server/db/types/publickey.go new file mode 100644 index 0000000000000000000000000000000000000000..397caf2ae7daf38e03fd55890f432853b87d54bd --- /dev/null +++ b/server/db/types/publickey.go @@ -0,0 +1,64 @@ +package types + +import ( + "fmt" + "strings" + "time" + + "golang.org/x/crypto/ssh" +) + +var _ ssh.PublicKey = &PublicKey{} +var _ fmt.Stringer = &PublicKey{} + +// PublicKey is a public key database model. +type PublicKey struct { + ID int + UserID int + PublicKey string + CreatedAt *time.Time + UpdatedAt *time.Time +} + +func (k *PublicKey) publicKey() ssh.PublicKey { + pk, err := ssh.ParsePublicKey([]byte(k.PublicKey)) + if err != nil { + return nil + } + return pk +} + +func (k *PublicKey) String() string { + pk := k.publicKey() + if pk == nil { + return "" + } + return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pk))) +} + +// Type returns the type of the public key. +func (k *PublicKey) Type() string { + pk := k.publicKey() + if pk == nil { + return "" + } + return pk.Type() +} + +// Marshal returns the serialized form of the public key. +func (k *PublicKey) Marshal() []byte { + pk := k.publicKey() + if pk == nil { + return nil + } + return pk.Marshal() +} + +// Verify verifies the signature of the given data. +func (k *PublicKey) Verify(data []byte, sig *ssh.Signature) error { + pk := k.publicKey() + if pk == nil { + return fmt.Errorf("invalid public key") + } + return pk.Verify(data, sig) +} diff --git a/server/db/types/repo.go b/server/db/types/repo.go new file mode 100644 index 0000000000000000000000000000000000000000..f9034c6fc40be73d4712dfe475e13a3d63b609cb --- /dev/null +++ b/server/db/types/repo.go @@ -0,0 +1,19 @@ +package types + +import "time" + +// Repo is a repository database model. +type Repo struct { + ID int + Name string + ProjectName string + Description string + Private bool + CreatedAt *time.Time + UpdatedAt *time.Time +} + +// String returns the name of the repository. +func (r *Repo) String() string { + return r.Name +} diff --git a/server/db/types/user.go b/server/db/types/user.go new file mode 100644 index 0000000000000000000000000000000000000000..beef68bea2b839a9e5e6fe0eadb3cc085f68c9d9 --- /dev/null +++ b/server/db/types/user.go @@ -0,0 +1,34 @@ +package types + +import ( + "net/mail" + "time" +) + +// User is a user database model. +type User struct { + ID int + Name string + Login *string + Email *string + Password *string + Admin bool + CreatedAt *time.Time + UpdatedAt *time.Time +} + +// Address returns the email address of the user. +func (u *User) Address() *mail.Address { + if u.Email == nil { + return nil + } + return &mail.Address{ + Name: u.Name, + Address: *u.Email, + } +} + +// String returns the name of the user. +func (u *User) String() string { + return u.Name +} diff --git a/server/file/file.go b/server/file/file.go new file mode 100644 index 0000000000000000000000000000000000000000..b3b306e6b752c52b3c0314fb2b99a8e6088a0699 --- /dev/null +++ b/server/file/file.go @@ -0,0 +1,108 @@ +package file + +import ( + "errors" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/charmbracelet/soft-serve/git" + "github.com/charmbracelet/soft-serve/proto" +) + +type key int + +const ( + projectName key = iota + description + private +) + +var keys = map[key]string{ + projectName: "soft-serve.projectName", + description: "soft-serve.description", + private: "soft-serve.private", +} + +var _ proto.Provider = &File{} + +// File is a file-based repository provider. +type File struct { + repoPath string +} + +// New returns a new File provider. +func New(repoPath string) *File { + f := &File{ + repoPath: repoPath, + } + return f +} + +// Open opens a new repository and returns a new FileRepo. +func (f *File) Open(name string) (proto.RepositoryService, error) { + fp := filepath.Join(f.repoPath, name) + r, err := git.Open(fp) + if errors.Is(err, os.ErrNotExist) { + r, err = git.Open(fp + ".git") + } + if err != nil { + return nil, err + } + return &FileRepo{r}, nil +} + +var _ proto.Repository = &FileRepo{} + +// FileRepo is a file-based repository. +type FileRepo struct { // nolint:revive + repo *git.Repository +} + +// Name returns the name of the repository. +func (r *FileRepo) Name() string { + return strings.TrimSuffix(r.repo.Name(), ".git") +} + +// ProjectName returns the project name of the repository. +func (r *FileRepo) ProjectName() string { + pn, err := r.repo.Config(keys[projectName]) + if err != nil { + return "" + } + return pn +} + +// SetProjectName sets the project name of the repository. +func (r *FileRepo) SetProjectName(name string) error { + return r.repo.SetConfig(keys[projectName], name) +} + +// Description returns the description of the repository. +func (r *FileRepo) Description() string { + desc, err := r.repo.Config(keys[description]) + if err != nil { + return "" + } + return desc +} + +// SetDescription sets the description of the repository. +func (r *FileRepo) SetDescription(desc string) error { + return r.repo.SetConfig(keys[description], desc) +} + +// IsPrivate returns whether the repository is private. +func (r *FileRepo) IsPrivate() bool { + p, err := r.repo.Config(keys[private]) + if err != nil { + return false + } + return p == "true" +} + +// SetPrivate sets whether the repository is private. +func (r *FileRepo) SetPrivate(p bool) error { + return r.repo.SetConfig(keys[private], strconv.FormatBool(p)) +} diff --git a/server/git/auth.go b/server/git/auth.go index 4cd894e5ca12f2ae579fa0e962ecbc3839922aae..cad91ada524c47270d0828bd583853a00baca73d 100644 --- a/server/git/auth.go +++ b/server/git/auth.go @@ -1,46 +1,16 @@ package git -import "github.com/gliderlabs/ssh" - -// AccessLevel is the level of access allowed to a repo. -type AccessLevel int - -const ( - // NoAccess does not allow access to the repo. - NoAccess AccessLevel = iota - - // ReadOnlyAccess allows read-only access to the repo. - ReadOnlyAccess - - // ReadWriteAccess allows read and write access to the repo. - ReadWriteAccess - - // AdminAccess allows read, write, and admin access to the repo. - AdminAccess +import ( + "github.com/charmbracelet/soft-serve/proto" + "github.com/gliderlabs/ssh" ) -// String implements the Stringer interface for AccessLevel. -func (a AccessLevel) String() string { - switch a { - case NoAccess: - return "no-access" - case ReadOnlyAccess: - return "read-only" - case ReadWriteAccess: - return "read-write" - case AdminAccess: - return "admin-access" - default: - return "" - } -} - // Hooks is an interface that allows for custom authorization // implementations and post push/fetch notifications. Prior to git access, // AuthRepo will be called with the ssh.Session public key and the repo name. // Implementers return the appropriate AccessLevel. type Hooks interface { - AuthRepo(string, ssh.PublicKey) AccessLevel + AuthRepo(string, ssh.PublicKey) proto.AccessLevel Push(string, ssh.PublicKey) Fetch(string, ssh.PublicKey) } diff --git a/server/git/daemon/daemon.go b/server/git/daemon/daemon.go index 1c350a0a623154d0a8837e5071089aed92bc8731..eb61b9c8dfe1458212e27a2e18eb57a815264db4 100644 --- a/server/git/daemon/daemon.go +++ b/server/git/daemon/daemon.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/server/config" "github.com/charmbracelet/soft-serve/server/git" "github.com/go-git/go-git/v5/plumbing/format/pktline" @@ -159,7 +160,7 @@ func (d *Daemon) handleClient(c net.Conn) { defer log.Printf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, repo) repo = strings.TrimPrefix(repo, "/") auth := d.auth.AuthRepo(strings.TrimSuffix(repo, ".git"), nil) - if auth < git.ReadOnlyAccess { + if auth < proto.ReadOnlyAccess { fatal(c, git.ErrNotAuthed) return } diff --git a/server/git/daemon/daemon_test.go b/server/git/daemon/daemon_test.go index 5de891ac8fbcf6e9f3e0b5785eda31683f5fc28d..aa4ef2744c06d4803b5d209cb8d097c02017fb56 100644 --- a/server/git/daemon/daemon_test.go +++ b/server/git/daemon/daemon_test.go @@ -17,11 +17,14 @@ import ( var testDaemon *Daemon func TestMain(m *testing.M) { - testdata := "testdata" - defer os.RemoveAll(testdata) + tmp, err := os.MkdirTemp("", "soft-serve-test") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(tmp) cfg := &config.Config{ Host: "", - DataPath: testdata, + DataPath: tmp, Git: config.GitConfig{ // Reduce the max timeout to 100 second so we can test the timeout. MaxTimeout: 100, diff --git a/server/git/ssh/ssh.go b/server/git/ssh/ssh.go index c1f3c56a9e2d5b1bde36cb7efe4e205b9cb5fdda..b2c0ecd7dad2e8d909da5490e149aea90341b29a 100644 --- a/server/git/ssh/ssh.go +++ b/server/git/ssh/ssh.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/server/git" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" @@ -41,7 +42,7 @@ func Middleware(repoDir string, gh git.Hooks) wish.Middleware { switch gc { case "git-receive-pack": switch access { - case git.ReadWriteAccess, git.AdminAccess: + case proto.ReadWriteAccess, proto.AdminAccess: err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo) if err != nil { Fatal(s, git.ErrSystemMalfunction) @@ -54,7 +55,7 @@ func Middleware(repoDir string, gh git.Hooks) wish.Middleware { return case "git-upload-archive", "git-upload-pack": switch access { - case git.ReadOnlyAccess, git.ReadWriteAccess, git.AdminAccess: + case proto.ReadOnlyAccess, proto.ReadWriteAccess, proto.AdminAccess: // try to upload .git first, then err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo) if err != nil { diff --git a/server/git/ssh/ssh_test.go b/server/git/ssh/ssh_test.go index 88f7914a5ba03532870d8015aace60c5341c7a50..b400a5865ed893180737cd067084e601be190c29 100644 --- a/server/git/ssh/ssh_test.go +++ b/server/git/ssh/ssh_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/charmbracelet/keygen" - "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" ) @@ -28,13 +28,13 @@ func TestGitMiddleware(t *testing.T) { pushes: []action{}, fetches: []action{}, access: []accessDetails{ - {pubkey, "repo1", git.AdminAccess}, - {pubkey, "repo2", git.AdminAccess}, - {pubkey, "repo3", git.AdminAccess}, - {pubkey, "repo4", git.AdminAccess}, - {pubkey, "repo5", git.NoAccess}, - {pubkey, "repo6", git.ReadOnlyAccess}, - {pubkey, "repo7", git.AdminAccess}, + {pubkey, "repo1", proto.AdminAccess}, + {pubkey, "repo2", proto.AdminAccess}, + {pubkey, "repo3", proto.AdminAccess}, + {pubkey, "repo4", proto.AdminAccess}, + {pubkey, "repo5", proto.NoAccess}, + {pubkey, "repo6", proto.ReadOnlyAccess}, + {pubkey, "repo7", proto.AdminAccess}, }, } srv, err := wish.NewServer( @@ -180,7 +180,7 @@ func createKeyPair(t *testing.T) (ssh.PublicKey, string) { type accessDetails struct { key ssh.PublicKey repo string - level git.AccessLevel + level proto.AccessLevel } type action struct { @@ -195,13 +195,13 @@ type testHooks struct { access []accessDetails } -func (h *testHooks) AuthRepo(repo string, key ssh.PublicKey) git.AccessLevel { +func (h *testHooks) AuthRepo(repo string, key ssh.PublicKey) proto.AccessLevel { for _, dets := range h.access { if dets.repo == repo && ssh.KeysEqual(key, dets.key) { return dets.level } } - return git.NoAccess + return proto.NoAccess } func (h *testHooks) Push(repo string, key ssh.PublicKey) { diff --git a/server/server_test.go b/server/server_test.go index 778a191fb301346586f989124ec3ac84c956fed9..064d5ae296bac38a4e00fa95bed28c3c52d8e827 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2,6 +2,9 @@ package server import ( "fmt" + "log" + "net" + "os" "path/filepath" "testing" @@ -20,9 +23,7 @@ import ( var ( cfg = &config.Config{ Host: "", - SSH: config.SSHConfig{ - Port: 22222, - }, + Git: config.GitConfig{Port: 9418}, } ) @@ -30,10 +31,8 @@ func TestPushRepo(t *testing.T) { is := is.New(t) _, pkPath := createKeyPair(t) s := setupServer(t) - defer s.Close() err := s.Reload() is.NoErr(err) - rp := t.TempDir() r, err := git.PlainInit(rp, false) is.NoErr(err) @@ -55,7 +54,7 @@ func TestPushRepo(t *testing.T) { is.NoErr(err) _, err = r.CreateRemote(&gconfig.RemoteConfig{ Name: "origin", - URLs: []string{fmt.Sprintf("ssh://%s:%d/%s", cfg.Host, cfg.SSH.Port, "testrepo")}, + URLs: []string{fmt.Sprintf("ssh://localhost:%d/%s", cfg.SSH.Port, "testrepo")}, }) auth, err := gssh.NewPublicKeysFromFile("git", pkPath, "") is.NoErr(err) @@ -73,12 +72,13 @@ func TestCloneRepo(t *testing.T) { is := is.New(t) _, pkPath := createKeyPair(t) s := setupServer(t) - defer s.Close() + log.Print("starting server") err := s.Reload() + log.Print("reloaded server") is.NoErr(err) - dst := t.TempDir() - url := fmt.Sprintf("ssh://%s:%d/config", cfg.Host, cfg.SSH.Port) + url := fmt.Sprintf("ssh://localhost:%d/config", cfg.SSH.Port) + log.Print("cloning repo") err = ggit.Clone(url, dst, ggit.CloneOptions{ CommandOptions: ggit.CommandOptions{ Envs: []string{ @@ -89,16 +89,24 @@ func TestCloneRepo(t *testing.T) { is.NoErr(err) } +func randomPort() int { + addr, _ := net.Listen("tcp", ":0") //nolint:gosec + _ = addr.Close() + return addr.Addr().(*net.TCPAddr).Port +} + func setupServer(t *testing.T) *Server { t.Helper() - tmpdir := t.TempDir() - cfg.DataPath = tmpdir + cfg.DataPath = t.TempDir() + cfg.SSH.Port = randomPort() s := NewServer(cfg) go func() { + log.Print("starting server") s.Start() }() t.Cleanup(func() { s.Close() + os.RemoveAll(cfg.DataPath) }) return s } diff --git a/server/session.go b/server/session.go index c9f4329c93f6115c9e71bfdce0dcc84759db5aaf..8c4ef3b24d1868d2df40212f9ebf26be3dfcf6c1 100644 --- a/server/session.go +++ b/server/session.go @@ -6,8 +6,8 @@ import ( "github.com/aymanbagabas/go-osc52" tea "github.com/charmbracelet/bubbletea" appCfg "github.com/charmbracelet/soft-serve/config" + "github.com/charmbracelet/soft-serve/proto" cm "github.com/charmbracelet/soft-serve/server/cmd" - gm "github.com/charmbracelet/soft-serve/server/git" "github.com/charmbracelet/soft-serve/ui" "github.com/charmbracelet/soft-serve/ui/common" "github.com/charmbracelet/soft-serve/ui/keymap" @@ -30,7 +30,7 @@ func SessionHandler(ac *appCfg.Config) bm.ProgramHandler { if len(cmd) == 1 { initialRepo = cmd[0] auth := ac.AuthRepo(initialRepo, s.PublicKey()) - if auth < gm.ReadOnlyAccess { + if auth < proto.ReadOnlyAccess { wish.Fatalln(s, cm.ErrUnauthorized) return nil } diff --git a/server/session_test.go b/server/session_test.go index ac6b24645f33a7b9f7b0827c5c0f9388cdcdbee7..7abd26e7771196ceaee8a0c23883a6313ae92fe8 100644 --- a/server/session_test.go +++ b/server/session_test.go @@ -55,9 +55,10 @@ func TestSession(t *testing.T) { func setup(tb testing.TB) *gossh.Session { is := is.New(tb) tb.Helper() - cfg.DataPath = tb.TempDir() ac, err := appCfg.NewConfig(&config.Config{ - SSH: config.SSHConfig{Port: 22226}, + Host: "", + SSH: config.SSHConfig{Port: randomPort()}, + Git: config.GitConfig{Port: 9418}, DataPath: tb.TempDir(), InitialAdminKeys: []string{ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMJlb/qf2B2kMNdBxfpCQqI2ctPcsOkdZGVh5zTRhKtH", diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index 5f1a2f3864a49e5a8ac07268297067e777fafe97..159ee9bd3e9b4ebee85ae4363ca29ee9f1dbe853 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -9,7 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/config" - gm "github.com/charmbracelet/soft-serve/server/git" + "github.com/charmbracelet/soft-serve/proto" "github.com/charmbracelet/soft-serve/ui/common" "github.com/charmbracelet/soft-serve/ui/components/code" "github.com/charmbracelet/soft-serve/ui/components/selector" @@ -190,7 +190,7 @@ func (s *Selection) Init() tea.Cmd { // Put configured repos first for _, r := range cfg.Repos { acc := cfg.AuthRepo(r.Repo, pk) - if r.Private && acc < gm.ReadOnlyAccess { + if r.Private && acc < proto.ReadOnlyAccess { continue } repo, err := cfg.Source.GetRepo(r.Repo) @@ -209,7 +209,7 @@ func (s *Selection) Init() tea.Cmd { readmeCmd = s.readme.SetContent(rm, rp) } acc := cfg.AuthRepo(r.Repo(), pk) - if r.IsPrivate() && acc < gm.ReadOnlyAccess { + if r.IsPrivate() && acc < proto.ReadOnlyAccess { continue } exists := false