aboutsummaryrefslogtreecommitdiff
path: root/ident.go
blob: 50676a45c6d1e8c7834ac36db0444097f070cdd6 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package furgit

import (
	"bytes"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

// Ident represents a Git identity (author/committer/tagger).
type Ident struct {
	// Name represents the person's name.
	Name []byte
	// Email represents the person's email.
	Email []byte
	// WhenUnix represents the timestamp as a Unix time.
	// This value is in UTC.
	WhenUnix int64
	// The timezone offset in minutes.
	OffsetMinutes int32
}

// parseIdent parses an identity line from the canonical Git format:
// "Name <email> 123456789 +0000".
func parseIdent(line []byte) (*Ident, error) {
	lt := bytes.IndexByte(line, '<')
	if lt < 0 {
		return nil, errors.New("furgit: ident: missing opening <")
	}
	gtRel := bytes.IndexByte(line[lt+1:], '>')
	if gtRel < 0 {
		return nil, errors.New("furgit: ident: missing closing >")
	}
	gt := lt + 1 + gtRel
	nameBytes := append([]byte(nil), line[:lt]...)
	emailBytes := append([]byte(nil), line[lt+1:gt]...)

	rest := line[gt+1:]
	if len(rest) == 0 || rest[0] != ' ' {
		return nil, errors.New("furgit: ident: missing timestamp separator")
	}
	rest = rest[1:]
	sp := bytes.IndexByte(rest, ' ')
	if sp < 0 {
		return nil, errors.New("furgit: ident: missing timezone separator")
	}
	whenStr := string(rest[:sp])
	when, err := strconv.ParseInt(whenStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("furgit: ident: invalid timestamp: %w", err)
	}

	tz := rest[sp+1:]
	if len(tz) < 5 {
		return nil, errors.New("furgit: ident: invalid timezone encoding")
	}
	sign := 1
	switch tz[0] {
	case '-':
		sign = -1
	case '+':
	default:
		return nil, errors.New("furgit: ident: invalid timezone sign")
	}

	hh, err := strconv.Atoi(string(tz[1:3]))
	if err != nil {
		return nil, fmt.Errorf("furgit: ident: invalid timezone hours: %w", err)
	}
	mm, err := strconv.Atoi(string(tz[3:5]))
	if err != nil {
		return nil, fmt.Errorf("furgit: ident: invalid timezone minutes: %w", err)
	}
	if hh < 0 || hh > 23 {
		return nil, errors.New("furgit: ident: invalid timezone hours range")
	}
	if mm < 0 || mm > 59 {
		return nil, errors.New("furgit: ident: invalid timezone minutes range")
	}
	total := int64(hh)*60 + int64(mm)
	if total > math.MaxInt32 {
		return nil, errors.New("furgit: ident: timezone overflow")
	}
	offset := int32(total)
	if sign < 0 {
		offset = -offset
	}

	return &Ident{
		Name:          nameBytes,
		Email:         emailBytes,
		WhenUnix:      when,
		OffsetMinutes: offset,
	}, nil
}

// Serialize renders an Ident into canonical Git format.
func (ident Ident) Serialize() ([]byte, error) {
	var b strings.Builder
	b.Grow(len(ident.Name) + len(ident.Email) + 32)
	b.Write(ident.Name)
	b.WriteString(" <")
	b.Write(ident.Email)
	b.WriteString("> ")
	b.WriteString(strconv.FormatInt(ident.WhenUnix, 10))
	b.WriteByte(' ')

	offset := ident.OffsetMinutes
	sign := '+'
	if offset < 0 {
		sign = '-'
		offset = -offset
	}
	hh := offset / 60
	mm := offset % 60
	fmt.Fprintf(&b, "%c%02d%02d", sign, hh, mm)
	return []byte(b.String()), nil
}

// When returns the ident's time.Time with the correct timezone.
func (ident Ident) When() time.Time {
	loc := time.FixedZone("git", int(ident.OffsetMinutes)*60)
	return time.Unix(ident.WhenUnix, 0).In(loc)
}