-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
working_dir.go
163 lines (142 loc) · 6.28 KB
/
working_dir.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
// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package events
import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
)
const workingDirPrefix = "repos"
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_working_dir.go WorkingDir
// WorkingDir handles the workspace on disk for running commands.
type WorkingDir interface {
// Clone git clones headRepo, checks out the branch and then returns the
// absolute path to the root of the cloned repo.
Clone(log *logging.SimpleLogger, baseRepo models.Repo, headRepo models.Repo, p models.PullRequest, workspace string) (string, error)
// GetWorkingDir returns the path to the workspace for this repo and pull.
// If workspace does not exist on disk, error will be of type os.IsNotExist.
GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error)
GetPullDir(r models.Repo, p models.PullRequest) (string, error)
// Delete deletes the workspace for this repo and pull.
Delete(r models.Repo, p models.PullRequest) error
DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) error
}
// FileWorkspace implements WorkingDir with the file system.
type FileWorkspace struct {
DataDir string
// TestingOverrideCloneURL can be used during testing to override the URL
// that is cloned. If it's empty then we clone normally.
TestingOverrideCloneURL string
}
// Clone git clones headRepo, checks out the branch and then returns the absolute
// path to the root of the cloned repo. If the repo already exists and is at
// the right commit it does nothing. This is to support running commands in
// multiple dirs of the same repo without deleting existing plans.
func (w *FileWorkspace) Clone(
log *logging.SimpleLogger,
baseRepo models.Repo,
headRepo models.Repo,
p models.PullRequest,
workspace string) (string, error) {
cloneDir := w.cloneDir(baseRepo, p, workspace)
// If the directory already exists, check if it's at the right commit.
// If so, then we do nothing.
if _, err := os.Stat(cloneDir); err == nil {
log.Debug("clone directory %q already exists, checking if it's at the right commit", cloneDir)
revParseCmd := exec.Command("git", "rev-parse", "HEAD") // #nosec
revParseCmd.Dir = cloneDir
output, err := revParseCmd.CombinedOutput()
if err != nil {
log.Err("will re-clone repo, could not determine if was at correct commit: git rev-parse HEAD: %s: %s", err, string(output))
return w.forceClone(log, cloneDir, headRepo, p)
}
currCommit := strings.Trim(string(output), "\n")
// We're prefix matching here because BitBucket doesn't give us the full
// commit, only a 12 character prefix.
if strings.HasPrefix(currCommit, p.HeadCommit) {
log.Debug("repo is at correct commit %q so will not re-clone", p.HeadCommit)
return cloneDir, nil
}
log.Debug("repo was already cloned but is not at correct commit, wanted %q got %q", p.HeadCommit, currCommit)
// We'll fall through to re-clone.
}
// Otherwise we clone the repo.
return w.forceClone(log, cloneDir, headRepo, p)
}
func (w *FileWorkspace) forceClone(log *logging.SimpleLogger,
cloneDir string,
headRepo models.Repo,
p models.PullRequest) (string, error) {
err := os.RemoveAll(cloneDir)
if err != nil {
return "", errors.Wrapf(err, "deleting dir %q before cloning", cloneDir)
}
// Create the directory and parents if necessary.
log.Info("creating dir %q", cloneDir)
if err := os.MkdirAll(cloneDir, 0700); err != nil {
return "", errors.Wrap(err, "creating new workspace")
}
log.Info("git cloning %q into %q", headRepo.SanitizedCloneURL, cloneDir)
cloneURL := headRepo.CloneURL
if w.TestingOverrideCloneURL != "" {
cloneURL = w.TestingOverrideCloneURL
}
cloneCmd := exec.Command("git", "clone", cloneURL, cloneDir) // #nosec
if output, err := cloneCmd.CombinedOutput(); err != nil {
return "", errors.Wrapf(err, "cloning %s: %s", headRepo.SanitizedCloneURL, string(output))
}
// Check out the branch for this PR.
log.Info("checking out branch %q", p.Branch)
checkoutCmd := exec.Command("git", "checkout", p.Branch) // #nosec
checkoutCmd.Dir = cloneDir
if err := checkoutCmd.Run(); err != nil {
return "", errors.Wrapf(err, "checking out branch %s", p.Branch)
}
return cloneDir, nil
}
// GetWorkingDir returns the path to the workspace for this repo and pull.
func (w *FileWorkspace) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) {
repoDir := w.cloneDir(r, p, workspace)
if _, err := os.Stat(repoDir); err != nil {
return "", errors.Wrap(err, "checking if workspace exists")
}
return repoDir, nil
}
// GetPullDir returns the dir where the workspaces for this pull are cloned.
// If the dir doesn't exist it will return an error.
func (w *FileWorkspace) GetPullDir(r models.Repo, p models.PullRequest) (string, error) {
dir := w.repoPullDir(r, p)
if _, err := os.Stat(dir); err != nil {
return "", err
}
return dir, nil
}
// Delete deletes the workspace for this repo and pull.
func (w *FileWorkspace) Delete(r models.Repo, p models.PullRequest) error {
return os.RemoveAll(w.repoPullDir(r, p))
}
// DeleteForWorkspace deletes the working dir for this workspace.
func (w *FileWorkspace) DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) error {
return os.RemoveAll(w.cloneDir(r, p, workspace))
}
func (w *FileWorkspace) repoPullDir(r models.Repo, p models.PullRequest) string {
return filepath.Join(w.DataDir, workingDirPrefix, r.FullName, strconv.Itoa(p.Num))
}
func (w *FileWorkspace) cloneDir(r models.Repo, p models.PullRequest, workspace string) string {
return filepath.Join(w.repoPullDir(r, p), workspace)
}