ugit/cmd/ugitd/main.go

198 lines
4.3 KiB
Go
Raw Normal View History

package main
import (
"errors"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httplog/v2"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/utils/trace"
"go.jolheiser.com/tailroute"
"go.jolheiser.com/ugit/internal/git"
"go.jolheiser.com/ugit/internal/http"
"go.jolheiser.com/ugit/internal/ssh"
)
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) {
return
}
panic(err)
}
args.RepoDir, err = filepath.Abs(args.RepoDir)
if err != nil {
panic(err)
}
log.SetLevel(args.Log.Level)
middleware.DefaultLogger = httplog.RequestLogger(httplog.NewLogger("ugit", httplog.Options{
JSON: args.Log.JSON,
LogLevel: slog.Level(args.Log.Level),
Concise: args.Log.Level != log.DebugLevel,
}))
if args.Log.Level == log.DebugLevel {
trace.SetTarget(trace.Packet)
} else {
middleware.DefaultLogger = http.NoopLogger
ssh.DefaultLogger = ssh.NoopLogger
}
if args.Log.JSON {
log.SetFormatter(log.JSONFormatter)
}
if err := requiredFS(args.RepoDir); err != nil {
panic(err)
}
if args.SSH.Enable {
sshSettings := ssh.Settings{
AuthorizedKeys: args.SSH.AuthorizedKeys,
CloneURL: args.SSH.CloneURL,
Port: args.SSH.Port,
HostKey: args.SSH.HostKey,
RepoDir: args.RepoDir,
}
sshSrv, err := ssh.New(sshSettings)
if err != nil {
panic(err)
}
go func() {
log.Debugf("SSH listening on ssh://localhost:%d\n", sshSettings.Port)
if err := sshSrv.ListenAndServe(); err != nil {
panic(err)
}
}()
}
httpSettings := http.Settings{
Title: args.Meta.Title,
Description: args.Meta.Description,
CloneURL: args.HTTP.CloneURL,
Port: args.HTTP.Port,
RepoDir: args.RepoDir,
Profile: http.Profile{
Username: args.Profile.Username,
Email: args.Profile.Email,
},
ShowPrivate: false,
}
for _, link := range args.Profile.Links {
httpSettings.Profile.Links = append(httpSettings.Profile.Links, http.Link{
Name: link.Name,
URL: link.URL,
})
}
if args.HTTP.Enable {
httpSrv := http.New(httpSettings)
go func() {
log.Debugf("HTTP listening on http://localhost:%d\n", httpSettings.Port)
if err := httpSrv.ListenAndServe(); err != nil {
panic(err)
}
}()
}
if args.Tailscale.Enable {
tailnetSettings := httpSettings
tailnetSettings.ShowPrivate = true
tailnetSrv := http.New(tailnetSettings)
tr := tailroute.Router{
Tailnet: tailnetSrv.Mux,
}
go func() {
log.Debugf("Tailnet listening on http://%s\n", args.Tailscale.Hostname)
if err := tr.Serve(args.Tailscale.Hostname, args.Tailscale.DataDir); err != nil {
panic(err)
}
}()
}
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")
if err := os.MkdirAll(fp+".d", os.ModePerm); err != nil {
return err
}
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.WriteString(fmt.Sprintf(`for hook in %s.d/*; do
"${hook}"
done`, fp))
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)
}
}