Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

236 lines
8.9 KiB
Go

// Copyright 2019 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datas
import (
"errors"
"fmt"
"time"
errors2 "gopkg.in/src-d/go-errors.v1"
"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
)
const (
commitMetaNameKey = "name"
commitMetaEmailKey = "email"
commitMetaDescKey = "desc"
commitMetaTimestampKey = "timestamp"
commitMetaUserTSKey = "user_timestamp"
commitMetaVersionKey = "metaversion"
commitMetaSignature = "signature"
commitMetaStName = "metadata"
commitMetaVersion = "1.0"
)
const defaultInitialCommitMessage = "Initialize data repository"
var ErrNameNotConfigured = errors2.NewKind("Aborting commit due to empty %s name. Is your config set?")
var ErrEmailNotConfigured = errors2.NewKind("Aborting commit due to empty %s email. Is your config set?")
var ErrEmptyCommitMessage = errors.New("Aborting commit due to empty commit message.")
// CommitNow is the function used to get the committer time when creating commits. Tests can replace
// this function to produce deterministic commit hashes.
var CommitNow = time.Now
// CommitLoc is the location used when formatting commit timestamps for display.
var CommitLoc = time.Local
// CommitDate holds a timestamp for a commit author or committer. The zero value is "unset" -
// the serialization path replaces it with [CommitNow] via [CommitDate.Or]. Use [CommitDateAt]
// to set an explicit time; the zero time is a valid explicit value.
//
// TODO(elianddb): the flatbuffer schema stores only UTC milliseconds; the timezone is
// dropped on write and reconstructed as the reader's local zone on read. See
// [git commit-tree] for the format that preserves both.
//
// [git commit-tree]: https://git-scm.com/docs/git-commit-tree#_commit_information
type CommitDate struct {
t time.Time
set bool
}
// CommitDateAt returns a [CommitDate] with the given explicit time. The zero time is a valid value.
func CommitDateAt(t time.Time) CommitDate { return CommitDate{t: t, set: true} }
// Or returns the receiver when an explicit time was set; otherwise it returns a [CommitDate] pinned to |t|.
func (d CommitDate) Or(t time.Time) CommitDate {
if d.set {
return d
}
return CommitDateAt(t)
}
// Time returns the captured timestamp. Panics if the receiver is unset; callers must construct
// with [CommitDateAt] or pin via [CommitDate.Or] before reading.
func (d CommitDate) Time() time.Time {
if !d.set {
panic("datas: CommitDate.Time called on unset date; construct with CommitDateAt or pin via Or first")
}
return d.t
}
// NewCommitDate parses |dateStr| and returns a [CommitDateAt]. If |dateStr| is empty or cannot be
// parsed, it returns the unset [CommitDate] zero value and an error.
func NewCommitDate(dateStr string) (commitDate CommitDate, err error) {
if dateStr == "" {
return CommitDate{}, fmt.Errorf("date string cannot be empty for new CommitDate")
}
t, err := dconfig.ParseDate(dateStr)
if err != nil {
return CommitDate{}, err
}
return CommitDateAt(t), nil
}
// CommitIdent holds the name, email, and date for a single author or committer. Use [CommitDateAt]
// for an explicit date or the [CommitDate] zero value to capture the current time at serialization.
type CommitIdent struct {
Name string
Email string
Date CommitDate
}
// CommitMeta holds the author and committer identities, commit message, and optional cryptographic
// signature for a commit.
type CommitMeta struct {
Author CommitIdent
Committer CommitIdent
Description string
Signature string
}
// TimestampMillis returns the committer date as milliseconds since Unix epoch.
func (cm *CommitMeta) TimestampMillis() uint64 {
return uint64(cm.Committer.Date.Time().UnixMilli())
}
// UserTimestampMillis returns the author date as milliseconds since Unix epoch.
func (cm *CommitMeta) UserTimestampMillis() int64 {
return cm.Author.Date.Time().UnixMilli()
}
// NewCommitMeta returns a [CommitMeta] using |name|, |email|, and |desc| as both the author and committer
// identity. Both dates are left unset and are captured when the commit is written to storage.
func NewCommitMeta(name, email, desc string) (*CommitMeta, error) {
identity := CommitIdent{Name: name, Email: email}
return NewCommitMetaWithAuthorCommitter(identity, identity, desc)
}
// NewCommitMetaWithAuthor returns a [CommitMeta] using |name| and |email| as both the author and committer
// identity, with |authorDate| as the explicit author date. The committer date is left unset and is
// captured when the commit is written to storage.
func NewCommitMetaWithAuthor(name, email, desc string, authorDate time.Time) (*CommitMeta, error) {
author := CommitIdent{Name: name, Email: email, Date: CommitDateAt(authorDate)}
committer := CommitIdent{Name: name, Email: email}
return NewCommitMetaWithAuthorCommitter(author, committer, desc)
}
// NewCommitMetaWithAuthorCommitter returns a [CommitMeta] with distinct |author| and |committer| identities.
// When either date is unset, both are resolved to the same timestamp when the commit is written
// to storage.
func NewCommitMetaWithAuthorCommitter(author CommitIdent, committer CommitIdent, description string) (*CommitMeta, error) {
if author.Name == "" {
return nil, ErrNameNotConfigured.New("author")
}
if author.Email == "" {
return nil, ErrEmailNotConfigured.New("author")
}
if description == "" {
return nil, ErrEmptyCommitMessage
}
if committer.Name == "" {
return nil, ErrNameNotConfigured.New("committer")
}
if committer.Email == "" {
return nil, ErrEmailNotConfigured.New("committer")
}
return &CommitMeta{
Author: author,
Committer: committer,
Description: description,
}, nil
}
// FormatTS returns the author date as a [time.RubyDate] string. The timestamp is rounded to the nearest
// second to match MySQL timestamp precision, producing output consistent with the standard Git log format.
func (cm *CommitMeta) FormatTS() string {
return cm.Author.Date.Time().In(CommitLoc).Round(time.Second).Format(time.RubyDate)
}
// String returns a human-readable summary of the commit metadata.
func (cm *CommitMeta) String() string {
return fmt.Sprintf("name: %s, email: %s, timestamp: %s, description: %s", cm.Author.Name, cm.Author.Email, cm.FormatTS(), cm.Description)
}
// CommitMetaGenerator is an interface that generates a sequence of CommitMeta structs, and implements a predicate to check whether
// a proposed commit is acceptable.
type CommitMetaGenerator interface {
Next() (*CommitMeta, error)
IsGoodCommit(*Commit) bool
}
// The default implementation of CommitMetaGenerator, which generates a single commit which is always acceptable.
type simpleCommitMetaGenerator struct {
name, email string
timestamp time.Time
message string
alreadyGenerated bool
}
func (g *simpleCommitMetaGenerator) Next() (*CommitMeta, error) {
if g.alreadyGenerated {
return nil, fmt.Errorf("Called simpleCommitMetaGenerator.Next twice. This should never happen.")
}
g.alreadyGenerated = true
author := CommitIdent{Name: g.name, Email: g.email, Date: CommitDateAt(g.timestamp)}
committer := CommitIdent{Name: g.name, Email: g.email}
return NewCommitMetaWithAuthorCommitter(author, committer, g.message)
}
func (*simpleCommitMetaGenerator) IsGoodCommit(*Commit) bool {
return true
}
func MakeCommitMetaGenerator(name, email string, timestamp time.Time) CommitMetaGenerator {
return &simpleCommitMetaGenerator{name: name, email: email, timestamp: timestamp, message: defaultInitialCommitMessage, alreadyGenerated: false}
}
// signaturePayloadV1 generates the legacy signature payload format that includes only author information.
func signaturePayloadV1(dbName string, meta *CommitMeta, headHash, stagedHash string) string {
return fmt.Sprintf("db: %s\nMessage: %s\nName: %s\nEmail: %s\nDate: %s\nHead: %s\nStaged: %s",
dbName,
meta.Description,
meta.Author.Name,
meta.Author.Email,
meta.Author.Date.Time().String(),
headHash,
stagedHash,
)
}
// SignaturePayloadV2 generates the signature payload including both author and committer information.
// Committer fields are appended after the author fields so V1 is a strict prefix, enabling forward compatibility.
func SignaturePayloadV2(dbName string, meta *CommitMeta, headHash, stagedHash string) string {
return fmt.Sprintf("%s\nCommitterName: %s\nCommitterEmail: %s\nCommitterDate: %s",
signaturePayloadV1(dbName, meta, headHash, stagedHash),
meta.Committer.Name,
meta.Committer.Email,
meta.Committer.Date.Time().String())
}