-
Notifications
You must be signed in to change notification settings - Fork 2
/
shared.go
219 lines (177 loc) · 4.95 KB
/
shared.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
package slugcmplr
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra"
)
// BuildpackReference is a reference to a buildpack, containing its raw URL and
// Name.
type BuildpackReference struct {
Name string
URL string
}
// Outputter mimics the interface implemented by *cobra.Command to inject
// custom Stdout and Stderr streams, while also allowing control over the
// verbosity of output.
type Outputter interface {
OutOrStdout() io.Writer
ErrOrStderr() io.Writer
IsVerbose() bool
}
// OutputterFromCmd builds an Outputter based on a *cobra.Command.
func OutputterFromCmd(cmd *cobra.Command, verbose bool) Outputter {
return &StdOutputter{
Err: cmd.ErrOrStderr(),
Out: cmd.OutOrStdout(),
Verbose: verbose,
}
}
// StdOutputter is an Outputter that will default to os.Stdout and os.Stderr.
type StdOutputter struct {
Out io.Writer
Err io.Writer
Verbose bool
}
// IsVerbose returnes whether the StdOutputter is in Verbose mode.
func (o *StdOutputter) IsVerbose() bool {
return o.Verbose
}
// OutOrStdout returns either the configured Out, or os.Stdout if it is nil.
func (o *StdOutputter) OutOrStdout() io.Writer {
if o.Out == nil {
return os.Stdout
}
return o.Out
}
// ErrOrStderr returns either the configured Err, or os.Stderr if it is nil.
func (o *StdOutputter) ErrOrStderr() io.Writer {
if o.Err == nil {
return os.Stdout
}
return o.Err
}
// Commit attempts to return the current resolved HEAD commit for the git
// repository at dir.
func Commit(dir string) (string, error) {
r, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
return "", fmt.Errorf("error opening git directory: %w", err)
}
hsh, err := r.ResolveRevision(plumbing.Revision("HEAD"))
if err != nil {
return "", fmt.Errorf("error resolving HEAD revision: %w", err)
}
return hsh.String(), nil
}
// Tarball represents a GZipped Tar file.
type Tarball struct {
Path string
Checksum string
}
// Targz will walk srcDirPath recursively and write the corresponding GZipped Tar
// Archive to the given writers.
func Targz(srcDirPath, dstDirPath string) (*Tarball, error) {
f, err := os.Create(dstDirPath)
if err != nil {
return nil, fmt.Errorf("failed to create tarfile: %w", err)
}
defer f.Close() // nolint:errcheck
sha := sha256.New()
mw := io.MultiWriter(sha, f)
gzw := gzip.NewWriter(mw)
defer gzw.Close() // nolint:errcheck
tw := tar.NewWriter(gzw)
defer tw.Close() // nolint:errcheck
walk := func(file string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return fmt.Errorf("file moved or removed while building tarball: %w", err)
}
header, err := buildHeader(srcDirPath, file, info)
if err != nil {
return err
}
if err := tw.WriteHeader(header); err != nil {
return err
}
// Only write a body for regular files.
if !info.Mode().IsRegular() {
return nil
}
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // nolint:errcheck
if _, err := io.Copy(tw, f); err != nil {
return err
}
return f.Close()
}
if err := filepath.WalkDir(srcDirPath, walk); err != nil {
return nil, fmt.Errorf("error walking directory: %w", err)
}
// explicitly close to ensure we flush to archive and sha, make sure we get
// a correct checksum.
if err := tw.Close(); err != nil {
return nil, err
}
if err := gzw.Close(); err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
return &Tarball{
Path: dstDirPath,
Checksum: fmt.Sprintf("SHA256:%v", hex.EncodeToString(sha.Sum(nil))),
}, nil
}
func buildHeader(srcDirPath, file string, info fs.FileInfo) (*tar.Header, error) {
fmode := info.Mode()
if !(fmode.IsDir() || fmode.IsRegular() || isSymlink(fmode)) {
return nil, fmt.Errorf("unsupported filemode in archive: %v", file)
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return nil, fmt.Errorf("failed to infer header: %w", err)
}
// Heroku requires GNU Tar format (at least for slugs, maybe not for build sources?)
//
// https://devcenter.heroku.com/articles/platform-api-deploying-slugs#create-slug-archive
header.Format = tar.FormatGNU
relativePath, err := filepath.Rel(srcDirPath, file)
if err != nil {
return nil, fmt.Errorf("error getting relative path: %w", err)
}
// prefix all paths in this archive with ./app, as required by Heroku.
header.Name = "./app/" + relativePath
// Append a trailing / for directories.
if info.IsDir() {
header.Name += "/"
}
// Set the linkname for symbolic links.
if isSymlink(fmode) {
link, err := os.Readlink(file)
if err != nil {
return nil, fmt.Errorf("failed to readlink: %w", err)
}
header.Linkname = link
}
return header, nil
}
func isSymlink(fm fs.FileMode) bool {
return (fm & fs.ModeSymlink) != 0
}