پروژهٔ ۳ — نجات مخزن
Project 3 — Rescue a damaged repository without losing recoverable work
آخرین پروژه با یک مخزن سالم شروع نمیشود. وقتی وارد میشوی، چند اشارهگر جابهجا شده، یک شاخه حذف شده و همتیمی میگوید کار هفتهٔ قبل از بین رفته. کار تو اول نجاتدادن نیست؛ اول باید جلوی خرابترشدن صحنه را بگیری.
This time, you are not facing a clean-looking log with one missing commit. Several names moved, a branch was deleted, someone moved the remote history backward, and one file was never seen by Git at all. Your mission is not to “undo everything” with one command; first determine what still exists.
مأموریت: پیش از نجات، چیزی را بدتر نکنMission: do not make the incident worse
صبح دوشنبه است و کسی میگوید «همهچیز پرید». این جمله هنوز هیچ تشخیصی نیست. بدون حرکتدادن اشارهگرها مشخص کن چه چیزی در clone اصلی، reflog، پایگاه اشیا، مخزن راه دور و clone همتیمی هنوز وجود دارد.
On Monday morning, a teammate says, “main is empty and last week’s work is gone.” Your git log shows only older commits. But another clone is still on a teammate’s laptop, untouched since Friday. A feature branch was deleted yesterday; commits from before an interactive rebase are no longer on the branch; and a draft someone hoped to recover was never git added.
سؤال وسوسهکننده این است: «کدام فرمان همه را برمیگرداند؟» فعلاً هیچکدام. فصل ۱ Git را بهشکل پایگاه اشیا و چند اشارهگر معرفی کرد؛ فصل ۱۰ نشان داد reflog حرکت اشارهگرهای محلی را ثبت میکند؛ و فصل ۱۶ هشدار داد clone دوم ممکن است تنها نسخهٔ باقیمانده باشد. حالا باید آن سه ایده را در یک رخداد به هم وصل کنی.
The tempting question is, “Which command brings it all back?” Not yet. Chapter 1 introduced Git as an object database plus pointers; Chapter 10 showed that a reflog records local ref movements; Chapter 16 warned that a second clone may be the only remaining copy. Now connect those ideas during one incident.
صحنهٔ رخداد را از نو بسازRecreate the incident scene
برای اینکه این پرونده به شانس وابسته نباشد، یک اسکریپت Python چند مخزن disposable میسازد. Git باید نصب باشد و python در دسترس. اسکریپت مقصد را اگر از قبل وجود داشته باشد رد میکند؛ خودش چیزی را پاک یا جایگزین نمیکند. برای اجرای تازه، نام مقصد تازه بده.
To keep this case reproducible rather than lucky, a small Python script creates disposable repositories. Git and python must be available. The script refuses an existing destination; it never deletes or replaces anything. Use a new destination name for each run.
این اسکریپت فقط پوشهای را میسازد که خودت به آن میدهی و قبل از شروع بررسی میکند که وجود نداشته باشد. آن را برای تمرین اجرا کن، نه روی مخزن واقعی تیم. تا پایان freeze سراغ gc، prune، clean -f یا reset تازه نرو.
The script creates only the path you provide and checks that it does not already exist. Run it for the exercise, never against a real team repository. Until the freeze is complete, avoid gc, prune, clean -f, or another reset.
اسکریپت ساخت وضعیت — بعد از خواندن مأموریت بازش کنIncident generator — open after reading the mission
from pathlib import Path
import subprocess
import sys
def git(repo, *args):
result = subprocess.run(
["git", *map(str, args)], cwd=repo,
text=True, capture_output=True
)
if result.returncode:
raise RuntimeError(result.stderr.strip() or result.stdout.strip())
return result.stdout.strip()
def write(repo, name, content):
path = Path(repo, name)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def commit(repo, message):
git(repo, "add", "-A")
git(repo, "commit", "-m", message)
if len(sys.argv) != 2:
raise SystemExit("usage: python seed_rescue_case.py NEW-DISPOSABLE-PATH")
lab = Path(sys.argv[1]).resolve()
if lab.exists():
raise SystemExit(f"Refusing to overwrite existing path: {lab}")
lab.mkdir(parents=True)
remote = lab / "team.git"
primary = lab / "primary"
team = lab / "teammate"
git(lab, "init", "--bare", str(remote))
git(lab, "--git-dir", str(remote), "symbolic-ref", "HEAD", "refs/heads/main")
git(primary.parent, "init", str(primary))
git(primary, "branch", "-M", "main")
for key, value in (("user.name", "Rescue Learner"),
("user.email", "learner@example.invalid")):
git(primary, "config", key, value)
write(primary, "README.md", "Disposable repository-rescue exercise.\n")
commit(primary, "Create the known starting point")
# Start a two-commit local line before main advances.
git(primary, "switch", "-c", "feature/review")
write(primary, "exports/csv.md", "CSV export draft: columns are name,count.\n")
commit(primary, "Draft CSV export")
write(primary, "exports/json.md", "JSON export draft: preserve names and counts.\n")
commit(primary, "Draft JSON export")
git(primary, "switch", "main")
write(primary, "training-credential.txt",
"TRAINING_FAKE_TOKEN=NOT-A-REAL-CREDENTIAL\n")
commit(primary, "Add synthetic training marker")
write(primary, "docs/decision.md", "Decision: keep exports deterministic.\n")
commit(primary, "Record export decision")
write(primary, "docs/operations.md", "Recovery contact: the project team.\n")
commit(primary, "Add operations note")
good = git(primary, "rev-parse", "HEAD")
git(primary, "remote", "add", "origin", str(remote))
git(primary, "push", "-u", "origin", "main")
git(lab, "clone", str(remote), str(team))
for key, value in (("user.name", "Teammate"),
("user.email", "teammate@example.invalid")):
git(team, "config", key, value)
# A deleted branch whose commit is still in the primary clone.
git(primary, "switch", "-c", "feature/cleanup")
write(primary, "cleanup.md", "A recoverable cleanup note.\n")
commit(primary, "Write cleanup note")
git(primary, "switch", "main")
git(primary, "branch", "-D", "feature/cleanup")
# A commit made while HEAD is detached.
git(primary, "switch", "--detach", good)
write(primary, "detached-note.md", "A recoverable detached-HEAD note.\n")
commit(primary, "Save detached investigation note")
git(primary, "switch", "main")
# Replay the two local commits interactively onto the newer main.
git(primary, "-c", "sequence.editor=:", "rebase", "-i", "main")
# Move main back exactly two commits, then reproduce the mistaken remote update.
git(primary, "switch", "main")
git(primary, "reset", "--hard", "HEAD~2")
git(primary, "push", "--force", "origin", "main")
# This file is created and deleted without ever entering Git.
write(primary, "draft-never-added.md", "This content was never staged.\n")
Path(primary, "draft-never-added.md").unlink()
(lab / "case-notes.txt").write_text(
"Incident notes (outside both repositories):\n"
"- main was moved back by two commits in the primary clone.\n"
"- feature/cleanup was deleted after its commit.\n"
"- a detached-HEAD commit was made, then HEAD moved away.\n"
"- feature/review was interactively rebased; compare old and new lines.\n"
"- the remote main was force-updated; inspect teammate before fetching.\n"
"- training-credential.txt contains a synthetic marker, not a real credential.\n"
"- draft-never-added.md was never staged and is absent from both repositories.\n",
encoding="utf-8"
)
print(f"Disposable case created at: {lab}")
print("Repositories: primary/, teammate/, team.git/; read case-notes.txt first.")در این وضعیت، دو commit آخر main در clone اصلی با reset از دسترس شاخه خارج شدهاند و push اجباری همان عقبگرد را به مخزن راه دور رسانده است. clone همتیمی قبل از push اجباری ساخته شده؛ اما آن را هنوز fetch نکن، چون اول باید از اشارهگر محلیاش نسخهٔ نجات بسازی.
In this state, the primary clone’s last two main commits were moved off the branch by reset, and the force-push carried that rollback to the remote. The teammate clone was made before the force-push; do not fetch there yet, because first preserve its local ref as a rescue point.
قیدها: اول نگهداری، بعد جابهجاییConstraints: preserve first, move later
این پرونده تا وقتی نقشهٔ اولیه را ثبت نکردهای اجازهٔ «تمیزکاری» نمیدهد. فرمانی که اشارهگر را جلو یا عقب میبرد، خودش تشخیص نیست.
This case does not permit cleanup until you have recorded the initial map. A command that moves a ref is not a diagnosis.
| فعلاً انجام ندهDo not do this yet | چرا خطر داردWhy it is risky | قدم امنترSafer next step |
|---|---|---|
git gc / git prune | ممکن است شیء بینامی که هنوز لازم داری حذف شود.An unnamed object you still need may be removed. | اول شناسه را پیدا کن و rescue اشارهگر بساز.Find the ID and create a rescue ref first. |
git clean -f / reset تازه | فایلهای کاری یا سرنخهای باقیمانده را نابود میکند.It can destroy remaining working files or clues. | وضعیت، شاخه، diff و یک کپی جدا را ثبت کن.Record status, refs, diffs, and a separate copy. |
| force-push دوباره | هنوز نمیدانیم کدام خط تاریخچه معتبر است.We do not yet know which history is authoritative. | دو clone و نوک فعلی مخزن راه دور را مقایسه کن.Compare both clones and the remote’s current tip. |
یک کپی از پوشهٔ تمرین بیرون مخزن هم بساز یا دستکم خروجی شناسهها را در پرونده ثبت کن. در مخزن واقعی، از همتیمیها بخواه تا پایان بررسی push نکنند. این freeze موقت است تا شواهد جابهجا نشوند؛ هنوز چیزی را اصلاح نکردهای.
Make a copy of the exercise directory outside the repositories, or at minimum record the observed IDs in incident notes. In a real repository, ask teammates to pause pushes until the investigation is complete. This temporary freeze keeps evidence still; it has repaired nothing.
مرحلهٔ اول: عکس فوری از وضعیت بگیرPhase one: take a snapshot of the current state
در primary با سادهترین پرسش شروع کن: الان کجا ایستادهام و چه چیزی دستنخورده مانده؟ خروجی را در پروندهٔ بیرون مخزن کپی کن. این ثبت کمک میکند بفهمی فرمانهای بازیابی چه چیزی را عوض کردند.
Start in primary with the simplest question: where am I, and what remains untouched? Copy the output into notes outside the repository. That record lets you tell later what your recovery commands changed.
git status --short --branch
git branch -avv
git rev-parse --show-toplevel
git rev-parse HEAD
git remote -vبعد شکل تاریخچه و دفترچهٔ حرکت اشارهگرها را جداگانه ببین. --all لازم است چون log عادی فقط از نوکهای مشخصشده شروع میکند؛ reflog هم فقط در همین clone شاهد محلی است.
Next inspect the graph and ref journal separately. --all matters because ordinary log starts only from selected tips; a reflog is local evidence from this clone only.
git log --all --graph --decorate --oneline
git reflog show --all --date=iso
git show-ref --headدر clone دوم هنوز fetch نکن. همان وضعیت محلیاش را ثبت کن، بهخصوص شناسهٔ main و origin/main. ممکن است fetch اشارهگر ردیابی مخزن راه دور را جابهجا کند؛ قبل از آن یک نام نجات پایدار میسازیم تا مقایسه به حافظه وابسته نباشد.
Do not fetch in the second clone yet. Record its local state, especially the IDs of main and origin/main. Fetch may move the remote-tracking ref; first create a durable rescue name so the comparison does not depend on memory.
git status --short --branch
git rev-parse HEAD main origin/main
git log --all --graph --decorate --oneline
git remote -vgit rev-parse شناسهٔ commitی را میدهد که یک اشارهگر در همین لحظه نام میبرد؛ ثابت نمیکند فایلهای بیرون از Git امناند. reflog حرکت ثبتشدهٔ اشارهگر را نشان میدهد؛ تضمین نمیکند شیء برای همیشه میماند. مخزن راه دور هم مخزنی جداست؛ از روی clone محلی برایش reflog فرض نکن.
git rev-parse identifies the commit a ref names now; it says nothing about files outside Git. A reflog shows recorded ref movement; it does not promise an object will remain forever. The remote is separate—do not assume its reflog is available from your clone.
reflog ثبت میکند اشارهگر از M2 به S رفته است. وقتی شناسهٔ M2 را پیدا کردی، rescue شاخه آن را از یک سرنخ به یک نام قابلدسترسی تبدیل میکند؛ اما reflog سرور از اینجا قابل مشاهده نیست.Diagram 2 — The reflog records that a ref moved from M2 to S. Once you identify M2, a rescue branch turns it from a clue into a reachable name; a server reflog is not visible from here.مرحلهٔ دوم: نقشهٔ «چه چیزی هنوز هست؟» بسازPhase two: map what still exists
فقط log اصلی را نگاه نکن. برای هر نشانه بپرس: آیا اشارهگر هنوز commit را نام میبرد؟ اگر نه، آیا reflog یا clone دوم آن را نگه داشته؟ اگر هیچ نامی پیدا نشد، شیء در پایگاهداده باقی مانده؟ این تفاوت، فرق «نام گم شده» با «خود داده پیدا نمیشود» است.
Do not stare only at the ordinary log. For each clue, ask: does a ref still name the commit? If not, does a reflog or second clone retain it? If no name appears, might the object still be in the database? That is the difference between a lost name and data that cannot be found.
| نشانهClue | جای جستوجوWhere to look | اگر پیدا شدIf found | محدودیتLimit |
|---|---|---|---|
main عقب رفته | reflog clone اصلی | شناسهٔ نوک قبلی را به rescue اشارهگر وصل کن.Anchor the old tip with a rescue ref. | ثبت محلی است و ممکن است منقضی شود.It is local and may expire. |
| شاخه حذف شده | reflog --all، سپس fsck | commit نوک شاخه را پیدا و نامگذاری کن.Find and name the branch-tip commit. | حذف اشارهگر مساوی حذف فوری شیء نیست.Deleting a ref does not instantly delete its objects. |
| commit در detached HEAD | HEAD reflog یا objectهای unreachable | commit درست را بررسی و rescue شاخه بساز.Inspect the candidate, then create a rescue branch. | هر dangling commit الزاماً کار گمشده نیست.Not every dangling commit is lost work. |
| تاریخچهٔ remote عقب رفته | clone همتیمی و ls-remote | نوک سالم را نگه دار؛ فقط پس از مقایسه ترمیم کن.Preserve the good tip; repair only after comparison. | Git سرور را از reflog محلی حدس نمیزند.A local reflog cannot reveal the server’s history. |
| فایل stageنشده و حذفشده | بیرون Git: backup، سطل زباله، نسخههای سیستم | منبع بیرونی را بررسی کن؛ ادعای بازیابی Git نکن.Check external backups; do not claim Git recovery. | هیچ blobی ساخته نشده که Git برگرداند.No blob was created for Git to restore. |
اگر اشارهگر مناسب پیدا نکردی، بررسی شیءها کمک میکند. فرمان اول وضعیت معمول پایگاه اشیا را میسنجد؛ فرمان دوم عمداً reflogها را از ریشههای دسترسی کنار میگذارد تا commitهایی را نشان دهد که شیءشان هست، اما از اشارهگر فعلی reachable نیست.
If no suitable ref appears, inspect the object database. The first command checks repository integrity; the second deliberately excludes reflogs as reachability roots so it can list commits whose objects exist but are not reachable from current refs.
git fsck --full
git fsck --full --no-reflogs --unreachableدر خروجی دوم، unreachable commit <id> میگوید شیء وجود دارد اما از ریشههایی که این اجرا بررسی کرده reachable نیست. این بهتنهایی نمیگوید کدام commit مهم است؛ پیام commit، tree و فایلهای داخلش را بخوان. missing یا hash mismatch مسئلهٔ دیگری است. تا وقتی گزینهٔ آزمایشی را ارزیابی نکردهای --lost-found یا پاکسازی لازم نیست.
In the second output, unreachable commit <id> means the object exists but is not reachable from the roots considered by this run. That alone does not tell you which commit matters; inspect its message, tree, and files. missing or hash mismatch is a different finding. You do not need --lost-found or cleanup before evaluating candidates.
مراحل مأموریت: از سرنخ تا ترمیم قابلدفاعMission milestones: from clues to a defensible repair
هر حرکت باید از یک مدرک بیاید. اگر نتوانی بگویی این شناسه را کجا پیدا کردی و چرا اشارهگر نجات را به آن وصل کردی، هنوز recovery نکردهای؛ فقط تاریخچه را دوباره جابهجا کردهای.
Each milestone should produce new evidence. If you can only list commands but cannot explain why a destination is safe, you have not rescued anything; you have merely moved refs.
۱. نوک سالم main را در هر clone پیدا کن1. Find the good main tip in each clone
در primary، reflog را با پیامها و شناسهها بخوان؛ در teammate، قبل از fetch از main یک rescue شاخه بساز و hash آن را ثبت کن. شناسهها را با git show --stat <id> و git show <id>:docs/operations.md بسنج. شباهت پیام commit کافی نیست؛ tree و محتوای مورد انتظار را هم بررسی کن.
In primary, read the reflog with messages and IDs; in teammate, create a rescue branch from main before fetching and record its hash. Assess IDs with git show --stat <id> and git show <id>:docs/operations.md. A familiar commit message is not enough; inspect its tree and expected content too.
۲. برای هر commit قابلبازیابی یک نام نجات بساز2. Give each recoverable commit a rescue name
قبل از تعویض شاخهها، نوک main قدیمی، commit شاخهٔ حذفشده، commit جداشده و نوک قدیمی خط rebase را در اشارهگرهای محلی جداگانه نگه دار؛ مثلاً rescue/main-before-reset و rescue/detached. هر نام باید به شیء شناسهی اشاره کند که خودت بررسی کردهای. یک hash مبهم را کورکورانه شاخه نکن.
Before switching branches again, preserve the former main tip, deleted branch commit, detached commit, and old rebase-line tip under separate local refs such as rescue/main-before-reset and rescue/detached. Each name must point to an object ID you inspected; do not branch blindly from an unexplained hash.
۳. دو خط rebase را مقایسه کن، نه اینکه یکی را حدس بزنی3. Compare the two rebase lines instead of guessing
commitهای پیش و بعد از rebase شناسههای متفاوت دارند، حتی اگر patchها همارز باشند. از git range-diff <old-base>..<old-tip> <new-base>..<new-tip> استفاده کن و یک فایل نمونه را از هر دو خط با git show بخوان. هدف این نیست هر دو را وارد main کنی؛ هدف این است خط قبلی را از دست ندهی و اثر بازپخش را بفهمی.
Commits before and after rebase have different IDs even when their patches are equivalent. Use git range-diff <old-base>..<old-tip> <new-base>..<new-tip>, then inspect a representative file from each line with git show. The goal is not to merge both lines into main; it is to preserve the old line and understand the replay.
۴. مخزن راه دور را فقط با یک پیشروی عادی ترمیم کن4. Repair the remote with a normal forward update
در clone همتیمی، بعد از ساخت rescue اشارهگر، وضعیت مخزن راه دور را fetch کن و git ls-remote origin refs/heads/main را ثبت کن. گراف باید نشان دهد نوک فعلی مخزن راه دور جدّ نوک سالمی است که در rescue اشارهگر نگه داشتی. فقط اگر این رابطه fast-forward است و تیم تأیید کرده بین این دو نقطه commit معتبر دیگری نیامده، نوک سالم را با push معمولی برگردان. اگر push رد شد، توقف کن؛ push اجباری را جایگزین بررسی نکن.
In teammate, after creating a rescue ref, fetch and record git ls-remote origin refs/heads/main. The graph must show the current remote tip as an ancestor of the good tip in your rescue ref. Only if that is a fast-forward and the team confirms no valid commit arrived in between should you restore it with a normal push. If the push is rejected, stop; do not substitute a force-push for investigation.
۵. نشانگر ساختگی را با اعتبارنامه واقعی یکی نگیر5. Do not confuse a synthetic marker with a real credential
فایل training-credential.txt عمداً فقط یک مقدار ساختگی دارد. از نوک نهایی آن را حذف کن و ثابت کن دیگر در tree جاری نیست؛ اما گزارش کن که commit قدیمی هنوز آن را نگه داشته است. در رخداد واقعی، اولین کار باطل یا rotate کردن اعتبارنامه است. حذف از تاریخچه فقط کاری تکمیلی و هماهنگشده است؛ cloneها، forkها و cacheهای قبلی را جادویی پاک نمیکند.
The file training-credential.txt contains only a deliberately synthetic value. Remove it from the final tree and prove it is no longer present there, but report that an older commit still contains it. In a real incident, first revoke or rotate the credential. History cleanup is a coordinated follow-up; it cannot magically erase old clones, forks, or caches.
۶. برای فایلِ هرگز ناحیه آمادهسازینشده، مرز را صادقانه بنویس6. State the boundary honestly for a file never staged
دربارهٔ draft-never-added.md مدرک پرونده میگوید فایل قبل از پاکشدن هیچوقت ناحیه آمادهسازی نشده بود. پس شیء قابلبازیابی در Git نساختهای. اگر نسخه پشتیبان سیستم یا نسخهبندی filesystem وجود دارد، آن مسیر را جدا بررسی کن؛ اما «Git نجاتش میدهد» را در گزارش ننویس.
The case notes say draft-never-added.md was never staged before deletion. Therefore Git has no recoverable object for it. Check system backups or filesystem versioning separately if available, but do not write “Git can recover it” in your report.
شرط قبولی: هر نجات باید شاهد داشته باشدAcceptance: every rescue needs evidence
در این پروژه «فایل برگشت» معیار کافی نیست. هر نجات باید شاهد داشته باشد: commit دقیق، مسیری که از آن پیدایش کردی و بررسیای که ثابت میکند نتیجه درست است. اگر چیزی هرگز در Git ذخیره نشده، همین را صریح بنویس؛ ادعای بازیابی بخشی از پروژه نیست.
The job is not done merely because main looks familiar again. Keep every intentionally recoverable commit under clear refs, make the final line agree in both clones, and state which file lies outside Git’s guarantee.
git status --short --branch
git fetch origin
git log --all --graph --decorate --oneline
git rev-parse main
git rev-parse origin/main
git branch --list 'rescue/*'
git fsck --full
git grep -n -E '^(<<<<<<<|=======|>>>>>>>)' main --اگر فرمان آخر نشانگر تعارضی پیدا نکند، git grep معمولاً کد خروجی ۱ میدهد؛ اینجا یعنی «تطبیقی نبود»، نه خرابی مخزن. از پیام dangling در fsck نترس؛ برای این پرونده مهم است خطای missing یا hash mismatch نداشته باشی. هر دو clone باید بعد از fetch شناسهٔ یکسانی برای main و origin/main نشان دهند.
If the final command finds no conflict marker, git grep normally exits with code 1; here that means “no match,” not repository failure. Do not panic at an fsck dangling notice; this case requires no missing or hash mismatch errors. After fetch, both clones should show the same ID for main and origin/main.
| تحویلDeliverable | چه چیزی باید داخلش باشدWhat it must contain | مدرکProof |
|---|---|---|
| Incident report | رخداد، علت، تصمیم و مرز بازیابی؛ جدا برای هر مورد.Symptom, cause, decision, and recovery boundary for each case. | شناسهها و فرمانهای مشاهدهشده، نه حدس.Observed IDs and commands, not guesses. |
| Rescue refs | main قبلی، شاخهٔ حذفشده، جداشده نوک شاخه و خط قدیمی rebase.Old main, deleted branch, detached tip, and old rebase line. | git show-ref یاor git branch --list 'rescue/*' |
| Remote + two clones | main ترمیمشده و clone همتیمی که کارش محفوظ مانده.Repaired main and teammate work still preserved. | شناسهٔ main برابر در هر دو clone.Matching main IDs in both clones. |
| Repository checks | درخت نهایی تمیز، نشانگر تعارض صفر، بدون خطای missing در fsck.Clean final tree, no conflict markers, no fsck missing-object error. | خروجی دستورهای پذیرش و گراف نهایی.Acceptance output and final graph. |
| Secret + missing file | نشانگر ساختگی از tree نهایی بیرون؛ فایل هرگز ذخیرهنشده با مرز درست گزارش شده.Synthetic marker removed from final tree; never-stored file reported honestly. | جستوجوی tree نهایی و یادداشت جدا دربارهٔ تاریخچه.Search of the final tree and a separate history note. |
فایل گزارش را بیرون مخزن بنویس تا خود گزارش با رخداد قاطی نشود. برای هر پرونده به زبان خودت ثبت کن چه دیدی، کدام شاهد تصمیمت را عوض کرد، علت چه بود، چه اشارهگر یا فایلی را چطور نگه داشتی، و چه چیزی هنوز قابلاثبات یا قابلبازیابی نیست.
Write the report outside the repository so the report itself does not become part of the incident. For each case, record what you observed, which evidence changed your decision, the cause, how you preserved the ref or file, and what remains unprovable or unrecoverable.
نردبان راهنمایی: فقط بهاندازهای باز کن که لازم داریHint ladder: reveal only what you need
پلهٔ ۱ — چیزی را جابهجا نکنStep 1 — do not move anything yet
راهنماها عمداً مثل نردباناند. اول سرنخ کلی میگیری، بعد محل ذخیره، بعد ابزار و در آخر دستور دقیق. اگر از همان اول پلهٔ آخر را باز کنی، پروژه تبدیل میشود به کپیکردن دستور و تمام هدف اتاق نجات از بین میرود.
Record status, HEAD, branches, graph, reflog, and remote in primary. Capture teammate’s main ID before fetching too. None of these observations creates a commit.
پلهٔ ۲ — نام و شیء دو چیزندStep 2 — a name and an object are different things
شاخهٔ حذفشده یعنی اشارهگر نامدارش حذف شده؛ نه اینکه همان لحظه همهٔ شیءهای commit حذف شده باشند. hash هر گزینهٔ آزمایشی را با پیام و فایلهایش بررسی کن.
A deleted branch means its named ref was removed; it does not mean every related commit object vanished immediately. Inspect each candidate hash by message and content.
پلهٔ ۳ — دفترچهها محلیاندStep 3 — journals are local
git reflog show --all را در primary بخوان. برای push اجباری مخزن راه دور از clone همتیمی استفاده کن؛ دنبال reflog جادویی روی میزبان نگرد.
Read git reflog show --all in primary. For the remote force-push, use the teammate clone; do not hunt for a magical host reflog.
پلهٔ ۴ — اشارهگر نجات قبل از fetch و ترمیمStep 4 — rescue refs before fetching and repairing
از main محلی teammate یک شاخهٔ rescue بساز. بعد fetch کن، گراف را بخوان و رابطهٔ رابطهٔ والدها را بررسی کن. فقط یک push fast-forward میتواند مخزن راه دور را بدون بازنویسی دیگری جلو ببرد.
Create a rescue branch from teammate’s local main. Then fetch, read the graph, and check ancestry. Only a fast-forward push advances the remote without rewriting it again.
پلهٔ ۵ — فرمانهای دقیق، وقتی آمادهایStep 5 — exact commands, when ready
راهحل مرجع پایین صفحه است. قبل از بازکردنش hashهای هر رخداد را در گزارش بنویس و مطمئن شو میدانی هر hash از کجا آمده.
The reference solution is near the end. Before opening it, write each incident’s hash in your report and note exactly where it came from.
راهحل مرجع — بعد از اینکه خودت نقشه را ساختیReference solution — after you have built your own map
شناسههای commit در هر اجرا فرق میکنند؛ پس <M2> یا <detached-tip> را با مقدار خودت جایگزین کن. این عبارتها placeholder هستند و نباید عیناً در ترمینال اجرا شوند. اگر پیام یا tree اطراف commit با روایت پرونده جور نیست، آن hash را مصرف نکن.
Commit IDs differ on every run, so replace <M2> and <detached-tip> with your own values. These are placeholders, not literal terminal input. If the message or tree does not fit the case, do not use that hash.
بازکردن مسیر پیشنهادی بازیابیOpen the suggested recovery path
الف) در primary شناسهها را پیدا و لنگر کنA) Find and anchor the IDs in primary
ابتدا سرنخها را کنار هم ببین. reflog را با شناسهٔ کامل، پیام و نام اشارهگر بخوان؛ برای هر سطر محتمل یک show جدا اجرا کن تا هم پیام و هم محتوا را بررسی کنی.
First compare the clues. Read the reflog with full IDs, messages, and ref names; for every candidate line, run a separate show to verify both message and content.
git reflog show --all --format='%H %gD %gs'
git show --stat <M2>
git show <deleted-tip>:cleanup.md
git show <detached-tip>:detached-note.md
git log --all --graph --decorate --onelineوقتی گزینهٔ آزمایشی را تأیید کردی، برای هر نوک شاخه یک نام rescue بساز. نام rescue کار را ادغام نمیکند؛ فقط شیء را زیر اشارهگر پایدار و قابلمشاهده نگه میدارد. نوک قدیمی خط rebase را هم جدا نگه دار تا با خط تازه اشتباه نشود.
Once a candidate is verified, give each tip a rescue name. A rescue name does not integrate the work; it keeps the object reachable through a stable, visible ref. Preserve the old rebase tip separately so it cannot be confused with the rewritten line.
git branch rescue/main-before-reset <M2>
git branch rescue/deleted-feature <deleted-tip>
git branch rescue/detached-commit <detached-tip>
git branch rescue/rebase-before <old-rebase-tip>
git branch --list 'rescue/*'
git show-refاین اشارهگرها محلیاند؛ هنوز آنها را push نکن. شاخهٔ feature/review خط بازنویسیشده را نگه میدارد. حالا patchهای دو خط را مقایسه کن؛ اگر range-diff یک patch را حذف یا عوضشده نشان داد، همان را دستی بررسی کن.
These refs are local; do not push them yet. feature/review retains the rewritten line. Compare both patch series; if range-diff shows a dropped or changed patch, inspect that item manually.
git range-diff <original-base>..rescue/rebase-before rescue/main-before-reset..feature/reviewب) مخزن راه دور را از clone دوم، با push معمولی ترمیم کنB) Repair the remote from the second clone with a normal push
در teammate، قبل از fetch از وضعیت محلی اشارهگر نجات بساز. بعد از fetch، نوک مخزن راه دور و نوک نجات را مقایسه کن. اگر گراف نشان داد نوک فعلی مخزن راه دور جدّ نوک نجات است، push معمولی باید fast-forward را بپذیرد؛ اگر نپذیرفت، فرض ما غلط است یا کار تازهای رسیده.
In teammate, create a rescue ref from its local state before fetching. Then compare the remote and rescue tips. If the graph shows the remote tip is an ancestor of the rescue tip, a normal push should fast-forward; if it does not, our assumption is wrong or new work arrived.
git branch rescue/team-main main
git rev-parse rescue/team-main
git fetch origin
git log --all --graph --decorate --oneline
git merge-base --is-ancestor origin/main rescue/team-main
git push origin rescue/team-main:refs/heads/main
git ls-remote origin refs/heads/mainاگر آزمون رابطهٔ والدها کد خروجی صفر داد، یعنی نوک فعلی مخزن راه دور جدّ نوک سالم است؛ refspec هم مقصد را صریح میگوید. بعد از push، ls-remote باید شناسهٔ rescue را نشان دهد. این مجوز push یک نوک دیگر نیست؛ کار تازهای که بعد از عکس فوری رسیده باید جداگانه بررسی شود.
An ancestry check that exits zero means the current remote tip is an ancestor of the good tip; the refspec states the destination explicitly. After pushing, ls-remote should show the rescue ID. This does not authorize pushing another tip; work created after the snapshot needs separate review.
پ) primary را fast-forward کن و clone دوم را همگام کنC) Fast-forward primary and synchronize the second clone
در primary، main فعلی روی S است. بعد از fetch، origin/main باید به M2 ترمیمشده رسیده باشد. فقط با پوشهٔ کاری تمیز و گراف تأییدشده، main را جلو ببر. بعد نشانگر ساختگی را از نسخهٔ جاری بردار و commit تازه بساز؛ commit قدیمی عمداً پاک نشده و در rescue تاریخچه قابلمشاهده میماند.
In primary, current main is at S. After fetching, origin/main should be restored to M2. Move main forward only with a clean working tree and a graph that confirms the fast-forward. Then remove the synthetic marker from the current version in a new commit; the old commit is intentionally not erased and remains visible in rescue history.
git fetch origin
git switch main
git merge --ff-only origin/main
git show main:docs/operations.md
git rm training-credential.txt
git commit -m "Remove synthetic training marker from current tree"
git push origin mainفقط فایل را از نوک شاخه جاری حذف کردیم؛ secret واقعی با این کار امن نمیشود. در تمرین بررسی کن نشانگر در tree نهایی پیدا نشود، اما در rescue نوک شاخه قدیمی هنوز قابلدیدن باشد. کد خروجی ۱ از جستوجوی بینتیجه یعنی تطبیقی پیدا نشده است. اگر مقدار واقعی بود، اعتبارنامه را اول باطل میکردیم و بعد دربارهٔ بازنویسی تاریخچه و هماهنگی cloneها تصمیم میگرفتیم.
We removed only the file from the current tip; that would not secure a real secret. Prove the marker is absent from the final tree but still visible in the old rescue tip. Exit code 1 from a search with no output means there was no match. If it were real, revoke the credential first, then decide whether coordinated history rewriting and clone cleanup are warranted.
git grep -n 'TRAINING_FAKE_TOKEN' main --
git show rescue/main-before-reset:training-credential.txt
git rev-parse main origin/mainدر teammate هم fetch کن و فقط وقتی main از وضعیت سالم قبلی عقب نیست، با --ff-only جلو بیا. شناسهها را در هر دو clone مقایسه کن؛ اسم شاخهٔ یکسان بهتنهایی هماهنگی را ثابت نمیکند.
Fetch in teammate too, and advance with --ff-only only if its main is not behind the preserved good state. Compare IDs in both clones; matching branch names alone do not prove synchronization.
git fetch origin
git switch main
git merge --ff-only origin/main
git rev-parse main origin/main
git status --short --branchت) بررسی نهایی، بدون تمیزکاری شتابزدهD) Final checks, without rushed cleanup
در primary، گراف را با همهٔ اشارهگرها ثبت کن. بعد fsck را اجرا و خطاهای واقعی را از گزارش شیءهای unreachable جدا بخوان. اشارهگرهای rescue/* را فعلاً نگه دار؛ حذفشان کار نگهداری جداگانه است، نه بخشی از نجات همین صبح.
In primary, record the graph with all refs. Then run fsck and distinguish actual errors from unreachable-object notices. Keep rescue/* for now; removing them is separate maintenance, not part of this morning’s rescue.
git status --short --branch
git log --all --graph --decorate --oneline
git show-ref
git fsck --full
git grep -n -E '^(<<<<<<<|=======|>>>>>>>)' main --نمودار نهایی: خط اصلی و شاخههای نجاتFinal graph: main line and rescue refs
در پایان، همهٔ رخدادها نباید وارد main شوند. کار شاخههای rescue/* این است commitهای قابلاستفاده را نامدار نگه دارند؛ کار main نگهداشتن خط محصولیِ مورد توافق تیم است. این دو هدف را در نمودار و گزارش جدا نشان بده.
Not every incident commit belongs on main. The rescue/* branches keep recoverable commits named; main holds the product line the team agreed to restore. Show those goals separately in the graph and report.
گزارش رخداد: از فرم پرکنی دوری کنIncident report: explain the decisions, do not fill a form blindly
گزارش رخداد قرار نیست فرم اداری باشد. برای هر مشکل یک داستان کوتاه بنویس: چه دیدی، چه چیزی هنوز وجود داشت، چه تصمیمی گرفتی و با چه بررسیای مطمئن شدی. اگر مرزی وجود داشت که Git از آن طرف دیگر چیزی نمیدانست، همانجا روشنش کن.
A useful report is written for the teammate taking over tomorrow. For each incident, first tell the short story: what was observed, and why did someone think data was gone? Then quote the decisive output, explain the decision it supported, and repeat one check to show the result still holds.
| پروندهCase | علتی که باید ثابت کنیCause to establish | مرز بازیابی را هم بنویسState the recovery boundary too |
|---|---|---|
main دو commit عقب رفت | reset محلی نوک شاخه را عوض کرد؛ مخزن راه دور هم force-update شد.A local reset moved the branch tip; the remote was force-updated too. | reflog primary و clone همتیمی دو شاهد جدا هستند.Primary reflog and teammate clone are separate evidence sources. |
| شاخهٔ حذفشده / detached commit | نام اشارهگر از دسترس خارج شد، ولی commit در clone پیدا شد.The ref name disappeared, but the commit was found in a clone. | بازیابی تا وقتی شیء موجود است و اشارهگر نجات ساخته میشود ممکن است.Recovery is possible while the object remains and a rescue ref is created. |
| خط پیشین rebase | بازپخش commitهای تازه با شناسههای تازه ساخت.Replay created new commits with new IDs. | همارزی patch را بررسی کردیم؛ دو خط را بیدلیل merge نکردیم.Patch equivalence was checked; we did not merge both lines blindly. |
| نشانگر ساختگی credential | مقدار آموزشی بود و اعتبارنامه واقعی نبود.It was a training marker, not a real credential. | از tree جاری حذف شد؛ نسخهٔ قدیمی هنوز در تاریخچه هست.Removed from current tree; old history still contains it. |
| پیشنویس هرگز stageنشده | هیچ شیءی وارد مخزن نشد.No object ever entered the repository. | Git راه بازیابی تضمینشده ندارد؛ نسخه پشتیبان بیرونی شاید کمک کند.Git has no guaranteed recovery path; an external backup may help. |
بخش آخر گزارش را «چه چیزی را نمیدانیم؟» بنام. اگر clone قدیمیای پیدا نکردی، reflog منقضی شده بود، یا شیءی واقعاً وجود نداشت، همان را صریح بنویس. یک گزارش صادقانه با مرز روشن از ادعای «همهچیز برگشت» حرفهایتر است.
Call the final part of the report “What do we not know?” If no old clone was found, a reflog had expired, or an object was truly absent, say so plainly. An honest report with a clear boundary is more professional than claiming “everything is back.”
تحویل نهایی: مدرک را نگه دار، بعد پرونده را ببندFinal handoff: keep the evidence, then close the case
آخر این مسیر قرار نیست همهٔ دستورهای Git را حفظ باشی. کافی است وقتی چیزی عجیب شد بتوانی چهار سؤال را جواب بدهی: چه شیئی وجود دارد؟ چه اسمی به آن اشاره میکند؟ چه چیزی آن اسم را جابهجا کرده؟ و آیا داده اصلاً زمانی وارد Git شده بود؟ وقتی این چهار سؤال را داری، Git دیگر جادو نیست.
Before later cleanup, save the final graph, rescue-ref IDs, main IDs from both clones, fsck output, and incident report outside the repository. Removing rescue refs early or discarding the second clone could destroy your only evidence. Coordinate removal of temporary refs with the repository owner; it is outside this mission.
حالا برگرد به جایی که این مسیر از آن شروع شد: در Git نام شاخه فقط یک اشارهگر است، اما commit، tree و blob چیزهایی هستند که آن اشارهگرها به هم وصل میکنند. وقتی مخزن بههم میریزد، از خودت بپرس: کدام شیء هنوز وجود دارد؟ کدام نام به آن اشاره میکند؟ چه چیزی آن نام را جابهجا کرد؟ و Git چه چیزی را هیچوقت ذخیره نکرد؟ اگر بتوانی با مدرک به این چهار پرسش جواب بدهی، Git دیگر جادو نیست؛ حتی وسط یک صبح خراب هم میتوانی قدم بعدی را آرام انتخاب کنی.
Now return to where this course began: a branch name is only a pointer, while commits, trees, and blobs are the objects those pointers connect. When a repository is damaged, ask: Which object still exists? Which name points to it? What moved that name? And what did Git never store? If you can answer those four questions with evidence, Git is no longer magic; even on a rough morning, you can choose the next step calmly.
مراجع رسمی برای جزئیاتOfficial references for exact behavior
اینها برای وقتیاند که خروجی نسخهٔ Git تو با مثال فرق دارد یا میخواهی رفتار دقیق یک گزینه را بررسی کنی؛ نه اینکه جای مشاهدهٔ مخزن خودت را بگیرند.
Use these when your Git version behaves differently from an example or you need exact option semantics; they do not replace inspecting your own repository.