aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--_examples/remotes/main.go2
-rw-r--r--config/branch.go33
-rw-r--r--config/config.go1
-rw-r--r--config/config_test.go11
-rw-r--r--options.go15
-rw-r--r--plumbing/protocol/packp/updreq.go6
-rw-r--r--remote.go43
-rw-r--r--remote_test.go127
-rw-r--r--storage/filesystem/dotgit/dotgit_test.go7
-rw-r--r--storage/filesystem/object_test.go4
10 files changed, 231 insertions, 18 deletions
diff --git a/_examples/remotes/main.go b/_examples/remotes/main.go
index b1a91a9..d09957e 100644
--- a/_examples/remotes/main.go
+++ b/_examples/remotes/main.go
@@ -33,7 +33,7 @@ func main() {
CheckIfError(err)
// List remotes from a repository
- Info("git remotes -v")
+ Info("git remote -v")
list, err := r.Remotes()
CheckIfError(err)
diff --git a/config/branch.go b/config/branch.go
index fe86cf5..652270a 100644
--- a/config/branch.go
+++ b/config/branch.go
@@ -2,6 +2,7 @@ package config
import (
"errors"
+ "strings"
"github.com/go-git/go-git/v5/plumbing"
format "github.com/go-git/go-git/v5/plumbing/format/config"
@@ -26,6 +27,12 @@ type Branch struct {
// "true" and "interactive". "false" is undocumented and
// typically represented by the non-existence of this field
Rebase string
+ // Description explains what the branch is for.
+ // Multi-line explanations may be used.
+ //
+ // Original git command to edit:
+ // git branch --edit-description
+ Description string
raw *format.Subsection
}
@@ -75,9 +82,27 @@ func (b *Branch) marshal() *format.Subsection {
b.raw.SetOption(rebaseKey, b.Rebase)
}
+ if b.Description == "" {
+ b.raw.RemoveOption(descriptionKey)
+ } else {
+ desc := quoteDescription(b.Description)
+ b.raw.SetOption(descriptionKey, desc)
+ }
+
return b.raw
}
+// hack to trigger conditional quoting in the
+// plumbing/format/config/Encoder.encodeOptions
+//
+// Current Encoder implementation uses Go %q format if value contains a backslash character,
+// which is not consistent with reference git implementation.
+// git just replaces newline characters with \n, while Encoder prints them directly.
+// Until value quoting fix, we should escape description value by replacing newline characters with \n.
+func quoteDescription(desc string) string {
+ return strings.ReplaceAll(desc, "\n", `\n`)
+}
+
func (b *Branch) unmarshal(s *format.Subsection) error {
b.raw = s
@@ -85,6 +110,14 @@ func (b *Branch) unmarshal(s *format.Subsection) error {
b.Remote = b.raw.Options.Get(remoteSection)
b.Merge = plumbing.ReferenceName(b.raw.Options.Get(mergeKey))
b.Rebase = b.raw.Options.Get(rebaseKey)
+ b.Description = unquoteDescription(b.raw.Options.Get(descriptionKey))
return b.Validate()
}
+
+// hack to enable conditional quoting in the
+// plumbing/format/config/Encoder.encodeOptions
+// goto quoteDescription for details.
+func unquoteDescription(desc string) string {
+ return strings.ReplaceAll(desc, `\n`, "\n")
+}
diff --git a/config/config.go b/config/config.go
index 1aee25a..a16a5e5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -247,6 +247,7 @@ const (
rebaseKey = "rebase"
nameKey = "name"
emailKey = "email"
+ descriptionKey = "description"
defaultBranchKey = "defaultBranch"
// DefaultPackWindow holds the number of previous objects used to
diff --git a/config/config_test.go b/config/config_test.go
index 6f0242d..91f7df2 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -50,6 +50,7 @@ func (s *ConfigSuite) TestUnmarshal(c *C) {
[branch "master"]
remote = origin
merge = refs/heads/master
+ description = "Add support for branch description.\\n\\nEdit branch description: git branch --edit-description\\n"
[init]
defaultBranch = main
[url "ssh://git@github.com/"]
@@ -86,6 +87,7 @@ func (s *ConfigSuite) TestUnmarshal(c *C) {
c.Assert(cfg.Submodules["qux"].Branch, Equals, "bar")
c.Assert(cfg.Branches["master"].Remote, Equals, "origin")
c.Assert(cfg.Branches["master"].Merge, Equals, plumbing.ReferenceName("refs/heads/master"))
+ c.Assert(cfg.Branches["master"].Description, Equals, "Add support for branch description.\n\nEdit branch description: git branch --edit-description\n")
c.Assert(cfg.Init.DefaultBranch, Equals, "main")
}
@@ -111,6 +113,7 @@ func (s *ConfigSuite) TestMarshal(c *C) {
[branch "master"]
remote = origin
merge = refs/heads/master
+ description = "Add support for branch description.\\n\\nEdit branch description: git branch --edit-description\\n"
[url "ssh://git@github.com/"]
insteadOf = https://github.com/
[init]
@@ -149,9 +152,10 @@ func (s *ConfigSuite) TestMarshal(c *C) {
}
cfg.Branches["master"] = &Branch{
- Name: "master",
- Remote: "origin",
- Merge: "refs/heads/master",
+ Name: "master",
+ Remote: "origin",
+ Merge: "refs/heads/master",
+ Description: "Add support for branch description.\n\nEdit branch description: git branch --edit-description\n",
}
cfg.URLs["ssh://git@github.com/"] = &URL{
@@ -364,4 +368,5 @@ func (s *ConfigSuite) TestRemoveUrlOptions(c *C) {
if strings.Contains(string(buf), "url") {
c.Fatal("conifg should not contain any url sections")
}
+ c.Assert(err, IsNil)
}
diff --git a/options.go b/options.go
index 4b18ec1..283435b 100644
--- a/options.go
+++ b/options.go
@@ -228,10 +228,25 @@ type PushOptions struct {
// FollowTags will send any annotated tags with a commit target reachable from
// the refs already being pushed
FollowTags bool
+ // ForceWithLease allows a force push as long as the remote ref adheres to a "lease"
+ ForceWithLease *ForceWithLease
// PushOptions sets options to be transferred to the server during push.
Options map[string]string
}
+// ForceWithLease sets fields on the lease
+// If neither RefName nor Hash are set, ForceWithLease protects
+// all refs in the refspec by ensuring the ref of the remote in the local repsitory
+// matches the one in the ref advertisement.
+type ForceWithLease struct {
+ // RefName, when set will protect the ref by ensuring it matches the
+ // hash in the ref advertisement.
+ RefName plumbing.ReferenceName
+ // Hash is the expected object id of RefName. The push will be rejected unless this
+ // matches the corresponding object id of RefName in the refs advertisement.
+ Hash plumbing.Hash
+}
+
// Validate validates the fields and sets the default values.
func (o *PushOptions) Validate() error {
if o.RemoteName == "" {
diff --git a/plumbing/protocol/packp/updreq.go b/plumbing/protocol/packp/updreq.go
index 46ad6fd..5dbd8ac 100644
--- a/plumbing/protocol/packp/updreq.go
+++ b/plumbing/protocol/packp/updreq.go
@@ -87,9 +87,9 @@ type Action string
const (
Create Action = "create"
- Update = "update"
- Delete = "delete"
- Invalid = "invalid"
+ Update Action = "update"
+ Delete Action = "delete"
+ Invalid Action = "invalid"
)
type Command struct {
diff --git a/remote.go b/remote.go
index 426bde9..fecdebf 100644
--- a/remote.go
+++ b/remote.go
@@ -326,7 +326,7 @@ func (r *Remote) newReferenceUpdateRequest(
}
}
- if err := r.addReferencesToUpdate(o.RefSpecs, localRefs, remoteRefs, req, o.Prune); err != nil {
+ if err := r.addReferencesToUpdate(o.RefSpecs, localRefs, remoteRefs, req, o.Prune, o.ForceWithLease); err != nil {
return nil, err
}
@@ -568,6 +568,7 @@ func (r *Remote) addReferencesToUpdate(
remoteRefs storer.ReferenceStorer,
req *packp.ReferenceUpdateRequest,
prune bool,
+ forceWithLease *ForceWithLease,
) error {
// This references dictionary will be used to search references by name.
refsDict := make(map[string]*plumbing.Reference)
@@ -581,7 +582,7 @@ func (r *Remote) addReferencesToUpdate(
return err
}
} else {
- err := r.addOrUpdateReferences(rs, localRefs, refsDict, remoteRefs, req)
+ err := r.addOrUpdateReferences(rs, localRefs, refsDict, remoteRefs, req, forceWithLease)
if err != nil {
return err
}
@@ -603,6 +604,7 @@ func (r *Remote) addOrUpdateReferences(
refsDict map[string]*plumbing.Reference,
remoteRefs storer.ReferenceStorer,
req *packp.ReferenceUpdateRequest,
+ forceWithLease *ForceWithLease,
) error {
// If it is not a wilcard refspec we can directly search for the reference
// in the references dictionary.
@@ -616,11 +618,11 @@ func (r *Remote) addOrUpdateReferences(
return nil
}
- return r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req)
+ return r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req, forceWithLease)
}
for _, ref := range localRefs {
- err := r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req)
+ err := r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req, forceWithLease)
if err != nil {
return err
}
@@ -706,7 +708,7 @@ func (r *Remote) addCommit(rs config.RefSpec,
func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec,
remoteRefs storer.ReferenceStorer, localRef *plumbing.Reference,
- req *packp.ReferenceUpdateRequest) error {
+ req *packp.ReferenceUpdateRequest, forceWithLease *ForceWithLease) error {
if localRef.Type() != plumbing.HashReference {
return nil
@@ -738,7 +740,11 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec,
return nil
}
- if !rs.IsForceUpdate() {
+ if forceWithLease != nil {
+ if err = r.checkForceWithLease(localRef, cmd, forceWithLease); err != nil {
+ return err
+ }
+ } else if !rs.IsForceUpdate() {
if err := checkFastForwardUpdate(r.s, remoteRefs, cmd); err != nil {
return err
}
@@ -748,6 +754,31 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec,
return nil
}
+func (r *Remote) checkForceWithLease(localRef *plumbing.Reference, cmd *packp.Command, forceWithLease *ForceWithLease) error {
+ remotePrefix := fmt.Sprintf("refs/remotes/%s/", r.Config().Name)
+
+ ref, err := storer.ResolveReference(
+ r.s,
+ plumbing.ReferenceName(remotePrefix+strings.Replace(localRef.Name().String(), "refs/heads/", "", -1)))
+ if err != nil {
+ return err
+ }
+
+ if forceWithLease.RefName.String() == "" || (forceWithLease.RefName == cmd.Name) {
+ expectedOID := ref.Hash()
+
+ if !forceWithLease.Hash.IsZero() {
+ expectedOID = forceWithLease.Hash
+ }
+
+ if cmd.Old != expectedOID {
+ return fmt.Errorf("non-fast-forward update: %s", cmd.Name.String())
+ }
+ }
+
+ return nil
+}
+
func (r *Remote) references() ([]*plumbing.Reference, error) {
var localRefs []*plumbing.Reference
diff --git a/remote_test.go b/remote_test.go
index df07c08..ebe9a55 100644
--- a/remote_test.go
+++ b/remote_test.go
@@ -816,6 +816,133 @@ func (s *RemoteSuite) TestPushForceWithOption(c *C) {
c.Assert(newRef, Not(DeepEquals), oldRef)
}
+func (s *RemoteSuite) TestPushForceWithLease_success(c *C) {
+ testCases := []struct {
+ desc string
+ forceWithLease ForceWithLease
+ }{
+ {
+ desc: "no arguments",
+ forceWithLease: ForceWithLease{},
+ },
+ {
+ desc: "ref name",
+ forceWithLease: ForceWithLease{
+ RefName: plumbing.ReferenceName("refs/heads/branch"),
+ },
+ },
+ {
+ desc: "ref name and sha",
+ forceWithLease: ForceWithLease{
+ RefName: plumbing.ReferenceName("refs/heads/branch"),
+ Hash: plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ c.Log("Executing test cases:", tc.desc)
+
+ f := fixtures.Basic().One()
+ sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault())
+ dstFs := f.DotGit()
+ dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault())
+
+ newCommit := plumbing.NewHashReference(
+ "refs/heads/branch", plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
+ )
+ c.Assert(sto.SetReference(newCommit), IsNil)
+
+ ref, err := sto.Reference("refs/heads/branch")
+ c.Log(ref.String())
+
+ url := dstFs.Root()
+ r := NewRemote(sto, &config.RemoteConfig{
+ Name: DefaultRemoteName,
+ URLs: []string{url},
+ })
+
+ oldRef, err := dstSto.Reference("refs/heads/branch")
+ c.Assert(err, IsNil)
+ c.Assert(oldRef, NotNil)
+
+ c.Assert(r.Push(&PushOptions{
+ RefSpecs: []config.RefSpec{"refs/heads/branch:refs/heads/branch"},
+ ForceWithLease: &ForceWithLease{},
+ }), IsNil)
+
+ newRef, err := dstSto.Reference("refs/heads/branch")
+ c.Assert(err, IsNil)
+ c.Assert(newRef, DeepEquals, newCommit)
+ }
+}
+
+func (s *RemoteSuite) TestPushForceWithLease_failure(c *C) {
+ testCases := []struct {
+ desc string
+ forceWithLease ForceWithLease
+ }{
+ {
+ desc: "no arguments",
+ forceWithLease: ForceWithLease{},
+ },
+ {
+ desc: "ref name",
+ forceWithLease: ForceWithLease{
+ RefName: plumbing.ReferenceName("refs/heads/branch"),
+ },
+ },
+ {
+ desc: "ref name and sha",
+ forceWithLease: ForceWithLease{
+ RefName: plumbing.ReferenceName("refs/heads/branch"),
+ Hash: plumbing.NewHash("152175bf7e5580299fa1f0ba41ef6474cc043b70"),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ c.Log("Executing test cases:", tc.desc)
+
+ f := fixtures.Basic().One()
+ sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault())
+ c.Assert(sto.SetReference(
+ plumbing.NewHashReference(
+ "refs/heads/branch", plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"),
+ ),
+ ), IsNil)
+
+ dstFs := f.DotGit()
+ dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault())
+ c.Assert(dstSto.SetReference(
+ plumbing.NewHashReference(
+ "refs/heads/branch", plumbing.NewHash("ad7897c0fb8e7d9a9ba41fa66072cf06095a6cfc"),
+ ),
+ ), IsNil)
+
+ url := dstFs.Root()
+ r := NewRemote(sto, &config.RemoteConfig{
+ Name: DefaultRemoteName,
+ URLs: []string{url},
+ })
+
+ oldRef, err := dstSto.Reference("refs/heads/branch")
+ c.Assert(err, IsNil)
+ c.Assert(oldRef, NotNil)
+
+ err = r.Push(&PushOptions{
+ RefSpecs: []config.RefSpec{"refs/heads/branch:refs/heads/branch"},
+ ForceWithLease: &ForceWithLease{},
+ })
+
+ c.Assert(err, DeepEquals, errors.New("non-fast-forward update: refs/heads/branch"))
+
+ newRef, err := dstSto.Reference("refs/heads/branch")
+ c.Assert(err, IsNil)
+ c.Assert(newRef, Not(DeepEquals), plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"))
+ }
+}
+
func (s *RemoteSuite) TestPushPrune(c *C) {
fs := fixtures.Basic().One().DotGit()
diff --git a/storage/filesystem/dotgit/dotgit_test.go b/storage/filesystem/dotgit/dotgit_test.go
index 4c2ae94..1a09fde 100644
--- a/storage/filesystem/dotgit/dotgit_test.go
+++ b/storage/filesystem/dotgit/dotgit_test.go
@@ -3,6 +3,7 @@ package dotgit
import (
"bufio"
"encoding/hex"
+ "io"
"io/ioutil"
"os"
"path/filepath"
@@ -510,13 +511,13 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) {
c.Assert(filepath.Ext(pack.Name()), Equals, ".pack")
// Move to an specific offset
- pack.Seek(42, os.SEEK_SET)
+ pack.Seek(42, io.SeekStart)
pack2, err := dir.ObjectPack(plumbing.NewHash(f.PackfileHash))
c.Assert(err, IsNil)
// If the file is the same the offset should be the same
- offset, err := pack2.Seek(0, os.SEEK_CUR)
+ offset, err := pack2.Seek(0, io.SeekCurrent)
c.Assert(err, IsNil)
c.Assert(offset, Equals, int64(42))
@@ -527,7 +528,7 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) {
c.Assert(err, IsNil)
// If the file is opened again its offset should be 0
- offset, err = pack2.Seek(0, os.SEEK_CUR)
+ offset, err = pack2.Seek(0, io.SeekCurrent)
c.Assert(err, IsNil)
c.Assert(offset, Equals, int64(0))
diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go
index 59b40d3..1c3267b 100644
--- a/storage/filesystem/object_test.go
+++ b/storage/filesystem/object_test.go
@@ -71,7 +71,7 @@ func (s *FsSuite) TestGetFromPackfileKeepDescriptors(c *C) {
pack1, err := dg.ObjectPack(packfiles[0])
c.Assert(err, IsNil)
- pack1.Seek(42, os.SEEK_SET)
+ pack1.Seek(42, io.SeekStart)
err = o.Close()
c.Assert(err, IsNil)
@@ -79,7 +79,7 @@ func (s *FsSuite) TestGetFromPackfileKeepDescriptors(c *C) {
pack2, err := dg.ObjectPack(packfiles[0])
c.Assert(err, IsNil)
- offset, err := pack2.Seek(0, os.SEEK_CUR)
+ offset, err := pack2.Seek(0, io.SeekCurrent)
c.Assert(err, IsNil)
c.Assert(offset, Equals, int64(0))