diff options
Diffstat (limited to 'plumbing/transport/server')
-rw-r--r-- | plumbing/transport/server/loader.go | 59 | ||||
-rw-r--r-- | plumbing/transport/server/loader_test.go | 57 | ||||
-rw-r--r-- | plumbing/transport/server/receive_pack_test.go | 40 | ||||
-rw-r--r-- | plumbing/transport/server/server.go | 448 | ||||
-rw-r--r-- | plumbing/transport/server/server_test.go | 70 | ||||
-rw-r--r-- | plumbing/transport/server/upload_pack_test.go | 40 |
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) +} |