آموزش Git و Branch
از اولین commit تا کار تیمی، حل conflict و انتخاب مدل branch مناسب
در این آموزش چه یاد میگیریم؟
با یک پروژه واقعی و دستورهای کوتاه یاد میگیریم که Git چطور تغییرات فایلها را نگه میدارد، چطور branch میسازیم، تغییرات را merge میکنیم و در یک تیم بدون خراب کردن کد دیگران کار میکنیم.
در پایان نیز مدلهای GitHub Flow، Git Flow و Trunk-Based Development را مقایسه میکنیم تا بدانید برای هر پروژه چه مدلی مناسبتر است.
۱. Git چیست؟
Git یک سیستم کنترل نسخه است. یعنی تغییرات فایلهای پروژه را ثبت میکند تا بدانیم چه کسی، چه چیزی و چه زمانی تغییر داده است.
Git را میتوان شبیه به «ذخیره بازی» در نظر گرفت. هر commit یک نقطه ذخیره است. اگر بعداً اشتباه کردیم، میتوانیم تاریخچه را ببینیم، تغییر را مقایسه کنیم یا به نسخه سالم برگردیم.
Git چه مشکلی را حل میکند؟
- دیگر فایلهایی مثل
project-final-final-2.zipنمیسازیم. - تاریخچه کامل و دلیل هر تغییر را داریم.
- چند نفر میتوانند همزمان روی بخشهای مختلف کار کنند.
- ویژگی جدید را بدون دست زدن به نسخه پایدار توسعه میدهیم.
- در صورت بروز خطا میتوانیم تغییر مشکلدار را پیدا یا لغو کنیم.
۲. چهار بخش مهم Git
| بخش | معنی ساده |
|---|---|
| Working Directory | فایلهایی که اکنون روی سیستم میبینید و ویرایش میکنید. |
| Staging Area | تغییراتی که برای commit بعدی انتخاب کردهاید. |
| Local Repository | تاریخچه commitهای ذخیرهشده روی سیستم شما. |
| Remote Repository | نسخه آنلاین مخزن روی GitHub، GitLab یا سرویس مشابه. |
چهار دستور اصلی این مسیر:
git status # وضعیت فایلها
git add Program.cs # بردن فایل به Staging
git commit -m "Add health endpoint"
git push # ارسال commitها به Remote
داخل پوشه .git چه چیزی نگهداری میشود؟
بعد از اجرای git init، یک پوشه مخفی به نام .git ساخته میشود. این پوشه خودِ Repository است: تاریخچه commitها، branchها، tagها، Staging Area و تنظیمات محلی داخل آن قرار دارند. فایلهای قابل مشاهده پروژه فقط Working Directory هستند.
در Repository معمولی .git یک پوشه است؛ در linked worktree یا بعضی submoduleها ممکن است یک فایل متنی باشد که مسیر Git Directory واقعی را نشان میدهد. دستور git rev-parse --git-dir همیشه مسیر درست را اعلام میکند.
.git/
├── HEAD
├── config
├── index
├── objects/
├── refs/
│ ├── heads/
│ ├── remotes/
│ └── tags/
├── logs/
├── hooks/
├── FETCH_HEAD
├── ORIG_HEAD
└── packed-refs
.git را دستی ویرایش یا حذف نکنید. بیشتر آنها فرمت داخلی یا باینری دارند. برای مشاهده و تغییر اطلاعات از دستورهای خود Git استفاده کنید.
HEAD — اکنون کجا هستیم؟
HEAD معمولاً به branch فعلی اشاره میکند. اگر روی main باشیم، محتوای مفهومی آن چنین است:
ref: refs/heads/main
در حالت Detached HEAD، این فایل مستقیماً شناسه یک commit را نگه میدارد. برای دیدن وضعیت، فایل را دستی نخوانید؛ این دستورها واضحترند:
git branch --show-current
git rev-parse HEAD
git status
config — تنظیمات همین Repository
آدرس remoteها، تنظیم tracking branch و گزینههای محلی در .git/config قرار میگیرند. تنظیمات این فایل فقط روی همین پروژه اثر دارند:
git config --local --list
git remote -v
git config --show-origin --get user.email
index — همان Staging Area
فایل .git/index نمای لحظهای فایلهایی است که برای commit بعدی آماده کردهاید. این فایل باینری است و نباید با ویرایشگر باز شود:
git status
git diff --staged
git ls-files --stage
objects — دیتابیس واقعی Git
Git داده را بهشکل objectهای محتوامحور نگه میدارد:
| Object | چه چیزی را نگه میدارد؟ |
|---|---|
| blob | محتوای یک فایل، بدون نام فایل |
| tree | نام فایلها، پوشهها و ارتباط آنها با blobها |
| commit | tree ریشه، parent، نویسنده، زمان و پیام commit |
| tag | اطلاعات Annotated Tag و اشاره به object دیگر |
شناسه commit یک نام تصادفی نیست؛ از محتوای object ساخته میشود. تغییر محتوا، شناسه جدید تولید میکند. برای بررسی امن objectها:
git cat-file -t HEAD # نوع object
git cat-file -p HEAD # محتوای قابل خواندن commit
git ls-tree HEAD # فایلهای snapshot
git show HEAD:README.md # نسخه README در commit فعلی
refs — Branchها و Tagها
Branch در اصل یک فایل کوچک زیر refs/heads است که به آخرین commit آن branch اشاره میکند. Remote-tracking branchها زیر refs/remotes و tagها زیر refs/tags هستند.
git show-ref
git show-ref --heads
git show-ref --tags
git rev-parse refs/remotes/origin/main
Git ممکن است refهای قدیمی را برای فشردهسازی داخل packed-refs قرار دهد؛ پس برای یافتن branchها فقط به پوشه refs نگاه نکنید و از git show-ref استفاده کنید.
logs — پایه Reflog
.git/logs جابهجایی محلی HEAD و branchها را ثبت میکند. دستور git reflog از همین اطلاعات برای پیدا کردن commit گمشده بعد از reset یا rebase استفاده میکند.
فایلهای موقت عملیات
| فایل | معنی |
|---|---|
FETCH_HEAD | refهایی که آخرین دستور fetch دریافت کرده است |
ORIG_HEAD | موقعیت قبلی HEAD پیش از بعضی عملیاتهای مهم مثل merge یا reset |
MERGE_HEAD | commit یا branch در حال merge؛ فقط هنگام merge ناتمام وجود دارد |
CHERRY_PICK_HEAD | commit در حال cherry-pick هنگام توقف یا conflict |
REBASE_HEAD | commit فعلی در جریان بعضی مراحل rebase |
hooks — اجرای اسکریپت در رویدادهای Git
پوشه .git/hooks نمونه hookهایی مثل pre-commit و pre-push دارد. Hook میتواند lint یا تست اجرا کند، اما hook محلی معمولاً با clone بین اعضای تیم توزیع نمیشود؛ برای قانون اجباری به CI و Branch Protection تکیه کنید.
مسیر امن برای بررسی Repository
git rev-parse --show-toplevel # ریشه Working Tree
git rev-parse --git-dir # مسیر پوشه داخلی Git
git count-objects -vH # اندازه objectها
git fsck # بررسی ارتباط و سلامت objectها
۳. ساخت اولین Repository
نصب و تنظیم اولیه
بعد از نصب Git، نام و ایمیل خود را یک بار تنظیم کنید. این اطلاعات روی commitها ثبت میشود:
git --version
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
ساخت یک پروژه نمونه
mkdir hello-git
cd hello-git
git init
echo "# Hello Git" > README.md
git status
دستور git init پوشه فعلی را به یک Git Repository تبدیل میکند. پوشه مخفی .git تاریخچه و تنظیمات مخزن را نگه میدارد.
اولین Commit
git add README.md
git commit -m "Add initial README"
برای دیدن تاریخچه:
git log
git log --oneline --graph --decorate --all
چرا اول Stage میکنیم؟
فرض کنید همزمان فایل ورود و صفحه تماس را تغییر دادهاید، اما فقط تغییر ورود آماده است. با Staging فقط فایلهای مرتبط را داخل یک commit تمیز قرار میدهید:
git add src/Login.cs
git commit -m "Validate login credentials"
# تغییر صفحه تماس هنوز commit نشده است
git status
فایل .gitignore
فایلهای تولیدشده، موقت و محرمانه نباید وارد Git شوند. نمونه ساده برای .NET:
bin/
obj/
.vs/
.idea/
*.user
TestResults/
.env
appsettings.Production.json
.gitignore فایلی را که قبلاً commit شده حذف نمیکند. مهمتر از آن، Secret افشاشده با حذف commit امن نمیشود؛ باید آن Secret را فوراً rotate کنید.
۴. اتصال به GitHub یا GitLab
بعد از ساخت Repository خالی در سرویس موردنظر، آدرس آن را به مخزن محلی اضافه کنید:
git remote add origin git@github.com:USERNAME/hello-git.git
git remote -v
git push -u origin main
origin فقط نام رایج Remote اصلی است. گزینه -u شاخه محلی main را به شاخه متناظر روی origin متصل میکند؛ بعد از آن معمولاً git push کافی است.
Clone کردن پروژه موجود
git clone git@github.com:USERNAME/hello-git.git
cd hello-git
تفاوت دستورهای دریافت تغییرات:
| دستور | کار |
|---|---|
git fetch | تغییرات Remote را دریافت میکند ولی branch فعلی را تغییر نمیدهد. |
git pull | معمولاً fetch و سپس merge یا rebase را انجام میدهد. |
git push | commitهای محلی را به Remote میفرستد. |
۵. Branch چیست؟
Branch یک مسیر مجزا برای تغییرات است. مثلاً نسخه سالم روی main باقی میماند و شما قابلیت ورود را روی feature/login میسازید. تا زمانی که کار کامل نشده، تغییرات branch جدید وارد main نمیشوند.
دستورهای اصلی Branch
git branch # فهرست branchهای محلی
git branch --all # محلی و remote
git switch -c feature/login # ساخت و ورود به branch جدید
git switch main # برگشت به main
git branch -d feature/login # حذف branch ادغامشده
دستور قدیمی git checkout -b feature/login نیز همان branch را میسازد، اما git switch برای تازهکارها واضحتر است.
۶. مثال کامل: ساخت قابلیت ورود
مرحله ۱: main را بهروز کنید
git switch main
git pull --ff-only origin main
گزینه --ff-only اجازه نمیدهد که pull بهطور ناخواسته یک merge commit بسازد. اگر تاریخچه محلی و remote جدا شده باشند، دستور متوقف میشود تا آگاهانه تصمیم بگیرید.
مرحله ۲: Branch بسازید
git switch -c feature/login
مرحله ۳: تغییرات کوچک و مرتبط را Commit کنید
# فایلها را ویرایش کنید
git status
git diff
git add src/LoginService.cs tests/LoginServiceTests.cs
git diff --staged
git commit -m "Add credential validation"
git diff تغییرات stageنشده و git diff --staged دقیقاً محتوای commit بعدی را نشان میدهد.
مرحله ۴: Branch را Push کنید
git push -u origin feature/login
مرحله ۵: Pull Request یا Merge Request بسازید
- در GitHub یک Pull Request یا در GitLab یک Merge Request باز کنید.
- هدف و روش تست تغییر را بنویسید.
- CI باید build و test را اجرا کند.
- عضو دیگری از تیم کد را review کند.
- بعد از تأیید، branch در
mainmerge شود.
مرحله ۶: پاکسازی بعد از Merge
git switch main
git pull --ff-only origin main
git branch -d feature/login
git push origin --delete feature/login
حذف branch بعد از merge تاریخچه commitها را پاک نمیکند؛ commitهای ادغامشده همچنان داخل main هستند.
-d، branch محلی را «ادغامشده» تشخیص ندهد. فقط پس از اطمینان از merge شدن PR و نبودن commit ارسالنشده، میتوانید آن branch محلی را با git branch -D feature/login حذف کنید.
۷. انواع و نامگذاری Branch
یک الگوی نامگذاری مشترک باعث میشود هدف هر branch بدون باز کردن کد مشخص باشد:
| نوع | کاربرد | مثال |
|---|---|---|
feature/ | قابلیت جدید | feature/user-login |
bugfix/ | اصلاح باگ عادی | bugfix/empty-cart-total |
hotfix/ | اصلاح فوری Production | hotfix/payment-timeout |
release/ | آمادهسازی یک نسخه در Git Flow | release/2.4.0 |
chore/ | کار فنی بدون قابلیت جدید | chore/update-dependencies |
docs/ | تغییر مستندات | docs/authentication-guide |
refactor/ | بهبود ساختار بدون تغییر رفتار | refactor/order-service |
spike/ | تحقیق یا آزمایش موقت | spike/redis-cache |
قواعد پیشنهادی:
- از حروف کوچک و خط تیره استفاده کنید.
- نام کوتاه ولی توصیفی باشد؛
fixیاmy-branchمبهم است. - در صورت استفاده از ابزار مدیریت کار، شناسه Task را اضافه کنید:
feature/PROJ-142-user-login. - branch را کوتاهعمر نگه دارید؛ branch چندماهه merge و review سختی دارد.
۸. Merge، Rebase و Squash چه فرقی دارند؟
در Merge روی branch مقصد میایستیم؛ یعنی branchی که قرار است تغییرات را دریافت کند.
در Rebase روی branch کاری یا feature میایستیم؛ یعنی branchی که قرار است روی آخرین نسخه main دوباره چیده شود.
Merge
Merge تاریخچه دو branch را به هم متصل میکند و معمولاً تاریخ واقعی انشعاب را حفظ میکند. دستور git merge X یعنی: «تغییرات branch به نام X را وارد branch فعلی کن».
پس اگر میخواهیم feature/login را وارد main کنیم، باید اول روی main باشیم:
git switch main
git pull --ff-only origin main
git merge feature/login
برای مطمئن شدن از branch فعلی:
git branch --show-current
# خروجی باید main باشد
git merge main بزنید. همیشه branch فعلی دریافتکننده است.
# دریافت تغییرات main داخل feature
git switch feature/login
git fetch origin
git merge origin/main
Rebase
Rebase commitهای branch کاری شما را برمیدارد و بعد از آخرین commit branch پایه دوباره میچیند. نتیجه، تاریخچهای خطی و تمیز است.
اگر میخواهیم feature/login را با آخرین نسخه main بهروز کنیم، باید روی feature/login باشیم:
git switch feature/login
git fetch origin
git rebase origin/main
git branch --show-current
# خروجی همچنان feature/login است
معنی دستور آخر این است: «commitهای feature/login را موقتاً بردار، آن را به انتهای origin/main ببر و commitهای feature را دوباره روی آن قرار بده».
main نایستید و git rebase feature/login نزنید؛ این کار branch main را بازنویسی میکند و جهت عملیات برعکس چیزی است که میخواهیم.
--force-with-lease استفاده کنید؛ نه --force.
Squash Merge
همه commitهای یک Pull Request را به یک commit روی main تبدیل میکند. برای branchهای کوچک و commitهای میانی نامرتب مناسب است.
اگر آن را محلی انجام دهید، مانند Merge روی branch مقصد یعنی main میایستید:
git switch main
git merge --squash feature/login
git commit -m "Add user login"
اگر از دکمه Squash and merge در GitHub یا GitLab استفاده کنید، سرویس این کار را روی branch مقصد Pull Request انجام میدهد و لازم نیست قبل از کلیک، روی سیستم خود branch خاصی باز باشد.
خلاصه: روی کدام Branch باشیم؟
| کاری که میخواهیم | Branch فعلی | دستور |
|---|---|---|
| وارد کردن feature به main با Merge | main | git merge feature/login |
| گرفتن تغییرات main داخل feature با Merge | feature/login | git merge origin/main |
| چیدن feature روی آخرین main با Rebase | feature/login | git rebase origin/main |
| Squash کردن feature داخل main بهصورت محلی | main | git merge --squash feature/login |
| روش | مزیت | مناسب برای |
|---|---|---|
| Merge commit | حفظ تاریخچه کامل branch | تغییرات بزرگ یا تیمی |
| Rebase and merge | تاریخچه خطی | تیم آشنا با بازنویسی تاریخچه |
| Squash and merge | یک commit تمیز برای هر PR | GitHub Flow و featureهای کوچک |
۹. Merge Conflict چیست و چطور حل میشود؟
اگر دو branch یک قسمت یکسان از یک فایل را متفاوت تغییر دهند، Git نمیتواند خودش انتخاب کند که کدام درست است و Conflict ایجاد میشود.
Git قسمت متعارض را اینطور علامت میزند:
<<<<<<< HEAD
return price * 0.9;
=======
return price - discount;
>>>>>>> feature/discount
برای حل:
- فایل را باز و هر دو تغییر را بررسی کنید.
- نسخه درست را انتخاب یا ترکیب کنید.
- علامتهای
<<<،===و>>>را کامل حذف کنید. - تستها را اجرا کنید؛ حل شدن متن به معنی درست بودن منطق نیست.
- فایل را stage و عملیات را ادامه دهید.
git status
# فایل را اصلاح و تست کنید
git add src/PriceService.cs
# اگر در حال merge بودید
git commit
# اگر در حال rebase بودید
git rebase --continue
اگر میخواهید عملیات را لغو و به حالت قبل برگردید:
git merge --abort
# یا
git rebase --abort
۱۰. مدلهای رایج Branching
GitHub Flow — ساده و مناسب بیشتر تیمها
mainهمیشه باید سالم و قابل deploy باشد.- برای هر تغییر یک feature branch کوتاهعمر بسازید.
- Pull Request باز کنید، CI و review انجام شود.
- بعد از merge، main را deploy و branch را حذف کنید.
مناسب وباپها، APIها، تیمهای کوچک و محصولاتی است که مرتب منتشر میشوند. برای شروع، معمولاً بهترین انتخاب است.
Git Flow — ساختارمند ولی سنگینتر
main: نسخههای منتشرشده Productiondevelop: محل جمع شدن تغییرات نسخه بعدیfeature/*: قابلیت جدید، معمولاً از developrelease/*: تثبیت و آمادهسازی نسخهhotfix/*: اصلاح فوری از main
برای محصولاتی با نسخهبندی رسمی، چند نسخه پشتیبانیشده و انتشارهای دورهای مفید است. برای تیمی که روزانه deploy میکند، Git Flow معمولاً branch و merge اضافی ایجاد میکند.
Trunk-Based Development — ادغام بسیار مداوم
توسعهدهندگان تغییرات کوچک را مستقیماً یا از branchهای بسیار کوتاهعمر وارد یک شاخه اصلی به نام trunk/main میکنند. قابلیت ناقص با Feature Flag مخفی میشود.
- branchها معمولاً چند ساعت تا حداکثر یکی دو روز عمر دارند.
- CI سریع و تست خودکار قوی ضروری است.
- mergeهای کوچک، conflict و فاصله بین branchها را کم میکنند.
- مناسب تیمهای بالغ با Continuous Delivery است.
GitLab Flow و Release Branch
GitLab Flow یک نسخه انعطافپذیر است که feature branch و Merge Request را با branchهای محیط یا release ترکیب میکند. مثلاً main مسیر توسعه باشد و branchهای production یا release/2.x نسخههای منتشرشده را نگه دارند.
| مدل | پیچیدگی | زمان انتشار | انتخاب مناسب |
|---|---|---|---|
| GitHub Flow | کم | مداوم | بیشتر وباپها و تیمها |
| Git Flow | زیاد | دورهای و نسخهبندیشده | نرمافزار سازمانی یا چند نسخه فعال |
| Trunk-Based | کم (اما نیاز فنی بالا) | بسیار مداوم | تیم بالغ با CI قوی و Feature Flag |
| GitLab Flow | متوسط | وابسته به محیط یا release | تیم نیازمند branch محیط یا نسخه |
۱۱. روال پیشنهادی کار تیمی
برای بیشتر تیمها این نسخه ساده GitHub Flow کافی است:
یک Pull Request خوب چه دارد؟
- عنوان روشن: «Add password reset endpoint» بهتر از «Update code» است.
- توضیح مسئله و دلیل راهحل
- روش تست و نتیجه تستها
- تصویر برای تغییرات رابط کاربری
- لینک Issue یا Task مرتبط
- تغییرات کوچک و متمرکز؛ refactor نامرتبط را جدا کنید.
۱۲. برگرداندن تغییرات بدون ترس
| نیاز | دستور امنتر |
|---|---|
| لغو تغییر ذخیرهنشده یک فایل | git restore file.txt |
| خارج کردن فایل از Staging | git restore --staged file.txt |
| اصلاح پیام آخرین commit محلی | git commit --amend |
| لغو یک commit منتشرشده | git revert COMMIT_SHA |
| نگهداری موقت تغییرات | git stash push -m "work in progress" |
# دیدن تغییر قبل از لغو
git show COMMIT_SHA
# ساخت یک commit جدید که اثر commit قبلی را برعکس میکند
git revert COMMIT_SHA
git reset --hard و force push میتوانند تغییرات را از دسترس خارج کنند یا تاریخچه تیم را خراب کنند. تا وقتی دقیقاً اثرشان را نمیدانید، برای commit منتشرشده از git revert استفاده کنید.
دستورهای حرفهایتر Git
حرفهای بودن در Git یعنی قبل از هر دستور بدانیم کدامیک از سه بخش HEAD، Staging Area و Working Directory تغییر میکند. دستورهای این بخش قدرتمندند؛ ابتدا روی یک مخزن آزمایشی تمرین کنید.
Fetch و FETCH_HEAD
git fetch اطلاعات Remote را دریافت میکند، اما فایلها و branch فعلی را تغییر نمیدهد. این ویژگی فرصت میدهد تغییرات را قبل از merge بررسی کنیم:
git fetch --prune origin
# commitهای جدید main روی remote
git log --oneline HEAD..origin/main
# تغییر branch شما از نقطه جدا شدن با main
git diff --stat origin/main...HEAD
گزینه --prune remote-tracking branchهایی را که روی سرور حذف شدهاند پاک میکند؛ branch محلی شما را حذف نمیکند.
FETCH_HEAD یک ref موقت به نتیجه آخرین fetch است. در این مثال فقط branch اصلی را دریافت و قبل از ادغام بررسی میکنیم:
git switch main
git fetch origin main
git show --stat FETCH_HEAD
git log --oneline HEAD..FETCH_HEAD
git merge --ff-only FETCH_HEAD
FETCH_HEAD با fetch بعدی تغییر میکند و اگر چند ref دریافت شوند میتواند چند ورودی داشته باشد. برای اسکریپتهای بلندمدت، ref مشخصی مثل origin/main یا commit SHA واضحتر است.
Reset دقیقاً چه چیزی را تغییر میدهد؟
| حالت | HEAD / Branch | Staging | فایلها |
|---|---|---|---|
--soft | جابهجا میشود | دستنخورده | دستنخورده |
--mixed | جابهجا میشود | با commit مقصد هماهنگ | دستنخورده |
--hard | جابهجا میشود | با مقصد هماهنگ | با مقصد جایگزین میشود |
# حذف commit آخر، ولی نگه داشتن تغییرات در Staging
git reset --soft HEAD~1
# حذف commit آخر و برگرداندن تغییرات به حالت stageنشده
git reset --mixed HEAD~1
# --mixed حالت پیشفرض است
git reset HEAD~1
reset برای اصلاح تاریخچه محلی مفید است. برای commitی که دیگران دریافت کردهاند معمولاً revert انتخاب امنتری است، چون تاریخچه را بازنویسی نمیکند.
reset --hard FETCH_HEAD چه میکند؟
این دستور branch فعلی، Staging Area و فایلهای trackشده را دقیقاً روی commit دریافتشده قرار میدهد. اگر روی main باشید، تغییر محلی commitنشده و commitهای محلی متفاوت کنار گذاشته میشوند:
git switch main
git fetch origin main
# اول اختلاف را بررسی کنید
git status
git log --oneline --left-right HEAD...FETCH_HEAD
git diff --stat HEAD FETCH_HEAD
# یک مسیر نجات بسازید
git stash push --include-untracked -m "before hard sync"
git branch backup/main-before-reset
# فقط وقتی واقعاً میخواهید نسخه محلی عین remote شود
git reset --hard FETCH_HEAD
git reset --hard FETCH_HEAD یک دستور عادی برای pull کردن نیست. برای بهروزرسانی روزمره از git pull --ff-only یا git merge --ff-only FETCH_HEAD استفاده کنید. reset سخت فقط وقتی مناسب است که آگاهانه میخواهید تغییرات محلی را کنار بگذارید.
ORIG_HEAD؛ یک راه برگشت سریع
Git پیش از بعضی عملیاتهای مهم، موقعیت قبلی را در ORIG_HEAD نگه میدارد:
git show --oneline ORIG_HEAD
# فقط بعد از بررسی status و اطمینان از مقصد
git reset --hard ORIG_HEAD
ORIG_HEAD همیشگی نیست و عملیات بعدی میتواند آن را تغییر دهد. برای بازیابی مطمئنتر از Reflog استفاده کنید.
Reflog؛ پیدا کردن Commit گمشده
Reflog جابهجاییهای محلی HEAD را ثبت میکند، حتی اگر commit دیگر از branch قابل مشاهده نباشد:
git reflog --date=local
# ابتدا commit پیداشده را بررسی کنید
git show abc1234
# امنترین کار: ساخت branch نجات
git switch -c recovery/lost-work abc1234
git reflog را ببینید و از commit سالم یک branch نجات بسازید.
Restore؛ برگرداندن فایل از یک منبع مشخص
# برگرداندن یک فایل به نسخه commit فعلی
git restore src/App.cs
# خارج کردن فایل از Staging، بدون حذف تغییر فایل
git restore --staged src/App.cs
# گرفتن نسخه فایل از main
git restore --source=origin/main -- src/App.cs
# هم Staging و هم فایل را با HEAD هماهنگ میکند
git restore --source=HEAD --staged --worktree src/App.cs
git restore روی Working Directory میتواند تغییر ذخیرهنشده را حذف کند. همیشه قبل از آن git diff را ببینید.
Stash حرفهایتر
# فایلهای trackشده و untracked را موقتاً ذخیره کن
git stash push --include-untracked -m "login work in progress"
git stash list
git stash show --patch stash@{0}
# apply نگه میدارد؛ pop بعد از اعمال موفق حذف میکند
git stash apply stash@{0}
# ساخت branch از نقطهای که stash ایجاد شده بود
git stash branch recovery/login stash@{0}
Stash جای commit طولانیمدت نیست. برای کار مهم، یک branch و commit موقت محلی قابل ردیابیتر است.
Cherry-pick؛ آوردن یک Commit مشخص
وقتی فقط یک اصلاح مشخص را از branch دیگر لازم داریم:
git switch release/2.0
git cherry-pick abc1234
# در صورت conflict
git add .
git cherry-pick --continue
# لغو کامل عملیات
git cherry-pick --abort
Bisect؛ پیدا کردن Commit خراب با جستوجوی دودویی
اگر میدانیم نسخه فعلی خراب و یک نسخه قدیمی سالم بوده، Git میتواند با کمترین تست commit مشکلدار را پیدا کند:
git bisect start
git bisect bad HEAD
git bisect good v2.0.0
# Git یک commit میانی را checkout میکند.
# بعد از تست، یکی از این دو را تکرار کنید:
git bisect good
git bisect bad
# پس از پیدا شدن commit مشکلدار
git bisect reset
اگر تست خودکار دارید:
git bisect run dotnet test
خروجی صفر یعنی commit سالم، ۱ تا ۱۲۷ بهجز ۱۲۵ یعنی خراب، و ۱۲۵ یعنی این commit قابل تست نیست.
Worktree؛ چند Branch همزمان بدون Stash
Worktree یک Working Directory دیگر برای همان Repository میسازد. مثلاً هنگام کار روی feature، یک hotfix فوری را در پوشه جدا باز میکنید:
git worktree add ../shop-hotfix -b hotfix/payment main
# بعد از پایان کار و خروج از آن پوشه
git worktree remove ../shop-hotfix
git worktree list
Clean؛ حذف فایلهای Untracked با پیشنمایش
# فقط نمایش میدهد چه چیزی حذف خواهد شد
git clean -nd
# فایلها و پوشههایی را که Git نادیده میگیرد هم در پیشنمایش بیاور
git clean -ndx
-n را اجرا کنید. گزینه -x فایلهایی را که Git نادیده میگیرد — مثل خروجی build یا حتی فایل محلی حساس — هم انتخاب میکند.
دستورهای مفید برای بررسی تاریخچه
# تاریخچه گرافی همه branchها
git log --graph --oneline --decorate --all
# commitهایی که feature دارد ولی main ندارد
git log --oneline main..feature/login
# تغییرات feature از نقطه جدا شدن با main
git diff main...feature/login
# چه commitی یک خط را تغییر داده؟
git blame -L 20,35 src/PaymentService.cs
# جستوجوی commit بر اساس متن پیام
git log --grep="payment" --oneline
# جستوجوی commitی که تعداد occurrence یک متن را تغییر داده
git log -S "CalculateFee" --oneline --all
۱۳. نکات مهم و خطاهای رایج
- قبل از commit، همیشه
git statusوgit diff --stagedرا ببینید. - فایلهای تولیدشده، Secretها و تنظیمات شخصی را commit نکنید.
- روی
mainمستقیم کار نکنید؛ از Pull Request و Branch Protection استفاده کنید. - روزها منتظر نمانید؛ branch کوچک را زود push و PR را بهصورت Draft باز کنید.
- مرتب main را دریافت کنید تا conflict بزرگ نشود.
- یک commit نباید همزمان feature، refactor و تغییر formatting نامرتبط داشته باشد.
- پیام commit باید فعل و هدف روشنی داشته باشد:
Fix duplicate payment submission. - قبل از merge، تستها و CI باید سبز باشند.
- دستور ناشناخته Git را فقط از روی اینترنت کپی نکنید؛ ابتدا اثر آن را با
git statusو مستندات بررسی کنید.
چند خطای رایج
| پیام یا مشکل | معنی و راهحل |
|---|---|
nothing to commit | تغییر جدیدی برای commit وجود ندارد یا فایلها در .gitignore هستند. |
non-fast-forward | Remote commit جدید دارد؛ ابتدا fetch/pull کنید و اختلاف را حل کنید. |
detached HEAD | روی یک commit هستید، نه branch؛ برای نگهداری تغییر git switch -c new-branch بزنید. |
| فایل بعد از افزودن به .gitignore هنوز دیده میشود | فایل قبلاً track شده است؛ باید آگاهانه آن را از index خارج کنید. |
| Conflict زیاد و سخت | branch بیش از حد بزرگ یا قدیمی است؛ تغییرات را کوچکتر و زودتر merge کنید. |
۱۴. جمعبندی
- Git تاریخچه تغییرات را با commitها نگه میدارد.
addتغییر را stage،commitآن را محلی ثبت وpushآن را به Remote ارسال میکند.- Branch مسیر سبک و جداگانهای برای توسعه امن یک تغییر است.
- برای هر feature یا bug یک branch کوتاهعمر و نامدار بسازید.
- Pull Request محل CI، گفتوگو، review و تصمیم merge است.
- برای بیشتر تیمها GitHub Flow کافی است؛ Git Flow برای releaseهای پیچیده و Trunk-Based برای ادغام بسیار مداوم مناسب است.
- commit کوچک، branch کوتاه و تست خودکار، conflict و ریسک انتشار را کم میکند.
بهترین راه یادگیری Git ساختن یک پروژه آزمایشی و تمرین branch، merge و conflict بدون ترس از خراب شدن کد واقعی است.