diff options
| author | 2026-02-20 21:20:35 +0800 | |
|---|---|---|
| committer | 2026-02-20 21:51:00 +0800 | |
| commit | b5a545a3d883026d61beac5556fec2a45e9ec3d3 (patch) | |
| tree | 6281f89df21cb4b63a16bbcb057c5f186c40b148 /object/ident.go | |
| parent | testgit: Add test harnesses (diff) | |
| signature | No signature | |
object: Add basic object code
Diffstat (limited to 'object/ident.go')
| -rw-r--r-- | object/ident.go | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/object/ident.go b/object/ident.go new file mode 100644 index 00000000..e601802a --- /dev/null +++ b/object/ident.go @@ -0,0 +1,122 @@ +package object + +import ( + "bytes" + "errors" + "fmt" + "math" + "strconv" + "strings" + "time" +) + +// Ident represents a Git identity (author/committer/tagger). +type Ident struct { + Name []byte + Email []byte + WhenUnix int64 + OffsetMinutes int32 +} + +// ParseIdent parses a canonical Git identity line: +// "Name <email> 123456789 +0000". +func ParseIdent(line []byte) (*Ident, error) { + lt := bytes.IndexByte(line, '<') + if lt < 0 { + return nil, errors.New("object: ident: missing opening <") + } + gtRel := bytes.IndexByte(line[lt+1:], '>') + if gtRel < 0 { + return nil, errors.New("object: ident: missing closing >") + } + gt := lt + 1 + gtRel + + nameBytes := append([]byte(nil), bytes.TrimRight(line[:lt], " ")...) + emailBytes := append([]byte(nil), line[lt+1:gt]...) + + rest := line[gt+1:] + if len(rest) == 0 || rest[0] != ' ' { + return nil, errors.New("object: ident: missing timestamp separator") + } + rest = rest[1:] + sp := bytes.IndexByte(rest, ' ') + if sp < 0 { + return nil, errors.New("object: ident: missing timezone separator") + } + when, err := strconv.ParseInt(string(rest[:sp]), 10, 64) + if err != nil { + return nil, fmt.Errorf("object: ident: invalid timestamp: %w", err) + } + + tz := rest[sp+1:] + if len(tz) < 5 { + return nil, errors.New("object: ident: invalid timezone encoding") + } + sign := 1 + switch tz[0] { + case '-': + sign = -1 + case '+': + default: + return nil, errors.New("object: ident: invalid timezone sign") + } + + hh, err := strconv.Atoi(string(tz[1:3])) + if err != nil { + return nil, fmt.Errorf("object: ident: invalid timezone hours: %w", err) + } + mm, err := strconv.Atoi(string(tz[3:5])) + if err != nil { + return nil, fmt.Errorf("object: ident: invalid timezone minutes: %w", err) + } + if hh < 0 || hh > 23 { + return nil, errors.New("object: ident: invalid timezone hours range") + } + if mm < 0 || mm > 59 { + return nil, errors.New("object: ident: invalid timezone minutes range") + } + total := int64(hh)*60 + int64(mm) + if total > math.MaxInt32 { + return nil, errors.New("object: ident: timezone overflow") + } + + offset := int32(total) + if sign < 0 { + offset = -offset + } + return &Ident{ + Name: nameBytes, + Email: emailBytes, + WhenUnix: when, + OffsetMinutes: offset, + }, nil +} + +// Serialize renders the identity in 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 a time.Time with the identity's timezone offset. +func (ident Ident) When() time.Time { + loc := time.FixedZone("git", int(ident.OffsetMinutes)*60) + return time.Unix(ident.WhenUnix, 0).In(loc) +} |
