Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 23 additions & 15 deletions internal/poller/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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
}
}
}

Expand Down
45 changes: 44 additions & 1 deletion internal/poller/poller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
23 changes: 17 additions & 6 deletions internal/tui/tui_wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:").
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down