| package git |
| |
| /* |
| Common utils used by Repo and Checkout. |
| */ |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path" |
| "strconv" |
| "strings" |
| "time" |
| |
| "go.skia.org/infra/go/exec" |
| "go.skia.org/infra/go/git/git_common" |
| "go.skia.org/infra/go/vcsinfo" |
| ) |
| |
| // Branch describes a Git branch. |
| type Branch struct { |
| // The human-readable name of the branch. |
| Name string |
| |
| // The commit hash pointed to by this branch. |
| Head string |
| } |
| |
| // GitDir is a directory in which one may run Git commands. |
| type GitDir string |
| |
| // newGitDir creates a GitDir instance based in the given directory. |
| func newGitDir(ctx context.Context, repoUrl, workdir string, mirror bool) (GitDir, error) { |
| dest := path.Join(workdir, strings.TrimSuffix(path.Base(repoUrl), ".git")) |
| if _, err := os.Stat(dest); err != nil { |
| if os.IsNotExist(err) { |
| // Clone the repo. |
| if mirror { |
| // We don't use a "real" mirror, since that syncs ALL refs, |
| // including every patchset of every CL that gets uploaded. Instead, |
| // we use a bare clone and then add the "mirror" config after |
| // cloning. It would be equivalent to use --mirror and then update |
| // the refspec to only sync the branches, but that would force the |
| // initial clone step to sync every ref. |
| if _, err := exec.RunCwd(ctx, workdir, "git", "clone", "--bare", repoUrl, dest); err != nil { |
| return "", fmt.Errorf("Failed to clone repo: %s", err) |
| } |
| if _, err := exec.RunCwd(ctx, dest, "git", "config", "remote.origin.mirror", "true"); err != nil { |
| return "", fmt.Errorf("Failed to set git mirror config: %s", err) |
| } |
| if _, err := exec.RunCwd(ctx, dest, "git", "config", "remote.origin.fetch", "refs/heads/*:refs/heads/*"); err != nil { |
| return "", fmt.Errorf("Failed to set git mirror config: %s", err) |
| } |
| if _, err := exec.RunCwd(ctx, dest, "git", "fetch", "--force", "--all"); err != nil { |
| return "", fmt.Errorf("Failed to set git mirror config: %s", err) |
| } |
| } else { |
| if _, err := exec.RunCwd(ctx, workdir, "git", "clone", repoUrl, dest); err != nil { |
| return "", fmt.Errorf("Failed to clone repo: %s", err) |
| } |
| } |
| } else { |
| return "", fmt.Errorf("There is a problem with the git directory: %s", err) |
| } |
| } |
| return GitDir(dest), nil |
| } |
| |
| // Dir returns the working directory of the GitDir. |
| func (g GitDir) Dir() string { |
| return string(g) |
| } |
| |
| // Git runs the given git command in the GitDir. |
| func (g GitDir) Git(ctx context.Context, cmd ...string) (string, error) { |
| return exec.RunCwd(ctx, string(g), append([]string{"git"}, cmd...)...) |
| } |
| |
| // Details returns a vcsinfo.LongCommit instance representing the given commit. |
| func (g GitDir) Details(ctx context.Context, name string) (*vcsinfo.LongCommit, error) { |
| output, err := g.Git(ctx, "log", "-n", "1", "--format=format:%H%n%P%n%an%x20(%ae)%n%s%n%ct%n%b", name) |
| if err != nil { |
| return nil, err |
| } |
| lines := strings.SplitN(output, "\n", 6) |
| if len(lines) != 6 { |
| return nil, fmt.Errorf("Failed to parse output of 'git log'.") |
| } |
| var parents []string |
| if lines[1] != "" { |
| parents = strings.Split(lines[1], " ") |
| } |
| ts, err := strconv.ParseInt(lines[4], 10, 64) |
| if err != nil { |
| return nil, err |
| } |
| return &vcsinfo.LongCommit{ |
| ShortCommit: &vcsinfo.ShortCommit{ |
| Hash: lines[0], |
| Author: lines[2], |
| Subject: lines[3], |
| }, |
| Parents: parents, |
| Body: strings.TrimRight(lines[5], "\n"), |
| Timestamp: time.Unix(ts, 0), |
| }, nil |
| } |
| |
| // RevParse runs "git rev-parse <name>" and returns the result. |
| func (g GitDir) RevParse(ctx context.Context, args ...string) (string, error) { |
| out, err := g.Git(ctx, append([]string{"rev-parse"}, args...)...) |
| if err != nil { |
| return "", err |
| } |
| // Ensure that we got a single, 40-character commit hash. |
| split := strings.Fields(out) |
| if len(split) != 1 { |
| return "", fmt.Errorf("Unable to parse commit hash from output: %s", out) |
| } |
| if len(split[0]) != 40 { |
| return "", fmt.Errorf("rev-parse returned invalid commit hash: %s", out) |
| } |
| return split[0], nil |
| } |
| |
| // RevList runs "git rev-list <name>" and returns a slice of commit hashes. |
| func (g GitDir) RevList(ctx context.Context, args ...string) ([]string, error) { |
| out, err := g.Git(ctx, append([]string{"rev-list"}, args...)...) |
| if err != nil { |
| return nil, err |
| } |
| return strings.Fields(out), nil |
| } |
| |
| // GetBranchHead returns the commit hash at the HEAD of the given branch. |
| func (g GitDir) GetBranchHead(ctx context.Context, branchName string) (string, error) { |
| return g.RevParse(ctx, "--verify", fmt.Sprintf("refs/heads/%s^{commit}", branchName)) |
| } |
| |
| // Branches runs "git branch" and returns a slice of Branch instances. |
| func (g GitDir) Branches(ctx context.Context) ([]*Branch, error) { |
| out, err := g.Git(ctx, "branch") |
| if err != nil { |
| return nil, err |
| } |
| branchNames := strings.Fields(out) |
| branches := make([]*Branch, 0, len(branchNames)) |
| for _, name := range branchNames { |
| if name == "*" { |
| continue |
| } |
| head, err := g.GetBranchHead(ctx, name) |
| if err != nil { |
| return nil, err |
| } |
| branches = append(branches, &Branch{ |
| Head: head, |
| Name: name, |
| }) |
| } |
| return branches, nil |
| } |
| |
| // GetFile returns the contents of the given file at the given commit. |
| func (g GitDir) GetFile(ctx context.Context, fileName, commit string) (string, error) { |
| return g.Git(ctx, "show", commit+":"+fileName) |
| } |
| |
| // NumCommits returns the number of commits in the repo. |
| func (g GitDir) NumCommits(ctx context.Context) (int64, error) { |
| out, err := g.Git(ctx, "rev-list", "--all", "--count") |
| if err != nil { |
| return 0, err |
| } |
| return strconv.ParseInt(strings.TrimSpace(out), 10, 64) |
| } |
| |
| // IsAncestor returns true iff A is an ancestor of B. |
| func (g GitDir) IsAncestor(ctx context.Context, a, b string) (bool, error) { |
| out, err := g.Git(ctx, "merge-base", "--is-ancestor", a, b) |
| if err != nil { |
| // Either a is not an ancestor of b, or we got a real error. If |
| // the output is empty, assume it's the former case. Otherwise, |
| // return an error. |
| if out == "" { |
| return false, nil |
| } |
| return false, fmt.Errorf("%s: %s", err, out) |
| } |
| return true, nil |
| } |
| |
| // Version returns the Git version. |
| func (g GitDir) Version(ctx context.Context) (int, int, error) { |
| return git_common.Version(ctx) |
| } |
| |
| // FullHash gives the full commit hash for the given ref. |
| func (g GitDir) FullHash(ctx context.Context, ref string) (string, error) { |
| output, err := g.RevParse(ctx, fmt.Sprintf("%s^{commit}", ref)) |
| if err != nil { |
| return "", fmt.Errorf("Failed to obtain full hash: %s", err) |
| } |
| return output, nil |
| } |