توی این درس یاد میگیری وقتی اشتباه بزرگ کردی، قبل از ناامیدی ابزار نجات را بشناسی: git reflog دفترچهی تمام جابهجاییهای HEAD است و با آن یک شاخهی پاکشده، یک reset --hard و یک rebase اشتباه را برمیگردانی؛ git fsck commit های «بیصاحب» را پیدا میکند؛ git cherry-pick فقط یک commit را از شاخهای دیگر برمیدارد؛ و git bisect با جستوجوی دودویی بین صدها commit، اولین commit خراب را در چند قدم (و حتی خودکار) پیدا میکند. تمرین: یک branch پاکشده را با reflog برگردان.
مسئله: «همهچیز را پاک کردم!»
Section titled “مسئله: «همهچیز را پاک کردم!»”اشتباهها پیش میآیند: git branch -D روی شاخهای که ادغام نشده بود، reset --hard با هش اشتباه، rebase ای که وسطش خراب شد. خبر خوب: Git تقریباً هیچچیز را فوراً پاک نمیکند. commit ها در .git/objects میمانند و فقط برچسبهایی که به آنها اشاره میکرد گم میشود. خبر بد: بدون برچسب، پیدا کردنشان با چشم ممکن نیست. reflog همان برچسبهای گمشده را یادآوری میکند. و مشکل مقابل: یک باگ در بین ۲۰۰ commit که کسی نمیداند از کجا آمده؛ bisect آن را پیدا میکند.
تشبیه: دوربین مداربستهی HEAD
Section titled “تشبیه: دوربین مداربستهی HEAD”git log تاریخچهی پروژه است (commit ها). git reflog تاریخچهی حرکتهای تو است: «ساعت ۱۰ به شاخهی x رفتم، ساعت ۱۰:۰۵ commit زدم، ۱۰:۱۰ reset کردم…». حتی وقتی commit ها از git log ناپدید شده باشند، حرکتها در این دفترچه هست. و bisect مثل پیدا کردن اسم در دفترچهی تلفن است: وسط را باز میکنی، میبینی اسم قبل یا بعد است، و نصف بخش را کنار میگذاری؛ ۲۰۰ commit در حدود ۸ قدم.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: خواندن reflog
Section titled “مثال ۱: خواندن reflog”هر بار HEAD جابهجا میشود (commit، checkout، merge، rebase، reset…) یک خط در reflog ثبت میشود. چند کار مختلف میکنم و ببین دفترچه چه میگوید:
mkdir -p ~/gitlab/rl && cd ~/gitlab/rl && git init -qecho 1 > a && git add a && git commit -qm "First"echo 2 >> a && git commit -qam "Second"git switch -qc featureecho 3 >> a && git commit -qam "Third on feature"git switch -q maingit merge -q --ff-only featuregit reset -q --hard HEAD~1echo "--- git reflog (تازهترین بالا):"git reflog--- git reflog (تازهترین بالا):992a3c3 HEAD@{0}: reset: moving to HEAD~1e8e88a5 HEAD@{1}: merge feature: Fast-forward992a3c3 HEAD@{2}: checkout: moving from feature to maine8e88a5 HEAD@{3}: commit: Third on feature992a3c3 HEAD@{4}: checkout: moving from main to feature992a3c3 HEAD@{5}: commit: Secondfe1bda6 HEAD@{6}: commit (initial): Firstهر خط: هش HEAD@{شماره}: نوع عملیات: توضیح. شمارهی {0} وضعیت فعلی است و بزرگتر یعنی قدیمیتر. HEAD@{3} یعنی «جایی که HEAD سه جابهجایی پیش بود». میتوانی آن را مثل هر آدرس commit به کار ببری:
cd ~/gitlab/rlecho "--- محتوای HEAD@{2}:"git log -1 --format='%h %s' 'HEAD@{2}'echo "--- reflog با زمان نسبی (--date=relative):"git reflog --date=relative | head -3echo "--- فرق وضعیت فعلی با HEAD@{3}:"git diff --stat 'HEAD@{3}' HEAD | tail -1--- محتوای HEAD@{2}:992a3c3 Second--- reflog با زمان نسبی (--date=relative):992a3c3 HEAD@{0 seconds ago}: reset: moving to HEAD~1e8e88a5 HEAD@{0 seconds ago}: merge feature: Fast-forward992a3c3 HEAD@{0 seconds ago}: checkout: moving from feature to main--- فرق وضعیت فعلی با HEAD@{3}: 1 file changed, 1 deletion(-)آدرسدهی زمانی هم ممکن است: HEAD@{5.minutes.ago}، HEAD@{yesterday}، main@{2.days.ago} (فقط تا جایی که reflog به عقب میرود؛ اینجا همهچیز چند ثانیه پیش اتفاق افتاد، پس زمانهای طولانی خطای «log only goes back to…» میدهند). {...} را داخل '...' بگذار تا shell تفسیرش نکند.
مثال ۲: برگرداندن شاخهی پاکشده (تمرین اصلی)
Section titled “مثال ۲: برگرداندن شاخهی پاکشده (تمرین اصلی)”شاخهای با کار ادغامنشده میسازم و اشتباهاً با -D پاکش میکنم:
mkdir -p ~/gitlab/lost && cd ~/gitlab/lost && git init -qecho base > base.txt && git add base.txt && git commit -qm "Base"git switch -qc feature/reportecho "report 1" > report.txt && git add report.txt && git commit -qm "Add report skeleton"echo "report 2" >> report.txt && git commit -qam "Add report totals"git switch -q mainecho "--- شاخهی ادغامنشده را اشتباهاً پاک میکنم:"git branch -D feature/reportgit branchls--- شاخهی ادغامنشده را اشتباهاً پاک میکنم:Deleted branch feature/report (was 7ee95a0).* mainbase.txtشاخه رفت، فایل report.txt هم. ولی commit ها در .git هنوز هستند. reflog را بخوان و آخرین commit آن شاخه را پیدا کن:
cd ~/gitlab/lostgit reflogecho "--- خطی که میگوید HEAD از شاخه رفت، و خط بعدی (قدیمیتر) که آخرین commit شاخه است:"git reflog | grep -A1 "moving from feature/report to main"h=$(git reflog | grep -A1 "moving from feature/report to main" | tail -1 | awk '{print $1}')echo "هش پیداشده: $h"git log -1 --format='%h %s' $hecho "--- برچسب را برمیگردانم:"git branch feature/report $hgit branchgit switch -q feature/report && ls && cat report.txtgit switch -q main422c0c9 HEAD@{0}: checkout: moving from feature/report to main7ee95a0 HEAD@{1}: commit: Add report totals2053df1 HEAD@{2}: commit: Add report skeleton422c0c9 HEAD@{3}: checkout: moving from main to feature/report422c0c9 HEAD@{4}: commit (initial): Base--- خطی که میگوید HEAD از شاخه رفت، و خط بعدی (قدیمیتر) که آخرین commit شاخه است:422c0c9 HEAD@{0}: checkout: moving from feature/report to main7ee95a0 HEAD@{1}: commit: Add report totalsهش پیداشده: 7ee95a07ee95a0 Add report totals--- برچسب را برمیگردانم: feature/report* mainbase.txtreport.txtreport 1report 2هر خط reflog با هشِ وضعیتِ بعد از آن عملیات شروع میشود. خط checkout: moving from feature/report to main یعنی «HEAD از آن شاخه به main رفت» و هشش مال main است؛ ولی خط بعدی (قدیمیتر)، commit: Add report totals، هشِ آخرین commit خود شاخه را دارد. git branch نام هش یک برچسب جدید روی آن commit میگذارد و همهی فایلها (report.txt) برمیگردند. اگر شاخه هرگز checkout نشده بود، فقط git fsck (مثال ۵) کمک میکند.
مثال ۳: برگشت از reset --hard
Section titled “مثال ۳: برگشت از reset --hard”درس برگرداندن تغییرات را یادآوری کن: بعد از reset --hard، commit ها از دید git log ناپدید میشوند ولی در reflog هستند. اینجا با آدرس زمانی:
mkdir -p ~/gitlab/rs && cd ~/gitlab/rs && git init -qfor i in 1 2 3 4 5; do echo $i > f; git add f; git commit -qm "Commit $i"; donegit log --oneline | head -3echo "--- reset --hard به دو commit قبل (و از دسترفتن 4 و 5):"git reset -q --hard HEAD~2git log --oneline | head -1echo "--- reflog:"git reflog | head -3echo "--- برگشتن به قبل از reset:"git reset -q --hard 'HEAD@{1}'git log --oneline | head -220ab952 Commit 59507c5a Commit 40f7f647 Commit 3--- reset --hard به دو commit قبل (و از دسترفتن 4 و 5):0f7f647 Commit 3--- reflog:0f7f647 HEAD@{0}: reset: moving to HEAD~220ab952 HEAD@{1}: commit: Commit 59507c5a HEAD@{2}: commit: Commit 4--- برگشتن به قبل از reset:20ab952 Commit 59507c5a Commit 4HEAD@{1} یعنی «یک جابهجایی پیش». ولی دقت کن: reflog فقط commit های ثبتشده را نگه میدارد؛ تغییرهای commit نشده (مثل ویرایشهایی که قبل از reset --hard داشتی) هرگز برنمیگردند (درس برگرداندن تغییرات).
مثال ۴: برگشت از rebase یا amend اشتباه
Section titled “مثال ۴: برگشت از rebase یا amend اشتباه”rebase یا amend یک commit را با نسخهی جدیدی عوض میکند. نسخهی قبلی در reflog است. روی خود شاخه هم reflog جداگانهای هست (git reflog show شاخه):
mkdir -p ~/gitlab/am && cd ~/gitlab/am && git init -qecho v1 > f && git add f && git commit -qm "Add feature (typo mesage)"old=$(git rev-parse --short HEAD)git commit --amend -qm "Add feature"echo "--- شناسهی commit قبل از amend: $old ؛ بعد:"git log --onelineecho "--- reflog شاخهی main:"git reflog show main | head -3echo "--- commit قدیمی هنوز قابلدیدن است:"git show -s --format='%h %s' $old--- شناسهی commit قبل از amend: ea6c7ac ؛ بعد:871b932 Add feature--- reflog شاخهی main:871b932 main@{0}: commit (amend): Add featureea6c7ac main@{1}: commit (initial): Add feature (typo mesage)--- commit قدیمی هنوز قابلدیدن است:ea6c7ac Add feature (typo mesage)با ORIG_HEAD هم برای عملیات بزرگ (merge، rebase، reset) میشود به وضعیت قبل برگشت: git reset --hard ORIG_HEAD؛ ولی ORIG_HEAD با عملیات بعدی عوض میشود، در حالی که reflog همهچیز را دارد.
مثال ۵: commit های dangling و git fsck
Section titled “مثال ۵: commit های dangling و git fsck”reflog وقتی کمک نمیکند (مثلاً entry های reflog منقضی شدهاند یا commit هرگز در HEAD نبوده)، commit ها هنوز ممکن است در .git/objects باشند. git fsck (File System ChecK) شیءهای بیصاحب را فهرست میکند. اینجا با پاککردن عمدی reflog وضعیت سخت را شبیهسازی میکنم:
mkdir -p ~/gitlab/dang && cd ~/gitlab/dang && git init -qecho a > f && git add f && git commit -qm "Base"git switch -qc secretecho "مهم" > important.txt && git add important.txt && git commit -qm "Important work"lost=$(git rev-parse --short HEAD)git switch -q main && git branch -D secret -qecho "--- reflog را کاملاً پاک میکنم (شبیه منقضیشدن):"git reflog expire --expire=now --allgit reflogecho "(reflog حالا هیچ ردی از commit ندارد)"echo "--- ولی fsck commit های بیصاحب را پیدا میکند:"git fsck --lost-found 2>/dev/null | grep commitecho "--- محتوای آن (آنچه گم شده بود):"git show -s --format='%h %s' $lostgit branch rescued $lost && git switch -q rescued && cat important.txt--- reflog را کاملاً پاک میکنم (شبیه منقضیشدن):(reflog حالا هیچ ردی از commit ندارد)--- ولی fsck commit های بیصاحب را پیدا میکند:dangling commit 70087d6e788c73ca531ec4cd49499b0dec74a1b2--- محتوای آن (آنچه گم شده بود):70087d6 Important workمهمdangling commit هش: commit ای که هیچ برچسب و هیچ commit دیگری به آن اشاره نمیکند. git fsck --lost-found آنها را (در .git/lost-found/) نگه میدارد. با git branch نام هش برچسب جدید میدهی. این فرصت تا وقتی git gc اجرا نشده هست:
cd ~/gitlab/danggit switch -q main && git branch -D rescued -qgit reflog expire --expire=now --allgit gc -q --prune=nowecho "--- بعد از gc --prune=now:"git fsck --lost-found 2>/dev/null | grep -c commit || true--- بعد از gc --prune=now:0بعد از gc --prune=now شیء واقعاً پاک میشود و دیگر هیچ راهی نیست. در حالت عادی Git commit های بیصاحب را دستکم ۲ هفته (پیشفرض gc.pruneExpire) نگه میدارد و reflog ورودیها را ۹۰ روز (دستیافتنی) یا ۳۰ روز (غیردستیافتنی)، پس عملاً چند هفته فرصت داری. (اگر git gc را خودت با --prune=now نزنی، Git خودکار بعد از مدتی اجرا میکند.)
مثال ۶: git cherry-pick، فقط یک commit
Section titled “مثال ۶: git cherry-pick، فقط یک commit”گاهی یک commit را از شاخهای میخواهی، نه کل شاخه را. معمولترین مثال: رفع یک باگ را روی main ساختهای و همان را روی شاخهی انتشار هم میخواهی (backport). cherry-pick تغییر آن commit را روی شاخهی فعلی دوباره اعمال و یک commit جدید میسازد:
mkdir -p ~/gitlab/cp && cd ~/gitlab/cp && git init -qprintf 'def total(p, q):\n return p * q\n' > shop.pygit add shop.py && git commit -qm "Add total"git branch release/1.0echo "--- روی main: یک ویژگی (فایل جدا) و یک رفع باگ:"printf 'def discount(t):\n return t * 0.9\n' > extras.py && git add extras.py && git commit -qm "feat: add discount"sed -i 's/p \* q/round(p * q, 2)/' shop.py && git commit -qam "fix: round total to two decimals"git log --onelinefix=$(git rev-parse --short HEAD)echo "--- فقط رفع باگ را به release/1.0 ببر:"git switch -q release/1.0git cherry-pick -x $fixgit log --onelinecat shop.pyls--- روی main: یک ویژگی (فایل جدا) و یک رفع باگ:2891a6d fix: round total to two decimalsab1f367 feat: add discount62694f7 Add total--- فقط رفع باگ را به release/1.0 ببر:[release/1.0 1404a38] fix: round total to two decimals Date: Sun Oct 4 09:09:53 2026 +0000 1 file changed, 1 insertion(+), 1 deletion(-)1404a38 fix: round total to two decimals62694f7 Add totaldef total(p, q): return round(p * q, 2)shop.pyنتیجه: شاخهی انتشار فقط رفع باگ را دارد، نه discount. -x یک خط «(cherry picked from commit …)» به پیام اضافه میکند تا بعداً معلوم باشد این commit از کجا آمده (برای backport خوب است). دقت کن commit جدید هش جدید دارد (چون پدر و زمان فرق میکند)؛ نه همان commit. گزینههای مفید:
| دستور | کار |
|---|---|
git cherry-pick A |
یک commit |
git cherry-pick A B C |
چند commit، بهترتیب |
git cherry-pick A^..C |
بازهی A تا C (شامل A) |
git cherry-pick -n A |
اعمال تغییر بدون commit (آمادهی ویرایش بیشتر) |
git cherry-pick -x A |
یادداشت مبدأ در پیام |
git cherry-pick -m 1 M |
برای commit ادغام (کدام والد اصلی است) |
--continue / --abort / --skip |
وقتی conflict شد |
مثال ۷: cherry-pick با conflict
Section titled “مثال ۷: cherry-pick با conflict”اگر شاخهی مقصد همان بخش را عوض کرده باشد، مثل ادغام conflict میشود:
mkdir -p ~/gitlab/cpc && cd ~/gitlab/cpc && git init -qecho "price=10" > cfg && git add cfg && git commit -qm "Base"git switch -qc release && echo "price=12" > cfg && git commit -qam "Release price"git switch -q main && echo "price=15" > cfg && git commit -qam "Main price"fix=$(git rev-parse --short HEAD)git switch -q releasegit cherry-pick $fix 2>&1 | tail -3git status -secho "--- حل (قیمت نهایی 13) و ادامه:"echo "price=13" > cfg && git add cfgGIT_EDITOR=true git cherry-pick --continue 2>&1 | tail -1git log --onelinehint: You can instead skip this commit with "git cherry-pick --skip".hint: To abort and get back to the state before "git cherry-pick",hint: run "git cherry-pick --abort".UU cfg--- حل (قیمت نهایی 13) و ادامه: 1 file changed, 1 insertion(+), 1 deletion(-)750871d Main price6f5e6da Release pricea4a3859 Baseمثل ادغام و rebase: فایل را حل کن، git add، و git cherry-pick --continue (یا --abort برای لغو). UU در git status -s یعنی هر دو طرف عوض کردهاند.
مثال ۸: git bisect، یافتن commit خراب
Section titled “مثال ۸: git bisect، یافتن commit خراب”یک رگرسیون: «نسخهی v1.0 درست بود؛ حالا جمع دو عدد را اشتباه میدهد». ۲۰ commit بین آنهاست. بهجای خواندن همه، bisect با جستوجوی دودویی پیدا میکند: تو یک commit خوب و یک commit بد میدهی؛ Git commit وسط را checkout میکند و تو میگویی خوب است یا بد؛ هر بار نصف بازه حذف میشود. اول تاریخچهی نمونه را میسازم (باگ در یک commit میانی وارد میشود، کسی هم نمیداند کدام):
mkdir -p ~/gitlab/bis && cd ~/gitlab/bis && git init -qprintf 'def add(a, b):\n return a + b\n' > calc.pygit add calc.py && git commit -qm "Add calc" && git tag v1.0for i in $(seq 1 9); do echo "note $i" >> notes.txt; git add notes.txt; git commit -qm "Update notes $i"; donesed -i 's/return a + b/return a - b/' calc.py && git commit -qam "Refactor add() for clarity"for i in $(seq 10 19); do echo "note $i" >> notes.txt; git add notes.txt; git commit -qm "Update notes $i"; donegit log --oneline | wc -lcat > test.sh <<'EOF'#!/bin/sh# 0 = خوب، 1 = بدpython3 -B -c "from calc import add; import sys; sys.exit(0 if add(2, 3) == 5 else 1)"EOFchmod +x test.shecho "--- نسخهی فعلی خراب است؟"./test.sh && echo "خوب" || echo "بد (add(2,3) برابر 5 نیست)"git switch -q --detach v1.0 && (./test.sh && echo "v1.0: خوب") ; git switch -q main21--- نسخهی فعلی خراب است؟بد (add(2,3) برابر 5 نیست)v1.0: خوبحالا bisect بهصورت دستی (آنچه خودت تایپ میکنی):
cd ~/gitlab/bisgit bisect startgit bisect badgit bisect good v1.0echo "--- Git commit وسط را checkout کرد؛ آزمایش:"./test.sh && echo "→ git bisect good" || echo "→ git bisect bad"status: waiting for both good and bad commitsstatus: waiting for good commit(s), bad commit knownBisecting: 9 revisions left to test after this (roughly 3 steps)[3b03987a9d2e79aebb493fd3719dd6b78aa9453f] Refactor add() for clarity--- Git commit وسط را checkout کرد؛ آزمایش:→ git bisect badgit bisect start شروع، bad (commit فعلی بد است) و good v1.0 (آن نسخه خوب بود). Git میگوید چند commit مانده و کدام را checkout کرده. حالا این commit را تست میکنی و نتیجه را اعلام میکنی. اینجا چند قدم را دستی میروم، بعد خودکارش میکنم:
cd ~/gitlab/bisfor step in 1 2; do if ./test.sh; then echo "قدم $step: خوب"; git bisect good 2>&1 | head -1; else echo "قدم $step: بد"; git bisect bad 2>&1 | head -1; fidoneقدم 1: بدBisecting: 4 revisions left to test after this (roughly 2 steps)قدم 2: خوبBisecting: 2 revisions left to test after this (roughly 1 step)مثال ۹: git bisect run، خودکار
Section titled “مثال ۹: git bisect run، خودکار”وقتی یک اسکریپت تست داری که کد خروج صفر (خوب) یا غیرصفر (بد) میدهد، دیگر لازم نیست دستی بروی: git bisect run اسکریپت تمام قدمها را خودش میرود:
cd ~/gitlab/bisgit bisect resetgit bisect start HEAD v1.0 > /dev/nullgit bisect run ./test.sh 2>&1 | tail -9echo "--- گزارش مراحل:"git bisect log | grep -E "^# (good|bad|first)" | head -10git bisect resetgit log -1 --format='%h %s'Previous HEAD position was d38e6fa Update notes 7Switched to branch 'main'commit 3b03987a9d2e79aebb493fd3719dd6b78aa9453fAuthor: Ali <ali@example.com>Date: Sun Oct 4 09:09:53 2026 +0000
Refactor add() for clarity
calc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)bisect found first bad commit--- گزارش مراحل:# bad: [6f8c9fbea016662290f2704435c10fa51d124daa] Update notes 19# good: [756773c7c45ecd887a71bb9996e211f2d98de366] Add calc# bad: [3b03987a9d2e79aebb493fd3719dd6b78aa9453f] Refactor add() for clarity# good: [87d126ddf10b6a88e5a72ac1a0f6e99b16424e55] Update notes 5# good: [d38e6fad1265ace35ced3cc1403c5d7af53f8a0f] Update notes 7# good: [efb88ec7d727a6bd9daf228911e9f78a45b2dc14] Update notes 9# first bad commit: [3b03987a9d2e79aebb493fd3719dd6b78aa9453f] Refactor add() for clarityPrevious HEAD position was efb88ec Update notes 9Switched to branch 'main'6f8c9fb Update notes 19در حدود ۴ تا ۵ قدم از میان ۲۱ commit، Git اعلام میکند «is the first bad commit» و commit را نشان میدهد؛ همان که میدانیم (Refactor add() for clarity). git bisect log تمام تصمیمها را فهرست میکند و git bisect reset را فراموش نکن که به شاخهی اول برگردی. کد خروج ویژه: 125 یعنی «این commit قابل آزمایش نیست (مثلاً build نمیشود)، رد کن» (skip). برای ۱۰۰۰ commit فقط حدود ۱۰ قدم لازم است (لگاریتم دودویی). و چون با -B، خروجی تستها ثابت و بدون فایل کش است.
پشت پرده: reflog فقط چند فایل متنی است
Section titled “پشت پرده: reflog فقط چند فایل متنی است”reflog در .git/logs/ ذخیره میشود: یک فایل برای HEAD و یک فایل برای هر شاخه. هر خط: هش قبلی، هش جدید، نویسنده، زمان (epoch) و توضیح:
cd ~/gitlab/rsecho "--- فایلهای reflog:"ls .git/logs .git/logs/refs/headsecho "--- آخرین خط reflogی HEAD (خام):"tail -n 1 .git/logs/HEAD | cut -c1-200echo "--- تنظیم نگهداری:"git config --get gc.reflogExpire || echo "gc.reflogExpire: پیشفرض 90 روز (دستیافتنی)"git config --get gc.reflogExpireUnreachable || echo "gc.reflogExpireUnreachable: پیشفرض 30 روز"--- فایلهای reflog:.git/logs:HEADrefs
.git/logs/refs/heads:main--- آخرین خط reflogی HEAD (خام):0f7f647852a6ad39f30010a7331af7ef1e04864f 20ab9529061305b1130ea937f09d24cb558e9db6 Ali <ali@example.com> 1791104993 +0000 reset: moving to HEAD@{1}--- تنظیم نگهداری:gc.reflogExpire: پیشفرض 90 روز (دستیافتنی)gc.reflogExpireUnreachable: پیشفرض 30 روزسه نکتهی مهم: ۱) reflog محلی است و با push/clone نمیرود؛ اگر کامپیوتر خراب شود reflog هم رفته. ۲) هر مخزن reflog خودش را دارد؛ فقط جابهجاییهای همان مخزن. ۳) ورودیها منقضی میشوند (پیشفرض: ۹۰ روز برای commit هایی که هنوز به برچسبی وصلاند، و ۳۰ روز برای بیصاحبها) و git gc بعد از آن شیءهای بیصاحب را پاک میکند. پس سریع برگردان؛ نه بعد از ماهها.
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
git reflog / git reflog show شاخه |
دفترچهی جابهجاییهای HEAD / یک شاخه |
HEAD@{n} / HEAD@{2.hours.ago} |
آدرسدهی بر اساس reflog |
git branch نام هش |
برچسب جدید روی یک commit (برگرداندن شاخه) |
git reset --hard 'HEAD@{1}' |
برگشت از reset اشتباه |
git fsck --lost-found |
commit های بیصاحب (dangling) |
git cherry-pick [-x] commit… |
کپی یک یا چند commit روی شاخهی فعلی |
git cherry-pick --continue/--abort |
بعد از conflict |
git bisect start BAD GOOD |
شروع جستوجوی commit خراب |
git bisect good / bad / skip |
اعلام نتیجهی commit فعلی |
git bisect run اسکریپت |
خودکار (کد خروج ۰ خوب، ۱..۱۲۴ بد، ۱۲۵ رد) |
git bisect log / reset |
گزارش / پایان و بازگشت |
| میخواهم… | بزن |
|---|---|
| شاخهی پاکشده را برگردانم | git reflog ← هش ← git branch نام هش |
| از reset –hard برگردم | git reset --hard 'HEAD@{1}' |
| یک رفع باگ را روی شاخهی انتشار هم داشته باشم | git switch release; git cherry-pick -x commit |
| بفهمم باگ از کدام commit آمده | git bisect start; bad; good tag یا git bisect run |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فکر اینکه reflog همهچیز را نجات میدهد
Section titled “۱) فکر اینکه reflog همهچیز را نجات میدهد”reflog تغییر commit نشده را ندارد، و روی کامپیوتر دیگر نیست، و منقضی میشود. راهحل: مرتب commit کن (حتی wip) و push کن؛ reflog تور نجات آخر است، نه روش کار.
۲) gc --prune=now یا reflog expire بدون فکر
Section titled “۲) gc --prune=now یا reflog expire بدون فکر”اینها دقیقاً همان چیزهاییاند که امکان نجات را از بین میبرند (مثال ۵). راهحل: هرگز آنها را برای «تمیزکاری» نزن؛ Git خودش مناسبترین زمان را انتخاب میکند.
۳) فراموشکردن git bisect reset
Section titled “۳) فراموشکردن git bisect reset”وسط bisect در حالت detached HEAD هستی و git status میگوید You are currently bisecting. راهحل: git bisect reset به شاخهی اولیه برمیگرداند. (و تغییر commit نشده را قبل از شروع commit یا stash کن.)
۴) تست ناپایدار در bisect
Section titled “۴) تست ناپایدار در bisect”اگر تست گاهی خوب و گاهی بد باشد (flaky)، bisect به commit اشتباه میرسد. راهحل: اسکریپت تست را قطعی بنویس (چند بار اجرا کن)، و commit های غیرقابلآزمایش را skip کن.
۵) cherry-pick روی commit ادغام
Section titled “۵) cherry-pick روی commit ادغام”cd ~/gitlab/cpcgit switch -q maingit switch -qc topic && echo t > t.txt && git add t.txt && git commit -qm "Topic work"git switch -q main && echo m > m.txt && git add m.txt && git commit -qm "Main work"git merge -q --no-edit topicgit switch -q releasegit cherry-pick main 2>&1 | head -2error: commit d6603fe3ad2c263432e6503272a50c341d18c5a1 is a merge but no -m option was given.fatal: cherry-pick failedcommit ادغام دو والد دارد و Git نمیداند کدام خط را «تغییر» حساب کند. راهحل: git cherry-pick -m 1 commit (والد اول اصلی است)، یا بهتر: خود commit های شاخه را جدا cherry-pick کن.
۶) cherry-pick بیش از حد
Section titled “۶) cherry-pick بیش از حد”اگر مدام commit ها را بین شاخهها کپی کنی، نسخههای تکراری با هشهای مختلف ساخته میشوند و ادغامهای بعدی گیج میشوند. راهحل: cherry-pick را برای مورد خاصی مثل backport نگه دار و در حالت عادی merge یا rebase بزن.
در یک مخزن چند commit بزن، یک reset --hard HEAD~2 بزن و با git reflog و git reset --hard 'HEAD@{1}' همه را برگردان. ثابت کن تاریخچه مثل قبل شد.
دیدن جواب
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -qfor i in 1 2 3 4; do echo $i > f; git add f; git commit -qm "Commit $i"; donebefore=$(git rev-parse HEAD)git reset -q --hard HEAD~2git log --oneline | head -1git reflog | head -2git reset -q --hard 'HEAD@{1}'[ "$(git rev-parse HEAD)" = "$before" ] && echo "برگشت: تاریخچه مثل قبل شد"git log --oneline | head -232d1fb3 Commit 232d1fb3 HEAD@{0}: reset: moving to HEAD~248b3857 HEAD@{1}: commit: Commit 4برگشت: تاریخچه مثل قبل شد48b3857 Commit 4723e62c Commit 3تمرین اصلی: یک branch پاکشده را با reflog برگردان. شاخهی feature/x را با دو commit بساز، با git branch -D پاک کن و فقط با reflog و git branch برگردان. ثابت کن فایلها برگشتند.
دیدن جواب
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2 && git init -qecho base > base.txt && git add base.txt && git commit -qm "Base"git switch -qc feature/xecho "x1" > x.txt && git add x.txt && git commit -qm "Add x"echo "x2" >> x.txt && git commit -qam "Extend x"git switch -q maingit branch -D feature/xecho "--- شاخه رفت؛ reflog:"git reflog | head -5h=$(git reflog | grep -A1 "moving from feature/x to main" | tail -1 | awk '{print $1}')git branch feature/x $hgit switch -q feature/xcat x.txtgit log --onelineDeleted branch feature/x (was fbd56da).--- شاخه رفت؛ reflog:3305612 HEAD@{0}: checkout: moving from feature/x to mainfbd56da HEAD@{1}: commit: Extend x2894615 HEAD@{2}: commit: Add x3305612 HEAD@{3}: checkout: moving from main to feature/x3305612 HEAD@{4}: commit (initial): Basex1x2fbd56da Extend x2894615 Add x3305612 Baseبا git bisect run، اولین commit خراب را خودکار پیدا کن. ۱۵ commit بساز که در یکی از آنها (بدون اینکه بگویی کدام) فایل config.txt مقدار debug=true بگیرد. یک اسکریپت تست بنویس که با debug=true شکست بخورد و bisect را روی آن اجرا کن؛ ثابت کن commit پیدا شده همان است.
دیدن جواب
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -qecho "debug=false" > config.txt && git add config.txt && git commit -qm "Add config" && git tag goodfor i in $(seq 1 6); do echo "n$i" >> log.txt; git add log.txt; git commit -qm "Update log $i"; donesed -i 's/debug=false/debug=true/' config.txt && git commit -qam "Tweak config for local testing"bad=$(git rev-parse --short HEAD)for i in $(seq 7 14); do echo "n$i" >> log.txt; git add log.txt; git commit -qm "Update log $i"; donecat > check.sh <<'EOF'#!/bin/sh! grep -q 'debug=true' config.txtEOFchmod +x check.shgit bisect start HEAD good > /dev/nullgit bisect run ./check.sh 2>&1 | grep -E "first bad commit|^[0-9a-f]{40}" | head -2found=$(git rev-parse --short refs/bisect/bad)git bisect reset > /dev/null 2>&1echo "commit مقصر واقعی: $bad ؛ commit پیدا شده توسط bisect: $found"[ "$bad" = "$found" ] && echo "✓ درست پیدا کرد"70e2cbe80151a527319fe394729e661fdf0d8176 is the first bad commitbisect found first bad commitcommit مقصر واقعی: 70e2cbe ؛ commit پیدا شده توسط bisect: 70e2cbe✓ درست پیدا کردآزمونک
Section titled “آزمونک”شاخهای را با «git branch -D» پاک کردهای و میخواهی برگردانی. اولین قدم؟
commit ها بعد از حذف شاخه هنوز در .git/objects هستند؛ reflog (دفترچهی جابهجاییهای HEAD) هش را نگه داشته. تا وقتی gc اجرا نشده یا ورودی منقضی نشده برمیگردد.
reflog چیست؟
و ورودیهایش بعد از ۹۰ (یا ۳۰) روز منقضی میشوند.
HEAD@{2.hours.ago} یعنی؟
main@{yesterday} هم ممکن است.
git cherry-pick چه میکند؟
مثلاً برای backport یک رفع باگ به شاخهی انتشار؛ -x مبدأ را در پیام مینویسد.
git bisect برای چیست و چرا سریع است؟
با git bisect run و اسکریپتی که کد خروج میدهد، کاملاً خودکار میشود.
کدام کار شانس نجات commit های گمشده را از بین میبرد؟
بعد از آن شیءهای بیصاحب واقعاً پاک میشوند.
جمعبندی
Section titled “جمعبندی”- Git تقریباً هیچچیز را فوراً پاک نمیکند؛ فقط برچسبها گم میشوند.
git reflogتمام جابهجاییهایHEADرا نگه میدارد:HEAD@{n}،HEAD@{2.hours.ago}. - شاخهی پاکشده: reflog ← هش آخر ←
git branch نام هش. reset اشتباه:git reset --hard 'HEAD@{1}'. amend/rebase اشتباه: commit قدیمی در reflog است (وORIG_HEAD). - reflog محلی، منقضیشدنی (۹۰/۳۰ روز) و بدون تغییر commit نشده است.
git fsck --lost-foundcommit های dangling را مییابد؛ بعد ازgc --prune=nowرفتهاند. git cherry-pick [-x] commit: تغییر یک commit را روی شاخهی فعلی اعمال میکند (backport)؛ conflict ←--continue/--abort؛ برای commit ادغام-m 1.git bisect:start،bad،good،reset؛bisect run اسکریپتخودکار (۰ خوب، ۱ تا ۱۲۴ بد، ۱۲۵ رد). اسکریپت تست قطعی باشد؛ حتماًbisect reset.
| دستور | کاری که میکند |
|---|---|
git reflog | دفترچهی جابهجاییهای HEAD |
git branch نام هش | برگرداندن شاخهی پاکشده |
git reset --hard 'HEAD@{1}' | برگشت از reset اشتباه |
git fsck --lost-found | commit های بیصاحب |
git cherry-pick -x هش | کپی یک commit با یادداشت مبدأ |
git cherry-pick --continue / --abort | بعد از conflict |
git bisect start بد خوب | شروع جستوجوی commit خراب |
git bisect good / bad / skip | نتیجهی commit فعلی |
git bisect run ./test.sh | جستوجوی خودکار |
git bisect reset | پایان و بازگشت به شاخهی اول |