diff --git a/internal/config/config.go b/internal/config/config.go index d55cdfe..c30b6ce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,11 +10,12 @@ import ( // Config represents the GitPoll configuration type Config struct { - RepoURL string `json:"repo_url,omitempty"` - RepoDir string `json:"repo_dir,omitempty"` - Branch string `json:"branch,omitempty"` - Command string `json:"command,omitempty"` - Interval time.Duration `json:"interval,omitempty"` + RepoURL string `json:"repo_url,omitempty"` + RepoDir string `json:"repo_dir,omitempty"` + Branch string `json:"branch,omitempty"` + Command string `json:"command,omitempty"` + Interval time.Duration `json:"interval,omitempty"` + ExecuteOnStartup bool `json:"execute_on_startup"` } // Marshal stringifies a value to JSON byte array diff --git a/internal/poller/poller.go b/internal/poller/poller.go index 58d5ac8..3c5f52a 100644 --- a/internal/poller/poller.go +++ b/internal/poller/poller.go @@ -88,10 +88,11 @@ type defaultPoller struct { client GitClient - baseInterval time.Duration - maxJitter time.Duration - backoffBase time.Duration - backoffMax time.Duration + baseInterval time.Duration + maxJitter time.Duration + backoffBase time.Duration + backoffMax time.Duration + executeOnStartup bool lastHash string } @@ -107,13 +108,14 @@ func NewPoller(cfg *config.Config, client GitClient) Poller { } return &defaultPoller{ - repoURL: cfg.RepoURL, - branch: cfg.Branch, - client: client, - baseInterval: interval, - maxJitter: 20 * time.Second, - backoffBase: 5 * time.Second, - backoffMax: 5 * time.Minute, + repoURL: cfg.RepoURL, + branch: cfg.Branch, + client: client, + baseInterval: interval, + maxJitter: 20 * time.Second, + backoffBase: 5 * time.Second, + backoffMax: 5 * time.Minute, + executeOnStartup: cfg.ExecuteOnStartup, } } @@ -153,11 +155,17 @@ func (p *defaultPoller) Start(ctx context.Context, out chan<- interface{}) { backoff = 0 if hash != "" && hash != p.lastHash { + isFirstPoll := p.lastHash == "" p.lastHash = hash - select { - case out <- events.UpdateDetectedMsg{NewHash: hash}: - case <-ctx.Done(): - return + + if isFirstPoll && !p.executeOnStartup { + // Skip emitting the initial update message + } else { + select { + case out <- events.UpdateDetectedMsg{NewHash: hash}: + case <-ctx.Done(): + return + } } } diff --git a/internal/poller/poller_test.go b/internal/poller/poller_test.go index 78a5849..5849a05 100644 --- a/internal/poller/poller_test.go +++ b/internal/poller/poller_test.go @@ -29,7 +29,7 @@ func TestPoller_BasicPolling(t *testing.T) { outCh := make(chan interface{}, 10) - cfg := &config.Config{RepoURL: "https://github.com/test/repo", Branch: "main"} + cfg := &config.Config{RepoURL: "https://github.com/test/repo", Branch: "main", ExecuteOnStartup: true} p := NewPoller(cfg, mockClient) p.(*defaultPoller).baseInterval = 10 * time.Millisecond p.(*defaultPoller).maxJitter = 5 * time.Millisecond @@ -68,6 +68,49 @@ func TestPoller_BasicPolling(t *testing.T) { } } +func TestPoller_BasicPolling_NoExecuteOnStartup(t *testing.T) { + mockClient := &mockGitClient{ + hashToReturn: "1234567890abcdef", + errToReturn: nil, + } + + outCh := make(chan interface{}, 10) + + cfg := &config.Config{RepoURL: "https://github.com/test/repo", Branch: "main", ExecuteOnStartup: false} + p := NewPoller(cfg, mockClient) + p.(*defaultPoller).baseInterval = 10 * time.Millisecond + p.(*defaultPoller).maxJitter = 5 * time.Millisecond + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go p.Start(ctx, outCh) + + // Since ExecuteOnStartup is false, the first update should not send an event + select { + case <-outCh: + t.Fatal("Expected no update message on first poll when ExecuteOnStartup is false") + case <-time.After(50 * time.Millisecond): + // Expected timeout + } + + mockClient.hashToReturn = "fedcba0987654321" + + // The second update with a new hash should send an event + select { + case msg := <-outCh: + updateMsg, ok := msg.(events.UpdateDetectedMsg) + if !ok { + t.Fatalf("Expected UpdateDetectedMsg, got %T", msg) + } + if updateMsg.NewHash != "fedcba0987654321" { + t.Errorf("Expected hash fedcba0987654321, got %s", updateMsg.NewHash) + } + case <-time.After(1 * time.Second): + t.Fatal("Timeout waiting for second polling update") + } +} + func TestPoller_ExponentialBackoff(t *testing.T) { mockClient := &mockGitClient{ errToReturn: errors.New("network error"), diff --git a/internal/tui/tui_wizard.go b/internal/tui/tui_wizard.go index 4b1b7d1..25e702d 100644 --- a/internal/tui/tui_wizard.go +++ b/internal/tui/tui_wizard.go @@ -40,6 +40,7 @@ func NewWizardModel(initialConfig *config.Config) *WizardModel { func (m *WizardModel) createForm() { var repoURL, repoDir, branch, command, intervalStr string var confirm bool + var executeOnStartup bool cwd, err := os.Getwd() if err != nil { @@ -91,6 +92,12 @@ func (m *WizardModel) createForm() { return nil }), ), + huh.NewGroup( + huh.NewConfirm(). + Key("executeOnStartup"). + Title("Execute command on startup regardless of git state?"). + Value(&executeOnStartup), + ), huh.NewGroup( huh.NewNote(). Title("Summary of settings:"). @@ -112,13 +119,15 @@ func (m *WizardModel) createForm() { if iStr == "" { iStr = "30" } + execOnStartup := m.form.GetBool("executeOnStartup") return fmt.Sprintf("\n"+ "Repository URL: %s\n"+ "Local Directory: %s\n"+ "Branch: %s\n"+ "Command: %s\n"+ - "Interval: %s seconds\n", url, dir, b, c, iStr) + "Interval: %s seconds\n"+ + "Execute on Startup: %t\n", url, dir, b, c, iStr, execOnStartup) }, &repoURL), // Note: huh DescriptionFunc expects exactly one dependency argument of type any in this version. // We can use a struct to track all dependencies if needed, but since we are navigating forward sequentially // without jumping, repoURL is technically enough to avoid compilation error while fulfilling the function signature. @@ -164,6 +173,7 @@ func (m *WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { branch := m.form.GetString("branch") command := m.form.GetString("command") intervalStr := m.form.GetString("interval") + executeOnStartup := m.form.GetBool("executeOnStartup") if repoDir == "" { cwd, err := os.Getwd() @@ -185,11 +195,12 @@ func (m *WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { intervalSeconds, _ := strconv.Atoi(intervalStr) newConfig := &config.Config{ - RepoURL: repoURL, - RepoDir: repoDir, - Branch: branch, - Command: command, - Interval: time.Duration(intervalSeconds) * time.Second, + RepoURL: repoURL, + RepoDir: repoDir, + Branch: branch, + Command: command, + Interval: time.Duration(intervalSeconds) * time.Second, + ExecuteOnStartup: executeOnStartup, } savePath := config.GetLocalConfigPath()