blob: 8a58c7329d3dc452829a1b9915c2459ab165c56a [file] [log] [blame]
package repo_manager
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
const (
MANIFEST_FILE_NAME = "userspace"
)
var (
// Use this function to instantiate a RepoManager. This is able to be
// overridden for testing.
NewManifestRepoManager func(string, string, string, string, string, string, *gerrit.Gerrit, NextRollStrategy, []string) (RepoManager, error) = newManifestRepoManager
)
// manifestRepoManager is a struct used by Manifest AutoRoller for managing checkouts.
type manifestRepoManager struct {
*depotToolsRepoManager
}
// newManifestRepoManager returns a RepoManager instance which operates in the
// given working directory and updates at the given frequency.
func newManifestRepoManager(workdir, parentRepo, parentBranch, childPath, childBranch string, depot_tools string, g *gerrit.Gerrit, strategy NextRollStrategy, preUploadStepNames []string) (RepoManager, error) {
wd := path.Join(workdir, "repo_manager")
parentBase := strings.TrimSuffix(path.Base(parentRepo), ".git")
parentDir := path.Join(wd, parentBase)
childDir := path.Join(wd, childPath)
childRepo, err := git.NewCheckout(common.REPO_SKIA, wd)
if err != nil {
return nil, err
}
user, err := g.GetUserEmail()
if err != nil {
return nil, fmt.Errorf("Failed to determine Gerrit user: %s", err)
}
sklog.Infof("Repo Manager user: %s", user)
preUploadSteps, err := GetPreUploadSteps(preUploadStepNames)
if err != nil {
return nil, err
}
mr := &manifestRepoManager{
depotToolsRepoManager: &depotToolsRepoManager{
commonRepoManager: &commonRepoManager{
parentBranch: parentBranch,
childDir: childDir,
childPath: childPath,
childRepo: childRepo,
childBranch: childBranch,
preUploadSteps: preUploadSteps,
strategy: strategy,
user: user,
workdir: wd,
g: g,
},
depot_tools: depot_tools,
gclient: path.Join(depot_tools, "gclient"),
parentDir: parentDir,
parentRepo: parentRepo,
},
}
// TODO(borenet): This update can be extremely expensive. Consider
// moving it out of the startup critical path.
return mr, mr.Update()
}
// Update syncs code in the relevant repositories.
func (mr *manifestRepoManager) Update() error {
// Sync the projects.
mr.repoMtx.Lock()
defer mr.repoMtx.Unlock()
if err := mr.createAndSyncParent(); err != nil {
return fmt.Errorf("Could not create and sync parent repo: %s", err)
}
if err := mr.childRepo.Update(); err != nil {
return err
}
// Get the last roll revision.
lastRollRev, err := mr.getLastRollRev()
if err != nil {
return err
}
// Get the next roll revision.
nextRollRev, err := mr.strategy.GetNextRollRev(mr.childRepo, lastRollRev)
if err != nil {
return err
}
mr.infoMtx.Lock()
defer mr.infoMtx.Unlock()
mr.lastRollRev = lastRollRev
mr.nextRollRev = nextRollRev
return nil
}
// getLastRollRev returns the commit hash of the last-completed DEPS roll.
// TODO(rmistry): File a bug to Fuchsia infra to make the below simpler by
// add a tool similar to roll-dep.
func (mr *manifestRepoManager) getLastRollRev() (string, error) {
// Parse the manifest file to extract the child repo revision.
content, err := ioutil.ReadFile(filepath.Join(mr.parentDir, MANIFEST_FILE_NAME))
if err != nil {
return "", fmt.Errorf("Could not read from %s: %s", MANIFEST_FILE_NAME, err)
}
childRepoName := path.Base(mr.childDir)
regex := regexp.MustCompile(fmt.Sprintf(`(?sm)%s(.*?)revision="(.*?)"`, childRepoName))
m := regex.FindStringSubmatch(string(content))
if m == nil {
return "", fmt.Errorf("Could not find target revision from %s", MANIFEST_FILE_NAME)
}
return m[len(m)-1], nil
}
// CreateNewRoll creates and uploads a new DEPS roll to the given commit.
// Returns the issue number of the uploaded roll.
func (mr *manifestRepoManager) CreateNewRoll(from, to string, emails []string, cqExtraTrybots string, dryRun bool) (int64, error) {
mr.repoMtx.Lock()
defer mr.repoMtx.Unlock()
// Clean the checkout, get onto a fresh branch.
if err := mr.cleanParent(); err != nil {
return 0, err
}
if _, err := exec.RunCwd(mr.parentDir, "git", "checkout", "-b", ROLL_BRANCH, "-t", fmt.Sprintf("origin/%s", mr.parentBranch), "-f"); err != nil {
return 0, err
}
// Defer some more cleanup.
defer func() {
util.LogErr(mr.cleanParent())
}()
// Create the roll CL.
cr := mr.childRepo
commits, err := cr.RevList(fmt.Sprintf("%s..%s", from, to))
if err != nil {
return 0, fmt.Errorf("Failed to list revisions: %s", err)
}
if _, err := exec.RunCwd(mr.parentDir, "git", "config", "user.name", mr.user); err != nil {
return 0, err
}
if _, err := exec.RunCwd(mr.parentDir, "git", "config", "user.email", mr.user); err != nil {
return 0, err
}
// Update the manifest file.
if err := mr.updateManifestFile(mr.lastRollRev, to); err != nil {
return 0, err
}
// Run the pre-upload steps.
for _, s := range mr.PreUploadSteps() {
if err := s(mr.parentDir); err != nil {
return 0, fmt.Errorf("Failed pre-upload step: %s", err)
}
}
// Get list of changes.
changeSummaries := []string{}
for _, c := range commits {
d, err := cr.Details(c)
if err != nil {
return 0, err
}
changeSummary := fmt.Sprintf("%s %s %s", d.Timestamp.Format("2006-01-02"), AUTHOR_EMAIL_RE.FindStringSubmatch(d.Author)[1], d.Subject)
changeSummaries = append(changeSummaries, changeSummary)
}
// Create commit message.
commitRange := fmt.Sprintf("%s..%s", from[:9], to[:9])
childRepoName := path.Base(mr.childDir)
commitMsg := fmt.Sprintf(
`Roll %s %s (%d commits)
https://%s.googlesource.com/%s.git/+log/%s
%s
`, mr.childPath, commitRange, len(commits), childRepoName, childRepoName, commitRange, strings.Join(changeSummaries, "\n"))
// Commit the change with the above message.
if _, addErr := exec.RunCwd(mr.parentDir, "git", "add", MANIFEST_FILE_NAME); addErr != nil {
return 0, fmt.Errorf("Failed to git add: %s", addErr)
}
if _, commitErr := exec.RunCwd(mr.parentDir, "git", "commit", "-m", commitMsg); commitErr != nil {
return 0, fmt.Errorf("Failed to commit: %s", commitErr)
}
// TODO(rmistry): Remove after things reliably work.
emails = append(emails, "rmistry@google.com")
// Upload the CL to Gerrit.
uploadCmd := &exec.Command{
Dir: mr.parentDir,
Env: mr.GetEnvForDepotTools(),
Name: "git",
Args: []string{"cl", "upload", "--bypass-hooks", "-f", "-v", "-v"},
}
uploadCmd.Args = append(uploadCmd.Args, "--gerrit")
if emails != nil && len(emails) > 0 {
emailStr := strings.Join(emails, ",")
uploadCmd.Args = append(uploadCmd.Args, "--send-mail", "--cc", emailStr)
}
uploadCmd.Args = append(uploadCmd.Args, "-m", commitMsg)
// Upload the CL.
sklog.Infof("Running command: git %s", strings.Join(uploadCmd.Args, " "))
if _, err := exec.RunCommand(uploadCmd); err != nil {
return 0, err
}
// Obtain the issue number.
tmp, err := ioutil.TempDir("", "")
if err != nil {
return 0, err
}
defer util.RemoveAll(tmp)
jsonFile := path.Join(tmp, "issue.json")
if _, err := exec.RunCommand(&exec.Command{
Dir: mr.parentDir,
Env: mr.GetEnvForDepotTools(),
Name: "git",
Args: []string{"cl", "issue", fmt.Sprintf("--json=%s", jsonFile)},
}); err != nil {
return 0, err
}
f, err := os.Open(jsonFile)
if err != nil {
return 0, err
}
var issue issueJson
if err := json.NewDecoder(f).Decode(&issue); err != nil {
return 0, err
}
// Set CR+2 and CQ+1/CQ+2 using the API.
change, err := mr.g.GetIssueProperties(issue.Issue)
if err != nil {
return issue.Issue, err
}
if err := mr.setChangeLabels(change, dryRun); err != nil {
return issue.Issue, err
}
return issue.Issue, nil
}
// setChangeLabels sets the appropriate labels on the Gerrit change.
// It uses the Gerrit REST API to set the following labels on the change:
// * Code-Review=2
// * Commit-Queue=2 (if dryRun=false else 1 is set)
func (r *manifestRepoManager) setChangeLabels(change *gerrit.ChangeInfo, dryRun bool) error {
labelValues := map[string]interface{}{
gerrit.CODEREVIEW_LABEL: "2",
}
if dryRun {
labelValues[gerrit.COMMITQUEUE_LABEL] = gerrit.COMMITQUEUE_LABEL_DRY_RUN
} else {
labelValues[gerrit.COMMITQUEUE_LABEL] = gerrit.COMMITQUEUE_LABEL_SUBMIT
}
return r.g.SetReview(change, "Roller setting labels to auto-land change.", labelValues)
}
func (mr *manifestRepoManager) updateManifestFile(prevHash, newHash string) error {
manifestFilePath := filepath.Join(mr.parentDir, MANIFEST_FILE_NAME)
sklog.Infof("Updating %s from %s to %s", manifestFilePath, prevHash, newHash)
content, err := ioutil.ReadFile(manifestFilePath)
if err != nil {
return fmt.Errorf("Could not read from %s: %s", manifestFilePath, err)
}
newContent := strings.Replace(string(content), prevHash, newHash, 1)
if err := ioutil.WriteFile(manifestFilePath, []byte(newContent), os.ModePerm); err != nil {
return fmt.Errorf("Could not write to %s: %s", manifestFilePath, err)
}
return nil
}
func (mr *manifestRepoManager) SendToGerritCQ(change *gerrit.ChangeInfo, comment string) error {
return mr.g.SendToCQ(change, "")
}
func (mr *manifestRepoManager) SendToGerritDryRun(change *gerrit.ChangeInfo, comment string) error {
return mr.g.SendToDryRun(change, "")
}