Skip to content

Commit

Permalink
git: add support for lightweight tags
Browse files Browse the repository at this point in the history
Add support for lightweight tags by checking the presence of a tag
reference and a tag object. Modify the cloning logic to always attach a
tag object to a commit object if checking out via a tag.

Signed-off-by: Sanskar Jaiswal <[email protected]>
  • Loading branch information
aryan9600 committed Aug 18, 2023
1 parent 009bfcc commit 8fe69f8
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 27 deletions.
33 changes: 24 additions & 9 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ type Commit struct {
Encoded []byte
// Message is the commit message, containing arbitrary text.
Message string
// ReferencingTag is the parent tag, that points to this commit.
ReferencingTag *AnnotatedTag
// ReferencingTag is the tag that points to this commit.
ReferencingTag *Tag
}

// String returns a string representation of the Commit, composed
Expand All @@ -115,8 +115,8 @@ func (c *Commit) AbsoluteReference() string {

// Verify the Signature of the commit with the given key rings.
// It returns the fingerprint of the key the signature was verified
// with, or an error. It does not verify the signature of the parent
// tag (if present). Users are expected to explicitly verify the parent
// with, or an error. It does not verify the signature of the referencing
// tag (if present). Users are expected to explicitly verify the referencing
// tag's signature using `c.ReferencingTag.Verify()`
func (c *Commit) Verify(keyRings ...string) (string, error) {
fingerprint, err := verifySignature(c.Signature, c.Encoded, keyRings...)
Expand All @@ -136,8 +136,8 @@ func (c *Commit) ShortMessage() string {
return subject
}

// AnnotatedTag represents an annotated Git tag.
type AnnotatedTag struct {
// Tag represents a Git tag.
type Tag struct {
// Hash is the hash of the tag.
Hash Hash
// Name is the name of the tag.
Expand All @@ -155,7 +155,7 @@ type AnnotatedTag struct {
// Verify the Signature of the tag with the given key rings.
// It returns the fingerprint of the key the signature was verified
// with, or an error.
func (t *AnnotatedTag) Verify(keyRings ...string) (string, error) {
func (t *Tag) Verify(keyRings ...string) (string, error) {
fingerprint, err := verifySignature(t.Signature, t.Encoded, keyRings...)
if err != nil {
return "", fmt.Errorf("unable to verify Git tag: %w", err)
Expand All @@ -164,8 +164,13 @@ func (t *AnnotatedTag) Verify(keyRings ...string) (string, error) {
}

// String returns a short string representation of the tag in the format
// of <name@hash>, for eg: <1.0.0@a0c14dc8580a23f79bc654faa79c4f62b46c2c22>
func (t *AnnotatedTag) String() string {
// of <name@hash>, for eg: "1.0.0@a0c14dc8580a23f79bc654faa79c4f62b46c2c22"
// If the tag is lightweight, it won't have a hash, so it'll simply return
// the tag name, i.e. "1.0.0".
func (t *Tag) String() string {
if len(t.Hash) == 0 {
return t.Name
}
return fmt.Sprintf("%s@%s", t.Name, t.Hash.String())
}

Expand Down Expand Up @@ -195,6 +200,16 @@ func IsConcreteCommit(c Commit) bool {
return false
}

// IsAnnotatedTag returns true if the provided tag is annotated.
func IsAnnotatedTag(t Tag) bool {
return len(t.Encoded) > 0
}

// IsSignedTag returns true if the provided tag has a signature.
func IsSignedTag(t Tag) bool {
return t.Signature != ""
}

func verifySignature(sig string, payload []byte, keyRings ...string) (string, error) {
if sig == "" {
return "", fmt.Errorf("unable to verify payload as the provided signature is empty")
Expand Down
12 changes: 7 additions & 5 deletions git/gogit/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,9 +535,11 @@ func buildSignature(s object.Signature) git.Signature {
}
}

func buildTag(t *object.Tag) (*git.AnnotatedTag, error) {
func buildTag(t *object.Tag, ref plumbing.ReferenceName) (*git.Tag, error) {
if t == nil {
return nil, fmt.Errorf("unable to contruct tag: no object")
return &git.Tag{
Name: ref.Short(),
}, nil
}

encoded := &plumbing.MemoryObject{}
Expand All @@ -553,7 +555,7 @@ func buildTag(t *object.Tag) (*git.AnnotatedTag, error) {
return nil, fmt.Errorf("unable to read encoded tag '%s': %w", t.Name, err)
}

return &git.AnnotatedTag{
return &git.Tag{
Hash: []byte(t.Hash.String()),
Name: t.Name,
Author: buildSignature(t.Tagger),
Expand Down Expand Up @@ -591,8 +593,8 @@ func buildCommitWithRef(c *object.Commit, t *object.Tag, ref plumbing.ReferenceN
Message: c.Message,
}

if t != nil {
tt, err := buildTag(t)
if ref.IsTag() {
tt, err := buildTag(t, ref)
if err != nil {
return nil, err
}
Expand Down
30 changes: 17 additions & 13 deletions git/gogit/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,22 +290,24 @@ func TestClone_cloneTag(t *testing.T) {
return
}

// Check if commit has a parent if the tag was annotated.
for _, tagInRepo := range tt.tagsInRepo {
if tagInRepo.annotated {
g.Expect(cc.ReferencingTag).ToNot(BeNil())
g.Expect(cc.ReferencingTag.Message).To(Equal(fmt.Sprintf("Annotated tag for: %s\n", tagInRepo.name)))
} else {
g.Expect(cc.ReferencingTag).To(BeNil())
}
}

// Check successful checkout results.
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectConcreteCommit))
targetTagHash := tagCommits[tt.checkoutTag]
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "@" + git.HashTypeSHA1 + ":" + targetTagHash))

if tt.expectConcreteCommit {
g.Expect(cc.ReferencingTag).ToNot(BeNil())
for _, tagInRepo := range tt.tagsInRepo {
if tagInRepo.annotated {
g.Expect(git.IsAnnotatedTag(*cc.ReferencingTag)).To(BeTrue())
g.Expect(cc.ReferencingTag.Message).To(Equal(fmt.Sprintf("Annotated tag for: %s\n", tagInRepo.name)))
} else {
g.Expect(git.IsAnnotatedTag(*cc.ReferencingTag)).To(BeFalse())
}
}
}

// Check file content only when there's an actual checkout.
if tt.lastRevTag != tt.checkoutTag {
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
Expand Down Expand Up @@ -505,11 +507,12 @@ func TestClone_cloneSemVer(t *testing.T) {
g.Expect(cc.String()).To(Equal(tt.expectTag + "@" + git.HashTypeSHA1 + ":" + refs[tt.expectTag]))
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag))
g.Expect(cc.ReferencingTag).ToNot(BeNil())
if tt.annotated {
g.Expect(cc.ReferencingTag).ToNot(BeNil())
g.Expect(git.IsAnnotatedTag(*cc.ReferencingTag)).To(BeTrue())
g.Expect(cc.ReferencingTag.Message).To(Equal(fmt.Sprintf("Annotated tag for: %s\n", tt.expectTag)))
} else {
g.Expect(cc.ReferencingTag).To(BeNil())
g.Expect(git.IsAnnotatedTag(*cc.ReferencingTag)).To(BeFalse())
}
})
}
Expand Down Expand Up @@ -653,9 +656,10 @@ func TestClone_cloneRefName(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc.AbsoluteReference()).To(Equal(tt.refName + "@" + git.HashTypeSHA1 + ":" + tt.expectedCommit))
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectedConcreteCommit))
if strings.Contains(tt.refName, "tags") && !strings.HasSuffix(tt.refName, tagDereferenceSuffix) {
if tt.expectedConcreteCommit && strings.Contains(tt.refName, "tags") && !strings.HasSuffix(tt.refName, tagDereferenceSuffix) {
g.Expect(cc.ReferencingTag).ToNot(BeNil())
g.Expect(cc.ReferencingTag.Message).To(ContainSubstring("Annotated tag for"))
g.Expect(git.IsAnnotatedTag(*cc.ReferencingTag)).To(BeTrue())
}

for k, v := range tt.filesCreated {
Expand Down

0 comments on commit 8fe69f8

Please sign in to comment.