← همه آموزش‌ها

آموزش Git و Branch

از اولین commit تا کار تیمی، حل conflict و انتخاب مدل branch مناسب

بنر آموزش Git: شاخه feature که از main جدا می‌شود و دوباره merge می‌شود، با کارت‌های commit، branch، merge و مدل‌های همکاری.
شاخه را از خط اصلی جدا کنید، کار را تمام کنید و تمیز برگردانید.

در این آموزش چه یاد می‌گیریم؟

با یک پروژه واقعی و دستورهای کوتاه یاد می‌گیریم که Git چطور تغییرات فایل‌ها را نگه می‌دارد، چطور branch می‌سازیم، تغییرات را merge می‌کنیم و در یک تیم بدون خراب کردن کد دیگران کار می‌کنیم.

در پایان نیز مدل‌های GitHub Flow، Git Flow و Trunk-Based Development را مقایسه می‌کنیم تا بدانید برای هر پروژه چه مدلی مناسب‌تر است.

۱. Git چیست؟

Git یک سیستم کنترل نسخه است. یعنی تغییرات فایل‌های پروژه را ثبت می‌کند تا بدانیم چه کسی، چه چیزی و چه زمانی تغییر داده است.

Git را می‌توان شبیه به «ذخیره بازی» در نظر گرفت. هر commit یک نقطه ذخیره است. اگر بعداً اشتباه کردیم، می‌توانیم تاریخچه را ببینیم، تغییر را مقایسه کنیم یا به نسخه سالم برگردیم.

Git با GitHub یکی نیست. Git ابزاری است که روی سیستم شما کار می‌کند. GitHub، GitLab و Azure Repos سرویس‌هایی برای نگه‌داری آنلاین مخزن Git و همکاری تیمی هستند.

Git چه مشکلی را حل می‌کند؟

  • دیگر فایل‌هایی مثل project-final-final-2.zip نمی‌سازیم.
  • تاریخچه کامل و دلیل هر تغییر را داریم.
  • چند نفر می‌توانند هم‌زمان روی بخش‌های مختلف کار کنند.
  • ویژگی جدید را بدون دست زدن به نسخه پایدار توسعه می‌دهیم.
  • در صورت بروز خطا می‌توانیم تغییر مشکل‌دار را پیدا یا لغو کنیم.

۲. چهار بخش مهم Git

مسیر تغییر فایل از Working Directory به Staging، Repository محلی و Remote Repository
فایل را ویرایش می‌کنیم، با git add آماده ثبت می‌کنیم، با git commit در تاریخچه محلی ذخیره و با git push به مخزن آنلاین می‌فرستیم.
بخشمعنی ساده
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ها
committree ریشه، 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_HEADrefهایی که آخرین دستور fetch دریافت کرده است
ORIG_HEADموقعیت قبلی HEAD پیش از بعضی عملیات‌های مهم مثل merge یا reset
MERGE_HEADcommit یا branch در حال merge؛ فقط هنگام merge ناتمام وجود دارد
CHERRY_PICK_HEADcommit در حال cherry-pick هنگام توقف یا conflict
REBASE_HEADcommit فعلی در جریان بعضی مراحل 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
هر commit بهتر است یک هدف مشخص داشته باشد. commit کوچک و مرتبط، review و پیدا کردن خطا را بسیار ساده‌تر می‌کند.

فایل .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 pushcommitهای محلی را به Remote می‌فرستد.

۵. Branch چیست؟

Branch یک مسیر مجزا برای تغییرات است. مثلاً نسخه سالم روی main باقی می‌ماند و شما قابلیت ورود را روی feature/login می‌سازید. تا زمانی که کار کامل نشده، تغییرات branch جدید وارد main نمی‌شوند.

جدا شدن feature/login از main و merge شدن دوباره آن پس از چند commit
Branch از یک commit روی main جدا می‌شود، تغییرات مستقل می‌گیرد و بعد از تکمیل و review دوباره merge می‌شود.
Branch کپی کامل پروژه نیست؛ در Git فقط یک اشاره‌گر سبک به commit است. به همین دلیل ساخت branch تقریباً فوری و کم‌هزینه است.

دستورهای اصلی 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 بسازید

  1. در GitHub یک Pull Request یا در GitLab یک Merge Request باز کنید.
  2. هدف و روش تست تغییر را بنویسید.
  3. CI باید build و test را اجرا کند.
  4. عضو دیگری از تیم کد را review کند.
  5. بعد از تأیید، branch در main merge شود.
برای خودکار کردن build، test و بررسی Pull Request، آموزش کامل CI/CD را نیز ببینید.

مرحله ۶: پاک‌سازی بعد از 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 هستند.

بعد از Squash Merge یا Rebase Merge ممکن است Git با دستور -d، branch محلی را «ادغام‌شده» تشخیص ندهد. فقط پس از اطمینان از merge شدن PR و نبودن commit ارسال‌نشده، می‌توانید آن branch محلی را با git branch -D feature/login حذف کنید.

۷. انواع و نام‌گذاری Branch

یک الگوی نام‌گذاری مشترک باعث می‌شود هدف هر branch بدون باز کردن کد مشخص باشد:

نوعکاربردمثال
feature/قابلیت جدیدfeature/user-login
bugfix/اصلاح باگ عادیbugfix/empty-cart-total
hotfix/اصلاح فوری Productionhotfix/payment-timeout
release/آماده‌سازی یک نسخه در Git Flowrelease/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 و Rebase و نمایش branch فعلی هنگام اجرای هر دستور
در Merge روی main ایستاده‌ایم و feature را وارد آن می‌کنیم؛ در Rebase روی feature ایستاده‌ایم و commitهای آن را بعد از آخرین commit main می‌چینیم.
قانون خیلی ساده:
در 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 باشد
برعکس آن هم ممکن است: اگر می‌خواهید آخرین تغییرات main را با Merge وارد feature کنید، روی feature بایستید و 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 را بازنویسی می‌کند و جهت عملیات برعکس چیزی است که می‌خواهیم.
Rebase شناسه commitها را تغییر می‌دهد. branch عمومی و مشترکی را که دیگران روی آن کار می‌کنند rebase نکنید. اگر branch خودتان را بعد از rebase قبلاً push کرده‌اید، فقط با آگاهی تیم از --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 با Mergemaingit merge feature/login
گرفتن تغییرات main داخل feature با Mergefeature/logingit merge origin/main
چیدن feature روی آخرین main با Rebasefeature/logingit rebase origin/main
Squash کردن feature داخل main به‌صورت محلیmaingit merge --squash feature/login
روشمزیتمناسب برای
Merge commitحفظ تاریخچه کامل branchتغییرات بزرگ یا تیمی
Rebase and mergeتاریخچه خطیتیم آشنا با بازنویسی تاریخچه
Squash and mergeیک commit تمیز برای هر PRGitHub Flow و featureهای کوچک

۹. Merge Conflict چیست و چطور حل می‌شود؟

اگر دو branch یک قسمت یکسان از یک فایل را متفاوت تغییر دهند، Git نمی‌تواند خودش انتخاب کند که کدام درست است و Conflict ایجاد می‌شود.

ایجاد conflict بین دو branch، بررسی تغییرها و ساخت نسخه نهایی حل‌شده
Conflict خطا یا خرابی Git نیست؛ فقط تصمیمی است که باید یک انسان با شناخت منطق برنامه بگیرد.

Git قسمت متعارض را این‌طور علامت می‌زند:

<<<<<<< HEAD
return price * 0.9;
=======
return price - discount;
>>>>>>> feature/discount

برای حل:

  1. فایل را باز و هر دو تغییر را بررسی کنید.
  2. نسخه درست را انتخاب یا ترکیب کنید.
  3. علامت‌های <<<، === و >>> را کامل حذف کنید.
  4. تست‌ها را اجرا کنید؛ حل شدن متن به معنی درست بودن منطق نیست.
  5. فایل را stage و عملیات را ادامه دهید.
git status

# فایل را اصلاح و تست کنید
git add src/PriceService.cs

# اگر در حال merge بودید
git commit

# اگر در حال rebase بودید
git rebase --continue

اگر می‌خواهید عملیات را لغو و به حالت قبل برگردید:

git merge --abort
# یا
git rebase --abort

۱۰. مدل‌های رایج Branching

مقایسه تصویری GitHub Flow، Git Flow و Trunk-Based Development
مدل ساده‌تر branchهای کمتر و کوتاه‌تری دارد؛ مدل پیچیده‌تر نقش‌های بیشتری برای release و hotfix تعریف می‌کند.

GitHub Flow — ساده و مناسب بیشتر تیم‌ها

  1. main همیشه باید سالم و قابل deploy باشد.
  2. برای هر تغییر یک feature branch کوتاه‌عمر بسازید.
  3. Pull Request باز کنید، CI و review انجام شود.
  4. بعد از merge، main را deploy و branch را حذف کنید.

مناسب وب‌اپ‌ها، APIها، تیم‌های کوچک و محصولاتی است که مرتب منتشر می‌شوند. برای شروع، معمولاً بهترین انتخاب است.

Git Flow — ساختارمند ولی سنگین‌تر

  • main: نسخه‌های منتشرشده Production
  • develop: محل جمع شدن تغییرات نسخه بعدی
  • feature/*: قابلیت جدید، معمولاً از develop
  • release/*: تثبیت و آماده‌سازی نسخه
  • 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 محیط یا نسخه
مدل پیچیده‌تر لزوماً حرفه‌ای‌تر نیست. مدلی را انتخاب کنید که کمترین branch لازم را برای نیاز واقعی انتشار شما داشته باشد.

۱۱. روال پیشنهادی کار تیمی

برای بیشتر تیم‌ها این نسخه ساده GitHub Flow کافی است:

۱. یک Issue یا Task کوچک و روشن انتخاب کنید.
↓
۲. main را pull و یک feature branch کوتاه‌عمر بسازید.
↓
۳. commitهای کوچک، مرتبط و قابل‌فهم ایجاد کنید.
↓
۴. branch را push و Pull Request کوچک باز کنید.
↓
۵. CI، تست و Code Review انجام شود.
↓
۶. Squash Merge یا روش توافق‌شده تیم اجرا شود.
↓
۷. branch حذف و نسخه merge‌شده deploy شود.

یک Pull Request خوب چه دارد؟

  • عنوان روشن: «Add password reset endpoint» بهتر از «Update code» است.
  • توضیح مسئله و دلیل راه‌حل
  • روش تست و نتیجه تست‌ها
  • تصویر برای تغییرات رابط کاربری
  • لینک Issue یا Task مرتبط
  • تغییرات کوچک و متمرکز؛ refactor نامرتبط را جدا کنید.

۱۲. برگرداندن تغییرات بدون ترس

نیازدستور امن‌تر
لغو تغییر ذخیره‌نشده یک فایلgit restore file.txt
خارج کردن فایل از Staginggit 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 / BranchStagingفایل‌ها
--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
اگر reset یا rebase اشتباه انجام دادید، فوراً دستورهای بیشتری اجرا نکنید. ابتدا 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
Cherry-pick یک commit جدید با SHA جدید می‌سازد. برای انتقال تعداد زیادی commit مرتبط، merge یا rebase معمولاً واضح‌تر است.

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
ابتدا همیشه حالت dry-run یعنی -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-forwardRemote 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 بدون ترس از خراب شدن کد واقعی است.