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