| 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] |
| } |