From b9f5efe3dbee0cd15a553bcc03f7a37f3dcaa676 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Sat, 18 Aug 2018 11:03:33 -0700 Subject: [PATCH 01/13] git: Add tagging support This adds a few methods: * CreateTag, which can be used to create both lightweight and annotated tags with a supplied TagObjectOptions struct. PGP signing is possible as well. * Tag, to fetch a single tag ref. As opposed to Tags or TagObjects, this will also fetch the tag object if it exists and return it along with the output. Lightweight tags just return the object as nil. * DeleteTag, to delete a tag. This simply deletes the ref. The object is left orphaned to be GCed later. I'm not 100% sure if DeleteTag is the correct behavior - looking for details on exactly *what* happens to a tag object if you delete the ref and not the tag were sparse, and groking the Git source did not really produce much insight to the untrained eye. This may be something that comes up in review. If deletion of the object is necessary, the in-memory storer may require some updates to allow DeleteLooseObject to be supported. Signed-off-by: Chris Marchesi --- options.go | 47 +++++++- repository.go | 134 ++++++++++++++++++++++- repository_test.go | 265 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 443 insertions(+), 3 deletions(-) diff --git a/options.go b/options.go index 7b1570f7b..6b00b0d02 100644 --- a/options.go +++ b/options.go @@ -348,8 +348,9 @@ type CommitOptions struct { // Parents are the parents commits for the new commit, by default when // len(Parents) is zero, the hash of HEAD reference is used. Parents []plumbing.Hash - // A key to sign the commit with. A nil value here means the commit will not - // be signed. The private key must be present and already decrypted. + // SignKey denotes a key to sign the commit with. A nil value here means the + // commit will not be signed. The private key must be present and already + // decrypted. SignKey *openpgp.Entity } @@ -377,6 +378,48 @@ func (o *CommitOptions) Validate(r *Repository) error { return nil } +var ( + ErrMissingName = errors.New("name field is required") + ErrMissingTagger = errors.New("tagger field is required") + ErrMissingMessage = errors.New("message field is required") + ErrBadObjectType = errors.New("bad object type for tagging") +) + +// TagObjectOptions describes how a tag object should be created. +type TagObjectOptions struct { + // Tagger defines the signature of the tag creator. + Tagger *object.Signature + // Message defines the annotation of the tag. + Message string + // TargetType is the object type of the target. The object specified by + // Target must be of this type. + TargetType plumbing.ObjectType + // SignKey denotes a key to sign the tag with. A nil value here means the tag + // will not be signed. The private key must be present and already decrypted. + SignKey *openpgp.Entity +} + +// Validate validates the fields and sets the default values. +func (o *TagObjectOptions) Validate(r *Repository, hash plumbing.Hash) error { + if o.Tagger == nil { + return ErrMissingTagger + } + + if o.Message == "" { + return ErrMissingMessage + } + + if o.TargetType == plumbing.InvalidObject || o.TargetType == plumbing.AnyObject { + return ErrBadObjectType + } + + if _, err := r.Object(o.TargetType, hash); err != nil { + return err + } + + return nil +} + // ListOptions describes how a remote list should be performed. type ListOptions struct { // Auth credentials, if required, to use with the remote repository. diff --git a/repository.go b/repository.go index 818cfb3c3..ab14eba0b 100644 --- a/repository.go +++ b/repository.go @@ -1,15 +1,18 @@ package git import ( + "bytes" "context" "errors" "fmt" stdioutil "io/ioutil" "os" + "path" "path/filepath" "strings" "time" + "golang.org/x/crypto/openpgp" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/internal/revision" "gopkg.in/src-d/go-git.v4/plumbing" @@ -31,7 +34,12 @@ var ( // ErrBranchExists an error stating the specified branch already exists ErrBranchExists = errors.New("branch already exists") // ErrBranchNotFound an error stating the specified branch does not exist - ErrBranchNotFound = errors.New("branch not found") + ErrBranchNotFound = errors.New("branch not found") + // ErrTagExists an error stating the specified tag already exists + ErrTagExists = errors.New("tag already exists") + // ErrTagNotFound an error stating the specified tag does not exist + ErrTagNotFound = errors.New("tag not found") + ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch") ErrRepositoryNotExists = errors.New("repository does not exist") ErrRepositoryAlreadyExists = errors.New("repository already exists") @@ -484,6 +492,130 @@ func (r *Repository) DeleteBranch(name string) error { return r.Storer.SetConfig(cfg) } +// CreateTag creates a tag. If opts is included, the tag is an annotated tag, +// otherwise a lightweight tag is created. +func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *TagObjectOptions) (*plumbing.Reference, error) { + rname := plumbing.ReferenceName(path.Join("refs", "tags", name)) + + _, err := r.Storer.Reference(rname) + switch err { + case nil: + // Tag exists, this is an error + return nil, ErrTagExists + case plumbing.ErrReferenceNotFound: + // Tag missing, available for creation, pass this + default: + // Some other error + return nil, err + } + + var target plumbing.Hash + if opts != nil { + target, err = r.createTagObject(name, hash, opts) + if err != nil { + return nil, err + } + } else { + target = hash + } + + ref := plumbing.NewHashReference(rname, target) + if err = r.Storer.SetReference(ref); err != nil { + return nil, err + } + + return ref, nil +} + +func (r *Repository) createTagObject(name string, hash plumbing.Hash, opts *TagObjectOptions) (plumbing.Hash, error) { + if err := opts.Validate(r, hash); err != nil { + return plumbing.ZeroHash, err + } + + tag := &object.Tag{ + Name: name, + Tagger: *opts.Tagger, + Message: opts.Message, + TargetType: opts.TargetType, + Target: hash, + } + + if opts.SignKey != nil { + sig, err := r.buildTagSignature(tag, opts.SignKey) + if err != nil { + return plumbing.ZeroHash, err + } + + tag.PGPSignature = sig + } + + obj := r.Storer.NewEncodedObject() + if err := tag.Encode(obj); err != nil { + return plumbing.ZeroHash, err + } + + return r.Storer.SetEncodedObject(obj) +} + +func (r *Repository) buildTagSignature(tag *object.Tag, signKey *openpgp.Entity) (string, error) { + encoded := &plumbing.MemoryObject{} + if err := tag.Encode(encoded); err != nil { + return "", err + } + + rdr, err := encoded.Reader() + if err != nil { + return "", err + } + + var b bytes.Buffer + if err := openpgp.ArmoredDetachSign(&b, signKey, rdr, nil); err != nil { + return "", err + } + + return b.String(), nil +} + +// Tag fetches a tag from the repository. The tag is returned as a raw +// reference. If the tag is annotated, a non-nil tag object is returned. +func (r *Repository) Tag(name string) (*plumbing.Reference, *object.Tag, error) { + ref, err := r.Reference(plumbing.ReferenceName(path.Join("refs", "tags", name)), false) + if err != nil { + if err == plumbing.ErrReferenceNotFound { + // Return a friendly error for this one, versus just ReferenceNotFound. + return nil, nil, ErrTagNotFound + } + + return nil, nil, err + } + + obj, err := r.TagObject(ref.Hash()) + if err != nil && err != plumbing.ErrObjectNotFound { + return nil, nil, err + } + + return ref, obj, nil +} + +// DeleteTag deletes a tag from the repository. +func (r *Repository) DeleteTag(name string) error { + _, obj, err := r.Tag(name) + if err != nil { + return err + } + + if err = r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name))); err != nil { + return err + } + + // Delete the tag object if this was an annotated tag. + if obj != nil { + return r.DeleteObject(obj.Hash) + } + + return nil +} + func (r *Repository) resolveToCommitHash(h plumbing.Hash) (plumbing.Hash, error) { obj, err := r.Storer.EncodedObject(plumbing.AnyObject, h) if err != nil { diff --git a/repository_test.go b/repository_test.go index 261af7a7b..4fd26a189 100644 --- a/repository_test.go +++ b/repository_test.go @@ -13,6 +13,9 @@ import ( "testing" "time" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + openpgperr "golang.org/x/crypto/openpgp/errors" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" @@ -1275,6 +1278,268 @@ func (s *RepositorySuite) TestTags(c *C) { c.Assert(count, Equals, 5) } +func (s *RepositorySuite) TestCreateTagLightweight(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + expected, err := r.Head() + c.Assert(err, IsNil) + + ref, err := r.CreateTag("foobar", expected.Hash(), nil) + c.Assert(err, IsNil) + c.Assert(ref, NotNil) + + actual, obj, err := r.Tag("foobar") + c.Assert(err, IsNil) + c.Assert(obj, IsNil) + + c.Assert(expected.Hash(), Equals, actual.Hash()) +} + +func (s *RepositorySuite) TestCreateTagLightweightExists(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + expected, err := r.Head() + c.Assert(err, IsNil) + + ref, err := r.CreateTag("lightweight-tag", expected.Hash(), nil) + c.Assert(ref, IsNil) + c.Assert(err, Equals, ErrTagExists) +} + +func (s *RepositorySuite) TestCreateTagAnnotated(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + h, err := r.Head() + c.Assert(err, IsNil) + + expectedHash := h.Hash() + + ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "foo bar baz qux", + TargetType: plumbing.CommitObject, + }) + c.Assert(err, IsNil) + + tag, obj, err := r.Tag("foobar") + c.Assert(err, IsNil) + c.Assert(obj, NotNil) + + c.Assert(ref, DeepEquals, tag) + c.Assert(obj.Hash, Equals, ref.Hash()) + c.Assert(obj.Type(), Equals, plumbing.TagObject) + c.Assert(obj.Target, Equals, expectedHash) +} + +func (s *RepositorySuite) TestCreateTagAnnotatedBadOpts(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + h, err := r.Head() + c.Assert(err, IsNil) + + expectedHash := h.Hash() + + ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + Message: "foo bar baz qux", + TargetType: plumbing.CommitObject, + }) + c.Assert(ref, IsNil) + c.Assert(err, Equals, ErrMissingTagger) + + ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + Tagger: defaultSignature(), + TargetType: plumbing.CommitObject, + }) + c.Assert(ref, IsNil) + c.Assert(err, Equals, ErrMissingMessage) + + ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "foo bar baz qux", + }) + c.Assert(ref, IsNil) + c.Assert(err, Equals, ErrBadObjectType) + + ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "foo bar baz qux", + TargetType: plumbing.TagObject, + }) + c.Assert(ref, IsNil) + c.Assert(err, Equals, plumbing.ErrObjectNotFound) +} + +func (s *RepositorySuite) TestCreateTagSigned(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + h, err := r.Head() + c.Assert(err, IsNil) + + key := commitSignKey(c, true) + _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "foo bar baz qux", + TargetType: plumbing.CommitObject, + SignKey: key, + }) + c.Assert(err, IsNil) + + _, obj, err := r.Tag("foobar") + c.Assert(err, IsNil) + c.Assert(obj, NotNil) + + // Verify the tag. + pks := new(bytes.Buffer) + pkw, err := armor.Encode(pks, openpgp.PublicKeyType, nil) + c.Assert(err, IsNil) + + err = key.Serialize(pkw) + c.Assert(err, IsNil) + err = pkw.Close() + c.Assert(err, IsNil) + + actual, err := obj.Verify(pks.String()) + c.Assert(err, IsNil) + c.Assert(actual.PrimaryKey, DeepEquals, key.PrimaryKey) +} + +func (s *RepositorySuite) TestCreateTagSignedBadKey(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + h, err := r.Head() + c.Assert(err, IsNil) + + key := commitSignKey(c, false) + _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "foo bar baz qux", + TargetType: plumbing.CommitObject, + SignKey: key, + }) + c.Assert(err, Equals, openpgperr.InvalidArgumentError("signing key is encrypted")) +} + +func (s *RepositorySuite) TestTagLightweight(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + expected := plumbing.NewHash("f7b877701fbf855b44c0a9e86f3fdce2c298b07f") + + tag, obj, err := r.Tag("lightweight-tag") + c.Assert(err, IsNil) + c.Assert(obj, IsNil) + + actual := tag.Hash() + c.Assert(expected, Equals, actual) +} + +func (s *RepositorySuite) TestTagLightweightMissingTag(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + tag, obj, err := r.Tag("lightweight-tag-tag") + c.Assert(tag, IsNil) + c.Assert(obj, IsNil) + c.Assert(err, Equals, ErrTagNotFound) +} + +func (s *RepositorySuite) TestTagAnnotated(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + tag, obj, err := r.Tag("annotated-tag") + c.Assert(err, IsNil) + c.Assert(obj, NotNil) + + expectedHash := plumbing.NewHash("b742a2a9fa0afcfa9a6fad080980fbc26b007c69") + expectedTarget := plumbing.NewHash("f7b877701fbf855b44c0a9e86f3fdce2c298b07f") + actualHash := tag.Hash() + c.Assert(expectedHash, Equals, actualHash) + c.Assert(obj.Hash, Equals, expectedHash) + c.Assert(obj.Type(), Equals, plumbing.TagObject) + c.Assert(obj.Target, Equals, expectedTarget) +} + +func (s *RepositorySuite) TestDeleteTag(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + err = r.DeleteTag("lightweight-tag") + c.Assert(err, IsNil) + + _, _, err = r.Tag("lightweight-tag") + c.Assert(err, Equals, ErrTagNotFound) +} + +func (s *RepositorySuite) TestDeleteTagMissingTag(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + err = r.DeleteTag("lightweight-tag-tag") + c.Assert(err, Equals, ErrTagNotFound) +} + func (s *RepositorySuite) TestBranches(c *C) { f := fixtures.ByURL("https://github.com/git-fixtures/root-references.git").One() sto, err := filesystem.NewStorage(f.DotGit()) From 9b73a3ead6559576cb017b09c41c23c251b5af1c Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Tue, 21 Aug 2018 17:38:09 -0700 Subject: [PATCH 02/13] plumbing: object, correct tag PGP encoding As with the update in ec3d2a8, tag encoding needed to be corrected to ensure extra newlines were not being added in during tag object encoding, so that it did not corrupt the object for verification. Signed-off-by: Chris Marchesi --- plumbing/object/tag.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plumbing/object/tag.go b/plumbing/object/tag.go index 905206bda..63549730f 100644 --- a/plumbing/object/tag.go +++ b/plumbing/object/tag.go @@ -195,13 +195,9 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { return err } - if t.PGPSignature != "" && includeSig { - // Split all the signature lines and write with a newline at the end. - lines := strings.Split(t.PGPSignature, "\n") - for _, line := range lines { - if _, err = fmt.Fprintf(w, "%s\n", line); err != nil { - return err - } + if includeSig { + if _, err = fmt.Fprint(w, "\n"+t.PGPSignature); err != nil { + return err } } From 8c3c8b30b3394677d8eb16b159bfe9b4f61726a8 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Tue, 21 Aug 2018 22:20:09 -0700 Subject: [PATCH 03/13] plumbing: object, don't add extra newline on PGP signature Tag encoding/decoding seems to be a lot more sensitive to requiring the exact expected format in the object, which generally includes messages canonicalized so that they have a newline on the end (even if they didn't before). As such, the message should be written with the newline (no need for an extra), and the PGP signature right after that, which will be newline split already, so there's no need to split it again. All of this means it's very important for the caller to send the message in the correct format - which I'm correcting in the next commit. Signed-off-by: Chris Marchesi --- plumbing/object/tag.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plumbing/object/tag.go b/plumbing/object/tag.go index 63549730f..03749f9a4 100644 --- a/plumbing/object/tag.go +++ b/plumbing/object/tag.go @@ -195,8 +195,13 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { return err } + // Note that this is highly sensitive to what it sent along in the message. + // Message *always* needs to end with a newline, or else the message and the + // signature will be concatenated into a corrupt object. Since this is a + // lower-level method, we assume you know what you are doing and have already + // done the needful on the message in the caller. if includeSig { - if _, err = fmt.Fprint(w, "\n"+t.PGPSignature); err != nil { + if _, err = fmt.Fprint(w, t.PGPSignature); err != nil { return err } } From 2c2b532a525ab2c05f6e9675efd529a052121713 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Tue, 21 Aug 2018 22:24:41 -0700 Subject: [PATCH 04/13] git: Canonicalize incoming annotated tag messages Tag messages are highly sensitive to being in the expected format, especially when encoding/decoding for PGP verification. As such, we do a simple trimming of whitespace on the incoming message and add a newline on the end, to ensure there are no surprises here. Signed-off-by: Chris Marchesi --- options.go | 8 +++++++- repository_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/options.go b/options.go index 6b00b0d02..ed0baa3ad 100644 --- a/options.go +++ b/options.go @@ -3,6 +3,7 @@ package git import ( "errors" "regexp" + "strings" "golang.org/x/crypto/openpgp" "gopkg.in/src-d/go-git.v4/config" @@ -389,7 +390,9 @@ var ( type TagObjectOptions struct { // Tagger defines the signature of the tag creator. Tagger *object.Signature - // Message defines the annotation of the tag. + // Message defines the annotation of the tag. It is canonicalized during + // validation into the format expected by git - no leading whitespace and + // ending in a newline. Message string // TargetType is the object type of the target. The object specified by // Target must be of this type. @@ -409,6 +412,9 @@ func (o *TagObjectOptions) Validate(r *Repository, hash plumbing.Hash) error { return ErrMissingMessage } + // Canonicalize the message into the expected message format. + o.Message = strings.TrimSpace(o.Message) + "\n" + if o.TargetType == plumbing.InvalidObject || o.TargetType == plumbing.AnyObject { return ErrBadObjectType } diff --git a/repository_test.go b/repository_test.go index 4fd26a189..fd80152e7 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1455,6 +1455,49 @@ func (s *RepositorySuite) TestCreateTagSignedBadKey(c *C) { c.Assert(err, Equals, openpgperr.InvalidArgumentError("signing key is encrypted")) } +func (s *RepositorySuite) TestCreateTagCanonicalize(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + h, err := r.Head() + c.Assert(err, IsNil) + + key := commitSignKey(c, true) + _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "\n\nfoo bar baz qux\n\nsome message here", + TargetType: plumbing.CommitObject, + SignKey: key, + }) + c.Assert(err, IsNil) + + _, obj, err := r.Tag("foobar") + c.Assert(err, IsNil) + c.Assert(obj, NotNil) + + // Assert the new canonicalized message. + c.Assert(obj.Message, Equals, "foo bar baz qux\n\nsome message here\n") + + // Verify the tag. + pks := new(bytes.Buffer) + pkw, err := armor.Encode(pks, openpgp.PublicKeyType, nil) + c.Assert(err, IsNil) + + err = key.Serialize(pkw) + c.Assert(err, IsNil) + err = pkw.Close() + c.Assert(err, IsNil) + + actual, err := obj.Verify(pks.String()) + c.Assert(err, IsNil) + c.Assert(actual.PrimaryKey, DeepEquals, key.PrimaryKey) +} + func (s *RepositorySuite) TestTagLightweight(c *C) { url := s.GetLocalRepositoryURL( fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), From 8d8d4c52f42d5e5e7eccd8e51671b7d73d331ea0 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Tue, 21 Aug 2018 22:33:29 -0700 Subject: [PATCH 05/13] git: Replace test signing key with one with longer expiry The old one was created with defaults, which would have caused CI failures in 2 years. The new one is valid for 10 years: > gpg --list-secret-keys /root/.gnupg/pubring.kbx ------------------------ sec rsa4096 2018-08-22 [SC] [expires: 2028-08-19] 93A17FF01E54328546087C8E029395402EFCCD53 uid [ unknown] foo bar Signed-off-by: Chris Marchesi --- worktree_commit_test.go | 106 ++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/worktree_commit_test.go b/worktree_commit_test.go index 004926fc5..fdeaec0c5 100644 --- a/worktree_commit_test.go +++ b/worktree_commit_test.go @@ -257,59 +257,59 @@ func commitSignKey(c *C, decrypt bool) *openpgp.Entity { const armoredKeyRing = ` -----BEGIN PGP PRIVATE KEY BLOCK----- -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 -=8g3t +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 +=gIM9 -----END PGP PRIVATE KEY BLOCK----- ` From 119459a6b9ddaa244f76f67b182bf2c627434d02 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Thu, 23 Aug 2018 10:14:21 -0700 Subject: [PATCH 06/13] git: Discern tag target type from supplied hash I figured there was a way to do this without having to have TagObjectOptions supply this in - there is. Added support for this in and removed the object type from TagObjectOptions. Signed-off-by: Chris Marchesi --- options.go | 12 ----------- repository.go | 7 ++++++- repository_test.go | 52 +++++++++++++++++++++------------------------- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/options.go b/options.go index ed0baa3ad..856bd5e24 100644 --- a/options.go +++ b/options.go @@ -383,7 +383,6 @@ var ( ErrMissingName = errors.New("name field is required") ErrMissingTagger = errors.New("tagger field is required") ErrMissingMessage = errors.New("message field is required") - ErrBadObjectType = errors.New("bad object type for tagging") ) // TagObjectOptions describes how a tag object should be created. @@ -394,9 +393,6 @@ type TagObjectOptions struct { // validation into the format expected by git - no leading whitespace and // ending in a newline. Message string - // TargetType is the object type of the target. The object specified by - // Target must be of this type. - TargetType plumbing.ObjectType // SignKey denotes a key to sign the tag with. A nil value here means the tag // will not be signed. The private key must be present and already decrypted. SignKey *openpgp.Entity @@ -415,14 +411,6 @@ func (o *TagObjectOptions) Validate(r *Repository, hash plumbing.Hash) error { // Canonicalize the message into the expected message format. o.Message = strings.TrimSpace(o.Message) + "\n" - if o.TargetType == plumbing.InvalidObject || o.TargetType == plumbing.AnyObject { - return ErrBadObjectType - } - - if _, err := r.Object(o.TargetType, hash); err != nil { - return err - } - return nil } diff --git a/repository.go b/repository.go index ab14eba0b..6da15a13a 100644 --- a/repository.go +++ b/repository.go @@ -532,11 +532,16 @@ func (r *Repository) createTagObject(name string, hash plumbing.Hash, opts *TagO return plumbing.ZeroHash, err } + rawobj, err := object.GetObject(r.Storer, hash) + if err != nil { + return plumbing.ZeroHash, err + } + tag := &object.Tag{ Name: name, Tagger: *opts.Tagger, Message: opts.Message, - TargetType: opts.TargetType, + TargetType: rawobj.Type(), Target: hash, } diff --git a/repository_test.go b/repository_test.go index fd80152e7..795ee55c0 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1333,9 +1333,8 @@ func (s *RepositorySuite) TestCreateTagAnnotated(c *C) { expectedHash := h.Hash() ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ - Tagger: defaultSignature(), - Message: "foo bar baz qux", - TargetType: plumbing.CommitObject, + Tagger: defaultSignature(), + Message: "foo bar baz qux", }) c.Assert(err, IsNil) @@ -1364,32 +1363,32 @@ func (s *RepositorySuite) TestCreateTagAnnotatedBadOpts(c *C) { expectedHash := h.Hash() ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ - Message: "foo bar baz qux", - TargetType: plumbing.CommitObject, + Message: "foo bar baz qux", }) c.Assert(ref, IsNil) c.Assert(err, Equals, ErrMissingTagger) ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ - Tagger: defaultSignature(), - TargetType: plumbing.CommitObject, + Tagger: defaultSignature(), }) c.Assert(ref, IsNil) c.Assert(err, Equals, ErrMissingMessage) +} - ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ +func (s *RepositorySuite) TestCreateTagAnnotatedBadHash(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + ref, err := r.CreateTag("foobar", plumbing.ZeroHash, &TagObjectOptions{ Tagger: defaultSignature(), Message: "foo bar baz qux", }) c.Assert(ref, IsNil) - c.Assert(err, Equals, ErrBadObjectType) - - ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ - Tagger: defaultSignature(), - Message: "foo bar baz qux", - TargetType: plumbing.TagObject, - }) - c.Assert(ref, IsNil) c.Assert(err, Equals, plumbing.ErrObjectNotFound) } @@ -1407,10 +1406,9 @@ func (s *RepositorySuite) TestCreateTagSigned(c *C) { key := commitSignKey(c, true) _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ - Tagger: defaultSignature(), - Message: "foo bar baz qux", - TargetType: plumbing.CommitObject, - SignKey: key, + Tagger: defaultSignature(), + Message: "foo bar baz qux", + SignKey: key, }) c.Assert(err, IsNil) @@ -1447,10 +1445,9 @@ func (s *RepositorySuite) TestCreateTagSignedBadKey(c *C) { key := commitSignKey(c, false) _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ - Tagger: defaultSignature(), - Message: "foo bar baz qux", - TargetType: plumbing.CommitObject, - SignKey: key, + Tagger: defaultSignature(), + Message: "foo bar baz qux", + SignKey: key, }) c.Assert(err, Equals, openpgperr.InvalidArgumentError("signing key is encrypted")) } @@ -1469,10 +1466,9 @@ func (s *RepositorySuite) TestCreateTagCanonicalize(c *C) { key := commitSignKey(c, true) _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ - Tagger: defaultSignature(), - Message: "\n\nfoo bar baz qux\n\nsome message here", - TargetType: plumbing.CommitObject, - SignKey: key, + Tagger: defaultSignature(), + Message: "\n\nfoo bar baz qux\n\nsome message here", + SignKey: key, }) c.Assert(err, IsNil) From 01631f0e5c4be73cefaa8b2cc8a4811005871656 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Thu, 23 Aug 2018 11:57:51 -0700 Subject: [PATCH 07/13] git: Don't return tag object with Tag, adjust docs for Tag and Tags I've mainly noticed that in using the current Tag function, that there were lots of times that I was ignoring the ref or the object, depending on what I needed. This was evident in the tests as well. As such, I think it just makes more sense for a singular tag fetcher to return just a ref, through which the caller may grab the annotation if they need it, and if it exists. Also, contrary to the docs on Tags, all tags have a ref, even if they are annotated. The difference between a lightweight tag and an annotated tag is the presence of the tag object, which the ref will point to if the tag is annotated. As such I've adjusted the docs with an example as to how one can get the annotation for a tag ref through the iterator. Source: https://git-scm.com/book/en/v2/Git-Internals-Git-References, tags section. Signed-off-by: Chris Marchesi --- repository.go | 70 ++++++++++++++++++++++++++++++++++++---------- repository_test.go | 51 +++++++++++---------------------- 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/repository.go b/repository.go index 6da15a13a..68cc5cc28 100644 --- a/repository.go +++ b/repository.go @@ -581,34 +581,52 @@ func (r *Repository) buildTagSignature(tag *object.Tag, signKey *openpgp.Entity) return b.String(), nil } -// Tag fetches a tag from the repository. The tag is returned as a raw -// reference. If the tag is annotated, a non-nil tag object is returned. -func (r *Repository) Tag(name string) (*plumbing.Reference, *object.Tag, error) { +// Tag fetches a tag from the repository. +// +// If you want to check to see if the tag is an annotated tag, you can call +// TagObject on the hash of the reference in ForEach: +// +// ref, err := r.Tag("v0.1.0") +// if err != nil { +// // Handle error +// } +// +// obj, err := r.TagObject(ref.Hash()) +// switch err { +// case nil: +// // Tag object present +// case plumbing.ErrObjectNotFound: +// // Not a tag object +// default: +// // Some other error +// } +// +func (r *Repository) Tag(name string) (*plumbing.Reference, error) { ref, err := r.Reference(plumbing.ReferenceName(path.Join("refs", "tags", name)), false) if err != nil { if err == plumbing.ErrReferenceNotFound { // Return a friendly error for this one, versus just ReferenceNotFound. - return nil, nil, ErrTagNotFound + return nil, ErrTagNotFound } - return nil, nil, err - } - - obj, err := r.TagObject(ref.Hash()) - if err != nil && err != plumbing.ErrObjectNotFound { - return nil, nil, err + return nil, err } - return ref, obj, nil + return ref, nil } // DeleteTag deletes a tag from the repository. func (r *Repository) DeleteTag(name string) error { - _, obj, err := r.Tag(name) + ref, err := r.Tag(name) if err != nil { return err } + obj, err := r.TagObject(ref.Hash()) + if err != nil && err != plumbing.ErrObjectNotFound { + return err + } + if err = r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name))); err != nil { return err } @@ -982,9 +1000,31 @@ func (r *Repository) Log(o *LogOptions) (object.CommitIter, error) { return nil, fmt.Errorf("invalid Order=%v", o.Order) } -// Tags returns all the References from Tags. This method returns only lightweight -// tags. Note that not all the tags are lightweight ones. To return annotated tags -// too, you need to call TagObjects() method. +// Tags returns all the tag References in a repository. +// +// If you want to check to see if the tag is an annotated tag, you can call +// TagObject on the hash Reference passed in through ForEach: +// +// iter, err := r.Tags() +// if err != nil { +// // Handle error +// } +// +// if err := iter.ForEach(func (ref *plumbing.Reference) error { +// obj, err := r.TagObject(ref.Hash()) +// switch err { +// case nil: +// // Tag object present +// case plumbing.ErrObjectNotFound: +// // Not a tag object +// default: +// // Some other error +// return err +// } +// }); err != nil { +// // Handle outer iterator error +// } +// func (r *Repository) Tags() (storer.ReferenceIter, error) { refIter, err := r.Storer.IterReferences() if err != nil { diff --git a/repository_test.go b/repository_test.go index 795ee55c0..0415cc41a 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1294,9 +1294,8 @@ func (s *RepositorySuite) TestCreateTagLightweight(c *C) { c.Assert(err, IsNil) c.Assert(ref, NotNil) - actual, obj, err := r.Tag("foobar") + actual, err := r.Tag("foobar") c.Assert(err, IsNil) - c.Assert(obj, IsNil) c.Assert(expected.Hash(), Equals, actual.Hash()) } @@ -1338,9 +1337,11 @@ func (s *RepositorySuite) TestCreateTagAnnotated(c *C) { }) c.Assert(err, IsNil) - tag, obj, err := r.Tag("foobar") + tag, err := r.Tag("foobar") + c.Assert(err, IsNil) + + obj, err := r.TagObject(tag.Hash()) c.Assert(err, IsNil) - c.Assert(obj, NotNil) c.Assert(ref, DeepEquals, tag) c.Assert(obj.Hash, Equals, ref.Hash()) @@ -1412,9 +1413,11 @@ func (s *RepositorySuite) TestCreateTagSigned(c *C) { }) c.Assert(err, IsNil) - _, obj, err := r.Tag("foobar") + tag, err := r.Tag("foobar") + c.Assert(err, IsNil) + + obj, err := r.TagObject(tag.Hash()) c.Assert(err, IsNil) - c.Assert(obj, NotNil) // Verify the tag. pks := new(bytes.Buffer) @@ -1472,9 +1475,11 @@ func (s *RepositorySuite) TestCreateTagCanonicalize(c *C) { }) c.Assert(err, IsNil) - _, obj, err := r.Tag("foobar") + tag, err := r.Tag("foobar") + c.Assert(err, IsNil) + + obj, err := r.TagObject(tag.Hash()) c.Assert(err, IsNil) - c.Assert(obj, NotNil) // Assert the new canonicalized message. c.Assert(obj.Message, Equals, "foo bar baz qux\n\nsome message here\n") @@ -1505,9 +1510,8 @@ func (s *RepositorySuite) TestTagLightweight(c *C) { expected := plumbing.NewHash("f7b877701fbf855b44c0a9e86f3fdce2c298b07f") - tag, obj, err := r.Tag("lightweight-tag") + tag, err := r.Tag("lightweight-tag") c.Assert(err, IsNil) - c.Assert(obj, IsNil) actual := tag.Hash() c.Assert(expected, Equals, actual) @@ -1522,34 +1526,11 @@ func (s *RepositorySuite) TestTagLightweightMissingTag(c *C) { err := r.clone(context.Background(), &CloneOptions{URL: url}) c.Assert(err, IsNil) - tag, obj, err := r.Tag("lightweight-tag-tag") + tag, err := r.Tag("lightweight-tag-tag") c.Assert(tag, IsNil) - c.Assert(obj, IsNil) c.Assert(err, Equals, ErrTagNotFound) } -func (s *RepositorySuite) TestTagAnnotated(c *C) { - url := s.GetLocalRepositoryURL( - fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), - ) - - r, _ := Init(memory.NewStorage(), nil) - err := r.clone(context.Background(), &CloneOptions{URL: url}) - c.Assert(err, IsNil) - - tag, obj, err := r.Tag("annotated-tag") - c.Assert(err, IsNil) - c.Assert(obj, NotNil) - - expectedHash := plumbing.NewHash("b742a2a9fa0afcfa9a6fad080980fbc26b007c69") - expectedTarget := plumbing.NewHash("f7b877701fbf855b44c0a9e86f3fdce2c298b07f") - actualHash := tag.Hash() - c.Assert(expectedHash, Equals, actualHash) - c.Assert(obj.Hash, Equals, expectedHash) - c.Assert(obj.Type(), Equals, plumbing.TagObject) - c.Assert(obj.Target, Equals, expectedTarget) -} - func (s *RepositorySuite) TestDeleteTag(c *C) { url := s.GetLocalRepositoryURL( fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), @@ -1562,7 +1543,7 @@ func (s *RepositorySuite) TestDeleteTag(c *C) { err = r.DeleteTag("lightweight-tag") c.Assert(err, IsNil) - _, _, err = r.Tag("lightweight-tag") + _, err = r.Tag("lightweight-tag") c.Assert(err, Equals, ErrTagNotFound) } From 6b3f46b3da8924c058abd7159b8c1212b7c78d07 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 7 Sep 2018 07:32:12 -0700 Subject: [PATCH 08/13] git: s/fetch/returns/ on Tag function doc This is to avoid any ambiguity with the act of "fetching" in git in general. Signed-off-by: Chris Marchesi --- repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repository.go b/repository.go index 68cc5cc28..67bc96a89 100644 --- a/repository.go +++ b/repository.go @@ -581,7 +581,7 @@ func (r *Repository) buildTagSignature(tag *object.Tag, signKey *openpgp.Entity) return b.String(), nil } -// Tag fetches a tag from the repository. +// Tag returns a tag from the repository. // // If you want to check to see if the tag is an annotated tag, you can call // TagObject on the hash of the reference in ForEach: From 1000bc0ef82a87049c6f01cebacd7aa9d06824c6 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 7 Sep 2018 08:55:05 -0700 Subject: [PATCH 09/13] git: Add Tag objects to the list of supported objects for walking This is necessary to support pruning on Tag objects. Signed-off-by: Chris Marchesi --- object_walker.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/object_walker.go b/object_walker.go index 4cbbcca65..f8b19cdb0 100644 --- a/object_walker.go +++ b/object_walker.go @@ -94,6 +94,8 @@ func (p *objectWalker) walkObjectTree(hash plumbing.Hash) error { return err } } + case *object.Tag: + return p.walkObjectTree(obj.Target) default: // Error out on unhandled object types. return fmt.Errorf("Unknown object %X %s %T\n", obj.ID(), obj.Type(), obj) From b19b3b84745ea2e2af13a859ff7943c6de20fe4e Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 7 Sep 2018 08:55:43 -0700 Subject: [PATCH 10/13] git: Don't touch tag objects orphaned by tag deletion Deleting a tag ref for an annotated tag in normal git behavior does not delete the tag object right away. This is handled by the normal GC process. Signed-off-by: Chris Marchesi --- repository.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/repository.go b/repository.go index 67bc96a89..096ec1321 100644 --- a/repository.go +++ b/repository.go @@ -617,26 +617,12 @@ func (r *Repository) Tag(name string) (*plumbing.Reference, error) { // DeleteTag deletes a tag from the repository. func (r *Repository) DeleteTag(name string) error { - ref, err := r.Tag(name) + _, err := r.Tag(name) if err != nil { return err } - obj, err := r.TagObject(ref.Hash()) - if err != nil && err != plumbing.ErrObjectNotFound { - return err - } - - if err = r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name))); err != nil { - return err - } - - // Delete the tag object if this was an annotated tag. - if obj != nil { - return r.DeleteObject(obj.Hash) - } - - return nil + return r.Storer.RemoveReference(plumbing.ReferenceName(path.Join("refs", "tags", name))) } func (r *Repository) resolveToCommitHash(h plumbing.Hash) (plumbing.Hash, error) { From 9cc654ccb69324c7ab9cd67d8a92f9641b3f77b4 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 7 Sep 2018 08:57:11 -0700 Subject: [PATCH 11/13] git: Add some tests for annotated tag deletion Added a couple of tests for annotated tag deletion: * The first one is a general test and should work regardless of the fixture used - the tag object could possibly be packed, so we do a prune *and* a repack operation before testing to see if the object was GCed correctly. * The second one actually creates the tag to be deleted, so that the tag object gets created as a loose, unpacked object. This is so we can effectively test that purning unpacked objects is now working 100% correctly (this was failing before because tag objects were not supported for walking). Signed-off-by: Chris Marchesi --- repository_test.go | 113 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/repository_test.go b/repository_test.go index 0415cc41a..88ad7154e 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1560,6 +1560,113 @@ func (s *RepositorySuite) TestDeleteTagMissingTag(c *C) { c.Assert(err, Equals, ErrTagNotFound) } +func (s *RepositorySuite) TestDeleteTagAnnotated(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + dir, err := ioutil.TempDir("", "go-git-test-deletetag-annotated") + c.Assert(err, IsNil) + + defer os.RemoveAll(dir) // clean up + + fss, err := filesystem.NewStorage(osfs.New(dir)) + c.Assert(err, IsNil) + + r, _ := Init(fss, nil) + err = r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + ref, err := r.Tag("annotated-tag") + c.Assert(ref, NotNil) + c.Assert(err, IsNil) + + obj, err := r.TagObject(ref.Hash()) + c.Assert(obj, NotNil) + c.Assert(err, IsNil) + + err = r.DeleteTag("annotated-tag") + c.Assert(err, IsNil) + + _, err = r.Tag("annotated-tag") + c.Assert(err, Equals, ErrTagNotFound) + + // Run a prune (and repack, to ensure that we are GCing everything regardless + // of the fixture in use) and try to get the tag object again. + // + // The repo needs to be re-opened after the repack. + err = r.Prune(PruneOptions{Handler: r.DeleteObject}) + c.Assert(err, IsNil) + + err = r.RepackObjects(&RepackConfig{}) + c.Assert(err, IsNil) + + r, err = PlainOpen(dir) + c.Assert(r, NotNil) + c.Assert(err, IsNil) + + // Now check to see if the GC was effective in removing the tag object. + obj, err = r.TagObject(ref.Hash()) + c.Assert(obj, IsNil) + c.Assert(err, Equals, plumbing.ErrObjectNotFound) +} + +func (s *RepositorySuite) TestDeleteTagAnnotatedUnpacked(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/tags.git").One(), + ) + + dir, err := ioutil.TempDir("", "go-git-test-deletetag-annotated-unpacked") + c.Assert(err, IsNil) + + defer os.RemoveAll(dir) // clean up + + fss, err := filesystem.NewStorage(osfs.New(dir)) + c.Assert(err, IsNil) + + r, _ := Init(fss, nil) + err = r.clone(context.Background(), &CloneOptions{URL: url}) + c.Assert(err, IsNil) + + // Create a tag for the deletion test. This ensures that the ultimate loose + // object will be unpacked (as we aren't doing anything that should pack it), + // so that we can effectively test that a prune deletes it, without having to + // resort to a repack. + h, err := r.Head() + c.Assert(err, IsNil) + + expectedHash := h.Hash() + + ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + Tagger: defaultSignature(), + Message: "foo bar baz qux", + }) + c.Assert(err, IsNil) + + tag, err := r.Tag("foobar") + c.Assert(err, IsNil) + + obj, err := r.TagObject(tag.Hash()) + c.Assert(obj, NotNil) + c.Assert(err, IsNil) + + err = r.DeleteTag("foobar") + c.Assert(err, IsNil) + + _, err = r.Tag("foobar") + c.Assert(err, Equals, ErrTagNotFound) + + // As mentioned, only run a prune. We are not testing for packed objects + // here. + err = r.Prune(PruneOptions{Handler: r.DeleteObject}) + c.Assert(err, IsNil) + + // Now check to see if the GC was effective in removing the tag object. + obj, err = r.TagObject(ref.Hash()) + c.Assert(obj, IsNil) + c.Assert(err, Equals, plumbing.ErrObjectNotFound) +} + func (s *RepositorySuite) TestBranches(c *C) { f := fixtures.ByURL("https://github.com/git-fixtures/root-references.git").One() sto, err := filesystem.NewStorage(f.DotGit()) @@ -1833,9 +1940,9 @@ func (s *RepositorySuite) TestResolveRevisionWithErrors(c *C) { c.Assert(err, IsNil) datas := map[string]string{ - "efs/heads/master~": "reference not found", - "HEAD^3": `Revision invalid : "3" found must be 0, 1 or 2 after "^"`, - "HEAD^{/whatever}": `No commit message match regexp : "whatever"`, + "efs/heads/master~": "reference not found", + "HEAD^3": `Revision invalid : "3" found must be 0, 1 or 2 after "^"`, + "HEAD^{/whatever}": `No commit message match regexp : "whatever"`, "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83": "reference not found", "918c48b83bd081e863dbe1b80f8998f058cd8294": `refname "918c48b83bd081e863dbe1b80f8998f058cd8294" is ambiguous`, } From f8adfff71d844df7efa1367b7958e8f26411aaf9 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 7 Sep 2018 10:11:44 -0700 Subject: [PATCH 12/13] git: s/TagObjectOptions/CreateTagOptions/ Just renaming the TagObjectOptions type to CreateTagOptions so that it's consistent with the other option types. Signed-off-by: Chris Marchesi --- options.go | 6 +++--- repository.go | 4 ++-- repository_test.go | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/options.go b/options.go index 856bd5e24..b5727703c 100644 --- a/options.go +++ b/options.go @@ -385,8 +385,8 @@ var ( ErrMissingMessage = errors.New("message field is required") ) -// TagObjectOptions describes how a tag object should be created. -type TagObjectOptions struct { +// CreateTagOptions describes how a tag object should be created. +type CreateTagOptions struct { // Tagger defines the signature of the tag creator. Tagger *object.Signature // Message defines the annotation of the tag. It is canonicalized during @@ -399,7 +399,7 @@ type TagObjectOptions struct { } // Validate validates the fields and sets the default values. -func (o *TagObjectOptions) Validate(r *Repository, hash plumbing.Hash) error { +func (o *CreateTagOptions) Validate(r *Repository, hash plumbing.Hash) error { if o.Tagger == nil { return ErrMissingTagger } diff --git a/repository.go b/repository.go index 096ec1321..a132c39f8 100644 --- a/repository.go +++ b/repository.go @@ -494,7 +494,7 @@ func (r *Repository) DeleteBranch(name string) error { // CreateTag creates a tag. If opts is included, the tag is an annotated tag, // otherwise a lightweight tag is created. -func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *TagObjectOptions) (*plumbing.Reference, error) { +func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *CreateTagOptions) (*plumbing.Reference, error) { rname := plumbing.ReferenceName(path.Join("refs", "tags", name)) _, err := r.Storer.Reference(rname) @@ -527,7 +527,7 @@ func (r *Repository) CreateTag(name string, hash plumbing.Hash, opts *TagObjectO return ref, nil } -func (r *Repository) createTagObject(name string, hash plumbing.Hash, opts *TagObjectOptions) (plumbing.Hash, error) { +func (r *Repository) createTagObject(name string, hash plumbing.Hash, opts *CreateTagOptions) (plumbing.Hash, error) { if err := opts.Validate(r, hash); err != nil { return plumbing.ZeroHash, err } diff --git a/repository_test.go b/repository_test.go index 88ad7154e..89911c0da 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1331,7 +1331,7 @@ func (s *RepositorySuite) TestCreateTagAnnotated(c *C) { expectedHash := h.Hash() - ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + ref, err := r.CreateTag("foobar", expectedHash, &CreateTagOptions{ Tagger: defaultSignature(), Message: "foo bar baz qux", }) @@ -1363,13 +1363,13 @@ func (s *RepositorySuite) TestCreateTagAnnotatedBadOpts(c *C) { expectedHash := h.Hash() - ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + ref, err := r.CreateTag("foobar", expectedHash, &CreateTagOptions{ Message: "foo bar baz qux", }) c.Assert(ref, IsNil) c.Assert(err, Equals, ErrMissingTagger) - ref, err = r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + ref, err = r.CreateTag("foobar", expectedHash, &CreateTagOptions{ Tagger: defaultSignature(), }) c.Assert(ref, IsNil) @@ -1385,7 +1385,7 @@ func (s *RepositorySuite) TestCreateTagAnnotatedBadHash(c *C) { err := r.clone(context.Background(), &CloneOptions{URL: url}) c.Assert(err, IsNil) - ref, err := r.CreateTag("foobar", plumbing.ZeroHash, &TagObjectOptions{ + ref, err := r.CreateTag("foobar", plumbing.ZeroHash, &CreateTagOptions{ Tagger: defaultSignature(), Message: "foo bar baz qux", }) @@ -1406,7 +1406,7 @@ func (s *RepositorySuite) TestCreateTagSigned(c *C) { c.Assert(err, IsNil) key := commitSignKey(c, true) - _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ + _, err = r.CreateTag("foobar", h.Hash(), &CreateTagOptions{ Tagger: defaultSignature(), Message: "foo bar baz qux", SignKey: key, @@ -1447,7 +1447,7 @@ func (s *RepositorySuite) TestCreateTagSignedBadKey(c *C) { c.Assert(err, IsNil) key := commitSignKey(c, false) - _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ + _, err = r.CreateTag("foobar", h.Hash(), &CreateTagOptions{ Tagger: defaultSignature(), Message: "foo bar baz qux", SignKey: key, @@ -1468,7 +1468,7 @@ func (s *RepositorySuite) TestCreateTagCanonicalize(c *C) { c.Assert(err, IsNil) key := commitSignKey(c, true) - _, err = r.CreateTag("foobar", h.Hash(), &TagObjectOptions{ + _, err = r.CreateTag("foobar", h.Hash(), &CreateTagOptions{ Tagger: defaultSignature(), Message: "\n\nfoo bar baz qux\n\nsome message here", SignKey: key, @@ -1637,7 +1637,7 @@ func (s *RepositorySuite) TestDeleteTagAnnotatedUnpacked(c *C) { expectedHash := h.Hash() - ref, err := r.CreateTag("foobar", expectedHash, &TagObjectOptions{ + ref, err := r.CreateTag("foobar", expectedHash, &CreateTagOptions{ Tagger: defaultSignature(), Message: "foo bar baz qux", }) From 9ce4eeab99708a2ac35124585103f9e4a04126df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Mon, 10 Sep 2018 12:36:46 +0200 Subject: [PATCH 13/13] repository: fix test for new Storage constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Máximo Cuadros --- repository_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/repository_test.go b/repository_test.go index e48f42d3c..2710d9d0d 100644 --- a/repository_test.go +++ b/repository_test.go @@ -1568,8 +1568,7 @@ func (s *RepositorySuite) TestDeleteTagAnnotated(c *C) { defer os.RemoveAll(dir) // clean up - fss, err := filesystem.NewStorage(osfs.New(dir)) - c.Assert(err, IsNil) + fss := filesystem.NewStorage(osfs.New(dir), cache.NewObjectLRUDefault()) r, _ := Init(fss, nil) err = r.clone(context.Background(), &CloneOptions{URL: url}) @@ -1619,8 +1618,7 @@ func (s *RepositorySuite) TestDeleteTagAnnotatedUnpacked(c *C) { defer os.RemoveAll(dir) // clean up - fss, err := filesystem.NewStorage(osfs.New(dir)) - c.Assert(err, IsNil) + fss := filesystem.NewStorage(osfs.New(dir), cache.NewObjectLRUDefault()) r, _ := Init(fss, nil) err = r.clone(context.Background(), &CloneOptions{URL: url}) @@ -1936,9 +1934,9 @@ func (s *RepositorySuite) TestResolveRevisionWithErrors(c *C) { c.Assert(err, IsNil) datas := map[string]string{ - "efs/heads/master~": "reference not found", - "HEAD^3": `Revision invalid : "3" found must be 0, 1 or 2 after "^"`, - "HEAD^{/whatever}": `No commit message match regexp : "whatever"`, + "efs/heads/master~": "reference not found", + "HEAD^3": `Revision invalid : "3" found must be 0, 1 or 2 after "^"`, + "HEAD^{/whatever}": `No commit message match regexp : "whatever"`, "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83": "reference not found", "918c48b83bd081e863dbe1b80f8998f058cd8294": `refname "918c48b83bd081e863dbe1b80f8998f058cd8294" is ambiguous`, }