diff --git a/config/auth.go b/config/auth.go index 30e8d6719c1605b68ee314d9f90f04ad97d004dd..8583867bf3e6c291fea00862877d4018a3026975 100644 --- a/config/auth.go +++ b/config/auth.go @@ -4,7 +4,7 @@ import ( "log" "strings" - gm "github.com/charmbracelet/wish/git" + gm "github.com/charmbracelet/soft-serve/server/git" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) @@ -45,12 +45,12 @@ func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel { // PasswordHandler returns whether or not password access is allowed. func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool { - return (cfg.AnonAccess != "no-access") && cfg.AllowKeyless + return (cfg.AnonAccess != gm.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 != "no-access") && cfg.AllowKeyless + return (cfg.AnonAccess != gm.NoAccess.String()) && cfg.AllowKeyless } // PublicKeyHandler returns whether or not the given public key may access the diff --git a/config/auth_test.go b/config/auth_test.go index 4f48bafb47cae15a0ba47b17a8a5f6db04c2bce8..a3be357e8b4157097108db0c9b09ef2bf0445343 100644 --- a/config/auth_test.go +++ b/config/auth_test.go @@ -3,7 +3,7 @@ package config import ( "testing" - "github.com/charmbracelet/wish/git" + "github.com/charmbracelet/soft-serve/server/git" "github.com/gliderlabs/ssh" "github.com/matryer/is" ) diff --git a/config/config.go b/config/config.go index 7046a52b71320b70ae02ead37877ce65b0441846..cb2937f14e4b11f1231e59cb897dc078a955c293 100644 --- a/config/config.go +++ b/config/config.go @@ -32,6 +32,10 @@ var ( ErrNoConfig = errors.New("no config file found") ) +const ( + defaultConfigRepo = "config" +) + // Config is the Soft Serve configuration. type Config struct { Name string `yaml:"name" json:"name"` @@ -93,7 +97,7 @@ func NewConfig(cfg *config.Config) (*Config, error) { c := &Config{ Cfg: cfg, } - c.Host = cfg.Host + c.Host = host c.Port = port c.Source = rs // Grant read-write access when no keys are provided. @@ -133,7 +137,7 @@ func NewConfig(cfg *config.Config) (*Config, error) { // readConfig reads the config file for the repo. All config files are stored in // the config repo. func (cfg *Config) readConfig(repo string, v interface{}) error { - cr, err := cfg.Source.GetRepo("config") + cr, err := cfg.Source.GetRepo(defaultConfigRepo) if err != nil { return err } @@ -176,7 +180,7 @@ func (cfg *Config) Reload() error { if err != nil { return err } - if err := cfg.readConfig("config", cfg); err != nil { + if err := cfg.readConfig(defaultConfigRepo, cfg); err != nil { return fmt.Errorf("error reading config: %w", err) } // sanitize repo configs @@ -187,7 +191,7 @@ func (cfg *Config) Reload() error { for _, r := range cfg.Source.AllRepos() { var rc RepoConfig repo := r.Repo() - if repo == "config" { + if repo == defaultConfigRepo { continue } if err := cfg.readConfig(repo, &rc); err != nil { @@ -253,8 +257,8 @@ func createFile(path string, content string) error { } func (cfg *Config) createDefaultConfigRepo(yaml string) error { - cn := "config" - rp := filepath.Join(cfg.Cfg.RepoPath, cn) + cn := defaultConfigRepo + rp := filepath.Join(cfg.Cfg.RepoPath, cn) + ".git" rs := cfg.Source err := rs.LoadRepo(cn) if errors.Is(err, fs.ErrNotExist) { diff --git a/config/git.go b/config/git.go index d1d4d6a3de94bb2b7a5d6d8e47e6486e4bf88840..ed565ecec6ff91e4397363c2121c9096895e6a4b 100644 --- a/config/git.go +++ b/config/git.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "strings" "sync" "github.com/charmbracelet/soft-serve/git" @@ -70,7 +71,7 @@ func (r *Repo) Repo() string { // Name returns the name of the repository. func (r *Repo) Name() string { if r.name == "" { - return r.Repo() + return strings.TrimSuffix(r.Repo(), ".git") } return r.name } @@ -205,6 +206,9 @@ func (rs *RepoSource) AllRepos() []*Repo { func (rs *RepoSource) GetRepo(name string) (*Repo, error) { rs.mtx.Lock() defer rs.mtx.Unlock() + if strings.HasSuffix(name, ".git") { + name = strings.TrimSuffix(name, ".git") + } r, ok := rs.repos[name] if !ok { return nil, ErrMissingRepo @@ -212,31 +216,19 @@ func (rs *RepoSource) GetRepo(name string) (*Repo, error) { return r, nil } -// InitRepo initializes a new Git repository. -func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) { - rs.mtx.Lock() - defer rs.mtx.Unlock() - rp := filepath.Join(rs.Path, name) - rg, err := git.Init(rp, bare) - if err != nil { - return nil, err - } - r := &Repo{ - path: rp, - repository: rg, - refs: []*git.Reference{ - git.NewReference(rp, git.RefsHeads+"master"), - }, - } - rs.repos[name] = r - return r, nil -} - // LoadRepo loads a repository from disk. func (rs *RepoSource) LoadRepo(name string) error { rs.mtx.Lock() defer rs.mtx.Unlock() + if strings.HasSuffix(name, ".git") { + name = strings.TrimSuffix(name, ".git") + } rp := filepath.Join(rs.Path, name) + if _, err := os.Stat(rp); os.IsNotExist(err) { + rp += ".git" + } else { + log.Printf("warning: %q should be renamed to %q", rp, rp+".git") + } r, err := rs.open(rp) if err != nil { log.Printf("error opening repository %q: %s", rp, err) diff --git a/server/cmd/cat.go b/server/cmd/cat.go index 75008918675b09849c19b662e83b0ccf6e205f34..3fb38a970dd0837275a9517e18de7aca12214977 100644 --- a/server/cmd/cat.go +++ b/server/cmd/cat.go @@ -8,8 +8,8 @@ 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/ui/common" - gitwish "github.com/charmbracelet/wish/git" "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 < gitwish.ReadOnlyAccess { + if auth < gm.ReadOnlyAccess { return ErrUnauthorized } var repo *config.Repo diff --git a/server/cmd/git.go b/server/cmd/git.go index d9a60070e6bdbf3ab3d1419d758bf93be75fc2c8..7c426edc1b4035dcae8dafb8d65d608e6be120c4 100644 --- a/server/cmd/git.go +++ b/server/cmd/git.go @@ -5,7 +5,7 @@ import ( "os/exec" "github.com/charmbracelet/soft-serve/config" - gitwish "github.com/charmbracelet/wish/git" + gm "github.com/charmbracelet/soft-serve/server/git" "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 < gitwish.AdminAccess { + if auth < gm.AdminAccess { return ErrUnauthorized } if len(args) < 1 { diff --git a/server/cmd/list.go b/server/cmd/list.go index ff4045c5b1f8a3fbbe6c2f6ca8eea42e928c7f34..bbff89b119d6ec8b6a0733c9bd90ec304b7ab63d 100644 --- a/server/cmd/list.go +++ b/server/cmd/list.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/charmbracelet/soft-serve/git" - gitwish "github.com/charmbracelet/wish/git" + gm "github.com/charmbracelet/soft-serve/server/git" "github.com/spf13/cobra" ) @@ -27,7 +27,7 @@ func ListCommand() *cobra.Command { ps = strings.Split(path, "/") rn = ps[0] auth := ac.AuthRepo(rn, s.PublicKey()) - if auth < gitwish.ReadOnlyAccess { + if auth < gm.ReadOnlyAccess { return ErrUnauthorized } } diff --git a/server/cmd/reload.go b/server/cmd/reload.go index 7f2312ab6e550109e9c52d0ee9c0ef80f3fa1253..568f5df5c77e5498ecf67dd68accb4251c26bc94 100644 --- a/server/cmd/reload.go +++ b/server/cmd/reload.go @@ -1,7 +1,7 @@ package cmd import ( - gitwish "github.com/charmbracelet/wish/git" + gm "github.com/charmbracelet/soft-serve/server/git" "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 < gitwish.AdminAccess { + if auth < gm.AdminAccess { return ErrUnauthorized } return ac.Reload() diff --git a/server/git/ssh.go b/server/git/ssh.go new file mode 100644 index 0000000000000000000000000000000000000000..d9987b751c01666a6fc527aad54d7a1304115173 --- /dev/null +++ b/server/git/ssh.go @@ -0,0 +1,247 @@ +package git + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/charmbracelet/wish" + "github.com/gliderlabs/ssh" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +// ErrNotAuthed represents unauthorized access. +var ErrNotAuthed = errors.New("you are not authorized to do this") + +// ErrSystemMalfunction represents a general system error returned to clients. +var ErrSystemMalfunction = errors.New("something went wrong") + +// ErrInvalidRepo represents an attempt to access a non-existent repo. +var ErrInvalidRepo = errors.New("invalid repo") + +// 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 "" + } +} + +// 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 + Push(string, ssh.PublicKey) + Fetch(string, ssh.PublicKey) +} + +// Middleware adds Git server functionality to the ssh.Server. Repos are stored +// in the specified repo directory. The provided Hooks implementation will be +// checked for access on a per repo basis for a ssh.Session public key. +// Hooks.Push and Hooks.Fetch will be called on successful completion of +// their commands. +func Middleware(repoDir string, gh Hooks) wish.Middleware { + return func(sh ssh.Handler) ssh.Handler { + return func(s ssh.Session) { + cmd := s.Command() + if len(cmd) == 2 { + gc := cmd[0] + // repo should be in the form of "repo.git" + repo := strings.TrimPrefix(cmd[1], "/") + repo = filepath.Clean(repo) + if strings.Contains(repo, "/") { + Fatal(s, fmt.Errorf("%s: %s", ErrInvalidRepo, "user repos not supported")) + } + // git bare repositories should end in ".git" + // https://git-scm.com/docs/gitrepository-layout + if !strings.HasSuffix(repo, ".git") { + repo += ".git" + } + pk := s.PublicKey() + access := gh.AuthRepo(repo, pk) + switch gc { + case "git-receive-pack": + switch access { + case ReadWriteAccess, AdminAccess: + err := gitPack(s, gc, repoDir, repo) + if err != nil { + Fatal(s, ErrSystemMalfunction) + } else { + gh.Push(repo, pk) + } + default: + Fatal(s, ErrNotAuthed) + } + return + case "git-upload-archive", "git-upload-pack": + switch access { + case ReadOnlyAccess, ReadWriteAccess, AdminAccess: + err := gitPack(s, gc, repoDir, repo) + switch err { + case ErrInvalidRepo: + Fatal(s, ErrInvalidRepo) + case nil: + gh.Fetch(repo, pk) + default: + log.Printf("unknown git error: %s", err) + Fatal(s, ErrSystemMalfunction) + } + default: + Fatal(s, ErrNotAuthed) + } + return + } + } + sh(s) + } + } +} + +func gitPack(s ssh.Session, gitCmd string, repoDir string, repo string) error { + cmd := strings.TrimPrefix(gitCmd, "git-") + rp := filepath.Join(repoDir, repo) + switch gitCmd { + case "git-upload-archive", "git-upload-pack": + exists, err := fileExists(rp) + if !exists { + return ErrInvalidRepo + } + if err != nil { + return err + } + return runGit(s, "", cmd, rp) + case "git-receive-pack": + err := ensureRepo(repoDir, repo) + if err != nil { + return err + } + err = runGit(s, "", cmd, rp) + if err != nil { + return err + } + err = ensureDefaultBranch(s, rp) + if err != nil { + return err + } + // Needed for git dumb http server + return runGit(s, rp, "update-server-info") + default: + return fmt.Errorf("unknown git command: %s", gitCmd) + } +} + +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} + +// Fatal prints to the session's STDOUT as a git response and exit 1. +func Fatal(s ssh.Session, v ...interface{}) { + msg := fmt.Sprint(v...) + // hex length includes 4 byte length prefix and ending newline + pktLine := fmt.Sprintf("%04x%s\n", len(msg)+5, msg) + _, _ = wish.WriteString(s, pktLine) + s.Exit(1) // nolint: errcheck +} + +func ensureRepo(dir string, repo string) error { + exists, err := fileExists(dir) + if err != nil { + return err + } + if !exists { + err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0700)) + if err != nil { + return err + } + } + rp := filepath.Join(dir, repo) + exists, err = fileExists(rp) + if err != nil { + return err + } + if !exists { + _, err := git.PlainInit(rp, true) + if err != nil { + return err + } + } + return nil +} + +func runGit(s ssh.Session, dir string, args ...string) error { + usi := exec.CommandContext(s.Context(), "git", args...) + usi.Dir = dir + usi.Stdout = s + usi.Stdin = s + if err := usi.Run(); err != nil { + return err + } + return nil +} + +func ensureDefaultBranch(s ssh.Session, repoPath string) error { + r, err := git.PlainOpen(repoPath) + if err != nil { + return err + } + brs, err := r.Branches() + if err != nil { + return err + } + defer brs.Close() + fb, err := brs.Next() + if err != nil { + return err + } + // Rename the default branch to the first branch available + _, err = r.Head() + if err == plumbing.ErrReferenceNotFound { + err = runGit(s, repoPath, "branch", "-M", fb.Name().Short()) + if err != nil { + return err + } + } + if err != nil && err != plumbing.ErrReferenceNotFound { + return err + } + return nil +} diff --git a/server/git/ssh_test.go b/server/git/ssh_test.go new file mode 100644 index 0000000000000000000000000000000000000000..909fcfc4a4f7dd79abb6bde85731ed3ced7f2a80 --- /dev/null +++ b/server/git/ssh_test.go @@ -0,0 +1,227 @@ +package git + +import ( + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/charmbracelet/keygen" + "github.com/charmbracelet/wish" + "github.com/gliderlabs/ssh" +) + +func TestGitMiddleware(t *testing.T) { + pubkey, pkPath := createKeyPair(t) + + l, err := net.Listen("tcp", "127.0.0.1:0") + requireNoError(t, err) + remote := "ssh://" + l.Addr().String() + + repoDir := t.TempDir() + hooks := &testHooks{ + pushes: []action{}, + fetches: []action{}, + access: []accessDetails{ + {pubkey, "repo1", AdminAccess}, + {pubkey, "repo2", AdminAccess}, + {pubkey, "repo3", AdminAccess}, + {pubkey, "repo4", AdminAccess}, + {pubkey, "repo5", NoAccess}, + {pubkey, "repo6", ReadOnlyAccess}, + {pubkey, "repo7", AdminAccess}, + }, + } + srv, err := wish.NewServer( + wish.WithMiddleware(Middleware(repoDir, hooks)), + wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { + return true + }), + ) + requireNoError(t, err) + go func() { srv.Serve(l) }() + t.Cleanup(func() { _ = srv.Close() }) + + t.Run("create repo on master", func(t *testing.T) { + cwd := t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "master")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo1")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "master")) + requireHasAction(t, hooks.pushes, pubkey, "repo1") + }) + + t.Run("create repo on main", func(t *testing.T) { + cwd := t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo2")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main")) + requireHasAction(t, hooks.pushes, pubkey, "repo2") + }) + + t.Run("create and clone repo", func(t *testing.T) { + cwd := t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo3")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main")) + + cwd = t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo3")) + + requireHasAction(t, hooks.pushes, pubkey, "repo3") + requireHasAction(t, hooks.fetches, pubkey, "repo3") + }) + + t.Run("clone repo that doesn't exist", func(t *testing.T) { + cwd := t.TempDir() + requireError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo4")) + }) + + t.Run("clone repo with no access", func(t *testing.T) { + cwd := t.TempDir() + requireError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo5")) + }) + + t.Run("push repo with with readonly", func(t *testing.T) { + cwd := t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "main")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo6")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) + requireError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main")) + }) + + t.Run("create and clone repo on weird branch", func(t *testing.T) { + cwd := t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "init", "-b", "a-weird-branch-name")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo7")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit")) + requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "a-weird-branch-name")) + + cwd = t.TempDir() + requireNoError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo7")) + + requireHasAction(t, hooks.pushes, pubkey, "repo7") + requireHasAction(t, hooks.fetches, pubkey, "repo7") + }) +} + +func runGitHelper(t *testing.T, pk, cwd string, args ...string) error { + t.Helper() + + allArgs := []string{ + "-c", "user.name='wish'", + "-c", "user.email='test@wish'", + "-c", "commit.gpgSign=false", + "-c", "tag.gpgSign=false", + "-c", "log.showSignature=false", + "-c", "ssh.variant=ssh", + } + allArgs = append(allArgs, args...) + + cmd := exec.Command("git", allArgs...) + cmd.Dir = cwd + cmd.Env = []string{fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i %s -F /dev/null`, pk)} + out, err := cmd.CombinedOutput() + if err != nil { + t.Log("git out:", string(out)) + } + return err +} + +func requireNoError(t *testing.T, err error) { + t.Helper() + + if err != nil { + t.Fatalf("expected no error, got %q", err.Error()) + } +} + +func requireError(t *testing.T, err error) { + t.Helper() + + if err == nil { + t.Fatalf("expected an error, got nil") + } +} + +func requireHasAction(t *testing.T, actions []action, key ssh.PublicKey, repo string) { + t.Helper() + + for _, action := range actions { + r1 := repo + if !strings.HasSuffix(r1, ".git") { + r1 += ".git" + } + r2 := action.repo + if !strings.HasSuffix(r2, ".git") { + r2 += ".git" + } + if r1 == r2 && ssh.KeysEqual(key, action.key) { + return + } + } + t.Fatalf("expected action for %q, got none", repo) +} + +func createKeyPair(t *testing.T) (ssh.PublicKey, string) { + t.Helper() + + keyDir := t.TempDir() + _, err := keygen.NewWithWrite(filepath.Join(keyDir, "id"), nil, keygen.Ed25519) + requireNoError(t, err) + pk := filepath.Join(keyDir, "id_ed25519") + pubBytes, err := os.ReadFile(filepath.Join(keyDir, "id_ed25519.pub")) + requireNoError(t, err) + pubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubBytes) + requireNoError(t, err) + return pubkey, pk +} + +type accessDetails struct { + key ssh.PublicKey + repo string + level AccessLevel +} + +type action struct { + key ssh.PublicKey + repo string +} + +type testHooks struct { + sync.Mutex + pushes []action + fetches []action + access []accessDetails +} + +func (h *testHooks) AuthRepo(repo string, key ssh.PublicKey) AccessLevel { + for _, dets := range h.access { + r1 := strings.TrimSuffix(dets.repo, ".git") + r2 := strings.TrimSuffix(repo, ".git") + if r1 == r2 && ssh.KeysEqual(key, dets.key) { + return dets.level + } + } + return NoAccess +} + +func (h *testHooks) Push(repo string, key ssh.PublicKey) { + h.Lock() + defer h.Unlock() + + h.pushes = append(h.pushes, action{key, repo}) +} + +func (h *testHooks) Fetch(repo string, key ssh.PublicKey) { + h.Lock() + defer h.Unlock() + + h.fetches = append(h.fetches, action{key, repo}) +} diff --git a/server/server.go b/server/server.go index c651776f11bc0c50119437eead438c1cb339ef96..9b615477432f0b9e7eb5c317a9e07a5f8a90fd05 100644 --- a/server/server.go +++ b/server/server.go @@ -10,9 +10,9 @@ import ( appCfg "github.com/charmbracelet/soft-serve/config" "github.com/charmbracelet/soft-serve/server/config" + gm "github.com/charmbracelet/soft-serve/server/git" "github.com/charmbracelet/wish" bm "github.com/charmbracelet/wish/bubbletea" - gm "github.com/charmbracelet/wish/git" lm "github.com/charmbracelet/wish/logging" rm "github.com/charmbracelet/wish/recover" "github.com/gliderlabs/ssh" diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index d2c46b1bd37ec6934ace27d6193d8688993cb9a9..5f1a2f3864a49e5a8ac07268297067e777fafe97 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -9,12 +9,12 @@ 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/ui/common" "github.com/charmbracelet/soft-serve/ui/components/code" "github.com/charmbracelet/soft-serve/ui/components/selector" "github.com/charmbracelet/soft-serve/ui/components/tabs" "github.com/charmbracelet/soft-serve/ui/git" - wgit "github.com/charmbracelet/wish/git" "github.com/gliderlabs/ssh" ) @@ -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 < wgit.ReadOnlyAccess { + if r.Private && acc < gm.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 < wgit.ReadOnlyAccess { + if r.IsPrivate() && acc < gm.ReadOnlyAccess { continue } exists := false