Path: blob/main/components/content-service/pkg/initializer/git.go
2499 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package initializer56import (7"context"8"errors"9"fmt"10"os"11"os/exec"12"strconv"13"strings"14"time"1516"github.com/cenkalti/backoff"17"github.com/opentracing/opentracing-go"18"golang.org/x/xerrors"1920"github.com/gitpod-io/gitpod/common-go/log"21"github.com/gitpod-io/gitpod/common-go/process"22"github.com/gitpod-io/gitpod/common-go/tracing"23csapi "github.com/gitpod-io/gitpod/content-service/api"24"github.com/gitpod-io/gitpod/content-service/pkg/archive"25"github.com/gitpod-io/gitpod/content-service/pkg/git"26)2728// CloneTargetMode is the target state in which we want to leave a GitInitializer29type CloneTargetMode string3031const (32// RemoteHead has the local WS point at the remote branch head33RemoteHead CloneTargetMode = "head"3435// RemoteCommit has the local WS point at a specific commit36RemoteCommit CloneTargetMode = "commit"3738// RemoteBranch has the local WS point at a remote branch39RemoteBranch CloneTargetMode = "remote-branch"4041// LocalBranch creates a local branch in the workspace42LocalBranch CloneTargetMode = "local-branch"43)4445// GitInitializer is a local workspace with a Git connection46type GitInitializer struct {47git.Client4849// The target mode determines what gets checked out50TargetMode CloneTargetMode5152// The value for the clone target mode - use depends on the target mode53CloneTarget string5455// If true, the Git initializer will chown(gitpod) after the clone56Chown bool57}5859// Run initializes the workspace using Git60func (ws *GitInitializer) Run(ctx context.Context, mappings []archive.IDMapping) (src csapi.WorkspaceInitSource, stats csapi.InitializerMetrics, err error) {61isGitWS := git.IsWorkingCopy(ws.Location)62//nolint:ineffassign63span, ctx := opentracing.StartSpanFromContext(ctx, "GitInitializer.Run")64span.SetTag("isGitWS", isGitWS)65defer tracing.FinishSpan(span, &err)66start := time.Now()67initialSize, fsErr := getFsUsage()68if fsErr != nil {69log.WithError(fsErr).Error("could not get disk usage")70}7172src = csapi.WorkspaceInitFromOther73if isGitWS {74log.WithField("stage", "init").WithField("location", ws.Location).Info("Not running git clone. Workspace is already a Git workspace")75return76}7778gitClone := func() error {79if err := os.MkdirAll(ws.Location, 0775); err != nil {80log.WithError(err).WithField("location", ws.Location).Error("cannot create directory")81return err82}8384// make sure that folder itself is owned by gitpod user prior to doing git clone85// this is needed as otherwise git clone will fail if the folder is owned by root86if ws.RunAsGitpodUser {87args := []string{"gitpod", ws.Location}88cmd := exec.Command("chown", args...)89res, cerr := cmd.CombinedOutput()90if cerr != nil && !process.IsNotChildProcess(cerr) {91err = git.OpFailedError{92Args: args,93ExecErr: cerr,94Output: string(res),95Subcommand: "chown",96}97return err98}99}100101log.WithField("stage", "init").WithField("location", ws.Location).Debug("Running git clone on workspace")102err = ws.Clone(ctx)103if err != nil {104if strings.Contains(err.Error(), "Access denied") {105err = &backoff.PermanentError{106Err: fmt.Errorf("Access denied. Please check that Gitpod was given permission to access the repository"),107}108}109110return err111}112113// we can only do `git config` stuffs after having a directory that is also git init'd114// commit-graph after every git fetch command that downloads a pack-file from a remote115err = ws.Git(ctx, "config", "fetch.writeCommitGraph", "true")116if err != nil {117log.WithError(err).WithField("location", ws.Location).Error("cannot configure fetch.writeCommitGraph")118}119120err = ws.Git(ctx, "config", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*")121if err != nil {122log.WithError(err).WithField("location", ws.Location).Error("cannot configure fetch behavior")123}124125err = ws.Git(ctx, "config", "--replace-all", "checkout.defaultRemote", "origin")126if err != nil {127log.WithError(err).WithField("location", ws.Location).Error("cannot configure checkout defaultRemote")128}129130return nil131}132onGitCloneFailure := func(e error, d time.Duration) {133if err := os.RemoveAll(ws.Location); err != nil {134log.135WithField("stage", "init").136WithField("location", ws.Location).137WithError(err).138Error("Cleaning workspace location failed.")139}140log.141WithField("stage", "init").142WithField("location", ws.Location).143WithField("sleepTime", d).144WithError(e).145Debugf("Running git clone on workspace failed. Retrying in %s ...", d)146}147148b := backoff.NewExponentialBackOff()149b.MaxElapsedTime = 5 * time.Minute150if err = backoff.RetryNotify(gitClone, b, onGitCloneFailure); err != nil {151err = checkGitStatus(err)152return src, nil, xerrors.Errorf("git initializer gitClone: %w", err)153}154155defer func() {156span.SetTag("Chown", ws.Chown)157if !ws.Chown {158return159}160// TODO (aledbf): refactor to remove the need of manual chown161args := []string{"-R", "-L", "gitpod", ws.Location}162cmd := exec.Command("chown", args...)163res, cerr := cmd.CombinedOutput()164if cerr != nil && !process.IsNotChildProcess(cerr) {165err = git.OpFailedError{166Args: args,167ExecErr: cerr,168Output: string(res),169Subcommand: "chown",170}171return172}173}()174175if err := ws.realizeCloneTarget(ctx); err != nil {176return src, nil, xerrors.Errorf("git initializer clone: %w", err)177}178if err := ws.UpdateRemote(ctx); err != nil {179return src, nil, xerrors.Errorf("git initializer updateRemote: %w", err)180}181if err := ws.UpdateSubmodules(ctx); err != nil {182log.WithError(err).Warn("error while updating submodules - continuing")183}184185log.WithField("stage", "init").WithField("location", ws.Location).Debug("Git operations complete")186187if fsErr == nil {188currentSize, fsErr := getFsUsage()189if fsErr != nil {190log.WithError(fsErr).Error("could not get disk usage")191}192193stats = csapi.InitializerMetrics{csapi.InitializerMetric{194Type: "git",195Duration: time.Since(start),196Size: currentSize - initialSize,197}}198}199return200}201202func (ws *GitInitializer) isShallowRepository(ctx context.Context) bool {203out, err := ws.GitWithOutput(ctx, nil, "rev-parse", "--is-shallow-repository")204if err != nil {205log.WithError(err).Error("unexpected error checking if git repository is shallow")206return true207}208isShallow, err := strconv.ParseBool(strings.TrimSpace(string(out)))209if err != nil {210log.WithError(err).WithField("input", string(out)).Error("unexpected error parsing bool")211return true212}213return isShallow214}215216// realizeCloneTarget ensures the clone target is checked out217func (ws *GitInitializer) realizeCloneTarget(ctx context.Context) (err error) {218//nolint:ineffassign219span, ctx := opentracing.StartSpanFromContext(ctx, "realizeCloneTarget")220span.SetTag("remoteURI", ws.RemoteURI)221span.SetTag("cloneTarget", ws.CloneTarget)222span.SetTag("targetMode", ws.TargetMode)223defer tracing.FinishSpan(span, &err)224225defer func() {226err = checkGitStatus(err)227}()228229// checkout branch230switch ws.TargetMode {231case RemoteBranch:232// confirm the value of the default branch name using rev-parse233gitout, _ := ws.GitWithOutput(ctx, nil, "rev-parse", "--abbrev-ref", "origin/HEAD")234defaultBranch := strings.TrimSpace(strings.Replace(string(gitout), "origin/", "", -1))235236branchName := ws.CloneTarget237238// we already cloned the git repository but we need to check CloneTarget exists239// to avoid calling fetch from a non-existing branch240gitout, err := ws.GitWithOutput(ctx, nil, "ls-remote", "--exit-code", "origin", ws.CloneTarget)241if err != nil || len(gitout) == 0 {242log.WithField("remoteURI", ws.RemoteURI).WithField("branch", ws.CloneTarget).Warnf("Invalid default branch name. Changing to %v", defaultBranch)243ws.CloneTarget = defaultBranch244}245246// No need to prune here because we fetch the specific branch only. If we were to try and fetch everything,247// we might end up trying to fetch at tag/branch which has since been recreated. It's exactly the specific248// fetch wich prevents this situation.249//250// We don't recurse submodules because callers realizeCloneTarget() are expected to update submodules explicitly,251// and deal with any error appropriately (i.e. emit a warning rather than fail).252fetchArgs := []string{"--depth=1", "origin", "--recurse-submodules=no", ws.CloneTarget}253isShallow := ws.isShallowRepository(ctx)254if !isShallow {255fetchArgs = []string{"origin", "--recurse-submodules=no", ws.CloneTarget}256}257if err := ws.Git(ctx, "fetch", fetchArgs...); err != nil {258log.WithError(err).WithField("isShallow", isShallow).WithField("remoteURI", ws.RemoteURI).WithField("branch", ws.CloneTarget).Error("Cannot fetch remote branch")259return err260}261262if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", "-B", branchName, "origin/"+ws.CloneTarget); err != nil {263log.WithError(err).WithField("remoteURI", ws.RemoteURI).WithField("branch", branchName).Error("Cannot fetch remote branch")264return err265}266case LocalBranch:267// checkout local branch based on remote HEAD268if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", "-B", ws.CloneTarget, "origin/HEAD", "--no-track"); err != nil {269return err270}271case RemoteCommit:272// We did a shallow clone before, hence need to fetch the commit we are about to check out.273// Because we don't want to make the "git fetch" mechanism in supervisor more complicated,274// we'll just fetch the 20 commits right away.275if err := ws.Git(ctx, "fetch", "origin", ws.CloneTarget, "--depth=20"); err != nil {276return err277}278279// checkout specific commit280if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", ws.CloneTarget); err != nil {281return err282}283default:284// update to remote HEAD285if _, err := ws.GitWithOutput(ctx, nil, "reset", "--hard", "origin/HEAD"); err != nil {286var giterr git.OpFailedError287if errors.As(err, &giterr) && strings.Contains(giterr.Output, "unknown revision or path not in the working tree") {288// 'git reset --hard origin/HEAD' returns a non-zero exit code if origin does not have a single commit (empty repository).289// In this case that's not an error though, hence we don't want to fail here.290} else {291return err292}293}294}295return nil296}297298func checkGitStatus(err error) error {299if err != nil {300if strings.Contains(err.Error(), "The requested URL returned error: 524") {301return fmt.Errorf("Git clone returned HTTP status 524 (see https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/8475). Please try restarting your workspace")302}303}304305return err306}307308309