1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
package cli
import (
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"codeberg.org/snonux/gitsyncer/internal/state"
)
const (
throttleMinDays = 60
throttleMaxDays = 120
recentDays = 7
)
func loadThrottleState(workDir string) (*state.Manager, *state.State, error) {
manager := state.NewManager(workDir)
st, err := manager.Load()
if err != nil {
return manager, &state.State{}, err
}
if st == nil {
st = &state.State{}
}
return manager, st, nil
}
type throttleDecision struct {
Skip bool
Message string
NextAllowed time.Time
SetNextAllowed bool
}
func evaluateThrottle(repoName string, st *state.State, dryRun bool) throttleDecision {
syncAction := "Syncing"
if dryRun {
syncAction = "[DRY RUN] Would sync"
}
recent, err := hasRecentLocalCommits(repoName)
if err != nil {
actionMsg := "Sync will proceed"
if dryRun {
actionMsg = "Sync would proceed"
}
return throttleDecision{
Skip: false,
Message: fmt.Sprintf("Warning: failed to check local activity for %s: %v. %s.", repoName, err, actionMsg),
}
}
if recent {
return throttleDecision{
Skip: false,
Message: fmt.Sprintf("%s %s: recent local commits within last %d days.", syncAction, repoName, recentDays),
}
}
now := time.Now()
if st == nil {
return throttleDecision{
Skip: false,
Message: fmt.Sprintf("%s %s: no recent local commits; throttle state unavailable.", syncAction, repoName),
}
}
nextAllowed := st.GetNextRepoSyncAllowed(repoName)
skipAction := "Skipping"
if dryRun {
skipAction = "[DRY RUN] Would skip"
}
if nextAllowed.IsZero() {
lastSync := st.GetLastRepoSync(repoName)
if !lastSync.IsZero() {
nextAllowed = lastSync.Add(randomThrottleDuration())
} else {
nextAllowed = now.Add(randomThrottleDuration())
}
return throttleDecision{
Skip: true,
NextAllowed: nextAllowed,
SetNextAllowed: true,
Message: fmt.Sprintf("%s %s: no recent local commits; throttle window set until %s.",
skipAction, repoName, nextAllowed.Format("2006-01-02")),
}
}
if now.Before(nextAllowed) {
return throttleDecision{
Skip: true,
Message: fmt.Sprintf("%s %s: no recent local commits; next allowed sync at %s.", skipAction, repoName, nextAllowed.Format("2006-01-02")),
}
}
return throttleDecision{
Skip: false,
Message: fmt.Sprintf("%s %s: throttle window elapsed (next allowed was %s).", syncAction, repoName, nextAllowed.Format("2006-01-02")),
}
}
func updateRepoSyncState(repoName string, st *state.State) {
if st == nil {
return
}
now := time.Now()
nextAllowed := now.Add(randomThrottleDuration())
st.SetRepoSync(repoName, now, nextAllowed)
}
func randomThrottleDuration() time.Duration {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
days := throttleMinDays + rng.Intn(throttleMaxDays-throttleMinDays+1)
return time.Duration(days) * 24 * time.Hour
}
func hasRecentLocalCommits(repoName string) (bool, error) {
home, err := os.UserHomeDir()
if err != nil {
return false, fmt.Errorf("failed to resolve home directory: %w", err)
}
repoPath := filepath.Join(home, "git", repoName)
info, err := os.Stat(repoPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("failed to stat %s: %w", repoPath, err)
}
if !info.IsDir() {
return false, nil
}
cmd := exec.Command("git", "-C", repoPath, "log", "-1", "--since="+fmt.Sprintf("%d.days", recentDays), "--format=%ct")
output, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("git log failed for %s: %w", repoPath, err)
}
return strings.TrimSpace(string(output)) != "", nil
}
|