Files
github-auto-sync-golang/config.go
T

235 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
type Remote struct {
Name string `json:"name"`
URL string `json:"url"`
}
type RepoConfig struct {
Name string `json:"name"`
RepoPath string `json:"repo_path"`
Branch string `json:"branch"`
Remotes []Remote `json:"remotes"`
Mode string `json:"mode"`
Interval int `json:"interval"`
LogFile string `json:"log_file"`
AuthMethod string `json:"auth_method"`
RetryCount int `json:"retry_count"`
RetryInterval int `json:"retry_interval"`
ConflictStrategy string `json:"conflict_strategy"`
PushStrategy string `json:"push_strategy"`
AutoCommit bool `json:"auto_commit"`
AutoSync bool `json:"auto_sync"`
Cron string `json:"cron"`
}
var nameRe = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
// normalizeWinPath 把 Git Bash 风格路径 /c/foo/bar 转成 Windows 的 C:/foo/bar,
// 兼容旧脚本配置里存放的 bash 风格路径
func normalizeWinPath(p string) string {
p = strings.ReplaceAll(strings.TrimSpace(p), "\\", "/")
if len(p) >= 3 && p[0] == '/' && p[2] == '/' {
d := p[1]
if (d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') {
return strings.ToUpper(string(d)) + ":/" + p[3:]
}
}
return p
}
func unquote(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
return s[1 : len(s)-1]
}
}
return s
}
func orDefault(v, def string) string {
v = strings.TrimSpace(v)
if v == "" {
return def
}
return v
}
func atoiOr(v string, def int) int {
n, err := strconv.Atoi(strings.TrimSpace(v))
if err != nil {
return def
}
return n
}
func parseConfFile(path string) (map[string]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
m := map[string]string{}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
m[strings.TrimSpace(k)] = unquote(v)
}
return m, nil
}
func loadRepoConfig(dir, name string) (*RepoConfig, error) {
m, err := parseConfFile(filepath.Join(dir, name+".conf"))
if err != nil {
return nil, err
}
c := &RepoConfig{Name: name}
c.RepoPath = normalizeWinPath(m["REPO_PATH"])
c.Branch = orDefault(m["BRANCH"], "main")
c.Mode = orDefault(m["MODE"], "both")
c.Interval = atoiOr(m["INTERVAL"], 60)
c.LogFile = normalizeWinPath(m["LOG_FILE"])
c.AuthMethod = orDefault(m["AUTH_METHOD"], "ssh")
c.RetryCount = atoiOr(m["RETRY_COUNT"], 0)
c.RetryInterval = atoiOr(m["RETRY_INTERVAL"], 0)
c.ConflictStrategy = orDefault(m["CONFLICT_STRATEGY"], "merge")
c.PushStrategy = orDefault(m["PUSH_STRATEGY"], "manual")
c.AutoCommit = strings.EqualFold(m["AUTO_COMMIT"], "1") || strings.EqualFold(m["AUTO_COMMIT"], "true")
// 自动同步默认开启;仅在显式配置 0 / false 时关闭(关闭后不再自动触发,但手动同步仍可用)
c.AutoSync = !(strings.EqualFold(m["AUTO_SYNC"], "0") || strings.EqualFold(m["AUTO_SYNC"], "false"))
c.Cron = strings.TrimSpace(m["CRON"])
if remotes := strings.TrimSpace(m["REMOTES"]); remotes != "" {
for _, pair := range strings.Split(remotes, ",") {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
n, u, ok := strings.Cut(pair, "=")
if !ok {
continue
}
c.Remotes = append(c.Remotes, Remote{Name: strings.TrimSpace(n), URL: strings.TrimSpace(u)})
}
}
// 兼容旧版单远程格式:REMOTE_NAME + REMOTE_URL
if len(c.Remotes) == 0 && m["REMOTE_NAME"] != "" && m["REMOTE_URL"] != "" {
c.Remotes = append(c.Remotes, Remote{Name: m["REMOTE_NAME"], URL: m["REMOTE_URL"]})
}
return c, nil
}
func q(s string) string { return "\"" + s + "\"" }
func saveRepoConfig(dir string, c *RepoConfig) error {
var b strings.Builder
fmt.Fprintf(&b, "# Git 自动同步配置 - %s\n", c.Name)
fmt.Fprintf(&b, "REPO_PATH=%s\n", q(c.RepoPath))
fmt.Fprintf(&b, "BRANCH=%s\n", q(c.Branch))
pairs := make([]string, len(c.Remotes))
for i, r := range c.Remotes {
pairs[i] = r.Name + "=" + r.URL
}
fmt.Fprintf(&b, "REMOTES=%s\n", q(strings.Join(pairs, ",")))
fmt.Fprintf(&b, "MODE=%s\n", q(c.Mode))
fmt.Fprintf(&b, "INTERVAL=%s\n", q(strconv.Itoa(c.Interval)))
fmt.Fprintf(&b, "LOG_FILE=%s\n", q(c.LogFile))
fmt.Fprintf(&b, "AUTH_METHOD=%s\n", q(c.AuthMethod))
fmt.Fprintf(&b, "RETRY_COUNT=%s\n", q(strconv.Itoa(c.RetryCount)))
fmt.Fprintf(&b, "RETRY_INTERVAL=%s\n", q(strconv.Itoa(c.RetryInterval)))
fmt.Fprintf(&b, "CONFLICT_STRATEGY=%s\n", q(c.ConflictStrategy))
fmt.Fprintf(&b, "PUSH_STRATEGY=%s\n", q(c.PushStrategy))
ac := "0"
if c.AutoCommit {
ac = "1"
}
fmt.Fprintf(&b, "AUTO_COMMIT=%s\n", q(ac))
as := "0"
if c.AutoSync {
as = "1"
}
fmt.Fprintf(&b, "AUTO_SYNC=%s\n", q(as))
fmt.Fprintf(&b, "CRON=%s\n", q(c.Cron))
return os.WriteFile(filepath.Join(dir, c.Name+".conf"), []byte(b.String()), 0o644)
}
func validateRepoConfig(cfgDir string, c *RepoConfig, isCreate bool) error {
if !nameRe.MatchString(c.Name) {
return fmt.Errorf("配置名只能包含字母、数字、下划线和连字符")
}
if isCreate {
if _, err := os.Stat(filepath.Join(cfgDir, c.Name+".conf")); err == nil {
return fmt.Errorf("配置 %q 已存在", c.Name)
}
}
p := normalizeWinPath(c.RepoPath)
if !isRepo(p) {
return fmt.Errorf("仓库路径无效或不是 Git 仓库: %s", c.RepoPath)
}
c.RepoPath = p
c.Branch = strings.TrimSpace(c.Branch)
if c.Branch == "" {
c.Branch = "main"
}
if len(c.Remotes) == 0 {
return fmt.Errorf("至少需要一个远程仓库(格式:名称=URL)")
}
for _, r := range c.Remotes {
if strings.TrimSpace(r.Name) == "" || strings.TrimSpace(r.URL) == "" {
return fmt.Errorf("远程配置格式应为 名称=URL")
}
}
switch c.Mode {
case "push", "pull", "both":
default:
return fmt.Errorf("操作模式必须是 push/pull/both")
}
switch c.AuthMethod {
case "ssh", "https":
default:
return fmt.Errorf("认证方式必须是 ssh/https")
}
switch c.ConflictStrategy {
case "merge", "rebase", "abort":
default:
return fmt.Errorf("冲突策略必须是 merge/rebase/abort")
}
switch c.PushStrategy {
case "manual", "rebase", "force":
default:
return fmt.Errorf("推送策略必须是 manual/rebase/force")
}
if c.Interval < 5 {
c.Interval = 60
}
if c.RetryCount < 0 {
c.RetryCount = 0
}
if c.RetryInterval < 0 {
c.RetryInterval = 0
}
if c.AuthMethod != "https" {
c.RetryCount, c.RetryInterval = 0, 0
}
if strings.TrimSpace(c.Cron) != "" {
if _, err := parseCron(c.Cron); err != nil {
return fmt.Errorf("cron 表达式无效: %v", err)
}
}
return nil
}