Files
swe-agent--swe-agent/sweagent/utils/github.py
T
wehub-resource-sync 4e0f4422d0
Check Markdown links / markdown-link-check (push) Waiting to run
Pytest / test (3.11) (push) Waiting to run
Pytest / test (3.12) (push) Waiting to run
build-docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:42 +08:00

155 lines
5.1 KiB
Python

import json
import re
import urllib.error
import urllib.request
from ghapi.all import GhApi
from sweagent.utils.log import get_logger
_logger = get_logger("swea-github", emoji="🔧")
_repo_privacy_cache: dict[str, bool] = {}
GITHUB_ISSUE_URL_PATTERN = re.compile(r"github\.com\/(.*?)\/(.*?)\/issues\/(\d+)")
class InvalidGithubURL(Exception):
"""Raised when a github URL is invalid"""
GITHUB_REPO_URL_PATTERN = re.compile(r".*[/@]?github\.com\/([^/]+)\/([^/]+)")
def _is_github_repo_url(data_path: str) -> bool:
"""Check if data_path is an URL pointing to a github repository.
Paths to issues or PRs will also match this pattern.
"""
return GITHUB_REPO_URL_PATTERN.search(data_path) is not None
def _is_github_issue_url(data_path: str) -> bool:
"""Check if data_path is an URL pointing to a github issue"""
return GITHUB_ISSUE_URL_PATTERN.search(data_path) is not None
def _get_commit(api: GhApi, owner: str, repo: str, ref: str | None = None):
"""Get commit object from github api
Args:
api (GhApi):
owner (str): Repo owner, e.g., "SWE-agent"
repo (str): Repo, e.g., "SWE-agent"
ref (str, optional): Branch, tag or commit hash
Returns:
_type_: _description_
"""
if ref:
return api.repos.get_commit(owner, repo, ref) # type: ignore
return api.repos.list_commits(owner, repo)[0] # type: ignore
def _parse_gh_issue_url(issue_url: str) -> tuple[str, str, str]:
"""
Returns:
owner: Repo owner
repo: Repo name
issue number: Issue number as str
Raises:
InvalidGithubURL: If the URL is not a valid github issue URL
"""
match = GITHUB_ISSUE_URL_PATTERN.search(issue_url)
if not match:
msg = f"Invalid GitHub issue URL: {issue_url}"
raise InvalidGithubURL(msg)
res = match.groups()
assert len(res) == 3
return tuple(res) # type: ignore
def _parse_gh_repo_url(repo_url: str) -> tuple[str, str]:
"""
Returns:
owner: Repo owner/org
repo: Repo name
Raises:
InvalidGithubURL: If the URL is not a valid github repo URL
"""
match = GITHUB_REPO_URL_PATTERN.search(repo_url)
if not match:
msg = f"Invalid GitHub issue URL: {repo_url}"
raise InvalidGithubURL(msg)
owner, repo = match.groups()
return owner, repo.removesuffix(".git")
def _get_gh_issue_data(issue_url: str, *, token: str = ""):
"""Returns github issue data in the form of a dictionary.
See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue
for return format
"""
owner, repo, issue_number = _parse_gh_issue_url(issue_url)
api = GhApi(token=token)
return api.issues.get(owner, repo, issue_number) # type: ignore
def _get_problem_statement_from_github_issue(
owner: str, repo: str, issue_number: str, *, token: str | None = ""
) -> str:
"""Return problem statement from github issue"""
api = GhApi(token=token)
issue = api.issues.get(owner, repo, issue_number) # type: ignore
title = issue.title if issue.title else ""
body = issue.body if issue.body else ""
return f"{title}\n{body}\n"
def _get_associated_commit_urls(org: str, repo: str, issue_number: str, *, token: str = "") -> list[str]:
"""Return the URLs of commits that would close an issue."""
api = GhApi(token=token)
# Strangely the "pull_request" field of api.issues.get is often not set
# so we have to go through the events to check if there's a commit
events = api.issues.list_events(org, repo, issue_number) # type: ignore
commit_urls = []
for event in events:
if event.event != "referenced":
continue
if not event.commit_id:
continue
commit = api.repos.get_commit(org, repo, event.commit_id) # type: ignore
message = commit.commit.message
if f"fixes #{issue_number}" in message.lower() or f"closes #{issue_number}" in message.lower():
commit_urls.append(commit.html_url)
return commit_urls
def _is_repo_private(owner_repo: str, token: str) -> bool:
"""Check if a GitHub repository is private via the GitHub API.
Returns True if the repo is private or if a 404 is returned (GitHub returns
404 for private repos when the token lacks access). Any other HTTP or
network error is raised so callers can handle it explicitly.
"""
if owner_repo in _repo_privacy_cache:
return _repo_privacy_cache[owner_repo]
url = f"https://api.github.com/repos/{owner_repo}"
headers = {"User-Agent": "sweagent"}
if token:
headers["Authorization"] = f"token {token}"
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
private = data.get("private", False)
except urllib.error.HTTPError as e:
if e.code == 404:
_logger.warning("Repo '%s' returned 404 — assuming private", owner_repo)
private = True
else:
raise
_repo_privacy_cache[owner_repo] = private
return private