diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-11 18:49:14 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-11 18:49:14 +0200 |
| commit | ad84bcb992ba0552d582f8a6d53ac330f799a955 (patch) | |
| tree | 2c3d386e090a8a52a45f52b0b424abb4143c0062 /internal | |
| parent | 0011f18e8494a4e57dc277b826d56c0a1df041ce (diff) | |
feat(sync): enforce daily repo sync intervals
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/flags.go | 2 | ||||
| -rw-r--r-- | internal/cli/sync_handlers.go | 201 | ||||
| -rw-r--r-- | internal/cli/throttle.go | 78 | ||||
| -rw-r--r-- | internal/cli/throttle_test.go | 108 | ||||
| -rw-r--r-- | internal/cmd/sync.go | 6 | ||||
| -rw-r--r-- | internal/state/state.go | 19 |
6 files changed, 282 insertions, 132 deletions
diff --git a/internal/cli/flags.go b/internal/cli/flags.go index bb00a39..8c69797 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -67,7 +67,7 @@ func ParseFlags() *Flags { flag.StringVar(&f.DeleteRepo, "delete-repo", "", "delete specified repository from all configured organizations (with confirmation)") flag.BoolVar(&f.Backup, "backup", false, "enable syncing to backup locations") flag.BoolVar(&f.Showcase, "showcase", false, "generate project showcase using AI (amp by default) after syncing") - flag.BoolVar(&f.Force, "force", false, "force regeneration of cached data") + flag.BoolVar(&f.Force, "force", false, "force operations even when cache or sync interval checks would skip work") flag.BoolVar(&f.BatchRun, "batch-run", false, "enable --full and --showcase (runs only once per week)") flag.BoolVar(&f.CheckReleases, "check-releases", false, "manually check for version tags without releases and create them (with confirmation)") flag.BoolVar(&f.NoCheckReleases, "no-check-releases", false, "disable automatic release checking after sync operations") diff --git a/internal/cli/sync_handlers.go b/internal/cli/sync_handlers.go index 8fb3a93..6a96b92 100644 --- a/internal/cli/sync_handlers.go +++ b/internal/cli/sync_handlers.go @@ -14,30 +14,24 @@ import ( // HandleSync handles syncing a single repository func HandleSync(cfg *config.Config, flags *Flags) int { - var throttleManager *state.Manager - var throttleState *state.State - if flags.Throttle { - manager, st, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - throttleManager = manager - throttleState = st + stateManager, syncState, err := loadSyncState(flags.WorkDir) + if err != nil { + fmt.Printf("Warning: Failed to load sync state: %v\n", err) + } - decision := evaluateThrottle(flags.SyncRepo, throttleState, flags.DryRun) - if decision.Message != "" { - fmt.Println(decision.Message) - } - if decision.SetNextAllowed && throttleManager != nil && !flags.DryRun { - throttleState.SetNextRepoSyncAllowed(flags.SyncRepo, decision.NextAllowed) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) - } - } - if decision.Skip { - return 0 + decision := evaluateSyncPolicy(flags.SyncRepo, syncState, flags.DryRun, flags.Force, flags.Throttle) + if decision.Message != "" { + fmt.Println(decision.Message) + } + if decision.SetNextAllowed && stateManager != nil && !flags.DryRun { + syncState.SetNextRepoSyncAllowed(flags.SyncRepo, decision.NextAllowed) + if err := stateManager.Save(syncState); err != nil { + fmt.Printf("Warning: Failed to save sync state: %v\n", err) } } + if decision.Skip { + return 0 + } // If create-github-repos is enabled, create the repo if needed if flags.CreateGitHubRepos { @@ -62,10 +56,10 @@ func HandleSync(cfg *config.Config, flags *Flags) int { return 1 } - if flags.Throttle && throttleManager != nil { - updateRepoSyncState(flags.SyncRepo, throttleState) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) + if stateManager != nil { + recordRepoSync(flags.SyncRepo, syncState, flags.Throttle) + if err := stateManager.Save(syncState); err != nil { + fmt.Printf("Warning: Failed to save sync state: %v\n", err) } } @@ -87,15 +81,9 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int { repoNames := shuffledRepoNames(cfg.Repositories) - var throttleManager *state.Manager - var throttleState *state.State - if flags.Throttle { - manager, st, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - throttleManager = manager - throttleState = st + stateManager, syncState, err := loadSyncState(flags.WorkDir) + if err != nil { + fmt.Printf("Warning: Failed to load sync state: %v\n", err) } // Initialize GitHub client if needed @@ -127,21 +115,19 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int { for i, repo := range repoNames { fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repo) - if flags.Throttle { - decision := evaluateThrottle(repo, throttleState, flags.DryRun) - if decision.Message != "" { - fmt.Println(decision.Message) - } - if decision.SetNextAllowed && throttleManager != nil && !flags.DryRun { - throttleState.SetNextRepoSyncAllowed(repo, decision.NextAllowed) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) - } - } - if decision.Skip { - continue + decision := evaluateSyncPolicy(repo, syncState, flags.DryRun, flags.Force, flags.Throttle) + if decision.Message != "" { + fmt.Println(decision.Message) + } + if decision.SetNextAllowed && stateManager != nil && !flags.DryRun { + syncState.SetNextRepoSyncAllowed(repo, decision.NextAllowed) + if err := stateManager.Save(syncState); err != nil { + fmt.Printf("Warning: Failed to save sync state: %v\n", err) } } + if decision.Skip { + continue + } // Create GitHub repo if needed if hasGithubClient { @@ -165,10 +151,10 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int { fmt.Printf("Stopping sync due to error.\n") return 1 } - if flags.Throttle && throttleManager != nil { - updateRepoSyncState(repo, throttleState) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) + if stateManager != nil { + recordRepoSync(repo, syncState, flags.Throttle) + if err := stateManager.Save(syncState); err != nil { + fmt.Printf("Warning: Failed to save sync state: %v\n", err) } } successCount++ @@ -223,23 +209,8 @@ func HandleSyncCodebergPublic(cfg *config.Config, flags *Flags) int { return 0 } - if flags.Throttle && flags.DryRun { - _, throttleState, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - filtered := make([]string, 0, len(repoNames)) - for _, name := range repoNames { - decision := evaluateThrottle(name, throttleState, true) - if decision.Message != "" { - fmt.Println(decision.Message) - } - if decision.Skip { - continue - } - filtered = append(filtered, name) - } - repoNames = filtered + if flags.DryRun { + repoNames = filterDryRunRepoNames(repoNames, flags) } repoNames = shuffledRepoNames(repoNames) @@ -295,23 +266,8 @@ func HandleSyncGitHubPublic(cfg *config.Config, flags *Flags) int { return 0 } - if flags.Throttle && flags.DryRun { - _, throttleState, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - filtered := make([]string, 0, len(repoNames)) - for _, name := range repoNames { - decision := evaluateThrottle(name, throttleState, true) - if decision.Message != "" { - fmt.Println(decision.Message) - } - if decision.Skip { - continue - } - filtered = append(filtered, name) - } - repoNames = filtered + if flags.DryRun { + repoNames = filterDryRunRepoNames(repoNames, flags) } repoNames = shuffledRepoNames(repoNames) @@ -418,6 +374,27 @@ func showReposToSync(repoNames []string) { } } +func filterDryRunRepoNames(repoNames []string, flags *Flags) []string { + _, syncState, err := loadSyncState(flags.WorkDir) + if err != nil { + fmt.Printf("Warning: Failed to load sync state: %v\n", err) + } + + filtered := make([]string, 0, len(repoNames)) + for _, repoName := range repoNames { + decision := evaluateSyncPolicy(repoName, syncState, true, flags.Force, flags.Throttle) + if decision.Message != "" { + fmt.Println(decision.Message) + } + if decision.Skip { + continue + } + filtered = append(filtered, repoName) + } + + return filtered +} + func shuffledRepoNames(repoNames []string) []string { shuffled := append([]string(nil), repoNames...) rand.Shuffle(len(shuffled), func(i, j int) { @@ -433,10 +410,10 @@ func printFullSyncSeparator() { } type syncExecution struct { - syncer *sync.Syncer - descCache map[string]string - throttleManager *state.Manager - throttleState *state.State + syncer *sync.Syncer + descCache map[string]string + stateManager *state.Manager + syncState *state.State } func newSyncExecution(cfg *config.Config, flags *Flags) *syncExecution { @@ -446,45 +423,39 @@ func newSyncExecution(cfg *config.Config, flags *Flags) *syncExecution { } execution.syncer.SetBackupEnabled(flags.Backup) - if flags.Throttle { - manager, st, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - execution.throttleManager = manager - execution.throttleState = st + manager, st, err := loadSyncState(flags.WorkDir) + if err != nil { + fmt.Printf("Warning: Failed to load sync state: %v\n", err) } + execution.stateManager = manager + execution.syncState = st return execution } -func (e *syncExecution) maybeThrottle(repoName string, flags *Flags) bool { - if !flags.Throttle { - return false - } - - decision := evaluateThrottle(repoName, e.throttleState, flags.DryRun) +func (e *syncExecution) maybeSkipRepo(repoName string, flags *Flags) bool { + decision := evaluateSyncPolicy(repoName, e.syncState, flags.DryRun, flags.Force, flags.Throttle) if decision.Message != "" { fmt.Println(decision.Message) } - if decision.SetNextAllowed && e.throttleManager != nil && !flags.DryRun { - e.throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed) - if err := e.throttleManager.Save(e.throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) + if decision.SetNextAllowed && e.stateManager != nil && !flags.DryRun { + e.syncState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed) + if err := e.stateManager.Save(e.syncState); err != nil { + fmt.Printf("Warning: Failed to save sync state: %v\n", err) } } return decision.Skip } -func (e *syncExecution) markSynced(repoName string, flags *Flags) { - if !flags.Throttle || e.throttleManager == nil { +func (e *syncExecution) markRepoSynced(repoName string, flags *Flags) { + if e.stateManager == nil || flags.DryRun { return } - updateRepoSyncState(repoName, e.throttleState) - if err := e.throttleManager.Save(e.throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) + recordRepoSync(repoName, e.syncState, flags.Throttle) + if err := e.stateManager.Save(e.syncState); err != nil { + fmt.Printf("Warning: Failed to save sync state: %v\n", err) } } @@ -556,7 +527,7 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi for i, repoName := range repoNames { fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName) - if execution.maybeThrottle(repoName, flags) { + if execution.maybeSkipRepo(repoName, flags) { continue } @@ -580,7 +551,7 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi fmt.Printf("Stopping sync due to error.\n") return 1 } - execution.markSynced(repoName, flags) + execution.markRepoSynced(repoName, flags) successCount++ // After syncing, sync descriptions according to precedence @@ -627,7 +598,7 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository for i, repoName := range repoNames { fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName) - if execution.maybeThrottle(repoName, flags) { + if execution.maybeSkipRepo(repoName, flags) { continue } @@ -651,7 +622,7 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository fmt.Printf("Stopping sync due to error.\n") return 1 } - execution.markSynced(repoName, flags) + execution.markRepoSynced(repoName, flags) successCount++ // After syncing, sync descriptions according to precedence diff --git a/internal/cli/throttle.go b/internal/cli/throttle.go index b48094e..d285350 100644 --- a/internal/cli/throttle.go +++ b/internal/cli/throttle.go @@ -13,12 +13,13 @@ import ( ) const ( - throttleMinDays = 60 - throttleMaxDays = 120 - recentDays = 7 + defaultSyncInterval = 24 * time.Hour + throttleMinDays = 60 + throttleMaxDays = 120 + recentDays = 7 ) -func loadThrottleState(workDir string) (*state.Manager, *state.State, error) { +func loadSyncState(workDir string) (*state.Manager, *state.State, error) { manager := state.NewManager(workDir) st, err := manager.Load() if err != nil { @@ -30,14 +31,57 @@ func loadThrottleState(workDir string) (*state.Manager, *state.State, error) { return manager, st, nil } -type throttleDecision struct { +type syncDecision struct { Skip bool Message string NextAllowed time.Time SetNextAllowed bool } -func evaluateThrottle(repoName string, st *state.State, dryRun bool) throttleDecision { +func evaluateSyncPolicy(repoName string, st *state.State, dryRun bool, force bool, throttle bool) syncDecision { + if force { + return syncDecision{} + } + + decision := evaluateDailySync(repoName, st, dryRun) + if decision.Skip || !throttle { + return decision + } + + return evaluateThrottle(repoName, st, dryRun) +} + +func evaluateDailySync(repoName string, st *state.State, dryRun bool) syncDecision { + if st == nil { + return syncDecision{} + } + + lastSync := st.GetLastRepoSync(repoName) + if lastSync.IsZero() { + return syncDecision{} + } + + nextAllowed := lastSync.Add(defaultSyncInterval) + if time.Now().Before(nextAllowed) { + skipAction := "Skipping" + if dryRun { + skipAction = "[DRY RUN] Would skip" + } + + return syncDecision{ + Skip: true, + Message: fmt.Sprintf("%s %s: last synced at %s; next sync after %s. Use --force to override.", + skipAction, + repoName, + lastSync.Format("2006-01-02 15:04"), + nextAllowed.Format("2006-01-02 15:04")), + } + } + + return syncDecision{} +} + +func evaluateThrottle(repoName string, st *state.State, dryRun bool) syncDecision { syncAction := "Syncing" if dryRun { syncAction = "[DRY RUN] Would sync" @@ -49,14 +93,14 @@ func evaluateThrottle(repoName string, st *state.State, dryRun bool) throttleDec if dryRun { actionMsg = "Sync would proceed" } - return throttleDecision{ + return syncDecision{ Skip: false, Message: fmt.Sprintf("Warning: failed to check local activity for %s: %v. %s.", repoName, err, actionMsg), } } if recent { - return throttleDecision{ + return syncDecision{ Skip: false, Message: fmt.Sprintf("%s %s: recent local commits within last %d days.", syncAction, repoName, recentDays), } @@ -64,7 +108,7 @@ func evaluateThrottle(repoName string, st *state.State, dryRun bool) throttleDec now := time.Now() if st == nil { - return throttleDecision{ + return syncDecision{ Skip: false, Message: fmt.Sprintf("%s %s: no recent local commits; throttle state unavailable.", syncAction, repoName), } @@ -82,7 +126,7 @@ func evaluateThrottle(repoName string, st *state.State, dryRun bool) throttleDec } else { nextAllowed = now.Add(randomThrottleDuration()) } - return throttleDecision{ + return syncDecision{ Skip: true, NextAllowed: nextAllowed, SetNextAllowed: true, @@ -92,25 +136,29 @@ func evaluateThrottle(repoName string, st *state.State, dryRun bool) throttleDec } if now.Before(nextAllowed) { - return throttleDecision{ + return syncDecision{ 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{ + return syncDecision{ 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) { +func recordRepoSync(repoName string, st *state.State, throttle bool) { if st == nil { return } now := time.Now() - nextAllowed := now.Add(randomThrottleDuration()) - st.SetRepoSync(repoName, now, nextAllowed) + st.SetLastRepoSync(repoName, now) + if throttle { + st.SetNextRepoSyncAllowed(repoName, now.Add(randomThrottleDuration())) + return + } + st.ClearNextRepoSyncAllowed(repoName) } func randomThrottleDuration() time.Duration { diff --git a/internal/cli/throttle_test.go b/internal/cli/throttle_test.go new file mode 100644 index 0000000..6833fe4 --- /dev/null +++ b/internal/cli/throttle_test.go @@ -0,0 +1,108 @@ +package cli + +import ( + "strings" + "testing" + "time" + + "codeberg.org/snonux/gitsyncer/internal/state" +) + +func TestEvaluateSyncPolicy_SkipsRepoSyncedWithinDay(t *testing.T) { + st := &state.State{} + st.SetLastRepoSync("repo", time.Now().Add(-23*time.Hour)) + + decision := evaluateSyncPolicy("repo", st, false, false, false) + + if !decision.Skip { + t.Fatal("expected repo synced within 24 hours to be skipped") + } + if !strings.Contains(decision.Message, "Use --force to override.") { + t.Fatalf("expected force override hint, got %q", decision.Message) + } +} + +func TestEvaluateSyncPolicy_AllowsRepoAfterDailyWindow(t *testing.T) { + st := &state.State{} + st.SetLastRepoSync("repo", time.Now().Add(-25*time.Hour)) + + decision := evaluateSyncPolicy("repo", st, false, false, false) + + if decision.Skip { + t.Fatalf("expected repo synced more than 24 hours ago to proceed, got %q", decision.Message) + } +} + +func TestEvaluateSyncPolicy_ForceBypassesDailyAndThrottleLimits(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + st := &state.State{} + now := time.Now() + st.SetRepoSync("repo", now.Add(-1*time.Hour), now.Add(30*24*time.Hour)) + + decision := evaluateSyncPolicy("repo", st, false, true, true) + + if decision.Skip { + t.Fatalf("expected --force to bypass sync limits, got %q", decision.Message) + } + if decision.SetNextAllowed { + t.Fatal("did not expect --force to request throttle-window persistence") + } +} + +func TestEvaluateSyncPolicy_ThrottleSetsWindowWhenRepoIsIdle(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + start := time.Now() + decision := evaluateSyncPolicy("repo", &state.State{}, false, false, true) + end := time.Now() + + if !decision.Skip { + t.Fatal("expected idle repo to be skipped when throttle is enabled") + } + if !decision.SetNextAllowed { + t.Fatal("expected throttle evaluation to request a persisted next-allowed time") + } + + minAllowed := start.Add(throttleMinDays * 24 * time.Hour) + maxAllowed := end.Add(throttleMaxDays*24*time.Hour + time.Minute) + if decision.NextAllowed.Before(minAllowed) || decision.NextAllowed.After(maxAllowed) { + t.Fatalf("expected throttle window between %s and %s, got %s", minAllowed, maxAllowed, decision.NextAllowed) + } +} + +func TestRecordRepoSync_ClearsThrottleWindowWhenThrottleDisabled(t *testing.T) { + st := &state.State{} + st.SetRepoSync("repo", time.Now().Add(-72*time.Hour), time.Now().Add(72*time.Hour)) + + recordRepoSync("repo", st, false) + + if st.GetLastRepoSync("repo").IsZero() { + t.Fatal("expected last sync time to be recorded") + } + if !st.GetNextRepoSyncAllowed("repo").IsZero() { + t.Fatal("expected throttle window to be cleared when throttle is disabled") + } +} + +func TestRecordRepoSync_SetsThrottleWindowWhenThrottleEnabled(t *testing.T) { + st := &state.State{} + + recordRepoSync("repo", st, true) + + lastSync := st.GetLastRepoSync("repo") + if lastSync.IsZero() { + t.Fatal("expected last sync time to be recorded") + } + + nextAllowed := st.GetNextRepoSyncAllowed("repo") + if nextAllowed.IsZero() { + t.Fatal("expected throttle window to be recorded") + } + + minAllowed := lastSync.Add(throttleMinDays * 24 * time.Hour) + maxAllowed := lastSync.Add(throttleMaxDays*24*time.Hour + time.Minute) + if nextAllowed.Before(minAllowed) || nextAllowed.After(maxAllowed) { + t.Fatalf("expected throttle window between %s and %s, got %s", minAllowed, maxAllowed, nextAllowed) + } +} diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go index df7aa5b..5681ccc 100644 --- a/internal/cmd/sync.go +++ b/internal/cmd/sync.go @@ -16,6 +16,7 @@ var ( noAIReleaseNotes bool syncAITool string throttle bool + syncForce bool ) var syncCmd = &cobra.Command{ @@ -39,6 +40,9 @@ var syncRepoCmd = &cobra.Command{ # Preview what would be synced gitsyncer sync repo myproject --dry-run + + # Override sync interval checks + gitsyncer sync repo myproject --force # Sync without AI-generated release notes gitsyncer sync repo myproject --no-ai-release-notes @@ -191,6 +195,7 @@ func init() { syncCmd.PersistentFlags().BoolVar(&autoCreate, "auto-create-releases", false, "automatically create releases without confirmation") syncCmd.PersistentFlags().BoolVar(&noAIReleaseNotes, "no-ai-release-notes", false, "disable AI-generated release notes (AI notes are enabled by default)") syncCmd.PersistentFlags().StringVar(&syncAITool, "ai-tool", "amp", "AI tool to use for release notes when auto-creating (amp, claude, aichat, or hexai; amp is tried first if available)") + syncCmd.PersistentFlags().BoolVarP(&syncForce, "force", "f", false, "force sync even if normal sync interval checks would skip a repository") syncCmd.PersistentFlags().BoolVar(&throttle, "throttle", false, "throttle syncing based on local repo activity") } @@ -204,6 +209,7 @@ func buildFlags() *cli.Flags { AutoCreateReleases: autoCreate, AIReleaseNotes: !noAIReleaseNotes, AITool: syncAITool, + Force: syncForce, Throttle: throttle, CreateGitHubRepos: createRepos, CreateCodebergRepos: createRepos, diff --git a/internal/state/state.go b/internal/state/state.go index 3288db1..4d5f197 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -11,7 +11,7 @@ import ( // State represents the persistent state of gitsyncer type State struct { LastBatchRun time.Time `json:"lastBatchRun"` - // Per-repo sync tracking for throttling + // Per-repo sync tracking for default daily sync limits and optional throttling LastRepoSync map[string]time.Time `json:"lastRepoSync,omitempty"` NextRepoSyncAllowed map[string]time.Time `json:"nextRepoSyncAllowed,omitempty"` } @@ -117,6 +117,15 @@ func (s *State) SetRepoSync(repoName string, lastSync time.Time, nextAllowed tim s.NextRepoSyncAllowed[repoName] = nextAllowed } +// SetLastRepoSync updates only the last sync time for a repo. +func (s *State) SetLastRepoSync(repoName string, lastSync time.Time) { + if s == nil { + return + } + s.EnsureRepoMaps() + s.LastRepoSync[repoName] = lastSync +} + // SetNextRepoSyncAllowed updates only the next allowed sync time for a repo func (s *State) SetNextRepoSyncAllowed(repoName string, nextAllowed time.Time) { if s == nil { @@ -125,3 +134,11 @@ func (s *State) SetNextRepoSyncAllowed(repoName string, nextAllowed time.Time) { s.EnsureRepoMaps() s.NextRepoSyncAllowed[repoName] = nextAllowed } + +// ClearNextRepoSyncAllowed removes the next allowed sync time for a repo. +func (s *State) ClearNextRepoSyncAllowed(repoName string) { + if s == nil || s.NextRepoSyncAllowed == nil { + return + } + delete(s.NextRepoSyncAllowed, repoName) +} |
