0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
|
// Copyright (C) 2023, Jakob Wakeling
// All rights reserved.
package goit
import (
"bytes"
"compress/gzip"
"io"
"log"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
)
type gitCommand struct {
prog string
args []string
Dir string
env []string
}
func HandleInfoRefs(w http.ResponseWriter, r *http.Request) {
service := r.FormValue("service")
repo := gitHttpBase(w, r, service)
if repo == nil {
return
}
c := NewGitCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", "--advertise-refs", ".")
c.AddEnv(os.Environ()...)
c.AddEnv("GIT_PROTOCOL=version=2")
c.Dir = RepoPath(repo.Name, true)
refs, _, err := c.Run(nil, nil)
if err != nil {
log.Println("[Git HTTP]", err.Error())
HttpError(w, http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/x-"+service+"-advertisement")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
w.WriteHeader(http.StatusOK)
w.Write(pktLine("# service=" + service + "\n"))
w.Write(pktFlush())
w.Write(refs)
}
func HandleUploadPack(w http.ResponseWriter, r *http.Request) {
const service = "git-upload-pack"
repo := gitHttpBase(w, r, service)
if repo == nil {
return
}
gitHttpRpc(w, r, service, repo)
}
func HandleReceivePack(w http.ResponseWriter, r *http.Request) {
const service = "git-receive-pack"
repo := gitHttpBase(w, r, service)
if repo == nil {
return
}
gitHttpRpc(w, r, service, repo)
}
func gitHttpBase(w http.ResponseWriter, r *http.Request, service string) *Repo {
reponame := chi.URLParam(r, "repo")
/* Check that the Git service and protocol version are supported */
if service != "git-upload-pack" && service != "git-receive-pack" {
w.WriteHeader(http.StatusForbidden)
return nil
}
if service == "git-upload-pack" && r.Header.Get("Git-Protocol") != "version=2" {
w.WriteHeader(http.StatusForbidden)
return nil
}
/* Load the repository from the database */
repo, err := GetRepoByName(reponame)
if err != nil {
log.Println("[Git HTTP]", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return nil
}
/* Require authentication other than for public pull */
if repo == nil || repo.Visibility != Public || service == "git-receive-pack" {
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", "Basic realm=\"git\"")
w.WriteHeader(http.StatusUnauthorized)
return nil
}
user, err := GetUserByName(username)
if err != nil {
log.Println("[Git HTTP]", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return nil
}
/* If the user doesn't exist or has invalid credentials */
if user == nil || !bytes.Equal(Hash(password, user.Salt), user.Pass) {
w.Header().Set("WWW-Authenticate", "Basic realm=\"git\"")
w.WriteHeader(http.StatusUnauthorized)
return nil
}
/* If the repo doesn't exist or is private and not owned by the user */
if repo == nil || (repo.Visibility == Private && user.Id != repo.OwnerId) {
w.WriteHeader(http.StatusNotFound)
return nil
}
}
if repo == nil {
w.WriteHeader(http.StatusNotFound)
return nil
}
return repo
}
func gitHttpRpc(w http.ResponseWriter, r *http.Request, service string, repo *Repo) {
defer func() {
if err := r.Body.Close(); err != nil {
log.Println("[Git RPC]", err.Error())
}
}()
if r.Header.Get("Content-Type") != "application/x-"+service+"-request" {
log.Println("[Git RPC]", "Content-Type mismatch")
HttpError(w, http.StatusUnauthorized)
return
}
body := r.Body
if r.Header.Get("Content-Encoding") == "gzip" {
if b, err := gzip.NewReader(r.Body); err != nil {
log.Println("[Git RPC]", err.Error())
HttpError(w, http.StatusInternalServerError)
return
} else {
body = b
}
}
c := NewGitCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", ".")
c.AddEnv(os.Environ()...)
c.Dir = RepoPath(repo.Name, true)
if p := r.Header.Get("Git-Protocol"); p == "version=2" {
c.AddEnv("GIT_PROTOCOL=version=2")
}
w.Header().Add("Content-Type", "application/x-"+service+"-result")
w.WriteHeader(http.StatusOK)
if _, _, err := c.Run(body, w); err != nil {
log.Println("[Git RPC]", err.Error())
HttpError(w, http.StatusInternalServerError)
return
}
}
func pktLine(str string) []byte {
s := strconv.FormatUint(uint64(len(str)+4), 16)
s = strings.Repeat("0", 4-len(s)%4) + s
return []byte(s + str)
}
func pktFlush() []byte { return []byte("0000") }
func NewGitCommand(args ...string) *gitCommand {
return &gitCommand{prog: "git", args: args}
}
func (C *gitCommand) AddEnv(env ...string) {
C.env = append(C.env, env...)
}
func (C *gitCommand) Run(in io.Reader, out io.Writer) ([]byte, []byte, error) {
c := exec.Command(C.prog, C.args...)
c.Dir = C.Dir
c.Env = C.env
c.Stdin = in
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
c.Stdout = stdout
c.Stderr = os.Stderr
if out != nil {
c.Stdout = out
}
if err := c.Run(); err != nil {
return nil, stderr.Bytes(), err
}
return stdout.Bytes(), stderr.Bytes(), nil
}
type DiffStat struct {
Name, Prev string
Status string
Addition int
Deletion int
IsBinary bool
}
var diffs = map[plumbing.Hash][]DiffStat{}
var diffsLock sync.RWMutex
var Sizes = map[plumbing.Hash]uint64{}
var SizesLock sync.RWMutex
func DiffStats(c *object.Commit) ([]DiffStat, error) {
diffsLock.RLock()
if stats, ok := diffs[c.Hash]; ok {
diffsLock.RUnlock()
return stats, nil
}
diffsLock.RUnlock()
from, err := c.Tree()
if err != nil {
return nil, err
}
to := &object.Tree{}
if c.NumParents() != 0 {
parent, err := c.Parents().Next()
if err != nil {
return nil, err
}
to, err = parent.Tree()
if err != nil {
return nil, err
}
}
patch, err := to.Patch(from)
if err != nil {
return nil, err
}
var stats []DiffStat
for _, fp := range patch.FilePatches() {
var stat DiffStat
if len(fp.Chunks()) == 0 {
if !fp.IsBinary() {
continue
}
stat.IsBinary = true
}
from, to := fp.Files()
if from == nil && to == nil {
continue
} else if from == nil /* Added */ {
stat.Name = to.Path()
stat.Status = "A"
} else if to == nil /* Deleted */ {
stat.Name = from.Path()
stat.Status = "D"
} else if from.Path() != to.Path() /* Renamed */ {
stat.Name = to.Path()
stat.Prev = from.Path()
stat.Status = "R"
} else {
stat.Name = from.Path()
stat.Status = "M"
}
for _, chunk := range fp.Chunks() {
s := chunk.Content()
if len(s) == 0 {
continue
}
switch chunk.Type() {
case diff.Add:
stat.Addition += strings.Count(s, "\n")
if s[len(s)-1] != '\n' {
stat.Addition++
}
case diff.Delete:
stat.Deletion += strings.Count(s, "\n")
if s[len(s)-1] != '\n' {
stat.Deletion++
}
}
}
stats = append(stats, stat)
}
diffsLock.Lock()
diffs[c.Hash] = stats
diffsLock.Unlock()
return stats, nil
}
type countPair struct {
hash plumbing.Hash
count uint64
}
var counts = map[string]countPair{}
var countsLock sync.RWMutex
func CommitCount(repo, branch string, hash plumbing.Hash) (uint64, error) {
countsLock.RLock()
if count, ok := counts[repo+"/"+branch]; ok && count.hash == hash {
countsLock.RUnlock()
return count.count, nil
}
countsLock.RUnlock()
c := NewGitCommand("rev-list", "--count", branch)
c.Dir = RepoPath(repo, true)
out, _, err := c.Run(nil, nil)
if err != nil {
return 0, err
}
count, err := strconv.ParseUint(strings.TrimSpace(string(out)), 10, 64)
if err != nil {
return 0, err
}
countsLock.Lock()
counts[repo+"/"+branch] = countPair{hash, count}
countsLock.Unlock()
return count, nil
}
func GetFileType(file *object.File) (string, error) {
rc, err := file.Blob.Reader()
if err != nil {
return "", err
}
defer rc.Close()
buf := make([]byte, min(file.Size, 512))
if _, err := rc.Read(buf); err != nil {
return "", err
}
return http.DetectContentType(buf), nil
}
|