仰望星辰工作室

gitcat

gitcat/ main.go 5.0 KB · 186 行 原始文件
1// Command gitcat 是面向小型工作室的自建 Git 服务。
2//
3// gitcat -addr :12747 -data ./data
4//
5// 首次启动后访问 http://<主机>:12747 会进入安装向导,设置站点名称与管理员账号。
6package main
7
8import (
9 "context"
10 "errors"
11 "flag"
12 "fmt"
13 "log"
14 "net/http"
15 "os"
16 "os/exec"
17 "os/signal"
18 "path/filepath"
19 "strings"
20 "syscall"
21 "time"
22
23 "gitcat/internal/server"
24 "gitcat/internal/store"
25 "gitcat/internal/upgrade"
26)
27
28// 这些变量可在构建时注入:
29//
30// go build -ldflags "-X main.version=1.2.0 -X main.commit=$(git rev-parse --short HEAD) -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
31//
32// version 保持语义化格式,后台"版本与升级"页依赖它做版本比较;
33// 未注入时属于 dev 构建,自动升级会拒绝执行。
34var (
35 version = "1.0.0-dev"
36 commit = ""
37 buildDate = ""
38)
39
40func main() {
41 log.SetFlags(log.LstdFlags)
42 log.SetPrefix("[gitcat] ")
43
44 addr := flag.String("addr", envOr("GITCAT_ADDR", ":12747"), "HTTP 监听地址")
45 dataDir := flag.String("data", envOr("GITCAT_DATA", "./data"), "数据目录(SQLite 数据库、仓库与头像)")
46 showVersion := flag.Bool("version", false, "打印版本号后退出")
47 flag.Parse()
48
49 if *showVersion {
50 // 一键升级会用 `-version` 的输出做试运行校验,因此首行格式必须稳定。
51 fmt.Println("gitcat", version)
52 if commit != "" {
53 fmt.Println("commit:", commit)
54 }
55 if buildDate != "" {
56 fmt.Println("built:", buildDate)
57 }
58 return
59 }
60
61 if err := run(*addr, *dataDir); err != nil {
62 log.Fatalf("启动失败: %v", err)
63 }
64}
65
66func run(addr, dataDir string) error {
67 if _, err := exec.LookPath("git"); err != nil {
68 return errors.New("未找到 git 命令,请先安装 git 并确保在 PATH 中")
69 }
70 abs, err := filepath.Abs(dataDir)
71 if err != nil {
72 return err
73 }
74 if err := os.MkdirAll(abs, 0o755); err != nil {
75 return fmt.Errorf("创建数据目录: %w", err)
76 }
77
78 st, err := store.Open(filepath.Join(abs, "gitcat.db"))
79 if err != nil {
80 return fmt.Errorf("打开数据库: %w", err)
81 }
82 defer st.Close()
83
84 exePath, _ := os.Executable()
85 srv, err := server.New(server.Config{
86 Addr: addr,
87 DataDir: abs,
88 Version: version,
89 Commit: commit,
90 BuildDate: buildDate,
91 ExePath: exePath,
92 // 一键升级替换完二进制后调用它:走与外部信号完全相同的优雅关闭
93 // 路径,这样 systemd 的 Restart=always 就能干净地拉起新版本。
94 Restart: func() {
95 proc, err := os.FindProcess(os.Getpid())
96 if err != nil {
97 log.Printf("触发重启失败: %v", err)
98 return
99 }
100 _ = proc.Signal(syscall.SIGTERM)
101 },
102 }, st)
103 if err != nil {
104 return err
105 }
106
107 httpSrv := &http.Server{
108 Addr: addr,
109 Handler: srv.Handler(),
110 ReadHeaderTimeout: 20 * time.Second,
111 IdleTimeout: 90 * time.Second,
112 // 刻意不设 WriteTimeout:clone / push 需要长时间持续写响应,写超时
113 // 会把大仓库的传输硬切断。git 子进程的超时改由
114 // server.gitTransferTimeout 单独控制。
115 }
116
117 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
118 defer stop()
119
120 // 升级待生效提醒:替换二进制之后到下次重启之间,这个标记会一直存在。
121 // 一旦确认新版本真的跑起来了(当前版本已不低于目标版本),就清掉标记,
122 // 免得每次启动都以为还有升级没生效。
123 if pend := upgrade.ReadPending(abs); pend != nil {
124 if upgrade.Compare(version, pend.ToVersion) >= 0 {
125 log.Printf("升级已生效:%s → %s(旧版本备份在 %s)",
126 pend.FromVersion, version, pend.BackupPath)
127 _ = upgrade.ClearPending(abs)
128 } else {
129 log.Printf("提示: 已替换为 %s,但当前仍在运行 %s,请检查部署流程;备份在 %s",
130 pend.ToVersion, version, pend.BackupPath)
131 }
132 }
133
134 // 定时清理过期会话。
135 go func() {
136 t := time.NewTicker(time.Hour)
137 defer t.Stop()
138 for {
139 select {
140 case <-ctx.Done():
141 return
142 case <-t.C:
143 if err := st.CleanupSessions(); err != nil {
144 log.Printf("清理过期会话失败: %v", err)
145 }
146 }
147 }
148 }()
149
150 errCh := make(chan error, 1)
151 go func() {
152 log.Printf("gitcat %s 已启动", version)
153 log.Printf("监听地址: %s", displayAddr(addr))
154 log.Printf("数据目录: %s", abs)
155 if !st.Installed() {
156 log.Printf("尚未安装,请打开 %s 完成初始化", displayAddr(addr))
157 }
158 if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
159 errCh <- err
160 }
161 }()
162
163 select {
164 case err := <-errCh:
165 return err
166 case <-ctx.Done():
167 log.Println("收到退出信号,正在关闭…")
168 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
169 defer cancel()
170 return httpSrv.Shutdown(shutdownCtx)
171 }
172}
173
174func displayAddr(addr string) string {
175 if strings.HasPrefix(addr, ":") {
176 return "http://localhost" + addr
177 }
178 return "http://" + addr
179}
180
181func envOr(key, def string) string {
182 if v := strings.TrimSpace(os.Getenv(key)); v != "" {
183 return v
184 }
185 return def
186}