all repos

mugit @ 99ee247

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix: 404 on file not found, 4 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/http"
14
)
15
16
// Thanks https://git.icyphox.sh/legit/blob/master/git/git.go
17
18
var (
19
	ErrEmptyRepo    = errors.New("repository has no commits")
20
	ErrFileNotFound = errors.New("file not found")
21
)
22
23
type Repo struct {
24
	path string
25
	r    *git.Repository
26
	h    plumbing.Hash
27
}
28
29
// Open opens a git repository at path. If ref is empty, HEAD is used.
30
func Open(path string, ref string) (*Repo, error) {
31
	var err error
32
	g := Repo{}
33
	g.path = path
34
	g.r, err = git.PlainOpen(path)
35
	if err != nil {
36
		return nil, fmt.Errorf("opening %s: %w", path, err)
37
	}
38
39
	if ref == "" {
40
		head, err := g.r.Head()
41
		if err != nil {
42
			if errors.Is(err, plumbing.ErrReferenceNotFound) {
43
				return &g, nil
44
			}
45
			return nil, fmt.Errorf("getting head of %s: %w", path, err)
46
		}
47
		g.h = head.Hash()
48
	} else {
49
		hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
50
		if err != nil {
51
			return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
52
		}
53
		g.h = *hash
54
	}
55
	return &g, nil
56
}
57
58
func (g *Repo) IsEmpty() bool {
59
	return g.h == plumbing.ZeroHash
60
}
61
62
// Init initializes a bare repo in path.
63
func Init(path string) error {
64
	_, err := git.PlainInit(path, true)
65
	return err
66
}
67
68
func (g *Repo) Name() string {
69
	name := filepath.Base(g.path)
70
	return strings.TrimSuffix(name, ".git")
71
}
72
73
type Commit struct {
74
	Message     string
75
	Hash        string
76
	HashShort   string
77
	AuthorName  string
78
	AuthorEmail string
79
	Committed   time.Time
80
}
81
82
func (g *Repo) Commits() ([]*Commit, error) {
83
	if g.IsEmpty() {
84
		return []*Commit{}, nil
85
	}
86
87
	ci, err := g.r.Log(&git.LogOptions{
88
		From:  g.h,
89
		Order: git.LogOrderCommitterTime,
90
	})
91
	if err != nil {
92
		return nil, fmt.Errorf("commits from ref: %w", err)
93
	}
94
95
	var commits []*Commit
96
	ci.ForEach(func(c *object.Commit) error {
97
		commits = append(commits, &Commit{
98
			AuthorEmail: c.Author.Email,
99
			AuthorName:  c.Author.Name,
100
			Committed:   c.Author.When,
101
			Hash:        c.Hash.String(),
102
			HashShort:   c.Hash.String()[:8],
103
			Message:     c.Message,
104
		})
105
		return nil
106
	})
107
108
	return commits, nil
109
}
110
111
func (g *Repo) LastCommit() (*Commit, error) {
112
	if g.IsEmpty() {
113
		return nil, ErrEmptyRepo
114
	}
115
116
	c, err := g.r.CommitObject(g.h)
117
	if err != nil {
118
		return nil, fmt.Errorf("last commit: %w", err)
119
	}
120
121
	return &Commit{
122
		AuthorEmail: c.Author.Email,
123
		AuthorName:  c.Author.Name,
124
		Committed:   c.Author.When,
125
		Hash:        c.Hash.String(),
126
		HashShort:   c.Hash.String()[:8],
127
		Message:     c.Message,
128
	}, nil
129
}
130
131
func (g *Repo) FileContent(path string) (string, error) {
132
	c, err := g.r.CommitObject(g.h)
133
	if err != nil {
134
		return "", fmt.Errorf("commit object: %w", err)
135
	}
136
137
	tree, err := c.Tree()
138
	if err != nil {
139
		return "", fmt.Errorf("file tree: %w", err)
140
	}
141
142
	file, err := tree.File(path)
143
	if err != nil {
144
		if errors.Is(err, object.ErrFileNotFound) {
145
			return "", ErrFileNotFound
146
		}
147
		return "", err
148
	}
149
150
	isbin, _ := file.IsBinary()
151
	if !isbin {
152
		return file.Contents()
153
	} else {
154
		return "Not displaying binary file", nil
155
	}
156
}
157
158
type Branch struct{ Name string }
159
160
func (g *Repo) Branches() ([]*Branch, error) {
161
	bi, err := g.r.Branches()
162
	if err != nil {
163
		return nil, fmt.Errorf("branch: %w", err)
164
	}
165
166
	var branches []*Branch
167
	err = bi.ForEach(func(r *plumbing.Reference) error {
168
		branches = append(branches, &Branch{
169
			Name: r.Name().Short(),
170
		})
171
		return nil
172
	})
173
	return branches, err
174
}
175
176
func (g *Repo) IsGoMod() bool {
177
	_, err := g.FileContent("go.mod")
178
	return err == nil
179
}
180
181
func (g *Repo) FindMasterBranch(masters []string) (string, error) {
182
	if g.IsEmpty() {
183
		return "", ErrEmptyRepo
184
	}
185
186
	for _, b := range masters {
187
		if _, err := g.r.ResolveRevision(plumbing.Revision(b)); err == nil {
188
			return b, nil
189
		}
190
	}
191
	return "", fmt.Errorf("unable to find master branch")
192
}
193
194
func (g *Repo) Fetch() error { return g.fetch(nil) }
195
196
func (g *Repo) FetchFromGithubWithToken(token string) error {
197
	return g.fetch(&http.BasicAuth{
198
		Username: token,
199
		Password: "x-oauth-basic",
200
	})
201
}
202
203
func (g *Repo) fetch(auth http.AuthMethod) error {
204
	rmt, err := g.r.Remote(originRemote)
205
	if err != nil {
206
		return fmt.Errorf("failed to get remote: %w", err)
207
	}
208
209
	return rmt.Fetch(&git.FetchOptions{
210
		Auth:  auth,
211
		Tags:  git.AllTags,
212
		Prune: true,
213
		Force: true,
214
	})
215
}