Skip to content

Commit 59ee606

Browse files
Add GitHub and Bitbucket OAuth login and list every accessible repo.
Sign in from Settings (GitHub device flow, Bitbucket authorization code), keep PATs as a fallback, and pick from the account's repositories on the Pull requests tab. Co-authored-by: zord.lack.net <zord.lack.net@gmail.com>
1 parent 8649cd0 commit 59ee606

22 files changed

Lines changed: 1579 additions & 96 deletions

‎README.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ Toggle **This review** (impact subgraph) vs **Indexed architecture** (the repo m
4343

4444
### Pull requests
4545

46-
GitHub and Bitbucket via API tokens from Settings. Pick a PR and jump to a branch-range review.
46+
GitHub and Bitbucket via **Sign in** (OAuth) or a token in Settings. After connecting, **My repos** lists every repository the account can access. Pick one and jump to a branch-range review. Review still runs against a **local clone** — if that clone is already a Loadpath workspace, selecting the remote repo fills the local path from `git remote`.
4747

4848
![Pull requests](docs/screenshots/pull-requests.png)
4949

5050
### Settings
5151

52-
GitHub / Bitbucket tokens; AI providers (Anthropic, OpenAI, Grok/xAI, DeepSeek, Cursor-compatible, Ollama). Residual analysis only — Loadpath does not comment every hunk.
52+
GitHub / Bitbucket **OAuth sign-in** (or a classic PAT / app password); AI providers (Anthropic, OpenAI, Grok/xAI, DeepSeek, Cursor-compatible, Ollama). Residual analysis only — Loadpath does not comment every hunk.
53+
54+
GitHub uses device flow (`repo read:user read:org`). Create an OAuth App, enable Device Flow, then set `LOADPATH_GITHUB_CLIENT_ID` or paste the client ID in Settings.
55+
56+
Bitbucket uses authorization code. Create an OAuth consumer whose callback is `http://127.0.0.1:7345/api/oauth/bitbucket/callback` (or your `--public-url`), then set `LOADPATH_BITBUCKET_CLIENT_ID` / `LOADPATH_BITBUCKET_CLIENT_SECRET` or paste the key and secret in Settings. Access tokens are refreshed automatically.
5357

5458
![Settings](docs/screenshots/settings.png)
5559

@@ -140,7 +144,7 @@ MCP URL: `https://your-tunnel.example/mcp` (or `http://127.0.0.1:7345/mcp` on th
140144

141145
**Cursor / Claude / ChatGPT / Gemini (HTTP + OAuth)** — add that MCP URL in the host’s connectors. The first connect opens a consent page on the Loadpath machine. Tokens stay in `~/.loadpath/oauth.json`.
142146

143-
Tools: `list_workspaces`, `init_repo`, `index_repo`, `architecture`, `review`, `detect_repo`, `list_pull_requests`, `post_review_comment`. `review` returns the load-path brief (confidence, sinks, reviewers) — not hunk comments.
147+
Tools: `list_workspaces`, `init_repo`, `index_repo`, `architecture`, `review`, `detect_repo`, `list_pull_requests`, `list_remote_repositories`, `post_review_comment`. `review` returns the load-path brief (confidence, sinks, reviewers) — not hunk comments.
144148

145149
Put `loadpath.yml` at the repo root (see [`loadpath.yml.example`](loadpath.yml.example) and [`fixtures/demo_monorepo/loadpath.yml`](fixtures/demo_monorepo/loadpath.yml)). The tool is opinionated about *your* architecture, not a generic module graph.
146150

‎desktop/urls.mjs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ export function isAllowedExternalUrl(url) {
88
if (parsed.protocol !== "https:") return false;
99
if (parsed.username || parsed.password) return false;
1010
const host = parsed.hostname.toLowerCase();
11-
return (
11+
return (
1212
host === "github.com" ||
1313
host.endsWith(".github.com") ||
1414
host === "bitbucket.org" ||
15-
host.endsWith(".bitbucket.org")
15+
host.endsWith(".bitbucket.org") ||
16+
host === "id.atlassian.com" ||
17+
host.endsWith(".atlassian.com")
1618
);
1719
}
1820

‎desktop/urls.test.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe("isAllowedExternalUrl", () => {
77
it("allows GitHub and Bitbucket https PR links", () => {
88
assert.equal(isAllowedExternalUrl("https://github.com/acme/demo/pull/12"), true);
99
assert.equal(isAllowedExternalUrl("https://bitbucket.org/acme/demo/pull-requests/3"), true);
10+
assert.equal(isAllowedExternalUrl("https://id.atlassian.com/login"), true);
1011
});
1112

1213
it("rejects credentials, other hosts, and non-https schemes", () => {

‎src/loadpath/mcp/server.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def _register_tools(mcp: MCPServer) -> None:
6767
name="list_pull_requests",
6868
description="List GitHub or Bitbucket pull requests using tokens stored in Loadpath settings.",
6969
)(tools.list_pull_requests)
70+
mcp.tool(
71+
name="list_remote_repositories",
72+
description="List GitHub or Bitbucket repositories the signed-in account can access.",
73+
)(tools.list_remote_repositories)
7074
mcp.tool(
7175
name="post_review_comment",
7276
description="Upsert the single Loadpath markdown brief on a pull request (updated in place).",

‎src/loadpath/mcp/tools.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from loadpath.detect import detect_layout, write_draft_config
99
from loadpath.index import index_repo
1010
from loadpath.mcp.compact import compact_architecture, compact_review
11-
from loadpath.providers.scm import provider_for
11+
from loadpath.providers.scm import attach_local_paths, provider_for
1212
from loadpath.review.engine import run_review
1313
from loadpath.review.render import render_markdown
1414
from loadpath.settings import AppSettings, register_workspace
@@ -137,6 +137,23 @@ def list_pull_requests(
137137
return {"pull_requests": [p.to_dict() for p in prs]}
138138

139139

140+
def list_remote_repositories(provider: str) -> dict[str, Any]:
141+
"""List GitHub or Bitbucket repositories the saved token can access."""
142+
settings = AppSettings.load()
143+
token = settings.github_token if provider == "github" else settings.bitbucket_token
144+
username = settings.bitbucket_username
145+
if not token:
146+
return {"error": f"No {provider} token configured in Loadpath settings"}
147+
try:
148+
scm = provider_for(provider, token, username=username)
149+
repos = scm.list_repositories()
150+
profile = scm.current_user()
151+
except Exception as exc: # noqa: BLE001
152+
return {"error": str(exc)}
153+
attach_local_paths(repos, [w.path for w in settings.workspaces])
154+
return {"provider": provider, "user": profile, "repos": [r.to_dict() for r in repos]}
155+
156+
140157
def post_review_comment(
141158
provider: str,
142159
repo: str,

‎src/loadpath/providers/__init__.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1-
from loadpath.providers.scm import BitbucketProvider, GitHubProvider, PullRequest, provider_for
1+
from loadpath.providers.scm import (
2+
BitbucketProvider,
3+
GitHubProvider,
4+
PullRequest,
5+
RemoteRepo,
6+
attach_local_paths,
7+
parse_remote_url,
8+
provider_for,
9+
)
210

3-
__all__ = ["BitbucketProvider", "GitHubProvider", "PullRequest", "provider_for"]
11+
__all__ = [
12+
"BitbucketProvider",
13+
"GitHubProvider",
14+
"PullRequest",
15+
"RemoteRepo",
16+
"attach_local_paths",
17+
"parse_remote_url",
18+
"provider_for",
19+
]

0 commit comments

Comments
 (0)