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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Auto-watch on Bitbucket Cloud: `prw watch --mine` / `--review-requested` and `prw run` also find your Bitbucket PRs (when a Bitbucket token or `bitbucket_user` is set): your open PRs in every workspace (or `bitbucket_workspaces`), and open PRs listing you as a reviewer that you haven't approved or requested changes on, in repositories active in the last 30 days (rescanned every 15 minutes). Without a GitHub token, auto-watch works on Bitbucket alone.

### Changed
- `prw run` checks Bitbucket PRs less often as the watch list grows (every 18 seconds × the number of Bitbucket PRs, at least a minute), keeping checks around 600 of Bitbucket's 1,000 requests per hour.

## [0.5.0] - 2026-10-31

prw now works with Bitbucket Cloud as well as GitHub, and `prw dashboard` shows all your PRs across repositories in one live table, grouped by task, with one column per target branch — handy when a change travels dev → rc → master as cherry-picks.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The tool is deliberately minimal. It does one thing well: watch PRs and tell you
- **`prw wait`**: block until a PR's CI finishes and exit 0/1/2 — for scripts, pipelines and AI coding agents
- **MCP server** (`prw mcp`): lets Claude Code, Claude Desktop, Cursor and other agents check and wait for PR CI as tools
- **Bitbucket Cloud**: watch, wait for and list the builds of Bitbucket PRs (Pipelines, Jenkins, ...) with the same commands
- **Auto-watch**: `prw watch --mine` / `--review-requested` follow your PRs and review requests across all repositories, no URLs needed
- **Auto-watch**: `prw watch --mine` / `--review-requested` follow your PRs and review requests across all repositories, on GitHub and Bitbucket, no URLs needed
- **Auto-cleanup**: merged/closed PRs are notified once and dropped from the watch list by `prw run`
- **Watch multiple PRs** from different repositories simultaneously
- **Instant notifications** when CI status changes (pending, success, failure, error)
Expand Down Expand Up @@ -156,7 +156,7 @@ prw watch --mine # all your open PRs, in every repository
prw watch --review-requested # open PRs where your review is requested (incl. your teams)
```

These follow a GitHub search instead of a fixed list: matching PRs are added right away, and `prw run` searches again every 5 minutes, so new PRs show up by themselves. Merged or closed PRs are dropped after their final notification, and review-request PRs are also dropped once your review is no longer requested. `prw list` shows which PRs were added automatically (`ADDED BY`). Undo with `prw unwatch --mine` / `--review-requested`, which removes only the PRs that search added — PRs you watched by hand are kept.
These follow a search instead of a fixed list, on GitHub and — when a Bitbucket token or `bitbucket_user` is set — on Bitbucket Cloud too (see [Bitbucket Cloud](#bitbucket-cloud)): matching PRs are added right away, and `prw run` searches again every 5 minutes, so new PRs show up by themselves. Merged or closed PRs are dropped after their final notification, and review-request PRs are also dropped once your review is no longer requested. `prw list` shows which PRs were added automatically (`ADDED BY`). Undo with `prw unwatch --mine` / `--review-requested`, which removes only the PRs that search added — PRs you watched by hand are kept.

### 3. Start the watcher

Expand Down Expand Up @@ -404,8 +404,9 @@ prw checks https://bitbucket.org/acme/api/pull-requests/42
- **CI** comes from the commit's build statuses, so it covers Bitbucket Pipelines, Jenkins and anything else that reports to Bitbucket, each with its link and duration. A stopped build counts as failed.
- **Reviews**: participants who approved or requested changes. **Merge state** comes from the PR's merge checks: `conflict`, `blocked` (e.g. missing approvals or successful builds), or `ready`.
- `prw run` notifies about Bitbucket PRs just like GitHub ones, `prw list` shows them as `bitbucket:workspace/repo`, and the MCP tools accept their URLs.
- Bitbucket Cloud allows 1,000 API requests per hour, so `prw run` checks each Bitbucket PR at most once a minute whatever the poll interval (about 8 PRs fit in the limit). A rate limit on one service doesn't pause the other.
- Not yet for Bitbucket: `--mine` / `--review-requested`, `--logs` and `prw flaky`.
- **Auto-watch**: `prw watch --mine` follows your open PRs in all your workspaces (or those in `bitbucket_workspaces`). `--review-requested` follows open PRs that list you as a reviewer and that you haven't approved or requested changes on yet; Bitbucket can only search those per repository, so prw scans the repositories active in the last 30 days (up to 60 per workspace) every 15 minutes.
- Bitbucket Cloud allows 1,000 API requests per hour, so `prw run` spaces out the checks of Bitbucket PRs whatever the poll interval: each is checked at most once a minute, and less often as the list grows (every 18 seconds × the number of Bitbucket PRs, e.g. every 9 minutes with 30), so checks use about 600 requests an hour and leave room for auto-watch and other commands. A rate limit on one service doesn't pause the other.
- Not yet for Bitbucket: `--logs` and `prw flaky`.

**Authentication.** Public repositories work without credentials. For private ones, create an [Atlassian API token](https://id.atlassian.com/manage-profile/security/api-tokens) with Bitbucket read scopes (repositories and pull requests) and give prw your Atlassian email with it:

Expand Down
1 change: 1 addition & 0 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Legend — Impact: 1–5 · Effort: S (≤½ day), M (1–2 days), L (3+ days)
- 🆕 Bitbucket Cloud support (requested by the owner, who uses Bitbucket at work). Phase 1 done: PR identity carries the host; a Bitbucket client maps PRs, build statuses (Pipelines, Jenkins), participants and merge checks onto the existing types, so run / wait / checks / list / broadcast / MCP work unchanged; per-host rate-limit pauses; ≥60s between checks of a Bitbucket PR (1,000 req/h limit). Verified live on public atlassian/atlassian-connect-express PRs. Next: auto-watch (`--mine` / `--review-requested`) on Bitbucket, Pipelines step logs for `--logs`, then GitHub Enterprise (#2) on the same host abstraction.
- ✅ `prw dashboard` (owner's use case: ~25 repos × dev/rc/master cherry-picks at work) — live table grouped by task across GitHub + Bitbucket, smart CI re-reads. Bitbucket discovery endpoints verified live anonymously (workspace user PRs, merged since, recent repos, reviewer filter); `/user` and `/user/workspaces` still need a check with the owner's token. Verified live on the owner's GitHub PRs.
- 🚀 v0.5.0 released with Bitbucket Cloud and `prw dashboard` (PRs #24–#25).
- ✅ Bitbucket auto-watch (`--mine` / `--review-requested`): per-host finders, review scan every 15 min skipping PRs already reviewed, check interval scaled to the number of Bitbucket PRs (600 req/h budget). Verified live with a public Bitbucket user + the owner's GitHub account in one watch list.
- Next: #17 packaging (Homebrew/Scoop) + keychain token storage, #8 `gh` extension publishing — both need owner decisions

## Planned
Expand Down
4 changes: 3 additions & 1 deletion cmd/prw/autowatch_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -217,9 +218,10 @@ func TestWatchCmd_MineNeedsToken(t *testing.T) {
setupAutoWatchCmd(t, nil, nil)
config.Update(func(c *config.Config) error { c.GitHubToken = ""; return nil })
t.Setenv("GITHUB_TOKEN", "")
t.Setenv("BITBUCKET_TOKEN", "")
watchMine = true

if err := watchCmd.RunE(watchCmd, nil); err != errMissingToken {
if err := watchCmd.RunE(watchCmd, nil); !errors.Is(err, errMissingToken) {
t.Errorf("err = %v, want errMissingToken", err)
}
if cfg, _ := config.Load(); len(cfg.AutoWatch) != 0 {
Expand Down
34 changes: 32 additions & 2 deletions cmd/prw/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/devblac/prw/internal/config"
"github.com/devblac/prw/internal/forge"
"github.com/devblac/prw/internal/github"
"github.com/devblac/prw/internal/watcher"
)

// prClient is everything the commands use from a code host's client. Both the
Expand Down Expand Up @@ -41,8 +42,8 @@ func clientFor(cfg *config.Config, host string) (prClient, error) {

// needsGitHub reports whether the watch list or auto-watch involves GitHub.
func needsGitHub(cfg *config.Config) bool {
if len(cfg.AutoWatch) > 0 {
return true // auto-watch searches GitHub
if len(cfg.AutoWatch) > 0 && !bitbucketConfigured(cfg) {
return true // auto-watch has nothing else to search
}
Comment on lines +45 to 47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow Bitbucket-only auto-watch to start with an empty list

When Bitbucket credentials are configured, auto-watch is enabled, and no PRs are currently watched, this condition falls through to return len(cfg.WatchedPRs) == 0, which still returns true. Without a GitHub token, runCmd consequently returns errMissingToken before running Bitbucket discovery. This prevents Bitbucket-only users from starting the watcher while waiting for their first matching PR, or restarting it after all watched PRs have closed. Exempt configured Bitbucket auto-watch from the empty-list GitHub default while retaining the token requirement for explicitly watched GitHub PRs.

Useful? React with 👍 / 👎.

for _, pr := range cfg.WatchedPRs {
if !forge.IsBitbucket(pr.Host) {
Expand All @@ -52,6 +53,35 @@ func needsGitHub(cfg *config.Config) bool {
return len(cfg.WatchedPRs) == 0 // nothing yet: GitHub is the default
}

// bitbucketConfigured reports whether prw knows who you are on Bitbucket, which
// finding your PRs there needs.
func bitbucketConfigured(cfg *config.Config) bool {
auth, _ := cfg.ResolveBitbucketAuth()
return auth.Token != "" || cfg.BitbucketUser != ""
}

// autoWatchFinders returns the auto-watch finders of the configured accounts; bb is
// the Bitbucket client to use.
func autoWatchFinders(cfg *config.Config, bb *bitbucket.Client) []watcher.Finder {
var finders []watcher.Finder
if token := cfg.GetToken(); token != "" {
finders = append(finders, watcher.GitHubFinder{Searcher: newGitHubClient(token)})
}
if f := bitbucketFinder(cfg, bb); f != nil {
finders = append(finders, f)
}
return finders
}

// bitbucketFinder returns the Bitbucket auto-watch finder, or nil without a Bitbucket
// account.
func bitbucketFinder(cfg *config.Config, bb *bitbucket.Client) *watcher.BitbucketFinder {
if !bitbucketConfigured(cfg) {
return nil
}
return &watcher.BitbucketFinder{API: bb, User: cfg.BitbucketUser, Workspaces: cfg.BitbucketWorkspaces}
}

// bitbucketAuthHint explains how to authenticate when Bitbucket refuses a request:
// it answers 404 rather than 401/403 for private repositories without access.
func bitbucketAuthHint(host string, err error) string {
Expand Down
4 changes: 1 addition & 3 deletions cmd/prw/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ type dashboardView struct {
}

func (v dashboardView) render(w io.Writer, items []dashboard.Item, warnings, requests []string, now time.Time, live bool) {
var mine, review, targets []string
var targets []string
var mineItems, reviewItems []dashboard.Item
for _, it := range items {
targets = append(targets, it.Target)
Expand All @@ -202,8 +202,6 @@ func (v dashboardView) render(w io.Writer, items []dashboard.Item, warnings, req
mineItems = append(mineItems, it)
}
}
_ = mine
_ = review
cols := dashboard.Columns(targets, v.order)

open := 0
Expand Down
40 changes: 25 additions & 15 deletions cmd/prw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,20 @@ var watchCmd = &cobra.Command{
Short: "Add a PR to the watch list, or follow your PRs automatically",
Long: `Add a pull request to the watch list.

With --mine or --review-requested, prw instead follows a GitHub search and keeps
the watch list up to date by itself: matching PRs are added right away, and
'prw run' refreshes the search every 5 minutes.

--mine your open PRs (author:@me), in every repository
--review-requested open PRs where your review is requested (incl. your teams)
With --mine or --review-requested, prw instead finds your PRs on GitHub and on
Bitbucket Cloud (when a Bitbucket token or bitbucket_user is set) and keeps the
watch list up to date by itself: matching PRs are added right away, and
'prw run' searches again every 5 minutes.

--mine your open PRs, in every repository (on Bitbucket, in every
workspace, or those in bitbucket_workspaces)
--review-requested open PRs where your review is requested (on GitHub incl.
your teams; on Bitbucket, PRs listing you as a reviewer in
repositories active in the last 30 days, rescanned every
15 minutes)

PRs added by --review-requested are dropped again once your review is no longer
requested. Merged or closed PRs are always dropped by 'prw run' after it notifies.
requested (on Bitbucket: once you approve or request changes). Merged or closed PRs are always dropped by 'prw run' after it notifies.
Undo with 'prw unwatch --mine' / 'prw unwatch --review-requested'.`,
Example: ` prw watch https://github.com/owner/repo/pull/123
prw watch --mine
Expand Down Expand Up @@ -204,9 +209,10 @@ func enableAutoWatch(sources []string) error {
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
token := cfg.GetToken()
if token == "" {
return errMissingToken
bbAuth, _ := cfg.ResolveBitbucketAuth()
finders := autoWatchFinders(cfg, newBitbucketClient(bbAuth.Email, bbAuth.Token))
if len(finders) == 0 {
return fmt.Errorf("%w; for Bitbucket, set 'prw config set bitbucket_email' and 'prw config set bitbucket_token'", errMissingToken)
}

var enabled []string
Expand All @@ -232,21 +238,21 @@ func enableAutoWatch(sources []string) error {
}

// Run the searches now so the user sees the result immediately.
res, err := watcher.SyncAutoWatch(context.Background(), newGitHubClient(token), sources)
res, err := watcher.Sync(context.Background(), finders, sources)
for _, warning := range res.Warnings {
fmt.Printf("Warning: %s\n", warning)
}
if err != nil {
return fmt.Errorf("auto-watch is enabled, but searching GitHub failed (prw run will retry): %w", err)
return fmt.Errorf("auto-watch is enabled, but searching failed (prw run will retry): %w", err)
}
if len(res.Added) == 0 {
fmt.Println("No new PRs to add right now.")
}
for _, pr := range res.Added {
fmt.Printf(" + %s/%s#%d - %s\n", pr.Owner, pr.Repo, pr.Number, pr.Title)
fmt.Printf(" + %s - %s\n", watcher.DisplayPR(pr), pr.Title)
}
for _, pr := range res.Removed {
fmt.Printf(" - %s/%s#%d (review no longer requested)\n", pr.Owner, pr.Repo, pr.Number)
fmt.Printf(" - %s (review no longer requested)\n", watcher.DisplayPR(pr))
}
fmt.Println("'prw run' keeps this list up to date (searches again every 5 minutes).")
return nil
Expand Down Expand Up @@ -448,7 +454,11 @@ Press Ctrl+C to stop.`,

w := watcher.New(client, cfg, notifier)
bbAuth, _ := cfg.ResolveBitbucketAuth()
w.SetClient(forge.Bitbucket, newBitbucketClient(bbAuth.Email, bbAuth.Token))
bb := newBitbucketClient(bbAuth.Email, bbAuth.Token)
w.SetClient(forge.Bitbucket, bb)
if f := bitbucketFinder(cfg, bb); f != nil {
w.AddFinder(f)
}

// Setup signal handling
ctx, cancel := context.WithCancel(context.Background())
Expand Down
1 change: 1 addition & 0 deletions internal/bitbucket/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ type pullRequest struct {
Participants []struct {
User struct {
UUID string `json:"uuid"`
AccountID string `json:"account_id"`
DisplayName string `json:"display_name"`
Nickname string `json:"nickname"`
} `json:"user"`
Expand Down
8 changes: 7 additions & 1 deletion internal/bitbucket/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
const prListFields = "next,values.id,values.title,values.state,values.updated_on," +
"values.source.branch.name,values.source.commit.hash," +
"values.destination.branch.name,values.destination.repository.full_name," +
"values.participants.approved,values.participants.state,values.participants.user.uuid"
"values.participants.approved,values.participants.state,values.participants.user.uuid,values.participants.user.account_id"

const (
listPageLen = 50
Expand All @@ -36,6 +36,9 @@ type ListedPR struct {
// participant requested changes.
Approvals int
ChangesRequested bool
// ReviewedBy holds the UUIDs and account ids of the participants who approved or
// requested changes.
ReviewedBy []string
}

func toListed(pr pullRequest) ListedPR {
Expand All @@ -52,7 +55,10 @@ func toListed(pr pullRequest) ListedPR {
l.ChangesRequested = true
case p.Approved:
l.Approvals++
default:
continue
}
l.ReviewedBy = append(l.ReviewedBy, p.User.UUID, p.User.AccountID)
}
return l
}
Expand Down
Loading
Loading