diff --git a/main.go b/main.go index 21f6528..2e1e1a7 100644 --- a/main.go +++ b/main.go @@ -192,6 +192,7 @@ func main() { verbose = flag.Bool("verbose", false, "Show verbose logging from libraries") excludeOrgs = flag.String("exclude-orgs", "", "Comma-separated list of orgs to exclude") includeStale = flag.Bool("include-stale", false, "Include PRs that haven't been modified in 90 days") + user = flag.String("user", "", "View PRs for specified user instead of authenticated user") ) flag.Parse() @@ -229,14 +230,21 @@ func main() { } logger.Print("INFO: Successfully retrieved GitHub token") - username, err := currentUser(ctx, token, logger, httpClient) - if err != nil { - logger.Printf("ERROR: Failed to get current user: %v", err) - fmt.Fprint(os.Stderr, "error: failed to identify github user\n") - cancel() - return + // Determine the username to use - either specified via --user or the authenticated user + var username string + if *user != "" { + username = *user + logger.Printf("INFO: Using specified user: %s", username) + } else { + username, err = currentUser(ctx, token, logger, httpClient) + if err != nil { + logger.Printf("ERROR: Failed to get current user: %v", err) + fmt.Fprint(os.Stderr, "error: failed to identify github user\n") + cancel() + return + } + logger.Printf("INFO: Authenticated as user: %s", username) } - logger.Printf("INFO: Authenticated as user: %s", username) // Set up turn client var turnClient *turn.Client @@ -809,9 +817,9 @@ type displayConfig struct { httpClient *http.Client turnClient *turn.Client lastDisplayHash *string - excludedOrgs []string token string username string + excludedOrgs []string blockingOnly bool verbose bool includeStale bool @@ -820,13 +828,13 @@ type displayConfig struct { // watchConfig holds configuration for watch mode. type watchConfig struct { - logger *log.Logger httpClient *http.Client turnClient *turn.Client - excludedOrgs []string + logger *log.Logger + org string token string username string - org string + excludedOrgs []string interval time.Duration blockingOnly bool notifyMode bool diff --git a/main_test.go b/main_test.go index cb47dee..300311a 100644 --- a/main_test.go +++ b/main_test.go @@ -247,3 +247,169 @@ func TestCategorizePRs(t *testing.T) { } } } + +func TestGeneratePRDisplayBlockedFlag(t *testing.T) { + username := "alice" + + // Create test PRs with blocking/non-blocking scenarios + incomingBlocked := PR{ + Number: 1, + Title: "Incoming blocked PR", + HTMLURL: "https://github.com/org/repo/pull/1", + User: User{Login: "bob"}, + RequestedReviewers: []User{{Login: "alice"}}, // alice is requested reviewer + } + + incomingNotBlocked := PR{ + Number: 2, + Title: "Incoming non-blocked PR", + HTMLURL: "https://github.com/org/repo/pull/2", + User: User{Login: "charlie"}, + RequestedReviewers: []User{{Login: "dave"}}, // alice is not requested reviewer + } + + outgoingBlocked := PR{ + Number: 3, + Title: "Outgoing blocked PR", + HTMLURL: "https://github.com/org/repo/pull/3", + User: User{Login: "alice"}, // alice is author + RequestedReviewers: []User{{Login: "alice"}}, // alice is requested reviewer (perhaps due to turn server logic) + } + + outgoingNotBlocked := PR{ + Number: 4, + Title: "Outgoing non-blocked PR", + HTMLURL: "https://github.com/org/repo/pull/4", + User: User{Login: "alice"}, // alice is author + RequestedReviewers: []User{{Login: "eve"}}, // alice is not requested reviewer + } + + allPRs := []PR{incomingBlocked, incomingNotBlocked, outgoingBlocked, outgoingNotBlocked} + + tests := []struct { + name string + prs []PR + blockingOnly bool + expectIncoming bool + expectOutgoing bool + expectIncomingCount int + expectOutgoingCount int + }{ + { + name: "normal mode shows all PRs", + prs: allPRs, + blockingOnly: false, + expectIncoming: true, + expectOutgoing: true, + expectIncomingCount: 2, // both incoming PRs + expectOutgoingCount: 2, // both outgoing PRs + }, + { + name: "blocked mode shows only blocked PRs", + prs: allPRs, + blockingOnly: true, + expectIncoming: true, // has blocked incoming + expectOutgoing: true, // has blocked outgoing + expectIncomingCount: 1, // only blocked incoming + expectOutgoingCount: 1, // only blocked outgoing + }, + { + name: "blocked mode with only incoming blocked", + prs: []PR{incomingBlocked, incomingNotBlocked, outgoingNotBlocked}, + blockingOnly: true, + expectIncoming: true, // has blocked incoming + expectOutgoing: false, // no blocked outgoing + expectIncomingCount: 1, // only blocked incoming + expectOutgoingCount: 0, // no outgoing shown + }, + { + name: "blocked mode with only outgoing blocked", + prs: []PR{incomingNotBlocked, outgoingBlocked, outgoingNotBlocked}, + blockingOnly: true, + expectIncoming: false, // no blocked incoming + expectOutgoing: true, // has blocked outgoing + expectIncomingCount: 0, // no incoming shown + expectOutgoingCount: 1, // only blocked outgoing + }, + { + name: "blocked mode with no blocked PRs", + prs: []PR{incomingNotBlocked, outgoingNotBlocked}, + blockingOnly: true, + expectIncoming: false, // no blocked incoming + expectOutgoing: false, // no blocked outgoing + expectIncomingCount: 0, // no incoming shown + expectOutgoingCount: 0, // no outgoing shown + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := generatePRDisplay(tt.prs, username, tt.blockingOnly, false, true, nil) + + // Debug output for failing test + if tt.name == "blocked mode with only outgoing blocked" { + t.Logf("Debug output: %q", output) + } + + // For the case with no blocked PRs, expect empty output + if !tt.expectIncoming && !tt.expectOutgoing { + if strings.TrimSpace(output) != "" { + t.Errorf("Expected empty output for no blocked PRs, got: %q", output) + } + return + } + + // Check for incoming section presence + hasIncomingSection := strings.Contains(output, "incoming -") + if hasIncomingSection != tt.expectIncoming { + t.Errorf("Expected incoming section: %v, got: %v", tt.expectIncoming, hasIncomingSection) + } + + // Check for outgoing section presence + hasOutgoingSection := strings.Contains(output, "outgoing -") + if hasOutgoingSection != tt.expectOutgoing { + t.Errorf("Expected outgoing section: %v, got: %v", tt.expectOutgoing, hasOutgoingSection) + } + + // Count actual PRs shown in each section + if tt.expectIncoming { + incomingLines := 0 + lines := strings.Split(output, "\n") + incomingStarted := false + for _, line := range lines { + if strings.Contains(line, "incoming -") { + incomingStarted = true + continue + } + if strings.Contains(line, "outgoing -") { + break + } + if incomingStarted && (strings.HasPrefix(line, "• ") || strings.HasPrefix(line, " ")) { + incomingLines++ + } + } + if incomingLines != tt.expectIncomingCount { + t.Errorf("Expected %d incoming PRs shown, got %d", tt.expectIncomingCount, incomingLines) + } + } + + if tt.expectOutgoing { + outgoingLines := 0 + lines := strings.Split(output, "\n") + outgoingStarted := false + for _, line := range lines { + if strings.Contains(line, "outgoing -") { + outgoingStarted = true + continue + } + if outgoingStarted && (strings.HasPrefix(line, "• ") || strings.HasPrefix(line, " ")) { + outgoingLines++ + } + } + if outgoingLines != tt.expectOutgoingCount { + t.Errorf("Expected %d outgoing PRs shown, got %d", tt.expectOutgoingCount, outgoingLines) + } + } + }) + } +}