blob: fad76be8f76c973243d27e331618265a7851614a [file] [log] [blame]
package git_common
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"go.skia.org/infra/autoroll/go/config"
"go.skia.org/infra/autoroll/go/config_vars"
"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"
)
// Checkout provides common functionality for git checkouts.
type Checkout struct {
git.Checkout
Branch *config_vars.Template
defaultBugProject string
Dependencies []*config.VersionFileConfig
RepoURL string
RevLinkTmpl string
}
// NewCheckout returns a Checkout instance.
func NewCheckout(ctx context.Context, c *config.GitCheckoutConfig, reg *config_vars.Registry, workdir, userName, userEmail string, co git.Checkout) (*Checkout, error) {
// Clean up any lockfiles, in case the process was interrupted.
if err := git.DeleteLockFiles(ctx, workdir); err != nil {
return nil, skerr.Wrap(err)
}
// Register the configured branch template.
branch, err := config_vars.NewTemplate(c.Branch)
if err != nil {
return nil, skerr.Wrap(err)
}
if err := reg.Register(branch); err != nil {
return nil, skerr.Wrap(err)
}
// Create the local checkout.
if co == nil {
var err error
co, err = git.NewCheckout(ctx, c.RepoUrl, workdir)
if err != nil {
return nil, skerr.Wrap(err)
}
}
// Set the git user name and email.
if _, err := co.Git(ctx, "config", "--local", "user.name", userName); err != nil {
return nil, skerr.Wrap(err)
}
if _, err := co.Git(ctx, "config", "--local", "user.email", userEmail); err != nil {
return nil, skerr.Wrap(err)
}
return &Checkout{
Checkout: co,
Branch: branch,
defaultBugProject: c.DefaultBugProject,
Dependencies: c.Dependencies,
RepoURL: c.RepoUrl,
RevLinkTmpl: c.RevLinkTmpl,
}, nil
}
// GetRevision implements Child.
func (c *Checkout) GetRevision(ctx context.Context, id string) (*revision.Revision, error) {
details, err := c.Details(ctx, id)
if err != nil {
return nil, skerr.Wrap(err)
}
rev := revision.FromLongCommit(c.RevLinkTmpl, c.defaultBugProject, details)
if len(c.Dependencies) > 0 {
deps, err := version_file_common.GetPinnedRevs(ctx, c.Dependencies, func(ctx context.Context, path string) (string, error) {
return c.GetFile(ctx, path, rev.Id)
})
if err != nil {
return nil, skerr.Wrap(err)
}
rev.Dependencies = deps
}
return rev, nil
}
// Download implements Child.
func (c *Checkout) Download(ctx context.Context, rev *revision.Revision, dest string) error {
return Clone(ctx, c.RepoURL, dest, rev)
}
// Update resolves the configured branch template, updates the Checkout to the
// newest Revision on the resulting branch and returns both the revision and
// resolved branch name.
func (c *Checkout) Update(ctx context.Context) (*revision.Revision, string, error) {
branch := c.Branch.String()
if err := c.UpdateBranch(ctx, branch); err != nil {
return nil, "", skerr.Wrap(err)
}
tipRev, err := c.GetRevision(ctx, "HEAD")
if err != nil {
return nil, "", skerr.Wrap(err)
}
return tipRev, branch, nil
}
// LogRevisions implements Child.
func (c *Checkout) LogRevisions(ctx context.Context, from, to *revision.Revision) ([]*revision.Revision, error) {
hashes, err := c.RevList(ctx, "--first-parent", git.LogFromTo(from.Id, to.Id))
if err != nil {
return nil, skerr.Wrap(err)
}
revs := make([]*revision.Revision, 0, len(hashes))
for _, hash := range hashes {
rev, err := c.GetRevision(ctx, hash)
if err != nil {
return nil, skerr.Wrap(err)
}
revs = append(revs, rev)
}
return revs, nil
}
// ApplyExternalChangeFunc applies the specified ExternalChangeId in whichever
// way makes sense for the implementation. Example: git_checkout_github uses
// the ExternalChangeId as a Github PR and cherry-picks the PR patch locally.
type ApplyExternalChangeFunc func(context.Context, git.Checkout, string) error
// CreateRollFunc 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 CreateRollFunc func(context.Context, git.Checkout, *revision.Revision, *revision.Revision, []*revision.Revision, string) (string, error)
// UploadRollFunc uploads a CL using the given commit hash and
// returns its ID.
type UploadRollFunc func(context.Context, git.Checkout, string, string, []string, bool, string) (int64, error)
// CreateNewRoll uploads a new roll using the given createRoll and uploadRoll
// functions.
// See documentation for the Parent interface for more details.
func (c *Checkout) CreateNewRoll(ctx context.Context, from, to *revision.Revision, rolling []*revision.Revision, emails []string, dryRun bool, commitMsg string, createRoll CreateRollFunc, uploadRoll UploadRollFunc) (int64, error) {
// Create the roll branch.
_, upstreamBranch, err := c.Update(ctx)
if err != nil {
return 0, skerr.Wrap(err)
}
_, _ = c.Git(ctx, "branch", "-D", RollBranch) // Fails if the branch does not exist.
if _, err := c.Git(ctx, "checkout", "-b", RollBranch, "-t", fmt.Sprintf("origin/%s", upstreamBranch)); err != nil {
return 0, skerr.Wrap(err)
}
if _, err := c.Git(ctx, "reset", "--hard", upstreamBranch); err != nil {
return 0, skerr.Wrap(err)
}
// Run the provided function to create the changes for the roll.
hash, err := createRoll(ctx, c.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 := c.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 := c.Git(ctx, "diff", "--quiet"); err != nil {
return 0, skerr.Wrapf(err, "createRoll left uncommitted changes")
}
out, err := c.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 uploadRoll(ctx, c.Checkout, upstreamBranch, hash, emails, dryRun, commitMsg)
}
// Clone clones the given repo into the given destination and syncs it to the
// given Revision.
func Clone(ctx context.Context, repoUrl, dest string, rev *revision.Revision) error {
// If the checkout does not already exist in dest, create it.
gitDir := filepath.Join(dest, ".git")
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
if err := git.Clone(ctx, repoUrl, dest, false); err != nil {
return skerr.Wrap(err)
}
}
// Fetch and reset to the given revision.
co := git.CheckoutDir(dest)
if err := co.Fetch(ctx); err != nil {
return skerr.Wrap(err)
}
if _, err := co.Git(ctx, "reset", "--hard", rev.Id); err != nil {
return skerr.Wrap(err)
}
return nil
}