رفتن به محتوا
LoopX

Pull Request و بازبینی کد

توی این درس یاد می‌گیری چطور تغییرت را پیش از ادغام به بازبینی بگذاری: Pull Request (PR) یعنی «این شاخه را بخوان و اگر خوب بود در main ادغامش کنید». چرخه‌ی کامل را می‌بینی: شاخه‌ی ویژگی، push، باز کردن PR، بازبینی (review)، اصلاح و ادغام. با fork و remote ‌ی upstream به پروژه‌ای که دسترسی نوشتن ندارد مشارکت می‌کنی، با diff سه‌نقطه‌ای (git diff main...feature) همان چیزی را می‌بینی که GitHub در «Files changed» نشان می‌دهد، و سه روش ادغام GitHub را (merge commit، squash، rebase) با دستورهای محلی مقایسه می‌کنی. و gh pr create، gh pr checkout، gh pr merge. تمرین: روی پروژه‌ی خودت یک PR باز کن، کامنت بگذار و ادغامش کن.

مسئله: ادغام بدون نگاه دوم

Section titled “مسئله: ادغام بدون نگاه دوم”

اگر همه مستقیم به main push کنند، یک اشتباه (باگ، رمزی که وارد شده، تغییر ناخواسته) فوراً در محصول می‌نشیند و کسی قبل از آن نگاهش نکرده. Pull Request یک مرحله‌ی توقف است: تغییر روی یک شاخه‌ی جدا است، همکارها آن را می‌خوانند و نظر می‌دهند، تست‌های خودکار اجرا می‌شوند و فقط بعد از تأیید در main ادغام می‌شود. در پروژه‌های متن‌باز هم PR تنها راه مشارکت آدم‌هایی است که اصلاً دسترسی نوشتن ندارند.

تشبیه: ارسال پیش‌نویس برای ویراستار

Section titled “تشبیه: ارسال پیش‌نویس برای ویراستار”

نویسنده فصل جدید را مستقیم در کتاب چاپ‌شده نمی‌نویسد؛ پیش‌نویس را برای ویراستار می‌فرستد. ویراستار روی حاشیه‌ی صفحه می‌نویسد، نویسنده اصلاح می‌کند و دوباره می‌فرستد و بعد از موافقت، فصل در کتاب می‌رود. PR همان پیش‌نویس و حاشیه‌نویسی است، و GitHub برگه‌ای که همه‌ی این رفت‌وبرگشت را نگه می‌دارد.

چرخه‌ی Pull Request: شاخه‌ی ویژگی بساز، push کن، PR باز کن، بازبینی و اصلاح (هر push به همان PR اضافه می‌شود)، و بعد از تأیید ادغام کن.

PR خودش یک ویژگی GitHub است (روی خود Git وجود ندارد) و به حساب کاربری نیاز دارد؛ پس مراحل مرورگر و دستورهای gh «نمونه (اجرا نشده)» هستند. ولی هر چیزی که زیر PR از Git است واقعاً اجرا شده: شاخه‌ی ویژگی، push، دیدن تغییرها مثل «Files changed»، سه روش ادغام، fork و upstream، و حل conflict. «GitHub» در مثال‌ها یک مخزن bare محلی است.

Terminal window
mkdir -p ~/gitlab/github ~/gitlab/sara && cd ~/gitlab
git init -q --bare github/shop.git
cd sara && git init -q && git config user.name "Sara" && git config user.email "sara@example.com"
printf 'def total(price, qty):\n return price * qty\n' > shop.py
printf '# Shop\n' > README.md
git add . && git commit -qm "Add shop skeleton"
git remote add origin ~/gitlab/github/shop.git
git push -q -u origin main
git log --oneline
خروجی
7238e3e Add shop skeleton

مثال ۱: شاخه‌ی ویژگی و push

Section titled “مثال ۱: شاخه‌ی ویژگی و push”

PR همیشه از یک شاخه‌ی جدا می‌آید (نه از main). سارا شاخه را می‌سازد، commit های تمیز می‌زند و push می‌کند:

Terminal window
cd ~/gitlab/sara
git switch -qc feature/coupon
printf '\ndef apply_coupon(total, code):\n if code == "SAVE10":\n return total * 0.9\n return total\n' >> shop.py
git commit -qam "Add apply_coupon with SAVE10 code"
printf '\n## Coupons\n\nUse code SAVE10 for 10 percent off.\n' >> README.md
git commit -qam "Document coupon code in README"
git push -u origin feature/coupon 2>&1 | tail -3
خروجی
To /home/ali/gitlab/github/shop.git
* [new branch] feature/coupon -> feature/coupon
branch 'feature/coupon' set up to track 'origin/feature/coupon'.

روی GitHub، بعد از این push، یک نوار زرد کنار نام شاخه می‌نویسد «Compare & pull request» که با یک کلیک PR می‌سازد. (در پیام push هم گاهی لینک ساخت PR چاپ می‌شود؛ برای سرور محلی این نیست.)

مثال ۲ (نمونه): باز کردن PR

Section titled “مثال ۲ (نمونه): باز کردن PR”

در GitHub: Pull requests ← New pull request؛ شاخه‌ی مبدأ (feature/coupon) و مقصد (main) را انتخاب می‌کنی. فرم این‌هاست: عنوان، توضیح (چه چیزی عوض شد، چرا، چطور آزمایش کردی، اسکرین‌شات، Closes #12)، Reviewers، Assignees، Labels، و گزینه‌ی Draft (پیش‌نویس: هنوز آماده‌ی بازبینی نیست). با gh:

نمونه (اجرا نشده): gh pr create
# با عنوان و توضیح
gh pr create --title "Add coupon codes" --body "Adds SAVE10. Closes #12" --base main --head feature/coupon
# عنوان و توضیح از commit ها پر شود، و دو نفر را برای بازبینی بخواه
gh pr create --fill --reviewer monalisa,hubot
# به‌صورت پیش‌نویس (Draft) و در مرورگر
gh pr create --draft --web

گزینه‌های مهم gh pr create (از مستندات رسمی): -t/--title، -b/--body، -B/--base (شاخه‌ی مقصد)، -H/--head (شاخه‌ی مبدأ)، -f/--fill، -d/--draft، -r/--reviewer، -a/--assignee، -l/--label، -m/--milestone، -w/--web و --dry-run (فقط نشان بده چه می‌ساخت).

مثال ۳: «Files changed»، diff سه‌نقطه‌ای

Section titled “مثال ۳: «Files changed»، diff سه‌نقطه‌ای”

بازبین در برگه‌ی Files changed فقط تغییرهای شاخه را می‌بیند، نه تغییرهای بعدی main. این دقیقاً diff سه‌نقطه‌ای است: git diff main...feature یعنی «تفاوت شاخه‌ی ویژگی با جدّ مشترک با main». همین را برای بازبینی محلی هم می‌توانی بگیری:

Terminal window
mkdir -p ~/gitlab/reza && cd ~/gitlab/reza
git clone -q ~/gitlab/github/shop.git . && git config user.name "Reza" && git config user.email "reza@example.com"
git fetch -q origin
echo "--- commit های PR (در feature هست، در main نیست):"
git log --oneline main..origin/feature/coupon
echo "--- «Files changed»: آمار فایل‌ها"
git diff --stat main...origin/feature/coupon
echo "--- و خود تغییر:"
git diff main...origin/feature/coupon -- shop.py
خروجی
--- commit های PR (در feature هست، در main نیست):
610c228 Document coupon code in README
50f0512 Add apply_coupon with SAVE10 code
--- «Files changed»: آمار فایل‌ها
README.md | 4 ++++
shop.py | 5 +++++
2 files changed, 9 insertions(+)
--- و خود تغییر:
diff --git a/shop.py b/shop.py
index d9ae872..8fcb7d1 100644
--- a/shop.py
+++ b/shop.py
@@ -1,2 +1,7 @@
def total(price, qty):
return price * qty
+
+def apply_coupon(total, code):
+ if code == "SAVE10":
+ return total * 0.9
+ return total

فرق دو نقطه و سه نقطه: main..feature (در log) یعنی commit هایی که در feature هست و در main نیست؛ و main...feature (در diff) یعنی تفاوت feature با جدّ مشترک. اگر main بعد از جداشدن feature جلو رفته باشد، git diff main origin/feature (بدون نقطه) تغییرهای main را هم به‌عنوان «حذف‌شده» نشان می‌دهد و گمراه می‌کند؛ سه‌نقطه همان چیزی است که PR نشان می‌دهد.

مثال ۴: بازبینی کد، چه می‌بینی و چه می‌نویسی

Section titled “مثال ۴: بازبینی کد، چه می‌بینی و چه می‌نویسی”

بازبینی یعنی خواندن تغییر با ذهن یک خواننده‌ی جدید. چه چیزهایی را بررسی کن:

بپرس مثال
درست کار می‌کند؟ (منطق، حالت‌های لبه‌ای) «اگر code خالی یا None باشد چه می‌شود؟»
خوانا است؟ (اسم‌ها، تابع‌های کوتاه) «اسم apply_coupon خوب است ولی پارامتر total مبهم است»
تست دارد؟ «تست برای کد نامعتبر هم اضافه شود»
امن است؟ (ورودی کاربر، رمز) «کد تخفیف در لاگ چاپ نشود»
با بقیه‌ی پروژه جور است؟ (سبک، معماری) «بقیه‌ی فایل‌ها از Decimal استفاده می‌کنند»

در GitHub روی هر خط می‌توانی کامنت بگذاری یا یک پیشنهاد (suggestion) بدهی که نویسنده با یک کلیک اعمالش کند. بازبین هنگام تمام‌کردن یکی از سه را انتخاب می‌کند: Comment (فقط نظر)، Approve (تأیید) یا Request changes (باید اصلاح شود). نکته‌های ادب: روی کد نظر بده نه آدم («این خط می‌تواند ساده‌تر باشد» نه «تو بد نوشتی»)، سؤال بپرس، و فرق «باید» و «پیشنهاد» را روشن کن.

مثال ۵: اصلاح بعد از بازبینی

Section titled “مثال ۵: اصلاح بعد از بازبینی”

رضا خواسته کد خالی هم مدیریت شود. سارا روی همان شاخه commit جدید می‌زند و push می‌کند؛ PR خودکار به‌روز می‌شود (نیازی به PR جدید نیست):

Terminal window
cd ~/gitlab/sara
printf '\n\ndef is_valid_code(code):\n return bool(code) and code.isalnum()\n' >> shop.py
sed -i 's/ if code == "SAVE10":/ if is_valid_code(code) and code == "SAVE10":/' shop.py
git commit -qam "Handle empty or invalid coupon codes (review feedback)"
git push 2>&1 | tail -2
echo "--- حالا PR سه commit دارد:"
git log --oneline main..feature/coupon
خروجی
To /home/ali/gitlab/github/shop.git
610c228..76a57a9 feature/coupon -> feature/coupon
--- حالا PR سه commit دارد:
76a57a9 Handle empty or invalid coupon codes (review feedback)
610c228 Document coupon code in README
50f0512 Add apply_coupon with SAVE10 code

دو راه معمول برای تاریخچه‌ی PR: commit های جدید برای هر نظر (ساده و شفاف؛ بازبین می‌بیند چه عوض شد) یا قبل از ادغام همه را با rebase -i/squash تمیز کن. توجه: بعد از شروع بازبینی، rebase و push --force را با احتیاط بزن؛ کامنت‌های بازبین به commit های قدیمی وصل‌اند و بازنویسی می‌توانند آن‌ها را گم کنند (درس rebase: --force-with-lease).

مثال ۶: سه روش ادغام در GitHub، و معادل محلی‌شان

Section titled “مثال ۶: سه روش ادغام در GitHub، و معادل محلی‌شان”

دکمه‌ی سبز «Merge pull request» سه حالت دارد. هر کدام تاریخچه‌ی main را متفاوت می‌سازد. ببین هر کدام روی همین تغییر چه می‌کند (من معادل Git هر کدام را روی کپی‌های جدا اجرا می‌کنم):

Terminal window
cd ~/gitlab
for m in merge squash rebase; do rm -rf try-$m; git clone -q github/shop.git try-$m; (cd try-$m && git config user.name Ali && git config user.email ali@example.com && git fetch -q origin feature/coupon:feature/coupon); done
echo "=== ۱) Create a merge commit (git merge --no-ff):"
cd try-merge && git merge -q --no-ff --no-edit feature/coupon && git log --oneline --graph | head -6
echo "=== ۲) Squash and merge (git merge --squash + یک commit):"
cd ../try-squash && git merge -q --squash feature/coupon && git commit -qm "Add coupon codes (#12)" && git log --oneline --graph | head -3
echo "=== ۳) Rebase and merge (rebase + fast-forward):"
cd ../try-rebase
echo "(main در این فاصله یک commit جلو رفته است)"
echo "notes" > NOTES.md && git add NOTES.md && git commit -qm "Main moved on while the PR was open"
echo "--- commit های PR قبل از rebase:"
git log --oneline main..feature/coupon
git switch -q feature/coupon && git rebase -q main && git switch -q main && git merge -q --ff-only feature/coupon
echo "--- بعد: خطی، و همان commit ها با هش جدید:"
git log --oneline --graph | head -5
خروجی
=== ۱) Create a merge commit (git merge --no-ff):
* 933f565 Merge branch 'feature/coupon'
|\
| * 76a57a9 Handle empty or invalid coupon codes (review feedback)
| * 610c228 Document coupon code in README
| * 50f0512 Add apply_coupon with SAVE10 code
|/
=== ۲) Squash and merge (git merge --squash + یک commit):
Squash commit -- not updating HEAD
* 96e3a80 Add coupon codes (#12)
* 7238e3e Add shop skeleton
=== ۳) Rebase and merge (rebase + fast-forward):
(main در این فاصله یک commit جلو رفته است)
--- commit های PR قبل از rebase:
76a57a9 Handle empty or invalid coupon codes (review feedback)
610c228 Document coupon code in README
50f0512 Add apply_coupon with SAVE10 code
--- بعد: خطی، و همان commit ها با هش جدید:
* e68a626 Handle empty or invalid coupon codes (review feedback)
* 1c1082a Document coupon code in README
* d3ec592 Add apply_coupon with SAVE10 code
* cbe837a Main moved on while the PR was open
* 7238e3e Add shop skeleton
سه روش ادغام یک PR با سه commit: merge commit (همه‌ی commit ها + یک commit ادغام)، squash (همه به یک commit تبدیل می‌شوند)، rebase (commit ها یکی‌یکی روی main و تاریخچه‌ی خطی).
روش تاریخچه‌ی main مناسب برای
Create a merge commit commit های شاخه + یک commit ادغام وقتی تاریخچه‌ی کامل و واقعی مهم است
Squash and merge یک commit برای کل PR (پیام قابل ویرایش) PR هایی با commit های شلوغ (wip، fix)؛ رایج‌ترین انتخاب
Rebase and merge commit ها یکی‌یکی و خطی روی main (با شناسه‌ی جدید) تیم‌هایی که commit های هر PR را تمیز و معنادار می‌نویسند

یک تفاوت ریز بین «Rebase and merge» در GitHub و git rebase محلی (طبق مستندات GitHub): GitHub همیشه اطلاعات committer را به‌روز و commit ها را با شناسه‌ی جدید می‌سازد، حتی اگر main جلو نرفته باشد؛ ولی git rebase محلی وقتی روی یک جدّ مستقیم بازپخش می‌کند، commit ها را همان‌طور نگه می‌دارد. (برای همین مثال بالا main را جلو بردم تا شناسه‌ها واقعاً عوض شوند.)

سرپرست مخزن می‌تواند تعیین کند کدام روش‌ها مجازند (Settings ← General ← Pull Requests). با gh: gh pr merge --merge، --squash یا --rebase (و -d/--delete-branch برای حذف شاخه بعد از ادغام). نکته‌ی مهم برای squash: پیام یک commit نهایی را خودت می‌نویسی؛ آن را مثل یک commit خوب بنویس و شماره‌ی PR یا Issue را بیاور.

مثال ۷: fork و upstream، مشارکت در پروژه‌ای که دسترسی نداری

Section titled “مثال ۷: fork و upstream، مشارکت در پروژه‌ای که دسترسی نداری”

برای پروژه‌ای که دسترسی نوشتن به آن نداری، اول یک fork می‌سازی: یک کپی از کل مخزن در حساب خودت (روی خود GitHub، با دکمه‌ی Fork). حالا سه مخزن داری: upstream (پروژه‌ی اصلی، فقط‌خواندنی برای تو)، fork (کپی تو در GitHub، که روی آن push می‌کنی) و کلون محلی. PR از fork تو به upstream فرستاده می‌شود. این چیدمان را با دو مخزن bare شبیه‌سازی می‌کنم:

Terminal window
cd ~/gitlab
git init -q --bare github/upstream.git
git clone -q github/shop.git /tmp/lx-seed 2>/dev/null
(cd /tmp/lx-seed && git push -q ~/gitlab/github/upstream.git main)
rm -rf /tmp/lx-seed
echo "--- fork (کپی سرور-به-سرور، مثل دکمه‌ی Fork در GitHub):"
git clone -q --bare github/upstream.git github/fork.git
echo "--- کلون محلی از fork خودت:"
git clone -q github/fork.git mine && cd mine
git config user.name "Ali" && git config user.email "ali@example.com"
git remote add upstream ~/gitlab/github/upstream.git
git remote -v
خروجی
--- fork (کپی سرور-به-سرور، مثل دکمه‌ی Fork در GitHub):
--- کلون محلی از fork خودت:
origin /home/ali/gitlab/github/fork.git (fetch)
origin /home/ali/gitlab/github/fork.git (push)
upstream /home/ali/gitlab/github/upstream.git (fetch)
upstream /home/ali/gitlab/github/upstream.git (push)

origin = fork تو، و upstream = پروژه‌ی اصلی (نام قراردادی). همیشه از یک شاخه‌ی جدید کار کن، نه از main خودت. بعد از مدتی upstream جلو می‌رود و fork تو عقب می‌افتد؛ هماهنگش کن:

Terminal window
cd ~/gitlab
git clone -q github/upstream.git /tmp/lx-maint && (cd /tmp/lx-maint && git config user.name "Maintainer" && git config user.email "m@example.com" && echo "v2 notes" >> README.md && git commit -qam "Maintainer updates README" && git push -q origin main)
rm -rf /tmp/lx-maint
cd mine
echo "--- upstream جلو رفته؛ fork من عقب است:"
git fetch -q upstream
git log --oneline main..upstream/main
echo "--- هماهنگ‌سازی main با upstream و push به fork:"
git switch -q main
git merge -q --ff-only upstream/main
git push -q origin main
git log --oneline -2
خروجی
--- upstream جلو رفته؛ fork من عقب است:
86a54d3 Maintainer updates README
--- هماهنگ‌سازی main با upstream و push به fork:
86a54d3 Maintainer updates README
7238e3e Add shop skeleton

الگوی همیشگی: git fetch upstream ← git merge --ff-only upstream/main (یا rebase) ← git push origin main. --ff-only یعنی اگر main تو چیز اضافه دارد خطا بده (که نباید داشته باشد: روی main کار نکن). GitHub هم دکمه‌ی «Sync fork» و دستور gh repo sync دارد که همین را می‌کند.

اگر بعد از باز کردن PR، main جلو برود و همان خط‌ها را عوض کرده باشد، GitHub می‌گوید «This branch has conflicts that must be resolved» و ادغام را می‌بندد. راهش همان conflict درس‌های قبل است: در شاخه‌ی خودت، main را بگیر و ادغام کن، حل کن و push کن (PR خودکار پاک می‌شود):

Terminal window
cd ~/gitlab/sara
git switch -q main
git pull -q
printf 'def total(price, qty):\n return round(price * qty, 2)\n' > shop.py.new && mv shop.py.new shop.py
git commit -qam "Round totals on main" && git push -q
git switch -q feature/coupon
echo "=== PR با main ناسازگار شد؛ در شاخه‌ی خودم main را می‌گیرم:"
git fetch -q origin
git merge origin/main 2>&1 | tail -2
git status -s
خروجی
=== PR با main ناسازگار شد؛ در شاخه‌ی خودم main را می‌گیرم:
CONFLICT (content): Merge conflict in shop.py
Automatic merge failed; fix conflicts and then commit the result.
UU shop.py

حل: فایل را ترکیب می‌کنم (هم round هم کد تخفیف)، add و commit، و push:

Terminal window
cd ~/gitlab/sara
cat > shop.py <<'EOF'
def total(price, qty):
return round(price * qty, 2)
def apply_coupon(total, code):
if is_valid_code(code) and code == "SAVE10":
return total * 0.9
return total
def is_valid_code(code):
return bool(code) and code.isalnum()
EOF
git add shop.py && git commit -q --no-edit
git push -q 2>&1 | tail -1
git log --oneline --graph -6
خروجی
* 118ca6c Merge remote-tracking branch 'origin/main' into feature/coupon
|\
| * 2be5826 Round totals on main
* | 76a57a9 Handle empty or invalid coupon codes (review feedback)
* | 610c228 Document coupon code in README
* | 50f0512 Add apply_coupon with SAVE10 code
|/
* 7238e3e Add shop skeleton

حالا PR دوباره بدون conflict است. (جایگزین: git rebase origin/main و git push --force-with-lease؛ تاریخچه تمیزتر ولی بازنویسی می‌شود، پس بعد از بازبینی را با احتیاط.)

مثال ۹: بازبینی یک PR روی کامپیوتر خودت

Section titled “مثال ۹: بازبینی یک PR روی کامپیوتر خودت”

برای اجرای کد یک PR (و نه فقط خواندنش) آن را checkout کن. روی GitHub هر PR یک ref ویژه دارد (refs/pull/N/head) که با git fetch origin pull/N/head:نام می‌گیری؛ با ابزار gh: gh pr checkout N. (این رفرنس را GitHub خودش می‌سازد؛ برای سرور محلی ما، آن را دستی ساختم تا فقط دستور را ببینی، و این یک شبیه‌سازی از رفتار GitHub است.)

Terminal window
cd ~/gitlab
git -C github/shop.git update-ref refs/pull/1/head $(git -C github/shop.git rev-parse feature/coupon)
cd reza
echo "--- گرفتن PR شماره‌ی 1 به‌عنوان یک شاخه‌ی محلی:"
git fetch -q origin pull/1/head:pr-1
git switch -q pr-1
git log --oneline -3
echo "--- حالا می‌توانی کد را اجرا و تست کنی:"
python3 -c "
import importlib.util, sys
spec = importlib.util.spec_from_file_location('shop', 'shop.py'); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(m.apply_coupon(100, 'SAVE10'), m.apply_coupon(100, ''))
"
git switch -q main
خروجی
--- گرفتن PR شماره‌ی 1 به‌عنوان یک شاخه‌ی محلی:
118ca6c Merge remote-tracking branch 'origin/main' into feature/coupon
76a57a9 Handle empty or invalid coupon codes (review feedback)
2be5826 Round totals on main
--- حالا می‌توانی کد را اجرا و تست کنی:
90.0 100

یک بازبینی خوب فقط خواندن نیست: اجرا کن، تست کن، حالت‌های لبه‌ای را امتحان کن. این‌جا apply_coupon(100, 'SAVE10') برابر ۹۰ و کد خالی بدون تخفیف است. با gh: gh pr checkout 32 (یا با آدرس یا اسم شاخه) همین کار را یک‌جا می‌کند؛ گزینه‌ها: -b/--branch نام شاخه‌ی محلی، --detach، -f/--force.

نمونه (اجرا نشده): gh pr checkout و merge
gh pr checkout 32 # PR شماره‌ی 32 را بگیر و به شاخه‌اش برو
gh pr view --web # باز کردن در مرورگر
gh pr merge 32 --squash --delete-branch # ادغام با squash و حذف شاخه

پشت پرده: PR فقط یک مقایسه‌ی دو شاخه است

Section titled “پشت پرده: PR فقط یک مقایسه‌ی دو شاخه است”

در Git هیچ مفهومی به اسم «Pull Request» وجود ندارد. PR یک رکورد در GitHub است که این‌ها را نگه می‌دارد: یک شاخه‌ی مبدأ، یک شاخه‌ی مقصد، توضیح، کامنت‌ها، وضعیت تست‌ها و تأییدها. وقتی PR را ادغام می‌کنی، GitHub روی سرور همان کاری را می‌کند که تو محلی با git merge --no-ff، --squash یا rebase می‌کردی. اطلاعات Git دقیقاً همان‌هاست؛ فقط یک رابط همکاری رویش است. پیش از GitHub، پروژه‌هایی مثل هسته‌ی لینوکس همین کار را با ایمیل می‌کردند و Git برایش فرمان دارد: git request-pull خلاصه‌ی یک «PR متنی» می‌سازد (بدون هیچ سرویس‌ای):

Terminal window
cd ~/gitlab/sara
git request-pull main ~/gitlab/github/shop.git feature/coupon 2>&1 | head -12
خروجی
The following changes since commit 2be5826df78ffb50b6225ee49c0756da22f9498e:
Round totals on main (2026-10-04 08:55:05 +0000)
are available in the Git repository at:
/home/ali/gitlab/github/shop.git feature/coupon
for you to fetch changes up to 118ca6c1547084a2de9c957aae36997ca5cf77df:
Merge remote-tracking branch 'origin/main' into feature/coupon (2026-10-04 08:55:05 +0000)

این متن می‌گوید «این تغییرها از main جدا شده‌اند و در این آدرس و شاخه برای گرفتن آماده‌اند». GitHub همین ایده را با رابط و گفت‌وگو و تست خودکار پیاده کرده است.

دستور کار
git push -u origin شاخه فرستادن شاخه‌ی ویژگی
git log --oneline main..origin/شاخه commit های PR
git diff main...origin/شاخه «Files changed» (diff سه‌نقطه‌ای)
git merge --no-ff / --squash / rebase + merge --ff-only معادل محلی سه روش ادغام
git remote add upstream آدرس پروژه‌ی اصلی در fork
git fetch upstream + git merge --ff-only upstream/main هماهنگ‌سازی fork
git fetch origin pull/N/head:pr-N گرفتن یک PR (GitHub)
gh pr create / checkout N / merge N ساخت / گرفتن / ادغام (نمونه)
git request-pull شروع آدرس شاخه خلاصه‌ی متنی تغییرها
بخش یک PR خوب چه بنویس
عنوان کوتاه و دقیق، فعل امری («Add coupon codes»)
چرا مسئله‌ای که حل می‌کند + لینک Issue (Closes #12)
چه چیزی خلاصه‌ی تغییرها (برای خواننده، نه فهرست فایل‌ها)
چطور آزمایش کردی دستور یا مراحل
اسکرین‌شات اگر ظاهر عوض شده
اندازه کوچک: ترجیحاً زیر چند صد خط؛ بزرگ را تکه کن

هزار خط تغییر را کسی دقیق بازبینی نمی‌کند («LGTM» می‌زنند و رد می‌شوند). راه‌حل: PR های کوچک و متمرکز (یک ویژگی، یک باگ)؛ تغییرهای فرعی (فرمت‌دهی، تغییر نام) را در PR جدا بفرست.

اگر روی main fork خودت commit بزنی، با upstream جدا می‌شود و هماهنگ‌کردنش دردسر است. راه‌حل: برای هر PR یک شاخه‌ی جدید؛ main فقط آینه‌ی upstream باشد (و merge --ff-only همین را تضمین می‌کند). ببین اگر main تو چیز اضافه داشته باشد و upstream هم جلو رفته باشد چه می‌شود:

Terminal window
cd ~/gitlab/mine
echo "oops: commit on my main" >> README.md && git commit -qam "Oops on main"
git clone -q ~/gitlab/github/upstream.git /tmp/lx-maint2 && (cd /tmp/lx-maint2 && git config user.name M && git config user.email m@e.com && echo "v3" >> README.md && git commit -qam "Maintainer v3" && git push -q origin main)
rm -rf /tmp/lx-maint2
git fetch -q upstream
git merge --ff-only upstream/main 2>&1 | tail -3
git reset -q --hard origin/main
خروجی
hint:
hint: Disable this message with "git config advice.diverging false"
fatal: Not possible to fast-forward, aborting.

fatal: Not possible to fast-forward, aborting: --ff-only نمی‌گذارد تاریخچه‌ات بی‌صدا به‌هم بریزد و به تو یادآوری می‌کند که main تو چیز اضافه دارد. (آخرین دستور برای ادامه‌ی مثال‌ها، commit اشتباهی را دور ریخت.)

۳) بازنویسی تاریخچه‌ی PR بعد از بازبینی

Section titled “۳) بازنویسی تاریخچه‌ی PR بعد از بازبینی”

push --force بعد از شروع بازبینی کامنت‌ها را به commit های ازبین‌رفته می‌چسباند و بازبین نمی‌فهمد چه عوض شده. راه‌حل: commit های جدید برای اصلاح، و در پایان با «Squash and merge» تمیز کن.

۴) ادغام بدون تست و بدون خواندن توضیح

Section titled “۴) ادغام بدون تست و بدون خواندن توضیح”

دکمه‌ی سبز را زدن قبل از اتمام تست‌ها (یا بدون یک نفر دوم) یعنی PR فایده‌ای نداشت. راه‌حل: قاعده‌ی محافظت از main (درس بعد): تست‌ها و حداقل یک تأیید اجباری.

۵) PR از شاخه‌ی اشتباه (مثلاً به مقصد اشتباه)

Section titled “۵) PR از شاخه‌ی اشتباه (مثلاً به مقصد اشتباه)”

PR را به‌جای main به شاخه‌ی دیگری باز می‌کنی. راه‌حل: پیش از ساخت، base و compare را در بالای صفحه بخوان؛ با gh pr create --base main.

✎ تمرینآسان

شاخه‌ی ویژگی بساز، دو commit بزن و با git diff --stat main...feature ببین PR چه فایل‌هایی را نشان می‌داد.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -q
echo base > a.txt && git add a.txt && git commit -qm "Base"
git switch -qc feature/x
echo 1 > b.txt && git add b.txt && git commit -qm "Add b"
echo 2 >> a.txt && git commit -qam "Update a"
git switch -q main && echo main > c.txt && git add c.txt && git commit -qm "Main moves"
echo "--- سه‌نقطه‌ای (فقط تغییرهای feature):"
git diff --stat main...feature/x
echo "--- بدون سه‌نقطه (تغییر main هم قاطی می‌شود):"
git diff --stat main feature/x
خروجی
--- سه‌نقطه‌ای (فقط تغییرهای feature):
a.txt | 1 +
b.txt | 1 +
2 files changed, 2 insertions(+)
--- بدون سه‌نقطه (تغییر main هم قاطی می‌شود):
a.txt | 1 +
b.txt | 1 +
c.txt | 1 -
3 files changed, 2 insertions(+), 1 deletion(-)
✎ تمرینمتوسط

تمرین اصلی: روی پروژه‌ی خودت یک PR باز کن، کامنت بگذار و merge کن. روی GitHub واقعی به حساب نیاز دارد و اجرایش نکرده‌ام (نمونه)؛ دستورها در پایین است. بخش محلی (شاخه‌ی ویژگی، commit، push، دیدن «Files changed» و ادغام با squash) را واقعاً اجرا کرده‌ام.

دیدن جواب
نمونه (اجرا نشده): PR واقعی با gh
git switch -c feature/hello
echo "print('hello')" > hello.py && git add hello.py && git commit -m "Add hello script"
git push -u origin feature/hello
gh pr create --title "Add hello script" --body "Adds a tiny hello script. Closes #1" --base main
gh pr view --web # کامنت بگذار (در مرورگر)
gh pr merge --squash --delete-branch # بعد از تأیید

معادل محلی که اجرا کردم:

Terminal window
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2
git init -q --bare ../github/ex2.git
git clone -q ../github/ex2.git work && cd work
git config user.name "Ali" && git config user.email "ali@example.com"
echo "# Hello project" > README.md && git add README.md && git commit -qm "Add README" && git push -q origin main
git switch -qc feature/hello
echo "print('hello')" > hello.py && git add hello.py && git commit -qm "Add hello script"
git push -q -u origin feature/hello
echo "--- Files changed:"
git diff --stat main...feature/hello
echo "--- Squash and merge (معادل محلی) و حذف شاخه:"
git switch -q main && git merge -q --squash feature/hello && git commit -qm "Add hello script (#1)"
git push -q origin main && git push -q origin --delete feature/hello
git log --oneline --graph
خروجی
warning: You appear to have cloned an empty repository.
--- Files changed:
hello.py | 1 +
1 file changed, 1 insertion(+)
--- Squash and merge (معادل محلی) و حذف شاخه:
Squash commit -- not updating HEAD
* 69a44ca Add hello script (#1)
* ef4da32 Add README
✎ تمرینسخت

fork را شبیه‌سازی کن: یک upstream بساز، آن را به fork کپی کن، از fork یک کلون بگیر، شاخه‌ی مشارکت بساز، push کن؛ بعد upstream یک commit جدید بگیرد و تو با fetch و merge --ff-only همگام شو. ثابت کن بعد از همگام‌سازی main تو با upstream/main یکی است.

دیدن جواب
Terminal window
cd ~/gitlab
git init -q --bare github/up3.git
git clone -q github/up3.git /tmp/lx-seed3 2>/dev/null
(cd /tmp/lx-seed3 && git config user.name M && git config user.email m@e.com && echo "v1" > f && git add f && git commit -qm "v1" && git push -q origin main)
rm -rf /tmp/lx-seed3
git clone -q --bare github/up3.git github/fork3.git
git clone -q github/fork3.git me3 && cd me3
git config user.name Ali && git config user.email ali@example.com
git remote add upstream ~/gitlab/github/up3.git
git switch -qc fix/typo && echo "contribution" > c.txt && git add c.txt && git commit -qm "Add contribution" && git push -q -u origin fix/typo
echo "--- upstream جلو می‌رود:"
git clone -q ~/gitlab/github/up3.git /tmp/lx-m3 && (cd /tmp/lx-m3 && git config user.name M && git config user.email m@e.com && echo "v2" >> f && git commit -qam "v2" && git push -q origin main); rm -rf /tmp/lx-m3
git switch -q main && git fetch -q upstream && git merge -q --ff-only upstream/main
git push -q origin main
echo "--- یکی هستند؟"
git rev-parse main upstream/main
خروجی
--- upstream جلو می‌رود:
--- یکی هستند؟
79bc1c3b610b479bc5ea1971654b77e0f8b16dbb
79bc1c3b610b479bc5ea1971654b77e0f8b16dbb
⚡ بررسی سریع

در GitHub برگه‌ی «Files changed» یک PR دقیقاً معادل کدام دستور است؟

؟ آزمونک
  1. Pull Request چیست؟

  2. در fork-based workflow کدام remote ها را داری؟

  3. تفاوت «Squash and merge» و «Create a merge commit»؟

  4. بعد از کامنت بازبین، اصلاح را چطور وارد PR می‌کنی؟

  5. PR می‌گوید «This branch has conflicts». راه‌حل؟

  • PR پیشنهاد ادغام یک شاخه با بازبینی؛ رکوردی در GitHub، نه مفهوم Git. چرخه: شاخه‌ی ویژگی ← push ← PR ← بازبینی و اصلاح (push های بعدی به همان PR می‌روند) ← ادغام.
  • «Files changed» = git diff main...feature (سه‌نقطه). commit های PR: git log main..origin/feature.
  • بازبینی: درستی، خوانایی، تست، امنیت؛ اجرا کن (gh pr checkout N یا git fetch origin pull/N/head:pr-N). نظرت روی کد باشد نه آدم.
  • سه روش ادغام: merge commit (همه + یک commit ادغام)، squash (یک commit)، rebase (خطی)؛ معادل محلی: merge --no-ff، merge --squash، rebase + merge --ff-only.
  • fork: origin = fork تو، upstream = پروژه‌ی اصلی؛ همگام‌سازی: fetch upstream + merge --ff-only upstream/main + push origin main. روی main کار نکن.
  • PR کوچک، توضیح روشن، تست، و Closes #شماره. conflict را در شاخه‌ی خودت حل کن. gh pr create/checkout/merge (نمونه).
برگه‌ی تقلب این درس
دستورکاری که می‌کند
git push -u origin feature/xارسال شاخه‌ی ویژگی (برای PR)
git diff main...origin/feature/x«Files changed» (سه‌نقطه‌ای)
git log --oneline main..origin/feature/xcommit های PR
git remote add upstream آدرسپروژه‌ی اصلی (در fork)
git fetch upstream && git merge --ff-only upstream/mainهمگام‌سازی fork
git fetch origin pull/N/head:pr-Nگرفتن PR (GitHub)
gh pr create --fill --reviewer نامساخت PR (نمونه)
gh pr checkout Nگرفتن PR (نمونه)
gh pr merge N --squash --delete-branchادغام با squash (نمونه)
git merge origin/main (در شاخه‌ی PR)رفع conflict PR