git-sync-web 自动提交 2026-09-09 11:53:34
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user