all repos

mugit @ 01d1381

🐮 git server that your cow will love

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

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