مقالات › ابزار و DevOps

گردش کار Git در تیم؛ شاخه‌بندی، Commit، تفاوت Merge و Rebase و Pull Request

یک روش کاری عملی با Git برای پروژه‌های واقعی: استراتژی شاخه‌بندی، نوشتن Commit خوب، تفاوت Merge و Rebase، حل Conflict، Pull Request و برگرداندن امن اشتباهات.

همه‌ی توسعه‌دهنده‌ها از Git استفاده می‌کنند، اما خیلی‌ها فقط add، commit و push را بلدند. وقتی تیم بزرگ می‌شود — یا وقتی چیزی خراب می‌شود — به یک گردش کار روشن و چند ابزار بیشتر نیاز دارید. این راهنما یک مدل شاخه‌بندی مناسب اکثر تیم‌ها، نوشتن Commitهای قابل فهم، زمان استفاده از Merge یا Rebase و راه برگرداندن تقریباً هر اشتباهی را پوشش می‌دهد.

Git در یک تصویر

Git از پروژه‌ی شما تصویر لحظه‌ای (Snapshot) ذخیره می‌کند. هر Commit به والدش اشاره می‌کند و یک گراف می‌سازد. شاخه (Branch) فقط یک برچسب متحرک است که به یک Commit اشاره می‌کند و HEAD یعنی «الان کجا هستید».

main:      A ── B ── C ─────────── F (merge)
                      \           /
feature/login:         D ── E ──┘

چون شاخه‌ها برچسب‌های ارزانی هستند، ساختن آن‌ها برای هر کار هزینه‌ای ندارد.

یک استراتژی شاخه‌بندی که کار می‌کند

توسعه‌ی مبتنی بر تنه (Trunk-based) — پیشنهاد برای اکثر تیم‌ها

  • شاخه‌ی main همیشه قابل انتشار است.
  • هر تغییر در یک شاخه‌ی کوتاه‌عمر (چند ساعت تا چند روز) انجام می‌شود.
  • شاخه‌ها از طریق Pull Request همراه با بازبینی و CI ادغام می‌شوند.
  • ویژگی‌های نیمه‌کاره به‌جای شاخه‌های بلندعمر، پشت Feature Flag پنهان می‌شوند.

Git Flow (برای انتشارهای زمان‌بندی‌شده)

Git Flow شاخه‌های develop، release/* و hotfix/* را اضافه می‌کند. برای محصولاتی با نسخه‌های مشخص (نرم‌افزار دسکتاپ، کتابخانه‌ها) مفید است، اما برای اپلیکیشن‌های وبی که مداوم منتشر می‌شوند سنگین است.

نام‌گذاری شاخه‌ها

نام‌های یکدست تاریخچه را خوانا می‌کنند:

feature/article-search
fix/jwt-expiry-check
chore/upgrade-dotnet-9
docs/api-readme

نوشتن Commit خوب

هر Commit باید یک تغییر منطقی باشد، با پیامی که توضیح دهد چرا:

fix(auth): reject expired refresh tokens

Refresh tokens were validated only by signature, so expired tokens
could still issue new access tokens. Check ExpiresAt and revoke the
token family when an expired token is reused.

قالب Conventional Commits (type(scope): summary) تاریخچه را قابل مرور می‌کند و ساخت خودکار Changelog را ممکن می‌سازد:

نوع معنی
feat ویژگی جدید
fix رفع باگ
refactor تغییر کد بدون تغییر رفتار
perf بهبود کارایی
docs، test، chore، ci مستندات، تست، نگهداری، Pipeline

چند نکته:

  • از فعل امری استفاده کنید: add، fix، remove.
  • خلاصه را زیر حدود ۷۰ کاراکتر نگه دارید.
  • با git add -p گزینشی Stage کنید تا Commitها متمرکز بمانند.

نکته: پیام Commit را حتی در تیم‌های فارسی‌زبان معمولاً انگلیسی می‌نویسند تا با ابزارها و Changelogها سازگار باشد؛ مهم این است که در کل تیم یکدست باشد.

گردش کار روزانه

git switch main
git pull --rebase                       # آخرین نسخه‌ی main
git switch -c feature/article-search    # شاخه‌ی جدید

# ...کار...
git add -p
git commit -m "feat(articles): add full-text search endpoint"

git fetch origin
git rebase origin/main                  # کارتان را روی آخرین main دوباره اعمال کنید
git push -u origin feature/article-search
# یک Pull Request باز کنید

Merge یا Rebase؟

هر دو تغییرات یک شاخه را به شاخه‌ی دیگر وارد می‌کنند، اما به دو روش متفاوت.

Merge یک Commit ادغام می‌سازد که دو تاریخچه را به هم وصل می‌کند:

git switch main
git merge feature/article-search
  • ✅ تاریخچه‌ی دقیق حفظ می‌شود.
  • ❌ Commitهای ادغام زیاد، Log را شلوغ می‌کنند.

Rebase Commitهای شما را روی شاخه‌ی دیگر دوباره اجرا می‌کند و تاریخچه‌ی خطی می‌سازد:

git switch feature/article-search
git rebase main
  • ✅ تاریخچه‌ی تمیز و خطی؛ git bisect راحت‌تر.
  • ❌ هش Commitها بازنویسی می‌شود.

هشدار: قانون طلایی: هرگز Commitهایی را که دیگران Pull کرده‌اند Rebase نکنید (شاخه‌های مشترک مثل main). شاخه‌های محلی و Feature خودتان را آزادانه Rebase کنید؛ بعد از Rebase یک شاخه‌ی Push‌شده از git push --force-with-lease استفاده کنید، نه --force ساده.

یک سیاست رایج در تیم‌ها: در طول کار شاخه‌ی Feature را روی main ریبیس کنید و در پایان Pull Request را Squash Merge کنید تا برای هر ویژگی یک Commit تمیز در main بنشیند.

مرتب کردن تاریخچه با Rebase تعاملی

قبل از باز کردن PR، Commitها را مرتب کنید:

git rebase -i origin/main

در ویرایشگر می‌توانید Commitها را pick، reword، squash، fixup یا drop کنید و Commitهای «wip» و «fix typo» را در Commitهای معنادار ادغام کنید.

حل Conflict

Conflict وقتی پیش می‌آید که دو شاخه خطوط یکسانی را تغییر داده باشند:

<<<<<<< HEAD
const pageSize = 12;
=======
const pageSize = 20;
>>>>>>> feature/pagination
  1. کد نهایی درست را تصمیم بگیرید (گاهی ترکیبی از هر دو).
  2. علامت‌ها را پاک کنید، ذخیره کنید و تست‌ها را اجرا کنید.
  3. git add <file> و ادامه: git rebase --continue یا در Merge، git commit.
  4. مطمئن نیستید؟ git rebase --abort یا git merge --abort شما را به حالت امن برمی‌گرداند.

ادغام‌های کوچک و مکرر، Conflictها را کوچک نگه می‌دارند.

برگرداندن اشتباهات

موقعیت دستور
دور ریختن تغییرات یک فایل git restore file.cs
خارج کردن فایل از Stage git restore --staged file.cs
اصلاح پیام یا محتوای آخرین Commit git commit --amend
لغو آخرین Commit با حفظ تغییرات git reset --soft HEAD~1
لغو امن یک Commit Push‌شده git revert <hash>
دور ریختن کامل Commitهای محلی git reset --hard origin/main ⚠️
پیدا کردن Commit «گم‌شده» git reflog
ذخیره‌ی موقت کار git stash / git stash pop

git reflog تور ایمنی شماست: جاهایی که HEAD رفته را ثبت می‌کند، پس حتی Commitهای «حذف‌شده» معمولاً تا مدتی قابل بازیابی‌اند.

بهترین روش‌های Pull Request

  • PRها را کوچک نگه دارید (ترجیحاً زیر حدود ۴۰۰ خط تغییر). PR کوچک سریع‌تر و بهتر بازبینی می‌شود.
  • توضیح بنویسید: چه چیزی تغییر کرد، چرا، چطور تست شود و برای رابط کاربری اسکرین‌شات.
  • CI را طوری تنظیم کنید که برای هر PR بیلد، تست و Linter اجرا شود.
  • از main محافظت کنید: قبل از ادغام، بازبینی و موفقیت بررسی‌ها الزامی باشد.
  • کد را بازبینی کنید، نه آدم را؛ پیشنهاد بدهید، دستور ندهید.

ابزارهای کاربردی دیگر

git log --oneline --graph --decorate --all   # تاریخچه‌ی تصویری
git blame -L 40,60 Program.cs                # چه کسی این خطوط را تغییر داده
git bisect start                             # جستجوی دودویی Commit عامل باگ
git cherry-pick <hash>                       # اعمال یک Commit روی شاخه‌ی دیگر
git tag -a v1.4.0 -m "Release 1.4.0"         # علامت‌گذاری نسخه‌ها

فایل .gitignore برای پروژه‌های ‎.NET و React باید حداقل شامل bin/، obj/، node_modules/، dist/، .env و پوشه‌های IDE باشد. هرگز رمز و کلید را Commit نکنید؛ اگر کردید، فوراً عوضشان کنید. حذف فایل آن را از تاریخچه پاک نمی‌کند.

جمع‌بندی

یک گردش کار خوب با Git ساده است: main را قابل انتشار نگه دارید، در شاخه‌های کوتاه‌عمر کار کنید، Commitهای متمرکز با پیام معنادار بنویسید، شاخه‌های خودتان را Rebase و از طریق Pull Request بازبینی‌شده ادغام کنید. revert، reset، stash و reflog را یاد بگیرید تا تقریباً هیچ اشتباهی غیرقابل جبران نباشد.

پرسش‌های متداول

هنگام ادغام Commitها را Squash کنم؟

برای بیشتر تیم‌های محصول بله؛ یک Commit برای هر ویژگی main را خوانا نگه می‌دارد. کتابخانه‌هایی که تاریخچه‌ی جزئی برایشان مهم است ممکن است Merge معمولی را ترجیح دهند.

تفاوت git fetch و git pull چیست؟

fetch تغییرات ریموت را بدون دست زدن به شاخه‌ی شما دانلود می‌کند. pull یعنی fetch به‌علاوه‌ی یک Merge (یا با --rebase، یک Rebase).

هر چند وقت یک بار Commit کنم؟

هر وقت به یک قدم کوچک و سالم رسیدید. Commitهای مکرر ارزان‌اند و برگشتن به عقب را آسان می‌کنند.