| package commit_cache |
| |
| import ( |
| "encoding/gob" |
| "fmt" |
| "os" |
| "sort" |
| "sync" |
| "time" |
| |
| "github.com/skia-dev/glog" |
| |
| "go.skia.org/infra/go/buildbot" |
| "go.skia.org/infra/go/gitinfo" |
| "go.skia.org/infra/go/timer" |
| "go.skia.org/infra/go/util" |
| "go.skia.org/infra/go/vcsinfo" |
| ) |
| |
| /* |
| Utilities for caching commit data. |
| */ |
| |
| func init() { |
| gob.Register([]interface{}{}) |
| gob.Register(map[string]interface{}{}) |
| } |
| |
| // CommitCache is a struct used for caching commit data. Stores ALL commits in |
| // the repository. |
| type CommitCache struct { |
| BranchHeads []*gitinfo.GitBranch |
| cacheFile string |
| Commits []*vcsinfo.LongCommit |
| Comments map[string][]*buildbot.CommitComment |
| db buildbot.DB |
| mutex sync.RWMutex |
| repo *gitinfo.GitInfo |
| requestSize int |
| } |
| |
| // fromFile attempts to load the CommitCache from the given file. |
| func fromFile(cacheFile string) (*CommitCache, error) { |
| defer timer.New("commit_cache.fromFile()").Stop() |
| c := CommitCache{} |
| if _, err := os.Stat(cacheFile); err != nil { |
| return nil, fmt.Errorf("Could not stat cache file: %v", err) |
| } |
| f, err := os.Open(cacheFile) |
| if err != nil { |
| return nil, fmt.Errorf("Failed to open cache file %s: %v", cacheFile, err) |
| } |
| defer util.Close(f) |
| if err := gob.NewDecoder(f).Decode(&c); err != nil { |
| return nil, fmt.Errorf("Failed to read cache file %s: %v", cacheFile, err) |
| } |
| return &c, nil |
| } |
| |
| // toFile saves the CommitCache to a file. |
| func (c *CommitCache) toFile() error { |
| defer timer.New("commit_cache.toFile()").Stop() |
| f, err := os.Create(c.cacheFile) |
| if err != nil { |
| return err |
| } |
| defer util.Close(f) |
| if err := gob.NewEncoder(f).Encode(c); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // New creates and returns a new CommitCache which watches the given repo. |
| // The initial update will load ALL commits from the repository, so expect |
| // this to be slow. |
| func New(repo *gitinfo.GitInfo, cacheFile string, requestSize int, db buildbot.DB) (*CommitCache, error) { |
| defer timer.New("commit_cache.New()").Stop() |
| c, err := fromFile(cacheFile) |
| if err != nil { |
| glog.Warningf("Failed to read commit cache from file; starting from scratch. Error: %v", err) |
| c = &CommitCache{} |
| } |
| c.cacheFile = cacheFile |
| c.db = db |
| c.repo = repo |
| c.requestSize = requestSize |
| |
| // Update the cache. |
| if err := c.update(); err != nil { |
| return nil, err |
| } |
| |
| // Update in a loop. |
| go func() { |
| for _ = range time.Tick(time.Minute) { |
| if err := c.update(); err != nil { |
| glog.Errorf("Failed to update commit cache: %v", err) |
| } |
| } |
| }() |
| return c, nil |
| } |
| |
| // NumCommits returns the number of commits in the cache. |
| func (c *CommitCache) NumCommits() int { |
| c.mutex.RLock() |
| defer c.mutex.RUnlock() |
| return len(c.Commits) |
| } |
| |
| // Get returns the commit at the given index. |
| func (c *CommitCache) Get(idx int) (*vcsinfo.LongCommit, error) { |
| c.mutex.RLock() |
| defer c.mutex.RUnlock() |
| if idx < 0 || idx >= len(c.Commits) { |
| return nil, fmt.Errorf("Index out of range: %d not in [%d, %d)", idx, 0, len(c.Commits)) |
| } |
| return c.Commits[idx], nil |
| } |
| |
| // Slice returns a slice of LongCommits from the cache. |
| func (c *CommitCache) Slice(startIdx, endIdx int) ([]*vcsinfo.LongCommit, error) { |
| c.mutex.RLock() |
| defer c.mutex.RUnlock() |
| if startIdx < 0 || startIdx > endIdx || endIdx > len(c.Commits) { |
| return nil, fmt.Errorf("Index out of range: (%d < 0 || %d > %d || %d > %d)", startIdx, startIdx, endIdx, endIdx, len(c.Commits)) |
| } |
| return c.Commits[startIdx:endIdx], nil |
| } |
| |
| // RevisionsInDateRange returns a slice of revisions that happen within the bounded time. |
| func (c *CommitCache) RevisionsInDateRange(start, end time.Time) ([]string, error) { |
| c.mutex.RLock() |
| defer c.mutex.RUnlock() |
| |
| if len(c.Commits) == 0 { |
| glog.Warning("No commits yet in commit cache") |
| return []string{}, nil |
| } |
| |
| startIdx := sort.Search(len(c.Commits), func(i int) bool { |
| return c.Commits[i].Timestamp.After(start) |
| }) |
| endIdx := sort.Search(len(c.Commits), func(i int) bool { |
| return c.Commits[i].Timestamp.After(end) |
| }) |
| commits := c.Commits[startIdx:endIdx] |
| |
| r := make([]string, 0, len(commits)) |
| for _, commit := range commits { |
| r = append(r, commit.Hash) |
| } |
| return r, nil |
| } |
| |
| // update syncs the source code repository and loads any new commits. |
| func (c *CommitCache) update() (rv error) { |
| defer timer.New("CommitCache.update()").Stop() |
| glog.Info("Reloading commits.") |
| if err := c.repo.Update(true, true); err != nil { |
| return fmt.Errorf("Failed to update the repo: %v", err) |
| } |
| from := time.Time{} |
| n := c.NumCommits() |
| if n > 0 { |
| last, err := c.Get(n - 1) |
| if err != nil { |
| return fmt.Errorf("Failed to get last commit: %v", err) |
| } |
| from = last.Timestamp |
| } |
| newCommitHashes := c.repo.From(from) |
| glog.Infof("Processing %d new commits.", len(newCommitHashes)) |
| newComments, err := c.db.GetCommitsComments(newCommitHashes) |
| if err != nil { |
| return fmt.Errorf("Failed to retrieve commit comments: %s", err) |
| } |
| newCommits := make([]*vcsinfo.LongCommit, len(newCommitHashes)) |
| if len(newCommitHashes) > 0 { |
| for i, h := range newCommitHashes { |
| d, err := c.repo.Details(h, false) |
| if err != nil { |
| return fmt.Errorf("Failed to obtain commit details for %s: %v", h, err) |
| } |
| newCommits[i] = d |
| } |
| } |
| branchHeads, err := c.repo.GetBranches() |
| if err != nil { |
| return fmt.Errorf("Failed to read branch information from the repo: %v", err) |
| } |
| |
| allCommits := append(c.Commits, newCommits...) |
| // Update the cached values all at once at at the end. |
| glog.Infof("Updating the cache.") |
| // Write the cache to disk *after* unlocking it. |
| defer func() { |
| rv = c.toFile() |
| }() |
| defer timer.New(" CommitCache locked").Stop() |
| c.mutex.Lock() |
| defer c.mutex.Unlock() |
| c.BranchHeads = branchHeads |
| c.Commits = allCommits |
| if c.Comments == nil { |
| c.Comments = map[string][]*buildbot.CommitComment{} |
| } |
| for k, v := range newComments { |
| c.Comments[k] = v |
| } |
| glog.Infof("Finished updating the cache.") |
| return nil |
| } |
| |
| type CommitData struct { |
| Comments map[string][]*buildbot.CommitComment `json:"comments"` |
| Commits []*vcsinfo.LongCommit `json:"commits"` |
| BranchHeads []*gitinfo.GitBranch `json:"branch_heads"` |
| StartIdx int `json:"startIdx"` |
| EndIdx int `json:"endIdx"` |
| } |
| |
| // getRange returns the given commit range along with the branch heads. |
| // Assumes that the caller holds a read lock. |
| func (c *CommitCache) getRange(startIdx, endIdx int) (*CommitData, error) { |
| glog.Infof("CommitCache.RangeAsJson") |
| commits, err := c.Slice(startIdx, endIdx) |
| if err != nil { |
| return nil, err |
| } |
| hashes := make([]string, 0, len(commits)) |
| comments := map[string][]*buildbot.CommitComment{} |
| for _, commit := range commits { |
| hashes = append(hashes, commit.Hash) |
| comments[commit.Hash] = c.Comments[commit.Hash] |
| } |
| |
| data := &CommitData{ |
| Comments: comments, |
| Commits: commits, |
| BranchHeads: c.BranchHeads, |
| StartIdx: startIdx, |
| EndIdx: endIdx, |
| } |
| return data, nil |
| } |
| |
| // GetLastN returns the last N commits along with the branch heads. |
| func (c *CommitCache) GetLastN(n int) (*CommitData, error) { |
| end := c.NumCommits() |
| start := end - n |
| if start < 0 { |
| start = 0 |
| } |
| return c.getRange(start, end) |
| } |