Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/content-service/pkg/initializer/git.go
2499 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package initializer
6
7
import (
8
"context"
9
"errors"
10
"fmt"
11
"os"
12
"os/exec"
13
"strconv"
14
"strings"
15
"time"
16
17
"github.com/cenkalti/backoff"
18
"github.com/opentracing/opentracing-go"
19
"golang.org/x/xerrors"
20
21
"github.com/gitpod-io/gitpod/common-go/log"
22
"github.com/gitpod-io/gitpod/common-go/process"
23
"github.com/gitpod-io/gitpod/common-go/tracing"
24
csapi "github.com/gitpod-io/gitpod/content-service/api"
25
"github.com/gitpod-io/gitpod/content-service/pkg/archive"
26
"github.com/gitpod-io/gitpod/content-service/pkg/git"
27
)
28
29
// CloneTargetMode is the target state in which we want to leave a GitInitializer
30
type CloneTargetMode string
31
32
const (
33
// RemoteHead has the local WS point at the remote branch head
34
RemoteHead CloneTargetMode = "head"
35
36
// RemoteCommit has the local WS point at a specific commit
37
RemoteCommit CloneTargetMode = "commit"
38
39
// RemoteBranch has the local WS point at a remote branch
40
RemoteBranch CloneTargetMode = "remote-branch"
41
42
// LocalBranch creates a local branch in the workspace
43
LocalBranch CloneTargetMode = "local-branch"
44
)
45
46
// GitInitializer is a local workspace with a Git connection
47
type GitInitializer struct {
48
git.Client
49
50
// The target mode determines what gets checked out
51
TargetMode CloneTargetMode
52
53
// The value for the clone target mode - use depends on the target mode
54
CloneTarget string
55
56
// If true, the Git initializer will chown(gitpod) after the clone
57
Chown bool
58
}
59
60
// Run initializes the workspace using Git
61
func (ws *GitInitializer) Run(ctx context.Context, mappings []archive.IDMapping) (src csapi.WorkspaceInitSource, stats csapi.InitializerMetrics, err error) {
62
isGitWS := git.IsWorkingCopy(ws.Location)
63
//nolint:ineffassign
64
span, ctx := opentracing.StartSpanFromContext(ctx, "GitInitializer.Run")
65
span.SetTag("isGitWS", isGitWS)
66
defer tracing.FinishSpan(span, &err)
67
start := time.Now()
68
initialSize, fsErr := getFsUsage()
69
if fsErr != nil {
70
log.WithError(fsErr).Error("could not get disk usage")
71
}
72
73
src = csapi.WorkspaceInitFromOther
74
if isGitWS {
75
log.WithField("stage", "init").WithField("location", ws.Location).Info("Not running git clone. Workspace is already a Git workspace")
76
return
77
}
78
79
gitClone := func() error {
80
if err := os.MkdirAll(ws.Location, 0775); err != nil {
81
log.WithError(err).WithField("location", ws.Location).Error("cannot create directory")
82
return err
83
}
84
85
// make sure that folder itself is owned by gitpod user prior to doing git clone
86
// this is needed as otherwise git clone will fail if the folder is owned by root
87
if ws.RunAsGitpodUser {
88
args := []string{"gitpod", ws.Location}
89
cmd := exec.Command("chown", args...)
90
res, cerr := cmd.CombinedOutput()
91
if cerr != nil && !process.IsNotChildProcess(cerr) {
92
err = git.OpFailedError{
93
Args: args,
94
ExecErr: cerr,
95
Output: string(res),
96
Subcommand: "chown",
97
}
98
return err
99
}
100
}
101
102
log.WithField("stage", "init").WithField("location", ws.Location).Debug("Running git clone on workspace")
103
err = ws.Clone(ctx)
104
if err != nil {
105
if strings.Contains(err.Error(), "Access denied") {
106
err = &backoff.PermanentError{
107
Err: fmt.Errorf("Access denied. Please check that Gitpod was given permission to access the repository"),
108
}
109
}
110
111
return err
112
}
113
114
// we can only do `git config` stuffs after having a directory that is also git init'd
115
// commit-graph after every git fetch command that downloads a pack-file from a remote
116
err = ws.Git(ctx, "config", "fetch.writeCommitGraph", "true")
117
if err != nil {
118
log.WithError(err).WithField("location", ws.Location).Error("cannot configure fetch.writeCommitGraph")
119
}
120
121
err = ws.Git(ctx, "config", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*")
122
if err != nil {
123
log.WithError(err).WithField("location", ws.Location).Error("cannot configure fetch behavior")
124
}
125
126
err = ws.Git(ctx, "config", "--replace-all", "checkout.defaultRemote", "origin")
127
if err != nil {
128
log.WithError(err).WithField("location", ws.Location).Error("cannot configure checkout defaultRemote")
129
}
130
131
return nil
132
}
133
onGitCloneFailure := func(e error, d time.Duration) {
134
if err := os.RemoveAll(ws.Location); err != nil {
135
log.
136
WithField("stage", "init").
137
WithField("location", ws.Location).
138
WithError(err).
139
Error("Cleaning workspace location failed.")
140
}
141
log.
142
WithField("stage", "init").
143
WithField("location", ws.Location).
144
WithField("sleepTime", d).
145
WithError(e).
146
Debugf("Running git clone on workspace failed. Retrying in %s ...", d)
147
}
148
149
b := backoff.NewExponentialBackOff()
150
b.MaxElapsedTime = 5 * time.Minute
151
if err = backoff.RetryNotify(gitClone, b, onGitCloneFailure); err != nil {
152
err = checkGitStatus(err)
153
return src, nil, xerrors.Errorf("git initializer gitClone: %w", err)
154
}
155
156
defer func() {
157
span.SetTag("Chown", ws.Chown)
158
if !ws.Chown {
159
return
160
}
161
// TODO (aledbf): refactor to remove the need of manual chown
162
args := []string{"-R", "-L", "gitpod", ws.Location}
163
cmd := exec.Command("chown", args...)
164
res, cerr := cmd.CombinedOutput()
165
if cerr != nil && !process.IsNotChildProcess(cerr) {
166
err = git.OpFailedError{
167
Args: args,
168
ExecErr: cerr,
169
Output: string(res),
170
Subcommand: "chown",
171
}
172
return
173
}
174
}()
175
176
if err := ws.realizeCloneTarget(ctx); err != nil {
177
return src, nil, xerrors.Errorf("git initializer clone: %w", err)
178
}
179
if err := ws.UpdateRemote(ctx); err != nil {
180
return src, nil, xerrors.Errorf("git initializer updateRemote: %w", err)
181
}
182
if err := ws.UpdateSubmodules(ctx); err != nil {
183
log.WithError(err).Warn("error while updating submodules - continuing")
184
}
185
186
log.WithField("stage", "init").WithField("location", ws.Location).Debug("Git operations complete")
187
188
if fsErr == nil {
189
currentSize, fsErr := getFsUsage()
190
if fsErr != nil {
191
log.WithError(fsErr).Error("could not get disk usage")
192
}
193
194
stats = csapi.InitializerMetrics{csapi.InitializerMetric{
195
Type: "git",
196
Duration: time.Since(start),
197
Size: currentSize - initialSize,
198
}}
199
}
200
return
201
}
202
203
func (ws *GitInitializer) isShallowRepository(ctx context.Context) bool {
204
out, err := ws.GitWithOutput(ctx, nil, "rev-parse", "--is-shallow-repository")
205
if err != nil {
206
log.WithError(err).Error("unexpected error checking if git repository is shallow")
207
return true
208
}
209
isShallow, err := strconv.ParseBool(strings.TrimSpace(string(out)))
210
if err != nil {
211
log.WithError(err).WithField("input", string(out)).Error("unexpected error parsing bool")
212
return true
213
}
214
return isShallow
215
}
216
217
// realizeCloneTarget ensures the clone target is checked out
218
func (ws *GitInitializer) realizeCloneTarget(ctx context.Context) (err error) {
219
//nolint:ineffassign
220
span, ctx := opentracing.StartSpanFromContext(ctx, "realizeCloneTarget")
221
span.SetTag("remoteURI", ws.RemoteURI)
222
span.SetTag("cloneTarget", ws.CloneTarget)
223
span.SetTag("targetMode", ws.TargetMode)
224
defer tracing.FinishSpan(span, &err)
225
226
defer func() {
227
err = checkGitStatus(err)
228
}()
229
230
// checkout branch
231
switch ws.TargetMode {
232
case RemoteBranch:
233
// confirm the value of the default branch name using rev-parse
234
gitout, _ := ws.GitWithOutput(ctx, nil, "rev-parse", "--abbrev-ref", "origin/HEAD")
235
defaultBranch := strings.TrimSpace(strings.Replace(string(gitout), "origin/", "", -1))
236
237
branchName := ws.CloneTarget
238
239
// we already cloned the git repository but we need to check CloneTarget exists
240
// to avoid calling fetch from a non-existing branch
241
gitout, err := ws.GitWithOutput(ctx, nil, "ls-remote", "--exit-code", "origin", ws.CloneTarget)
242
if err != nil || len(gitout) == 0 {
243
log.WithField("remoteURI", ws.RemoteURI).WithField("branch", ws.CloneTarget).Warnf("Invalid default branch name. Changing to %v", defaultBranch)
244
ws.CloneTarget = defaultBranch
245
}
246
247
// No need to prune here because we fetch the specific branch only. If we were to try and fetch everything,
248
// we might end up trying to fetch at tag/branch which has since been recreated. It's exactly the specific
249
// fetch wich prevents this situation.
250
//
251
// We don't recurse submodules because callers realizeCloneTarget() are expected to update submodules explicitly,
252
// and deal with any error appropriately (i.e. emit a warning rather than fail).
253
fetchArgs := []string{"--depth=1", "origin", "--recurse-submodules=no", ws.CloneTarget}
254
isShallow := ws.isShallowRepository(ctx)
255
if !isShallow {
256
fetchArgs = []string{"origin", "--recurse-submodules=no", ws.CloneTarget}
257
}
258
if err := ws.Git(ctx, "fetch", fetchArgs...); err != nil {
259
log.WithError(err).WithField("isShallow", isShallow).WithField("remoteURI", ws.RemoteURI).WithField("branch", ws.CloneTarget).Error("Cannot fetch remote branch")
260
return err
261
}
262
263
if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", "-B", branchName, "origin/"+ws.CloneTarget); err != nil {
264
log.WithError(err).WithField("remoteURI", ws.RemoteURI).WithField("branch", branchName).Error("Cannot fetch remote branch")
265
return err
266
}
267
case LocalBranch:
268
// checkout local branch based on remote HEAD
269
if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", "-B", ws.CloneTarget, "origin/HEAD", "--no-track"); err != nil {
270
return err
271
}
272
case RemoteCommit:
273
// We did a shallow clone before, hence need to fetch the commit we are about to check out.
274
// Because we don't want to make the "git fetch" mechanism in supervisor more complicated,
275
// we'll just fetch the 20 commits right away.
276
if err := ws.Git(ctx, "fetch", "origin", ws.CloneTarget, "--depth=20"); err != nil {
277
return err
278
}
279
280
// checkout specific commit
281
if err := ws.Git(ctx, "-c", "core.hooksPath=/dev/null", "checkout", ws.CloneTarget); err != nil {
282
return err
283
}
284
default:
285
// update to remote HEAD
286
if _, err := ws.GitWithOutput(ctx, nil, "reset", "--hard", "origin/HEAD"); err != nil {
287
var giterr git.OpFailedError
288
if errors.As(err, &giterr) && strings.Contains(giterr.Output, "unknown revision or path not in the working tree") {
289
// 'git reset --hard origin/HEAD' returns a non-zero exit code if origin does not have a single commit (empty repository).
290
// In this case that's not an error though, hence we don't want to fail here.
291
} else {
292
return err
293
}
294
}
295
}
296
return nil
297
}
298
299
func checkGitStatus(err error) error {
300
if err != nil {
301
if strings.Contains(err.Error(), "The requested URL returned error: 524") {
302
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")
303
}
304
}
305
306
return err
307
}
308
309