all repos

mugit @ b6e1830

🐮 git server that your cow will love

mugit/internal/git/repo.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
git: fix fetching for empty repos, 3 months ago
1
package git
2
3
import (
4
	"errors"
5
	"fmt"
6
	"path/filepath"
7
	"strings"
8
	"time"
9
10
	"github.com/go-git/go-git/v5"
11
	"github.com/go-git/go-git/v5/plumbing"
12
	"github.com/go-git/go-git/v5/plumbing/object"
13
	"github.com/go-git/go-git/v5/plumbing/transport"
14
	"github.com/go-git/go-git/v5/plumbing/transport/http"
15
)
16
17
// Thanks https://git.icyphox.sh/legit/blob/master/git/git.go
18
19
var (
20
	ErrEmptyRepo    = errors.New("repository has no commits")
21
	ErrFileNotFound = errors.New("file not found")
22
)
23
24
type Repo struct {
25
	path string
26
	r    *git.Repository
27
	h    plumbing.Hash
28
}
29
30
// Open opens a git repository at path. If ref is empty, HEAD is used.
31
func Open(path string, ref string) (*Repo, error) {
32
	var err error
33
	g := Repo{}
34
	g.path = path
35
	g.r, err = git.PlainOpen(path)
36
	if err != nil {
37
		return nil, fmt.Errorf("opening %s: %w", path, err)
38
	}
39
40
	if ref == "" {
41
		head, err := g.r.Head()
42
		if err != nil {
43
			if errors.Is(err, plumbing.ErrReferenceNotFound) {
44
				return &g, nil
45
			}
46
			return nil, fmt.Errorf("getting head of %s: %w", path, err)
47
		}
48
		g.h = head.Hash()
49
	} else {
50
		hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
51
		if err != nil {
52
			return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
53
		}
54
		g.h = *hash
55
	}
56
	return &g, nil
57
}
58
59
func (g *Repo) IsEmpty() bool {
60
	return g.h == plumbing.ZeroHash
61
}
62
63
// Init initializes a bare repo in path.
64
func Init(path string) error {
65
	if _, err := git.PlainInit(path, true); err != nil {
66
		return fmt.Errorf("failed to initialize repo: %w", err)
67
	}
68
	return nil
69
}
70
71
func (g *Repo) Name() string {
72
	name := filepath.Base(g.path)
73
	return strings.TrimSuffix(name, ".git")
74
}
75
76
type Commit struct {
77
	Message     string
78
	AuthorEmail string
79
	AuthorName  string
80
	ChangeID    string
81
	Committed   time.Time
82
	Hash        string
83
	HashShort   string
84
}
85
86
func newShortHash(h plumbing.Hash) string { return h.String()[:7] }
87
func newCommit(c *object.Commit) *Commit {
88
	var changeID string
89
	for _, header := range c.ExtraHeaders {
90
		if header.Key == "change-id" {
91
			changeID = header.Value
92
			break
93
		}
94
	}
95
96
	return &Commit{
97
		Message:     c.Message,
98
		AuthorEmail: c.Author.Email,
99
		AuthorName:  c.Author.Name,
100
		ChangeID:    changeID,
101
		Committed:   c.Committer.When,
102
		Hash:        c.Hash.String(),
103
		HashShort:   newShortHash(c.Hash),
104
	}
105
}
106
107
func (g *Repo) Commits() ([]*Commit, error) {
108
	if g.IsEmpty() {
109
		return []*Commit{}, nil
110
	}
111
112
	ci, err := g.r.Log(&git.LogOptions{
113
		From:  g.h,
114
		Order: git.LogOrderCommitterTime,
115
	})
116
	if err != nil {
117
		return nil, fmt.Errorf("commits from ref: %w", err)
118
	}
119
120
	var commits []*Commit
121
	ci.ForEach(func(c *object.Commit) error {
122
		commits = append(commits, newCommit(c))
123
		return nil
124
	})
125
126
	return commits, nil
127
}
128
129
func (g *Repo) LastCommit() (*Commit, error) {
130
	if g.IsEmpty() {
131
		return &Commit{}, nil
132
	}
133
134
	c, err := g.r.CommitObject(g.h)
135
	if err != nil {
136
		return nil, fmt.Errorf("last commit: %w", err)
137
	}
138
139
	return newCommit(c), nil
140
}
141
142
func (g *Repo) FileContent(path string) (string, error) {
143
	c, err := g.r.CommitObject(g.h)
144
	if err != nil {
145
		return "", fmt.Errorf("commit object: %w", err)
146
	}
147
148
	tree, err := c.Tree()
149
	if err != nil {
150
		return "", fmt.Errorf("file tree: %w", err)
151
	}
152
153
	file, err := tree.File(path)
154
	if err != nil {
155
		if errors.Is(err, object.ErrFileNotFound) {
156
			return "", ErrFileNotFound
157
		}
158
		return "", err
159
	}
160
161
	isbin, _ := file.IsBinary()
162
	if !isbin {
163
		return file.Contents()
164
	} else {
165
		return "Not displaying binary file", nil
166
	}
167
}
168
169
type Branch struct{ Name string }
170
171
func (g *Repo) Branches() ([]*Branch, error) {
172
	bi, err := g.r.Branches()
173
	if err != nil {
174
		return nil, fmt.Errorf("branch: %w", err)
175
	}
176
177
	var branches []*Branch
178
	err = bi.ForEach(func(r *plumbing.Reference) error {
179
		branches = append(branches, &Branch{
180
			Name: r.Name().Short(),
181
		})
182
		return nil
183
	})
184
	return branches, err
185
}
186
187
func (g *Repo) IsGoMod() bool {
188
	_, err := g.FileContent("go.mod")
189
	return err == nil
190
}
191
192
func (g *Repo) FindMasterBranch(masters []string) (string, error) {
193
	if g.IsEmpty() {
194
		return "", ErrEmptyRepo
195
	}
196
197
	for _, b := range masters {
198
		if _, err := g.r.ResolveRevision(plumbing.Revision(b)); err == nil {
199
			return b, nil
200
		}
201
	}
202
	return "", fmt.Errorf("unable to find master branch")
203
}
204
205
func (g *Repo) Fetch() error { return g.fetch(nil) }
206
207
func (g *Repo) FetchFromGithubWithToken(token string) error {
208
	return g.fetch(&http.BasicAuth{
209
		Username: "x-access-token", // this can be anything but empty
210
		Password: token,
211
	})
212
}
213
214
func (g *Repo) fetch(auth transport.AuthMethod) error {
215
	rmt, err := g.r.Remote(originRemote)
216
	if err != nil {
217
		return fmt.Errorf("failed to get remote: %w", err)
218
	}
219
220
	if err = rmt.Fetch(&git.FetchOptions{
221
		Auth:  auth,
222
		Tags:  git.AllTags,
223
		Prune: true,
224
		Force: true,
225
	}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
226
		return fmt.Errorf("failed to fetch: %w", err)
227
	}
228
229
	// for some reason fetch doesn't change head for empty repos
230
	if !g.IsEmpty() {
231
		return nil
232
	}
233
234
	refs, err := rmt.List(&git.ListOptions{Auth: auth})
235
	if err != nil {
236
		return fmt.Errorf("failed to list references: %w", err)
237
	}
238
239
	for _, ref := range refs {
240
		if ref.Name() == plumbing.HEAD {
241
			if err := g.r.Storer.SetReference(
242
				plumbing.NewSymbolicReference(plumbing.HEAD, ref.Target()),
243
			); err != nil {
244
				return fmt.Errorf("failed to set HEAD: %w", err)
245
			}
246
			break
247
		}
248
	}
249
250
	return nil
251
}