تعليقات المراجعة الآلية لـ PR
قراءة فروق PR ونشر تعليقات المراجعة المضمنة عبر API
تعليقات المراجعة الآلية لـ PR درس مجاني في AI Agents على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Agents، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Agents 4 دروس في المجموع.
ما المقصود بمراجعة طلبات السحب آليًا؟
يقرأ وكيل المراجعة الآلية لطلبات السحب الملفات التي تغيّرت، ويحلل الفروقات، وينشر تعليقات مراجعة محددة — تمامًا مثل المراجع البشري، لكن بسرعة واتساق أكبر. وتشمل حالات الاستخدام: فحص الأمان، وفرض معايير الأسلوب، واكتشاف الأخطاء الشائعة، والتحقق من تغطية الاختبارات، أو تلخيص ما يفعله طلب السحب.
from github import Github
import os
g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
# Get a specific pull request by number
pr = repo.get_pull(42)
print(f'PR #{pr.number}: {pr.title}')
print(f'Author: {pr.user.login}')
print(f'Base branch: {pr.base.ref} <- Head: {pr.head.ref}')
print(f'State: {pr.state}')
print(f'Changed files: {pr.changed_files}')
print(f'Additions: +{pr.additions}')
print(f'Deletions: -{pr.deletions}')سرد طلبات السحب المفتوحة
استخدموا repo.get_pulls() لسرد طلبات السحب. صفّوا النتائج حسب الحالة (open أو closed) أو الفرع الأساسي أو ترتيب الفرز. وبالنسبة إلى بوت المراجعة، عادةً ما تعالجون طلبات السحب open التي لم تُراجع بعد.
from github import Github
import os
g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
# All open PRs targeting main
open_prs = repo.get_pulls(
state='open',
base='main',
sort='created',
direction='desc'
)
for pr in open_prs:
# Check if agent has already reviewed it
reviews = list(pr.get_reviews())
agent_reviewed = any(
r.user.login == 'my-agent-bot[bot]'
for r in reviews
)
if not agent_reviewed:
print(f'Needs review: PR #{pr.number} - {pr.title}')الحصول على الملفات التي تغيّرت
يُرجع pr.get_files() قائمة من كائنات File — يمثل كل منها ملفًا تغيّر في طلب السحب. ومن الخصائص المهمة: filename وstatus (مضاف/معدّل/محذوف) وadditions وdeletions وpatch (نص الفرق الموحّد).
from github import Github
import os
g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)
changed_files = list(pr.get_files())
print(f'Changed files: {len(changed_files)}')
for file in changed_files:
print(f'\n{file.status.upper()}: {file.filename}')
print(f' +{file.additions} -{file.deletions}')
# The patch is the unified diff
if file.patch:
print(f' Patch preview: {file.patch[:200]}')
# Filter to only Python files
python_files = [
f for f in changed_files
if f.filename.endswith('.py') and f.status != 'removed'
]قراءة رقعة الملف (Diff)
إن file.patch عبارة عن سلسلة نصية لفرق موحّد. أُضيفت الأسطر التي تبدأ بالرمز +، وحُذفت الأسطر التي تبدأ بالرمز -. ويعرض الرأس @@ أرقام الأسطر التي تبدأ عندها مجموعة التغييرات. حلّلوا ذلك لتحديد موضع المشكلات بدقة في التعليمة البرمجية الجديدة.
def parse_patch_hunks(patch):
if not patch:
return []
hunks = []
current_hunk = None
new_line_num = 0
for line in patch.split('\n'):
if line.startswith('@@'):
# Parse: @@ -old_start,old_count +new_start,new_count @@
import re
match = re.search(r'\+([0-9]+)', line)
if match:
new_line_num = int(match.group(1))
current_hunk = {'header': line, 'lines': [], 'start_line': new_line_num}
hunks.append(current_hunk)
elif current_hunk is not None:
if line.startswith('+'):
current_hunk['lines'].append({'type': 'add', 'content': line[1:], 'line': new_line_num})
new_line_num += 1
elif line.startswith('-'):
current_hunk['lines'].append({'type': 'remove', 'content': line[1:], 'line': None})
else:
new_line_num += 1
return hunks
# --- demo ---
patch = (
'@@ -10,3 +10,4 @@\n'
' def add(a, b):\n'
'- return a - b\n'
'+ return a + b\n'
'+ # fixed subtraction bug\n'
)
for hunk in parse_patch_hunks(patch):
print(hunk['header'])
for line in hunk['lines']:
print(' ', line)
الحصول على عمليات إيداع طلب السحب
يُرجع pr.get_commits() عمليات الإيداع الموجودة في طلب السحب. يحتوي كل إيداع على رسالة ومؤلف وقائمة بالملفات التي تغيّرت. يساعد تحليل رسائل الإيداع بوت المراجعة على تلخيص ما يفعله طلب السحب والتحقق من سلامة ممارسات الإيداع.
from github import Github
import os
g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)
commits = list(pr.get_commits())
print(f'Commits in PR: {len(commits)}')
for commit in commits:
print(f'\nCommit: {commit.sha[:8]}')
print(f'Author: {commit.commit.author.name}')
print(f'Message: {commit.commit.message[:80]}')
print(f'Changes: +{commit.stats.additions} -{commit.stats.deletions}')
# Check commit message quality
poor_messages = [
c for c in commits
if len(c.commit.message.split()[0] if c.commit.message else '') < 3
or c.commit.message.lower().startswith(('wip', 'fix', 'test'))
]
if poor_messages:
print(f'{len(poor_messages)} commits have poor message quality')نشر مراجعة لطلب سحب
استخدموا pr.create_review() لإرسال مراجعة رسمية. يتحكم المعامل event في نوع المراجعة: 'APPROVE' أو 'REQUEST_CHANGES' أو 'COMMENT'. ويمكن أن تتضمن المراجعة نصًا عامًا وقائمة من التعليقات المضمّنة على أسطر محددة.
from github import Github
import os
g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)
# Submit a review with a general comment (no inline comments)
pr.create_review(
body=(
'**Automated Code Review**\n\n'
'I analyzed this PR and found the following:\n'
'- No security issues detected\n'
'- Style conforms to project guidelines\n'
'- Test coverage looks adequate\n\n'
'_This review was generated automatically._'
),
event='COMMENT' # 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT'
)نشر تعليقات مراجعة مضمّنة
تظهر التعليقات المضمّنة مباشرةً على أسطر محددة في الفرق. يحدد كل تعليق path (مسار الملف) وline (رقم السطر في الملف الجديد) وbody. مرّروا قائمة التعليقات إلى pr.create_review(comments=[...]).
from github import Github
import os
g = Github(token=os.environ['GITHUB_TOKEN'])
repo = g.get_repo('myorg/myrepo')
pr = repo.get_pull(42)
# Build inline comments from analysis results
inline_comments = [
{
'path': 'src/auth.py',
'line': 47,
'body': (
':warning: **Security Issue:** Using `md5` for password hashing.\n'
'Use `bcrypt` or `argon2` instead.\n'
'Example: `import bcrypt; bcrypt.hashpw(password.encode(), bcrypt.gensalt())`'
)
},
{
'path': 'src/utils.py',
'line': 23,
'body': (
':bulb: Consider using `pathlib.Path` instead of `os.path.join` '
'for better cross-platform compatibility.'
)
}
]
pr.create_review(
body='Security and style review complete. See inline comments.',
event='REQUEST_CHANGES',
comments=inline_comments
)
print(f'Review submitted with {len(inline_comments)} inline comments')تحليل التعليمة البرمجية باستخدام LLM
مرّروا فرق الملف إلى LLM باستخدام مطالبة لمراجعة الأمان والجودة. اطلبوا استجابة JSON منظمة تسرد المشكلات مع مسار الملف ورقم السطر ودرجة الخطورة والوصف. استخدموا هذه البيانات لإنشاء قائمة التعليقات المضمّنة.
import anthropic
import json
import os
client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
def analyze_file_with_llm(filename, patch):
prompt = (
f'Review this Python code diff for issues.\n'
f'File: {filename}\n\n'
f'Return JSON array: [{{"line": N, "severity": "error|warning|info", '
f'"message": "..."}}]\n'
f'Focus on: security vulnerabilities, bugs, and anti-patterns.\n'
f'Diff:\n{patch[:3000]}'
)
response = client.messages.create(
model='claude-opus-4-5',
max_tokens=1000,
messages=[{'role': 'user', 'content': prompt}]
)
try:
issues = json.loads(response.content[0].text)
return [
{'path': filename, 'line': i.get('line', 1),
'body': f'[{i.get("severity", "info").upper()}] {i.get("message", "")}'}
for i in issues if isinstance(i, dict)
]
except json.JSONDecodeError:
return []التدفق الكامل لوكيل مراجعة طلبات السحب
اربطوا جميع الأجزاء في تدفق كامل لمراجعة طلبات السحب: جلب الملفات، وتصفية الملفات القابلة للمراجعة، وتحليل كل ملف باستخدام LLM، وتجميع التعليقات، وإرسال المراجعة في استدعاء API واحد. اقتصِروا على أول 10 ملفات تغيّرت للتحكم في تكاليف LLM.
def run_pr_review(g, repo_name, pr_number):
repo = g.get_repo(repo_name)
pr = repo.get_pull(pr_number)
print(f'Reviewing PR #{pr_number}: {pr.title}')
# Get changed files (limit to 10)
files = [
f for f in list(pr.get_files())[:10]
if f.filename.endswith(('.py', '.js', '.ts'))
and f.patch
]
all_comments = []
for file in files:
print(f' Analyzing: {file.filename}')
comments = analyze_file_with_llm(file.filename, file.patch)
all_comments.extend(comments)
# Determine review event based on findings
critical = [c for c in all_comments if '[ERROR]' in c['body']]
event = 'REQUEST_CHANGES' if critical else 'COMMENT'
summary = (
f'Analyzed {len(files)} files. '
f'Found {len(all_comments)} issues '
f'({len(critical)} critical).'
)
pr.create_review(body=summary, event=event, comments=all_comments[:25])
print(f'Review submitted: {event} with {len(all_comments)} comments')التحقق من وجود مراجعات سابقة للوكيل
قبل نشر مراجعة، تحققوا مما إذا كان بوت وكيلكم قد راجع طلب السحب هذا مسبقًا في حالته الحالية. تجنبوا المراجعات المكررة بالتحقق من قائمة المراجعات الحالية بحثًا عن اسم مستخدم البوت.
def should_review_pr(pr, bot_username):
reviews = list(pr.get_reviews())
# Check if bot already reviewed this PR
bot_reviews = [
r for r in reviews
if r.user and r.user.login == bot_username
]
if not bot_reviews:
return True # No previous review, proceed
# Check if new commits were pushed since last review
last_review_time = max(r.submitted_at for r in bot_reviews)
commits = list(pr.get_commits())
latest_commit_time = commits[-1].commit.author.date if commits else None
if latest_commit_time and latest_commit_time > last_review_time:
print(f'New commits since last review, re-reviewing...')
return True
print(f'PR #{pr.number} already reviewed by {bot_username}')
return False
# --- demo: minimal stand-ins for PyGithub-style PR/review/commit objects ---
import datetime
from types import SimpleNamespace
class _FakeCommit:
def __init__(self, date):
self.commit = SimpleNamespace(author=SimpleNamespace(date=date))
class _FakeReview:
def __init__(self, login, submitted_at):
self.user = SimpleNamespace(login=login)
self.submitted_at = submitted_at
class _FakePR:
def __init__(self, number, reviews, commits):
self.number = number
self._reviews = reviews
self._commits = commits
def get_reviews(self):
return self._reviews
def get_commits(self):
return self._commits
pr_never_reviewed = _FakePR(101, [], [_FakeCommit(datetime.datetime(2026, 8, 1))])
print('PR #101 needs review:', should_review_pr(pr_never_reviewed, 'coddy-bot'))
review_time = datetime.datetime(2026, 8, 1, 10, 0)
pr_up_to_date = _FakePR(102, [_FakeReview('coddy-bot', review_time)],
[_FakeCommit(review_time - datetime.timedelta(hours=1))])
print('PR #102 needs review:', should_review_pr(pr_up_to_date, 'coddy-bot'))
نمط فحص الأمان في مراجعة طلبات السحب
من الأنماط الشائعة للمراجعة الآلية فحص الأنماط غير السليمة أمنيًا في الأسطر المضافة، مثل الأسرار المضمّنة مباشرة في الشيفرة، أو استخدام الدوال المهجورة، أو ضم سلاسل SQL، أو استدعاءات eval(). افحص كل سطر مضاف مقابل مجموعة من الأنماط، وأبلغ عن النتائج في صورة تعليقات مضمنة.
import re
SECURITY_PATTERNS = [
(r'eval\s*\(', 'Dangerous eval() call — code injection risk'),
(r'os\.system\s*\(', 'os.system() is unsafe; use subprocess with a list'),
(r'pickle\.loads?\s*\(', 'pickle.load is unsafe with untrusted data'),
(r'execute\(.*%\s*', 'Possible SQL injection — use parameterized queries'),
(r'md5\s*\(|hashlib\.md5', 'MD5 is cryptographically broken for passwords; use bcrypt'),
]
def scan_patch_for_security(filename, patch):
if not patch:
return []
comments = []
new_line = 0
for line in patch.split('\n'):
if line.startswith('@@'):
import re as _re
m = _re.search(r'\+([0-9]+)', line)
if m:
new_line = int(m.group(1))
elif line.startswith('+') and not line.startswith('+++'):
content = line[1:]
for pattern, message in SECURITY_PATTERNS:
if re.search(pattern, content):
comments.append({'path': filename, 'line': new_line,
'body': f':rotating_light: **Security:** {message}'})
new_line += 1
elif not line.startswith('-'):
new_line += 1
return comments
# --- demo ---
patch = (
'@@ -1,2 +1,3 @@\n'
' def run(cmd):\n'
'+ os.system(cmd)\n'
'+ hashlib.md5(cmd.encode())\n'
)
for c in scan_patch_for_security('run.py', patch):
print(c)
تحقق سريع: أحداث المراجعة
اختبر مدى فهمك لخيارات مراجعة طلبات السحب.
خلاصة المراجعة الآلية لطلبات السحب
يمكن لوكيلك الآن مراجعة طلبات السحب برمجيًا:
- repo.get_pull(number) — الحصول على كائن طلب سحب يتضمن البيانات الوصفية
- pr.get_files() — عرض الملفات التي تغيّرت، مع
filenameوpatchوadditions - pr.get_commits() — فحص رسائل عمليات commit وإحصاءاتها
- pr.create_review(body, event, comments) — إرسال مراجعة مع تعليقات مضمنة اختيارية
- تنسيق التعليق المضمن:
{'path': filename, 'line': N, 'body': markdown} - استخدم
'COMMENT'للملاحظات، و'REQUEST_CHANGES'للمشكلات التي تمنع الدمج - تحقق من المراجعات الحالية قبل إعادة المراجعة لتجنب التكرار
- اقصر تحليل الملفات على 10 ملفات و25 تعليقًا مضمنًا للبقاء ضمن حدود API
الأسئلة الشائعة
هل درس «تعليقات المراجعة الآلية لـ PR» مجاني؟
نعم — نص درس «تعليقات المراجعة الآلية لـ PR» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Agents، انتقل إلى CoddyKit PRO. تتضمن دورة AI Agents 4 دروس في المجموع.
ماذا ستتعلم في «تعليقات المراجعة الآلية لـ PR»؟
قراءة فروق PR ونشر تعليقات المراجعة المضمنة عبر API تتمرن على AI Agents مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ AI Agents؟
لا تُشترط خبرة سابقة. AI Agents على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.
كم من الوقت يستغرق درس «تعليقات المراجعة الآلية لـ PR»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس AI Agents هذا؟
نعم. كل درس في AI Agents يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- نظرة عامة على GitHub REST API
- عرض المشكلات وإدارتها
- تعليقات المراجعة الآلية لـ PR
- تحليل سجل الالتزامات والفروق