shithub: furgit

ref: 618004eb3948345acaa5fa78e9ab2ee9bcbf8b7d
dir: /ident.go/

View raw version
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)
}