توی این درس یاد میگیری چطور تغییرت را پیش از ادغام به بازبینی بگذاری: 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 برگهای که همهی این رفتوبرگشت را نگه میدارد.
مثالهای عملی
Section titled “مثالهای عملی”PR خودش یک ویژگی GitHub است (روی خود Git وجود ندارد) و به حساب کاربری نیاز دارد؛ پس مراحل مرورگر و دستورهای gh «نمونه (اجرا نشده)» هستند. ولی هر چیزی که زیر PR از Git است واقعاً اجرا شده: شاخهی ویژگی، push، دیدن تغییرها مثل «Files changed»، سه روش ادغام، fork و upstream، و حل conflict. «GitHub» در مثالها یک مخزن bare محلی است.
mkdir -p ~/gitlab/github ~/gitlab/sara && cd ~/gitlabgit init -q --bare github/shop.gitcd 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.pyprintf '# Shop\n' > README.mdgit add . && git commit -qm "Add shop skeleton"git remote add origin ~/gitlab/github/shop.gitgit push -q -u origin maingit log --oneline7238e3e Add shop skeletonمثال ۱: شاخهی ویژگی و push
Section titled “مثال ۱: شاخهی ویژگی و push”PR همیشه از یک شاخهی جدا میآید (نه از main). سارا شاخه را میسازد، commit های تمیز میزند و push میکند:
cd ~/gitlab/saragit switch -qc feature/couponprintf '\ndef apply_coupon(total, code):\n if code == "SAVE10":\n return total * 0.9\n return total\n' >> shop.pygit commit -qam "Add apply_coupon with SAVE10 code"printf '\n## Coupons\n\nUse code SAVE10 for 10 percent off.\n' >> README.mdgit commit -qam "Document coupon code in README"git push -u origin feature/coupon 2>&1 | tail -3To /home/ali/gitlab/github/shop.git * [new branch] feature/coupon -> feature/couponbranch '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 --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». همین را برای بازبینی محلی هم میتوانی بگیری:
mkdir -p ~/gitlab/reza && cd ~/gitlab/rezagit clone -q ~/gitlab/github/shop.git . && git config user.name "Reza" && git config user.email "reza@example.com"git fetch -q originecho "--- commit های PR (در feature هست، در main نیست):"git log --oneline main..origin/feature/couponecho "--- «Files changed»: آمار فایلها"git diff --stat main...origin/feature/couponecho "--- و خود تغییر:"git diff main...origin/feature/coupon -- shop.py--- commit های PR (در feature هست، در main نیست):610c228 Document coupon code in README50f0512 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.pyindex 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 جدید نیست):
cd ~/gitlab/saraprintf '\n\ndef is_valid_code(code):\n return bool(code) and code.isalnum()\n' >> shop.pysed -i 's/ if code == "SAVE10":/ if is_valid_code(code) and code == "SAVE10":/' shop.pygit commit -qam "Handle empty or invalid coupon codes (review feedback)"git push 2>&1 | tail -2echo "--- حالا PR سه commit دارد:"git log --oneline main..feature/couponTo /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 README50f0512 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 هر کدام را روی کپیهای جدا اجرا میکنم):
cd ~/gitlabfor 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); doneecho "=== ۱) 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 -6echo "=== ۲) 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 -3echo "=== ۳) Rebase and merge (rebase + fast-forward):"cd ../try-rebaseecho "(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/coupongit switch -q feature/coupon && git rebase -q main && git switch -q main && git merge -q --ff-only feature/couponecho "--- بعد: خطی، و همان 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 README50f0512 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| روش | تاریخچهی 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 شبیهسازی میکنم:
cd ~/gitlabgit init -q --bare github/upstream.gitgit 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-seedecho "--- fork (کپی سرور-به-سرور، مثل دکمهی Fork در GitHub):"git clone -q --bare github/upstream.git github/fork.gitecho "--- کلون محلی از fork خودت:"git clone -q github/fork.git mine && cd minegit config user.name "Ali" && git config user.email "ali@example.com"git remote add upstream ~/gitlab/github/upstream.gitgit 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 تو عقب میافتد؛ هماهنگش کن:
cd ~/gitlabgit 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-maintcd mineecho "--- upstream جلو رفته؛ fork من عقب است:"git fetch -q upstreamgit log --oneline main..upstream/mainecho "--- هماهنگسازی main با upstream و push به fork:"git switch -q maingit merge -q --ff-only upstream/maingit push -q origin maingit log --oneline -2--- upstream جلو رفته؛ fork من عقب است:86a54d3 Maintainer updates README--- هماهنگسازی main با upstream و push به fork:86a54d3 Maintainer updates README7238e3e 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 با conflict
Section titled “مثال ۸: PR با conflict”اگر بعد از باز کردن PR، main جلو برود و همان خطها را عوض کرده باشد، GitHub میگوید «This branch has conflicts that must be resolved» و ادغام را میبندد. راهش همان conflict درسهای قبل است: در شاخهی خودت، main را بگیر و ادغام کن، حل کن و push کن (PR خودکار پاک میشود):
cd ~/gitlab/saragit switch -q maingit pull -qprintf 'def total(price, qty):\n return round(price * qty, 2)\n' > shop.py.new && mv shop.py.new shop.pygit commit -qam "Round totals on main" && git push -qgit switch -q feature/couponecho "=== PR با main ناسازگار شد؛ در شاخهی خودم main را میگیرم:"git fetch -q origingit merge origin/main 2>&1 | tail -2git status -s=== PR با main ناسازگار شد؛ در شاخهی خودم main را میگیرم:CONFLICT (content): Merge conflict in shop.pyAutomatic merge failed; fix conflicts and then commit the result.UU shop.pyحل: فایل را ترکیب میکنم (هم round هم کد تخفیف)، add و commit، و push:
cd ~/gitlab/saracat > 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()EOFgit add shop.py && git commit -q --no-editgit push -q 2>&1 | tail -1git 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 است.)
cd ~/gitlabgit -C github/shop.git update-ref refs/pull/1/head $(git -C github/shop.git rev-parse feature/coupon)cd rezaecho "--- گرفتن PR شمارهی 1 بهعنوان یک شاخهی محلی:"git fetch -q origin pull/1/head:pr-1git switch -q pr-1git log --oneline -3echo "--- حالا میتوانی کد را اجرا و تست کنی:"python3 -c "import importlib.util, sysspec = 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/coupon76a57a9 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 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 متنی» میسازد (بدون هیچ سرویسای):
cd ~/gitlab/saragit request-pull main ~/gitlab/github/shop.git feature/coupon 2>&1 | head -12The 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 همین ایده را با رابط و گفتوگو و تست خودکار پیاده کرده است.
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
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) |
| چه چیزی | خلاصهی تغییرها (برای خواننده، نه فهرست فایلها) |
| چطور آزمایش کردی | دستور یا مراحل |
| اسکرینشات | اگر ظاهر عوض شده |
| اندازه | کوچک: ترجیحاً زیر چند صد خط؛ بزرگ را تکه کن |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) PR بسیار بزرگ
Section titled “۱) PR بسیار بزرگ”هزار خط تغییر را کسی دقیق بازبینی نمیکند («LGTM» میزنند و رد میشوند). راهحل: PR های کوچک و متمرکز (یک ویژگی، یک باگ)؛ تغییرهای فرعی (فرمتدهی، تغییر نام) را در PR جدا بفرست.
۲) کار روی main فورک
Section titled “۲) کار روی main فورک”اگر روی main fork خودت commit بزنی، با upstream جدا میشود و هماهنگکردنش دردسر است. راهحل: برای هر PR یک شاخهی جدید؛ main فقط آینهی upstream باشد (و merge --ff-only همین را تضمین میکند). ببین اگر main تو چیز اضافه داشته باشد و upstream هم جلو رفته باشد چه میشود:
cd ~/gitlab/mineecho "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-maint2git fetch -q upstreamgit merge --ff-only upstream/main 2>&1 | tail -3git reset -q --hard origin/mainhint: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 چه فایلهایی را نشان میداد.
دیدن جواب
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -qecho base > a.txt && git add a.txt && git commit -qm "Base"git switch -qc feature/xecho 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/xecho "--- بدون سهنقطه (تغییر 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) را واقعاً اجرا کردهام.
دیدن جواب
git switch -c feature/helloecho "print('hello')" > hello.py && git add hello.py && git commit -m "Add hello script"git push -u origin feature/hellogh pr create --title "Add hello script" --body "Adds a tiny hello script. Closes #1" --base maingh pr view --web # کامنت بگذار (در مرورگر)gh pr merge --squash --delete-branch # بعد از تأییدمعادل محلی که اجرا کردم:
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2git init -q --bare ../github/ex2.gitgit clone -q ../github/ex2.git work && cd workgit 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 maingit switch -qc feature/helloecho "print('hello')" > hello.py && git add hello.py && git commit -qm "Add hello script"git push -q -u origin feature/helloecho "--- Files changed:"git diff --stat main...feature/helloecho "--- 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/hellogit log --oneline --graphwarning: 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 READMEfork را شبیهسازی کن: یک upstream بساز، آن را به fork کپی کن، از fork یک کلون بگیر، شاخهی مشارکت بساز، push کن؛ بعد upstream یک commit جدید بگیرد و تو با fetch و merge --ff-only همگام شو. ثابت کن بعد از همگامسازی main تو با upstream/main یکی است.
دیدن جواب
cd ~/gitlabgit init -q --bare github/up3.gitgit 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-seed3git clone -q --bare github/up3.git github/fork3.gitgit clone -q github/fork3.git me3 && cd me3git config user.name Ali && git config user.email ali@example.comgit remote add upstream ~/gitlab/github/up3.gitgit switch -qc fix/typo && echo "contribution" > c.txt && git add c.txt && git commit -qm "Add contribution" && git push -q -u origin fix/typoecho "--- 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-m3git switch -q main && git fetch -q upstream && git merge -q --ff-only upstream/maingit push -q origin mainecho "--- یکی هستند؟"git rev-parse main upstream/main--- upstream جلو میرود:--- یکی هستند؟79bc1c3b610b479bc5ea1971654b77e0f8b16dbb79bc1c3b610b479bc5ea1971654b77e0f8b16dbbآزمونک
Section titled “آزمونک”در GitHub برگهی «Files changed» یک PR دقیقاً معادل کدام دستور است؟
سهنقطه فقط تغییرهای خود شاخه را نشان میدهد و تغییرهای بعدی main را قاطی نمیکند؛ همان چیزی که بازبین باید ببیند.
Pull Request چیست؟
Git فقط شاخهها را دارد؛ PR لایهی همکاری GitHub است.
در fork-based workflow کدام remote ها را داری؟
روی origin push میکنی و از upstream تازهها را میگیری.
تفاوت «Squash and merge» و «Create a merge commit»؟
rebase and merge هم commit ها را یکییکی و خطی روی main میگذارد.
بعد از کامنت بازبین، اصلاح را چطور وارد PR میکنی؟
از force-push بعد از شروع بازبینی با احتیاط استفاده کن.
PR میگوید «This branch has conflicts». راهحل؟
همان مهارت حل conflict؛ بعد از push، PR دوباره قابل ادغام میشود.
جمعبندی
Section titled “جمعبندی”- 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/x | commit های 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 |