| // gitinfo enables querying info from a Git repository. |
| package gitinfo |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "go.skia.org/infra/go/depot_tools" |
| "go.skia.org/infra/go/exec" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/tiling" |
| "go.skia.org/infra/go/util" |
| "go.skia.org/infra/go/vcsinfo" |
| ) |
| |
| // commitLineRe matches one line of commit log and captures hash, author and |
| // subject groups. |
| var commitLineRe = regexp.MustCompile(`([0-9a-f]{40}),([^,\n]+),(.+)$`) |
| |
| // GitInfo allows querying a Git repo. |
| type GitInfo struct { |
| dir string |
| hashes []string |
| timestamps map[string]time.Time // The git hash is the key. |
| detailsCache map[string]*vcsinfo.LongCommit // The git hash is the key. |
| firstCommit string |
| secondaryVCS vcsinfo.VCS |
| secondaryExtractor depot_tools.DEPSExtractor |
| |
| // Any access to hashes or timestamps must be protected. |
| mutex sync.Mutex |
| } |
| |
| // NewGitInfo creates a new GitInfo for the Git repository found in directory |
| // dir. If pull is true then a git pull is done on the repo before querying it |
| // for history. |
| func NewGitInfo(ctx context.Context, dir string, pull, allBranches bool) (*GitInfo, error) { |
| g := &GitInfo{ |
| dir: dir, |
| hashes: []string{}, |
| detailsCache: map[string]*vcsinfo.LongCommit{}, |
| } |
| return g, g.Update(ctx, pull, allBranches) |
| } |
| |
| // Clone creates a new GitInfo by running "git clone" in the given directory. |
| func Clone(ctx context.Context, repoUrl, dir string, allBranches bool) (*GitInfo, error) { |
| if _, err := exec.RunSimple(ctx, fmt.Sprintf("git clone %s %s", repoUrl, dir)); err != nil { |
| return nil, fmt.Errorf("Failed to clone %s into %s: %s", repoUrl, dir, err) |
| } |
| return NewGitInfo(ctx, dir, false, allBranches) |
| } |
| |
| // CloneOrUpdate creates a new GitInfo by running "git clone" or "git pull" |
| // depending on whether the repo already exists. |
| func CloneOrUpdate(ctx context.Context, repoUrl, dir string, allBranches bool) (*GitInfo, error) { |
| gitDir := path.Join(dir, ".git") |
| _, err := os.Stat(gitDir) |
| if err == nil { |
| return NewGitInfo(ctx, dir, true, allBranches) |
| } |
| if os.IsNotExist(err) { |
| return Clone(ctx, repoUrl, dir, allBranches) |
| } |
| return nil, err |
| } |
| |
| // Update refreshes the history that GitInfo stores for the repo. If pull is |
| // true then git pull is performed before refreshing. |
| func (g *GitInfo) Update(ctx context.Context, pull, allBranches bool) error { |
| // If there is a secondary repository update it in the background and wait |
| // at the end until the update is done. |
| doneCh := g.updateSecondary(ctx, pull, allBranches) |
| |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| defer func() { <-doneCh }() |
| |
| sklog.Info("Beginning Update.") |
| if pull { |
| if _, err := exec.RunCwd(ctx, g.dir, "git", "pull"); err != nil { |
| return fmt.Errorf("Failed to sync to HEAD: %s", err) |
| } |
| } |
| sklog.Info("Finished pull.") |
| var hashes []string |
| var timestamps map[string]time.Time |
| var err error |
| if allBranches { |
| hashes, timestamps, err = readCommitsFromGitAllBranches(ctx, g.dir) |
| } else { |
| hashes, timestamps, err = readCommitsFromGit(ctx, g.dir, "HEAD") |
| } |
| sklog.Infof("Finished reading commits: %s", g.dir) |
| if err != nil { |
| return fmt.Errorf("Failed to read commits from: %s : %s", g.dir, err) |
| } |
| g.hashes = hashes |
| g.timestamps = timestamps |
| g.firstCommit, err = g.InitialCommit(ctx) |
| if err != nil { |
| return fmt.Errorf("Failed to get initial commit: %s", err) |
| } |
| return nil |
| } |
| |
| // Dir returns the checkout dir of the GitInfo.. |
| func (g *GitInfo) Dir() string { |
| return g.dir |
| } |
| |
| // Details returns more information than ShortCommit about a given commit. |
| // See the vcsinfo.VCS interface for details. |
| func (g *GitInfo) Details(ctx context.Context, hash string, includeBranchInfo bool) (*vcsinfo.LongCommit, error) { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| return g.details(ctx, hash, includeBranchInfo) |
| } |
| |
| // details returns more information than ShortCommit about a given commit. |
| // See the vcsinfo.VCS interface for details. |
| // |
| // Caller is responsible for locking the mutex. |
| func (g *GitInfo) details(ctx context.Context, hash string, includeBranchInfo bool) (*vcsinfo.LongCommit, error) { |
| if c, ok := g.detailsCache[hash]; ok { |
| // Return the cached value if the branchInfo request matches. |
| if !includeBranchInfo || (len(c.Branches) > 0) { |
| return c, nil |
| } |
| } |
| output, err := exec.RunCwd(ctx, g.dir, "git", "log", "-n", "1", "--format=format:%H%n%P%n%an%x20(%ae)%n%s%n%b", hash) |
| if err != nil { |
| return nil, fmt.Errorf("Failed to execute Git: %s", err) |
| } |
| lines := strings.SplitN(output, "\n", 5) |
| if len(lines) != 5 { |
| return nil, fmt.Errorf("Failed to parse output of 'git log'.") |
| } |
| branches := map[string]bool{} |
| if includeBranchInfo { |
| branches, err = g.getBranchesForCommit(ctx, hash) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| parents := []string{} |
| if lines[1] != "" { |
| parents = strings.Split(lines[1], " ") |
| } |
| c := vcsinfo.LongCommit{ |
| ShortCommit: &vcsinfo.ShortCommit{ |
| Hash: lines[0], |
| Author: lines[2], |
| Subject: lines[3], |
| }, |
| Parents: parents, |
| Body: lines[4], |
| Timestamp: g.timestamps[hash], |
| Branches: branches, |
| } |
| g.detailsCache[hash] = &c |
| return &c, nil |
| } |
| |
| func (g *GitInfo) Reset(ctx context.Context, ref string) error { |
| _, err := exec.RunCwd(ctx, g.dir, "git", "reset", "--hard", ref) |
| if err != nil { |
| return fmt.Errorf("Failed to roll back/forward to commit %s: %s", ref, err) |
| } |
| return nil |
| } |
| |
| func (g *GitInfo) Checkout(ctx context.Context, ref string) error { |
| if _, err := exec.RunCwd(ctx, g.dir, "git", "checkout", ref); err != nil { |
| return fmt.Errorf("Failed to checkout %s: %s", ref, err) |
| } |
| return nil |
| } |
| |
| // getBranchesForCommit returns a string set with all the branches that can reach |
| // the commit with the given hash. |
| // TODO(stephana): Speed up this method, there are either better ways to do this |
| // in git or the results can be cached. |
| func (g *GitInfo) getBranchesForCommit(ctx context.Context, hash string) (map[string]bool, error) { |
| output, err := exec.RunCwd(ctx, g.dir, "git", "branch", "--all", "--list", "--contains", hash) |
| if err != nil { |
| return nil, fmt.Errorf("Failed to get branches for commit %s: %s", hash, err) |
| } |
| |
| lines := strings.Split(strings.TrimSpace(output), "\n") |
| ret := map[string]bool{} |
| for _, line := range lines { |
| l := strings.TrimSpace(line) |
| if l != "" { |
| // Splitting the line to filter out the '*' that marks the active branch. |
| parts := strings.Split(l, " ") |
| ret[parts[len(parts)-1]] = true |
| } |
| } |
| return ret, nil |
| } |
| |
| // RevList returns the results of "git rev-list". |
| func (g *GitInfo) RevList(ctx context.Context, args ...string) ([]string, error) { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| output, err := exec.RunCwd(ctx, g.dir, append([]string{"git", "rev-list"}, args...)...) |
| if err != nil { |
| return nil, fmt.Errorf("git rev-list failed: %v", err) |
| } |
| res := strings.Trim(output, "\n") |
| if res == "" { |
| return []string{}, nil |
| } |
| return strings.Split(res, "\n"), nil |
| } |
| |
| // From returns all commits from 'start' to HEAD. |
| func (g *GitInfo) From(start time.Time) []string { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| ret := []string{} |
| for _, h := range g.hashes { |
| if g.timestamps[h].After(start) { |
| ret = append(ret, h) |
| } |
| } |
| return ret |
| } |
| |
| // Range returns all commits from the half open interval ['begin', 'end'), i.e. |
| // includes 'begin' and excludes 'end'. |
| func (g *GitInfo) Range(begin, end time.Time) []*vcsinfo.IndexCommit { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| ret := []*vcsinfo.IndexCommit{} |
| first := sort.Search(len(g.hashes), func(i int) bool { |
| ts := g.timestamps[g.hashes[i]] |
| return ts.After(begin) || ts.Equal(begin) |
| }) |
| if first == len(g.timestamps) { |
| return ret |
| } |
| for i, h := range g.hashes[first:] { |
| if g.timestamps[h].Before(end) { |
| ret = append(ret, &vcsinfo.IndexCommit{ |
| Hash: h, |
| Index: first + i, |
| Timestamp: g.timestamps[h], |
| }) |
| } else { |
| break |
| } |
| } |
| return ret |
| } |
| |
| // LastNIndex returns the last N commits. |
| func (g *GitInfo) LastNIndex(N int) []*vcsinfo.IndexCommit { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| var hashes []string |
| offset := 0 |
| if len(g.hashes) < N { |
| hashes = g.hashes |
| } else { |
| hashes = g.hashes[len(g.hashes)-N:] |
| offset = len(g.hashes) - N |
| } |
| ret := []*vcsinfo.IndexCommit{} |
| for i, h := range hashes { |
| ret = append(ret, &vcsinfo.IndexCommit{ |
| Hash: h, |
| Index: i + offset, |
| Timestamp: g.timestamps[h], |
| }) |
| } |
| return ret |
| } |
| |
| // IndexOf returns the index of given hash as counted from the first commit in |
| // this branch by 'rev-list'. The index is 0 based. |
| func (g *GitInfo) IndexOf(ctx context.Context, hash string) (int, error) { |
| // Count the lines from running: |
| // git rev-list --count <first-commit>..hash. |
| output, err := g.RevList(ctx, "--count", fmt.Sprintf("%s..%s", g.firstCommit, hash)) |
| if err != nil { |
| return 0, fmt.Errorf("git rev-list failed: %s", err) |
| } |
| if len(output) != 1 { |
| return 0, fmt.Errorf("git rev-list wrong size output: %s", err) |
| } |
| n, err := strconv.Atoi(output[0]) |
| if err != nil { |
| return 0, fmt.Errorf("Didn't get a number: %s", err) |
| } |
| return n, nil |
| } |
| |
| // ByIndex returns a LongCommit describing the commit |
| // at position N, as ordered in the current branch. |
| // |
| // Does not make sense if readCommitsFromGitAllBranches has been |
| // called. |
| func (g *GitInfo) ByIndex(ctx context.Context, N int) (*vcsinfo.LongCommit, error) { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| numHashes := len(g.hashes) |
| if N < 0 || N >= numHashes { |
| return nil, fmt.Errorf("Hash index not found: %d", N) |
| } |
| return g.details(ctx, g.hashes[N], false) |
| } |
| |
| // LastN returns the last N commits. |
| func (g *GitInfo) LastN(ctx context.Context, N int) []string { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| if len(g.hashes) < N { |
| return g.hashes[0:len(g.hashes)] |
| } else { |
| return g.hashes[len(g.hashes)-N:] |
| } |
| } |
| |
| // Timestamp returns the timestamp for the given hash. |
| func (g *GitInfo) Timestamp(hash string) time.Time { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| return g.timestamps[hash] |
| } |
| |
| // Log returns a --name-only short log for every commit in (begin, end]. |
| // |
| // If end is "" then it returns just the short log for the single commit at |
| // begin. |
| // |
| // Example response: |
| // |
| // commit b7988a21fdf23cc4ace6145a06ea824aa85db099 |
| // Author: Joe Gregorio <jcgregorio@google.com> |
| // Date: Tue Aug 5 16:19:48 2014 -0400 |
| // |
| // A description of the commit. |
| // |
| // perf/go/skiaperf/perf.go |
| // perf/go/types/types.go |
| // perf/res/js/logic.js |
| // |
| func (g *GitInfo) Log(ctx context.Context, begin, end string) (string, error) { |
| command := []string{"git", "log", "--name-only"} |
| hashrange := begin |
| if end != "" { |
| hashrange += ".." + end |
| command = append(command, hashrange) |
| } else { |
| command = append(command, "-n", "1", hashrange) |
| } |
| output, err := exec.RunCwd(ctx, g.dir, command...) |
| if err != nil { |
| return "", err |
| } |
| return output, nil |
| } |
| |
| // LogFine is the same as Log() but appends all the 'args' to the Log |
| // request to allow finer control of the log output. I.e. you could call: |
| // |
| // LogFine(begin, end, "--format=format:%ct", "infra/bots/assets/skp/VERSION") |
| |
| func (g *GitInfo) LogFine(ctx context.Context, begin, end string, args ...string) (string, error) { |
| command := []string{"git", "log"} |
| hashrange := begin |
| if end != "" { |
| hashrange += ".." + end |
| command = append(command, hashrange) |
| } else { |
| command = append(command, "-n", "1", hashrange) |
| } |
| command = append(command, args...) |
| output, err := exec.RunCwd(ctx, g.dir, command...) |
| if err != nil { |
| return "", err |
| } |
| return output, nil |
| } |
| |
| // LogArgs is the same as Log() but appends all the 'args' to the Log |
| // request to allow finer control of the log output. I.e. you could call: |
| // |
| // LogArgs("--since=2015-10-24", "--format=format:%ct", "infra/bots/assets/skp/VERSION") |
| func (g *GitInfo) LogArgs(ctx context.Context, args ...string) (string, error) { |
| command := []string{"git", "log"} |
| command = append(command, args...) |
| output, err := exec.RunCwd(ctx, g.dir, command...) |
| if err != nil { |
| return "", err |
| } |
| return output, nil |
| } |
| |
| // FullHash gives the full commit hash for the given ref. |
| func (g *GitInfo) FullHash(ctx context.Context, ref string) (string, error) { |
| output, err := exec.RunCwd(ctx, g.dir, "git", "rev-parse", fmt.Sprintf("%s^{commit}", ref)) |
| if err != nil { |
| return "", fmt.Errorf("Failed to obtain full hash: %s", err) |
| } |
| return strings.Trim(output, "\n"), nil |
| } |
| |
| // GetFile returns the contents of the given file at the given commit. |
| func (g *GitInfo) GetFile(ctx context.Context, fileName, commit string) (string, error) { |
| output, err := exec.RunCwd(ctx, g.dir, "git", "show", commit+":"+fileName) |
| if err != nil { |
| return "", err |
| } |
| return output, nil |
| } |
| |
| // InitalCommit returns the hash of the initial commit. |
| func (g *GitInfo) InitialCommit(ctx context.Context) (string, error) { |
| output, err := exec.RunCwd(ctx, g.dir, "git", "rev-list", "--max-parents=0", "HEAD") |
| if err != nil { |
| return "", fmt.Errorf("Failed to determine initial commit: %v", err) |
| } |
| return strings.Trim(output, "\n"), nil |
| } |
| |
| // GetBranches returns a slice of strings naming the branches in the repo. |
| func (g *GitInfo) GetBranches(ctx context.Context) ([]*GitBranch, error) { |
| return GetBranches(ctx, g.dir) |
| } |
| |
| // ShortCommits stores a slice of ShortCommit struct. |
| type ShortCommits struct { |
| Commits []*vcsinfo.ShortCommit |
| } |
| |
| // ShortList returns a slice of ShortCommit for every commit in (begin, end]. |
| func (g *GitInfo) ShortList(ctx context.Context, begin, end string) (*ShortCommits, error) { |
| command := []string{"git", "log", "--pretty='%H,%an,%s", begin + ".." + end} |
| output, err := exec.RunCwd(ctx, g.dir, command...) |
| if err != nil { |
| return nil, err |
| } |
| ret := &ShortCommits{ |
| Commits: []*vcsinfo.ShortCommit{}, |
| } |
| for _, line := range strings.Split(output, "\n") { |
| match := commitLineRe.FindStringSubmatch(line) |
| if match == nil { |
| // This could happen if the subject has new line, in which case we truncate it and ignore the remainder. |
| continue |
| } |
| commit := &vcsinfo.ShortCommit{ |
| Hash: match[1], |
| Author: match[2], |
| Subject: match[3], |
| } |
| ret.Commits = append(ret.Commits, commit) |
| } |
| |
| return ret, nil |
| } |
| |
| func (g *GitInfo) IsAncestor(ctx context.Context, a, b string) bool { |
| cmd := []string{"git", "merge-base", "--is-ancestor", a, b} |
| if _, err := exec.RunCwd(ctx, g.dir, cmd...); err != nil { |
| return false |
| } |
| return true |
| } |
| |
| // ResolveCommit see vcsinfo.VCS interface. |
| func (g *GitInfo) ResolveCommit(ctx context.Context, commitHash string) (string, error) { |
| if g.secondaryVCS == nil { |
| return "", nil |
| } |
| |
| foundCommit, err := g.secondaryExtractor.ExtractCommit(g.secondaryVCS.GetFile(ctx, "DEPS", commitHash)) |
| if err != nil { |
| return "", err |
| } |
| return foundCommit, 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 (g *GitInfo) SetSecondaryRepo(secVCS vcsinfo.VCS, extractor depot_tools.DEPSExtractor) { |
| g.secondaryVCS = secVCS |
| g.secondaryExtractor = extractor |
| } |
| |
| // updateSecondary updates the secondary VCS if one is defined. The returned |
| // channel will be closed once the update this done. This allows to synchronize |
| // with the completion via simple read from the channel. |
| func (g *GitInfo) updateSecondary(ctx context.Context, pull, allBranches bool) <-chan bool { |
| doneCh := make(chan bool) |
| |
| // Update the secondary VCS if necessary and close the done channel after. |
| go func() { |
| if g.secondaryVCS != nil { |
| if err := g.secondaryVCS.Update(ctx, pull, allBranches); err != nil { |
| sklog.Errorf("Error updating secondary VCS: %s", err) |
| } |
| } |
| close(doneCh) |
| }() |
| return doneCh |
| } |
| |
| // gitHash represents information on a single Git commit. |
| type gitHash struct { |
| hash string |
| timeStamp time.Time |
| } |
| |
| type gitHashSlice []*gitHash |
| |
| func (p gitHashSlice) Len() int { return len(p) } |
| func (p gitHashSlice) Less(i, j int) bool { return p[i].timeStamp.Before(p[j].timeStamp) } |
| func (p gitHashSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } |
| |
| // GitBranch represents a Git branch. |
| type GitBranch struct { |
| Name string `json:"name"` |
| Head string `json:"head"` |
| } |
| |
| // includeBranchPrefixes is the list of branch prefixes that we should consider in the |
| // output of the 'git show-ref' command issued in GetBranches below. |
| var includeBranchPrefixes = []string{ |
| "refs/remotes/", |
| "refs/heads/", |
| } |
| |
| // GetBranches returns the list of branch heads in a Git repository. |
| // In order to separate local working branches from published branches, only |
| // remote branches in 'origin' are returned. |
| func GetBranches(ctx context.Context, dir string) ([]*GitBranch, error) { |
| output, err := exec.RunCwd(ctx, dir, "git", "show-ref") |
| if err != nil { |
| return nil, fmt.Errorf("Failed to get branch list: %v", err) |
| } |
| branches := []*GitBranch{} |
| lines := strings.Split(output, "\n") |
| for _, line := range lines { |
| if line == "" { |
| continue |
| } |
| parts := strings.SplitN(line, " ", 2) |
| if len(parts) != 2 { |
| return nil, fmt.Errorf("Could not parse output of 'git show-ref'.") |
| } |
| |
| for _, prefix := range includeBranchPrefixes { |
| if strings.HasPrefix(parts[1], prefix) { |
| name := parts[1][len(prefix):] |
| branches = append(branches, &GitBranch{ |
| Name: name, |
| Head: parts[0], |
| }) |
| } |
| } |
| } |
| return branches, nil |
| } |
| |
| // readCommitsFromGit reads the commit history from a Git repository. |
| func readCommitsFromGit(ctx context.Context, dir, branch string) ([]string, map[string]time.Time, error) { |
| output, err := exec.RunCwd(ctx, dir, "git", "log", "--format=format:%H%x20%ci", branch) |
| if err != nil { |
| return nil, nil, fmt.Errorf("Failed to execute git log: %s", err) |
| } |
| lines := strings.Split(output, "\n") |
| gitHashes := make([]*gitHash, 0, len(lines)) |
| timestamps := map[string]time.Time{} |
| for _, line := range lines { |
| parts := strings.SplitN(line, " ", 2) |
| if len(parts) == 2 { |
| t, err := time.Parse("2006-01-02 15:04:05 -0700", parts[1]) |
| if err != nil { |
| return nil, nil, fmt.Errorf("Failed parsing Git log timestamp: %s", err) |
| } |
| t = t.UTC() |
| hash := parts[0] |
| gitHashes = append(gitHashes, &gitHash{hash: hash, timeStamp: t}) |
| timestamps[hash] = t |
| } |
| } |
| sort.Sort(gitHashSlice(gitHashes)) |
| hashes := make([]string, len(gitHashes), len(gitHashes)) |
| for i, h := range gitHashes { |
| hashes[i] = h.hash |
| } |
| return hashes, timestamps, nil |
| } |
| |
| func readCommitsFromGitAllBranches(ctx context.Context, dir string) ([]string, map[string]time.Time, error) { |
| branches, err := GetBranches(ctx, dir) |
| if err != nil { |
| return nil, nil, fmt.Errorf("Could not read commits; unable to get branch list: %v", err) |
| } |
| timestamps := map[string]time.Time{} |
| for _, b := range branches { |
| _, ts, err := readCommitsFromGit(ctx, dir, b.Name) |
| if err != nil { |
| return nil, nil, err |
| } |
| for k, v := range ts { |
| timestamps[k] = v |
| } |
| } |
| gitHashes := make([]*gitHash, len(timestamps), len(timestamps)) |
| i := 0 |
| for h, t := range timestamps { |
| gitHashes[i] = &gitHash{hash: h, timeStamp: t} |
| i++ |
| } |
| sort.Sort(gitHashSlice(gitHashes)) |
| hashes := make([]string, len(timestamps), len(timestamps)) |
| for i, h := range gitHashes { |
| hashes[i] = h.hash |
| } |
| return hashes, timestamps, nil |
| } |
| |
| // SkpCommits returns the indices for all the commits that contain SKP updates. |
| func (g *GitInfo) SkpCommits(ctx context.Context, tile *tiling.Tile) ([]int, error) { |
| // Executes a git log command that looks like: |
| // |
| // git log --format=format:%h 32956400b4d8f33394e2cdef9b66e8369ba2a0f3..e7416bfc9858bde8fc6eb5f3bfc942bc3350953a SKP_VERSION |
| // |
| // The output should be a \n separated list of hashes that match. |
| first, last := tile.CommitRange() |
| output, err := exec.RunCwd(ctx, g.dir, "git", "log", "--format=format:%H", first+".."+last, path.Join("infra", "bots", "assets", "skp", "VERSION")) |
| if err != nil { |
| return nil, fmt.Errorf("SkpCommits: Failed to find git log of SKP_VERSION: %s", err) |
| } |
| hashes := strings.Split(output, "\n") |
| |
| ret := []int{} |
| for i, c := range tile.Commits { |
| if c.CommitTime != 0 && util.In(c.Hash, hashes) { |
| ret = append(ret, i) |
| } |
| } |
| return ret, nil |
| } |
| |
| // LastSkpCommit returns the time of the last change to the SKP_VERSION file. |
| func (g *GitInfo) LastSkpCommit(ctx context.Context) (time.Time, error) { |
| // Executes a git log command that looks like: |
| // |
| // git log --format=format:%ct -n 1 SKP_VERSION |
| // |
| // The output should be a single unix timestamp. |
| output, err := exec.RunCwd(ctx, g.dir, "git", "log", "--format=format:%ct", "-n", "1", "SKP_VERSION") |
| if err != nil { |
| return time.Time{}, fmt.Errorf("LastSkpCommit: Failed to read git log: %s", err) |
| } |
| ts, err := strconv.ParseInt(output, 10, 64) |
| if err != nil { |
| return time.Time{}, fmt.Errorf("LastSkpCommit: Failed to parse timestamp: %s", err) |
| } |
| return time.Unix(ts, 0), nil |
| } |
| |
| // TileAddressFromHash takes a commit hash and time, then returns the Level 0 |
| // tile number that contains the hash, and its position in the tile commit array. |
| // This assumes that tiles are built for commits since after the given time. |
| func (g *GitInfo) TileAddressFromHash(hash string, start time.Time) (num, offset int, err error) { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| i := 0 |
| for _, h := range g.hashes { |
| if g.timestamps[h].Before(start) { |
| continue |
| } |
| if h == hash { |
| return i / tiling.TILE_SIZE, i % tiling.TILE_SIZE, nil |
| } |
| i++ |
| } |
| return -1, -1, fmt.Errorf("Cannot find hash %s.\n", hash) |
| } |
| |
| // NumCommits returns the number of commits in the repo. |
| func (g *GitInfo) NumCommits() int { |
| g.mutex.Lock() |
| defer g.mutex.Unlock() |
| return len(g.hashes) |
| } |
| |
| // RepoMap is a struct used for managing multiple Git repositories. |
| type RepoMap struct { |
| repos map[string]*GitInfo |
| mutex sync.RWMutex |
| workdir string |
| } |
| |
| // NewRepoMap creates and returns a RepoMap which operates within the given |
| // workdir. |
| func NewRepoMap(workdir string) *RepoMap { |
| return &RepoMap{ |
| repos: map[string]*GitInfo{}, |
| workdir: workdir, |
| } |
| } |
| |
| // Repo retrieves a pointer to a GitInfo for the requested repo URL. If the |
| // repo does not yet exist in the repoMap, it is cloned and added before it is |
| // returned. |
| func (m *RepoMap) Repo(ctx context.Context, r string) (*GitInfo, error) { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| repo, ok := m.repos[r] |
| if !ok { |
| var err error |
| split := strings.Split(r, "/") |
| repoPath := path.Join(m.workdir, split[len(split)-1]) |
| repo, err = CloneOrUpdate(ctx, r, repoPath, true) |
| if err != nil { |
| return nil, fmt.Errorf("Failed to check out %s: %v", r, err) |
| } |
| m.repos[r] = repo |
| } |
| return repo, nil |
| } |
| |
| // RepoForCommit attempts to determine which repository the given commit hash |
| // belongs to and returns the associated repo URL if found. This is fragile, |
| // because it's possible, though very unlikely, that there may be collisions |
| // of commit hashes between repositories. |
| func (m *RepoMap) RepoForCommit(ctx context.Context, hash string) (string, error) { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| |
| for url, r := range m.repos { |
| if _, err := r.FullHash(ctx, hash); err == nil { |
| return url, nil |
| } |
| } |
| return "", fmt.Errorf("Could not find commit %s in any repo!", hash) |
| } |
| |
| // Update causes all of the repos in the RepoMap to be updated. |
| func (m *RepoMap) Update(ctx context.Context) error { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| for _, r := range m.repos { |
| if err := r.Update(ctx, true, true); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // Repos returns the list of repos contained in the RepoMap. |
| func (m *RepoMap) Repos() []string { |
| m.mutex.Lock() |
| defer m.mutex.Unlock() |
| rv := make([]string, 0, len(m.repos)) |
| for url := range m.repos { |
| rv = append(rv, url) |
| } |
| return rv |
| } |
| |
| // Ensure that GitInfo implements vscinfo.VCS. |
| var _ vcsinfo.VCS = &GitInfo{} |