Fast implementation of Git in pure Go
1package object
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "strconv"
8 "strings"
9 "time"
10
11 "codeberg.org/lindenii/furgit/internal/intconv"
12)
13
14// Signature represents a Git signature (author/committer/tagger).
15type Signature struct {
16 Name []byte
17 Email []byte
18 WhenUnix int64
19 OffsetMinutes int32
20}
21
22// ParseSignature parses a canonical Git signature line:
23// "Name <email> 123456789 +0000".
24func ParseSignature(line []byte) (*Signature, error) {
25 lt := bytes.IndexByte(line, '<')
26 if lt < 0 {
27 return nil, errors.New("object: signature: missing opening <")
28 }
29
30 gtRel := bytes.IndexByte(line[lt+1:], '>')
31 if gtRel < 0 {
32 return nil, errors.New("object: signature: missing closing >")
33 }
34
35 gt := lt + 1 + gtRel
36
37 nameBytes := append([]byte(nil), bytes.TrimRight(line[:lt], " ")...)
38 emailBytes := append([]byte(nil), line[lt+1:gt]...)
39
40 rest := line[gt+1:]
41 if len(rest) == 0 || rest[0] != ' ' {
42 return nil, errors.New("object: signature: missing timestamp separator")
43 }
44
45 rest = rest[1:]
46
47 before, after, ok := bytes.Cut(rest, []byte{' '})
48 if !ok {
49 return nil, errors.New("object: signature: missing timezone separator")
50 }
51
52 when, err := strconv.ParseInt(string(before), 10, 64)
53 if err != nil {
54 return nil, fmt.Errorf("object: signature: invalid timestamp: %w", err)
55 }
56
57 tz := after
58 if len(tz) < 5 {
59 return nil, errors.New("object: signature: invalid timezone encoding")
60 }
61
62 sign := 1
63
64 switch tz[0] {
65 case '-':
66 sign = -1
67 case '+':
68 default:
69 return nil, errors.New("object: signature: invalid timezone sign")
70 }
71
72 hh, err := strconv.Atoi(string(tz[1:3]))
73 if err != nil {
74 return nil, fmt.Errorf("object: signature: invalid timezone hours: %w", err)
75 }
76
77 mm, err := strconv.Atoi(string(tz[3:5]))
78 if err != nil {
79 return nil, fmt.Errorf("object: signature: invalid timezone minutes: %w", err)
80 }
81
82 if hh < 0 || hh > 23 {
83 return nil, errors.New("object: signature: invalid timezone hours range")
84 }
85
86 if mm < 0 || mm > 59 {
87 return nil, errors.New("object: signature: invalid timezone minutes range")
88 }
89
90 total := int64(hh)*60 + int64(mm)
91
92 offset, err := intconv.Int64ToInt32(total)
93 if err != nil {
94 return nil, errors.New("object: signature: timezone overflow")
95 }
96
97 if sign < 0 {
98 offset = -offset
99 }
100
101 return &Signature{
102 Name: nameBytes,
103 Email: emailBytes,
104 WhenUnix: when,
105 OffsetMinutes: offset,
106 }, nil
107}
108
109// Serialize renders the signature in canonical Git format.
110func (signature Signature) Serialize() ([]byte, error) {
111 var b strings.Builder
112 b.Grow(len(signature.Name) + len(signature.Email) + 32)
113 b.Write(signature.Name)
114 b.WriteString(" <")
115 b.Write(signature.Email)
116 b.WriteString("> ")
117 b.WriteString(strconv.FormatInt(signature.WhenUnix, 10))
118 b.WriteByte(' ')
119
120 offset := signature.OffsetMinutes
121
122 sign := '+'
123 if offset < 0 {
124 sign = '-'
125 offset = -offset
126 }
127
128 hh := offset / 60
129 mm := offset % 60
130 fmt.Fprintf(&b, "%c%02d%02d", sign, hh, mm)
131
132 return []byte(b.String()), nil
133}
134
135// When returns a time.Time with the signature's timezone offset.
136func (signature Signature) When() time.Time {
137 loc := time.FixedZone("git", int(signature.OffsetMinutes)*60)
138
139 return time.Unix(signature.WhenUnix, 0).In(loc)
140}