blob: e3f7f09a59451578703d9813e7e18aed9663f1f8 [file] [log] [blame]
package parent
/*
Parent implementations which use a local checkout to create changes.
*/
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"go.skia.org/infra/autoroll/go/config_vars"
"go.skia.org/infra/autoroll/go/repo_manager/common/git_common"
"go.skia.org/infra/autoroll/go/repo_manager/common/version_file_common"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/skerr"
)
const (
// rollBranch is the git branch which is used to create rolls.
rollBranch = "roll_branch"
)
// GitCheckoutConfig provides configuration for a Parent which uses a local
// checkout to create changes.
type GitCheckoutConfig struct {
BaseConfig
git_common.GitCheckoutConfig
version_file_common.DependencyConfig
}
// See documentation for util.Validator interface.
func (c GitCheckoutConfig) Validate() error {
if err := c.BaseConfig.Validate(); err != nil {
return skerr.Wrap(err)
}
if err := c.GitCheckoutConfig.Validate(); err != nil {
return skerr.Wrap(err)
}
if err := c.DependencyConfig.Validate(); err != nil {
return skerr.Wrap(err)
}
if len(c.GitCheckoutConfig.Dependencies) != 0 {
return skerr.Fmt("Dependencies are inherited from the DependencyConfig and should not be set on the GitCheckoutConfig.")
}
return nil
}
// GitCheckoutParent is a base for implementations of Parent which use a local
// Git checkout.
type GitCheckoutParent struct {
*baseParent
*git_common.Checkout
childID string
createRoll GitCheckoutCreateRollFunc
uploadRoll GitCheckoutUploadRollFunc
}
// GitCheckoutCreateRollFunc generates commit(s) in the local Git checkout to
// be used in the next roll and returns the hash of the commit to be uploaded.
// GitCheckoutParent handles creation of the roll branch.
type GitCheckoutCreateRollFunc func(context.Context, *git.Checkout, *revision.Revision, *revision.Revision, []*revision.Revision, string) (string, error)
// GitCheckoutUploadRollFunc uploads a CL using the given commit hash and
// returns its ID.
type GitCheckoutUploadRollFunc func(context.Context, *git.Checkout, string, string, []string, bool, string) (int64, error)
// NewGitCheckout returns a base for implementations of Parent which use
// a local checkout to create changes.
func NewGitCheckout(ctx context.Context, c GitCheckoutConfig, reg *config_vars.Registry, serverURL, workdir, userName, userEmail string, co *git.Checkout, createRoll GitCheckoutCreateRollFunc, uploadRoll GitCheckoutUploadRollFunc) (*GitCheckoutParent, error) {
if err := c.Validate(); err != nil {
return nil, skerr.Wrap(err)
}
// Create a baseParent.
base, err := newBaseParent(ctx, c.BaseConfig, serverURL)
if err != nil {
return nil, skerr.Wrap(err)
}
// Create the local checkout.
deps := make([]*version_file_common.VersionFileConfig, 0, len(c.DependencyConfig.TransitiveDeps)+1)
deps = append(deps, &c.DependencyConfig.VersionFileConfig)
for _, td := range c.TransitiveDeps {
deps = append(deps, td.Parent)
}
c.GitCheckoutConfig.Dependencies = deps
checkout, err := git_common.NewCheckout(ctx, c.GitCheckoutConfig, reg, workdir, userName, userEmail, co)
if err != nil {
return nil, skerr.Wrap(err)
}
return &GitCheckoutParent{
baseParent: base,
Checkout: checkout,
childID: c.DependencyConfig.ID,
createRoll: createRoll,
uploadRoll: uploadRoll,
}, nil
}
// See documentation for Parent interface.
func (p *GitCheckoutParent) Update(ctx context.Context) (string, error) {
rev, _, err := p.Checkout.Update(ctx)
if err != nil {
return "", skerr.Wrap(err)
}
lastRollRev, ok := rev.Dependencies[p.childID]
if !ok {
return "", skerr.Fmt("Unable to find dependency %q in %#v", p.childID, rev)
}
return lastRollRev, nil
}
// See documentation for Parent interface.
func (p *GitCheckoutParent) CreateNewRoll(ctx context.Context, from, to *revision.Revision, rolling []*revision.Revision, emails []string, cqExtraTrybots string, dryRun bool) (int64, error) {
// Create the roll branch.
_, upstreamBranch, err := p.Checkout.Update(ctx)
if err != nil {
return 0, skerr.Wrap(err)
}
_, _ = p.Git(ctx, "branch", "-D", rollBranch) // Fails if the branch does not exist.
if _, err := p.Git(ctx, "checkout", "-b", rollBranch, "-t", fmt.Sprintf("origin/%s", upstreamBranch)); err != nil {
return 0, skerr.Wrap(err)
}
if _, err := p.Git(ctx, "reset", "--hard", upstreamBranch); err != nil {
return 0, skerr.Wrap(err)
}
// Generate the commit message.
// TODO(borenet): This should probably move into parentChildRepoManager.
commitMsg, err := p.buildCommitMsg(from, to, rolling, emails, cqExtraTrybots, nil)
if err != nil {
return 0, skerr.Wrap(err)
}
// Run the provided function to create the changes for the roll.
hash, err := p.createRoll(ctx, p.Checkout.Checkout, from, to, rolling, commitMsg)
if err != nil {
return 0, skerr.Wrap(err)
}
// Ensure that createRoll generated at least one commit downstream of
// p.baseCommit, and that it did not leave uncommitted changes.
commits, err := p.RevList(ctx, "--ancestry-path", "--first-parent", fmt.Sprintf("%s..%s", upstreamBranch, hash))
if err != nil {
return 0, skerr.Wrap(err)
}
if len(commits) == 0 {
return 0, skerr.Fmt("createRoll generated no commits!")
}
if _, err := p.Git(ctx, "diff", "--quiet"); err != nil {
return 0, skerr.Wrapf(err, "createRoll left uncommitted changes")
}
out, err := p.Git(ctx, "ls-files", "--others", "--exclude-standard")
if err != nil {
return 0, skerr.Wrap(err)
}
if len(strings.Fields(out)) > 0 {
return 0, skerr.Fmt("createRoll left untracked files:\n%s", out)
}
// Upload the CL.
return p.uploadRoll(ctx, p.Checkout.Checkout, upstreamBranch, hash, emails, dryRun, commitMsg)
}
// gitCheckoutFileCreateRollFunc returns a GitCheckoutCreateRollFunc which uses
// a local Git checkout and pins dependencies using a file checked into the
// repo.
func gitCheckoutFileCreateRollFunc(dep version_file_common.DependencyConfig) GitCheckoutCreateRollFunc {
return func(ctx context.Context, co *git.Checkout, from *revision.Revision, to *revision.Revision, rolling []*revision.Revision, commitMsg string) (string, error) {
// Determine what changes need to be made.
getFile := func(ctx context.Context, path string) (string, error) {
return co.GetFile(ctx, path, "HEAD")
}
changes, _, err := version_file_common.UpdateDep(ctx, dep, to, getFile)
if err != nil {
return "", skerr.Wrap(err)
}
// Perform the changes.
for path, contents := range changes {
fullPath := filepath.Join(co.Dir(), path)
if err := ioutil.WriteFile(fullPath, []byte(contents), os.ModePerm); err != nil {
return "", skerr.Wrap(err)
}
if _, err := co.Git(ctx, "add", path); err != nil {
return "", skerr.Wrap(err)
}
}
// Commit.
if _, err := co.Git(ctx, "commit", "-m", commitMsg); err != nil {
return "", skerr.Wrap(err)
}
out, err := co.RevParse(ctx, "HEAD")
if err != nil {
return "", skerr.Wrap(err)
}
return strings.TrimSpace(out), nil
}
}
var _ Parent = &GitCheckoutParent{}