b6041efb6bceedfa89bc26d0671a32d01c268e6c
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/git/repo.go b/git/repo.go
2index b6d95b56d75c1cd6f18e6224b12d8223ff79daae..9dd674032cdab567f48a64092386e78f5ac327c0 100644
3--- a/git/repo.go
4+++ b/git/repo.go
5@@ -2,6 +2,7 @@ package git
6
7 import (
8 "path/filepath"
9+ "strings"
10
11 "github.com/gogs/git-module"
12 )
13@@ -29,6 +30,10 @@ func Clone(src, dst string, opts ...git.CloneOptions) error {
14
15 // Init initializes and opens a new git repository.
16 func Init(path string, bare bool) (*Repository, error) {
17+ if bare {
18+ path = strings.TrimSuffix(path, ".git") + ".git"
19+ }
20+
21 err := git.Init(path, git.InitOptions{Bare: bare})
22 if err != nil {
23 return nil, err
24diff --git a/server/backend/access.go b/server/backend/access.go
25new file mode 100644
26index 0000000000000000000000000000000000000000..52cc16eff8357c46fa84d18bed37336ba81ded82
27--- /dev/null
28+++ b/server/backend/access.go
29@@ -0,0 +1,42 @@
30+package backend
31+
32+import "golang.org/x/crypto/ssh"
33+
34+// AccessLevel is the level of access allowed to a repo.
35+type AccessLevel int
36+
37+const (
38+ // NoAccess does not allow access to the repo.
39+ NoAccess AccessLevel = iota
40+
41+ // ReadOnlyAccess allows read-only access to the repo.
42+ ReadOnlyAccess
43+
44+ // ReadWriteAccess allows read and write access to the repo.
45+ ReadWriteAccess
46+
47+ // AdminAccess allows read, write, and admin access to the repo.
48+ AdminAccess
49+)
50+
51+// String returns the string representation of the access level.
52+func (a AccessLevel) String() string {
53+ switch a {
54+ case NoAccess:
55+ return "no-access"
56+ case ReadOnlyAccess:
57+ return "read-only"
58+ case ReadWriteAccess:
59+ return "read-write"
60+ case AdminAccess:
61+ return "admin-access"
62+ default:
63+ return "unknown"
64+ }
65+}
66+
67+// AccessMethod is an interface that handles repository authorization.
68+type AccessMethod interface {
69+ // AccessLevel returns the access level for the given repository and key.
70+ AccessLevel(repo string, pk ssh.PublicKey) AccessLevel
71+}
72diff --git a/server/backend/backend.go b/server/backend/backend.go
73new file mode 100644
74index 0000000000000000000000000000000000000000..0bc7d6f62fe26e5bfb9c38e3a024bed57cff0a25
75--- /dev/null
76+++ b/server/backend/backend.go
77@@ -0,0 +1,84 @@
78+package backend
79+
80+import (
81+ "bytes"
82+
83+ "golang.org/x/crypto/ssh"
84+)
85+
86+// Backend is an interface that handles repositories management and any
87+// non-Git related operations.
88+type Backend interface {
89+ // ServerName returns the server's name.
90+ ServerName() string
91+ // SetServerName sets the server's name.
92+ SetServerName(name string) error
93+ // ServerHost returns the server's host.
94+ ServerHost() string
95+ // SetServerHost sets the server's host.
96+ SetServerHost(host string) error
97+ // ServerPort returns the server's port.
98+ ServerPort() string
99+ // SetServerPort sets the server's port.
100+ SetServerPort(port string) error
101+
102+ // AnonAccess returns the access level for anonymous users.
103+ AnonAccess() AccessLevel
104+ // SetAnonAccess sets the access level for anonymous users.
105+ SetAnonAccess(level AccessLevel) error
106+ // AllowKeyless returns true if keyless access is allowed.
107+ AllowKeyless() bool
108+ // SetAllowKeyless sets whether or not keyless access is allowed.
109+ SetAllowKeyless(allow bool) error
110+
111+ // Repository finds the given repository.
112+ Repository(repo string) (Repository, error)
113+ // Repositories returns a list of all repositories.
114+ Repositories() ([]Repository, error)
115+ // CreateRepository creates a new repository.
116+ CreateRepository(name string, private bool) (Repository, error)
117+ // DeleteRepository deletes a repository.
118+ DeleteRepository(name string) error
119+ // RenameRepository renames a repository.
120+ RenameRepository(oldName, newName string) error
121+ // DefaultBranch returns the repository's default branch.
122+ DefaultBranch(repo string) (string, error)
123+ // SetDefaultBranch sets the default branch for a repository.
124+ SetDefaultBranch(repo string, branch string) error
125+
126+ // Description returns the repo's description.
127+ Description(repo string) string
128+ // SetDescription sets the repo's description.
129+ SetDescription(repo, desc string) error
130+ // IsPrivate returns true if the repository is private.
131+ IsPrivate(repo string) bool
132+ // SetPrivate sets the repository's private status.
133+ SetPrivate(repo string, priv bool) error
134+
135+ // IsCollaborator returns true if the authorized key is a collaborator on the repository.
136+ IsCollaborator(pk ssh.PublicKey, repo string) bool
137+ // AddCollaborator adds the authorized key as a collaborator on the repository.
138+ AddCollaborator(pk ssh.PublicKey, repo string) error
139+ // IsAdmin returns true if the authorized key is an admin.
140+ IsAdmin(pk ssh.PublicKey) bool
141+ // AddAdmin adds the authorized key as an admin.
142+ AddAdmin(pk ssh.PublicKey) error
143+}
144+
145+// ParseAuthorizedKey parses an authorized key string into a public key.
146+func ParseAuthorizedKey(ak string) (ssh.PublicKey, string, error) {
147+ pk, c, _, _, err := ssh.ParseAuthorizedKey([]byte(ak))
148+ return pk, c, err
149+}
150+
151+// MarshalAuthorizedKey marshals a public key into an authorized key string.
152+//
153+// This is the inverse of ParseAuthorizedKey.
154+// This function is a copy of ssh.MarshalAuthorizedKey, but without the trailing newline.
155+// It returns an empty string if pk is nil.
156+func MarshalAuthorizedKey(pk ssh.PublicKey) string {
157+ if pk == nil {
158+ return ""
159+ }
160+ return string(bytes.TrimSuffix(ssh.MarshalAuthorizedKey(pk), []byte("\n")))
161+}
162diff --git a/server/backend/file/file.go b/server/backend/file/file.go
163new file mode 100644
164index 0000000000000000000000000000000000000000..9cdcd3e183e3214bb44ed90fc344c01f495f39ef
165--- /dev/null
166+++ b/server/backend/file/file.go
167@@ -0,0 +1,598 @@
168+// Package file implements a backend that uses the filesystem to store non-Git related data
169+//
170+// The following files and directories are used:
171+//
172+// - anon-access: contains the access level for anonymous users
173+// - allow-keyless: contains a boolean value indicating whether or not keyless access is allowed
174+// - admins: contains a list of authorized keys for admin users
175+// - host: contains the server's server hostname
176+// - name: contains the server's name
177+// - port: contains the server's port
178+// - repos: is a the directory containing all Git repositories
179+//
180+// Each repository has the following files and directories:
181+// - collaborators: contains a list of authorized keys for collaborators
182+// - description: contains the repository's description
183+// - private: when present, indicates that the repository is private
184+// - git-daemon-export-ok: when present, indicates that the repository is public
185+// - project-name: contains the repository's project name
186+package file
187+
188+import (
189+ "bufio"
190+ "errors"
191+ "fmt"
192+ "io"
193+ "io/fs"
194+ "os"
195+ "path/filepath"
196+ "strconv"
197+ "strings"
198+
199+ "github.com/charmbracelet/log"
200+ "github.com/charmbracelet/soft-serve/git"
201+ "github.com/charmbracelet/soft-serve/server/backend"
202+ "github.com/gliderlabs/ssh"
203+ gitm "github.com/gogs/git-module"
204+ gossh "golang.org/x/crypto/ssh"
205+)
206+
207+// sub file and directory names.
208+const (
209+ anonAccess = "anon-access"
210+ allowKeyless = "allow-keyless"
211+ admins = "admins"
212+ serverHost = "host"
213+ serverName = "name"
214+ serverPort = "port"
215+ repos = "repos"
216+ collabs = "collaborators"
217+ description = "description"
218+ private = "private"
219+)
220+
221+var (
222+ logger = log.WithPrefix("backend.file")
223+)
224+
225+var _ backend.Backend = &FileBackend{}
226+
227+var _ backend.AccessMethod = &FileBackend{}
228+
229+// FileBackend is a backend that uses the filesystem.
230+type FileBackend struct { // nolint:revive
231+ // path is the path to the directory containing the repositories and config
232+ // files.
233+ path string
234+ // AdditionalAdmins additional admins to the server.
235+ AdditionalAdmins []string
236+}
237+
238+func (fb *FileBackend) reposPath() string {
239+ return filepath.Join(fb.path, repos)
240+}
241+
242+func (fb *FileBackend) adminsPath() string {
243+ return filepath.Join(fb.path, admins)
244+}
245+
246+func (fb *FileBackend) collabsPath(repo string) string {
247+ return filepath.Join(fb.reposPath(), repo, collabs)
248+}
249+
250+func sanatizeRepo(repo string) string {
251+ return strings.TrimSuffix(repo, ".git")
252+}
253+
254+func readOneLine(path string) (string, error) {
255+ f, err := os.Open(path)
256+ if err != nil {
257+ return "", err
258+ }
259+ defer f.Close() // nolint:errcheck
260+ s := bufio.NewScanner(f)
261+ s.Scan()
262+ return s.Text(), s.Err()
263+}
264+
265+func readAll(path string) (string, error) {
266+ f, err := os.Open(path)
267diff --git a/server/backend/file/repo.go b/server/backend/file/repo.go
268new file mode 100644
269index 0000000000000000000000000000000000000000..6764f5fe3164fa5518712159b5f140d895c6bfa3
270--- /dev/null
271+++ b/server/backend/file/repo.go
272@@ -0,0 +1,56 @@
273+package file
274+
275+import (
276+ "errors"
277+ "os"
278+ "path/filepath"
279+ "strings"
280+
281+ "github.com/charmbracelet/soft-serve/git"
282+ "github.com/charmbracelet/soft-serve/server/backend"
283+)
284+
285+var _ backend.Repository = (*Repo)(nil)
286+
287+// Repo is a filesystem Git repository.
288+//
289+// It implemenets backend.Repository.
290+type Repo struct {
291+ path string
292+}
293+
294+// Name returns the repository's name.
295+//
296+// It implements backend.Repository.
297+func (r *Repo) Name() string {
298+ return strings.TrimSuffix(filepath.Base(r.path), ".git")
299+}
300+
301+// Description returns the repository's description.
302+//
303+// It implements backend.Repository.
304+func (r *Repo) Description() string {
305+ desc, err := readAll(r.path)
306+ if err != nil {
307+ logger.Debug("failed to read description file", "err", err,
308+ "path", filepath.Join(r.path, description))
309+ return ""
310+ }
311+
312+ return desc
313+}
314+
315+// IsPrivate returns whether the repository is private.
316+//
317+// It implements backend.Repository.
318+func (r *Repo) IsPrivate() bool {
319+ _, err := os.Stat(filepath.Join(r.path, private))
320+ return errors.Is(err, os.ErrExist)
321+}
322+
323+// Repository returns the underlying git.Repository.
324+//
325+// It implements backend.Repository.
326+func (r *Repo) Repository() (*git.Repository, error) {
327+ return git.Open(r.path)
328+}
329diff --git a/server/backend/repo.go b/server/backend/repo.go
330new file mode 100644
331index 0000000000000000000000000000000000000000..846134b83e9873ae0874ad5e9f702ab7b801a78b
332--- /dev/null
333+++ b/server/backend/repo.go
334@@ -0,0 +1,15 @@
335+package backend
336+
337+import "github.com/charmbracelet/soft-serve/git"
338+
339+// Repository is a Git repository interface.
340+type Repository interface {
341+ // Name returns the repository's name.
342+ Name() string
343+ // Description returns the repository's description.
344+ Description() string
345+ // IsPrivate returns whether the repository is private.
346+ IsPrivate() bool
347+ // Repository returns the underlying git.Repository.
348+ Repository() (*git.Repository, error)
349+}
350diff --git a/server/backend/utils.go b/server/backend/utils.go
351new file mode 100644
352index 0000000000000000000000000000000000000000..6f7d87b3239c3889607467e67d1e981713c0f5ab
353--- /dev/null
354+++ b/server/backend/utils.go
355@@ -0,0 +1,51 @@
356+package backend
357+
358+import (
359+ "path/filepath"
360+
361+ "github.com/charmbracelet/soft-serve/git"
362+ "github.com/gobwas/glob"
363+)
364+
365+// LatestFile returns the contents of the latest file at the specified path in
366+// the repository and its file path.
367+func LatestFile(r Repository, pattern string) (string, string, error) {
368+ g := glob.MustCompile(pattern)
369+ dir := filepath.Dir(pattern)
370+ repo, err := r.Repository()
371+ if err != nil {
372+ return "", "", err
373+ }
374+ head, err := repo.HEAD()
375+ if err != nil {
376+ return "", "", err
377+ }
378+ t, err := repo.TreePath(head, dir)
379+ if err != nil {
380+ return "", "", err
381+ }
382+ ents, err := t.Entries()
383+ if err != nil {
384+ return "", "", err
385+ }
386+ for _, e := range ents {
387+ fp := filepath.Join(dir, e.Name())
388+ if e.IsTree() {
389+ continue
390+ }
391+ if g.Match(fp) {
392+ bts, err := e.Contents()
393+ if err != nil {
394+ return "", "", err
395+ }
396+ return string(bts), fp, nil
397+ }
398+ }
399+ return "", "", git.ErrFileNotFound
400+}
401+
402+// Readme returns the repository's README.
403+func Readme(r Repository) (readme string, path string, err error) {
404+ readme, path, err = LatestFile(r, "README*")
405+ return
406+}