git-sync-web 自动提交 2026-09-09 11:53:34

This commit is contained in:
tamakyi
2026-09-09 11:53:35 +08:00
parent 2dd4a46ad6
commit 05d6f1bc84
3 changed files with 98 additions and 10 deletions
+86 -8
View File
@@ -71,6 +71,7 @@ type RepoSnapshot struct {
RetryCount int `json:"retry_count"`
RetryInterval int `json:"retry_interval"`
ConflictStrategy string `json:"conflict_strategy"`
PushStrategy string `json:"push_strategy"`
LogFile string `json:"log_file"`
AutoCommit bool `json:"auto_commit"`
Cron string `json:"cron"`
@@ -210,11 +211,18 @@ func (e *Engine) logf(s *repoState, level, format string, args ...any) {
e.publish(map[string]any{"type": "log", "repo": s.cfg.Name, "level": level, "msg": msg, "time": ts})
}
// statMaxLines 控制单个仓库统计 CSV 的最大行数(含表头),超过则滚动保留最近 N 行
const statMaxLines = 2000
func (e *Engine) stat(s *repoState, action, result string, commits int) {
path := filepath.Join(e.statsDir, s.cfg.Name+".csv")
if _, err := os.Stat(path); os.IsNotExist(err) {
_ = os.WriteFile(path, []byte("timestamp,action,result,commits\n"), 0o644)
}
// 文件过大时滚动截断,避免长期运行无限膨胀(统计频率低,重写成本可接受)
if rotateStatIfNeeded(path, statMaxLines) {
e.logf(s, "info", "统计文件已滚动截断至最近 %d 行", statMaxLines)
}
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return
@@ -223,6 +231,23 @@ func (e *Engine) stat(s *repoState, action, result string, commits int) {
fmt.Fprintf(f, "%s,%s,%s,%d\n", time.Now().Format("2006-01-02 15:04:05"), action, result, commits)
}
// rotateStatIfNeeded 当 CSV 行数超过 maxLines 时,保留表头 + 最近 (maxLines-1) 条数据行并重写
func rotateStatIfNeeded(path string, maxLines int) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
if len(lines) <= maxLines {
return false
}
keep := append([]string{lines[0]}, lines[len(lines)-(maxLines-1):]...)
if err := os.WriteFile(path, []byte(strings.Join(keep, "\n")+"\n"), 0o644); err != nil {
return false
}
return true
}
func firstRemoteName(cfg *RepoConfig) string {
if len(cfg.Remotes) > 0 {
return cfg.Remotes[0].Name
@@ -243,6 +268,7 @@ func (s *repoState) snapshot(live bool) RepoSnapshot {
RetryCount: cfg.RetryCount,
RetryInterval: cfg.RetryInterval,
ConflictStrategy: cfg.ConflictStrategy,
PushStrategy: cfg.PushStrategy,
LogFile: cfg.LogFile,
AutoCommit: cfg.AutoCommit,
Cron: cfg.Cron,
@@ -516,6 +542,19 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
s.mu.Unlock()
}()
// 自动 stash 恢复(B4):若本次同步前暂存了改动,结束时尝试恢复
stashed := false
defer func() {
if !stashed {
return
}
if _, perr := gitRun(cfg.RepoPath, 60*time.Second, "stash", "pop"); perr != nil {
e.logf(s, "warn", "同步前自动 stash 的恢复失败(可能与其他改动冲突),请手动执行 git stash pop: %v", perr)
} else {
e.logf(s, "info", "已恢复同步前自动 stash 的未提交改动")
}
}()
if !isRepo(cfg.RepoPath) {
e.setFail(s, "仓库路径无效: "+cfg.RepoPath)
return
@@ -536,6 +575,16 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
return
}
// 自动 stash(B4):未开启自动提交且有未提交改动时先暂存,避免分支切换/推送被脏工作区阻断
if !cfg.AutoCommit && dirtyCount(cfg.RepoPath) > 0 {
if _, serr := gitRun(cfg.RepoPath, 60*time.Second, "stash", "push", "-u", "-m", "git-sync-web auto-stash"); serr == nil {
stashed = true
e.logf(s, "info", "已自动 stash 未提交改动(同步结束后恢复)")
} else {
e.logf(s, "warn", "自动 stash 失败,将尝试直接同步")
}
}
// 分支对齐(安全切换)
if cur, cerr := currentBranch(cfg.RepoPath); cerr == nil && cur != cfg.Branch {
tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch)
@@ -620,19 +669,48 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
// ---- 再推送 ----
if ahead > 0 && (cfg.Mode == "push" || cfg.Mode == "both") {
// pushAll 可能返回错误,如果是 non-fast-forward,设置 push_conflict 状态
pushOK, pushErr := e.pushAll(s, cfg, ahead)
if !pushOK {
// 检查是否是 non-fast-forward 错误
// 检查是否是 non-fast-forward 错误,按推送策略处理(B3)
if pushErr != nil && strings.Contains(strings.ToLower(pushErr.Error()), "non-fast-forward") {
// 记录冲突远程(取第一个冲突的远程)
// 在 pushAll 内部已经设置了 pushConflict 状态,这里只需退出
e.logf(s, "warn", "推送被拒绝(non-fast-forward),等待用户决策")
switch cfg.PushStrategy {
case "force":
e.logf(s, "warn", "按推送策略 force 执行强制推送(--force-with-lease)")
if _, ferr := gitRun(cfg.RepoPath, 300*time.Second, "push", "--force-with-lease", remote, cfg.Branch); ferr != nil {
e.setFail(s, "强制推送失败: "+ferr.Error())
return
}
e.logf(s, "info", "强制推送成功")
e.stat(s, "push", "success", ahead)
s.mu.Lock()
s.pushConflict = false
s.lastError = ""
s.mu.Unlock()
case "rebase":
e.logf(s, "warn", "按推送策略 rebase 后重试推送")
if _, rerr := gitRun(cfg.RepoPath, 300*time.Second, "rebase", remote+"/"+cfg.Branch); rerr != nil {
e.setFail(s, "变基到远程失败(请手动解决冲突): "+rerr.Error())
return
}
if _, perr := gitRun(cfg.RepoPath, 300*time.Second, "push", remote, cfg.Branch); perr != nil {
e.setFail(s, "变基后推送失败: "+perr.Error())
return
}
e.logf(s, "info", "变基后推送成功")
e.stat(s, "push", "success", ahead)
s.mu.Lock()
s.pushConflict = false
s.lastError = ""
s.mu.Unlock()
default:
// manual:保持 push_conflict 状态,等待用户在控制台决策(强制推送 / 放弃)
e.logf(s, "warn", "推送被拒绝(non-fast-forward),等待用户决策")
return
}
} else {
e.setFail(s, "推送失败: "+pushErr.Error())
return
}
// 其他错误
e.setFail(s, "推送失败: "+pushErr.Error())
return
}
}