-
Notifications
You must be signed in to change notification settings - Fork 32
/
git.go
177 lines (156 loc) · 5.17 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
// Copyright 2020 GM Cruise LLC
//
// 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.
package dep
import (
"errors"
"fmt"
"os/exec"
"path/filepath"
"strings"
"text/template"
log "github.com/golang/glog"
"go.starlark.net/starlark"
"github.com/cruise-automation/isopod/pkg/loader"
)
const (
// NameKey is the name of the git repo target.
NameKey = "name"
// RemoteKey is the remote address of this git repo.
RemoteKey = "remote"
// CommitKey is the full commit SHA of the source to download.
CommitKey = "commit"
)
var (
// asserts *GitRepo implements starlark.HasAttrs interface.
_ starlark.HasAttrs = (*GitRepo)(nil)
// asserts *GitRepo implements loader.Dependency interface.
_ loader.Dependency = (*GitRepo)(nil)
// RequiredFields is the list of required fields to initialize a GitRepo target.
RequiredFields = []string{NameKey, RemoteKey, CommitKey}
)
// GitRepo represents Isopod module source as remote git repo.
type GitRepo struct {
*AbstractDependency
name, remote, commit string
}
// NewGitRepoBuiltin creates a new git_repository built-in.
func NewGitRepoBuiltin() *starlark.Builtin {
return starlark.NewBuiltin(
"git_repository",
func(t *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
absDep, err := NewAbstractDependency("git_repository", RequiredFields, kwargs)
if err != nil {
return nil, err
}
name, remote, commit, err := nameRemoteCommit(absDep)
if err != nil {
return nil, fmt.Errorf("cannot read params: %v", err)
}
gitRepo := &GitRepo{absDep, name, remote, commit}
loader.Register(gitRepo)
return gitRepo, nil
},
)
}
// Name returns the name of this git repo target.
func (g *GitRepo) Name() string {
return g.name
}
// Version returns the version of this git repo target.
func (g *GitRepo) Version() string {
return g.commit
}
// LocalDir returns the path to the directory storing the source.
func (g *GitRepo) LocalDir() string {
return filepath.Join(Workspace, g.name, g.commit)
}
// Fetch is part of the Dependency interface.
// It downloads the source of this dependency.
func (g *GitRepo) Fetch() error {
script, err := gitCloneShellScript(&GitCloneParams{
OutputDir: g.LocalDir(),
GitRemoteURL: g.remote,
GitCommitSHA: g.commit,
})
if err != nil {
return err
}
if _, err := Shellf(script); err != nil {
return fmt.Errorf("failed to clone git repo `%v': %v", g.name, err)
}
return nil
}
func nameRemoteCommit(absDep *AbstractDependency) (name, remote, commit string, err error) {
if name, err = stringFromValue(absDep.Attrs[NameKey]); err != nil {
return
}
if remote, err = stringFromValue(absDep.Attrs[RemoteKey]); err != nil {
return
}
if commit, err = stringFromValue(absDep.Attrs[CommitKey]); err != nil {
return
}
return
}
func stringFromValue(v starlark.Value) (string, error) {
if v == nil {
return "", errors.New("nil value")
}
s, ok := v.(starlark.String)
if !ok {
return "", fmt.Errorf("%v is not a starlark string (got a `%s')", v, v.Type())
}
return string(s), nil
}
// Shellf execute the given shell command and wait until it finishes. Then
// return the combined stdout and stderr, and error if any.
func Shellf(format string, a ...interface{}) (string, error) {
s := fmt.Sprintf(format, a...)
log.V(1).Infof("Executing shell command: %v\n", s)
bytes, err := exec.Command("sh", "-c", s).CombinedOutput()
log.V(1).Infof("Shell command `%s' finished:\n%s", s, string(bytes))
return string(bytes), err
}
// GitCloneParams is used to templatize git clone command.
type GitCloneParams struct {
OutputDir, GitRemoteURL, GitCommitSHA string
}
// gitCloneBashScript composes a shell script to clone a repo at a given commit sha.
func gitCloneShellScript(params *GitCloneParams) (string, error) {
script := `
set -e
if [ -d "{{.OutputDir}}" ]; then
# {{.OutputDir}} already exists, meaning dependency version unchanged.
exit 0
fi
mkdir -p "{{.OutputDir}}"
cd "{{.OutputDir}}"
git init
git remote add origin "{{.GitRemoteURL}}"
# Try to fetch just the specified commit first, which only works if there is a ref
# pointing at this commit. This is true for commits that were just pushed.
# Otherwise, fetch the entire repo history, which supports checking out arbituary commits.
(git fetch origin "{{.GitCommitSHA}}" && git reset --hard FETCH_HEAD) || \
(git fetch origin && git checkout "{{.GitCommitSHA}}")
`
tpl, err := template.New("script").Parse(script)
if err != nil {
return "", fmt.Errorf("failed to parse git-clone shell script template: %v", err)
}
var sb strings.Builder
if err = tpl.Execute(&sb, params); err != nil {
return "", fmt.Errorf("failed to render git-clone shell script template: %v", err)
}
return sb.String(), nil
}