blob: 811132239306e7783329c12898b126e8424e5926 [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 (
buildbucketpb ""
// strategy checks out a git repository according to a Buildbucket build input.
type strategy interface {
Checkout(ctx context.Context, e *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 at defaultRevision, which can be either a ref (e.g.
// refs/heads/main) or a commit hash.
func newStrategy(input *buildbucketpb.Build_Input, repoURL url.URL, defaultRevision 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
// The current build was not triggered by a change to the specified
// repository, so we'll checkout at the default revision.
commit := &buildbucketpb.GitilesCommit{
Host: repoURL.Host,
Project: strings.TrimLeft(repoURL.Path, "/"),
Id: defaultRevision,
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)
return false
func (c checkoutChange) String() string {
return fmt.Sprintf("change: %s parent: %s", c.change, c.parent)
func (c checkoutChange) Checkout(ctx context.Context, executor *execution.Executor) error {
host := RemoveCodeReviewSuffix(c.change.Host)
url := fmt.Sprintf("https://%s/%s", host, c.change.Project)
ref := GitilesChangeRef(c.change)
// If rebase is not necessary, we only need to fetch depth 1.
var fetchOpt string
if c.parent == nil {
fetchOpt = "--depth=1"
} else {
fetchOpt = "--tags"
cmds := []execution.Command{
// Checkout the patch.
{Args: []string{git, "init", "--quiet"}},
{Args: []string{git, "remote", "add", "origin", url}},
{Args: []string{git, "fetch", fetchOpt, "origin", ref}},
{Args: []string{git, "checkout", "--force", "FETCH_HEAD"}},
if c.parent != nil {
cmds = append(cmds, []execution.Command{
// Rebase on top of parent.
{Args: []string{git, "fetch", "origin", c.parent.Id}},
// Rebase failures are generally user-caused, e.g. because the user
// is trying to rebase a change that has a merge conflict with the
// parent that needs to be manually fixed.
{Args: []string{git, "rebase", "FETCH_HEAD"}, UserCausedError: true},
return executor.ExecAll(ctx, cmds)
// checks out from a Gitiles commit.
type checkoutCommit struct {
commit *buildbucketpb.GitilesCommit
func (c checkoutCommit) String() string {
return fmt.Sprintf("commit: %s", c.commit)
func (c checkoutCommit) Equal(o strategy) bool {
switch other := o.(type) {
case checkoutCommit:
return proto.Equal(other.commit, c.commit)
return false
func (c checkoutCommit) Checkout(ctx context.Context, executor *execution.Executor) error {
url := fmt.Sprintf("https://%s/%s", c.commit.Host, c.commit.Project)
return executor.ExecAll(ctx, []execution.Command{
{Args: []string{git, "init", "--quiet"}},
{Args: []string{git, "remote", "add", "origin", url}},
{Args: []string{git, "fetch", "--depth=1", "origin", c.commit.Id}},
{Args: []string{git, "checkout", "FETCH_HEAD"}},
// Converts to
func RemoveCodeReviewSuffix(host string) string {
return strings.ReplaceAll(host, "", "")
// 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)