blob: fa295ce5d2ad90cfc07011b5fe3a1d20b55b2c3a [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package checkout
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/golang/protobuf/proto"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.fuchsia.dev/infra/cmd/build_init/execution"
)
// strategy checks out a git repository according to a Buildbucket build input.
type strategy interface {
Checkout(*execution.Executor) error
Equal(other strategy) bool
}
// newStrategy selects a strategy for the given git repo URL and build input. input is the
// current Buildbucket build input. repoURL is the repository containing test inputs. This
// ensures that the patched versions of test inputs are used if they're modified, or
// fetched from an expected revision if not.
//
// If the build input indicates that the repo at repoURL was modified:
// * checkout from patchset if the build input has a Gerrit change.
// * checkout from a commit if the build input has Gitiles commit.
//
// If the build input indicates otherwise:
// * error if there is no Gitiles commit or Gerrit change, to avoid masking the bug
// that there is no build input (nothing to test).
// * otherwise checkout from specRef (default is HEAD).
func newStrategy(input buildbucketpb.Build_Input, repoURL url.URL, specRef string) (strategy, error) {
if len(input.GerritChanges) == 0 && input.GitilesCommit == nil {
return nil, errors.New("build input has no changes")
}
if len(input.GerritChanges) > 0 {
change := input.GerritChanges[0]
changeURL := url.URL{
Scheme: repoURL.Scheme, // gerrit changes have no scheme.
Host: removeCodeReviewSuffix(change.Host),
Path: change.Project,
}
if changeURL.String() == repoURL.String() {
return checkoutChange{change: change, parent: input.GitilesCommit}, nil
}
} else if input.GitilesCommit != nil {
commit := input.GitilesCommit
commitURL := url.URL{
Scheme: repoURL.Scheme, // gitiles commits have no scheme.
Host: commit.Host,
Path: commit.Project,
}
if commitURL.String() == repoURL.String() {
return checkoutCommit{commit: input.GitilesCommit}, nil
}
}
commit := &buildbucketpb.GitilesCommit{
Host: repoURL.Host,
Project: strings.TrimLeft(repoURL.Path, "/"),
Id: specRef,
}
return checkoutCommit{commit}, nil
}
// checks out from a Gerrit change by rebasing that change on top of a Gitiles commit.
type checkoutChange struct {
change *buildbucketpb.GerritChange
parent *buildbucketpb.GitilesCommit
}
func (c checkoutChange) Equal(o strategy) bool {
switch other := o.(type) {
case checkoutChange:
return proto.Equal(other.parent, c.parent) && proto.Equal(other.change, c.change)
default:
return false
}
}
func (c checkoutChange) String() string {
return fmt.Sprintf("change: %v parent: %v", c.change.String(), c.parent.String())
}
func (c checkoutChange) Checkout(executor *execution.Executor) error {
host := removeCodeReviewSuffix(c.change.Host)
url := fmt.Sprintf("https://%s/%s", host, c.change.Project)
ref := gitilesChangeRef(*c.change)
return executor.ExecAll([][]string{
// Checkout the patch.
{git, "init"},
{git, "remote", "add", "origin", url},
{git, "fetch", "--tags", "origin", ref},
{git, "checkout", "--force", "FETCH_HEAD"},
// Rebase on top of parent.
{git, "fetch", "origin", c.parent.Id},
{git, "rebase", "FETCH_HEAD"},
})
}
// checks out from a Gitiles commit.
type checkoutCommit struct {
commit *buildbucketpb.GitilesCommit
}
func (c checkoutCommit) String() string {
return fmt.Sprintf("commit: %v", c.commit.String())
}
func (c checkoutCommit) Equal(o strategy) bool {
switch other := o.(type) {
case checkoutCommit:
return proto.Equal(other.commit, c.commit)
default:
return false
}
}
func (c checkoutCommit) Checkout(executor *execution.Executor) error {
url := fmt.Sprintf("https://%s/%s", c.commit.Host, c.commit.Project)
return executor.ExecAll([][]string{
{git, "init"},
{git, "remote", "add", "origin", url},
{git, "fetch", "--depth=1", "origin", c.commit.Id},
{git, "checkout", "FETCH_HEAD"},
})
}
// Converts foo-review.googlesource.com to foo.googlesource.com
func removeCodeReviewSuffix(host string) string {
return strings.ReplaceAll(host, "-review.googlesource.com", ".googlesource.com")
}
// Returns the Gitiles ref for a gerrit changeNumber. The ref has the form xx/yyyy/zz
// where xx is `yyyy modulo 100` (always 2 digits), and zz is the patchset number.
func gitilesChangeRef(change buildbucketpb.GerritChange) string {
changeno := change.Change
return fmt.Sprintf("refs/changes/%02d/%d/%d", changeno%100, changeno, change.Patchset)
}