blob: a546bf919b724d958195cea7ba6c3a85f42f3f30 [file] [log] [blame]
package repo_manager
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"go.skia.org/infra/autoroll/go/codereview"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/git"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/util"
)
const (
ftReadmePath = "third_party/freetype/README.chromium"
ftReadmeVersionTmpl = "%sVersion: %s"
ftReadmeRevisionTmpl = "%sRevision: %s"
ftIncludeSrc = "include/freetype/config"
ftIncludeDest = "third_party/freetype/include/freetype-custom-config"
)
var (
// Use this function to instantiate a RepoManager. This is able to be
// overridden for testing.
NewFreeTypeRepoManager func(context.Context, *FreeTypeRepoManagerConfig, string, gerrit.GerritInterface, string, string, *http.Client, codereview.CodeReview, bool) (RepoManager, error) = newFreeTypeRepoManager
ftReadmeVersionRegex = regexp.MustCompile(fmt.Sprintf(ftReadmeVersionTmpl, "(?m)^", ".*"))
ftReadmeRevisionRegex = regexp.MustCompile(fmt.Sprintf(ftReadmeRevisionTmpl, "(?m)^", ".*"))
ftIncludesToMerge = []string{
"ftoption.h",
"ftconfig.h",
}
)
// FreeTypeRepoManagerConfig provides configuration for FreeTypeRepoManager.
type FreeTypeRepoManagerConfig struct {
NoCheckoutDEPSRepoManagerConfig
}
// See documentation for RepoManagerConfig interface.
func (c *FreeTypeRepoManagerConfig) NoCheckout() bool {
return false
}
// freeTypeRepoManager is a RepoManager which rolls FreeType in DEPS and updates
// header files and README.chromium.
type freetypeRepoManager struct {
*noCheckoutDEPSRepoManager
localChildRepo *git.Repo
}
// newFreeTypeRepoManager returns a RepoManager instance which rolls FreeType
// in DEPS and updates header files and README.chromium.
func newFreeTypeRepoManager(ctx context.Context, c *FreeTypeRepoManagerConfig, workdir string, g gerrit.GerritInterface, recipeCfgFile, serverURL string, client *http.Client, cr codereview.CodeReview, local bool) (RepoManager, error) {
ncrm, err := newNoCheckoutDEPSRepoManager(ctx, &c.NoCheckoutDEPSRepoManagerConfig, workdir, g, recipeCfgFile, serverURL, client, cr, local)
if err != nil {
return nil, err
}
localChildRepo, err := git.NewRepo(ctx, c.ChildRepo, workdir)
if err != nil {
return nil, err
}
rv := &freetypeRepoManager{
localChildRepo: localChildRepo,
noCheckoutDEPSRepoManager: ncrm.(*noCheckoutDEPSRepoManager),
}
rv.noCheckoutDEPSRepoManager.noCheckoutRepoManager.createRoll = rv.createRoll
rv.noCheckoutDEPSRepoManager.noCheckoutRepoManager.updateHelper = rv.updateHelper
return rv, nil
}
// Perform a three-way merge for this header file in a temporary dir. Adds the
// new contents to the changes map.
func (rm *freetypeRepoManager) mergeInclude(ctx context.Context, include, from, to string, changes map[string]string) error {
wd, err := ioutil.TempDir("", "")
if err != nil {
return err
}
defer util.RemoveAll(wd)
gd := git.GitDir(wd)
_, err = gd.Git(ctx, "init")
// Obtain the current version of the file in the parent repo.
parentHeader := path.Join(ftIncludeDest, include)
dest := filepath.Join(wd, include)
var buf bytes.Buffer
if err := rm.parentRepo.ReadFileAtRef(ctx, parentHeader, rm.baseCommit, &buf); err != nil {
return err
}
oldParentContents := buf.String()
if err != nil {
return err
}
if err := ioutil.WriteFile(dest, buf.Bytes(), os.ModePerm); err != nil {
return err
}
if _, err := gd.Git(ctx, "add", dest); err != nil {
return err
}
if _, err := gd.Git(ctx, "commit", "-m", "fake"); err != nil {
return err
}
// Obtain the old version of the file in the child repo.
ftHeader := path.Join(ftIncludeSrc, include)
oldChildContents, err := rm.localChildRepo.GetFile(ctx, ftHeader, from)
if err != nil {
return err
}
oldPath := filepath.Join(wd, "old")
if err := ioutil.WriteFile(oldPath, []byte(oldChildContents), os.ModePerm); err != nil {
return err
}
// Obtain the new version of the file in the child repo.
newChildContents, err := rm.localChildRepo.GetFile(ctx, ftHeader, to)
if err != nil {
return err
}
newPath := filepath.Join(wd, "new")
if err := ioutil.WriteFile(newPath, []byte(newChildContents), os.ModePerm); err != nil {
return err
}
// Perform the merge.
if _, err := gd.Git(ctx, "merge-file", dest, oldPath, newPath); err != nil {
return err
}
// Read the resulting contents.
newParentContents, err := ioutil.ReadFile(dest)
if err != nil {
return err
}
if string(newParentContents) != string(oldParentContents) {
changes[parentHeader] = string(newParentContents)
}
return nil
}
// See documentation for noCheckoutRepoManagerCreateRollHelperFunc.
func (rm *freetypeRepoManager) createRoll(ctx context.Context, from, to *revision.Revision, rolling []*revision.Revision, serverURL, cqExtraTrybots string, emails []string) (string, map[string]string, error) {
commitMsg, changes, err := rm.noCheckoutDEPSRepoManager.createRoll(ctx, from, to, rolling, serverURL, cqExtraTrybots, emails)
if err != nil {
return "", nil, err
}
rm.infoMtx.RLock()
defer rm.infoMtx.RUnlock()
// Update README.chromium.
ftVersion, err := rm.localChildRepo.Git(ctx, "describe", "--long", to.Id)
if err != nil {
return "", nil, err
}
ftVersion = strings.TrimSpace(ftVersion)
var buf bytes.Buffer
if err := rm.parentRepo.ReadFileAtRef(ctx, ftReadmePath, rm.baseCommit, &buf); err != nil {
return "", nil, err
}
oldReadmeContents := buf.String()
newReadmeContents := ftReadmeVersionRegex.ReplaceAllString(oldReadmeContents, fmt.Sprintf(ftReadmeVersionTmpl, "", ftVersion))
newReadmeContents = ftReadmeRevisionRegex.ReplaceAllString(newReadmeContents, fmt.Sprintf(ftReadmeRevisionTmpl, "", to.Id))
if newReadmeContents != oldReadmeContents {
changes[ftReadmePath] = newReadmeContents
}
// Merge includes.
for _, include := range ftIncludesToMerge {
if err := rm.mergeInclude(ctx, include, from.Id, to.Id, changes); err != nil {
return "", nil, err
}
}
// Check modules.cfg. Give up if it has changed.
diff, err := rm.localChildRepo.Git(ctx, "diff", "--name-only", git.LogFromTo(from.Id, to.Id))
if err != nil {
return "", nil, err
}
if strings.Contains(diff, "modules.cfg") {
return "", nil, errors.New("modules.cfg has been modified; cannot roll automatically.")
}
return commitMsg, changes, nil
}
// See documentation for noCheckoutRepoManagerUpdateHelperFunc.
func (rm *freetypeRepoManager) updateHelper(ctx context.Context, parentRepo *gitiles.Repo, baseCommit string) (*revision.Revision, *revision.Revision, []*revision.Revision, error) {
lastRollRev, tipRev, notRolledRevs, err := rm.noCheckoutDEPSRepoManager.updateHelper(ctx, parentRepo, baseCommit)
if err != nil {
return nil, nil, nil, err
}
if err := rm.localChildRepo.Update(ctx); err != nil {
return nil, nil, nil, err
}
return lastRollRev, tipRev, notRolledRevs, nil
}
// See documentation for RepoManager interface.
func (r *freetypeRepoManager) GetRevision(ctx context.Context, id string) (*revision.Revision, error) {
r.repoMtx.RLock()
defer r.repoMtx.RUnlock()
details, err := r.localChildRepo.Details(ctx, id)
if err != nil {
return nil, err
}
return revision.FromLongCommit(r.childRevLinkTmpl, details), nil
}