کدنامهمرجع‌های مهندسی نرم‌افزار، به فارسی
Git · پروژهٔ نهایی ۳Git · Final project 3

پروژهٔ ۳ — نجات مخزن

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.

۶رخدادincidents
۵نمودارdiagrams
۱گزارش رخدادincident report

مأموریت: پیش از نجات، چیزی را بدتر نکن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.

The damaged repository has three different evidence locationsThe primary clone has moved refs and local reflogs; the bare remote main points backward; the teammate clone still has the previous good tip. A separate unsaved draft was never stored by Git. primary clonemain → older commitreflog · deleted / rewritten tipsunreachable objects may remainbare remotemain was force-updatedno guaranteed server reflogteammate clonelocal main → previous good tipnot yet fetched after incident bad pushclone retained draft-never-added.md · deleted before the incident · Git never stored it
نمودار ۱ — clone اصلی، مخزن راه دور و clone هم‌تیمی سه مخزن جدا با اشاره‌گرها و شیءهای خودشان‌اند. پیکان‌ها رخداد push اجباری و باقی‌ماندن نسخه در clone دوم را نشان می‌دهند؛ فایل پایین بیرون از Git است.Diagram 1 — The primary clone, remote, and teammate clone are separate repositories with their own refs and objects. The arrows show the force-push and the copy retained in the second clone; the draft below is outside Git.

صحنهٔ رخداد را از نو بساز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.

مرز امنSafety boundary

این اسکریپت فقط پوشه‌ای را می‌سازد که خودت به آن می‌دهی و قبل از شروع بررسی می‌کند که وجود نداشته باشد. آن را برای تمرین اجرا کن، نه روی مخزن واقعی تیم. تا پایان 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
seed_rescue_case.py · save outside the destination folder
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.

in primary/ · inspection only
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.

read the graph, then the local movement journal
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.

+
in teammate/ · capture before fetch
git status --short --branch
git rev-parse HEAD main origin/main
git log --all --graph --decorate --oneline
git remote -v
خروجی چه چیزی را ثابت می‌کند؟What does this output prove?

git 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.

A reflog records ref movement and a rescue ref preserves the old tipThe main pointer moves from M2 back to S. The local reflog records the old and new IDs; creating a rescue branch at M2 gives that commit a durable name. SM1M2main nowold main tip rescue/main-before-reset → M2 local reflog: main moved M2 → Sa clue to the old ID, not a permanent backup
نمودار ۲ — 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، سپس fsckcommit نوک شاخه را پیدا و نام‌گذاری کن.Find and name the branch-tip commit.حذف اشاره‌گر مساوی حذف فوری شیء نیست.Deleting a ref does not instantly delete its objects.
commit در detached HEADHEAD reflog یا objectهای unreachablecommit درست را بررسی و 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.

inspection · run in primary/, before making rescue refs
git fsck --full
git fsck --full --no-reflogs --unreachable
unreachable به معنی خراب نیستUnreachable does not mean corrupt

در خروجی دوم، 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.

Deleting a branch removes its name, not necessarily its commit objectBefore deletion a branch ref points to commit D1. After deletion that ref is absent; D1 and its ancestors may still exist as objects, with a reflog or second reference providing clues. A rescue branch makes D1 reachable again. feature/deleted → D1a ref names the tipbranch name removedref no longer existsrescue/deleted → D1new ref anchors it D1 + tree + blobs may still exist as Git objectsreflog / second clone / fsck are clues, not permanent guarantees
نمودار ۳ — حذف شاخه در درجهٔ اول نام را حذف می‌کند. شیءهای commit و tree ممکن است هنوز در مخزن باشند؛ وقتی نوک درست را پیدا کردی، rescue اشاره‌گر آن را دوباره reachable می‌کند. «ممکن است» را با تضمین اشتباه نگیر.Diagram 3 — Deleting a branch primarily removes a name. Its commit and tree objects may still exist; once you identify the right tip, a rescue ref makes it reachable again. Do not turn “may” into a guarantee.

مراحل مأموریت: از سرنخ تا ترمیم قابل‌دفاع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.

A teammate clone can restore a force-updated remote with a safe fast-forwardCommits S, M1, and M2 form a linear history. After the bad force push, the bare remote main points to S while the teammate local main still points to M2. A normal push can move the remote forward if S is an ancestor of M2 and no legitimate update intervened. SM1M2futurecurrent remote mainteammate rescue/main normal push moves remote main: S → M2 Proceed only after proving ancestry and checking for intervening work.
نمودار ۴ — بعد از fetch، مخزن راه دور روی S است ولی clone هم‌تیمی نوک سالم M2 را نگه داشته. اگر S جدّ M2 باشد، push معمولی می‌تواند اشاره‌گر سرور را جلو ببرد؛ این نمودار مجوز push بی‌بررسی یا تضمین نداشتن کار تازه نیست.Diagram 4 — After fetch, the remote is at S while the teammate clone retains M2. If S is an ancestor of M2, a normal push can move the server ref forward; the diagram is not permission to push without checking or proof that no newer work exists.

۵. نشانگر ساختگی را با اعتبارنامه واقعی یکی نگیر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.

run in both clones after repair
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 refsmain قبلی، شاخهٔ حذف‌شده، جداشده نوک شاخه و خط قدیمی rebase.Old main, deleted branch, detached tip, and old rebase line.git show-ref یاor git branch --list 'rescue/*'
Remote + two clonesmain ترمیم‌شده و 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.

in primary/ · substitute only verified IDs
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.

create local rescue branches from verified tips
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.

compare old and rewritten patch series
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.

in teammate/ · preserve, inspect, then update
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.

in primary/ · fast-forward, then remove the synthetic file
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.

current tree versus preserved historical commit
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.

in teammate/ · update and prove agreement
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.

final evidence in primary/
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.

The repaired repository preserves main and all recoverable incident tipsMain advances from base through the synthetic marker and two good commits to a cleanup commit. Rescue refs preserve the deleted branch and detached commit from M2, the pre-rebase line from base, and the rewritten review line from M2. C0SM1M2Fbasefake markergood changegood tipmain D1rescue/deleted X1rescue/detached R1R2rescue/rebase-before R1′R2′feature/review
نمودار ۵ — خط بالایی main ترمیم‌شده است؛ خط‌های پایین نوک‌های رخداد را زیر اشاره‌گرهای جدا نگه می‌دارند. فلش‌ها رابطهٔ والد commitها را نشان می‌دهند. حذف نشانگر ساختگی در F انجام شده، اما rescue نوک شاخه عمداً تاریخچهٔ قبلی را حفظ می‌کند.Diagram 5 — The top row is repaired main; lower lines preserve incident tips under separate refs. Arrows show commit-parent relationships. F removes the synthetic marker from the current tree, while rescue refs intentionally retain the earlier history.

گزارش رخداد: از فرم پرکنی دوری کن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.