aboutsummaryrefslogtreecommitdiff
path: root/object/tag
diff options
context:
space:
mode:
Diffstat (limited to 'object/tag')
-rw-r--r--object/tag/parse.go89
-rw-r--r--object/tag/parse_test.go47
-rw-r--r--object/tag/serialize.go68
-rw-r--r--object/tag/serialize_test.go35
-rw-r--r--object/tag/tag.go17
-rw-r--r--object/tag/type.go10
6 files changed, 266 insertions, 0 deletions
diff --git a/object/tag/parse.go b/object/tag/parse.go
new file mode 100644
index 00000000..f24d5965
--- /dev/null
+++ b/object/tag/parse.go
@@ -0,0 +1,89 @@
+package tag
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+
+ objectid "codeberg.org/lindenii/furgit/object/id"
+ objectsignature "codeberg.org/lindenii/furgit/object/signature"
+ objecttype "codeberg.org/lindenii/furgit/object/type"
+)
+
+// Parse decodes a tag object body.
+func Parse(body []byte, algo objectid.Algorithm) (*Tag, error) {
+ t := new(Tag)
+ i := 0
+
+ var haveTarget, haveType bool
+
+ for i < len(body) {
+ rel := bytes.IndexByte(body[i:], '\n')
+ if rel < 0 {
+ return nil, errors.New("object: tag: missing newline")
+ }
+
+ line := body[i : i+rel]
+ i += rel + 1
+
+ if len(line) == 0 {
+ break
+ }
+
+ key, value, found := bytes.Cut(line, []byte{' '})
+ if !found {
+ return nil, errors.New("object: tag: malformed header")
+ }
+
+ switch string(key) {
+ case "object":
+ id, err := objectid.ParseHex(algo, string(value))
+ if err != nil {
+ return nil, fmt.Errorf("object: tag: object: %w", err)
+ }
+
+ t.Target = id
+ haveTarget = true
+ case "type":
+ ty, ok := objecttype.ParseName(string(value))
+ if !ok {
+ return nil, errors.New("object: tag: unknown target type")
+ }
+
+ t.TargetType = ty
+ haveType = true
+ case "tag":
+ t.Name = append([]byte(nil), value...)
+ case "tagger":
+ idt, err := objectsignature.Parse(value)
+ if err != nil {
+ return nil, fmt.Errorf("object: tag: tagger: %w", err)
+ }
+
+ t.Tagger = idt
+ case "gpgsig", "gpgsig-sha256":
+ for i < len(body) {
+ nextRel := bytes.IndexByte(body[i:], '\n')
+ if nextRel < 0 {
+ return nil, errors.New("object: tag: unterminated gpgsig")
+ }
+
+ if body[i] != ' ' {
+ break
+ }
+
+ i += nextRel + 1
+ }
+ default:
+ // Ignore unknown headers for now.
+ }
+ }
+
+ if !haveTarget || !haveType {
+ return nil, errors.New("object: tag: missing required headers")
+ }
+
+ t.Message = append([]byte(nil), body[i:]...)
+
+ return t, nil
+}
diff --git a/object/tag/parse_test.go b/object/tag/parse_test.go
new file mode 100644
index 00000000..293350ed
--- /dev/null
+++ b/object/tag/parse_test.go
@@ -0,0 +1,47 @@
+package tag_test
+
+import (
+ "bytes"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+ "codeberg.org/lindenii/furgit/object/tag"
+ objecttype "codeberg.org/lindenii/furgit/object/type"
+)
+
+func TestTagParseFromGit(t *testing.T) {
+ t.Parallel()
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
+ tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message")
+
+ rawBody := testRepo.CatFile(t, "tag", tagID)
+
+ parsed, err := tag.Parse(rawBody, algo)
+ if err != nil {
+ t.Fatalf("ParseTag: %v", err)
+ }
+
+ if parsed.Target != commitID {
+ t.Fatalf("tag target mismatch: got %s want %s", parsed.Target, commitID)
+ }
+
+ if parsed.TargetType != objecttype.TypeCommit {
+ t.Fatalf("tag target type = %v, want %v", parsed.TargetType, objecttype.TypeCommit)
+ }
+
+ if !bytes.Equal(parsed.Name, []byte("v1")) {
+ t.Fatalf("tag name = %q, want %q", parsed.Name, "v1")
+ }
+
+ if parsed.Tagger == nil {
+ t.Fatalf("expected tagger")
+ }
+
+ if !bytes.Contains(parsed.Message, []byte("tag message")) {
+ t.Fatalf("tag message mismatch: %q", parsed.Message)
+ }
+ })
+}
diff --git a/object/tag/serialize.go b/object/tag/serialize.go
new file mode 100644
index 00000000..5f712950
--- /dev/null
+++ b/object/tag/serialize.go
@@ -0,0 +1,68 @@
+package tag
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+
+ objectheader "codeberg.org/lindenii/furgit/object/header"
+ objecttype "codeberg.org/lindenii/furgit/object/type"
+)
+
+// SerializeWithoutHeader renders the raw tag body bytes.
+func (tag *Tag) SerializeWithoutHeader() ([]byte, error) {
+ if tag.Target.Size() == 0 {
+ return nil, errors.New("object: tag: missing target id")
+ }
+
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, "object %s\n", tag.Target.String())
+
+ tyName, ok := objecttype.Name(tag.TargetType)
+ if !ok {
+ return nil, fmt.Errorf("object: tag: invalid target type %d", tag.TargetType)
+ }
+
+ buf.WriteString("type ")
+ buf.WriteString(tyName)
+ buf.WriteByte('\n')
+
+ buf.WriteString("tag ")
+ buf.Write(tag.Name)
+ buf.WriteByte('\n')
+
+ if tag.Tagger != nil {
+ taggerBytes, err := tag.Tagger.Serialize()
+ if err != nil {
+ return nil, err
+ }
+
+ buf.WriteString("tagger ")
+ buf.Write(taggerBytes)
+ buf.WriteByte('\n')
+ }
+
+ buf.WriteByte('\n')
+ buf.Write(tag.Message)
+
+ return buf.Bytes(), nil
+}
+
+// SerializeWithHeader renders the raw object (header + body).
+func (tag *Tag) SerializeWithHeader() ([]byte, error) {
+ body, err := tag.SerializeWithoutHeader()
+ if err != nil {
+ return nil, err
+ }
+
+ header, ok := objectheader.Encode(objecttype.TypeTag, int64(len(body)))
+ if !ok {
+ return nil, errors.New("object: tag: failed to encode object header")
+ }
+
+ raw := make([]byte, len(header)+len(body))
+ copy(raw, header)
+ copy(raw[len(header):], body)
+
+ return raw, nil
+}
diff --git a/object/tag/serialize_test.go b/object/tag/serialize_test.go
new file mode 100644
index 00000000..a1311c39
--- /dev/null
+++ b/object/tag/serialize_test.go
@@ -0,0 +1,35 @@
+package tag_test
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+ "codeberg.org/lindenii/furgit/object/tag"
+)
+
+func TestTagSerialize(t *testing.T) {
+ t.Parallel()
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
+ tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message")
+
+ rawBody := testRepo.CatFile(t, "tag", tagID)
+
+ parsed, err := tag.Parse(rawBody, algo)
+ if err != nil {
+ t.Fatalf("ParseTag: %v", err)
+ }
+
+ rawObj, err := parsed.SerializeWithHeader()
+ if err != nil {
+ t.Fatalf("SerializeWithHeader: %v", err)
+ }
+
+ gotID := algo.Sum(rawObj)
+ if gotID != tagID {
+ t.Fatalf("tag id mismatch: got %s want %s", gotID, tagID)
+ }
+ })
+}
diff --git a/object/tag/tag.go b/object/tag/tag.go
new file mode 100644
index 00000000..4301557e
--- /dev/null
+++ b/object/tag/tag.go
@@ -0,0 +1,17 @@
+// Package tag provides representations, parsers, and serializers for tag objects.
+package tag
+
+import (
+ objectid "codeberg.org/lindenii/furgit/object/id"
+ objectsignature "codeberg.org/lindenii/furgit/object/signature"
+ objecttype "codeberg.org/lindenii/furgit/object/type"
+)
+
+// Tag represents a Git annotated tag object.
+type Tag struct {
+ Target objectid.ObjectID
+ TargetType objecttype.Type
+ Name []byte
+ Tagger *objectsignature.Signature
+ Message []byte
+}
diff --git a/object/tag/type.go b/object/tag/type.go
new file mode 100644
index 00000000..215103ab
--- /dev/null
+++ b/object/tag/type.go
@@ -0,0 +1,10 @@
+package tag
+
+import objecttype "codeberg.org/lindenii/furgit/object/type"
+
+// ObjectType returns TypeTag.
+func (tag *Tag) ObjectType() objecttype.Type {
+ _ = tag
+
+ return objecttype.TypeTag
+}