1package server
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "log"
8 "os"
9 "path/filepath"
10
11 "github.com/charmbracelet/soft-serve/git"
12 "github.com/go-git/go-git/v5/plumbing/format/pktline"
13)
14
15var (
16
17 // ErrNotAuthed represents unauthorized access.
18 ErrNotAuthed = errors.New("you are not authorized to do this")
19
20 // ErrSystemMalfunction represents a general system error returned to clients.
21 ErrSystemMalfunction = errors.New("something went wrong")
22
23 // ErrInvalidRepo represents an attempt to access a non-existent repo.
24 ErrInvalidRepo = errors.New("invalid repo")
25
26 // ErrInvalidRequest represents an invalid request.
27 ErrInvalidRequest = errors.New("invalid request")
28
29 // ErrMaxConnections represents a maximum connection limit being reached.
30 ErrMaxConnections = errors.New("too many connections, try again later")
31
32 // ErrTimeout is returned when the maximum read timeout is exceeded.
33 ErrTimeout = errors.New("I/O timeout reached")
34)
35
36// Git protocol commands.
37const (
38 ReceivePackBin = "git-receive-pack"
39 UploadPackBin = "git-upload-pack"
40 UploadArchiveBin = "git-upload-archive"
41)
42
43// UploadPack runs the git upload-pack protocol against the provided repo.
44func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
45 exists, err := fileExists(repoDir)
46 if !exists {
47 return ErrInvalidRepo
48 }
49 if err != nil {
50 return err
51 }
52 return RunGit(in, out, er, "", UploadPackBin[4:], repoDir)
53}
54
55// UploadArchive runs the git upload-archive protocol against the provided repo.
56func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
57 exists, err := fileExists(repoDir)
58 if !exists {
59 return ErrInvalidRepo
60 }
61 if err != nil {
62 return err
63 }
64 return RunGit(in, out, er, "", UploadArchiveBin[4:], repoDir)
65}
66
67// ReceivePack runs the git receive-pack protocol against the provided repo.
68func ReceivePack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
69 if err := ensureRepo(repoDir, ""); err != nil {
70 return err
71 }
72 if err := RunGit(in, out, er, "", ReceivePackBin[4:], repoDir); err != nil {
73 return err
74 }
75 return ensureDefaultBranch(in, out, er, repoDir)
76}
77
78// RunGit runs a git command in the given repo.
79func RunGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...string) error {
80 c := git.NewCommand(args...)
81 return c.RunInDirWithOptions(dir, git.RunInDirOptions{
82 Stdin: in,
83 Stdout: out,
84 Stderr: err,
85 })
86}
87
88// WritePktline encodes and writes a pktline to the given writer.
89func WritePktline(w io.Writer, v ...interface{}) {
90 msg := fmt.Sprintln(v...)
91 pkt := pktline.NewEncoder(w)
92 if err := pkt.EncodeString(msg); err != nil {
93 log.Printf("git: error writing pkt-line message: %s", err)
94 }
95 if err := pkt.Flush(); err != nil {
96 log.Printf("git: error flushing pkt-line message: %s", err)
97 }
98}
99
100func fileExists(path string) (bool, error) {
101 _, err := os.Stat(path)
102 if err == nil {
103 return true, nil
104 }
105 if os.IsNotExist(err) {
106 return false, nil
107 }
108 return true, err
109}
110
111func ensureRepo(dir string, repo string) error {
112 exists, err := fileExists(dir)
113 if err != nil {
114 return err
115 }
116 if !exists {
117 err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0700))
118 if err != nil {
119 return err
120 }
121 }
122 rp := filepath.Join(dir, repo)
123 exists, err = fileExists(rp)
124 if err != nil {
125 return err
126 }
127 // FIXME: use backend.CreateRepository
128 if !exists {
129 _, err := git.Init(rp, true)
130 if err != nil {
131 return err
132 }
133 }
134 return nil
135}
136
137func ensureDefaultBranch(in io.Reader, out io.Writer, er io.Writer, repoPath string) error {
138 r, err := git.Open(repoPath)
139 if err != nil {
140 return err
141 }
142 brs, err := r.Branches()
143 if err != nil {
144 return err
145 }
146 if len(brs) == 0 {
147 return fmt.Errorf("no branches found")
148 }
149 // Rename the default branch to the first branch available
150 _, err = r.HEAD()
151 if err == git.ErrReferenceNotExist {
152 // FIXME: use backend.SetDefaultBranch
153 err = RunGit(in, out, er, repoPath, "branch", "-M", brs[0])
154 if err != nil {
155 return err
156 }
157 }
158 if err != nil && err != git.ErrReferenceNotExist {
159 return err
160 }
161 return nil
162}