-
Notifications
You must be signed in to change notification settings - Fork 37
/
gitClient.go
152 lines (129 loc) · 3.86 KB
/
gitClient.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
package git
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
)
// Client allows for retrieving data from git repo
type Client interface {
GetManifestFile(repository, commitHash, path string) ([]byte, error)
}
type gitSvc interface {
PlainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error)
PlainOpen(path string) (*git.Repository, error)
Fetch(r *git.Repository, o *git.FetchOptions) error
Worktree(r *git.Repository) (*git.Worktree, error)
Checkout(w *git.Worktree, opts *git.CheckoutOptions) error
}
type gitSvcImpl struct{}
func (g gitSvcImpl) PlainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error) {
return git.PlainClone(path, isBare, o)
}
func (g gitSvcImpl) PlainOpen(path string) (*git.Repository, error) {
return git.PlainOpen(path)
}
func (g gitSvcImpl) Fetch(r *git.Repository, o *git.FetchOptions) error {
return r.Fetch(o)
}
func (g gitSvcImpl) Worktree(r *git.Repository) (*git.Worktree, error) {
return r.Worktree()
}
func (g gitSvcImpl) Checkout(w *git.Worktree, opts *git.CheckoutOptions) error {
return w.Checkout(opts)
}
// BasicClient connects to git using ssh
type BasicClient struct {
auth transport.AuthMethod
mu *sync.Mutex
git gitSvc
fs fs.FS
baseDir string // base directory to run git operations from
}
// NewSSHBasicClient creates a new ssh based git client
func NewSSHBasicClient(sshPemFile string) (BasicClient, error) {
auth, err := ssh.NewPublicKeysFromFile("git", sshPemFile, "")
if err != nil {
return BasicClient{}, err
}
return BasicClient{
auth: auth,
mu: &sync.Mutex{},
git: gitSvcImpl{},
fs: os.DirFS(os.TempDir()),
baseDir: os.TempDir(),
}, nil
}
// NewHTTPSBasicClient creates a new https based git client
func NewHTTPSBasicClient(user, pass string) (BasicClient, error) {
return BasicClient{
auth: &http.BasicAuth{
Username: user,
Password: pass,
},
mu: &sync.Mutex{},
git: gitSvcImpl{},
fs: os.DirFS(os.TempDir()),
baseDir: os.TempDir(),
}, nil
}
func (g BasicClient) GetManifestFile(repository, commitHash, path string) ([]byte, error) {
// filePath should only be used for git calls. direct fs calls should use repository directly
repPath := strings.ReplaceAll(repository, "/", "")
filePath := filepath.Join(g.baseDir, repPath)
// Locking here since we need to make sure nobody else is using the repo at the same time to ensure the right sha is checked out
// TODO: use a lock per repository instead of a single global lock
g.mu.Lock()
defer g.mu.Unlock()
var repo *git.Repository
if _, err := fs.Stat(g.fs, repPath); os.IsNotExist(err) {
// TODO: use context version and make depth configurable
repo, err = g.git.PlainClone(filePath, false, &git.CloneOptions{
URL: repository,
Auth: g.auth,
Progress: os.Stdout,
})
if err != nil {
return []byte{}, err
}
} else {
repo, err = g.git.PlainOpen(filePath)
if err != nil {
return []byte{}, err
}
err = g.git.Fetch(repo, &git.FetchOptions{
Progress: os.Stdout,
Auth: g.auth,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return []byte{}, err
}
}
w, err := g.git.Worktree(repo)
if err != nil {
return []byte{}, err
}
err = g.git.Checkout(w, &git.CheckoutOptions{
Hash: plumbing.NewHash(commitHash),
})
if err != nil {
return []byte{}, err
}
pathToManifest := filepath.Join(repPath, path)
fileStat, err := fs.Stat(g.fs, pathToManifest)
if err != nil {
return []byte{}, err
}
if fileStat.IsDir() {
return []byte{}, fmt.Errorf("path provided is not a file '%s'", path)
}
return fs.ReadFile(g.fs, pathToManifest)
}