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
23 changes: 6 additions & 17 deletions backend/app/schemas/github_link.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""GitHub 連携 API 用の Pydantic スキーマ。"""

from typing import Dict, Optional
from typing import Dict, List, Optional

from pydantic import BaseModel, Field

Expand All @@ -24,8 +24,9 @@ class ContributionDay(BaseModel):


class ContributionCalendar(BaseModel):
"""直近1年のコントリビューションカレンダー(GitHub の緑の四角)。"""
"""1年分のコントリビューションカレンダー(GitHub の緑の四角)。"""

year: int = Field(description="このカレンダーが対象とする西暦年")
total_contributions: int = Field(description="期間内のコントリビューション総数")
weeks: list[list[ContributionDay]] = Field(
default_factory=list,
Expand All @@ -42,21 +43,9 @@ class GitHubLinkResponse(BaseModel):
default_factory=dict,
description="言語ごとのバイト数(GitHub linguist ベース)",
)
detected_frameworks: Dict[str, int] = Field(
default_factory=dict,
description="依存関係から検出したフレームワーク名 → 使用リポジトリ数",
)
detected_devtools: Dict[str, int] = Field(
default_factory=dict,
description="ルートファイルから検出した DevTools 名 → 使用リポジトリ数",
)
detected_infras: Dict[str, int] = Field(
default_factory=dict,
description="ルートファイルから検出したインフラツール名 → 使用リポジトリ数",
)
contribution_calendar: Optional[ContributionCalendar] = Field(
default=None,
description="直近1年のコントリビューションカレンダー(取得失敗時は None)",
contribution_calendars: List[ContributionCalendar] = Field(
default_factory=list,
description="年ごとのコントリビューションカレンダー(新しい年順。取得失敗時は空配列)",
)


Expand Down
3 changes: 2 additions & 1 deletion backend/app/services/intelligence/github/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
GitHub 分析サブパッケージ。

api_client: GitHub REST API 呼び出し
repo_analyzer: 言語 ratio 算出・フレームワーク検出・依存関係パース
repo_analyzer: 言語 ratio 算出
contributions: コントリビューションカレンダー取得(GraphQL)
"""
145 changes: 2 additions & 143 deletions backend/app/services/intelligence/github/api_client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""
GitHub REST API 呼び出しを担うモジュール。

リポジトリ一覧・言語情報・ファイル内容取得などの純粋な API 通信を行う。
リポジトリ一覧・言語情報などの純粋な API 通信を行う。
"""

import logging
import re
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List

import httpx

Expand Down Expand Up @@ -54,49 +54,6 @@ def _is_valid_owner_repo(owner: str, repo: str) -> bool:
# これより小さいリポジトリはスキップ(バイト)
_REPO_MIN_SIZE_BYTES = 1024

# 特定のスキルを示すルートファイル/ディレクトリ
_INTERESTING_ROOT_FILES = {
"Dockerfile",
"docker-compose.yml",
"docker-compose.yaml",
"package.json",
"requirements.txt",
"pyproject.toml",
"pom.xml",
"go.mod",
"Makefile",
"Gemfile",
".github",
"terraform",
".terraform",
"infra",
"k8s",
"kubernetes",
"helm",
"cdk.json",
"pulumi.yaml",
"pulumi.yml",
"Jenkinsfile",
".gitlab-ci.yml",
".circleci",
}

# モノレポ構成でよく使われるサブディレクトリ内の依存関係ファイル
# ルートに dep ファイルがない Python/Node プロジェクトのフォールバック探索パス
_SUBDIRECTORY_DEP_FILES = [
"backend/requirements.txt",
"backend/pyproject.toml",
"server/requirements.txt",
"server/pyproject.toml",
"api/requirements.txt",
"api/pyproject.toml",
"src/requirements.txt",
"app/requirements.txt",
"frontend/package.json",
"client/package.json",
"web/package.json",
]


class GitHubUserNotFoundError(Exception):
"""GitHub ユーザーが見つからない場合の例外。"""
Expand Down Expand Up @@ -206,101 +163,3 @@ async def fetch_languages(
except httpx.HTTPError:
logger.warning("Failed to fetch languages for %s/%s", owner, repo)
return {}


async def fetch_root_files(
client: httpx.AsyncClient,
owner: str,
repo: str,
) -> List[str]:
"""リポジトリのルートレベルの注目すべきファイル名/ディレクトリ名を取得する。"""
if not _is_valid_owner_repo(owner, repo):
logger.warning("不正な owner/repo をスキップ: %s/%s", owner, repo)
return []
try:
resp = await client.get(f"/repos/{owner}/{repo}/contents/")
if resp.status_code in (403, 404):
return []
resp.raise_for_status()
items: List[Any] = resp.json()
if not isinstance(items, list):
return []
return [
item["name"]
for item in items
if isinstance(item, dict)
and "name" in item
and item["name"] in _INTERESTING_ROOT_FILES
]
except httpx.HTTPError:
logger.warning("Failed to fetch contents for %s/%s", owner, repo)
return []


async def fetch_file_content(
client: httpx.AsyncClient,
owner: str,
repo: str,
path: str,
) -> Optional[str]:
"""リポジトリから生のファイルコンテンツをダウンロードする。"""
if not _is_valid_owner_repo(owner, repo):
logger.warning("不正な owner/repo をスキップ: %s/%s", owner, repo)
return None
try:
resp = await client.get(
f"/repos/{owner}/{repo}/contents/{path}",
headers={"Accept": "application/vnd.github.raw+json"},
)
if resp.status_code in (403, 404):
return None
resp.raise_for_status()
return resp.text
except httpx.HTTPError:
logger.warning(
"Failed to fetch %s for %s/%s",
path,
owner,
repo,
)
return None


async def fetch_subdirectory_dep_files(
client: httpx.AsyncClient,
owner: str,
repo: str,
root_files: List[str],
) -> List[str]:
"""ルートに依存関係ファイルがないモノレポ向けに、サブディレクトリの dep ファイルパスを返す。

ルートにすでに Python/Node の dep ファイルがある場合はスキップする。
見つかったパスのリストを返す(ファイル名ではなく相対パス)。
"""
_PYTHON_DEP_FILES = {"requirements.txt", "pyproject.toml"}
_NODE_DEP_FILES = {"package.json"}

has_python_deps = bool(_PYTHON_DEP_FILES & set(root_files))
has_node_deps = bool(_NODE_DEP_FILES & set(root_files))

if has_python_deps and has_node_deps:
return []

found: List[str] = []
for path in _SUBDIRECTORY_DEP_FILES:
filename = path.split("/")[-1]
if filename in _PYTHON_DEP_FILES and has_python_deps:
continue
if filename in _NODE_DEP_FILES and has_node_deps:
continue
content = await fetch_file_content(client, owner, repo, path)
if content is not None:
found.append(path)
# Python か Node のどちらかで1ファイル見つかれば探索終了
if filename in _PYTHON_DEP_FILES:
has_python_deps = True
elif filename in _NODE_DEP_FILES:
has_node_deps = True
if has_python_deps and has_node_deps:
break
return found
Loading
Loading