Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/component/module/git/internal/vcs/git.go
4096 views
1
package vcs
2
3
import (
4
"context"
5
"errors"
6
"io"
7
"os"
8
"path/filepath"
9
10
"github.com/go-git/go-git/v5"
11
"github.com/go-git/go-git/v5/plumbing"
12
)
13
14
type GitRepoOptions struct {
15
Repository string
16
Revision string
17
}
18
19
// GitRepo manages a Git repository for the purposes of retrieving a file from
20
// it.
21
type GitRepo struct {
22
opts GitRepoOptions
23
repo *git.Repository
24
workTree *git.Worktree
25
}
26
27
// NewGitRepo creates a new instance of a GitRepo, where the Git repository is
28
// managed at storagePath.
29
//
30
// If storagePath is empty on disk, NewGitRepo initializes GitRepo by cloning
31
// the repository. Otherwise, NewGitRepo will do a fetch.
32
//
33
// After GitRepo is initialized, it checks out to the Revision specified in
34
// GitRepoOptions.
35
func NewGitRepo(ctx context.Context, storagePath string, opts GitRepoOptions) (*GitRepo, error) {
36
var (
37
repo *git.Repository
38
err error
39
)
40
41
if !isRepoCloned(storagePath) {
42
repo, err = git.PlainCloneContext(ctx, storagePath, false, &git.CloneOptions{
43
URL: opts.Repository,
44
ReferenceName: plumbing.HEAD,
45
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
46
Tags: git.AllTags,
47
})
48
} else {
49
repo, err = git.PlainOpen(storagePath)
50
}
51
if err != nil {
52
return nil, DownloadFailedError{
53
Repository: opts.Repository,
54
Inner: err,
55
}
56
}
57
58
// Fetch the latest contents. This may be a no-op if we just did a clone.
59
err = repo.FetchContext(ctx, &git.FetchOptions{
60
RemoteName: "origin",
61
Force: true,
62
})
63
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
64
return nil, UpdateFailedError{
65
Repository: opts.Repository,
66
Inner: err,
67
}
68
}
69
70
// Finally, hard reset to our requested revision.
71
hash, err := findRevision(opts.Revision, repo)
72
if err != nil {
73
return nil, InvalidRevisionError{Revision: opts.Revision}
74
}
75
76
workTree, err := repo.Worktree()
77
if err != nil {
78
return nil, err
79
}
80
err = workTree.Reset(&git.ResetOptions{
81
Commit: hash,
82
Mode: git.HardReset,
83
})
84
if err != nil {
85
return nil, err
86
}
87
88
return &GitRepo{
89
opts: opts,
90
repo: repo,
91
workTree: workTree,
92
}, nil
93
}
94
95
func isRepoCloned(dir string) bool {
96
fi, dirError := os.ReadDir(filepath.Join(dir, git.GitDirName))
97
return dirError == nil && len(fi) > 0
98
}
99
100
// Update updates the repository by fetching new content and re-checking out to
101
// latest version of Revision.
102
func (repo *GitRepo) Update(ctx context.Context) error {
103
err := repo.repo.FetchContext(ctx, &git.FetchOptions{
104
RemoteName: "origin",
105
Force: true,
106
})
107
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
108
return UpdateFailedError{
109
Repository: repo.opts.Repository,
110
Inner: err,
111
}
112
}
113
114
// Find the latest revision being requested and hard-reset to it.
115
hash, err := findRevision(repo.opts.Revision, repo.repo)
116
if err != nil {
117
return InvalidRevisionError{Revision: repo.opts.Revision}
118
}
119
120
err = repo.workTree.Reset(&git.ResetOptions{
121
Commit: hash,
122
Mode: git.HardReset,
123
})
124
if err != nil {
125
return err
126
}
127
128
return nil
129
}
130
131
// ReadFile returns a file from the repository specified by path.
132
func (repo *GitRepo) ReadFile(path string) ([]byte, error) {
133
f, err := repo.workTree.Filesystem.Open(path)
134
if err != nil {
135
return nil, err
136
}
137
defer f.Close()
138
139
return io.ReadAll(f)
140
}
141
142
// CurrentRevision returns the current revision of the repository (by SHA).
143
func (repo *GitRepo) CurrentRevision() (string, error) {
144
ref, err := repo.repo.Head()
145
if err != nil {
146
return "", err
147
}
148
return ref.Hash().String(), nil
149
}
150
151
func findRevision(rev string, repo *git.Repository) (plumbing.Hash, error) {
152
// Try looking for the revision in the following order:
153
//
154
// 1. Search by tag name.
155
// 2. Search by remote ref name.
156
// 3. Try to resolve the revision directly.
157
158
if tagRef, err := repo.Tag(rev); err == nil {
159
return tagRef.Hash(), nil
160
}
161
162
if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", rev), true); err == nil {
163
return remoteRef.Hash(), nil
164
}
165
166
if hash, err := repo.ResolveRevision(plumbing.Revision(rev)); err == nil {
167
return *hash, nil
168
}
169
170
return plumbing.ZeroHash, plumbing.ErrReferenceNotFound
171
}
172
173