git.go

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