git-sync-web 自动提交 2026-09-08 19:13:24
This commit is contained in:
@@ -16,8 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------- 事件总线(SSE 广播) ----------
|
// ---------- 事件总线 ----------
|
||||||
|
|
||||||
type EventBus struct {
|
type EventBus struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
next int
|
next int
|
||||||
@@ -50,16 +49,15 @@ func (b *EventBus) Publish(data []byte) {
|
|||||||
for _, ch := range b.subs {
|
for _, ch := range b.subs {
|
||||||
select {
|
select {
|
||||||
case ch <- data:
|
case ch <- data:
|
||||||
default: // 订阅者阻塞时丢弃,避免拖慢同步循环
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 状态 ----------
|
// ---------- 状态 ----------
|
||||||
|
|
||||||
type SyncResult struct {
|
type SyncResult struct {
|
||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
Result string `json:"result"` // ok | error | conflict
|
Result string `json:"result"` // ok | error | conflict | push_conflict
|
||||||
Detail string `json:"detail"`
|
Detail string `json:"detail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +76,17 @@ type RepoSnapshot struct {
|
|||||||
Cron string `json:"cron"`
|
Cron string `json:"cron"`
|
||||||
Remotes []Remote `json:"remotes"`
|
Remotes []Remote `json:"remotes"`
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"` // idle | ok | error | conflict | push_conflict
|
||||||
Ahead int `json:"ahead"`
|
Ahead int `json:"ahead"`
|
||||||
Behind int `json:"behind"`
|
Behind int `json:"behind"`
|
||||||
Dirty int `json:"dirty"`
|
Dirty int `json:"dirty"`
|
||||||
Conflicts []string `json:"conflicts"`
|
Conflicts []string `json:"conflicts"`
|
||||||
LastError string `json:"last_error"`
|
LastError string `json:"last_error"`
|
||||||
LastSync *SyncResult `json:"last_sync"`
|
LastSync *SyncResult `json:"last_sync"`
|
||||||
|
// 新增:推送冲突信息
|
||||||
|
PushConflict bool `json:"push_conflict"`
|
||||||
|
PushConflictRemote string `json:"push_conflict_remote"`
|
||||||
|
PushConflictBranch string `json:"push_conflict_branch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type repoState struct {
|
type repoState struct {
|
||||||
@@ -92,7 +94,7 @@ type repoState struct {
|
|||||||
cfg *RepoConfig
|
cfg *RepoConfig
|
||||||
running bool
|
running bool
|
||||||
syncing bool
|
syncing bool
|
||||||
status string // idle | ok | error | conflict
|
status string // idle | ok | error | conflict | push_conflict
|
||||||
ahead int
|
ahead int
|
||||||
behind int
|
behind int
|
||||||
dirty int
|
dirty int
|
||||||
@@ -100,6 +102,10 @@ type repoState struct {
|
|||||||
lastError string
|
lastError string
|
||||||
lastSync *SyncResult
|
lastSync *SyncResult
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
|
// 推送冲突信息
|
||||||
|
pushConflict bool
|
||||||
|
pushConflictRemote string
|
||||||
|
pushConflictBranch string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
@@ -107,7 +113,6 @@ type Engine struct {
|
|||||||
cfgDir string
|
cfgDir string
|
||||||
logDir string
|
logDir string
|
||||||
statsDir string
|
statsDir string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
states map[string]*repoState
|
states map[string]*repoState
|
||||||
bus *EventBus
|
bus *EventBus
|
||||||
@@ -164,7 +169,6 @@ func (e *Engine) sortedNames() []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: LoadAll 加锁,避免并发读写 map
|
|
||||||
func (e *Engine) LoadAll() {
|
func (e *Engine) LoadAll() {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
@@ -251,6 +255,9 @@ func (s *repoState) snapshot(live bool) RepoSnapshot {
|
|||||||
Conflicts: append([]string(nil), s.conflicts...),
|
Conflicts: append([]string(nil), s.conflicts...),
|
||||||
LastError: s.lastError,
|
LastError: s.lastError,
|
||||||
LastSync: s.lastSync,
|
LastSync: s.lastSync,
|
||||||
|
PushConflict: s.pushConflict,
|
||||||
|
PushConflictRemote: s.pushConflictRemote,
|
||||||
|
PushConflictBranch: s.pushConflictBranch,
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if live {
|
if live {
|
||||||
@@ -286,7 +293,6 @@ func (e *Engine) pushStatus(s *repoState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 启动 / 停止 ----------
|
// ---------- 启动 / 停止 ----------
|
||||||
|
|
||||||
func (e *Engine) Start(name string) error {
|
func (e *Engine) Start(name string) error {
|
||||||
s := e.get(name)
|
s := e.get(name)
|
||||||
if s == nil {
|
if s == nil {
|
||||||
@@ -401,7 +407,6 @@ func (e *Engine) SyncNow(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 同步核心 ----------
|
// ---------- 同步核心 ----------
|
||||||
|
|
||||||
func (e *Engine) withRetry(s *repoState, label string, fn func() (string, error)) (string, error) {
|
func (e *Engine) withRetry(s *repoState, label string, fn func() (string, error)) (string, error) {
|
||||||
maxAttempts, sleep, retryCount := 1, 0, 0
|
maxAttempts, sleep, retryCount := 1, 0, 0
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -448,7 +453,7 @@ func (e *Engine) setOK(s *repoState, detail string) {
|
|||||||
e.pushStatus(s)
|
e.pushStatus(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureRemotes 把配置中的远程同步到仓库(缺失则添加,URL 变化则更新)
|
// ensureRemotes 把配置中的远程同步到仓库
|
||||||
func (e *Engine) ensureRemotes(s *repoState, cfg *RepoConfig) {
|
func (e *Engine) ensureRemotes(s *repoState, cfg *RepoConfig) {
|
||||||
for _, r := range cfg.Remotes {
|
for _, r := range cfg.Remotes {
|
||||||
out, err := gitRun(cfg.RepoPath, 15*time.Second, "remote", "get-url", r.Name)
|
out, err := gitRun(cfg.RepoPath, 15*time.Second, "remote", "get-url", r.Name)
|
||||||
@@ -480,7 +485,7 @@ func (e *Engine) refreshCounts(s *repoState, cfg *RepoConfig, remote string) {
|
|||||||
e.pushStatus(s)
|
e.pushStatus(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// safeSync 包装 syncOnce:捕获后台 goroutine 的 panic,避免整个进程被杀死。
|
// safeSync 包装 syncOnce:捕获 panic
|
||||||
func (e *Engine) safeSync(s *repoState, trigger string) {
|
func (e *Engine) safeSync(s *repoState, trigger string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -495,7 +500,7 @@ func (e *Engine) safeSync(s *repoState, trigger string) {
|
|||||||
e.syncOnce(s, trigger)
|
e.syncOnce(s, trigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: 调整同步顺序:先拉取后推送;分支切换避免强制覆盖;pull 使用重试
|
// syncOnce 核心同步逻辑(已调整顺序,并增加 push_conflict 处理)
|
||||||
func (e *Engine) syncOnce(s *repoState, trigger string) {
|
func (e *Engine) syncOnce(s *repoState, trigger string) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.syncing {
|
if s.syncing {
|
||||||
@@ -531,19 +536,17 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: 分支对齐逻辑 – 避免使用 -B 强制覆盖,本地存在则普通切换,不存在则创建
|
// 分支对齐(安全切换)
|
||||||
if cur, cerr := currentBranch(cfg.RepoPath); cerr == nil && cur != cfg.Branch {
|
if cur, cerr := currentBranch(cfg.RepoPath); cerr == nil && cur != cfg.Branch {
|
||||||
tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch)
|
tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch)
|
||||||
localExists := branchExists(cfg.RepoPath, cfg.Branch)
|
localExists := branchExists(cfg.RepoPath, cfg.Branch)
|
||||||
if localExists {
|
if localExists {
|
||||||
// 本地分支存在,普通切换(可能因未提交改动而失败)
|
|
||||||
if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", cfg.Branch); err != nil {
|
if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", cfg.Branch); err != nil {
|
||||||
e.setFail(s, fmt.Sprintf("切换到配置分支 %s 失败,请先提交或暂存改动: %v", cfg.Branch, err))
|
e.setFail(s, fmt.Sprintf("切换到配置分支 %s 失败,请先提交或暂存改动: %v", cfg.Branch, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.logf(s, "info", "本地分支与配置不一致(%s → %s),已自动切换", cur, cfg.Branch)
|
e.logf(s, "info", "本地分支与配置不一致(%s → %s),已自动切换", cur, cfg.Branch)
|
||||||
} else {
|
} else {
|
||||||
// 本地分支不存在,创建(不会覆盖任何内容)
|
|
||||||
if remoteHasBranch {
|
if remoteHasBranch {
|
||||||
if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-b", cfg.Branch, tip); err != nil {
|
if _, err := gitRun(cfg.RepoPath, 30*time.Second, "checkout", "-b", cfg.Branch, tip); err != nil {
|
||||||
e.setFail(s, fmt.Sprintf("基于远程 %s/%s 创建分支失败: %v", remote, cfg.Branch, err))
|
e.setFail(s, fmt.Sprintf("基于远程 %s/%s 创建分支失败: %v", remote, cfg.Branch, err))
|
||||||
@@ -561,7 +564,7 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动提交(若有未提交改动)
|
// 自动提交
|
||||||
dirty := dirtyCount(cfg.RepoPath)
|
dirty := dirtyCount(cfg.RepoPath)
|
||||||
if dirty > 0 {
|
if dirty > 0 {
|
||||||
if cfg.AutoCommit {
|
if cfg.AutoCommit {
|
||||||
@@ -582,7 +585,7 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算初始 ahead/behind
|
// 计算 ahead/behind
|
||||||
var ahead, behind int
|
var ahead, behind int
|
||||||
var err error
|
var err error
|
||||||
tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch)
|
tip, remoteHasBranch := remoteTip(cfg.RepoPath, remote, cfg.Branch)
|
||||||
@@ -593,7 +596,6 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 远程分支不存在:认为所有本地提交领先
|
|
||||||
if out, cerr := gitRun(cfg.RepoPath, 15*time.Second, "rev-list", "--count", "HEAD"); cerr == nil {
|
if out, cerr := gitRun(cfg.RepoPath, 15*time.Second, "rev-list", "--count", "HEAD"); cerr == nil {
|
||||||
ahead, _ = strconv.Atoi(strings.TrimSpace(out))
|
ahead, _ = strconv.Atoi(strings.TrimSpace(out))
|
||||||
}
|
}
|
||||||
@@ -604,12 +606,11 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
e.pushStatus(s)
|
e.pushStatus(s)
|
||||||
|
|
||||||
// FIX: 先拉取(pull),再推送(push)
|
// ---- 先拉取 ----
|
||||||
if behind > 0 && (cfg.Mode == "pull" || cfg.Mode == "both") {
|
if behind > 0 && (cfg.Mode == "pull" || cfg.Mode == "both") {
|
||||||
if !e.pullRemote(s, cfg, remote, behind) {
|
if !e.pullRemote(s, cfg, remote, behind) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 拉取后重新计算 ahead/behind
|
|
||||||
ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch)
|
ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch)
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.ahead, s.behind = ahead, behind
|
s.ahead, s.behind = ahead, behind
|
||||||
@@ -617,14 +618,25 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
e.pushStatus(s)
|
e.pushStatus(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 再推送 ----
|
||||||
if ahead > 0 && (cfg.Mode == "push" || cfg.Mode == "both") {
|
if ahead > 0 && (cfg.Mode == "push" || cfg.Mode == "both") {
|
||||||
if !e.pushAll(s, cfg, ahead) {
|
// pushAll 可能返回错误,如果是 non-fast-forward,设置 push_conflict 状态
|
||||||
e.setFail(s, "推送失败")
|
pushOK, pushErr := e.pushAll(s, cfg, ahead)
|
||||||
|
if !pushOK {
|
||||||
|
// 检查是否是 non-fast-forward 错误
|
||||||
|
if pushErr != nil && strings.Contains(strings.ToLower(pushErr.Error()), "non-fast-forward") {
|
||||||
|
// 记录冲突远程(取第一个冲突的远程)
|
||||||
|
// 在 pushAll 内部已经设置了 pushConflict 状态,这里只需退出
|
||||||
|
e.logf(s, "warn", "推送被拒绝(non-fast-forward),等待用户决策")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 其他错误
|
||||||
|
e.setFail(s, "推送失败: "+pushErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终刷新状态
|
// 最终刷新
|
||||||
ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch)
|
ahead, behind, _ = aheadBehind(cfg.RepoPath, remote, cfg.Branch)
|
||||||
now := time.Now().Format("2006-01-02 15:04:05")
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -632,13 +644,20 @@ func (e *Engine) syncOnce(s *repoState, trigger string) {
|
|||||||
s.status = "ok"
|
s.status = "ok"
|
||||||
s.lastError = ""
|
s.lastError = ""
|
||||||
s.lastSync = &SyncResult{Time: now, Result: "ok", Detail: "同步完成"}
|
s.lastSync = &SyncResult{Time: now, Result: "ok", Detail: "同步完成"}
|
||||||
|
// 清除可能遗留的推送冲突状态
|
||||||
|
s.pushConflict = false
|
||||||
|
s.pushConflictRemote = ""
|
||||||
|
s.pushConflictBranch = ""
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
e.logf(s, "info", "=== 同步完成(本地领先 %d,落后 %d)===", ahead, behind)
|
e.logf(s, "info", "=== 同步完成(本地领先 %d,落后 %d)===", ahead, behind)
|
||||||
e.pushStatus(s)
|
e.pushStatus(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) pushAll(s *repoState, cfg *RepoConfig, n int) bool {
|
// pushAll 返回 (是否全部成功, 错误信息)
|
||||||
|
// 如果遇到 non-fast-forward,会设置 pushConflict 状态并返回 false,错误包含 "non-fast-forward"
|
||||||
|
func (e *Engine) pushAll(s *repoState, cfg *RepoConfig, n int) (bool, error) {
|
||||||
allOK := true
|
allOK := true
|
||||||
|
var firstErr error
|
||||||
for _, r := range cfg.Remotes {
|
for _, r := range cfg.Remotes {
|
||||||
_, err := e.withRetry(s, "push → "+r.Name, func() (string, error) {
|
_, err := e.withRetry(s, "push → "+r.Name, func() (string, error) {
|
||||||
return gitRun(cfg.RepoPath, 300*time.Second, "push", r.Name, cfg.Branch)
|
return gitRun(cfg.RepoPath, 300*time.Second, "push", r.Name, cfg.Branch)
|
||||||
@@ -646,19 +665,38 @@ func (e *Engine) pushAll(s *repoState, cfg *RepoConfig, n int) bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
e.logf(s, "error", "推送至 %s 失败: %v", r.Name, err)
|
e.logf(s, "error", "推送至 %s 失败: %v", r.Name, err)
|
||||||
allOK = false
|
allOK = false
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
// 检测到 non-fast-forward,记录状态并继续尝试其他远程
|
||||||
|
if strings.Contains(strings.ToLower(err.Error()), "non-fast-forward") {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.status = "push_conflict"
|
||||||
|
s.pushConflict = true
|
||||||
|
s.pushConflictRemote = r.Name
|
||||||
|
s.pushConflictBranch = cfg.Branch
|
||||||
|
s.lastError = "推送被拒绝(non-fast-forward),请选择强制覆盖或放弃"
|
||||||
|
s.lastSync = &SyncResult{
|
||||||
|
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
Result: "push_conflict",
|
||||||
|
Detail: fmt.Sprintf("远程 %s 有新提交,推送被拒绝", r.Name),
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
e.pushStatus(s)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e.logf(s, "info", "推送至 %s 成功(%d 个提交)", r.Name, n)
|
e.logf(s, "info", "推送至 %s 成功(%d 个提交)", r.Name, n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allOK {
|
if allOK {
|
||||||
e.stat(s, "push", "success", n)
|
e.stat(s, "push", "success", n)
|
||||||
} else {
|
return true, nil
|
||||||
e.stat(s, "push", "failed", n)
|
|
||||||
}
|
}
|
||||||
return allOK
|
e.stat(s, "push", "failed", n)
|
||||||
|
return false, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: pullRemote 使用 withRetry 包裹主 pull 命令
|
// pullRemote 已使用重试
|
||||||
func (e *Engine) pullRemote(s *repoState, cfg *RepoConfig, remote string, n int) bool {
|
func (e *Engine) pullRemote(s *repoState, cfg *RepoConfig, remote string, n int) bool {
|
||||||
args := []string{"pull"}
|
args := []string{"pull"}
|
||||||
if cfg.ConflictStrategy == "rebase" {
|
if cfg.ConflictStrategy == "rebase" {
|
||||||
@@ -673,7 +711,6 @@ func (e *Engine) pullRemote(s *repoState, cfg *RepoConfig, remote string, n int)
|
|||||||
e.logf(s, "info", "拉取成功(%d 个提交)", n)
|
e.logf(s, "info", "拉取成功(%d 个提交)", n)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// 处理冲突
|
|
||||||
files, cerr := conflictFiles(cfg.RepoPath)
|
files, cerr := conflictFiles(cfg.RepoPath)
|
||||||
if cerr == nil && len(files) > 0 {
|
if cerr == nil && len(files) > 0 {
|
||||||
if cfg.ConflictStrategy == "abort" {
|
if cfg.ConflictStrategy == "abort" {
|
||||||
@@ -699,8 +736,65 @@ func (e *Engine) pullRemote(s *repoState, cfg *RepoConfig, remote string, n int)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 手动操作 ----------
|
// ---------- 推送冲突处理 ----------
|
||||||
|
// ForcePush 强制推送(--force-with-lease)到冲突的远程
|
||||||
|
func (e *Engine) ForcePush(name string) error {
|
||||||
|
s := e.get(name)
|
||||||
|
if s == nil {
|
||||||
|
return fmt.Errorf("配置不存在: %s", name)
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
if !s.pushConflict {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return fmt.Errorf("当前没有待处理的推送冲突")
|
||||||
|
}
|
||||||
|
remote := s.pushConflictRemote
|
||||||
|
branch := s.pushConflictBranch
|
||||||
|
path := s.cfg.RepoPath
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
e.logf(s, "info", "执行强制推送至 %s %s", remote, branch)
|
||||||
|
_, err := gitRun(path, 300*time.Second, "push", "--force-with-lease", remote, branch)
|
||||||
|
if err != nil {
|
||||||
|
e.logf(s, "error", "强制推送失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.logf(s, "info", "强制推送成功")
|
||||||
|
s.mu.Lock()
|
||||||
|
s.pushConflict = false
|
||||||
|
s.pushConflictRemote = ""
|
||||||
|
s.pushConflictBranch = ""
|
||||||
|
s.status = "ok"
|
||||||
|
s.lastError = ""
|
||||||
|
s.lastSync = &SyncResult{Time: time.Now().Format("2006-01-02 15:04:05"), Result: "ok", Detail: "强制推送完成"}
|
||||||
|
s.mu.Unlock()
|
||||||
|
e.pushStatus(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbortPush 放弃强制推送,清除冲突状态
|
||||||
|
func (e *Engine) AbortPush(name string) error {
|
||||||
|
s := e.get(name)
|
||||||
|
if s == nil {
|
||||||
|
return fmt.Errorf("配置不存在: %s", name)
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if !s.pushConflict {
|
||||||
|
return fmt.Errorf("当前没有待处理的推送冲突")
|
||||||
|
}
|
||||||
|
s.pushConflict = false
|
||||||
|
s.pushConflictRemote = ""
|
||||||
|
s.pushConflictBranch = ""
|
||||||
|
s.status = "ok"
|
||||||
|
s.lastError = "用户放弃强制推送"
|
||||||
|
s.lastSync = &SyncResult{Time: time.Now().Format("2006-01-02 15:04:05"), Result: "ok", Detail: "放弃强制推送,状态已清除"}
|
||||||
|
e.logf(s, "warn", "放弃强制推送")
|
||||||
|
e.pushStatus(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 手动操作 ----------
|
||||||
func (e *Engine) ManualGit(name, action string) error {
|
func (e *Engine) ManualGit(name, action string) error {
|
||||||
s := e.get(name)
|
s := e.get(name)
|
||||||
if s == nil {
|
if s == nil {
|
||||||
@@ -739,7 +833,6 @@ func (e *Engine) ManualGit(name, action string) error {
|
|||||||
e.logf(s, "info", "fetch 完成")
|
e.logf(s, "info", "fetch 完成")
|
||||||
e.refreshCounts(s, cfg, remote)
|
e.refreshCounts(s, cfg, remote)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "push":
|
case "push":
|
||||||
e.logf(s, "info", "手动 push…")
|
e.logf(s, "info", "手动 push…")
|
||||||
ahead, behind, _ := aheadBehind(cfg.RepoPath, remote, cfg.Branch)
|
ahead, behind, _ := aheadBehind(cfg.RepoPath, remote, cfg.Branch)
|
||||||
@@ -751,14 +844,14 @@ func (e *Engine) ManualGit(name, action string) error {
|
|||||||
e.refreshCounts(s, cfg, remote)
|
e.refreshCounts(s, cfg, remote)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !e.pushAll(s, cfg, ahead) {
|
ok, err := e.pushAll(s, cfg, ahead)
|
||||||
e.setFail(s, "推送失败")
|
if !ok {
|
||||||
return fmt.Errorf("推送失败")
|
e.logf(s, "error", "推送失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
e.refreshCounts(s, cfg, remote)
|
e.refreshCounts(s, cfg, remote)
|
||||||
e.setOK(s, "手动推送完成")
|
e.setOK(s, "手动推送完成")
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "pull":
|
case "pull":
|
||||||
e.logf(s, "info", "手动 pull…")
|
e.logf(s, "info", "手动 pull…")
|
||||||
if _, err := gitRun(cfg.RepoPath, 90*time.Second, "fetch", remote); err != nil {
|
if _, err := gitRun(cfg.RepoPath, 90*time.Second, "fetch", remote); err != nil {
|
||||||
@@ -782,7 +875,6 @@ func (e *Engine) ManualGit(name, action string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 冲突处理 ----------
|
// ---------- 冲突处理 ----------
|
||||||
|
|
||||||
func (e *Engine) ConflictAbort(name string) error {
|
func (e *Engine) ConflictAbort(name string) error {
|
||||||
s := e.get(name)
|
s := e.get(name)
|
||||||
if s == nil {
|
if s == nil {
|
||||||
@@ -836,7 +928,6 @@ func (e *Engine) ConflictResolve(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 配置 CRUD ----------
|
// ---------- 配置 CRUD ----------
|
||||||
|
|
||||||
func (e *Engine) Create(c *RepoConfig) error {
|
func (e *Engine) Create(c *RepoConfig) error {
|
||||||
if err := validateRepoConfig(e.cfgDir, c, true); err != nil {
|
if err := validateRepoConfig(e.cfgDir, c, true); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -877,6 +968,10 @@ func (e *Engine) Update(c *RepoConfig) error {
|
|||||||
s.lastError = ""
|
s.lastError = ""
|
||||||
s.ahead, s.behind, s.dirty = 0, 0, 0
|
s.ahead, s.behind, s.dirty = 0, 0, 0
|
||||||
s.lastSync = nil
|
s.lastSync = nil
|
||||||
|
// 清除推送冲突
|
||||||
|
s.pushConflict = false
|
||||||
|
s.pushConflictRemote = ""
|
||||||
|
s.pushConflictBranch = ""
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
e.logf(s, "info", "配置已更新")
|
e.logf(s, "info", "配置已更新")
|
||||||
if wasRunning {
|
if wasRunning {
|
||||||
@@ -903,8 +998,7 @@ func (e *Engine) Delete(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 日志 / 统计 / 冲突 diff 读取 ----------
|
// ---------- 日志 / 统计 / 冲突 diff ----------
|
||||||
|
|
||||||
type StatRow struct {
|
type StatRow struct {
|
||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
@@ -985,7 +1079,6 @@ func (e *Engine) ConflictDiffs(name string) []ConflictFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 提交历史 ----------
|
// ---------- 提交历史 ----------
|
||||||
|
|
||||||
var hashRe = regexp.MustCompile(`^[0-9a-fA-F]{6,40}$`)
|
var hashRe = regexp.MustCompile(`^[0-9a-fA-F]{6,40}$`)
|
||||||
|
|
||||||
type Commit struct {
|
type Commit struct {
|
||||||
|
|||||||
@@ -328,3 +328,6 @@ hint: its remote counterpart. If you want to integrate the remote changes,
|
|||||||
hint: use 'git pull' before pushing again.
|
hint: use 'git pull' before pushing again.
|
||||||
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
|
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
|
||||||
[2026-09-08 18:55:22] 推送失败,本轮同步已终止(跳过拉取以避免分叉加剧)
|
[2026-09-08 18:55:22] 推送失败,本轮同步已终止(跳过拉取以避免分叉加剧)
|
||||||
|
[2026-09-08 19:13:17] 监控已启动
|
||||||
|
[2026-09-08 19:13:17] === 开始同步(启动检查,模式 push)===
|
||||||
|
[2026-09-08 19:13:24] 检测到 3 个未提交改动,执行自动提交
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ func NewServer(e *Engine, static fs.FS, password string) *Server {
|
|||||||
mux.HandleFunc("POST /api/repos/{name}/stop", s.wrapRepo(s.stopRepo))
|
mux.HandleFunc("POST /api/repos/{name}/stop", s.wrapRepo(s.stopRepo))
|
||||||
mux.HandleFunc("POST /api/repos/{name}/sync", s.wrapRepo(s.syncRepo))
|
mux.HandleFunc("POST /api/repos/{name}/sync", s.wrapRepo(s.syncRepo))
|
||||||
mux.HandleFunc("POST /api/repos/{name}/action", s.wrapRepo(s.actionRepo))
|
mux.HandleFunc("POST /api/repos/{name}/action", s.wrapRepo(s.actionRepo))
|
||||||
|
mux.HandleFunc("POST /api/repos/{name}/force-push", s.wrapRepo(s.forcePushRepo))
|
||||||
|
mux.HandleFunc("POST /api/repos/{name}/abort-push", s.wrapRepo(s.abortPushRepo))
|
||||||
mux.HandleFunc("GET /api/repos/{name}/conflicts", s.wrapRepo(s.conflictsRepo))
|
mux.HandleFunc("GET /api/repos/{name}/conflicts", s.wrapRepo(s.conflictsRepo))
|
||||||
mux.HandleFunc("GET /api/repos/{name}/log", s.wrapRepo(s.logRepo))
|
mux.HandleFunc("GET /api/repos/{name}/log", s.wrapRepo(s.logRepo))
|
||||||
mux.HandleFunc("GET /api/repos/{name}/stats", s.wrapRepo(s.statsRepo))
|
mux.HandleFunc("GET /api/repos/{name}/stats", s.wrapRepo(s.statsRepo))
|
||||||
@@ -52,6 +54,7 @@ func NewServer(e *Engine, static fs.FS, password string) *Server {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// 访问密码保护:静态资源开放,/api/* 需要会话(登录接口除外)
|
// 访问密码保护:静态资源开放,/api/* 需要会话(登录接口除外)
|
||||||
if s.password != "" && strings.HasPrefix(r.URL.Path, "/api/") &&
|
if s.password != "" && strings.HasPrefix(r.URL.Path, "/api/") &&
|
||||||
@@ -311,3 +314,19 @@ func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) forcePushRepo(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
if err := s.engine.ForcePush(name); err != nil {
|
||||||
|
writeErr(w, 400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, 200, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) abortPushRepo(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
if err := s.engine.AbortPush(name); err != nil {
|
||||||
|
writeErr(w, 400, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, 200, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user