diff --git a/cmd/ugitd/main.go b/cmd/ugitd/main.go index e7fab794e4e55915ced861e8b15647ca0bcf36e5..3286e60ad9d56cec90db13ed2193ed617f68ea86 100644 --- a/cmd/ugitd/main.go +++ b/cmd/ugitd/main.go @@ -6,12 +6,6 @@ "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" @@ -23,12 +17,6 @@ ) func main() { -package main - preReceive() - return - } - - "flag" if err != nil { if errors.Is(err, flag.ErrHelp) { @@ -36,10 +24,6 @@ return } panic(err) } - args.RepoDir, err = filepath.Abs(args.RepoDir) - if err != nil { - panic(err) - } if args.Debug { trace.SetTarget(trace.Packet) @@ -49,7 +33,7 @@ middleware.DefaultLogger = http.NoopLogger ssh.DefaultLogger = ssh.NoopLogger } - if err := requiredFS(args.RepoDir); err != nil { + if err := os.MkdirAll(args.RepoDir, os.ModePerm); err != nil { panic(err) } @@ -100,62 +84,3 @@ 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 26a5ae72dbc1859ff030052fc4791a69df8c2dab..2da2417798c0c11c7f1bab387683c84a8f47475e 100644 --- a/internal/git/protocol.go +++ b/internal/git/protocol.go @@ -1,15 +1,25 @@ package git import ( + "bufio" "context" "io" +package git + "io" "strconv" "strings" + + "io" "github.com/go-git/go-git/v5" + "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/serverinfo" + "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/storage/filesystem" + "github.com/go-git/go-git/v5/utils/ioutil" ) // ReadWriteContexter is the interface required to operate on git protocols @@ -18,36 +28,202 @@ io.ReadWriteCloser Context() context.Context } +// Protocol handles the endpoint and server of the git protocols +type Protocol struct { + endpoint *transport.Endpoint + "strconv" package git +} + +// NewProtocol constructs a Protocol for a given repo +func NewProtocol(repoPath string) (Protocol, error) { + endpoint, err := transport.NewEndpoint("/") + if err != nil { + return Protocol{}, err + "github.com/go-git/go-git/v5" + fs := osfs.New(repoPath) + loader := server.NewFilesystemLoader(fs) + gitServer := server.NewServer(loader) + return Protocol{ + endpoint: endpoint, + "strings" package git + }, 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 { + "strings" "github.com/go-git/go-git/v5/plumbing/protocol/packp" + "strconv" + return err + } + + "github.com/go-git/go-git/v5" + ar.Prefix = [][]byte{ + "github.com/go-git/go-git/v5" + pktline.Flush, + } + } + + if err := ar.Encode(rwc); err != nil { + return err + } + + return nil package git + "strings" +// 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) + "strconv" + return err + } + defer ioutil.CheckClose(rwc, &err) + + if ssh { + "github.com/go-git/go-git/v5/plumbing/protocol/packp" import ( + 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 { + "strings" + } + + if err := resp.Encode(rwc); err != nil { + return fmt.Errorf("could not encode upload pack: %w", err) + } + "context" + "github.com/go-git/go-git/v5/plumbing/protocol/packp" +} +// 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 { + "github.com/go-git/go-git/v5/plumbing/serverinfo" "io" + "github.com/go-git/go-git/v5" + _ = 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) + "strconv" 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], +package git fs := r.Storer.(*filesystem.Storage).Filesystem() + } + } + +package git return serverinfo.UpdateServerInfo(r.Storer, fs) package git +// HandlePushOptions handles all relevant push options for a [Repo] and saves the new [RepoMeta] + } + + // 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 +// ReadWriteContexter is the interface required to operate on git protocols "strings" + } + "github.com/go-git/go-git/v5" +package git import ( + "github.com/go-git/go-git/v5" package git + case "private": + } + + 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 { +type ReadWriteContexter interface { import ( + } + + return nil +} +func handlePushOptions(repo *Repo, opts []*packp.Option) error { var changed bool for _, opt := range opts { switch strings.ToLower(opt.Key) { @@ -68,3 +244,13 @@ 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 deleted file mode 100644 index 7ee650c851b5b3bc68411cb3e3358320fc2b0920..0000000000000000000000000000000000000000 --- a/internal/git/protocol_git.go +++ /dev/null @@ -1,62 +0,0 @@ -//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 deleted file mode 100644 index c437b29f60cc2b9d48b59d26289e630917578afe..0000000000000000000000000000000000000000 --- a/internal/git/protocol_gogit.go +++ /dev/null @@ -1,191 +0,0 @@ -//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 e4cd47c3973ef5614006335cab201dbff0f59710..678f3a3babecb7155a85926f508d3e8db3ebb5d3 100644 --- a/internal/html/generate.go +++ b/internal/html/generate.go @@ -6,11 +6,10 @@ 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 20c3b856d38c9acacac58db6793d54ad0eaa6597..787fe1ba85feb78e5aed37e4a009b1564e845144 100644 --- a/internal/html/markup/markdown.go +++ b/internal/html/markup/markdown.go @@ -3,12 +3,11 @@ 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 56a17981903efe6145ba0ee63c3fcf65655403a9..9e43dfb8a3ee7ad34c1de7d64b38351d0c697135 100644 --- a/internal/http/index.go +++ b/internal/http/index.go @@ -4,7 +4,6 @@ import ( "net/http" "os" "sort" - "strings" "time" "go.jolheiser.com/ugit/internal/git" @@ -20,9 +19,6 @@ } 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 82b5af6e177f4af731630454b2f2da168ce46bd6..2c4aa9358a63521f591ceb32318e27ff4b6ac94a 100644 --- a/internal/http/repo.go +++ b/internal/http/repo.go @@ -3,11 +3,10 @@ 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"