git.go

  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}