// Command nugitea is a minimal git server: no users, no auth, no web UI — // just smart-HTTP and SSH clone/push against bare repos, plus pull/push // mirroring, implemented by shelling out to git the same way Gitea does. 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 [args] commands: serve run the HTTP + SSH git server repo create create a new bare repo repo list list repos mirror add-pull [--interval 5m] periodic fetch from a remote mirror add-push [--interval 5m] periodic mirror-push to a remote hook 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 ...") } 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 ") } 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 [--interval 5m] ") } 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] ", 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) } } // cmdHook implements the `nugitea hook ` entry point that installed // git hook delegator scripts call back into. There is no policy to enforce // yet (no protected branches, no webhooks) — this drains stdin so it plays // nicely with git's hook protocol and exits 0, leaving a stub for future // extension. func cmdHook(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: nugitea hook ") } switch args[0] { case "pre-receive", "post-receive": _, _ = io.Copy(io.Discard, os.Stdin) case "update": // git invokes this as `update `, no stdin 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" }