ugit @main -
refs -
log -
-
https://git.jolheiser.com/ugit.git
The code powering this h*ckin' site
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-----
8 changed files, 343 additions(+), 186 deletions(-)
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)
+ }
+}
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)
-}
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()
+}
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
+}
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"
)
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"
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)
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"