Detailed changes
@@ -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
}
@@ -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.
@@ -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
}
@@ -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
@@ -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 <repo>.git first, then <repo>
+ 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
@@ -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
}
}
@@ -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)