Fast implementation of Git in pure Go
at master 140 lines 3.2 kB view raw
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}