Home

ugit @main - refs - log -
-
https://git.jolheiser.com/ugit.git
The code powering this h*ckin' site
tree log patch
feat: native git Signed-off-by: jolheiser <john.olheiser@gmail.com>
Signature
-----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEgqEQpE3xoo1QwJO/uFOtpdp7v3oFAmXXnP0ACgkQuFOtpdp7 v3onZhAAprlmN+qMVz0te/u4rs+TDhtgX0CY6q1LEcnWPzPbN77dyg2VFEXFconb 8PS17Lzki4R9KwRXB7KVpDL15mXqPHb7B+RbZcn7uJNm/1ro+zdvG9bE4UJWO/nf ChoTcWch8Ic3ow3B8u0uDWa87YCFgBHagesZZjm+jnQcztVpHSHE91u6BSkmf1Bm GoIs4hDHuwepodcoH6vRHTjpEW77zITxo9cqhSXgaeB6dYL6xntt2uDTdmQOfreE is97xAgbTjEEBJFj9/LqKEFOw7p3OEBOnJp14B1F5lzExS0hNDHAKIV3hLj+Na9Z 0greaIW4eCVqhAs7wIIDakRkEm1ey9kvjpfg9R35tBUsK0kkLqwJDzGInXqo8dbC jdFjssJeRcSR0pSD8w+qtqaCV8OG3FNmnxSJTm68uGEkNZgAlKL9+O4kuoZywZAp E5V3bwo7sJ32w+gHfj2Xrij0RY11AFaneG11p1oW0vdysP/KpNafzRwt+Cij7DL8 2AR+cjFoOMwGHZ2Hhidbkgloz45O5oUKMArU+y7Yle2MlzxjxuYfOMrbyLALyYa8 /RayX6gBEIITu9jI/l4ZkD0nhzsElT2mHNyDg5zSz4LmpyZRO/nhyFWzrgEzRBax 7bYCDaPg4pPW5z4fO2SDxV4m0UymOW/WhQaDgewobyDNpLRQN6A= =PkGb -----END PGP SIGNATURE-----
jolheiser <john.olheiser@gmail.com>
10 months ago
8 changed files, 343 additions(+), 186 deletions(-)
M cmd/ugitd/main.go -> cmd/ugitd/main.go
diff --git a/cmd/ugitd/main.go b/cmd/ugitd/main.go
index 3286e60ad9d56cec90db13ed2193ed617f68ea86..e7fab794e4e55915ced861e8b15647ca0bcf36e5 100644
--- a/cmd/ugitd/main.go
+++ b/cmd/ugitd/main.go
@@ -6,6 +6,12 @@ 	"flag"
 	"fmt"
 	"os"
 	"os/signal"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
+	"go.jolheiser.com/ugit/internal/git"
 
 	"go.jolheiser.com/ugit/internal/http"
 	"go.jolheiser.com/ugit/internal/ssh"
@@ -16,6 +22,11 @@ 	"github.com/go-git/go-git/v5/utils/trace"
 )
 
 func main() {
+	if len(os.Args) > 1 && os.Args[1] == "pre-receive-hook" {
+		preReceive()
+		return
+	}
+
 	args, err := parseArgs(os.Args[1:])
 	if err != nil {
 		if errors.Is(err, flag.ErrHelp) {
@@ -23,6 +34,10 @@ 			return
 		}
 		panic(err)
 	}
+	args.RepoDir, err = filepath.Abs(args.RepoDir)
+	if err != nil {
+		panic(err)
+	}
 
 	if args.Debug {
 		trace.SetTarget(trace.Packet)
@@ -32,7 +47,7 @@ 		middleware.DefaultLogger = http.NoopLogger
 		ssh.DefaultLogger = ssh.NoopLogger
 	}
 
-	if err := os.MkdirAll(args.RepoDir, os.ModePerm); err != nil {
+	if err := requiredFS(args.RepoDir); err != nil {
 		panic(err)
 	}
 
@@ -83,3 +98,62 @@ 	ch := make(chan os.Signal, 1)
 	signal.Notify(ch, os.Kill, os.Interrupt)
 	<-ch
 }
+
+func requiredFS(repoDir string) error {
+	if err := os.MkdirAll(repoDir, os.ModePerm); err != nil {
+		return err
+	}
+
+	if !git.RequiresHook {
+		return nil
+	}
+	bin, err := os.Executable()
+	if err != nil {
+		return err
+	}
+
+	fp := filepath.Join(repoDir, "hooks")
+	if err := os.MkdirAll(fp, os.ModePerm); err != nil {
+		return err
+	}
+	fp = filepath.Join(fp, "pre-receive")
+
+	fi, err := os.Create(fp)
+	if err != nil {
+		return err
+	}
+	fi.WriteString("#!/usr/bin/env bash\n")
+	fi.WriteString(fmt.Sprintf("%s pre-receive-hook\n", bin))
+	fi.Close()
+
+	return os.Chmod(fp, 0o755)
+}
+
+func preReceive() {
+	repoDir, ok := os.LookupEnv("UGIT_REPODIR")
+	if !ok {
+		panic("UGIT_REPODIR is not set")
+	}
+
+	opts := make([]*packp.Option, 0)
+	if pushCount, err := strconv.Atoi(os.Getenv("GIT_PUSH_OPTION_COUNT")); err == nil {
+		for idx := 0; idx < pushCount; idx++ {
+			opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
+			kv := strings.SplitN(opt, "=", 2)
+			if len(kv) == 2 {
+				opts = append(opts, &packp.Option{
+					Key:   kv[0],
+					Value: kv[1],
+				})
+			}
+		}
+	}
+
+	repo, err := git.NewRepo(filepath.Dir(repoDir), filepath.Base(repoDir))
+	if err != nil {
+		panic(err)
+	}
+	if err := git.HandlePushOptions(repo, opts); err != nil {
+		panic(err)
+	}
+}
M internal/git/protocol.go -> internal/git/protocol.go
diff --git a/internal/git/protocol.go b/internal/git/protocol.go
index 2da2417798c0c11c7f1bab387683c84a8f47475e..26a5ae72dbc1859ff030052fc4791a69df8c2dab 100644
--- a/internal/git/protocol.go
+++ b/internal/git/protocol.go
@@ -1,30 +1,19 @@
 package git
 
 import (
-	"bufio"
 	"context"
-	"fmt"
 	"io"
 	"strconv"
 	"strings"
 
 package git
-package git
 package git
 package git
-
-package git
 import (
 package git
-	"bufio"
-package git
 	"context"
 package git
-	"fmt"
-	"github.com/go-git/go-git/v5/plumbing/transport/server"
-package git
 	"strconv"
-	"github.com/go-git/go-git/v5/utils/ioutil"
 )
 
 // ReadWriteContexter is the interface required to operate on git protocols
@@ -33,200 +22,44 @@ 	io.ReadWriteCloser
 	Context() context.Context
 }
 
-// Protocol handles the endpoint and server of the git protocols
-type Protocol struct {
-	endpoint *transport.Endpoint
-	server   transport.Transport
-}
-
-// NewProtocol constructs a Protocol for a given repo
-import (
 package git
-	endpoint, err := transport.NewEndpoint("/")
-	if err != nil {
-		return Protocol{}, err
-	}
-	fs := osfs.New(repoPath)
-	loader := server.NewFilesystemLoader(fs)
 	gitServer := server.NewServer(loader)
+package git
 	return Protocol{
-		endpoint: endpoint,
-	"bufio"
 package git
-	}, nil
-}
-
-// HTTPInfoRefs handles the inforef part of the HTTP protocol
-	"bufio"
 	"bufio"
-	session, err := p.server.NewUploadPackSession(p.endpoint, nil)
-	if err != nil {
-		return err
-	}
-	defer ioutil.CheckClose(rwc, &err)
-	return p.infoRefs(rwc, session, "# service=git-upload-pack")
-}
-
-func (p Protocol) infoRefs(rwc ReadWriteContexter, session transport.UploadPackSession, prefix string) error {
-	"context"
-	if err != nil {
-		return err
-	}
-
-	"context"
 package git
-		ar.Prefix = [][]byte{
-			[]byte(prefix),
-	"context"
 	"bufio"
-		}
-	}
-
-	if err := ar.Encode(rwc); err != nil {
+package git
+package git
 	"bufio"
-	"fmt"
-	}
 
-	return nil
 }
 
-// HTTPUploadPack handles the upload-pack process for HTTP
-func (p Protocol) HTTPUploadPack(rwc ReadWriteContexter) error {
-	return p.uploadPack(rwc, false)
-}
-
-	"fmt"
 package git
-func (p Protocol) SSHUploadPack(rwc ReadWriteContexter) error {
-	return p.uploadPack(rwc, true)
-}
-
-func (p Protocol) uploadPack(rwc ReadWriteContexter, ssh bool) error {
-	session, err := p.server.NewUploadPackSession(p.endpoint, nil)
 	if err != nil {
-		return err
-	}
-	defer ioutil.CheckClose(rwc, &err)
-
-	if ssh {
-		if err := p.infoRefs(rwc, session, ""); err != nil {
-			return err
-		}
-	}
-
-	req := packp.NewUploadPackRequest()
-	if err := req.Decode(rwc); err != nil {
-		return err
-	}
-
-	var resp *packp.UploadPackResponse
-	"io"
 package git
-import (
 import (
 	"bufio"
-	"fmt"
+package git
 	}
-
-	if err := resp.Encode(rwc); err != nil {
-		return fmt.Errorf("could not encode upload pack: %w", err)
-	}
-
-	return nil
-}
-
-// SSHReceivePack handles the receive-pack process for SSH
-func (p Protocol) SSHReceivePack(rwc ReadWriteContexter, repo *Repo) error {
-	buf := bufio.NewReader(rwc)
-
-	session, err := p.server.NewReceivePackSession(p.endpoint, nil)
 	if err != nil {
 		return err
 	}
-
-	ar, err := session.AdvertisedReferencesContext(rwc.Context())
-	if err != nil {
-		return fmt.Errorf("internal error in advertised references: %w", err)
-	}
-	_ = ar.Capabilities.Set(capability.PushOptions)
-	_ = ar.Capabilities.Set("no-thin")
-
-	if err := ar.Encode(rwc); err != nil {
-	"strconv"
 package git
-	}
-
-	req := packp.NewReferenceUpdateRequest()
-	"strconv"
 import (
-	if err := req.Decode(buf); err != nil {
-		// FIXME this is a hack, but go-git doesn't accept a 0000 if there are no refs to update
-	"strconv"
 	"fmt"
-			return fmt.Errorf("error decoding: %w", err)
-		}
-	}
-
-	// FIXME also a hack, if the next bytes are PACK then we have a packfile, otherwise assume it's push options
-	peek, err := buf.Peek(4)
-	if err != nil {
-		return err
-	}
-	if string(peek) != "PACK" {
-	"strings"
 package git
-		for s.Scan() {
-	"strings"
 import (
-			if val == "" {
-				break
-			}
-	"strings"
 	"io"
-				return s.Err()
-			}
-			parts := strings.SplitN(val, "=", 2)
-			req.Options = append(req.Options, &packp.Option{
-				Key:   parts[0],
-	"github.com/go-git/go-billy/v5/osfs"
 
-			})
-		}
-import (
 	"context"
 
 package git
 	"bufio"
-		return fmt.Errorf("could not handle push options: %w", err)
 import (
-	"context"
-
-	// FIXME if there are only delete commands, there is no packfile and ReceivePack will block forever
-	noPack := true
-	for _, c := range req.Commands {
-		if c.Action() != packp.Delete {
-			noPack = false
-			break
-		}
-	}
-	if noPack {
-		req.Packfile = nil
-	}
-
-package git
 	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
-	if err != nil {
-		return fmt.Errorf("error in receive pack: %w", err)
-	}
-
-	if err := rs.Encode(rwc); err != nil {
-		return fmt.Errorf("could not encode receive pack: %w", err)
-	}
-
-	return nil
-}
-
-func handlePushOptions(repo *Repo, opts []*packp.Option) error {
+	"bufio"
 	var changed bool
 	for _, opt := range opts {
 		switch strings.ToLower(opt.Key) {
@@ -245,13 +80,3 @@ 		return repo.SaveMeta()
 	}
 	return nil
 }
-
-// UpdateServerInfo handles updating server info for the git repo
-func UpdateServerInfo(repo string) error {
-	r, err := git.PlainOpen(repo)
-	if err != nil {
-		return err
-	}
-	fs := r.Storer.(*filesystem.Storage).Filesystem()
-	return serverinfo.UpdateServerInfo(r.Storer, fs)
-}
I internal/git/protocol_git.go
diff --git a/internal/git/protocol_git.go b/internal/git/protocol_git.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ee650c851b5b3bc68411cb3e3358320fc2b0920
--- /dev/null
+++ b/internal/git/protocol_git.go
@@ -0,0 +1,62 @@
+//go:build !gogit
+
+package git
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+
+	"github.com/go-git/go-git/v5/plumbing/format/pktline"
+)
+
+var RequiresHook = true
+
+type CmdProtocol string
+
+func NewProtocol(repoPath string) (Protocoler, error) {
+	return CmdProtocol(repoPath), nil
+}
+
+func (c CmdProtocol) HTTPInfoRefs(ctx ReadWriteContexter) error {
+	pkt := pktline.NewEncoder(ctx)
+	if err := pkt.EncodeString("# service=git-upload-pack"); err != nil {
+		return err
+	}
+	if err := pkt.Flush(); err != nil {
+		return err
+	}
+	return gitService(ctx, "receive-pack", string(c), "--stateless-rpc", "--advertise-refs")
+}
+
+func (c CmdProtocol) HTTPUploadPack(ctx ReadWriteContexter) error {
+	return gitService(ctx, "upload-pack", string(c), "--stateless-rpc")
+}
+
+func (c CmdProtocol) SSHUploadPack(ctx ReadWriteContexter) error {
+	return gitService(ctx, "upload-pack", string(c))
+}
+
+func (c CmdProtocol) SSHReceivePack(ctx ReadWriteContexter, _ *Repo) error {
+	return gitService(ctx, "receive-pack", string(c))
+}
+
+func gitService(ctx ReadWriteContexter, command, repoDir string, args ...string) error {
+	cmd := exec.CommandContext(ctx.Context(), "git")
+	cmd.Args = append(cmd.Args, []string{
+		"-c", "uploadpack.allowFilter=true",
+		"-c", "receive.advertisePushOptions=true",
+		"-c", fmt.Sprintf("core.hooksPath=%s", filepath.Join(filepath.Dir(repoDir), "hooks")),
+		command,
+	}...)
+	if len(args) > 0 {
+		cmd.Args = append(cmd.Args, args...)
+	}
+	cmd.Args = append(cmd.Args, repoDir)
+	cmd.Env = append(os.Environ(), fmt.Sprintf("UGIT_REPODIR=%s", repoDir))
+	cmd.Stdin = ctx
+	cmd.Stdout = ctx
+
+	return cmd.Run()
+}
I internal/git/protocol_gogit.go
diff --git a/internal/git/protocol_gogit.go b/internal/git/protocol_gogit.go
new file mode 100644
index 0000000000000000000000000000000000000000..c437b29f60cc2b9d48b59d26289e630917578afe
--- /dev/null
+++ b/internal/git/protocol_gogit.go
@@ -0,0 +1,191 @@
+//go:build gogit
+
+package git
+
+import (
+	"bufio"
+	"fmt"
+	"strings"
+
+	"github.com/go-git/go-billy/v5/osfs"
+	"github.com/go-git/go-git/v5/plumbing/format/pktline"
+	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
+	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
+	"github.com/go-git/go-git/v5/plumbing/transport"
+	"github.com/go-git/go-git/v5/plumbing/transport/server"
+	"github.com/go-git/go-git/v5/utils/ioutil"
+)
+
+var RequiresHook = false
+
+// Protocol handles the endpoint and server of the git protocols
+type Protocol struct {
+	endpoint *transport.Endpoint
+	server   transport.Transport
+}
+
+// NewProtocol constructs a Protocol for a given repo
+func NewProtocol(repoPath string) (Protocoler, error) {
+	endpoint, err := transport.NewEndpoint("/")
+	if err != nil {
+		return Protocol{}, err
+	}
+	fs := osfs.New(repoPath)
+	loader := server.NewFilesystemLoader(fs)
+	gitServer := server.NewServer(loader)
+	return Protocol{
+		endpoint: endpoint,
+		server:   gitServer,
+	}, nil
+}
+
+// HTTPInfoRefs handles the inforef part of the HTTP protocol
+func (p Protocol) HTTPInfoRefs(rwc ReadWriteContexter) error {
+	session, err := p.server.NewUploadPackSession(p.endpoint, nil)
+	if err != nil {
+		return err
+	}
+	defer ioutil.CheckClose(rwc, &err)
+	return p.infoRefs(rwc, session, "# service=git-upload-pack")
+}
+
+func (p Protocol) infoRefs(rwc ReadWriteContexter, session transport.UploadPackSession, prefix string) error {
+	ar, err := session.AdvertisedReferencesContext(rwc.Context())
+	if err != nil {
+		return err
+	}
+
+	if prefix != "" {
+		ar.Prefix = [][]byte{
+			[]byte(prefix),
+			pktline.Flush,
+		}
+	}
+
+	if err := ar.Encode(rwc); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// HTTPUploadPack handles the upload-pack process for HTTP
+func (p Protocol) HTTPUploadPack(rwc ReadWriteContexter) error {
+	return p.uploadPack(rwc, false)
+}
+
+// SSHUploadPack handles the upload-pack process for SSH
+func (p Protocol) SSHUploadPack(rwc ReadWriteContexter) error {
+	return p.uploadPack(rwc, true)
+}
+
+func (p Protocol) uploadPack(rwc ReadWriteContexter, ssh bool) error {
+	session, err := p.server.NewUploadPackSession(p.endpoint, nil)
+	if err != nil {
+		return err
+	}
+	defer ioutil.CheckClose(rwc, &err)
+
+	if ssh {
+		if err := p.infoRefs(rwc, session, ""); err != nil {
+			return err
+		}
+	}
+
+	req := packp.NewUploadPackRequest()
+	if err := req.Decode(rwc); err != nil {
+		return err
+	}
+
+	var resp *packp.UploadPackResponse
+	resp, err = session.UploadPack(rwc.Context(), req)
+	if err != nil {
+		return err
+	}
+
+	if err := resp.Encode(rwc); err != nil {
+		return fmt.Errorf("could not encode upload pack: %w", err)
+	}
+
+	return nil
+}
+
+// SSHReceivePack handles the receive-pack process for SSH
+func (p Protocol) SSHReceivePack(rwc ReadWriteContexter, repo *Repo) error {
+	buf := bufio.NewReader(rwc)
+
+	session, err := p.server.NewReceivePackSession(p.endpoint, nil)
+	if err != nil {
+		return err
+	}
+
+	ar, err := session.AdvertisedReferencesContext(rwc.Context())
+	if err != nil {
+		return fmt.Errorf("internal error in advertised references: %w", err)
+	}
+	_ = ar.Capabilities.Set(capability.PushOptions)
+	_ = ar.Capabilities.Set("no-thin")
+
+	if err := ar.Encode(rwc); err != nil {
+		return fmt.Errorf("error in advertised references encoding: %w", err)
+	}
+
+	req := packp.NewReferenceUpdateRequest()
+	_ = req.Capabilities.Set(capability.ReportStatus)
+	if err := req.Decode(buf); err != nil {
+		// FIXME this is a hack, but go-git doesn't accept a 0000 if there are no refs to update
+		if !strings.EqualFold(err.Error(), "capabilities delimiter not found") {
+			return fmt.Errorf("error decoding: %w", err)
+		}
+	}
+
+	// FIXME also a hack, if the next bytes are PACK then we have a packfile, otherwise assume it's push options
+	peek, err := buf.Peek(4)
+	if err != nil {
+		return err
+	}
+	if string(peek) != "PACK" {
+		s := pktline.NewScanner(buf)
+		for s.Scan() {
+			val := string(s.Bytes())
+			if val == "" {
+				break
+			}
+			if s.Err() != nil {
+				return s.Err()
+			}
+			parts := strings.SplitN(val, "=", 2)
+			req.Options = append(req.Options, &packp.Option{
+				Key:   parts[0],
+				Value: parts[1],
+			})
+		}
+	}
+
+	if err := HandlePushOptions(repo, req.Options); err != nil {
+		return fmt.Errorf("could not handle push options: %w", err)
+	}
+
+	// FIXME if there are only delete commands, there is no packfile and ReceivePack will block forever
+	noPack := true
+	for _, c := range req.Commands {
+		if c.Action() != packp.Delete {
+			noPack = false
+			break
+		}
+	}
+	if noPack {
+		req.Packfile = nil
+	}
+
+	rs, err := session.ReceivePack(rwc.Context(), req)
+	if err != nil {
+		return fmt.Errorf("error in receive pack: %w", err)
+	}
+
+	if err := rs.Encode(rwc); err != nil {
+		return fmt.Errorf("could not encode receive pack: %w", err)
+	}
+
+	return nil
+}
M internal/html/generate.go -> internal/html/generate.go
diff --git a/internal/html/generate.go b/internal/html/generate.go
index 678f3a3babecb7155a85926f508d3e8db3ebb5d3..e4cd47c3973ef5614006335cab201dbff0f59710 100644
--- a/internal/html/generate.go
+++ b/internal/html/generate.go
@@ -6,10 +6,11 @@ import (
 	"bytes"
 	_ "embed"
 	"fmt"
-	"go.jolheiser.com/ugit/internal/html/markup"
 	"go/format"
 	"os"
 	"os/exec"
+
+	"go.jolheiser.com/ugit/internal/html/markup"
 
 	"github.com/alecthomas/chroma/v2/styles"
 )
M internal/html/markup/markdown.go -> internal/html/markup/markdown.go
diff --git a/internal/html/markup/markdown.go b/internal/html/markup/markdown.go
index 787fe1ba85feb78e5aed37e4a009b1564e845144..20c3b856d38c9acacac58db6793d54ad0eaa6597 100644
--- a/internal/html/markup/markdown.go
+++ b/internal/html/markup/markdown.go
@@ -3,11 +3,12 @@
 import (
 	"bytes"
 	"fmt"
-	"golang.org/x/net/html"
 	"io"
 	"net/url"
 	"path/filepath"
 	"strings"
+
+	"golang.org/x/net/html"
 
 	"go.jolheiser.com/ugit/internal/git"
 
M internal/http/index.go -> internal/http/index.go
diff --git a/internal/http/index.go b/internal/http/index.go
index 9e43dfb8a3ee7ad34c1de7d64b38351d0c697135..56a17981903efe6145ba0ee63c3fcf65655403a9 100644
--- a/internal/http/index.go
+++ b/internal/http/index.go
@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"os"
 	"sort"
+	"strings"
 	"time"
 
 	"go.jolheiser.com/ugit/internal/git"
@@ -19,6 +20,9 @@ 	}
 
 	repos := make([]*git.Repo, 0, len(repoPaths))
 	for _, repoName := range repoPaths {
+		if !strings.HasSuffix(repoName.Name(), ".git") {
+			continue
+		}
 		repo, err := git.NewRepo(rh.s.RepoDir, repoName.Name())
 		if err != nil {
 			return httperr.Error(err)
M internal/http/repo.go -> internal/http/repo.go
diff --git a/internal/http/repo.go b/internal/http/repo.go
index 2c4aa9358a63521f591ceb32318e27ff4b6ac94a..82b5af6e177f4af731630454b2f2da168ce46bd6 100644
--- a/internal/http/repo.go
+++ b/internal/http/repo.go
@@ -3,10 +3,11 @@
 import (
 	"bytes"
 	"errors"
-	"go.jolheiser.com/ugit/internal/html/markup"
 	"mime"
 	"net/http"
 	"path/filepath"
+
+	"go.jolheiser.com/ugit/internal/html/markup"
 
 	"go.jolheiser.com/ugit/internal/git"
 	"go.jolheiser.com/ugit/internal/html"