aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--remote.go86
-rw-r--r--remote_test.go87
-rw-r--r--repository.go64
-rw-r--r--repository_test.go87
4 files changed, 293 insertions, 31 deletions
diff --git a/remote.go b/remote.go
index c7bc00b..bc3f209 100644
--- a/remote.go
+++ b/remote.go
@@ -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)