// Command gitcat 是面向小型工作室的自建 Git 服务。 // // gitcat -addr :12747 -data ./data // // 首次启动后访问 http://<主机>:12747 会进入安装向导,设置站点名称与管理员账号。 package main import ( "context" "errors" "flag" "fmt" "log" "net/http" "os" "os/exec" "os/signal" "path/filepath" "strings" "syscall" "time" "gitcat/internal/server" "gitcat/internal/store" ) // version 可在构建时通过 -ldflags "-X main.version=..." 注入。 var version = "1.0.0" func main() { log.SetFlags(log.LstdFlags) log.SetPrefix("[gitcat] ") addr := flag.String("addr", envOr("GITCAT_ADDR", ":12747"), "HTTP 监听地址") dataDir := flag.String("data", envOr("GITCAT_DATA", "./data"), "数据目录(SQLite 数据库、仓库与头像)") showVersion := flag.Bool("version", false, "打印版本号后退出") flag.Parse() if *showVersion { fmt.Println("gitcat", version) return } if err := run(*addr, *dataDir); err != nil { log.Fatalf("启动失败: %v", err) } } func run(addr, dataDir string) error { if _, err := exec.LookPath("git"); err != nil { return errors.New("未找到 git 命令,请先安装 git 并确保在 PATH 中") } abs, err := filepath.Abs(dataDir) if err != nil { return err } if err := os.MkdirAll(abs, 0o755); err != nil { return fmt.Errorf("创建数据目录: %w", err) } st, err := store.Open(filepath.Join(abs, "gitcat.db")) if err != nil { return fmt.Errorf("打开数据库: %w", err) } defer st.Close() srv, err := server.New(server.Config{Addr: addr, DataDir: abs, Version: version}, st) if err != nil { return err } httpSrv := &http.Server{ Addr: addr, Handler: srv.Handler(), ReadHeaderTimeout: 20 * time.Second, IdleTimeout: 90 * time.Second, } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // 定时清理过期会话。 go func() { t := time.NewTicker(time.Hour) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: if err := st.CleanupSessions(); err != nil { log.Printf("清理过期会话失败: %v", err) } } } }() errCh := make(chan error, 1) go func() { log.Printf("gitcat %s 已启动", version) log.Printf("监听地址: %s", displayAddr(addr)) log.Printf("数据目录: %s", abs) if !st.Installed() { log.Printf("尚未安装,请打开 %s 完成初始化", displayAddr(addr)) } if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } }() select { case err := <-errCh: return err case <-ctx.Done(): log.Println("收到退出信号,正在关闭…") shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return httpSrv.Shutdown(shutdownCtx) } } func displayAddr(addr string) string { if strings.HasPrefix(addr, ":") { return "http://localhost" + addr } return "http://" + addr } func envOr(key, def string) string { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return v } return def }