138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""성장일지 — git 변경이력 자동 생성·조회."""
|
|
from __future__ import annotations
|
|
import asyncio, logging, subprocess
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select, desc
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from core.auth import get_current_user, require_admin_role
|
|
from database import get_db
|
|
from models import User, ChangelogEntry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/changelog", tags=["성장일지-변경이력"])
|
|
|
|
GIT_ROOT = "/opt/guardia"
|
|
|
|
|
|
def _run_git(cmd: list[str]) -> str:
|
|
try:
|
|
r = subprocess.run(
|
|
["git", "-C", GIT_ROOT] + cmd,
|
|
capture_output=True, text=True, timeout=15
|
|
)
|
|
return r.stdout.strip()
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
|
|
async def _collect_and_save():
|
|
"""최신 커밋 50개에서 ChangelogEntry 자동 생성."""
|
|
log = _run_git([
|
|
"log", "--oneline", "--no-merges", "-50",
|
|
"--format=%H|||%s|||%an|||%ad", "--date=short"
|
|
])
|
|
if not log:
|
|
return 0
|
|
|
|
from database import AsyncSessionLocal
|
|
async with AsyncSessionLocal() as db:
|
|
count = 0
|
|
for line in log.splitlines():
|
|
parts = line.split("|||")
|
|
if len(parts) < 4:
|
|
continue
|
|
commit_hash, subject, author, date = parts[0], parts[1], parts[2], parts[3]
|
|
|
|
existing = await db.execute(
|
|
select(ChangelogEntry).where(ChangelogEntry.commit_hash == commit_hash).limit(1)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
continue
|
|
|
|
cat = "feat"
|
|
for prefix in ("fix", "docs", "refactor", "test", "chore", "style"):
|
|
if subject.lower().startswith(prefix):
|
|
cat = prefix
|
|
break
|
|
|
|
entry = ChangelogEntry(
|
|
version="auto",
|
|
category=cat,
|
|
title=subject[:290],
|
|
commit_hash=commit_hash,
|
|
author=author,
|
|
created_at=datetime.fromisoformat(date) if date else datetime.utcnow(),
|
|
)
|
|
db.add(entry)
|
|
count += 1
|
|
await db.commit()
|
|
return count
|
|
|
|
|
|
@router.post("/collect")
|
|
async def collect_changelog(
|
|
background_tasks: BackgroundTasks,
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
"""git 커밋 이력에서 변경이력 자동 수집."""
|
|
background_tasks.add_task(_collect_and_save)
|
|
return {"ok": True, "message": "변경이력 수집 시작됨 (백그라운드)"}
|
|
|
|
|
|
@router.get("/list")
|
|
async def list_changelog(
|
|
limit: int = 50,
|
|
category: str = Query(None),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
"""변경이력 목록."""
|
|
q = select(ChangelogEntry).order_by(desc(ChangelogEntry.created_at)).limit(limit)
|
|
if category:
|
|
q = q.where(ChangelogEntry.category == category)
|
|
rows = await db.execute(q)
|
|
return [{
|
|
"id": e.id, "version": e.version, "category": e.category,
|
|
"title": e.title, "author": e.author,
|
|
"commit_hash": e.commit_hash, "created_at": e.created_at,
|
|
} for e in rows.scalars().all()]
|
|
|
|
|
|
@router.get("/summary")
|
|
async def changelog_summary(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
"""카테고리별 변경 수 요약."""
|
|
from sqlalchemy import func as sqlfunc
|
|
rows = await db.execute(
|
|
select(ChangelogEntry.category, sqlfunc.count().label("cnt"))
|
|
.group_by(ChangelogEntry.category)
|
|
.order_by(desc("cnt"))
|
|
)
|
|
return {"summary": [{"category": r.category, "count": r.cnt} for r in rows.all()]}
|
|
|
|
|
|
class ManualEntry(BaseModel):
|
|
version: str
|
|
category: str = "feat"
|
|
title: str
|
|
description: str = ""
|
|
|
|
|
|
@router.post("/entry")
|
|
async def add_entry(
|
|
entry: ManualEntry,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
"""수동 변경이력 추가."""
|
|
row = ChangelogEntry(
|
|
version=entry.version, category=entry.category,
|
|
title=entry.title, description=entry.description,
|
|
author=user.username, created_at=datetime.utcnow(),
|
|
)
|
|
db.add(row)
|
|
await db.commit()
|
|
await db.refresh(row)
|
|
return {"id": row.id, "ok": True}
|