ساخت پیام Commit با هوش مصنوعی؛ تولید Conventional Commit و توضیحات Pull Request
در این آموزش ابزاری میسازید که Git Diff را تحلیل میکند، پیام Conventional Commit معتبر، توضیحات Pull Request و Changelog تولید میکند و ادعاهای خروجی را با فایلهای تغییرکرده تطبیق میدهد.
ساخت پیام Commit با هوش مصنوعی؛ از Git Diff تا Conventional Commit و Pull Request
پیامهایی مانند موارد زیر اطلاعات چندانی درباره تغییر ارائه نمیکنند:
fix
update code
changes
final fix 2
وقتی تعداد Commitها زیاد شود، این پیامها مشکلاتی ایجاد میکنند:
- پیدا کردن زمان ایجاد یک تغییر دشوار میشود.
- بررسی تاریخچه پروژه زمان بیشتری میگیرد.
- تولید Changelog دقیق ممکن نیست.
- هدف Pull Request بهدرستی مشخص نمیشود.
- بازبین باید تمام Diff را برای درک تغییر بخواند.
- Rollback و عیبیابی تغییرات سختتر میشود.
- مشخص نیست تغییر Feature، Fix یا Refactor بوده است.
هوش مصنوعی میتواند Git Diff را بخواند و پیشنویسهای زیر را تولید کند:
- پیام Conventional Commit
- عنوان Pull Request
- خلاصه تغییرات
- فهرست فایلهای مهم
- نحوه آزمایش
- ریسکهای قابل مشاهده
- Checklist بازبینی
- Changelog کاربرمحور
- توضیح Migration در صورت وجود تغییر قرارداد
اما مدل نباید صرفاً با مشاهده نام فایل، قابلیت یا تستی را اختراع کند. برای مثال، وجود فایل tests/test_cart.py اثبات نمیکند تمام تستها اجرا و موفق شدهاند.
خروجی درست:
Tests added:
- Added pagination boundary tests
Test execution:
- Not verified
خروجی نادرست:
All tests pass successfully.
مگر اینکه نتیجه واقعی اجرای تست در Context وجود داشته باشد.
در این مقاله یک ابزار عملی با Python، Git و API درواره میسازیم که تغییرات Stageشده را تحلیل میکند و پیام Commit و توضیحات PR ساختاریافته تولید میکند.
چرا پیام Commit اهمیت دارد؟
یک پیام مناسب باید پاسخ دهد:
- چه چیزی تغییر کرده است؟
- چرا تغییر کرده است؟
- اثر اصلی آن چیست؟
- آیا رفتار عمومی تغییر کرده است؟
- آیا تغییر ناسازگار وجود دارد؟
- چه چیزی خارج از Commit باقی مانده است؟
پیام خوب:
fix(pagination): calculate offset from one-based page number
Body:
Use (page - 1) * page_size so the first page starts from
the first item instead of skipping one page.
Add boundary tests for the first, second and out-of-range pages.
Conventional Commits چیست؟
Conventional Commits یک قرارداد برای ساختار پیام Commit است:
type(scope): subject
نمونه:
feat(cart): add percentage discount support
fix(api): return 404 for missing products
refactor(pricing): extract money rounding helper
test(pagination): cover one-based page boundaries
ساختار کامل:
type(scope)!: subject
body
footer
علامت ! تغییر ناسازگار را مشخص میکند:
feat(api)!: rename user_id response field to id
Footer:
BREAKING CHANGE: API clients must read the id field instead of user_id.
Typeهای رایج Commit
| Type | کاربرد |
|---|---|
feat | اضافهشدن رفتار یا قابلیت جدید |
fix | اصلاح رفتار اشتباه |
refactor | تغییر ساختار بدون تغییر رفتار |
test | افزودن یا اصلاح تست |
docs | تغییر مستندات |
perf | بهبود عملکرد |
build | تغییر Build System یا Dependency |
ci | تغییر CI |
chore | نگهداری عمومی |
revert | بازگرداندن تغییر قبلی |
مدل نباید هر تغییر را feat معرفی کند. اگر فقط ساختار داخلی تغییر کرده و رفتار حفظ شده است، refactor مناسبتر است.
پیام Commit خوب چه ویژگیهایی دارد؟
- کوتاه و مشخص است.
- فعل امری دارد.
- موضوع واقعی تغییر را بیان میکند.
- فقط Diff فعلی را پوشش میدهد.
- ادعای اثباتنشده ندارد.
- جزئیات غیرضروری فایلها را تکرار نمیکند.
- Scope معنادار دارد.
- تغییر ناسازگار را مخفی نمیکند.
- با قرارداد تیم هماهنگ است.
- Body درباره چرایی یا اثر تغییر توضیح میدهد.
چه اطلاعاتی را به مدل بدهیم؟
Git Diff Stageشده
برای ساخت پیام Commit باید فقط تغییراتی تحلیل شوند که قرار است Commit شوند:
git diff --cached
فهرست فایلها
git diff --cached --name-status
آمار تغییر
git diff --cached --stat
Context پروژه
- نام پروژه
- زبان
- ساختار Scopeها
- نوعهای مجاز Commit
- محدودیت طول Subject
- زبان پیام
- قرارداد Breaking Change
نتیجه واقعی تست
اگر تست اجرا شده است:
Command: pytest -q
Exit code: 0
Result: 28 passed
Issue یا هدف تغییر
Diff همیشه چرایی تغییر را نشان نمیدهد. هدف باید جداگانه ارائه شود.
پرامپت آماده برای تولید پیام Commit
از Git Diff زیر یک پیام Conventional Commit تولید کن.
هدف تغییر:
[GOAL]
نوعهای مجاز:
feat, fix, refactor, test, docs, perf, build, ci, chore
قواعد:
- فقط تغییر Stageشده را توصیف کن.
- Subject حداکثر 72 کاراکتر باشد.
- Subject با فعل امری نوشته شود.
- در انتهای Subject نقطه نگذار.
- Scope را فقط از ماژول اصلی تغییر انتخاب کن.
- اگر چند تغییر نامرتبط وجود دارد، پیشنهاد Split Commit بده.
- نتیجه موفق تست را فقط در صورت وجود خروجی واقعی اعلام کن.
- Breaking Change را فقط با شاهد مشخص گزارش کن.
- نام فایلها را بیدلیل در Subject تکرار نکن.
- خروجی JSON معتبر باشد.
Diff:
[STAGED DIFF]
Test Result:
[TEST RESULT]
پروژه عملی این آموزش
ابزار ما:
- تغییرات Stageشده را میخواند.
- فایلهای تغییرکرده را استخراج میکند.
- Diffهای بزرگ یا Generated را فیلتر میکند.
- Context و نتیجه تست را اضافه میکند.
- خروجی ساختاریافته از مدل دریافت میکند.
- Conventional Commit را اعتبارسنجی میکند.
- ادعاهای مربوط به فایل و تست را بررسی میکند.
- پیام Commit را در فایل ذخیره میکند.
- توضیحات Pull Request تولید میکند.
- هیچ Commit یا Push خودکاری انجام نمیدهد.
ایجاد پروژه
mkdir ai-git-writer
cd ai-git-writer
python -m venv .venv
فعالسازی در Linux و macOS:
source .venv/bin/activate
فعالسازی در Windows:
.venv\Scripts\Activate.ps1
نصب وابستگیها:
pip install \
openai \
python-dotenv \
pydantic
ساخت پوشهها:
mkdir git_writer output context
فایلهای زیر را ایجاد کنید:
git_writer/__init__.py
git_writer/config.py
git_writer/schemas.py
git_writer/git_reader.py
git_writer/generator.py
git_writer/validator.py
git_writer/renderer.py
generate_git_text.py
context/change_goal.md
context/project_rules.md
تنظیم API درواره
فایل .env:
DARVAREH_API_KEY=YOUR_API_KEY
DARVAREH_MODEL=MODEL_ID_DARVAREH
COMMIT_LANGUAGE=en
PR_LANGUAGE=fa
فایل .gitignore:
.env
.venv/
__pycache__/
output/
برای دریافت API Key در درواره ثبتنام کنید. مدل مناسب را از صفحه مدلهای درواره انتخاب کنید.
تعریف قواعد پروژه
فایل context/project_rules.md:
# Git writing rules
## Commit
- از Conventional Commits استفاده شود.
- Typeهای مجاز:
- feat
- fix
- refactor
- test
- docs
- perf
- build
- ci
- chore
- revert
- Subject انگلیسی باشد.
- حداکثر طول Subject برابر 72 کاراکتر است.
- Subject با فعل امری نوشته شود.
- Subject نقطه پایانی ندارد.
- Scope کوتاه و با حروف کوچک باشد.
- Breaking Change فقط با شاهد مستقیم در Diff ثبت شود.
## Pull Request
- عنوان کوتاه و انگلیسی باشد.
- توضیحات اصلی فارسی باشند.
- نتیجه تست فقط از خروجی واقعی Test Command نوشته شود.
- موارد بررسینشده صریحاً مشخص شوند.
- تغییر خارج از Diff ادعا نشود.
تعریف هدف تغییر
فایل context/change_goal.md:
# Change goal
خطای محاسبه Offset در Pagination اصلاح شود.
شماره صفحه از یک شروع میشود و صفحه اول باید از اولین رکورد آغاز شود.
تستهای مرزی صفحه اول، دوم و صفحه خارج از محدوده نیز اضافه شوند.
هدف تغییر اطلاعاتی را فراهم میکند که همیشه از Diff قابل استخراج نیست.
تعریف Schema خروجی
فایل git_writer/schemas.py:
from typing import Literal
from pydantic import BaseModel, Field
CommitType = Literal[
"feat",
"fix",
"refactor",
"test",
"docs",
"perf",
"build",
"ci",
"chore",
"revert",
]
class CommitMessage(BaseModel):
type: CommitType
scope: str | None = None
breaking: bool = False
subject: str
body: list[str] = Field(
default_factory=list
)
footers: list[str] = Field(
default_factory=list
)
class ChangedFileSummary(BaseModel):
path: str
status: Literal[
"added",
"modified",
"deleted",
"renamed",
]
summary: str
class TestEvidence(BaseModel):
command: str | None = None
executed: bool = False
passed: bool | None = None
summary: str
class PullRequestContent(BaseModel):
title: str
summary: str
motivation: str
changes: list[str]
changed_files: list[
ChangedFileSummary
]
test_evidence: TestEvidence
review_notes: list[str] = Field(
default_factory=list
)
risks: list[str] = Field(
default_factory=list
)
out_of_scope: list[str] = Field(
default_factory=list
)
breaking_change: str | None = None
class SplitSuggestion(BaseModel):
recommended: bool
reason: str | None = None
suggested_commits: list[str] = Field(
default_factory=list
)
class GitWritingResult(BaseModel):
commit: CommitMessage
pull_request: PullRequestContent
split_suggestion: SplitSuggestion
assumptions: list[str] = Field(
default_factory=list
)
warnings: list[str] = Field(
default_factory=list
)
خواندن تغییرات Git
فایل git_writer/git_reader.py:
import subprocess
from dataclasses import dataclass
MAX_DIFF_CHARS = 60_000
class GitReadError(RuntimeError):
pass
@dataclass(frozen=True)
class GitContext:
diff: str
name_status: str
stat: str
changed_files: list[str]
def run_git(
arguments: list[str],
) -> str:
result = subprocess.run(
["git", *arguments],
capture_output=True,
text=True,
timeout=30,
check=False,
)
if result.returncode != 0:
raise GitReadError(
result.stderr.strip()
or "Git command failed."
)
return result.stdout
def get_staged_context() -> GitContext:
diff = run_git(
[
"diff",
"--cached",
"--unified=10",
"--",
".",
]
)
name_status = run_git(
[
"diff",
"--cached",
"--name-status",
"--",
".",
]
)
stat = run_git(
[
"diff",
"--cached",
"--stat",
"--",
".",
]
)
changed_files = []
for line in (
name_status.splitlines()
):
parts = line.split("\t")
if len(parts) >= 2:
changed_files.append(
parts[-1]
)
if len(diff) > MAX_DIFF_CHARS:
diff = (
diff[:MAX_DIFF_CHARS]
+ "\n\n[DIFF TRUNCATED]"
)
return GitContext(
diff=diff,
name_status=name_status,
stat=stat,
changed_files=changed_files,
)
ابزار فقط تغییرات Stageشده را میخواند. اگر فایل هنوز Stage نشده باشد در پیام Commit لحاظ نمیشود.
فیلتر فایلهای نامناسب برای تحلیل متنی
فایلهای Generated یا Lockfile ممکن است Diff را بسیار بزرگ کنند.
IGNORED_SUFFIXES = {
".min.js",
".map",
".lock",
}
IGNORED_NAMES = {
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"poetry.lock",
}
def should_ignore_file(
file_path: str,
) -> bool:
if file_path.split("/")[-1] in (
IGNORED_NAMES
):
return True
return any(
file_path.endswith(suffix)
for suffix in IGNORED_SUFFIXES
)
وجود تغییر Dependency باید در آمار فایلها حفظ شود، اما لازم نیست تمام Lockfile برای مدل ارسال شود.
دریافت نتیجه واقعی تست
فایل git_writer/test_runner.py:
import subprocess
import sys
def run_tests(
enabled: bool,
timeout_seconds: int = 120,
) -> dict:
if not enabled:
return {
"executed": False,
"command": None,
"return_code": None,
"stdout": "",
"stderr": "",
"timed_out": False,
}
command = [
sys.executable,
"-m",
"pytest",
"-q",
]
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired:
return {
"executed": True,
"command": command,
"return_code": None,
"stdout": "",
"stderr": "",
"timed_out": True,
}
return {
"executed": True,
"command": command,
"return_code": result.returncode,
"stdout": result.stdout[-10_000:],
"stderr": result.stderr[-5_000:],
"timed_out": False,
}
اجرای Test باید اختیاری باشد. ابزار نباید ادعا کند Testها Pass شدهاند، مگر اینکه return_code برابر صفر باشد.
تولید Commit و PR با API درواره
فایل git_writer/generator.py:
import json
import os
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import ValidationError
from git_writer.schemas import (
GitWritingResult,
)
load_dotenv()
api_key = os.getenv("DARVAREH_API_KEY")
model = os.getenv("DARVAREH_MODEL")
if not api_key:
raise RuntimeError(
"DARVAREH_API_KEY is not configured."
)
if not model:
raise RuntimeError(
"DARVAREH_MODEL is not configured."
)
client = OpenAI(
api_key=api_key,
base_url="https://api.darvareh.ir/v1",
)
SYSTEM_PROMPT = """
تو یک مهندس نرمافزار ارشد هستی و از روی Git Diff
پیام Conventional Commit و توضیحات Pull Request مینویسی.
منبع حقیقت:
- Git Diff
- فهرست فایلهای Stageشده
- هدف تغییر
- نتیجه واقعی تست
قواعد Commit:
- فقط تغییرات Stageشده را توصیف کن.
- Type را بر اساس اثر اصلی تغییر انتخاب کن.
- Subject حداکثر 72 کاراکتر باشد.
- Subject انگلیسی و به شکل فعل امری باشد.
- Subject نقطه پایانی نداشته باشد.
- Scope کوتاه و lowercase باشد.
- Breaking Change را فقط با شاهد صریح اعلام کن.
قواعد Pull Request:
- عنوان انگلیسی و توضیحات فارسی باشند.
- نتیجه موفق تست را فقط اگر اجرا و موفق شده بنویس.
- اگر تست اجرا نشده، صریحاً اعلام کن.
- فایل یا رفتار موجودنبوده اختراع نکن.
- تغییرات نامرتبط را برای Split پیشنهاد کن.
- Diff ناقص یا Truncated را در warnings اعلام کن.
- خروجی فقط JSON معتبر باشد.
"""
OUTPUT_TEMPLATE = {
"commit": {
"type": "fix",
"scope": "pagination",
"breaking": False,
"subject": (
"calculate offset from "
"one-based page number"
),
"body": [
"string"
],
"footers": [],
},
"pull_request": {
"title": (
"Fix one-based pagination offset"
),
"summary": "string",
"motivation": "string",
"changes": [
"string"
],
"changed_files": [
{
"path": "app/pagination.py",
"status": "modified",
"summary": "string",
}
],
"test_evidence": {
"command": "pytest -q",
"executed": True,
"passed": True,
"summary": "string",
},
"review_notes": [],
"risks": [],
"out_of_scope": [],
"breaking_change": None,
},
"split_suggestion": {
"recommended": False,
"reason": None,
"suggested_commits": [],
},
"assumptions": [],
"warnings": [],
}
def generate_git_writing(
context: dict,
) -> GitWritingResult:
response = client.chat.completions.create(
model=model,
temperature=0.1,
messages=[
{
"role": "system",
"content": SYSTEM_PROMPT,
},
{
"role": "user",
"content": (
"Context تغییر:\n\n"
+ json.dumps(
context,
ensure_ascii=False,
indent=2,
)
+ "\n\nقالب خروجی:\n"
+ json.dumps(
OUTPUT_TEMPLATE,
ensure_ascii=False,
indent=2,
)
),
},
],
)
raw_output = (
response.choices[0]
.message.content
)
if not raw_output:
raise RuntimeError(
"The model returned an "
"empty result."
)
try:
parsed = json.loads(raw_output)
except json.JSONDecodeError as error:
raise RuntimeError(
f"Invalid JSON from model: "
f"{error}"
) from error
try:
return (
GitWritingResult
.model_validate(parsed)
)
except ValidationError as error:
raise RuntimeError(
f"Generated Git content failed "
f"validation: {error}"
) from error
اعتبارسنجی Conventional Commit
فایل git_writer/validator.py:
import re
from git_writer.schemas import (
GitWritingResult,
)
SUBJECT_MAX_LENGTH = 72
SCOPE_PATTERN = re.compile(
r"^[a-z0-9][a-z0-9._-]*$"
)
class GitWritingValidationError(
ValueError
):
pass
def validate_commit(
result: GitWritingResult,
) -> None:
commit = result.commit
if len(commit.subject) > (
SUBJECT_MAX_LENGTH
):
raise GitWritingValidationError(
f"Commit subject exceeds "
f"{SUBJECT_MAX_LENGTH} characters."
)
if commit.subject.endswith("."):
raise GitWritingValidationError(
"Commit subject must not end "
"with a period."
)
if "\n" in commit.subject:
raise GitWritingValidationError(
"Commit subject must be "
"a single line."
)
if (
commit.scope is not None
and not SCOPE_PATTERN.fullmatch(
commit.scope
)
):
raise GitWritingValidationError(
"Commit scope is invalid."
)
has_breaking_footer = any(
footer.startswith(
"BREAKING CHANGE:"
)
for footer in commit.footers
)
if (
commit.breaking
!= has_breaking_footer
):
raise GitWritingValidationError(
"Breaking flag and footer "
"are inconsistent."
)
def validate_files(
result: GitWritingResult,
changed_files: list[str],
) -> None:
changed_file_set = set(
changed_files
)
documented_file_set = {
item.path
for item in (
result.pull_request
.changed_files
)
}
unknown_files = (
documented_file_set
- changed_file_set
)
if unknown_files:
raise GitWritingValidationError(
"The model documented files "
"outside the staged diff: "
+ ", ".join(
sorted(unknown_files)
)
)
def validate_test_claim(
result: GitWritingResult,
test_run: dict,
) -> None:
evidence = (
result.pull_request
.test_evidence
)
actually_executed = bool(
test_run["executed"]
)
actually_passed = (
test_run["return_code"] == 0
if actually_executed
and not test_run["timed_out"]
else None
)
if (
evidence.executed
!= actually_executed
):
raise GitWritingValidationError(
"Test execution claim does "
"not match actual execution."
)
if (
evidence.passed
!= actually_passed
):
raise GitWritingValidationError(
"Test result claim does not "
"match actual result."
)
مدل اجازه ندارد فایل یا نتیجه تست را اختراع کند.
ساخت متن نهایی Commit
فایل git_writer/renderer.py:
from pathlib import Path
from git_writer.schemas import (
CommitMessage,
PullRequestContent,
)
def render_commit_header(
commit: CommitMessage,
) -> str:
scope = (
f"({commit.scope})"
if commit.scope
else ""
)
breaking = (
"!"
if commit.breaking
else ""
)
return (
f"{commit.type}"
f"{scope}"
f"{breaking}: "
f"{commit.subject}"
)
def render_commit_message(
commit: CommitMessage,
) -> str:
lines = [
render_commit_header(commit),
]
if commit.body:
lines.append("")
lines.extend(commit.body)
if commit.footers:
lines.append("")
lines.extend(commit.footers)
return "\n".join(lines)
def render_pull_request(
pull_request: PullRequestContent,
) -> str:
lines = [
f"# {pull_request.title}",
"",
"## خلاصه",
"",
pull_request.summary,
"",
"## دلیل تغییر",
"",
pull_request.motivation,
"",
"## تغییرات",
"",
]
for change in pull_request.changes:
lines.append(f"- {change}")
lines.extend(
[
"",
"## فایلهای مهم",
"",
]
)
for file_item in (
pull_request.changed_files
):
lines.append(
f"- `{file_item.path}`: "
f"{file_item.summary}"
)
lines.extend(
[
"",
"## تست",
"",
]
)
test_evidence = (
pull_request.test_evidence
)
if test_evidence.executed:
status = (
"موفق"
if test_evidence.passed
else "ناموفق"
)
lines.append(
f"- وضعیت: {status}"
)
if test_evidence.command:
lines.append(
f"- دستور: "
f"`{test_evidence.command}`"
)
lines.append(
f"- نتیجه: "
f"{test_evidence.summary}"
)
else:
lines.append(
"- تست خودکار در این مرحله "
"اجرا نشده است."
)
if pull_request.review_notes:
lines.extend(
[
"",
"## نکات بازبینی",
"",
]
)
for note in (
pull_request.review_notes
):
lines.append(f"- {note}")
if pull_request.risks:
lines.extend(
[
"",
"## ریسکها و موارد قابل توجه",
"",
]
)
for risk in pull_request.risks:
lines.append(f"- {risk}")
if pull_request.out_of_scope:
lines.extend(
[
"",
"## خارج از محدوده",
"",
]
)
for item in (
pull_request.out_of_scope
):
lines.append(f"- {item}")
if pull_request.breaking_change:
lines.extend(
[
"",
"## تغییر ناسازگار",
"",
pull_request.breaking_change,
]
)
return "\n".join(lines)
def save_outputs(
commit: CommitMessage,
pull_request: PullRequestContent,
output_directory: Path,
) -> None:
output_directory.mkdir(
parents=True,
exist_ok=True,
)
(
output_directory
/ "commit-message.txt"
).write_text(
render_commit_message(commit),
encoding="utf-8",
)
(
output_directory
/ "pull-request.md"
).write_text(
render_pull_request(
pull_request
),
encoding="utf-8",
)
در قالب Pull Request از Divider خطی استفاده نشده است.
ساخت برنامه اصلی
فایل generate_git_text.py:
import argparse
from pathlib import Path
from git_writer.generator import (
generate_git_writing,
)
from git_writer.git_reader import (
get_staged_context,
)
from git_writer.renderer import (
render_commit_message,
save_outputs,
)
from git_writer.test_runner import (
run_tests,
)
from git_writer.validator import (
validate_commit,
validate_files,
validate_test_claim,
)
def read_context_file(
file_path: str,
) -> str:
path = Path(file_path)
if not path.exists():
return ""
return path.read_text(
encoding="utf-8"
)
def parse_arguments():
parser = argparse.ArgumentParser(
description=(
"Generate commit and PR text "
"from staged Git changes."
)
)
parser.add_argument(
"--run-tests",
action="store_true",
)
return parser.parse_args()
def main():
arguments = parse_arguments()
git_context = get_staged_context()
if not git_context.diff.strip():
raise RuntimeError(
"No staged changes found."
)
test_run = run_tests(
enabled=arguments.run_tests
)
context = {
"change_goal": read_context_file(
"context/change_goal.md"
),
"project_rules": (
read_context_file(
"context/project_rules.md"
)
),
"staged_diff": (
git_context.diff
),
"name_status": (
git_context.name_status
),
"diff_stat": git_context.stat,
"changed_files": (
git_context.changed_files
),
"test_run": test_run,
}
result = generate_git_writing(
context
)
validate_commit(result)
validate_files(
result,
git_context.changed_files,
)
validate_test_claim(
result,
test_run,
)
save_outputs(
result.commit,
result.pull_request,
Path("output"),
)
print(
"Suggested commit message:\n"
)
print(
render_commit_message(
result.commit
)
)
if (
result.split_suggestion
.recommended
):
print(
"\nWarning: split commit "
"is recommended."
)
print(
result.split_suggestion.reason
)
if __name__ == "__main__":
main()
آمادهکردن تغییرات
فایلهای مورد نظر را Stage کنید:
git add app/pagination.py tests/test_pagination.py
بررسی:
git diff --cached
تولید پیام بدون اجرای تست
python generate_git_text.py
در این حالت PR باید صریحاً اعلام کند:
تست خودکار در این مرحله اجرا نشده است.
تولید پیام همراه اجرای تست
python generate_git_text.py --run-tests
فایلهای خروجی:
output/commit-message.txt
output/pull-request.md
استفاده از پیام پیشنهادی
پس از بررسی:
git commit -F output/commit-message.txt
ابزار مقاله بهطور خودکار Commit نمیکند. ابتدا Diff و متن پیشنهادی را بررسی کنید.
نمونه پیام Commit
fix(pagination): calculate offset from one-based page number
Use (page - 1) * page_size so the first page starts from
the first item instead of skipping one page.
Add boundary tests for first, second and out-of-range pages.
نمونه توضیحات Pull Request
# Fix one-based pagination offset
## خلاصه
محاسبه Offset صفحهبندی اصلاح شد تا صفحه اول از اولین رکورد آغاز شود.
## دلیل تغییر
شماره صفحه در قرارداد API از یک شروع میشود، اما فرمول قبلی Offset را مانند شماره صفحه صفرمبنا محاسبه میکرد.
## تغییرات
- اصلاح فرمول Offset برای صفحهبندی یکمبنا
- افزودن تست صفحه اول
- افزودن تست صفحه دوم
- افزودن تست صفحه خارج از محدوده
## فایلهای مهم
- `app/pagination.py`: اصلاح محاسبه Offset
- `tests/test_pagination.py`: افزودن تستهای مرزی
## تست
- وضعیت: موفق
- دستور: `python -m pytest -q`
- نتیجه: تمام تستهای اجراشده موفق بودند
## نکات بازبینی
- رفتار Validation شماره صفحه تغییر نکرده است
- ساختار خروجی Pagination بدون تغییر باقی مانده است
تشخیص تغییرات نامرتبط
فرض کنید Diff شامل این موارد باشد:
- اصلاح Pagination
- تغییر رنگ Button
- ارتقای Dependency
- ویرایش README
یک Commit واحد احتمالاً بیش از یک مسئولیت دارد.
خروجی:
{
"recommended": true,
"reason": "تغییرات Pagination، رابط کاربری و Dependency هدف مشترک مستقیمی ندارند.",
"suggested_commits": [
"fix(pagination): calculate one-based page offset",
"style(ui): update primary button color",
"build(deps): update HTTP client dependency",
"docs(readme): clarify local setup"
]
}
هوش مصنوعی فقط Split را پیشنهاد میدهد. انتخاب و Stage کردن فایلها باید آگاهانه انجام شود.
ساخت Commitهای کوچک با Git
ابتدا همه تغییرها را از Stage خارج نکنید مگر اینکه قصد و وضعیت Worktree را میشناسید. روش معمول، Stage انتخابی است:
git add -p
سپس:
git diff --cached
بعد ابزار تولید پیام را اجرا کنید.
تشخیص Type از روی اثر تغییر
feat
رفتار جدید برای کاربر یا مصرفکننده اضافه شده است.
feat(search): add category filter
fix
رفتار موجود اشتباه بوده و اصلاح شده است.
fix(pagination): calculate one-based page offset
refactor
ساختار تغییر کرده اما Contract حفظ شده است.
refactor(pricing): extract money rounding helper
test
فقط Test تغییر کرده است.
test(cart): cover full discount boundary
docs
فقط مستندات تغییر کردهاند.
docs(api): add product creation example
مدل باید اثر اصلی Diff را بسنجد، نه فقط نام پوشه را.
تشخیص Scope
Scope معمولاً یکی از این موارد است:
- ماژول
- قابلیت
- سرویس
- Package
- Component
- دامنه کسبوکار
Scope خوب:
pagination
cart
billing
api
docs
ci
Scope ضعیف:
src
code
files
changes
misc
اگر تغییر چند Scope مرتبط دارد، میتوان Scope را حذف کرد یا Commit را تقسیم کرد.
Breaking Change را چگونه تشخیص دهیم؟
نمونههای احتمالی:
- حذف Endpoint
- تغییر نام فیلد Response
- اجباریشدن پارامتر اختیاری
- تغییر نوع داده
- حذف Event
- تغییر امضای API عمومی
- حذف Configuration قدیمی
مدل باید شاهد ارائه کند:
Response field user_id was removed and replaced with id.
وجود تغییر بزرگ در Diff بهتنهایی Breaking Change را اثبات نمیکند.
تولید Changelog کاربرمحور
Commit Message با Changelog یکسان نیست.
Commit:
refactor(cache): extract key builder
این تغییر ممکن است برای Changelog کاربر اهمیتی نداشته باشد.
Commit:
feat(export): add CSV export for monthly reports
نسخه Changelog:
- امکان دریافت خروجی CSV از گزارشهای ماهانه اضافه شد.
پرامپت Changelog:
تغییرات زیر را برای کاربر محصول خلاصه کن.
قواعد:
- Refactor داخلی بدون اثر کاربر را حذف کن.
- جزئیات فایل و تابع را ننویس.
- فقط رفتار قابل مشاهده را توضیح بده.
- قابلیت یا Fix موجودنبوده اختراع نکن.
- Breaking Change را همراه اقدام لازم بنویس.
تولید Release Notes از چند Commit
ورودی مناسب:
- پیام Commitهای معتبر
- عنوان PR
- Labelها
- نسخه قبلی و جدید
- Breaking Changeها
- نتیجه تست یا انتشار
- مخاطب Release Notes
خروجی:
{
"highlights": [],
"added": [],
"fixed": [],
"changed": [],
"deprecated": [],
"removed": [],
"migration_steps": []
}
برای Release Notes فقط Git Diff آخرین Commit کافی نیست.
ساخت Git Hook برای پیشنهاد پیام
Hook میتواند هنگام Commit پیشنهاد تولید کند، اما نباید مانع کار توسعهدهنده شود.
فایل نمونه:
.git/hooks/prepare-commit-msg
اسکریپت میتواند در صورت خالیبودن پیام، ابزار را اجرا و پیشنهاد را در فایل Commit Message قرار دهد.
بااینحال Hook باید:
- سریع باشد.
- در صورت نبود شبکه Commit را متوقف نکند.
- پیام موجود کاربر را بازنویسی نکند.
- امکان غیرفعالسازی داشته باشد.
- خطای مدل را به Git Failure تبدیل نکند.
برای تیمهای بزرگ، اجرای دستی یا دستور اختصاصی معمولاً قابل کنترلتر است.
ساخت Alias محلی
git config alias.ai-message \
'!python generate_git_text.py'
سپس:
git ai-message
این Alias وابسته به محل پروژه و محیط Python است و باید متناسب با ساختار Repository تنظیم شود.
استفاده در CI برای PR Description
CI میتواند:
- Base و Head Branch را مشخص کند.
- Diff را استخراج کند.
- تستها را اجرا کند.
- خلاصه PR بسازد.
- خروجی را بهصورت Artifact ذخیره کند.
- توسعهدهنده آن را بررسی کند.
در شروع بهتر است AI متن PR را جایگزین نکند؛ فقط پیشنویس پیشنهاد دهد.
جلوگیری از ادعاهای نادرست
قواعد مهم:
تست
فقط اگر Command واقعاً اجرا شده باشد:
{
"executed": true,
"passed": true
}
فایل
فایل مستندشده باید در git diff --cached --name-status وجود داشته باشد.
رفتار
هر تغییر رفتاری باید شاهدی در Diff یا هدف تغییر داشته باشد.
Issue
شماره Issue نباید بدون ورودی معتبر ساخته شود.
Performance
ادعای «بهبود ۵۰ درصدی» فقط با Benchmark واقعی مجاز است.
سازگاری
ادعای Backward Compatible بودن باید بر اساس Contract و تست مشخص باشد.
مدیریت Diff بزرگ
برای Diff بزرگ:
- هر فایل جداگانه خلاصه شود.
- فایلهای Generated فیلتر شوند.
- خلاصههای فایل ترکیب شوند.
- هدف مشترک تغییر پیدا شود.
- Split Commit پیشنهاد شود.
- فقط سپس Commit و PR ساخته شوند.
معماری Map-Reduce:
File Diffs
↓
Per-file Summaries
↓
Change Groups
↓
Commit Suggestions
↓
PR Summary
Chunk کردن Diff
هر Chunk باید:
- فقط به یک فایل تعلق داشته باشد.
- Hunk کامل را حفظ کند.
- نام فایل داشته باشد.
- خطوط Context کافی داشته باشد.
- از حد Context عبور نکند.
مدل نباید Summary چند فایل نامرتبط را در یک مرحله تولید کند.
ارزیابی AI Git Writer
Dataset مرجع:
- Bug Fix ساده
- Feature
- Refactor خالص
- فقط Test
- فقط Docs
- Dependency Update
- چند تغییر نامرتبط
- Breaking Change
- Diff بدون هدف روشن
- تست اجراشده و موفق
- تست اجراشده و ناموفق
- تست اجرانشده
- Diff Truncated
معیارها:
| معیار | توضیح |
|---|---|
| Type Accuracy | انتخاب صحیح Commit Type |
| Scope Accuracy | Scope مناسب |
| Subject Validity | رعایت ساختار و طول |
| Diff Grounding | تطابق ادعاها با Diff |
| Test Claim Accuracy | تطابق با اجرای واقعی |
| Breaking Change Recall | کشف تغییر ناسازگار |
| False Breaking Rate | اعلام اشتباه Breaking Change |
| Split Accuracy | تشخیص تغییر نامرتبط |
| Human Edit Rate | میزان ویرایش لازم |
| Acceptance Rate | درصد پیامهای پذیرفتهشده |
انتخاب مدل مناسب
مدل مناسب باید:
- Git Diff را درک کند.
- تغییر رفتار و Refactor را جدا کند.
- از Conventional Commits پیروی کند.
- JSON معتبر تولید کند.
- فارسی و انگلیسی را درست ترکیب کند.
- از ادعای بدون شاهد اجتناب کند.
- Context کافی برای Diff داشته باشد.
برای Commitهای کوچک، مدل سریع و اقتصادی کافی است. برای PRهای چندفایلی، مدل قویتر میتواند خلاصه بهتری تولید کند.
مدلها و قیمتهای بهروز را در صفحه مدلهای درواره ببینید.
اشتباهات رایج
تولید پیام از Working Tree کامل
فقط تغییرات Stageشده باید پیام Commit را تعیین کنند.
ندادن هدف تغییر
Diff همیشه چرایی تغییر را نشان نمیدهد.
اعلام موفقیت تست بدون اجرا
Test Evidence باید از Command واقعی ساخته شود.
انتخاب feat برای هر تغییر
Type بر اساس اثر تغییر تعیین شود.
Commit بزرگ و چندمنظوره
ابزار باید Split Commit پیشنهاد دهد.
Commit خودکار بدون Review
متن پیشنهادی را قبل از Commit بررسی کنید.
استفاده از نام فایل در Subject
Subject باید رفتار یا هدف را بیان کند.
تولید Changelog از تغییر داخلی
همه Commitها برای کاربر محصول مهم نیستند.
نقشه راه Production
مرحله اول: ابزار محلی
فقط پیام Commit و PR Draft تولید شود.
مرحله دوم: اعتبارسنجی
Type، Scope، طول، فایل و Test Evidence بررسی شوند.
مرحله سوم: Diff بزرگ
Summary فایلمحور و Split Detection اضافه شود.
مرحله چهارم: Git Hook اختیاری
پیشنهاد داخل Commit Editor قرار گیرد.
مرحله پنجم: CI
PR Draft بهصورت Artifact یا Comment تولید شود.
مرحله ششم: Release Notes
Commitهای تأییدشده به Changelog کاربرمحور تبدیل شوند.
چکلیست پیام Commit تولیدشده
- فقط تغییرات Stageشده را توصیف میکند.
- Type درست است.
- Scope معنادار است.
- Subject حداکثر ۷۲ کاراکتر است.
- Subject فعل امری دارد.
- Subject نقطه پایانی ندارد.
- Body چرایی یا اثر تغییر را توضیح میدهد.
- ادعای خارج از Diff ندارد.
- نتیجه تست واقعی است.
- Breaking Change دارای شاهد است.
- تغییرات نامرتبط شناسایی شدهاند.
- پیام قبل از Commit بررسی شده است.
- API Key فقط در Backend یا محیط محلی امن نگهداری میشود.
پرسشهای متداول
آیا هوش مصنوعی میتواند پیام Commit بسازد؟
بله. AI میتواند Git Diff را تحلیل و پیام Conventional Commit پیشنهاد کند. پیام باید پیش از Commit توسط توسعهدهنده بررسی شود.
آیا باید کل Repository را برای مدل بفرستیم؟
خیر. Diff Stageشده، فهرست فایلها، هدف تغییر و قواعد پروژه معمولاً کافیاند. برای تغییر پیچیده میتوان Context فایل مرتبط را اضافه کرد.
Conventional Commit چیست؟
قراردادی برای پیام Commit با ساختار type(scope): subject است که نوع و محدوده تغییر را مشخص میکند.
چگونه پیام Commit خودکار بسازیم؟
با git diff --cached تغییرات Stageشده را بخوانید، آن را به مدل ارسال و خروجی را با قواعد Conventional Commits اعتبارسنجی کنید.
آیا AI میتواند توضیحات Pull Request تولید کند؟
بله. عنوان، خلاصه، تغییرات، تستها و نکات Review را میتوان از Diff و Context ساخت. نتیجه Test فقط باید از اجرای واقعی گرفته شود.
آیا ابزار باید خودش Commit کند؟
بهتر است در نسخه اولیه فقط فایل پیام را بسازد. توسعهدهنده پس از بررسی میتواند با git commit -F از آن استفاده کند.
چگونه چند تغییر نامرتبط را تشخیص دهیم؟
فایلها، Scopeها و هدفهای تغییر را گروهبندی کنید. اگر هدف مشترک مستقیمی ندارند، Split Commit پیشنهاد شود.
بهترین مدل برای نوشتن پیام Commit چیست؟
مدل باید درک مناسبی از کد، Git Diff و خروجی ساختاریافته داشته باشد. مدلهای موجود را در صفحه مدلهای درواره مقایسه کنید.
API درواره چگونه استفاده میشود؟
در Backend یا ابزار محلی، base_url را برابر https://api.darvareh.ir/v1 قرار دهید و Diff و Context کنترلشده را برای مدل ارسال کنید.
جمعبندی
هوش مصنوعی میتواند پیام Commit و توضیحات Pull Request را سریعتر و دقیقتر تولید کند، اما منبع حقیقت باید Git Diff Stageشده و نتیجه واقعی ابزارهای پروژه باشد.
یک ابزار قابل اعتماد نباید فقط متن آزاد تولید کند. Type، Scope، Subject، Breaking Change، فایلهای تغییرکرده و Test Evidence باید در خروجی ساختاریافته باشند و پیش از استفاده اعتبارسنجی شوند.
در پروژه این مقاله یک AI Git Writer با Python، Git، Pydantic و API درواره ساختیم. ابزار تغییرات Stageشده را میخواند، نتیجه واقعی تست را جمعآوری میکند، پیام Conventional Commit و PR فارسی میسازد و ادعاهای مربوط به فایلها و تست را بررسی میکند.
برای شروع، در درواره ثبتنام و API Key دریافت کنید. سپس مدل مناسب را از صفحه مدلهای درواره انتخاب کرده و ابزار را ابتدا روی Commitهای کوچک و تکهدفه آزمایش کنید.
مقالات مرتبط
- ساخت دستیار برنامهنویسی اختصاصی برای شرکت
- راهنمای AGENTS.md برای عاملهای برنامهنویسی
- بهترین مدل هوش مصنوعی برای برنامهنویسی
- بهترین ابزارهای برنامهنویسی با هوش مصنوعی؛ بخش اول
- بهترین ابزارهای برنامهنویسی با هوش مصنوعی؛ بخش دوم
- آموزش Structured Outputs و JSON Schema
- آموزش ارزیابی مدلهای هوش مصنوعی و Evals
- راهنمای ساخت API هوش مصنوعی آماده Production
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.