diff --git a/config/git.go b/config/git.go index ed565ecec6ff91e4397363c2121c9096895e6a4b..73b3edcb1a619c9bd937430060ad3013df032738 100644 --- a/config/git.go +++ b/config/git.go @@ -53,6 +53,11 @@ func (rs *RepoSource) open(path string) (*Repo, error) { return r, nil } +// IsBare returns true if the repository is a bare repository. +func (r *Repo) IsBare() bool { + return r.repository.IsBare +} + // IsPrivate returns true if the repository is private. func (r *Repo) IsPrivate() bool { return r.private @@ -65,13 +70,13 @@ func (r *Repo) Path() string { // Repo returns the repository directory name. func (r *Repo) Repo() string { - return filepath.Base(r.path) + return strings.TrimSuffix(filepath.Base(r.path), ".git") } // Name returns the name of the repository. func (r *Repo) Name() string { if r.name == "" { - return strings.TrimSuffix(r.Repo(), ".git") + return r.Repo() } return r.name } @@ -226,14 +231,16 @@ func (rs *RepoSource) LoadRepo(name string) error { 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) return err } + if !r.IsBare() { + log.Printf("warning: %q is not a bare repository", rp) + } else if r.IsBare() && !strings.HasSuffix(rp, ".git") { + log.Printf("warning: %q should be renamed to %q", rp, rp+".git") + } rs.repos[name] = r return nil } @@ -249,13 +256,10 @@ func (rs *RepoSource) LoadRepos() error { log.Printf("warning: %q is not a directory", filepath.Join(rs.Path, de.Name())) continue } - err = rs.LoadRepo(de.Name()) - if err == git.ErrNotAGitRepository { + if err := rs.LoadRepo(de.Name()); err != nil { + log.Printf("error opening repository %q: %s", de.Name(), err) continue } - if err != nil { - return err - } } return nil } diff --git a/git/errors.go b/git/errors.go index e4c2ec35c8774584bdb34c1e0526cd8c4e66339c..40b0d390f3603c5168c9a4318229f8444869be44 100644 --- a/git/errors.go +++ b/git/errors.go @@ -11,8 +11,8 @@ var ( ErrFileNotFound = errors.New("file not found") // ErrDirectoryNotFound is returned when a directory is not found. ErrDirectoryNotFound = errors.New("directory not found") - // ErrReferenceNotFound is returned when a reference is not found. - ErrReferenceNotFound = errors.New("reference not found") + // ErrReferenceNotExist is returned when a reference does not exist. + ErrReferenceNotExist = git.ErrReferenceNotExist // ErrRevisionNotExist is returned when a revision is not found. ErrRevisionNotExist = git.ErrRevisionNotExist // ErrNotAGitRepository is returned when the given path is not a Git repository. diff --git a/git/repo.go b/git/repo.go index b6d95b56d75c1cd6f18e6224b12d8223ff79daae..d1b896e689861633dfb7b719c055e0c017e83f97 100644 --- a/git/repo.go +++ b/git/repo.go @@ -74,7 +74,7 @@ func (r *Repository) Name() string { // HEAD returns the HEAD reference for a repository. func (r *Repository) HEAD() (*Reference, error) { - rn, err := r.SymbolicRef() + rn, err := r.SymbolicRef(git.SymbolicRefOptions{Name: "HEAD"}) if err != nil { return nil, err } diff --git a/git/types.go b/git/types.go new file mode 100644 index 0000000000000000000000000000000000000000..daf89b03eead9808ef30154605faa288ce4a3e19 --- /dev/null +++ b/git/types.go @@ -0,0 +1,9 @@ +package git + +import "github.com/gogs/git-module" + +// CommandOptions contain options for running a git command. +type CommandOptions = git.CommandOptions + +// CloneOptions contain options for cloning a repository. +type CloneOptions = git.CloneOptions diff --git a/server/git/ssh.go b/server/git/ssh.go index d9987b751c01666a6fc527aad54d7a1304115173..d8aa7c37ba9273289f5443b792133eaaf14a70b4 100644 --- a/server/git/ssh.go +++ b/server/git/ssh.go @@ -9,10 +9,9 @@ import ( "path/filepath" "strings" + "github.com/charmbracelet/soft-serve/git" "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. @@ -75,55 +74,63 @@ type Hooks interface { 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) + func() { + 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, "/") { + log.Printf("invalid repo: %s", repo) + Fatal(s, fmt.Errorf("%s: %s", ErrInvalidRepo, "user repos not supported")) + return + } + pk := s.PublicKey() + access := gh.AuthRepo(strings.TrimSuffix(repo, ".git"), pk) + // git bare repositories should end in ".git" + // https://git-scm.com/docs/gitrepository-layout + if !strings.HasSuffix(repo, ".git") { + repo += ".git" } - 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) + 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: - log.Printf("unknown git error: %s", err) - Fatal(s, ErrSystemMalfunction) + Fatal(s, ErrNotAuthed) } - default: - Fatal(s, ErrNotAuthed) + return + case "git-upload-archive", "git-upload-pack": + switch access { + case ReadOnlyAccess, ReadWriteAccess, AdminAccess: + // try to upload .git first, then + err := gitPack(s, gc, repoDir, repo) + if err != nil { + err = gitPack(s, gc, repoDir, strings.TrimSuffix(repo, ".git")) + } + 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 } - return } - } + }() sh(s) } } @@ -199,7 +206,7 @@ func ensureRepo(dir string, repo string) error { return err } if !exists { - _, err := git.PlainInit(rp, true) + _, err := git.Init(rp, true) if err != nil { return err } @@ -219,7 +226,7 @@ func runGit(s ssh.Session, dir string, args ...string) error { } func ensureDefaultBranch(s ssh.Session, repoPath string) error { - r, err := git.PlainOpen(repoPath) + r, err := git.Open(repoPath) if err != nil { return err } @@ -227,20 +234,18 @@ func ensureDefaultBranch(s ssh.Session, repoPath string) error { if err != nil { return err } - defer brs.Close() - fb, err := brs.Next() - if err != nil { - return err + if len(brs) == 0 { + return fmt.Errorf("no branches found") } // 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()) + _, err = r.HEAD() + if err == git.ErrReferenceNotExist { + err = runGit(s, repoPath, "branch", "-M", brs[0]) if err != nil { return err } } - if err != nil && err != plumbing.ErrReferenceNotFound { + if err != nil && err != git.ErrReferenceNotExist { return err } return nil diff --git a/server/git/ssh_test.go b/server/git/ssh_test.go index 909fcfc4a4f7dd79abb6bde85731ed3ced7f2a80..e6ec6336ab0c69922197f4d973bf5602e1d86eb4 100644 --- a/server/git/ssh_test.go +++ b/server/git/ssh_test.go @@ -128,9 +128,7 @@ func runGitHelper(t *testing.T, pk, cwd string, args ...string) error { 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)) - } + t.Log("git out:", string(out)) return err } @@ -154,15 +152,7 @@ func requireHasAction(t *testing.T, actions []action, key ssh.PublicKey, repo st 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) { + if repo == strings.TrimSuffix(action.repo, ".git") && ssh.KeysEqual(key, action.key) { return } } @@ -203,9 +193,7 @@ type testHooks struct { 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) { + if dets.repo == repo && ssh.KeysEqual(key, dets.key) { return dets.level } } diff --git a/server/server_test.go b/server/server_test.go index 6f126ce9bbd81b60f608d936f05996d57914339f..27ddddd3891cca6feaaa2b9e097d6f87d991de11 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2,11 +2,11 @@ package server import ( "fmt" - "os" "path/filepath" "testing" "github.com/charmbracelet/keygen" + ggit "github.com/charmbracelet/soft-serve/git" "github.com/charmbracelet/soft-serve/server/config" "github.com/gliderlabs/ssh" "github.com/go-git/go-git/v5" @@ -18,32 +18,21 @@ import ( ) var ( - testdata = "testdata" - cfg = &config.Config{ + cfg = &config.Config{ BindAddr: "", Host: "localhost", Port: 22222, - RepoPath: fmt.Sprintf("%s/repos", testdata), - KeyPath: fmt.Sprintf("%s/key", testdata), } - pkPath = "" ) -func TestServer(t *testing.T) { - t.Cleanup(func() { - os.RemoveAll(testdata) - }) +func TestPushRepo(t *testing.T) { is := is.New(t) - _, pkPath = createKeyPair(t) + _, pkPath := createKeyPair(t) s := setupServer(t) + defer s.Close() err := s.Reload() is.NoErr(err) - t.Run("TestPushRepo", testPushRepo) - t.Run("TestCloneRepo", testCloneRepo) -} -func testPushRepo(t *testing.T) { - is := is.New(t) rp := t.TempDir() r, err := git.PlainInit(rp, false) is.NoErr(err) @@ -79,22 +68,31 @@ func testPushRepo(t *testing.T) { is.NoErr(err) } -func testCloneRepo(t *testing.T) { +func TestCloneRepo(t *testing.T) { is := is.New(t) - auth, err := gssh.NewPublicKeysFromFile("git", pkPath, "") + _, pkPath := createKeyPair(t) + s := setupServer(t) + defer s.Close() + err := s.Reload() is.NoErr(err) - auth.HostKeyCallbackHelper = gssh.HostKeyCallbackHelper{ - HostKeyCallback: cssh.InsecureIgnoreHostKey(), - } + dst := t.TempDir() - _, err = git.PlainClone(dst, false, &git.CloneOptions{ - URL: fmt.Sprintf("ssh://%s:%d/config", cfg.Host, cfg.Port), - Auth: auth, + url := fmt.Sprintf("ssh://%s:%d/config", cfg.Host, cfg.Port) + err = ggit.Clone(url, dst, ggit.CloneOptions{ + CommandOptions: ggit.CommandOptions{ + Envs: []string{ + fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i %s -F /dev/null`, pkPath), + }, + }, }) is.NoErr(err) } func setupServer(t *testing.T) *Server { + t.Helper() + tmpdir := t.TempDir() + cfg.RepoPath = filepath.Join(tmpdir, "repos") + cfg.KeyPath = filepath.Join(tmpdir, "key") s := NewServer(cfg) go func() { s.Start() @@ -106,8 +104,8 @@ func setupServer(t *testing.T) *Server { } func createKeyPair(t *testing.T) (ssh.PublicKey, string) { - is := is.New(t) t.Helper() + is := is.New(t) keyDir := t.TempDir() kp, err := keygen.NewWithWrite(filepath.Join(keyDir, "id"), nil, keygen.Ed25519) is.NoErr(err)