blob: ab5ff66d319fef26f7d058d03d57262921632ddf [file] [log] [blame]
/*
Package mem_git provides convenience functionality for writing test data into a GitStore.
*/
package mem_git
import (
"context"
"crypto/sha1"
"fmt"
"time"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/git/git_common"
"go.skia.org/infra/go/gitstore"
"go.skia.org/infra/go/sktest"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/util"
"go.skia.org/infra/go/vcsinfo"
)
const (
// These are the commit hashes generated by CommitN(2) when run as the
// first call to MemGit.
Commit0 = "407108ae8039ba9dce77e81057721fc7c4dbab87"
Commit1 = "a1dcf4a9669ff77919dfca71901ec98145b0af94"
)
var (
// BaseTime is an arbitrary timestamp used as the time of the first
// commit. Subsequent commits add a fixed duration to the timestamp
// of their parent(s). This keeps the commit hashes predictable.
BaseTime = time.Unix(1571926390, 0).UTC()
)
// FakeCommitAt creates a LongCommit with the given message at the given time,
// belonging to the given branch, and with the given parent commits. Its Index
// increases monotonically with respect to the parents.
func FakeCommitAt(t sktest.TestingT, msg, branch string, ts time.Time, parents ...*vcsinfo.LongCommit) *vcsinfo.LongCommit {
index := 0
var parentHashes []string
if len(parents) > 0 {
parentHashes = make([]string, 0, len(parents))
for _, p := range parents {
if p.Index >= index {
index = p.Index + 1
}
parentHashes = append(parentHashes, p.Hash)
}
}
lc := &vcsinfo.LongCommit{
ShortCommit: &vcsinfo.ShortCommit{
Author: "me@google.com",
Subject: msg,
},
Parents: parentHashes,
Timestamp: ts,
Index: index,
Branches: map[string]bool{
branch: true,
},
}
j := testutils.MarshalJSON(t, lc)
lc.Hash = fmt.Sprintf("%040x", sha1.Sum([]byte(j)))
return lc
}
// FakeCommit creates a LongCommit with the given message, belonging to the
// given branch, and with the given parent commits. Its Timestamp and Index
// increase monotonically with respect to the parents.
func FakeCommit(t sktest.TestingT, msg, branch string, parents ...*vcsinfo.LongCommit) *vcsinfo.LongCommit {
ts := BaseTime
if len(parents) > 0 {
for _, p := range parents {
if !ts.After(p.Timestamp) {
ts = p.Timestamp.Add(time.Minute)
}
}
}
return FakeCommitAt(t, msg, branch, ts, parents...)
}
// New returns a MemGit instance which writes to the given GitStore.
func New(t sktest.TestingT, gs gitstore.GitStore) *MemGit {
return &MemGit{
branch: git.MainBranch,
gs: gs,
t: t,
}
}
// MemGit is a struct used for writing fake commits into a GitStore.
type MemGit struct {
branch string
gs gitstore.GitStore
t sktest.TestingT
updaters []Updater
}
// head returns the current head of the given branch.
func (g *MemGit) head(branch string) string {
branches, err := g.gs.GetBranches(context.Background())
require.NoError(g.t, err)
ptr, ok := branches[branch]
if !ok {
return ""
}
return ptr.Head
}
// makeCommit creates a commit with the given message and parents on the
// currently-active branch.
func (g *MemGit) makeCommit(msg string, ts time.Time, parentHashes []string) *vcsinfo.LongCommit {
ctx := context.Background()
parents := []*vcsinfo.LongCommit{}
if len(parentHashes) > 0 {
var err error
parents, err = g.gs.Get(ctx, parentHashes)
require.NoError(g.t, err)
for _, p := range parents {
require.NotNil(g.t, p)
}
}
var lc *vcsinfo.LongCommit
if util.TimeIsZero(ts) {
lc = FakeCommit(g.t, msg, g.branch, parents...)
} else {
lc = FakeCommitAt(g.t, msg, g.branch, ts, parents...)
}
require.NoError(g.t, g.gs.Put(ctx, []*vcsinfo.LongCommit{lc}))
require.NoError(g.t, g.gs.PutBranches(ctx, map[string]string{
g.branch: lc.Hash,
}))
g.update()
return lc
}
// Commit adds a commit to the GitStore with the given commit message and
// parents, and returns its hash. If no parents are provided, the head of the
// current branch is used as the parent. In either case, the head of the
// current branch is set to be the new commit.
func (g *MemGit) Commit(msg string, parents ...string) string {
return g.CommitAt(msg, time.Time{}, parents...)
}
// CommitAt adds a commit to the GitStore with the given commit message,
// timestamp, and parents, and returns its hash. If no parents are provided,
// the head of the current branch is used as the parent. In either case, the
// head of the current branch is set to be the new commit.
func (g *MemGit) CommitAt(msg string, ts time.Time, parents ...string) string {
if len(parents) == 0 {
head := g.head(g.branch)
if head != "" {
parents = []string{head}
}
}
return g.makeCommit(msg, ts, parents).Hash
}
// CommitN adds N commits to the GitStore on the current branch and returns
// their hashes in reverse chronological order.
func (g *MemGit) CommitN(n int) []string {
hashes := make([]string, 0, n)
for i := 0; i < n; i++ {
hashes = append(hashes, g.Commit(fmt.Sprintf("Fake #%d/%d", i, n)))
}
return util.Reverse(hashes)
}
// CheckoutBranch switches to the given branch.
func (g *MemGit) CheckoutBranch(branch string) {
branches, err := g.gs.GetBranches(context.Background())
require.NoError(g.t, err)
ptr, ok := branches[branch]
require.True(g.t, ok)
require.NotNil(g.t, ptr)
require.NotEqual(g.t, "", ptr.Head)
g.branch = branch
}
// NewBranch creates the given branch at the given commit hash and switches to it.
func (g *MemGit) NewBranch(branch, head string) {
ctx := context.Background()
branches, err := g.gs.GetBranches(ctx)
require.NoError(g.t, err)
_, ok := branches[branch]
require.False(g.t, ok, "Already have %s", branch)
require.NoError(g.t, g.gs.PutBranches(ctx, map[string]string{
branch: head,
}))
g.branch = branch
g.update()
}
// Merge creates a new commit which merges the given branch into the currently
// active branch.
func (g *MemGit) Merge(branch string) string {
return g.makeCommit(fmt.Sprintf("Merge %s", branch), time.Time{}, []string{g.head(g.branch), g.head(branch)}).Hash
}
// Updater is an interface used for automatically receiving updates when the
// MemGit changes.
type Updater interface {
// Update indicates that the MemGit has changed. The Context passed to
// Update is likely to be a placeholder or empty.
Update(ctx context.Context) error
}
// AddUpdater adds the given Updater to the MemGit. Subsequent mutations to the
// MemGit will result in a call to Updater.Update().
func (g *MemGit) AddUpdater(u Updater) {
g.updaters = append(g.updaters, u)
}
// update all of the registered Updaters.
func (g *MemGit) update() {
for _, u := range g.updaters {
require.NoError(g.t, u.Update(context.Background()))
}
}
// FillWithBranchingHistory adds commits to the MemGit.
//
// The repo layout looks like this:
//
// older newer
// c0--c1------c3--c4--
//
// \-c2-----/
//
// Returns the commit hashes in order from c0-c4. This is analogous to
// git_builder.FillWithBranchingHistory.
func FillWithBranchingHistory(mg *MemGit) []string {
c0 := mg.Commit("c0")
c1 := mg.Commit("c1")
mg.NewBranch("branch2", c1)
c2 := mg.Commit("c2")
mg.CheckoutBranch(git_common.MainBranch)
c3 := mg.Commit("c3")
c4 := mg.Merge("branch2")
return []string{c0, c1, c2, c3, c4}
}