رفتن به محتوا
LoopX

نجات و ابزارهای پیشرفته

توی این درس یاد می‌گیری وقتی اشتباه بزرگ کردی، قبل از ناامیدی ابزار نجات را بشناسی: 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 در حدود ۸ قدم.

بعد از حذف شاخه‌ی feature، commit های F1 و F2 هنوز در .git/objects هستند ولی هیچ برچسبی به آن‌ها اشاره نمی‌کند (خط‌چین). reflog هش آخر را یادآوری می‌کند و با git branch برچسب را برمی‌گردانی.

هر بار HEAD جابه‌جا می‌شود (commit، checkout، merge، rebase، reset…) یک خط در reflog ثبت می‌شود. چند کار مختلف می‌کنم و ببین دفترچه چه می‌گوید:

Terminal window
mkdir -p ~/gitlab/rl && cd ~/gitlab/rl && git init -q
echo 1 > a && git add a && git commit -qm "First"
echo 2 >> a && git commit -qam "Second"
git switch -qc feature
echo 3 >> a && git commit -qam "Third on feature"
git switch -q main
git merge -q --ff-only feature
git reset -q --hard HEAD~1
echo "--- git reflog (تازه‌ترین بالا):"
git reflog
خروجی
--- git reflog (تازه‌ترین بالا):
992a3c3 HEAD@{0}: reset: moving to HEAD~1
e8e88a5 HEAD@{1}: merge feature: Fast-forward
992a3c3 HEAD@{2}: checkout: moving from feature to main
e8e88a5 HEAD@{3}: commit: Third on feature
992a3c3 HEAD@{4}: checkout: moving from main to feature
992a3c3 HEAD@{5}: commit: Second
fe1bda6 HEAD@{6}: commit (initial): First

هر خط: هش HEAD@{شماره}: نوع عملیات: توضیح. شماره‌ی {0} وضعیت فعلی است و بزرگ‌تر یعنی قدیمی‌تر. HEAD@{3} یعنی «جایی که HEAD سه جابه‌جایی پیش بود». می‌توانی آن را مثل هر آدرس commit به کار ببری:

Terminal window
cd ~/gitlab/rl
echo "--- محتوای HEAD@{2}:"
git log -1 --format='%h %s' 'HEAD@{2}'
echo "--- reflog با زمان نسبی (--date=relative):"
git reflog --date=relative | head -3
echo "--- فرق وضعیت فعلی با 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~1
e8e88a5 HEAD@{0 seconds ago}: merge feature: Fast-forward
992a3c3 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 پاکش می‌کنم:

Terminal window
mkdir -p ~/gitlab/lost && cd ~/gitlab/lost && git init -q
echo base > base.txt && git add base.txt && git commit -qm "Base"
git switch -qc feature/report
echo "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 main
echo "--- شاخه‌ی ادغام‌نشده را اشتباهاً پاک می‌کنم:"
git branch -D feature/report
git branch
ls
خروجی
--- شاخه‌ی ادغام‌نشده را اشتباهاً پاک می‌کنم:
Deleted branch feature/report (was 7ee95a0).
* main
base.txt

شاخه رفت، فایل report.txt هم. ولی commit ها در .git هنوز هستند. reflog را بخوان و آخرین commit آن شاخه را پیدا کن:

Terminal window
cd ~/gitlab/lost
git reflog
echo "--- خطی که می‌گوید 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' $h
echo "--- برچسب را برمی‌گردانم:"
git branch feature/report $h
git branch
git switch -q feature/report && ls && cat report.txt
git switch -q main
خروجی
422c0c9 HEAD@{0}: checkout: moving from feature/report to main
7ee95a0 HEAD@{1}: commit: Add report totals
2053df1 HEAD@{2}: commit: Add report skeleton
422c0c9 HEAD@{3}: checkout: moving from main to feature/report
422c0c9 HEAD@{4}: commit (initial): Base
--- خطی که می‌گوید HEAD از شاخه رفت، و خط بعدی (قدیمی‌تر) که آخرین commit شاخه است:
422c0c9 HEAD@{0}: checkout: moving from feature/report to main
7ee95a0 HEAD@{1}: commit: Add report totals
هش پیدا‌شده: 7ee95a0
7ee95a0 Add report totals
--- برچسب را برمی‌گردانم:
feature/report
* main
base.txt
report.txt
report 1
report 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، commit ها از دید git log ناپدید می‌شوند ولی در reflog هستند. این‌جا با آدرس زمانی:

Terminal window
mkdir -p ~/gitlab/rs && cd ~/gitlab/rs && git init -q
for i in 1 2 3 4 5; do echo $i > f; git add f; git commit -qm "Commit $i"; done
git log --oneline | head -3
echo "--- reset --hard به دو commit قبل (و از دست‌رفتن 4 و 5):"
git reset -q --hard HEAD~2
git log --oneline | head -1
echo "--- reflog:"
git reflog | head -3
echo "--- برگشتن به قبل از reset:"
git reset -q --hard 'HEAD@{1}'
git log --oneline | head -2
خروجی
20ab952 Commit 5
9507c5a Commit 4
0f7f647 Commit 3
--- reset --hard به دو commit قبل (و از دست‌رفتن 4 و 5):
0f7f647 Commit 3
--- reflog:
0f7f647 HEAD@{0}: reset: moving to HEAD~2
20ab952 HEAD@{1}: commit: Commit 5
9507c5a HEAD@{2}: commit: Commit 4
--- برگشتن به قبل از reset:
20ab952 Commit 5
9507c5a Commit 4

HEAD@{1} یعنی «یک جابه‌جایی پیش». ولی دقت کن: reflog فقط commit های ثبت‌شده را نگه می‌دارد؛ تغییرهای commit نشده (مثل ویرایش‌هایی که قبل از reset --hard داشتی) هرگز برنمی‌گردند (درس برگرداندن تغییرات).

مثال ۴: برگشت از rebase یا amend اشتباه

Section titled “مثال ۴: برگشت از rebase یا amend اشتباه”

rebase یا amend یک commit را با نسخه‌ی جدیدی عوض می‌کند. نسخه‌ی قبلی در reflog است. روی خود شاخه هم reflog جداگانه‌ای هست (git reflog show شاخه):

Terminal window
mkdir -p ~/gitlab/am && cd ~/gitlab/am && git init -q
echo 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 --oneline
echo "--- reflog شاخه‌ی main:"
git reflog show main | head -3
echo "--- commit قدیمی هنوز قابل‌دیدن است:"
git show -s --format='%h %s' $old
خروجی
--- شناسه‌ی commit قبل از amend: ea6c7ac ؛ بعد:
871b932 Add feature
--- reflog شاخه‌ی main:
871b932 main@{0}: commit (amend): Add feature
ea6c7ac 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 وضعیت سخت را شبیه‌سازی می‌کنم:

Terminal window
mkdir -p ~/gitlab/dang && cd ~/gitlab/dang && git init -q
echo a > f && git add f && git commit -qm "Base"
git switch -qc secret
echo "مهم" > 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 -q
echo "--- reflog را کاملاً پاک می‌کنم (شبیه منقضی‌شدن):"
git reflog expire --expire=now --all
git reflog
echo "(reflog حالا هیچ ردی از commit ندارد)"
echo "--- ولی fsck commit های بی‌صاحب را پیدا می‌کند:"
git fsck --lost-found 2>/dev/null | grep commit
echo "--- محتوای آن (آنچه گم شده بود):"
git show -s --format='%h %s' $lost
git 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 اجرا نشده هست:

Terminal window
cd ~/gitlab/dang
git switch -q main && git branch -D rescued -q
git reflog expire --expire=now --all
git gc -q --prune=now
echo "--- بعد از 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 جدید می‌سازد:

Terminal window
mkdir -p ~/gitlab/cp && cd ~/gitlab/cp && git init -q
printf 'def total(p, q):\n return p * q\n' > shop.py
git add shop.py && git commit -qm "Add total"
git branch release/1.0
echo "--- روی 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 --oneline
fix=$(git rev-parse --short HEAD)
echo "--- فقط رفع باگ را به release/1.0 ببر:"
git switch -q release/1.0
git cherry-pick -x $fix
git log --oneline
cat shop.py
ls
خروجی
--- روی main: یک ویژگی (فایل جدا) و یک رفع باگ:
2891a6d fix: round total to two decimals
ab1f367 feat: add discount
62694f7 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 decimals
62694f7 Add total
def 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 شد

اگر شاخه‌ی مقصد همان بخش را عوض کرده باشد، مثل ادغام conflict می‌شود:

Terminal window
mkdir -p ~/gitlab/cpc && cd ~/gitlab/cpc && git init -q
echo "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 release
git cherry-pick $fix 2>&1 | tail -3
git status -s
echo "--- حل (قیمت نهایی 13) و ادامه:"
echo "price=13" > cfg && git add cfg
GIT_EDITOR=true git cherry-pick --continue 2>&1 | tail -1
git log --oneline
خروجی
hint: 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 price
6f5e6da Release price
a4a3859 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 میانی وارد می‌شود، کسی هم نمی‌داند کدام):

Terminal window
mkdir -p ~/gitlab/bis && cd ~/gitlab/bis && git init -q
printf 'def add(a, b):\n return a + b\n' > calc.py
git add calc.py && git commit -qm "Add calc" && git tag v1.0
for i in $(seq 1 9); do echo "note $i" >> notes.txt; git add notes.txt; git commit -qm "Update notes $i"; done
sed -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"; done
git log --oneline | wc -l
cat > 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)"
EOF
chmod +x test.sh
echo "--- نسخه‌ی فعلی خراب است؟"
./test.sh && echo "خوب" || echo "بد (add(2,3) برابر 5 نیست)"
git switch -q --detach v1.0 && (./test.sh && echo "v1.0: خوب") ; git switch -q main
خروجی
21
--- نسخه‌ی فعلی خراب است؟
بد (add(2,3) برابر 5 نیست)
v1.0: خوب

حالا bisect به‌صورت دستی (آنچه خودت تایپ می‌کنی):

Terminal window
cd ~/gitlab/bis
git bisect start
git bisect bad
git bisect good v1.0
echo "--- Git commit وسط را checkout کرد؛ آزمایش:"
./test.sh && echo "→ git bisect good" || echo "→ git bisect bad"
خروجی
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
Bisecting: 9 revisions left to test after this (roughly 3 steps)
[3b03987a9d2e79aebb493fd3719dd6b78aa9453f] Refactor add() for clarity
--- Git commit وسط را checkout کرد؛ آزمایش:
→ git bisect bad

git bisect start شروع، bad (commit فعلی بد است) و good v1.0 (آن نسخه خوب بود). Git می‌گوید چند commit مانده و کدام را checkout کرده. حالا این commit را تست می‌کنی و نتیجه را اعلام می‌کنی. این‌جا چند قدم را دستی می‌روم، بعد خودکارش می‌کنم:

Terminal window
cd ~/gitlab/bis
for 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; fi
done
خروجی
قدم 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 اسکریپت تمام قدم‌ها را خودش می‌رود:

Terminal window
cd ~/gitlab/bis
git bisect reset
git bisect start HEAD v1.0 > /dev/null
git bisect run ./test.sh 2>&1 | tail -9
echo "--- گزارش مراحل:"
git bisect log | grep -E "^# (good|bad|first)" | head -10
git bisect reset
git log -1 --format='%h %s'
خروجی
Previous HEAD position was d38e6fa Update notes 7
Switched to branch 'main'
commit 3b03987a9d2e79aebb493fd3719dd6b78aa9453f
Author: 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 clarity
Previous HEAD position was efb88ec Update notes 9
Switched 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) و توضیح:

Terminal window
cd ~/gitlab/rs
echo "--- فایل‌های reflog:"
ls .git/logs .git/logs/refs/heads
echo "--- آخرین خط reflog‌ی HEAD (خام):"
tail -n 1 .git/logs/HEAD | cut -c1-200
echo "--- تنظیم نگه‌داری:"
git config --get gc.reflogExpire || echo "gc.reflogExpire: پیش‌فرض 90 روز (دست‌یافتنی)"
git config --get gc.reflogExpireUnreachable || echo "gc.reflogExpireUnreachable: پیش‌فرض 30 روز"
خروجی
--- فایل‌های reflog:
.git/logs:
HEAD
refs
.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 بعد از آن شیء‌های بی‌صاحب را پاک می‌کند. پس سریع برگردان؛ نه بعد از ماه‌ها.

دستور کار
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

۱) فکر اینکه 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 کن.)

اگر تست گاهی خوب و گاهی بد باشد (flaky)، bisect به commit اشتباه می‌رسد. راه‌حل: اسکریپت تست را قطعی بنویس (چند بار اجرا کن)، و commit های غیرقابل‌آزمایش را skip کن.

Terminal window
cd ~/gitlab/cpc
git switch -q main
git 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 topic
git switch -q release
git cherry-pick main 2>&1 | head -2
خروجی
error: commit d6603fe3ad2c263432e6503272a50c341d18c5a1 is a merge but no -m option was given.
fatal: cherry-pick failed

commit ادغام دو والد دارد و Git نمی‌داند کدام خط را «تغییر» حساب کند. راه‌حل: git cherry-pick -m 1 commit (والد اول اصلی است)، یا بهتر: خود commit های شاخه را جدا cherry-pick کن.

اگر مدام commit ها را بین شاخه‌ها کپی کنی، نسخه‌های تکراری با هش‌های مختلف ساخته می‌شوند و ادغام‌های بعدی گیج می‌شوند. راه‌حل: cherry-pick را برای مورد خاصی مثل backport نگه دار و در حالت عادی merge یا rebase بزن.

✎ تمرینآسان

در یک مخزن چند commit بزن، یک reset --hard HEAD~2 بزن و با git reflog و git reset --hard 'HEAD@{1}' همه را برگردان. ثابت کن تاریخچه مثل قبل شد.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -q
for i in 1 2 3 4; do echo $i > f; git add f; git commit -qm "Commit $i"; done
before=$(git rev-parse HEAD)
git reset -q --hard HEAD~2
git log --oneline | head -1
git reflog | head -2
git reset -q --hard 'HEAD@{1}'
[ "$(git rev-parse HEAD)" = "$before" ] && echo "برگشت: تاریخچه مثل قبل شد"
git log --oneline | head -2
خروجی
32d1fb3 Commit 2
32d1fb3 HEAD@{0}: reset: moving to HEAD~2
48b3857 HEAD@{1}: commit: Commit 4
برگشت: تاریخچه مثل قبل شد
48b3857 Commit 4
723e62c Commit 3
✎ تمرینمتوسط

تمرین اصلی: یک branch پاک‌شده را با reflog برگردان. شاخه‌ی feature/x را با دو commit بساز، با git branch -D پاک کن و فقط با reflog و git branch برگردان. ثابت کن فایل‌ها برگشتند.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2 && git init -q
echo base > base.txt && git add base.txt && git commit -qm "Base"
git switch -qc feature/x
echo "x1" > x.txt && git add x.txt && git commit -qm "Add x"
echo "x2" >> x.txt && git commit -qam "Extend x"
git switch -q main
git branch -D feature/x
echo "--- شاخه رفت؛ reflog:"
git reflog | head -5
h=$(git reflog | grep -A1 "moving from feature/x to main" | tail -1 | awk '{print $1}')
git branch feature/x $h
git switch -q feature/x
cat x.txt
git log --oneline
خروجی
Deleted branch feature/x (was fbd56da).
--- شاخه رفت؛ reflog:
3305612 HEAD@{0}: checkout: moving from feature/x to main
fbd56da HEAD@{1}: commit: Extend x
2894615 HEAD@{2}: commit: Add x
3305612 HEAD@{3}: checkout: moving from main to feature/x
3305612 HEAD@{4}: commit (initial): Base
x1
x2
fbd56da Extend x
2894615 Add x
3305612 Base
✎ تمرینسخت

با git bisect run، اولین commit خراب را خودکار پیدا کن. ۱۵ commit بساز که در یکی از آن‌ها (بدون اینکه بگویی کدام) فایل config.txt مقدار debug=true بگیرد. یک اسکریپت تست بنویس که با debug=true شکست بخورد و bisect را روی آن اجرا کن؛ ثابت کن commit پیدا شده همان است.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -q
echo "debug=false" > config.txt && git add config.txt && git commit -qm "Add config" && git tag good
for i in $(seq 1 6); do echo "n$i" >> log.txt; git add log.txt; git commit -qm "Update log $i"; done
sed -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"; done
cat > check.sh <<'EOF'
#!/bin/sh
! grep -q 'debug=true' config.txt
EOF
chmod +x check.sh
git bisect start HEAD good > /dev/null
git bisect run ./check.sh 2>&1 | grep -E "first bad commit|^[0-9a-f]{40}" | head -2
found=$(git rev-parse --short refs/bisect/bad)
git bisect reset > /dev/null 2>&1
echo "commit مقصر واقعی: $bad ؛ commit پیدا شده توسط bisect: $found"
[ "$bad" = "$found" ] && echo "✓ درست پیدا کرد"
خروجی
70e2cbe80151a527319fe394729e661fdf0d8176 is the first bad commit
bisect found first bad commit
commit مقصر واقعی: 70e2cbe ؛ commit پیدا شده توسط bisect: 70e2cbe
✓ درست پیدا کرد
⚡ بررسی سریع

شاخه‌ای را با «git branch -D» پاک کرده‌ای و می‌خواهی برگردانی. اولین قدم؟

؟ آزمونک
  1. reflog چیست؟

  2. HEAD@{2.hours.ago} یعنی؟

  3. git cherry-pick چه می‌کند؟

  4. git bisect برای چیست و چرا سریع است؟

  5. کدام کار شانس نجات commit های گم‌شده را از بین می‌برد؟

  • 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-found commit های 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-foundcommit های بی‌صاحب
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پایان و بازگشت به شاخه‌ی اول