-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathgit.go
245 lines (210 loc) · 8.52 KB
/
git.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.
package initializer
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/cenkalti/backoff"
"github.com/opentracing/opentracing-go"
"golang.org/x/xerrors"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/common-go/process"
"github.com/gitpod-io/gitpod/common-go/tracing"
csapi "github.com/gitpod-io/gitpod/content-service/api"
"github.com/gitpod-io/gitpod/content-service/pkg/archive"
"github.com/gitpod-io/gitpod/content-service/pkg/git"
)
// CloneTargetMode is the target state in which we want to leave a GitInitializer
type CloneTargetMode string
const (
// RemoteHead has the local WS point at the remote branch head
RemoteHead CloneTargetMode = "head"
// RemoteCommit has the local WS point at a specific commit
RemoteCommit CloneTargetMode = "commit"
// RemoteBranch has the local WS point at a remote branch
RemoteBranch CloneTargetMode = "remote-branch"
// LocalBranch creates a local branch in the workspace
LocalBranch CloneTargetMode = "local-branch"
)
// GitInitializer is a local workspace with a Git connection
type GitInitializer struct {
git.Client
// The target mode determines what gets checked out
TargetMode CloneTargetMode
// The value for the clone target mode - use depends on the target mode
CloneTarget string
// If true, the Git initializer will chown(gitpod) after the clone
Chown bool
}
// Run initializes the workspace using Git
func (ws *GitInitializer) Run(ctx context.Context, mappings []archive.IDMapping) (src csapi.WorkspaceInitSource, err error) {
isGitWS := git.IsWorkingCopy(ws.Location)
//nolint:ineffassign
span, ctx := opentracing.StartSpanFromContext(ctx, "GitInitializer.Run")
span.SetTag("isGitWS", isGitWS)
defer tracing.FinishSpan(span, &err)
src = csapi.WorkspaceInitFromOther
if isGitWS {
log.WithField("stage", "init").WithField("location", ws.Location).Info("Not running git clone. Workspace is already a Git workspace")
return
}
gitClone := func() error {
if err := os.MkdirAll(ws.Location, 0775); err != nil {
log.WithError(err).WithField("location", ws.Location).Error("cannot create directory")
return err
}
log.WithField("stage", "init").WithField("location", ws.Location).Debug("Running git clone on workspace")
err = ws.Clone(ctx)
if err != nil {
if strings.Contains(err.Error(), "Access denied") {
err = &backoff.PermanentError{
Err: fmt.Errorf("Access denied. Please check that Gitpod was given permission to access the repository"),
}
}
return err
}
err = ws.Git(ctx, "config", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*")
if err != nil {
log.WithError(err).WithField("location", ws.Location).Error("cannot configure fecth behavior")
}
err = ws.Git(ctx, "config", "--replace-all", "checkout.defaultRemote", "origin")
if err != nil {
log.WithError(err).WithField("location", ws.Location).Error("cannot configure checkout defaultRemote")
}
return nil
}
onGitCloneFailure := func(e error, d time.Duration) {
if err := os.RemoveAll(ws.Location); err != nil {
log.
WithField("stage", "init").
WithField("location", ws.Location).
WithError(err).
Error("Cleaning workspace location failed.")
}
log.
WithField("stage", "init").
WithField("location", ws.Location).
WithField("sleepTime", d).
WithError(e).
Debugf("Running git clone on workspace failed. Retrying in %s ...", d)
}
b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 5 * time.Minute
if err = backoff.RetryNotify(gitClone, b, onGitCloneFailure); err != nil {
err = checkGitStatus(err)
return src, xerrors.Errorf("git initializer gitClone: %w", err)
}
defer func() {
span.SetTag("Chown", ws.Chown)
if !ws.Chown {
return
}
// TODO (aledbf): refactor to remove the need of manual chown
args := []string{"-R", "-L", "gitpod", ws.Location}
cmd := exec.Command("chown", args...)
res, cerr := cmd.CombinedOutput()
if cerr != nil && !process.IsNotChildProcess(cerr) {
err = git.OpFailedError{
Args: args,
ExecErr: cerr,
Output: string(res),
Subcommand: "chown",
}
return
}
}()
if err := ws.realizeCloneTarget(ctx); err != nil {
return src, xerrors.Errorf("git initializer clone: %w", err)
}
if err := ws.UpdateRemote(ctx); err != nil {
return src, xerrors.Errorf("git initializer updateRemote: %w", err)
}
if err := ws.UpdateSubmodules(ctx); err != nil {
log.WithError(err).Warn("error while updating submodules - continuing")
}
log.WithField("stage", "init").WithField("location", ws.Location).Debug("Git operations complete")
return
}
// realizeCloneTarget ensures the clone target is checked out
func (ws *GitInitializer) realizeCloneTarget(ctx context.Context) (err error) {
//nolint:ineffassign
span, ctx := opentracing.StartSpanFromContext(ctx, "realizeCloneTarget")
span.SetTag("remoteURI", ws.RemoteURI)
span.SetTag("cloneTarget", ws.CloneTarget)
span.SetTag("targetMode", ws.TargetMode)
defer tracing.FinishSpan(span, &err)
defer func() {
err = checkGitStatus(err)
}()
// checkout branch
switch ws.TargetMode {
case RemoteBranch:
// confirm the value of the default branch name using rev-parse
gitout, _ := ws.GitWithOutput(ctx, nil, "rev-parse", "--abbrev-ref", "origin/HEAD")
defaultBranch := strings.TrimSpace(strings.Replace(string(gitout), "origin/", "", -1))
branchName := ws.CloneTarget
// we already cloned the git repository but we need to check CloneTarget exists
// to avoid calling fetch from a non-existing branch
gitout, err := ws.GitWithOutput(ctx, nil, "ls-remote", "--exit-code", "origin", ws.CloneTarget)
if err != nil || len(gitout) == 0 {
log.WithField("remoteURI", ws.RemoteURI).WithField("branch", ws.CloneTarget).Warnf("Invalid default branch name. Changing to %v", defaultBranch)
ws.CloneTarget = defaultBranch
}
// No need to prune here because we fetch the specific branch only. If we were to try and fetch everything,
// we might end up trying to fetch at tag/branch which has since been recreated. It's exactly the specific
// fetch wich prevents this situation.
//
// We don't recurse submodules because callers realizeCloneTarget() are expected to update submodules explicitly,
// and deal with any error appropriately (i.e. emit a warning rather than fail).
if err := ws.Git(ctx, "fetch", "--depth=1", "origin", "--recurse-submodules=no", ws.CloneTarget); err != nil {
log.WithError(err).WithField("remoteURI", ws.RemoteURI).WithField("branch", ws.CloneTarget).Error("Cannot fetch remote branch")
return err
}
if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", "-B", branchName, "origin/"+ws.CloneTarget); err != nil {
log.WithError(err).WithField("remoteURI", ws.RemoteURI).WithField("branch", branchName).Error("Cannot fetch remote branch")
return err
}
case LocalBranch:
// checkout local branch based on remote HEAD
if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", "-B", ws.CloneTarget, "origin/HEAD", "--no-track"); err != nil {
return err
}
case RemoteCommit:
// We did a shallow clone before, hence need to fetch the commit we are about to check out.
// Because we don't want to make the "git fetch" mechanism in supervisor more complicated,
// we'll just fetch the 20 commits right away.
if err := ws.Git(ctx, "fetch", "origin", ws.CloneTarget, "--depth=20"); err != nil {
return err
}
// checkout specific commit
if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", ws.CloneTarget); err != nil {
return err
}
default:
// update to remote HEAD
if _, err := ws.GitWithOutput(ctx, nil, "reset", "--hard", "origin/HEAD"); err != nil {
var giterr git.OpFailedError
if errors.As(err, &giterr) && strings.Contains(giterr.Output, "unknown revision or path not in the working tree") {
// 'git reset --hard origin/HEAD' returns a non-zero exit code if origin does not have a single commit (empty repository).
// In this case that's not an error though, hence we don't want to fail here.
} else {
return err
}
}
}
return nil
}
func checkGitStatus(err error) error {
if err != nil {
if strings.Contains(err.Error(), "The requested URL returned error: 524") {
return fmt.Errorf("Git clone returned HTTP status 524 (see https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/8475). Please try restarting your workspace")
}
}
return err
}