blob: f27572e9d9f3ee3ce1afc341999f53a524e9d929 [file] [log] [blame]
package gitstore
import (
"bytes"
"context"
"fmt"
"sort"
"sync"
"time"
"go.skia.org/infra/go/depot_tools"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/vcsinfo"
)
// btVCS implements the vcsinfo.VCS interface based on a BT-backed GitStore.
type btVCS struct {
gitStore GitStore
repo *gitiles.Repo
defaultBranch string
secondaryVCS vcsinfo.VCS
secondaryExtractor depot_tools.DEPSExtractor
branchInfo *BranchPointer
indexCommits []*vcsinfo.IndexCommit
hashes []string
timestamps map[string]time.Time //
detailsCache map[string]*vcsinfo.LongCommit // Details
mutex sync.RWMutex
}
// NewVCS returns an instance of vcsinfo.VCS that is backed by the given GitStore and uses the
// gittiles.Repo to retrieve files. Each instance provides an interface to one branch.
// If defaultBranch is "" all commits in the repository are considered.
// The instances of gitiles.Repo is only used to fetch files.
func NewVCS(gitstore GitStore, defaultBranch string, repo *gitiles.Repo) (vcsinfo.VCS, error) {
ret := &btVCS{
gitStore: gitstore,
repo: repo,
defaultBranch: defaultBranch,
}
if err := ret.Update(context.TODO(), true, false); err != nil {
return nil, err
}
return ret, nil
}
// SetSecondaryRepo allows to add a secondary repository and extractor to this instance.
// It is not included in the constructor since it is currently only used by the Gold ingesters.
func (b *btVCS) SetSecondaryRepo(secVCS vcsinfo.VCS, extractor depot_tools.DEPSExtractor) {
b.secondaryVCS = secVCS
b.secondaryExtractor = extractor
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) Update(ctx context.Context, pull, allBranches bool) error {
// Simulate a pull by fetching the latest head of the target branch.
if pull {
allBranches, err := b.gitStore.GetBranches(ctx)
if err != nil {
return err
}
var ok bool
b.branchInfo, ok = allBranches[b.defaultBranch]
if !ok {
return skerr.Fmt("Unable to find branch %s in BitTable repo %s", b.defaultBranch, (b.gitStore.(*btGitStore)).repoURL)
}
}
// Get all index commits for the current branch.
return b.fetchIndexRange(ctx, 0, b.branchInfo.Index+1)
}
// From implements the vcsinfo.VCS interface
func (b *btVCS) From(start time.Time) []string {
b.mutex.RLock()
defer b.mutex.RUnlock()
// Add a millisecond because we only want commits after the startTime. Timestamps in git are
// only at second level granularity.
found := b.timeRange(start.Add(time.Millisecond), MaxTime)
ret := make([]string, len(found))
for i, c := range found {
ret[i] = c.Hash
}
return ret
}
// Details implements the vcsinfo.VCS interface
func (b *btVCS) Details(ctx context.Context, hash string, includeBranchInfo bool) (*vcsinfo.LongCommit, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.details(ctx, hash, includeBranchInfo)
}
// TODO(stephan): includeBranchInfo currently does nothing. This needs to fixed for the few clients
// that need it.
// details returns all meta data details we care about.
func (b *btVCS) details(ctx context.Context, hash string, includeBranchInfo bool) (*vcsinfo.LongCommit, error) {
commits, err := b.gitStore.Get(ctx, []string{hash})
if err != nil {
return nil, err
}
if len(commits) == 0 {
return nil, skerr.Fmt("Commit %s not found", hash)
}
return commits[0], nil
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) LastNIndex(N int) []*vcsinfo.IndexCommit {
b.mutex.RLock()
defer b.mutex.RUnlock()
if N > len(b.indexCommits) {
N = len(b.indexCommits)
}
ret := make([]*vcsinfo.IndexCommit, 0, N)
return append(ret, b.indexCommits[len(b.indexCommits)-N:]...)
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) Range(begin, end time.Time) []*vcsinfo.IndexCommit {
return b.timeRange(begin, end)
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) IndexOf(ctx context.Context, hash string) (int, error) {
b.mutex.RLock()
defer b.mutex.Unlock()
for _, c := range b.indexCommits {
if c.Hash == hash {
return c.Index, nil
}
}
// If it was not in memory we need to fetch it
details, err := b.gitStore.Get(ctx, []string{hash})
if err != nil {
return 0, err
}
if len(details) == 0 {
return 0, skerr.Fmt("Hash %s does not exist in repository on branch %s", hash, b.defaultBranch)
}
return 0, nil
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) ByIndex(ctx context.Context, N int) (*vcsinfo.LongCommit, error) {
// findFn returns the hash when N is within commits
findFn := func(commits []*vcsinfo.IndexCommit) string {
i := sort.Search(len(commits), func(i int) bool { return commits[i].Index >= N })
return commits[i].Hash
}
var hash string
b.mutex.RLock()
if len(b.indexCommits) > 0 {
firstIdx := b.indexCommits[0].Index
lastIdx := b.indexCommits[len(b.indexCommits)-1].Index
if (N >= firstIdx) && (N <= lastIdx) {
hash = findFn(b.indexCommits)
}
}
b.mutex.RUnlock()
// Fetch the hash
if hash == "" {
return nil, fmt.Errorf("Hash index not found: %d", N)
}
return b.details(ctx, hash, false)
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) GetFile(ctx context.Context, fileName, commitHash string) (string, error) {
var buf bytes.Buffer
if err := b.repo.ReadFileAtRef(fileName, commitHash, &buf); err != nil {
return "", skerr.Fmt("Error reading file %s @ %s via gitiles: %s", fileName, commitHash, err)
}
return buf.String(), nil
}
// Update implements the vcsinfo.VCS interface
func (b *btVCS) ResolveCommit(ctx context.Context, commitHash string) (string, error) {
return "", skerr.Fmt("Not implemented yet")
}
// fetchIndexRange gets in the range [startIndex, endIndex).
func (b *btVCS) fetchIndexRange(ctx context.Context, startIndex, endIndex int) error {
newIC, err := b.gitStore.RangeN(ctx, startIndex, endIndex, b.defaultBranch)
if err != nil {
return err
}
if len(newIC) == 0 {
return nil
}
b.mutex.Lock()
defer b.mutex.Unlock()
b.indexCommits = newIC
return nil
}
func (b *btVCS) timeRangeNG(start time.Time, end time.Time) []*vcsinfo.IndexCommit {
startSec := start.Unix()
endSec := end.Unix()
n := len(b.indexCommits)
startIdx := 0
for ; startIdx < n; startIdx++ {
exp := b.indexCommits[startIdx].Timestamp.Unix() >= startSec
if exp {
break
}
}
endIdx := startIdx
for ; endIdx < n; endIdx++ {
exp := b.indexCommits[endIdx].Timestamp.Unix() >= endSec
if exp {
break
}
}
if endIdx <= startIdx {
return []*vcsinfo.IndexCommit{}
}
return b.indexCommits[startIdx:endIdx]
}
func (b *btVCS) timeRange(start time.Time, end time.Time) []*vcsinfo.IndexCommit {
n := len(b.indexCommits)
startIdx := 0
for ; startIdx < n; startIdx++ {
exp := b.indexCommits[startIdx].Timestamp.After(start) || b.indexCommits[startIdx].Timestamp.Equal(start)
if exp {
break
}
}
endIdx := startIdx
for ; endIdx < n; endIdx++ {
exp := b.indexCommits[endIdx].Timestamp.After(end) || b.indexCommits[endIdx].Timestamp.Equal(end)
if exp {
break
}
}
if endIdx <= startIdx {
return []*vcsinfo.IndexCommit{}
}
return b.indexCommits[startIdx:endIdx]
}