package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"nugitea/internal/auth"
"nugitea/internal/httpgit"
"nugitea/internal/mirror"
"nugitea/internal/repo"
"nugitea/internal/sshgit"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
var err error
switch os.Args[1] {
case "serve":
err = cmdServe(os.Args[2:])
case "repo":
err = cmdRepo(os.Args[2:])
case "mirror":
err = cmdMirror(os.Args[2:])
case "hook":
err = cmdHook(os.Args[2:])
default:
usage()
os.Exit(1)
}
if err != nil {
fmt.Fprintln(os.Stderr, "nugitea:", err)
os.Exit(1)
}
}
func usage() {
fmt.Fprintln(os.Stderr, `usage: nugitea <command> [args]
commands:
serve run the HTTP + SSH git server
repo create <name> create a new bare repo
repo list list repos
mirror add-pull [--interval 5m] <repo> <url> periodic fetch from a remote
mirror add-push [--interval 5m] <repo> <url> periodic mirror-push to a remote
hook <pre-receive|update|post-receive> internal, invoked by git hooks`)
}
func cmdServe(args []string) error {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
httpAddr := fs.String("http", ":3080", "HTTP listen address")
sshAddr := fs.String("ssh", ":2222", "SSH listen address")
repoRoot := fs.String("repo-root", "./data", "directory containing bare repos")
hostKeyPath := fs.String("ssh-host-key", "./ssh_host_ed25519", "path to SSH host key (created if missing)")
if err := fs.Parse(args); err != nil {
return err
}
repos, err := repo.NewStore(*repoRoot)
if err != nil {
return err
}
authz := auth.AllowAll{}
mirrors := mirror.NewStore(repos.Root)
scheduler := mirror.NewScheduler(repos, mirrors)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go scheduler.Run(ctx)
httpHandler := httpgit.New(repos, authz)
httpServer := &http.Server{Addr: *httpAddr, Handler: httpHandler}
go func() {
log.Printf("http: listening on %s", *httpAddr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("http server: %v", err)
}
}()
sshServer := sshgit.New(repos, authz, *sshAddr, *hostKeyPath)
go func() {
if err := sshServer.ListenAndServe(); err != nil {
log.Fatalf("ssh server: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return httpServer.Shutdown(shutdownCtx)
}
func cmdRepo(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: nugitea repo <create|list> ...")
}
repos, err := repo.NewStore(repoRootFromEnv())
if err != nil {
return err
}
switch args[0] {
case "create":
if len(args) != 2 {
return fmt.Errorf("usage: nugitea repo create <name>")
}
path, err := repos.InitBare(context.Background(), args[1])
if err != nil {
return err
}
fmt.Println(path)
return nil
case "list":
names, err := repos.List()
if err != nil {
return err
}
for _, n := range names {
fmt.Println(n)
}
return nil
default:
return fmt.Errorf("unknown repo subcommand %q", args[0])
}
}
func cmdMirror(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: nugitea mirror <add-pull|add-push> [--interval 5m] <repo> <url>")
}
sub := args[0]
fs := flag.NewFlagSet("mirror "+sub, flag.ExitOnError)
interval := fs.Duration("interval", 5*time.Minute, "sync interval")
if err := fs.Parse(args[1:]); err != nil {
return err
}
rest := fs.Args()
if len(rest) != 2 {
return fmt.Errorf("usage: nugitea mirror %s [--interval 5m] <repo> <url>", sub)
}
repoName, remoteURL := rest[0], rest[1]
repos, err := repo.NewStore(repoRootFromEnv())
if err != nil {
return err
}
mirrors := mirror.NewStore(repos.Root)
ctx := context.Background()
switch sub {
case "add-pull":
return mirror.AddPull(ctx, repos, mirrors, repoName, remoteURL, *interval)
case "add-push":
return mirror.AddPush(ctx, repos, mirrors, repoName, remoteURL, *interval)
default:
return fmt.Errorf("unknown mirror subcommand %q", sub)
}
}
func cmdHook(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: nugitea hook <pre-receive|update|post-receive>")
}
switch args[0] {
case "pre-receive", "post-receive":
_, _ = io.Copy(io.Discard, os.Stdin)
case "update":
default:
return fmt.Errorf("unknown hook %q", args[0])
}
return nil
}
func repoRootFromEnv() string {
if v := os.Getenv("NUGITEA_REPO_ROOT"); v != "" {
return v
}
return "./data"
}