diff options
-rw-r--r-- | remote.go | 86 | ||||
-rw-r--r-- | remote_test.go | 87 | ||||
-rw-r--r-- | repository.go | 64 | ||||
-rw-r--r-- | repository_test.go | 87 |
4 files changed, 293 insertions, 31 deletions
@@ -8,6 +8,7 @@ import ( "gopkg.in/src-d/go-git.v4/config" "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/protocol/packp/sideband" @@ -57,8 +58,7 @@ func (r *Remote) Fetch(o *FetchOptions) error { func (r *Remote) Push(o *PushOptions) (err error) { // TODO: Support deletes. // TODO: Support pushing tags. - // TODO: Check if force update is given, otherwise reject non-fast forward. - // TODO: Sideband suppor + // TODO: Sideband support if o.RemoteName == "" { o.RemoteName = r.c.Name @@ -265,37 +265,35 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, return nil } - dstName := rs.Dst(localRef.Name()) - oldHash := plumbing.ZeroHash - newHash := localRef.Hash() - - iter, err := remoteRefs.IterReferences() - if err != nil { - return err + cmd := &packp.Command{ + Name: rs.Dst(localRef.Name()), + Old: plumbing.ZeroHash, + New: localRef.Hash(), } - err = iter.ForEach(func(remoteRef *plumbing.Reference) error { + remoteRef, err := remoteRefs.Reference(cmd.Name) + if err == nil { if remoteRef.Type() != plumbing.HashReference { + //TODO: check actual git behavior here return nil } - if dstName != remoteRef.Name() { - return nil - } + cmd.Old = remoteRef.Hash() + } else if err != plumbing.ErrReferenceNotFound { + return err + } - oldHash = remoteRef.Hash() + if cmd.Old == cmd.New { return nil - }) + } - if oldHash == newHash { - return nil + if !rs.IsForceUpdate() { + if err := checkFastForwardUpdate(r.s, remoteRefs, cmd); err != nil { + return err + } } - req.Commands = append(req.Commands, &packp.Command{ - Name: dstName, - Old: oldHash, - New: newHash, - }) + req.Commands = append(req.Commands, cmd) return nil } @@ -390,6 +388,50 @@ func objectExists(s storer.EncodedObjectStorer, h plumbing.Hash) (bool, error) { return true, err } +func checkFastForwardUpdate(s storer.EncodedObjectStorer, remoteRefs storer.ReferenceStorer, cmd *packp.Command) error { + if cmd.Old == plumbing.ZeroHash { + _, err := remoteRefs.Reference(cmd.Name) + if err == plumbing.ErrReferenceNotFound { + return nil + } + + if err != nil { + return err + } + + return fmt.Errorf("non-fast-forward update: %s", cmd.Name.String()) + } + + ff, err := isFastForward(s, cmd.Old, cmd.New) + if err != nil { + return err + } + + if !ff { + return fmt.Errorf("non-fast-forward update: %s", cmd.Name.String()) + } + + return nil +} + +func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash) (bool, error) { + c, err := object.GetCommit(s, new) + if err != nil { + return false, err + } + + found := false + iter := object.NewCommitPreIterator(c) + return found, iter.ForEach(func(c *object.Commit) error { + if c.Hash != old { + return nil + } + + found = true + return storer.ErrStop + }) +} + func (r *Remote) newUploadPackRequest(o *FetchOptions, ar *packp.AdvRefs) (*packp.UploadPackRequest, error) { diff --git a/remote_test.go b/remote_test.go index 2cd80cf..d48b6ca 100644 --- a/remote_test.go +++ b/remote_test.go @@ -266,6 +266,93 @@ func (s *RemoteSuite) TestPushNoErrAlreadyUpToDate(c *C) { c.Assert(err, Equals, NoErrAlreadyUpToDate) } +func (s *RemoteSuite) TestPushRejectNonFastForward(c *C) { + f := fixtures.Basic().One() + sto, err := filesystem.NewStorage(f.DotGit()) + c.Assert(err, IsNil) + + dstFs := f.DotGit() + dstSto, err := filesystem.NewStorage(dstFs) + c.Assert(err, IsNil) + + url := fmt.Sprintf("file://%s", dstFs.Base()) + r := newRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: url, + }) + + oldRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + err = r.Push(&PushOptions{RefSpecs: []config.RefSpec{ + config.RefSpec("refs/heads/master:refs/heads/branch"), + }}) + c.Assert(err, ErrorMatches, "non-fast-forward update: refs/heads/branch") + + newRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(newRef, DeepEquals, oldRef) +} + +func (s *RemoteSuite) TestPushForce(c *C) { + f := fixtures.Basic().One() + sto, err := filesystem.NewStorage(f.DotGit()) + c.Assert(err, IsNil) + + dstFs := f.DotGit() + dstSto, err := filesystem.NewStorage(dstFs) + c.Assert(err, IsNil) + + url := fmt.Sprintf("file://%s", dstFs.Base()) + r := newRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: url, + }) + + oldRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + err = r.Push(&PushOptions{RefSpecs: []config.RefSpec{ + config.RefSpec("+refs/heads/master:refs/heads/branch"), + }}) + c.Assert(err, IsNil) + + newRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(newRef, Not(DeepEquals), oldRef) +} + +func (s *RemoteSuite) TestPushNewReference(c *C) { + f := fixtures.Basic().One() + sto, err := filesystem.NewStorage(f.DotGit()) + c.Assert(err, IsNil) + + dstFs := f.DotGit() + dstSto, err := filesystem.NewStorage(dstFs) + c.Assert(err, IsNil) + + url := fmt.Sprintf("file://%s", dstFs.Base()) + r := newRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: url, + }) + + oldRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch")) + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + err = r.Push(&PushOptions{RefSpecs: []config.RefSpec{ + config.RefSpec("refs/heads/branch:refs/heads/branch2"), + }}) + c.Assert(err, IsNil) + + newRef, err := dstSto.Reference(plumbing.ReferenceName("refs/heads/branch2")) + c.Assert(err, IsNil) + c.Assert(newRef.Hash(), Equals, oldRef.Hash()) +} + func (s *RemoteSuite) TestPushInvalidEndpoint(c *C) { r := newRemote(nil, &config.RemoteConfig{Name: "foo", URL: "qux"}) err := r.Push(&PushOptions{}) diff --git a/repository.go b/repository.go index bb59afe..8a7b348 100644 --- a/repository.go +++ b/repository.go @@ -3,8 +3,10 @@ package git import ( "errors" "fmt" + stdioutil "io/ioutil" "os" "path/filepath" + "strings" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/internal/revision" @@ -13,6 +15,7 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing/storer" "gopkg.in/src-d/go-git.v4/storage" "gopkg.in/src-d/go-git.v4/storage/filesystem" + "gopkg.in/src-d/go-git.v4/utils/ioutil" "gopkg.in/src-d/go-billy.v2" "gopkg.in/src-d/go-billy.v2/osfs" @@ -193,26 +196,69 @@ func PlainInit(path string, isBare bool) (*Repository, error) { // repository is bare or a normal one. If the path doesn't contain a valid // repository ErrRepositoryNotExists is returned func PlainOpen(path string) (*Repository, error) { - var wt, dot billy.Filesystem + dot, wt, err := dotGitToFilesystems(path) + if err != nil { + return nil, err + } + + s, err := filesystem.NewStorage(dot) + if err != nil { + return nil, err + } + return Open(s, wt) +} + +func dotGitToFilesystems(path string) (dot, wt billy.Filesystem, err error) { fs := osfs.New(path) - if _, err := fs.Stat(".git"); err != nil { + fi, err := fs.Stat(".git") + if err != nil { if !os.IsNotExist(err) { - return nil, err + return nil, nil, err } - dot = fs - } else { - wt = fs - dot = fs.Dir(".git") + return fs, nil, nil } - s, err := filesystem.NewStorage(dot) + if fi.IsDir() { + return fs.Dir(".git"), fs, nil + } + + dot, err = dotGitFileToFilesystem(fs) + if err != nil { + return nil, nil, err + } + + return dot, fs, nil +} + +func dotGitFileToFilesystem(fs billy.Filesystem) (billy.Filesystem, error) { + var err error + + f, err := fs.Open(".git") if err != nil { return nil, err } + defer ioutil.CheckClose(f, &err) - return Open(s, wt) + b, err := stdioutil.ReadAll(f) + if err != nil { + return nil, err + } + + line := string(b) + const prefix = "gitdir: " + if !strings.HasPrefix(line, prefix) { + return nil, fmt.Errorf(".git file has no %s prefix", prefix) + } + + gitdir := line[len(prefix):] + gitdir = strings.TrimSpace(gitdir) + if filepath.IsAbs(gitdir) { + return osfs.New(gitdir), nil + } + + return fs.Dir(gitdir), err } // PlainClone a repository into the path with the given options, isBare defines diff --git a/repository_test.go b/repository_test.go index 77bfde2..fd8d405 100644 --- a/repository_test.go +++ b/repository_test.go @@ -272,6 +272,93 @@ func (s *RepositorySuite) TestPlainOpenNotBare(c *C) { c.Assert(r, IsNil) } +func (s *RepositorySuite) testPlainOpenGitFile(c *C, f func(string, string) string) { + dir, err := ioutil.TempDir("", "plain-open") + c.Assert(err, IsNil) + defer os.RemoveAll(dir) + + r, err := PlainInit(dir, true) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + altDir, err := ioutil.TempDir("", "plain-open") + c.Assert(err, IsNil) + defer os.RemoveAll(altDir) + + err = ioutil.WriteFile(filepath.Join(altDir, ".git"), []byte(f(dir, altDir)), 0644) + c.Assert(err, IsNil) + + r, err = PlainOpen(altDir) + c.Assert(err, IsNil) + c.Assert(r, NotNil) +} + +func (s *RepositorySuite) TestPlainOpenBareAbsoluteGitDirFile(c *C) { + s.testPlainOpenGitFile(c, func(dir, altDir string) string { + return fmt.Sprintf("gitdir: %s\n", dir) + }) +} + +func (s *RepositorySuite) TestPlainOpenBareAbsoluteGitDirFileNoEOL(c *C) { + s.testPlainOpenGitFile(c, func(dir, altDir string) string { + return fmt.Sprintf("gitdir: %s", dir) + }) +} + +func (s *RepositorySuite) TestPlainOpenBareRelativeGitDirFile(c *C) { + s.testPlainOpenGitFile(c, func(dir, altDir string) string { + dir, err := filepath.Rel(altDir, dir) + c.Assert(err, IsNil) + return fmt.Sprintf("gitdir: %s\n", dir) + }) +} + +func (s *RepositorySuite) TestPlainOpenBareRelativeGitDirFileNoEOL(c *C) { + s.testPlainOpenGitFile(c, func(dir, altDir string) string { + dir, err := filepath.Rel(altDir, dir) + c.Assert(err, IsNil) + return fmt.Sprintf("gitdir: %s\n", dir) + }) +} + +func (s *RepositorySuite) TestPlainOpenBareRelativeGitDirFileTrailingGarbage(c *C) { + dir, err := ioutil.TempDir("", "plain-open") + c.Assert(err, IsNil) + defer os.RemoveAll(dir) + + r, err := PlainInit(dir, true) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + altDir, err := ioutil.TempDir("", "plain-open") + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(altDir, ".git"), []byte(fmt.Sprintf("gitdir: %s\nTRAILING", dir)), 0644) + c.Assert(err, IsNil) + + r, err = PlainOpen(altDir) + c.Assert(err, Equals, ErrRepositoryNotExists) + c.Assert(r, IsNil) +} + +func (s *RepositorySuite) TestPlainOpenBareRelativeGitDirFileBadPrefix(c *C) { + dir, err := ioutil.TempDir("", "plain-open") + c.Assert(err, IsNil) + defer os.RemoveAll(dir) + + r, err := PlainInit(dir, true) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + altDir, err := ioutil.TempDir("", "plain-open") + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(altDir, ".git"), []byte(fmt.Sprintf("xgitdir: %s\n", dir)), 0644) + c.Assert(err, IsNil) + + r, err = PlainOpen(altDir) + c.Assert(err, ErrorMatches, ".*gitdir.*") + c.Assert(r, IsNil) +} + func (s *RepositorySuite) TestPlainOpenNotExists(c *C) { r, err := PlainOpen("/not-exists/") c.Assert(err, Equals, ErrRepositoryNotExists) |