aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/transport/server
diff options
context:
space:
mode:
Diffstat (limited to 'plumbing/transport/server')
-rw-r--r--plumbing/transport/server/loader.go59
-rw-r--r--plumbing/transport/server/loader_test.go57
-rw-r--r--plumbing/transport/server/receive_pack_test.go40
-rw-r--r--plumbing/transport/server/server.go448
-rw-r--r--plumbing/transport/server/server_test.go70
-rw-r--r--plumbing/transport/server/upload_pack_test.go40
6 files changed, 714 insertions, 0 deletions
diff --git a/plumbing/transport/server/loader.go b/plumbing/transport/server/loader.go
new file mode 100644
index 0000000..55bcf1d
--- /dev/null
+++ b/plumbing/transport/server/loader.go
@@ -0,0 +1,59 @@
+package server
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+ "gopkg.in/src-d/go-git.v4/storage/filesystem"
+
+ "srcd.works/go-billy.v1"
+ "srcd.works/go-billy.v1/os"
+)
+
+// DefaultLoader is a filesystem loader ignoring host and resolving paths to /.
+var DefaultLoader = NewFilesystemLoader(os.New("/"))
+
+// Loader loads repository's storer.Storer based on an optional host and a path.
+type Loader interface {
+ // Load loads a storer.Storer given a transport.Endpoint.
+ // Returns transport.ErrRepositoryNotFound if the repository does not
+ // exist.
+ Load(ep transport.Endpoint) (storer.Storer, error)
+}
+
+type fsLoader struct {
+ base billy.Filesystem
+}
+
+// NewFilesystemLoader creates a Loader that ignores host and resolves paths
+// with a given base filesystem.
+func NewFilesystemLoader(base billy.Filesystem) Loader {
+ return &fsLoader{base}
+}
+
+// Load looks up the endpoint's path in the base file system and returns a
+// storer for it. Returns transport.ErrRepositoryNotFound if a repository does
+// not exist in the given path.
+func (l *fsLoader) Load(ep transport.Endpoint) (storer.Storer, error) {
+ fs := l.base.Dir(ep.Path)
+ if _, err := fs.Stat("config"); err != nil {
+ return nil, transport.ErrRepositoryNotFound
+ }
+
+ return filesystem.NewStorage(fs)
+}
+
+// MapLoader is a Loader that uses a lookup map of storer.Storer by
+// transport.Endpoint.
+type MapLoader map[transport.Endpoint]storer.Storer
+
+// Load returns a storer.Storer for given a transport.Endpoint by looking it up
+// in the map. Returns transport.ErrRepositoryNotFound if the endpoint does not
+// exist.
+func (l MapLoader) Load(ep transport.Endpoint) (storer.Storer, error) {
+ s, ok := l[ep]
+ if !ok {
+ return nil, transport.ErrRepositoryNotFound
+ }
+
+ return s, nil
+}
diff --git a/plumbing/transport/server/loader_test.go b/plumbing/transport/server/loader_test.go
new file mode 100644
index 0000000..b4a8c37
--- /dev/null
+++ b/plumbing/transport/server/loader_test.go
@@ -0,0 +1,57 @@
+package server
+
+import (
+ "fmt"
+ "os/exec"
+ "path/filepath"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ . "gopkg.in/check.v1"
+)
+
+type LoaderSuite struct {
+ RepoPath string
+}
+
+var _ = Suite(&LoaderSuite{})
+
+func (s *LoaderSuite) SetUpSuite(c *C) {
+ if err := exec.Command("git", "--version").Run(); err != nil {
+ c.Skip("git command not found")
+ }
+
+ dir := c.MkDir()
+ s.RepoPath = filepath.Join(dir, "repo.git")
+ c.Assert(exec.Command("git", "init", "--bare", s.RepoPath).Run(), IsNil)
+}
+
+func (s *LoaderSuite) endpoint(c *C, url string) transport.Endpoint {
+ ep, err := transport.NewEndpoint(url)
+ c.Assert(err, IsNil)
+ return ep
+}
+
+func (s *LoaderSuite) TestLoadNonExistent(c *C) {
+ sto, err := DefaultLoader.Load(s.endpoint(c, "file:///does-not-exist"))
+ c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(sto, IsNil)
+}
+
+func (s *LoaderSuite) TestLoadNonExistentIgnoreHost(c *C) {
+ sto, err := DefaultLoader.Load(s.endpoint(c, "https://github.com/does-not-exist"))
+ c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(sto, IsNil)
+}
+
+func (s *LoaderSuite) TestLoad(c *C) {
+ sto, err := DefaultLoader.Load(s.endpoint(c, fmt.Sprintf("file://%s", s.RepoPath)))
+ c.Assert(err, IsNil)
+ c.Assert(sto, NotNil)
+}
+
+func (s *LoaderSuite) TestLoadIgnoreHost(c *C) {
+ sto, err := DefaultLoader.Load(s.endpoint(c, fmt.Sprintf("file://%s", s.RepoPath)))
+ c.Assert(err, IsNil)
+ c.Assert(sto, NotNil)
+}
diff --git a/plumbing/transport/server/receive_pack_test.go b/plumbing/transport/server/receive_pack_test.go
new file mode 100644
index 0000000..2c4036a
--- /dev/null
+++ b/plumbing/transport/server/receive_pack_test.go
@@ -0,0 +1,40 @@
+package server_test
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/test"
+
+ . "gopkg.in/check.v1"
+)
+
+type ReceivePackSuite struct {
+ BaseSuite
+ test.ReceivePackSuite
+}
+
+var _ = Suite(&ReceivePackSuite{})
+
+func (s *ReceivePackSuite) SetUpSuite(c *C) {
+ s.BaseSuite.SetUpSuite(c)
+ s.ReceivePackSuite.Client = s.client
+}
+
+func (s *ReceivePackSuite) SetUpTest(c *C) {
+ s.prepareRepositories(c, &s.Endpoint, &s.EmptyEndpoint, &s.NonExistentEndpoint)
+}
+
+func (s *ReceivePackSuite) TearDownTest(c *C) {
+ s.Suite.TearDownSuite(c)
+}
+
+// TODO
+func (s *ReceivePackSuite) TestSendPackAddDeleteReference(c *C) {
+ c.Skip("delete reference not supported yet")
+}
+
+// Overwritten, server returns error earlier.
+func (s *ReceivePackSuite) TestAdvertisedReferencesNotExists(c *C) {
+ r, err := s.Client.NewReceivePackSession(s.NonExistentEndpoint)
+ c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(r, IsNil)
+}
diff --git a/plumbing/transport/server/server.go b/plumbing/transport/server/server.go
new file mode 100644
index 0000000..6787e9d
--- /dev/null
+++ b/plumbing/transport/server/server.go
@@ -0,0 +1,448 @@
+// Package server implements the git server protocol. For most use cases, the
+// transport-specific implementations should be used.
+package server
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packfile"
+ "gopkg.in/src-d/go-git.v4/plumbing/object"
+ "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp"
+ "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
+ "gopkg.in/src-d/go-git.v4/plumbing/revlist"
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+)
+
+var DefaultServer = NewServer(DefaultLoader)
+
+type server struct {
+ loader Loader
+ handler *handler
+}
+
+// NewServer returns a transport.Transport implementing a git server,
+// independent of transport. Each transport must wrap this.
+func NewServer(loader Loader) transport.Transport {
+ return &server{loader, &handler{}}
+}
+
+func (s *server) NewUploadPackSession(ep transport.Endpoint) (transport.UploadPackSession, error) {
+ sto, err := s.loader.Load(ep)
+ if err != nil {
+ return nil, err
+ }
+
+ return s.handler.NewUploadPackSession(sto)
+}
+
+func (s *server) NewReceivePackSession(ep transport.Endpoint) (transport.ReceivePackSession, error) {
+ sto, err := s.loader.Load(ep)
+ if err != nil {
+ return nil, err
+ }
+
+ return s.handler.NewReceivePackSession(sto)
+}
+
+type handler struct{}
+
+func (h *handler) NewUploadPackSession(s storer.Storer) (transport.UploadPackSession, error) {
+ return &upSession{
+ session: session{storer: s},
+ }, nil
+}
+
+func (h *handler) NewReceivePackSession(s storer.Storer) (transport.ReceivePackSession, error) {
+ return &rpSession{
+ session: session{storer: s},
+ cmdStatus: map[plumbing.ReferenceName]error{},
+ }, nil
+}
+
+type session struct {
+ storer storer.Storer
+ caps *capability.List
+}
+
+func (s *session) Close() error {
+ return nil
+}
+
+//TODO: deprecate
+func (s *session) SetAuth(transport.AuthMethod) error {
+ return nil
+}
+
+func (s *session) checkSupportedCapabilities(cl *capability.List) error {
+ for _, c := range cl.All() {
+ if !s.caps.Supports(c) {
+ return fmt.Errorf("unsupported capability: %s", c)
+ }
+ }
+
+ return nil
+}
+
+type upSession struct {
+ session
+}
+
+func (s *upSession) AdvertisedReferences() (*packp.AdvRefs, error) {
+ ar := packp.NewAdvRefs()
+
+ if err := s.setSupportedCapabilities(ar.Capabilities); err != nil {
+ return nil, err
+ }
+
+ s.caps = ar.Capabilities
+
+ if err := setReferences(s.storer, ar); err != nil {
+ return nil, err
+ }
+
+ if err := setHEAD(s.storer, ar); err != nil {
+ return nil, err
+ }
+
+ return ar, nil
+}
+
+func (s *upSession) UploadPack(req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) {
+ if req.IsEmpty() {
+ return nil, transport.ErrEmptyUploadPackRequest
+ }
+
+ if err := req.Validate(); err != nil {
+ return nil, err
+ }
+
+ if s.caps == nil {
+ s.caps = capability.NewList()
+ if err := s.setSupportedCapabilities(s.caps); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := s.checkSupportedCapabilities(req.Capabilities); err != nil {
+ return nil, err
+ }
+
+ s.caps = req.Capabilities
+
+ if len(req.Shallows) > 0 {
+ return nil, fmt.Errorf("shallow not supported")
+ }
+
+ objs, err := s.objectsToUpload(req)
+ if err != nil {
+ return nil, err
+ }
+
+ pr, pw := io.Pipe()
+ e := packfile.NewEncoder(pw, s.storer, false)
+ go func() {
+ _, err := e.Encode(objs)
+ pw.CloseWithError(err)
+ }()
+
+ return packp.NewUploadPackResponseWithPackfile(req, pr), nil
+}
+
+func (s *upSession) objectsToUpload(req *packp.UploadPackRequest) ([]plumbing.Hash, error) {
+ commits, err := s.commitsToUpload(req.Wants)
+ if err != nil {
+ return nil, err
+ }
+
+ return revlist.Objects(s.storer, commits, req.Haves)
+}
+
+func (s *upSession) commitsToUpload(wants []plumbing.Hash) ([]*object.Commit, error) {
+ var commits []*object.Commit
+ for _, h := range wants {
+ c, err := object.GetCommit(s.storer, h)
+ if err != nil {
+ return nil, err
+ }
+
+ commits = append(commits, c)
+ }
+
+ return commits, nil
+}
+
+func (*upSession) setSupportedCapabilities(c *capability.List) error {
+ if err := c.Set(capability.Agent, capability.DefaultAgent); err != nil {
+ return err
+ }
+
+ if err := c.Set(capability.OFSDelta); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type rpSession struct {
+ session
+ cmdStatus map[plumbing.ReferenceName]error
+ firstErr error
+ unpackErr error
+}
+
+func (s *rpSession) AdvertisedReferences() (*packp.AdvRefs, error) {
+ ar := packp.NewAdvRefs()
+
+ if err := s.setSupportedCapabilities(ar.Capabilities); err != nil {
+ return nil, err
+ }
+
+ s.caps = ar.Capabilities
+
+ if err := setReferences(s.storer, ar); err != nil {
+ return nil, err
+ }
+
+ if err := setHEAD(s.storer, ar); err != nil {
+ return nil, err
+ }
+
+ return ar, nil
+}
+
+var (
+ ErrUpdateReference = errors.New("failed to update ref")
+)
+
+func (s *rpSession) ReceivePack(req *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error) {
+ if s.caps == nil {
+ s.caps = capability.NewList()
+ if err := s.setSupportedCapabilities(s.caps); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := s.checkSupportedCapabilities(req.Capabilities); err != nil {
+ return nil, err
+ }
+
+ s.caps = req.Capabilities
+
+ //TODO: Implement 'atomic' update of references.
+
+ if err := s.writePackfile(req.Packfile); err != nil {
+ s.unpackErr = err
+ s.firstErr = err
+ return s.reportStatus(), err
+ }
+
+ updatedRefs := s.updatedReferences(req)
+
+ if s.caps.Supports(capability.Atomic) && s.firstErr != nil {
+ //TODO: add support for 'atomic' once we have reference
+ // transactions, currently we do not announce it.
+ rs := s.reportStatus()
+ for _, cs := range rs.CommandStatuses {
+ if cs.Error() == nil {
+ cs.Status = ""
+ }
+ }
+ }
+
+ for name, ref := range updatedRefs {
+ //TODO: add support for 'delete-refs' once we can delete
+ // references, currently we do not announce it.
+ err := s.storer.SetReference(ref)
+ s.setStatus(name, err)
+ }
+
+ return s.reportStatus(), s.firstErr
+}
+
+func (s *rpSession) updatedReferences(req *packp.ReferenceUpdateRequest) map[plumbing.ReferenceName]*plumbing.Reference {
+ refs := map[plumbing.ReferenceName]*plumbing.Reference{}
+ for _, cmd := range req.Commands {
+ exists, err := referenceExists(s.storer, cmd.Name)
+ if err != nil {
+ s.setStatus(cmd.Name, err)
+ continue
+ }
+
+ switch cmd.Action() {
+ case packp.Create:
+ if exists {
+ s.setStatus(cmd.Name, ErrUpdateReference)
+ continue
+ }
+
+ ref := plumbing.NewHashReference(cmd.Name, cmd.New)
+ refs[ref.Name()] = ref
+ case packp.Delete:
+ if !exists {
+ s.setStatus(cmd.Name, ErrUpdateReference)
+ continue
+ }
+
+ if !s.caps.Supports(capability.DeleteRefs) {
+ s.setStatus(cmd.Name, fmt.Errorf("delete not supported"))
+ continue
+ }
+
+ refs[cmd.Name] = nil
+ case packp.Update:
+ if !exists {
+ s.setStatus(cmd.Name, ErrUpdateReference)
+ continue
+ }
+
+ if err != nil {
+ s.setStatus(cmd.Name, err)
+ continue
+ }
+
+ ref := plumbing.NewHashReference(cmd.Name, cmd.New)
+ refs[ref.Name()] = ref
+ }
+ }
+
+ return refs
+}
+
+func (s *rpSession) failAtomicUpdate() (*packp.ReportStatus, error) {
+ rs := s.reportStatus()
+ for _, cs := range rs.CommandStatuses {
+ if cs.Error() == nil {
+ cs.Status = "atomic updated"
+ }
+ }
+
+ return rs, s.firstErr
+}
+
+func (s *rpSession) writePackfile(r io.ReadCloser) error {
+ if r == nil {
+ return nil
+ }
+
+ if err := packfile.UpdateObjectStorage(s.storer, r); err != nil {
+ _ = r.Close()
+ return err
+ }
+
+ return r.Close()
+}
+
+func (s *rpSession) setStatus(ref plumbing.ReferenceName, err error) {
+ s.cmdStatus[ref] = err
+ if s.firstErr == nil && err != nil {
+ s.firstErr = err
+ }
+}
+
+func (s *rpSession) reportStatus() *packp.ReportStatus {
+ if !s.caps.Supports(capability.ReportStatus) {
+ return nil
+ }
+
+ rs := packp.NewReportStatus()
+ rs.UnpackStatus = "ok"
+
+ if s.unpackErr != nil {
+ rs.UnpackStatus = s.unpackErr.Error()
+ }
+
+ if s.cmdStatus == nil {
+ return rs
+ }
+
+ for ref, err := range s.cmdStatus {
+ msg := "ok"
+ if err != nil {
+ msg = err.Error()
+ }
+ status := &packp.CommandStatus{
+ ReferenceName: ref,
+ Status: msg,
+ }
+ rs.CommandStatuses = append(rs.CommandStatuses, status)
+ }
+
+ return rs
+}
+
+func (*rpSession) setSupportedCapabilities(c *capability.List) error {
+ if err := c.Set(capability.Agent, capability.DefaultAgent); err != nil {
+ return err
+ }
+
+ if err := c.Set(capability.OFSDelta); err != nil {
+ return err
+ }
+
+ return c.Set(capability.ReportStatus)
+}
+
+func setHEAD(s storer.Storer, ar *packp.AdvRefs) error {
+ ref, err := s.Reference(plumbing.HEAD)
+ if err == plumbing.ErrReferenceNotFound {
+ return nil
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if ref.Type() == plumbing.SymbolicReference {
+ if err := ar.AddReference(ref); err != nil {
+ return nil
+ }
+
+ ref, err = storer.ResolveReference(s, ref.Target())
+ if err == plumbing.ErrReferenceNotFound {
+ return nil
+ }
+
+ if err != nil {
+ return err
+ }
+ }
+
+ if ref.Type() != plumbing.HashReference {
+ return plumbing.ErrInvalidType
+ }
+
+ h := ref.Hash()
+ ar.Head = &h
+
+ return nil
+}
+
+//TODO: add peeled references.
+func setReferences(s storer.Storer, ar *packp.AdvRefs) error {
+ iter, err := s.IterReferences()
+ if err != nil {
+ return err
+ }
+
+ return iter.ForEach(func(ref *plumbing.Reference) error {
+ if ref.Type() != plumbing.HashReference {
+ return nil
+ }
+
+ ar.References[ref.Name().String()] = ref.Hash()
+ return nil
+ })
+}
+
+func referenceExists(s storer.ReferenceStorer, n plumbing.ReferenceName) (bool, error) {
+ _, err := s.Reference(n)
+ if err == plumbing.ErrReferenceNotFound {
+ return false, nil
+ }
+
+ return err == nil, err
+}
diff --git a/plumbing/transport/server/server_test.go b/plumbing/transport/server/server_test.go
new file mode 100644
index 0000000..2020fe3
--- /dev/null
+++ b/plumbing/transport/server/server_test.go
@@ -0,0 +1,70 @@
+package server_test
+
+import (
+ "fmt"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v4/fixtures"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/client"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/server"
+ "gopkg.in/src-d/go-git.v4/storage/filesystem"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+const inprocScheme = "inproc"
+
+type BaseSuite struct {
+ fixtures.Suite
+ loader server.MapLoader
+ client transport.Transport
+ clientBackup transport.Transport
+}
+
+func (s *BaseSuite) SetUpSuite(c *C) {
+ s.Suite.SetUpSuite(c)
+ s.loader = server.MapLoader{}
+ s.client = server.NewServer(s.loader)
+ s.clientBackup = client.Protocols[inprocScheme]
+ client.Protocols[inprocScheme] = s.client
+}
+
+func (s *BaseSuite) TearDownSuite(c *C) {
+ if s.clientBackup == nil {
+ delete(client.Protocols, inprocScheme)
+ } else {
+ client.Protocols[inprocScheme] = s.clientBackup
+ }
+}
+
+func (s *BaseSuite) prepareRepositories(c *C, basic *transport.Endpoint,
+ empty *transport.Endpoint, nonExistent *transport.Endpoint) {
+
+ f := fixtures.Basic().One()
+ fs := f.DotGit()
+ path := fs.Base()
+ url := fmt.Sprintf("%s://%s", inprocScheme, path)
+ ep, err := transport.NewEndpoint(url)
+ c.Assert(err, IsNil)
+ *basic = ep
+ sto, err := filesystem.NewStorage(fs)
+ c.Assert(err, IsNil)
+ s.loader[ep] = sto
+
+ path = "/empty.git"
+ url = fmt.Sprintf("%s://%s", inprocScheme, path)
+ ep, err = transport.NewEndpoint(url)
+ c.Assert(err, IsNil)
+ *empty = ep
+ s.loader[ep] = memory.NewStorage()
+
+ path = "/non-existent.git"
+ url = fmt.Sprintf("%s://%s", inprocScheme, path)
+ ep, err = transport.NewEndpoint(url)
+ c.Assert(err, IsNil)
+ *nonExistent = ep
+}
diff --git a/plumbing/transport/server/upload_pack_test.go b/plumbing/transport/server/upload_pack_test.go
new file mode 100644
index 0000000..8919e8e
--- /dev/null
+++ b/plumbing/transport/server/upload_pack_test.go
@@ -0,0 +1,40 @@
+package server_test
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/test"
+
+ . "gopkg.in/check.v1"
+)
+
+type UploadPackSuite struct {
+ BaseSuite
+ test.UploadPackSuite
+}
+
+var _ = Suite(&UploadPackSuite{})
+
+func (s *UploadPackSuite) SetUpSuite(c *C) {
+ s.BaseSuite.SetUpSuite(c)
+ s.UploadPackSuite.Client = s.client
+}
+
+func (s *UploadPackSuite) SetUpTest(c *C) {
+ s.prepareRepositories(c, &s.Endpoint, &s.EmptyEndpoint, &s.NonExistentEndpoint)
+}
+
+// Overwritten, it's not an error in server-side.
+func (s *UploadPackSuite) TestAdvertisedReferencesEmpty(c *C) {
+ r, err := s.Client.NewUploadPackSession(s.EmptyEndpoint)
+ c.Assert(err, IsNil)
+ ar, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(len(ar.References), Equals, 0)
+}
+
+// Overwritten, server returns error earlier.
+func (s *UploadPackSuite) TestAdvertisedReferencesNotExists(c *C) {
+ r, err := s.Client.NewUploadPackSession(s.NonExistentEndpoint)
+ c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(r, IsNil)
+}