সরাসরি পর্বে যান
পর্ব ৫৩ · ৭০-এর মধ্যে · মডিউল ৯

Ref, reflog আর উদ্ধার — হারানো commit ফিরিয়ে আনা আর garbage collection

Git Refs, Reflogs and Recovery — finding lost commits, and what garbage collection really deletes

Taught in Bangla, this lesson explains Git refs: loose refs and packed-refs, symbolic refs and special refs such as ORIG_HEAD, FETCH_HEAD and MERGE_HEAD. It reads reflog syntax like @{n}, @{-1} and time-based entries, shows what reachability and garbage collection really delete, uses git fsck to find dangling objects, and gives six recovery recipes with real Git commands.

এই পর্বে যা শিখবেন

  • refs/heads, refs/tags, refs/remotes, refs/stash, refs/notes কোথায় থাকে, আর git for-each-ref দিয়ে নিজের মতো তালিকা বানানো
  • Loose ref বনাম packed-refs, symbolic ref, আর ORIG_HEAD, FETCH_HEAD, MERGE_HEAD, CHERRY_PICK_HEAD কখন জন্মায়
  • Reflog-এর @{n}, @{-1}, সময়ভিত্তিক main@{yesterday} নির্ভুলভাবে পড়া
  • Reachability, git gc, git prune, reflog-এর মেয়াদ আর git fsck-এর unreachable, dangling, lost-found
  • ছয়টি উদ্ধারের রেসিপি, আর কোন জিনিস সত্যিই ফেরানো যায় না তা চেনা

১ · Ref — commit-এর গায়ে লাগানো নামের লেবেল

রেলস্টেশনের কথা ভাবুন। ট্রেনের বগিগুলো লাইনের ওপর দাঁড়িয়ে আছে, আর প্ল্যাটফর্মের বোর্ডে লেখা “সুবর্ণ এক্সপ্রেস — ৩ নম্বর লাইন”। বোর্ডের লেখা বদলালে বগি নড়ে না; শুধু যাত্রীরা অন্য বগির দিকে যান। আর কোনো বগির নাম যদি কোনো বোর্ডেই না থাকে, তবু সেটি ইয়ার্ডে পড়ে থাকে — যতদিন না রেলের কর্মীরা পরিষ্কারের দিনে সেটি সরিয়ে নেন। Git-এ commit হলো বগি, আর ref হলো বোর্ডের লেখা। এই পর্বের পুরো গল্প এই ছবিতেই: নাম সরানো সস্তা আর নিরাপদ, বগি সরানো (garbage collection) ধীর আর সাবধানী।

পর্ব ১২-এ দেখেছেন, একটি branch আসলে .git/refs/heads/-এর নিচে একটি ছোট ফাইল, যার ভেতরে একটি commit hash. আর পর্ব ৫২-এ দেখছেন, commit, tree আর blob — এই object-গুলো hash দিয়ে চেনা যায়, একবার লেখার পর আর বদলায় না। এই দুইয়ের মাঝখানের সেতু হলো ref: অপরিবর্তনীয় object-এর জগতে একমাত্র বদলানো যায় এমন জিনিস। Git-এর প্রায় সব “ধ্বংসাত্মক” command — reset, rebase, commit --amend, branch -D, stash drop — আসলে শুধু ref সরায় বা মোছে। সেজন্যই উদ্ধার সম্ভব।

পাঁচ পরিবারের ref

নামের শুরুকীকে সরায়কোথায় শিখেছেন
refs/heads/Local branchআপনার commit, reset, merge, rebaseপর্ব ১২
refs/tags/Tag — lightweight বা annotatedসাধারণত কেউ না; স্থির থাকেপর্ব ১৮
refs/remotes/Remote-tracking branch, যেমন origin/mainশুধু fetch, pull, pushপর্ব ১৪
refs/stashসবচেয়ে নতুন stash; বাকিগুলো তার reflog-এgit stashপর্ব ৫৬
refs/notes/git notes — commit না বদলে জোড়া টীকাgit notes addএই পর্বে

এই পর্বের সব “উদাহরণ” আউটপুট এই যন্ত্রে Git ২.৫৩ দিয়ে scratchpad-এর একটি আলাদা ফোল্ডারে চালিয়ে হুবহু কপি করা। প্রজেক্টটি আগের পর্বগুলোর মতো একটি কাল্পনিক অনলাইন বাজার অ্যাপ। লেখক Rahim, ইমেল rahim@example.com, আর প্রতিটি commit-এর সময় নির্দিষ্ট (2026-01-15, সকাল ১০টা থেকে প্রতি ধাপে এক মিনিট করে), তাই একই ফাইল লিখলে আপনার যন্ত্রেও একই hash আসবে। Repository-তে তিনটি commit-এর main, একটি feature/coupon branch, দুই রকম tag, একটি local bare repository-কে remote ধরে origin/main, একটি stash আর একটি note আছে:

$ git -c init.defaultBranch=main init -q bazar && cd bazar
$ git config user.name "Rahim"; git config user.email "rahim@example.com"
$ git config core.autocrlf false
# প্রতিটি ধাপের আগে: export GIT_AUTHOR_DATE=… GIT_COMMITTER_DATE=… (10:00, 10:01, …)
$ git commit …  "Add README"          # 10:00
$ git commit …  "Add cart total"      # 10:01
$ git tag -a v1.0 -m "First release"; git tag light     # 10:02
$ git commit …  "Add discount rule"   # 10:03
$ git switch -c feature/coupon; git commit … "Add coupon codes"   # 10:04
$ git switch main
$ git notes add -m "Reviewed by Karim" HEAD             # 10:06
$ git init -q --bare ../origin.git; git remote add origin ../origin.git
$ git push -q origin main; git fetch -q origin          # 10:07, 10:08
$ git stash push -q -m "wip readme"                     # 10:09

এবার দেখা যাক এই নামগুলো ডিস্কে কোথায় আছে। .git/HEAD একটি সাধারণ text ফাইল, আর প্রতিটি ref .git/refs/-এর নিচে একটি করে ফাইল:

$ cat .git/HEAD
ref: refs/heads/main
$ find .git/refs -type f | sort
.git/refs/heads/feature/coupon
.git/refs/heads/main
.git/refs/notes/commits
.git/refs/remotes/origin/main
.git/refs/stash
.git/refs/tags/light
.git/refs/tags/v1.0
$ cat .git/refs/heads/main
5f738def208645b7aa88b553e0ea5d2514a65900

এই আলাদা আলাদা ফাইলগুলোকে বলে loose ref. খেয়াল করুন, feature/coupon নামের “/” আসলে একটি ফোল্ডার — refs/heads/feature/-এর ভেতরে coupon ফাইল। এজন্যই একসাথে feature আর feature/coupon নামে দুটি branch রাখা যায় না: একই নামে ফাইল আর ফোল্ডার দুটিই হতে পারে না।

git for-each-ref — ref-এর তালিকা, নিজের ছাঁচে

ফাইল ধরে ধরে cat করা কষ্টের, আর পরের অংশে দেখবেন সব ref ফাইল হিসেবে থাকেও না। সব ref-এর নির্ভরযোগ্য তালিকা দেয় git for-each-ref. এর --format-এ %(…) ঘর বসিয়ে যা খুশি দেখানো যায় — অনেকটা দর্জির মাপের খাতার মতো, কোন কলামে কী লিখবেন আপনি ঠিক করেন:

$ git for-each-ref --format='%(objectname:short) %(objecttype) %(refname)'
3193cd0 commit refs/heads/feature/coupon
5f738de commit refs/heads/main
37e32b9 commit refs/notes/commits
5f738de commit refs/remotes/origin/main
7212405 commit refs/stash
3457a7b commit refs/tags/light
62b7cb0 tag refs/tags/v1.0
$ git for-each-ref --format='%(refname:short) -> %(objectname:short) %(*objectname:short) %(subject)' refs/tags
light -> 3457a7b  Add cart total
v1.0 -> 62b7cb0 3457a7b First release
$ git for-each-ref --sort=-committerdate --format='%(committerdate:short) %(refname:short) %(authorname)' refs/heads
2026-01-15 feature/coupon Rahim
2026-01-15 main Rahim

প্রথম তালিকায় একটি সূক্ষ্ম জিনিস: v1.0-এর objecttype হলো tag, commit নয়। Annotated tag একটি আলাদা tag object (62b7cb0) দেখায়, যার ভেতরে tagger, তারিখ, বার্তা আর আসল commit-এর hash থাকে। Lightweight tag light সরাসরি commit 3457a7b দেখায়। দ্বিতীয় command-এ %(*objectname)-এর তারকা চিহ্ন বলে “tag object-এর খোসা ছাড়িয়ে ভেতরের জিনিসটি দেখাও” — তাই v1.0-এর পাশে 3457a7b, আর light-এর ঘর খালি (খোসা নেই)। তৃতীয়টিতে --sort=-committerdate নতুন থেকে পুরোনো সাজায়; বড় repository-তে “কোন branch মাসের পর মাস কেউ ছোঁয়নি” খুঁজতে এটিই দলের প্রিয় command. --merged, --contains আর --count দিয়ে তালিকা ছাঁকা যায়; git branch আর git tag-এর --format ভেতরে এই একই যন্ত্র ব্যবহার করে। Git ২.৫৩-এর documentation-এ একই কাজের নতুন নাম git refs list-ও আছে।

refs/notes/commits-ও একটি commit দেখায় — notes আসলে নিজস্ব একটি ছোট history, যার tree-তে ফাইলের নাম হলো যে commit-এর টীকা তার hash. তাই commit 5f738de-এর hash না বদলে তার সাথে “Reviewed by Karim” জুড়ে দেওয়া গেছে; git log --format='%h %s%n%N' লিখলে টীকাটি দেখা যায়। Notes ডিফল্টভাবে push বা fetch হয় না, আলাদা refspec লাগে (পর্ব ১৪)।

Ref-গুলো commit graph-এ কোথায় নির্দেশ করে, আর main-এর reflog-এর সময়রেখা

Refs point into the commit DAG; the reflog records where a ref pointed, and when

Ref, commit graph আর reflog df9c5fe 3457a7b 5f738de 3db278b 3193cd0 7212405 main HEAD symbolic light v1.0 tag 62b7cb0 origin/main feature/coupon refs/stash stash commit main-এর reflog 10:00 @{3}10:0110:03 @{1}10:11 @{0}
ছবি ৫৩.১ — তীর সবসময় নতুন commit থেকে parent-এর দিকে। প্রতিটি রঙিন বাক্স একটি ref; HEAD নিজে কোনো commit দেখায় না, main-কে দেখায়। নিচের সময়রেখায় main কখন কোন commit-এ ছিল (10:01-এ @{2}, 3457a7b); 3db278b commit-টি এই পর্বের অংশ ২-এ আসবে।
মূল ধারণা

Git-এর data দুই স্তরের: অপরিবর্তনীয় object (commit, tree, blob, tag) আর বদলানো যায় এমন ref. Ref-এর প্রতিটি বদল reflog-এ লেখা থাকে। কোনো commit “হারায়” না যতক্ষণ কোনো ref, কোনো reflog entry বা অন্য কোনো পৌঁছানো যায় এমন commit তাকে দেখায় — আর garbage collection শেষ পর্যন্ত শুধু সেগুলোকেই মোছে, যাদের কেউ দেখায় না।

২ · Loose ref আর packed-refs — নাম কোথায় লেখা থাকে

প্রতি ref-এ একটি ফাইল — ছোট project-এ চমৎকার। কিন্তু হাজারো tag আর branch থাকলে হাজারো ছোট ফাইল; প্রতিটি খোলা, পড়া আর ডিস্কে জায়গা নেওয়া ধীর। তাই Git মাঝে মাঝে সব ref-কে একটি ফাইলে গুছিয়ে রাখে: packed-refs. মুদি দোকানের খাতার কথা ভাবুন — দিনের প্রতিটি বিক্রি আলাদা চিরকুটে লেখা হলো, রাতে সব চিরকুট একটি খাতায় তুলে চিরকুটগুলো ফেলে দেওয়া হলো। হাতে করে এটি করে git pack-refs --all; সাধারণত git gc-ও এটি করে।

$ git pack-refs --all
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
3193cd0e7957a21cbb0f1bb8c0172d0239c4f80d refs/heads/feature/coupon
5f738def208645b7aa88b553e0ea5d2514a65900 refs/heads/main
37e32b9578349b90b61d5c076493f642d2825cb6 refs/notes/commits
5f738def208645b7aa88b553e0ea5d2514a65900 refs/remotes/origin/main
7212405518b426de49689c31e3d2558d6855a0c4 refs/stash
3457a7bc9716d067de7a2db4fd0308b5bc2b35db refs/tags/light
62b7cb075cc4df415f92ca813d07e0dcfa42e6bc refs/tags/v1.0
^3457a7bc9716d067de7a2db4fd0308b5bc2b35db
$ find .git/refs -type f | sort

শেষ find কিছুই দেখায়নি — সব loose ফাইল মুছে খাতায় উঠে গেছে (ফোল্ডারগুলো খালি পড়ে থাকে)। ফাইলের গঠন সহজ: প্রথম লাইন একটি শিরোনাম, যা বলে এই খাতা কোন কোন সুবিধা মেনে লেখা (sorted মানে নাম অনুযায়ী সাজানো, তাই দ্রুত খোঁজা যায়)। তারপর প্রতি লাইনে পুরো hash, একটি space, আর ref-এর নাম। সবচেয়ে মজার লাইনটি শেষেরটি: ^3457a7b…. এটি একটি peeled মান — ঠিক ওপরের লাইনের annotated tag v1.0-এর খোসা ছাড়ালে যে commit পাওয়া যায়। Git আগেভাগে লিখে রাখে, যাতে “v1.0 কোন commit” জানতে প্রতিবার tag object খুলতে না হয়। Command line-এ একই জিনিস লেখা হয় v1.0^{}:

$ git rev-parse v1.0 v1.0^{}
62b7cb075cc4df415f92ca813d07e0dcfa42e6bc
3457a7bc9716d067de7a2db4fd0308b5bc2b35db

দুই জায়গায় দুই মান — কোনটি সত্যি?

Pack করার পর main-এ একটি নতুন commit দিলে কী হয়? Git পুরো packed-refs নতুন করে লেখে না; শুধু main-এর জন্য আবার একটি loose ফাইল বানায়। খাতায় পুরোনো মান থেকেই যায়:

$ git commit -q -m "Update README"
$ find .git/refs/heads -type f
.git/refs/heads/main
$ grep refs/heads/main .git/packed-refs
5f738def208645b7aa88b553e0ea5d2514a65900 refs/heads/main
$ cat .git/refs/heads/main
3db278b74adee3db23f15155a0b2c38ae45cc495
$ git rev-parse main
3db278b74adee3db23f15155a0b2c38ae45cc495

নিয়ম: একই নামের loose ফাইল থাকলে সেটিই জেতে; খাতার লাইন তখন বাসি (stale) আর উপেক্ষিত। খাতা কেবল সেই ref-গুলোর জন্য পড়া হয়, যাদের loose ফাইল নেই। তাই কখনো হাতে packed-refs সম্পাদনা করবেন না, বা .git/refs/-এ ফাইল মুছে “branch মোছা” ভাববেন না — খাতায় থাকা পুরোনো মান আবার জেগে উঠতে পারে। Ref বদলানো বা মোছার সঠিক পথ git branch -d, git tag -d বা git update-ref, যারা দুই জায়গাই সামলায়। নিচের Python প্রোগ্রাম ঠিক এই নিয়মে ওপরের আসল খাতা আর loose ফাইল পড়ে:

# Read packed-refs + loose refs the way the "files" backend does: loose wins, "^" = peeled tag.
PACKED = """# pack-refs with: peeled fully-peeled sorted
5f738def208645b7aa88b553e0ea5d2514a65900 refs/heads/main
5f738def208645b7aa88b553e0ea5d2514a65900 refs/remotes/origin/main
3457a7bc9716d067de7a2db4fd0308b5bc2b35db refs/tags/light
62b7cb075cc4df415f92ca813d07e0dcfa42e6bc refs/tags/v1.0
^3457a7bc9716d067de7a2db4fd0308b5bc2b35db
"""
LOOSE = {"refs/heads/main": "3db278b74adee3db23f15155a0b2c38ae45cc495\n",  # .git/refs/heads/main
         "HEAD": "ref: refs/heads/main\n"}                                  # .git/HEAD

def parse_packed(text):
    refs, peeled, last = {}, {}, None
    for line in text.splitlines():
        if not line or line.startswith("#"): continue
        if line.startswith("^"):           # peeled value of the tag on the line above
            peeled[last] = line[1:]; continue
        oid, name = line.split(" ", 1)
        refs[name], last = oid, name
    return refs, peeled
def resolve(name, packed, depth=0):
    if depth > 5: raise ValueError("symbolic ref loop")
    if name in LOOSE:                      # a loose file always beats packed-refs
        value = LOOSE[name].strip()
        if value.startswith("ref: "):      # symbolic ref: follow the name written inside
            return resolve(value[5:], packed, depth + 1)
        return value, "loose"
    return packed[name], "packed"

packed, peeled = parse_packed(PACKED)
for name in ["HEAD", "refs/heads/main", "refs/remotes/origin/main", "refs/tags/v1.0"]:
    oid, src = resolve(name, packed)
    tag = f"  peeled ^{{}} -> {peeled[name][:7]}" if name in peeled else ""
    print(f"{name:26} {oid[:7]} ({src}){tag}")
print("stale packed value of main:", packed["refs/heads/main"][:7])

আউটপুট:

HEAD                       3db278b (loose)
refs/heads/main            3db278b (loose)
refs/remotes/origin/main   5f738de (packed)
refs/tags/v1.0             62b7cb0 (packed)  peeled ^{} -> 3457a7b
stale packed value of main: 5f738de

HEAD-এর loose ফাইলে hash নেই, লেখা ref: refs/heads/main — প্রোগ্রাম সেই নাম ধরে আবার খোঁজে, আর main-এর loose ফাইল পেয়ে 3db278b দেয়। origin/main-এর loose ফাইল নেই, তাই খাতার মান 5f738de. v1.0-এর মান tag object 62b7cb0, আর পরের ^ লাইন থেকে peeled commit 3457a7b — ঠিক ওপরের git rev-parse v1.0^{}-এর মতো। শেষ লাইন দেখায় খাতায় main-এর বাসি মান এখনো 5f738de, যা loose ফাইলের কারণে কেউ দেখে না। depth সীমাটি symbolic ref-এর চক্র (A দেখায় B, B দেখায় A) থেকে বাঁচায়; আসল Git-ও এমন একটি সীমা রাখে।

তৃতীয় পথ — reftable backend

Loose ফাইল আর packed-refs মিলিয়ে যে ব্যবস্থা, Git তাকে বলে files backend. এর কিছু পুরোনো ঝামেলা আছে: Windows আর macOS-এর ফাইল সিস্টেম সাধারণত বড়-ছোট হাতের অক্ষর আলাদা করে না, তাই Feature আর feature branch গোলমাল বাধায়; একসাথে অনেক ref বদলানো পুরোপুরি atomic নয়; আর লাখো ref-ওয়ালা hosting server-এ খাতা নতুন করে লেখা ব্যয়বহুল। এর সমাধান হিসেবে এসেছে reftable — ref আর reflog রাখার একটি binary, সাজানো table-এর ফরম্যাট। যতদূর জানা যায়, ফরম্যাটটির নকশা শুরু হয়েছিল JGit প্রজেক্টে (প্রায় ২০১৭ সালে), আর Git ২.৪৫ (২০২৪) থেকে নতুন repository বানানোর সময় এটি বেছে নেওয়া যায়:

$ git -c init.defaultBranch=main init -q --ref-format=reftable rt
$ git -C rt rev-parse --show-ref-format
reftable
$ ls rt/.git/reftable
0x000000000001-0x000000000001-88986262.ref
tables.list
$ cat rt/.git/HEAD
ref: refs/heads/.invalid
$ cat rt/.git/refs/heads
this repository uses the reftable format
$ git -C bazar rev-parse --show-ref-format
files

reftable repository-তে .git/HEAD আর .git/refs শুধু পুরোনো Git-কে বিভ্রান্ত না করার জন্য রাখা “পাহারাদার” ফাইল; আসল ref থাকে reftable/-এর table-এ (ফাইলের নামের শেষ অংশ প্রতিবার আলাদা হয়)। এর মানে “cat .git/refs/heads/main” ধরনের কৌশল সব repository-তে চলবে না — script লিখলে সবসময় git rev-parse, git for-each-ref বা git update-ref ব্যবহার করুন। পুরোনো repository রূপান্তরের জন্য git refs migrate --ref-format=reftable আছে (documentation-এ লেখা, ফরম্যাটটি এখনো experimental আর ভেতরের খুঁটিনাটি বদলাতে পারে)। ভবিষ্যতের কোনো বড় Git version-এ এটিকে ডিফল্ট করার আলোচনা আছে, তবে লেখার সময়ে ডিফল্ট files.

৩ · Symbolic ref আর বিশেষ ref — HEAD, ORIG_HEAD, FETCH_HEAD, MERGE_HEAD

বেশিরভাগ ref-এর ভেতরে একটি hash. কিন্তু .git/HEAD-এ লেখা ছিল ref: refs/heads/main — একটি hash নয়, আরেকটি ref-এর নাম। এমন ref-কে বলে symbolic ref. বিয়েবাড়ির আয়োজনের তালিকায় “রান্নার দায়িত্ব: যিনি বাবুর্চি” লেখা থাকলে বাবুর্চি বদলালেও তালিকা বদলাতে হয় না — HEAD ঠিক তেমন: main নতুন commit-এ সরলে HEAD নিজে বদলায় না, তবু সবসময় main-এর মাথা দেখায়। git symbolic-ref দিয়ে এটি সরাসরি পড়া যায়:

$ git symbolic-ref HEAD
refs/heads/main
$ git symbolic-ref --short HEAD
main
$ git rev-parse HEAD
5f738def208645b7aa88b553e0ea5d2514a65900
# আলাদা একটি throwaway repository-তে, detached অবস্থায়:
$ git switch -q --detach
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
$ git symbolic-ref -q HEAD; echo "exit code: $?"
exit code: 1

symbolic-ref নাম দেয়, rev-parse শেষ পর্যন্ত গিয়ে hash দেয়। Detached HEAD অবস্থায় HEAD-এ সরাসরি hash থাকে, তাই symbolic-ref ব্যর্থ হয় (পর্ব ১২)। Script-এ “এখন কোন branch-এ আছি, নাকি detached?” জানতে git symbolic-ref -q HEAD সবচেয়ে পরিষ্কার পথ: -q ভুলের বার্তা চেপে রাখে, শুধু exit code ১ দেয়। আরেকটি পরিচিত symbolic ref হলো refs/remotes/origin/HEAD, যা remote-এর ডিফল্ট branch দেখায়। এটি clone-এর সময় তৈরি হয়; Git ২.৪৮ থেকে fetch-ও না থাকলে এটি বানায়, যদি remote-এর HEAD একটি আসল branch দেখায়। আমাদের bare repository-র HEAD তখনো জন্ম না নেওয়া master দেখাচ্ছিল, তাই ওপরের তালিকায় এটি নেই; আপনার config-এ init.defaultBranch=main থাকলে তালিকায় refs/remotes/origin/HEAD-ও দেখবেন।

কাজের মাঝপথের বিশেষ ref

.git ফোল্ডারের একদম ওপরে কিছু বড়-হাতের নামের ফাইল থাকে। এগুলো branch নয়; Git কোনো কাজের মাঝখানে বা পরে “কোথা থেকে এসেছিলাম” মনে রাখতে এগুলো লেখে। পরীক্ষার জন্য আমরা main~1 থেকে একটি hotfix branch বানিয়ে README-এর সেই লাইনটিই অন্যভাবে বদলেছি, যা main-এও বদলেছে — তাই merge আর cherry-pick দুটিই conflict-এ থামে:

$ git merge hotfix
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
$ ls .git | grep HEAD
FETCH_HEAD
HEAD
MERGE_HEAD
ORIG_HEAD
$ cat .git/MERGE_HEAD
bf6efa2eb2f273be614c4fd32e2d961403b0b533
$ git merge --abort
$ git cherry-pick hotfix
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not apply bf6efa2... Hotfix README
…
$ cat .git/CHERRY_PICK_HEAD
bf6efa2eb2f273be614c4fd32e2d961403b0b533
$ git cherry-pick --abort
# অনেক আগে, git fetch-এর ঠিক পরে নেওয়া:
$ cat .git/FETCH_HEAD
5f738def208645b7aa88b553e0ea5d2514a65900	not-for-merge	branch 'main' of ../origin
নামকে লেখেকী রাখেকতক্ষণ থাকে
ORIG_HEADreset, merge, rebase (আর কিছু অন্য command)বড় চলাচলের ঠিক আগের HEADপরের এমন command ওপরে লেখা পর্যন্ত
FETCH_HEADfetch (আর pull-এর ভেতরের fetch)যা নামানো হলো তার তালিকা; merge-যোগ্য লাইনে not-for-merge থাকে নাপরের fetch পর্যন্ত
MERGE_HEADথেমে থাকা mergeযে commit মেলানো হচ্ছেcommit বা --abort পর্যন্ত
CHERRY_PICK_HEADথেমে থাকা cherry-pickযে commit বসানো হচ্ছিল--continue/--abort পর্যন্ত
REVERT_HEAD, REBASE_HEADথেমে থাকা revert ও rebaseযে commit নিয়ে কাজ চলছেকাজ শেষ বা বাতিল পর্যন্ত

আমাদের FETCH_HEAD-এ not-for-merge লেখা, কারণ main-এর কোনো upstream ঠিক করা ছিল না — তাই fetch জানত না কোন branch merge-যোগ্য। Upstream থাকলে সেই লাইনে এই চিহ্ন থাকে না, আর git pull আসলে “fetch, তারপর FETCH_HEAD-এর সেই লাইনটি merge”. MERGE_HEAD থাকলে git status জানায় যে merge চলছে (conflict মেটানো না হলে “You have unmerged paths”), আর আপনি commit দিলে Git দুই parent-ওয়ালা merge commit বানায় — দ্বিতীয় parent আসে এখান থেকেই। Conflict-এর পুরো যুক্তি পর্ব ৫৪-এ।

উদ্ধারের দিক থেকে সবচেয়ে কাজের ORIG_HEAD: ভুল merge বা rebase-এর ঠিক পরে git reset --hard ORIG_HEAD আগের অবস্থায় ফেরায় — পর্ব ১৫-এ এটি দেখেছেন। সমস্যা হলো এটি একটিই ঘর: পরের reset বা merge সেটি ওপরে লিখে দেয়। দীর্ঘ স্মৃতির জন্য দরকার reflog, পরের অংশে।

git update-ref — ref-এ সরাসরি লেখা

git update-ref হলো ref বদলানোর নিচু-স্তরের, script-বান্ধব command. git branch ভেতরে প্রায় এটাই করে, কিন্তু update-ref নিজে কোনো নিয়ম চাপায় না — তাই ক্ষমতাও বেশি, দায়ও বেশি। এর সবচেয়ে মূল্যবান রূপটি তিন-argument-এর: “নতুন মান বসাও, কিন্তু শুধু যদি এখনকার মান এটি হয়”:

$ git update-ref -m "safety copy" refs/heads/backup HEAD
$ git branch --list
  backup
  feature/coupon
* main
$ git update-ref refs/heads/backup 3457a7b 3193cd0e7957a21cbb0f1bb8c0172d0239c4f80d
fatal: update_ref failed for ref 'refs/heads/backup': cannot lock ref 'refs/heads/backup': is at 5f738def208645b7aa88b553e0ea5d2514a65900 but expected 3193cd0e7957a21cbb0f1bb8c0172d0239c4f80d
$ git log -g --format="%h %gd %gs" backup
5f738de backup@{0} safety copy
$ git update-ref -d refs/heads/backup
$ git show-ref --heads
3193cd0e7957a21cbb0f1bb8c0172d0239c4f80d refs/heads/feature/coupon
5f738def208645b7aa88b553e0ea5d2514a65900 refs/heads/main

প্রথম লাইন একটি backup branch বানাল, আর -m-এর বার্তা reflog-এ উঠল (backup@{0} safety copy). দ্বিতীয় চেষ্টায় আমরা বলেছিলাম “backup যদি 3193cd0-এ থাকে, তবে 3457a7b-এ সরাও” — কিন্তু সেটি ছিল 5f738de-এ, তাই Git কিছুই না বদলে থেমে গেল। এই “compare-and-swap” কৌশলেই একাধিক script বা মানুষ একই ref নিয়ে কাজ করলেও একজন আরেকজনের কাজ চুপচাপ মুছে দেয় না — --force-with-lease-এর পেছনের চিন্তাও এটাই (পর্ব ১৪)। -d ref মোছে — loose ফাইল আর খাতার লাইন দুটোই। git update-ref --stdin দিয়ে অনেকগুলো বদল একটি transaction-এ করা যায়।

৪ · Reflog গভীরে — প্রতিটি ref-এর নিজের খাতা

পর্ব ১১-এ reflog-কে চিনেছেন “HEAD-এর চলাচলের খাতা” হিসেবে। পুরো সত্যটা একটু বড়: প্রতিটি branch-এর নিজস্ব reflog আছে, আর HEAD-এর আলাদা একটি. Files backend-এ এগুলো .git/logs/HEAD আর .git/logs/refs/heads/… নামের সাধারণ text ফাইল; প্রতিটি লাইনে আগের hash, নতুন hash, কে, কখন আর কেন। Branch-এর খাতায় শুধু সেই branch-এর মাথা কখন কোথায় সরল তা থাকে; HEAD-এর খাতায় থাকে আপনার প্রতিটি পদক্ষেপ, branch বদলানোসহ। একই repository-র দুই খাতা পাশাপাশি:

$ git reflog -6
3db278b HEAD@{0}: commit: Update README
5f738de HEAD@{1}: reset: moving to HEAD
5f738de HEAD@{2}: checkout: moving from feature/coupon to main
3193cd0 HEAD@{3}: commit: Add coupon codes
5f738de HEAD@{4}: checkout: moving from main to feature/coupon
5f738de HEAD@{5}: commit: Add discount rule
$ git reflog show main
3db278b main@{0}: commit: Update README
5f738de main@{1}: commit: Add discount rule
3457a7b main@{2}: commit: Add cart total
df9c5fe main@{3}: commit (initial): Add README
$ git reflog show feature/coupon
3193cd0 feature/coupon@{0}: commit: Add coupon codes
5f738de feature/coupon@{1}: branch: Created from HEAD
$ git reflog show --date=iso -3 main
3db278b main@{2026-01-15 10:11:00 +0600}: commit: Update README
5f738de main@{2026-01-15 10:03:00 +0600}: commit: Add discount rule
3457a7b main@{2026-01-15 10:01:00 +0600}: commit: Add cart total

HEAD-এর খাতায় HEAD@{1}: reset: moving to HEAD লাইনটি কোথা থেকে এলো? ওটি git stash push-এর কাজ: stash পরিবর্তন সরিয়ে রেখে ভেতরে ভেতরে working tree-কে HEAD-এর মতো করে দেয়, আর সেটিও একটি চলাচল হিসেবে লেখা হয়। main-এর খাতায় এসব নেই — main কখনো সরেনি। --date=iso দিলে নম্বরের বদলে সময় দেখায়। git reflog show আসলে git log -g --abbrev-commit --pretty=oneline-এর ছোট নাম (documentation-এ এভাবেই লেখা), তাই git log -g --format="%h %gd %gs" main-এর মতো নিজের ছাঁচও বানানো যায়: %gd হলো main@{n} আর %gs হলো কারণের লেখা।

Reflog-এর ঠিকানা লেখার নিয়ম

Reflog পড়ে commit বোঝানোর লেখাগুলোকে reflog selector বলা যায় — এগুলোও একধরনের revision, যেকোনো command-এ hash-এর জায়গায় বসে:

লেখাঅর্থএই repository-তে
HEAD@{n}HEAD-এর খাতায় n চলাচল আগে HEAD কোথায় ছিলHEAD@{1} = 3193cd0
main@{n}main-এর খাতায় n চলাচল আগে main কোথায় ছিলmain@{2} = 3457a7b
@{n}বর্তমান branch-এর খাতা — HEAD-এর নয়!@{1} = main@{1} = 5f738de
@{-n}n-তম আগে checkout করা branch-এর নাম@{-1} = feature/coupon
main@{2026-01-15 10:05}, main@{yesterday}, main@{2.hours.ago}সেই মুহূর্তে main কোথায় ছিল — খাতায় সেই সময়ের আগের সবচেয়ে নতুন entry5f738de (১০:০৩-এর entry)

শেষ তিন সারির মান মিলিয়ে নিতে আমরা দুবার branch বদলে (feature/coupon-এ গিয়ে আবার main-এ ফিরে) আসল Git-কে জিজ্ঞেস করেছি:

$ git reflog -8
3db278b HEAD@{0}: checkout: moving from feature/coupon to main
3193cd0 HEAD@{1}: checkout: moving from main to feature/coupon
3db278b HEAD@{2}: commit: Update README
5f738de HEAD@{3}: reset: moving to HEAD
5f738de HEAD@{4}: checkout: moving from feature/coupon to main
3193cd0 HEAD@{5}: commit: Add coupon codes
5f738de HEAD@{6}: checkout: moving from main to feature/coupon
5f738de HEAD@{7}: commit: Add discount rule
$ for r in HEAD@{2} @{1} HEAD@{1} @{-1} @{-2}; do echo "$r = $(git rev-parse --short $r)"; done
HEAD@{2} = 3db278b
@{1} = 5f738de
HEAD@{1} = 3193cd0
@{-1} = 3193cd0
@{-2} = 3db278b
$ git rev-parse --abbrev-ref @{-1}
feature/coupon
$ git rev-parse --short 'main@{2026-01-15 10:02:00 +0600}'
3457a7b
$ git rev-parse --short 'main@{2026-01-15 09:00:00 +0600}'
warning: log for 'main' only goes back to Thu, 15 Jan 2026 10:00:00 +0600
df9c5fe

সবচেয়ে বেশি ভুল হয় দ্বিতীয় আর তৃতীয় লাইনে: @{1} = 5f738de কিন্তু HEAD@{1} = 3193cd0. HEAD-এর খাতায় এক ধাপ আগে আপনি feature/coupon-এ দাঁড়িয়ে ছিলেন; main-এর খাতায় এক ধাপ আগে main ছিল 5f738de-এ। ভুল reset --hard-এর পর main@{1} প্রায়ই HEAD@{1}-এর চেয়ে নির্ভরযোগ্য, কারণ মাঝখানে branch বদলালেও main-এর খাতায় সেই লাইন আসে না। আর খাতার শুরুর আগের সময় চাইলে Git একটি সতর্কবার্তা দিয়ে সবচেয়ে পুরোনো entry-টিই দেয়। main@{yesterday} ধরনের লেখা আপনার যন্ত্রের এখনকার ঘড়ি থেকে হিসাব হয়, তাই একই command আজ আর কাল ভিন্ন ফল দিতে পারে।

@{-1} কীভাবে খোঁজা হয় — ধাপে ধাপে

git switch - আর git checkout - আসলে @{-1}-এর সংক্ষেপ। Git এটি বের করে HEAD-এর খাতার “checkout: moving from X to Y” লাইনগুলো পড়ে। ওপরের আসল git reflog -8 ধরে দেখা যাক:

Git কী পড়ছে, কী পেল
পড়ছে
checkout লাইন?
@{-1}
@{-2}
তুলনা
  1. সবচেয়ে নতুন লাইন থেকে শুরু। HEAD@{0} একটি checkout লাইন: “moving from feature/coupon to main”. “from”-এর নামটিই প্রথম আগের branch, অর্থাৎ @{-1} = feature/coupon.
  2. পরের checkout লাইন। HEAD@{1}-এ “moving from main to feature/coupon”. দ্বিতীয় “from” = @{-2} = main — হ্যাঁ, এখন যে branch-এ আছেন, সেটিই দুই ধাপ আগের branch হতে পারে।
  3. Commit আর reset লাইন গোনা হয় না। @{-n} শুধু branch বদলানো গোনে। তাই @{-n}-এর n আর HEAD@{n}-এর n দুই ভিন্ন গোনা।
  4. নাম থেকে hash — এখনকার মান। Git নামটি পেয়ে সেই branch-এর এখনকার মাথা দেয়: feature/coupon → 3193cd0, main → 3db278b. সেই সময়ে branch কোথায় ছিল তা নয়। Branch-টি পরে মুছে ফেললে নাম পাওয়া যায়, কিন্তু hash মেলে না — command ব্যর্থ হয়।
  5. পাশাপাশি তুলনা। HEAD@{1} HEAD-এর খাতার এক ধাপ আগের hash (3193cd0); @{1} বর্তমান branch main-এর খাতার এক ধাপ আগের hash (5f738de). তিনটি লেখা, তিনটি খাতা-পড়ার নিয়ম।

একই নিয়ম একটি ছোট resolver-এ লেখা হলো। ডেটা ওপরের আসল reflog থেকে কপি করা; সময়গুলো শুধু ঘণ্টা:মিনিট, তুলনা সহজ রাখতে:

// Resolve @{n}, @{-n}, ref@{n} and ref@{date} over a recorded reflog (copied from a real repo).
const HEAD_LOG = [                       // newest first, like `git reflog`
  "3db278b checkout: moving from feature/coupon to main", "3193cd0 checkout: moving from main to feature/coupon",
  "3db278b commit: Update README", "5f738de reset: moving to HEAD",
  "5f738de checkout: moving from feature/coupon to main", "3193cd0 commit: Add coupon codes",
  "5f738de checkout: moving from main to feature/coupon", "5f738de commit: Add discount rule"];
const LOGS = { HEAD: HEAD_LOG.map(l => ({ id: l.slice(0, 7), msg: l.slice(8) })),
  main: [["3db278b", "10:11"], ["5f738de", "10:03"], ["3457a7b", "10:01"], ["df9c5fe", "10:00"]]
          .map(([id, at]) => ({ id, at })),
  "feature/coupon": [{ id: "3193cd0", at: "10:04" }, { id: "5f738de", at: "10:04" }] };
const current = "main";                  // HEAD -> refs/heads/main
function resolve(expr) {
  let m = expr.match(/^(.*)@\{(-?)(.+)\}$/);
  if (!m) return LOGS[expr] ? LOGS[expr][0].id : expr;
  let [, ref, minus, arg] = m;
  if (minus) {                           // @{-n}: n-th branch checked out before this one
    const from = LOGS.HEAD.map(e => e.msg.match(/^checkout: moving from (\S+) to/)).filter(Boolean);
    const name = from[Number(arg) - 1][1];
    return `${name} -> ${resolve(name)}`;
  }
  if (ref === "") ref = current;         // bare @{n} means the CURRENT BRANCH's reflog, not HEAD's
  const log = LOGS[ref];
  if (/^\d+$/.test(arg)) return log[Number(arg)].id;
  const e = log.find(x => x.at <= arg);  // time: newest entry at or before that moment
  return e ? e.id : log[log.length - 1].id + "  (log only goes back to " + log[log.length - 1].at + ")";
}
for (const x of ["HEAD@{0}", "HEAD@{1}", "HEAD@{2}", "@{1}", "main@{2}", "@{-1}", "@{-2}",
                 "main@{10:05}", "main@{10:02}", "main@{09:00}", "feature/coupon@{1}"])
  console.log(x.padEnd(19), "=", resolve(x));

আউটপুট:

HEAD@{0}            = 3db278b
HEAD@{1}            = 3193cd0
HEAD@{2}            = 3db278b
@{1}                = 5f738de
main@{2}            = 3457a7b
@{-1}               = feature/coupon -> 3193cd0
@{-2}               = main -> 3db278b
main@{10:05}        = 5f738de
main@{10:02}        = 3457a7b
main@{09:00}        = df9c5fe  (log only goes back to 10:00)
feature/coupon@{1}  = 5f738de

প্রতিটি লাইন আসল Git-এর উত্তরের সাথে মেলে: HEAD@{1} আর @{1}-এর পার্থক্য, @{-1} আর @{-2}-এর নাম আর এখনকার hash, সময় দিয়ে খোঁজায় “সেই মুহূর্ত বা তার আগের সবচেয়ে নতুন entry”, আর খাতার শুরুর আগের সময়ে সবচেয়ে পুরোনো entry (Git-এর সেই warning-এর মতো)। feature/coupon@{1} = 5f738de, কারণ branch তৈরির মুহূর্তে সে main-এর মাথাতেই ছিল (“Created from HEAD”). আসল Git সময় পড়তে “yesterday” বা “2.hours.ago”-র মতো অনেক রকম লেখা বোঝে; এই খেলনা resolver শুধু ঘণ্টা:মিনিট বোঝে।

৫ · Reachability আর garbage collection — Git আসলে কী মোছে

একটি commit-এর তিন রকম অবস্থা হতে পারে, আর পুরো উদ্ধারবিদ্যা এই তিনটি চেনার ওপর দাঁড়িয়ে:

Reachable

কোনো ref (branch, tag, remote-tracking, stash, notes বা HEAD) থেকে parent-এর তীর ধরে হেঁটে পৌঁছানো যায়। কখনো মোছে না।

নিরাপদ

শুধু reflog থেকে

কোনো ref দেখায় না, কিন্তু কোনো reflog entry দেখায়। reset, amend, rebase-এর পর পুরোনো commit-গুলো এখানে। Entry-র মেয়াদ থাকা পর্যন্ত নিরাপদ।

উদ্ধারযোগ্য

Unreachable

Ref বা reflog কেউই দেখায় না — drop করা stash, add করে হারানো blob, মেয়াদ পেরোনো entry-র commit. Object store-এ আছে, কিন্তু gc-এর প্রার্থী।

⚠ সময় কম

“পৌঁছানো যায়” মানে একটি graph traversal: সব ref-কে শুরুর বিন্দু ধরে commit-এর parent, commit-এর tree, tree-এর ভেতরের blob — এভাবে তীর ধরে যতদূর যাওয়া যায় (DSA কোর্সের DFS-এর সরাসরি প্রয়োগ)। যা ছোঁয়া হলো না, তা “অনাথ”। এটি প্রোগ্রামিং ভাষার garbage collector-এর mark-and-sweep-এর মতো (JavaScript কোর্সে memory), শুধু একটি বড় পার্থক্য: Git অনাথ পেলেই মোছে না, আগে একটি অনুগ্রহকাল (grace period) দেয়।

তিন সময়সীমা

Configডিফল্টকী নিয়ন্ত্রণ করে
gc.reflogExpire৯০ দিনযে reflog entry-র commit এখনো ref-এর মাথা থেকে পৌঁছানো যায়, সেটি কতদিন থাকে
gc.reflogExpireUnreachable৩০ দিনযে entry-র commit ref-এর বর্তমান মাথা থেকে আর পৌঁছানো যায় না (amend, rebase, reset-এর শিকার), সেটি কতদিন থাকে
gc.pruneExpire২ সপ্তাহঅনাথ loose object মোছার আগে ফাইলটির বয়স (লেখার সময় থেকে) কত হতে হবে

ডিফল্টগুলো Git ২.৫৩-এর documentation থেকে নেওয়া; version বা আপনার config-ভেদে আলাদা হতে পারে, তাই নিশ্চিত হতে git help gc আর git config --get gc.reflogExpire দেখুন (কিছু না এলে ডিফল্ট চলছে)। gc.<pattern>.reflogExpire দিয়ে নির্দিষ্ট ref-এর জন্য আলাদা মেয়াদও দেওয়া যায়, যেমন refs/stash-এর জন্য। git gc চালালে মোটামুটি এই ক্রমে কাজ হয়: ref গুছিয়ে packed-refs-এ তোলা, git reflog expire দিয়ে মেয়াদোত্তীর্ণ entry ছাঁটা, পৌঁছানো যায় এমন object-গুলো pack-এ তোলা, আর শেষে git prune-এর মতো ধাপে বয়স্ক অনাথ object মোছা। সাম্প্রতিক Git অনাথ object-গুলো আলাদা একটি “cruft pack”-এ রাখতে পারে, যেখানে প্রতিটির বয়স আলাদা লেখা থাকে — মেয়াদের নিয়ম একই থাকে।

আপনি কখনো garbage collection না চালালেও Git নিজে চালায়: commit, merge, fetch-এর মতো কিছু command শেষে Git git gc --auto চালিয়ে দেখে কাজ জমেছে কি না। ডিফল্টে ৬৭০০-র বেশি loose object (gc.auto) বা ৫০-এর বেশি pack (gc.autoPackLimit) হলে তবেই আসল gc চলে। নতুন Git-এ git maintenance এই দায়িত্বের কিছু অংশ নিয়েছে। মোদ্দা কথা: উদ্ধারের সময় আপনার হাতে কয়েক সপ্তাহ থাকে, কিন্তু তা নিশ্চিত নয় — কাজেই হারানো commit পেলে প্রথমেই একটি branch বসিয়ে তাকে reachable করুন।

একটি commit-এর জীবন: reachable থেকে মুছে যাওয়া

আসল প্রমাণ দেখতে একটি throwaway repository বানানো হলো। আমাদের উদাহরণের তারিখ পুরোনো (জানুয়ারি ২০২৬) আর এই যন্ত্রের ঘড়ি অক্টোবর ২০২৬, তাই তারিখের প্রভাব সরাতে প্রথমে দুই মেয়াদ never করা হলো — এতে ফল পাঠকের যন্ত্রের তারিখের ওপর নির্ভর করবে না:

$ git config gc.reflogExpire never
$ git config gc.reflogExpireUnreachable never
$ git log --oneline
e61471e Throw me away
947c1bf Keep me
$ git reset -q --hard HEAD~1
$ git reflog
947c1bf HEAD@{0}: reset: moving to HEAD~1
e61471e HEAD@{1}: commit: Throw me away
947c1bf HEAD@{2}: commit (initial): Keep me
$ git cat-file -t e61471e
commit
$ git fsck --unreachable
$ git fsck --unreachable --no-reflogs
unreachable commit e61471eda279d29ebafbb7ebdec7ef3fd0d38d28
unreachable blob f3d43775e65ac68c0589a8961c9e665ee8436944
unreachable tree d3913310880cad05f75019555a3dfbd5c61b0149
$ git gc --prune=now -q
$ git cat-file -t e61471e
commit
$ git reflog expire --expire-unreachable=now --all
$ git reflog
947c1bf HEAD@{0}: commit (initial): Keep me
$ git gc --prune=now -q
$ git cat-file -t e61471e
fatal: Not a valid object name e61471e
$ git count-objects -v
count: 0
size: 0
in-pack: 3
packs: 1
…
e61471e commit-এর জীবনচক্র 947c1bf ✕ e61471e main main HEAD@{1}
অবস্থা
অবস্থা
git fsck --unreachable
git cat-file -t e61471e
  1. Commit হলো। main e61471e-তে। এটি branch থেকে সরাসরি পৌঁছানো যায় — কোনো gc একে ছোঁবে না।
  2. git reset -q --hard HEAD~1. main ফিরে গেল 947c1bf-এ। e61471e এখন শুধু reflog-এ (HEAD@{1}, main@{1})। git fsck --unreachable কিছু দেখায় না, কারণ fsck ডিফল্টভাবে reflog-কেও শুরুর বিন্দু ধরে; --no-reflogs দিলে তবেই commit, তার tree আর blob “unreachable” দেখায়।
  3. git gc --prune=now — তবু বেঁচে গেল। এমনকি “এখনই prune” বলার পরও cat-file বলছে commit, কারণ reflog entry তাকে রক্ষা করছে, আর মেয়াদ এখানে never. Prune শুধু সেই object মোছে যাকে কেউ দেখায় না।
  4. git reflog expire --expire-unreachable=now --all. যে entry-গুলো এই commit-কে ছুঁয়ে ছিল (commit: Throw me away আর তার পরের reset লাইন), সেগুলো ছাঁটা হলো; খাতায় শুধু Keep me. এখন সত্যিকারের অনাথ।
  5. আবার git gc --prune=now. এবার আর রক্ষাকর্তা নেই, অনুগ্রহকালও শূন্য — object মুছে গেল। fatal: Not a valid object name e61471e. এই repository-তে আর কোনো উপায়ে এটি ফেরে না; অন্য কোনো clone-এ থাকলে শুধু সেখান থেকে।

তারিখের একটি ফাঁদ, যা আমরা নিজেরাই খেয়েছি। প্রথমবার never config ছাড়াই, সাধারণ ডিফল্টে একই পরীক্ষা চালিয়েছিলাম:

$ git reflog
947c1bf HEAD@{0}: reset: moving to HEAD~1
e61471e HEAD@{1}: commit: Throw me away
947c1bf HEAD@{2}: commit (initial): Keep me
$ git gc --prune=now -q
$ git reflog
$ git cat-file -t e61471e
fatal: Not a valid object name e61471e

একটিমাত্র gc-তেই পুরো reflog খালি আর commit মুছে গেছে! কারণ reflog entry-র সময় নেওয়া হয় committer-এর সময় থেকে — আমরা GIT_COMMITTER_DATE দিয়ে ১৫ জানুয়ারি বসিয়েছিলাম — আর gc মেয়াদ হিসাব করে যন্ত্রের এখনকার ঘড়ি দিয়ে (এখানে ৩০ অক্টোবর)। ফলে সব entry ৯০ দিনের বেশি “পুরোনো”। শিক্ষা দুটি: reflog-এর নিরাপত্তা জাল বয়সভিত্তিক, আর পরীক্ষার জন্য কৃত্রিম তারিখ বসানো repository-তে কখনো আসল কাজ রাখবেন না।

নিচের মডেলে ছয়টি commit-এর একটি ছোট history; প্রতিটি object কবে লেখা হয়েছিল আর reflog entry কবের, তা বলে দেওয়া আছে। তারপর তিনটি নির্দিষ্ট দিনে gc চালানো হয় — ডিফল্ট তিন সময়সীমা মেনে। (সরলতার জন্য মডেলে “পৌঁছানো যায়” মানে যেকোনো ref থেকে; আসল Git প্রতিটি খাতার নিজের ref-এর মাথা ধরে দেখে।)

// Reachability + gc expiry. Dates are stated, so the run is the same every time.
const DAY = 86400000, t = s => Date.parse(s + "T10:00:00+06:00");
const parentOf = { A: null, B: "A", C: "B", D: "C", X: "D", S: "C" };     // commit DAG
const born = { A: "2026-01-01", B: "2026-01-02", C: "2026-01-03",
               D: "2026-01-04", S: "2026-01-05", X: "2026-01-06" };     // object write date
const refs = { "refs/heads/main": "D" };                                // X: reset away; S: dropped stash
let reflog = [ { ref: "main", id: "A", at: "2026-01-01" }, { ref: "main", id: "B", at: "2026-01-02" },
  { ref: "main", id: "C", at: "2026-01-03" }, { ref: "main", id: "D", at: "2026-01-04" },
  { ref: "HEAD", id: "X", at: "2026-01-06" } ];                         // stash entry already dropped
let objects = Object.keys(parentOf);
function walk(tips) {                                                   // follow parent arrows
  const seen = new Set();
  for (let c of tips) while (c && !seen.has(c)) { seen.add(c); c = parentOf[c]; }
  return seen;
}
function classify() {
  const live = walk(Object.values(refs)), logged = walk(reflog.map(e => e.id));
  const pick = f => objects.filter(f).join(" ") || "-";
  console.log("  reachable from refs :", pick(c => live.has(c)));
  console.log("  reflog-only         :", pick(c => !live.has(c) && logged.has(c)));
  console.log("  unreachable         :", pick(c => !live.has(c) && !logged.has(c)));
}
function gc(now) {                         // like git gc: expire reflog, then prune old garbage
  const live = walk(Object.values(refs)), age = s => (t(now) - t(s)) / DAY;
  reflog = reflog.filter(e => age(e.at) < (live.has(e.id) ? 90 : 30)); // reflogExpire / ...Unreachable
  const keep = walk([...Object.values(refs), ...reflog.map(e => e.id)]);
  const gone = objects.filter(c => !keep.has(c) && age(born[c]) > 14);  // gc.pruneExpire: 2 weeks
  objects = objects.filter(c => !gone.includes(c));
  console.log(`gc on ${now}: reflog entries left ${reflog.length}, pruned: ${gone.join(" ") || "none"}`);
}
console.log("before any gc:"); classify();
for (const day of ["2026-01-10", "2026-02-10", "2026-04-10"]) { gc(day); classify(); }

আউটপুট:

before any gc:
  reachable from refs : A B C D
  reflog-only         : X
  unreachable         : S
gc on 2026-01-10: reflog entries left 5, pruned: none
  reachable from refs : A B C D
  reflog-only         : X
  unreachable         : S
gc on 2026-02-10: reflog entries left 4, pruned: X S
  reachable from refs : A B C D
  reflog-only         : -
  unreachable         : -
gc on 2026-04-10: reflog entries left 0, pruned: none
  reachable from refs : A B C D
  reflog-only         : -
  unreachable         : -

শুরুতে: main থেকে A–D পৌঁছানো যায়; X (reset-এ সরে যাওয়া commit) শুধু HEAD-এর reflog-এ; S (drop করা stash) কোথাও নেই। ১০ জানুয়ারি: X-এর entry মাত্র ৪ দিনের, তাই থাকে; S অনাথ, কিন্তু তার বয়স ৫ দিন, ১৪ দিনের অনুগ্রহকাল পেরোয়নি — কিছুই মোছে না। ১০ ফেব্রুয়ারি: X-এর entry ৩৫ দিনের, আর X main থেকে পৌঁছানো যায় না, তাই ৩০ দিনের নিয়মে entry বাদ; এখন X আর S দুটিই অনাথ, দুটিরই বয়স ১৪ দিনের বেশি — মুছে গেল। ১০ এপ্রিল: main-এর চারটি entry-ও ৯০ দিন পেরিয়ে ছাঁটা হলো (বাকি ০), কিন্তু কোনো commit মোছেনি — কারণ A–D এখনো main থেকে reachable. অর্থাৎ reflog entry হারানো মানে commit হারানো নয়; ref-ই আসল রক্ষাকর্তা।

৬ · git fsck — হারানো object খোঁজার যন্ত্র

“fsck” নামটি এসেছে Unix-এর file system check থেকে — ডিস্ক বিপর্যয়ের পর ফাইল সিস্টেম যাচাইয়ের টুল (Operating Systems কোর্সে দেখেছেন)। git fsck-এর প্রধান কাজও যাচাই: প্রতিটি object ঠিকঠাক আছে কি না, প্রতিটি তীর কোনো আসল object-এ পৌঁছায় কি না। কিন্তু সেই কাজ করতে গিয়ে সে জেনে যায় কোন object কারও সাথে যুক্ত নয় — আর সেটিই উদ্ধারের সোনার খনি। তিনটি শব্দ আলাদা করে চিনুন:

  • Unreachable: কোনো ref (আর ডিফল্টভাবে কোনো reflog entry) থেকে পৌঁছানো যায় না এমন যেকোনো object. git fsck --unreachable সবগুলো দেখায়।
  • Dangling: unreachable-দের মধ্যে যাদের অন্য কোনো unreachable object-ও দেখায় না — হারানো স্তূপের “মাথা”। শুধু git fsck ডিফল্টভাবে এগুলোই দেখায়।
  • Lost-found: git fsck --lost-found dangling object-গুলো .git/lost-found/-এ লিখে দেয় — commit-এর নাম commit/-এ, আর blob-এর পুরো লেখা other/-এ ফাইল হিসেবে।

একটি drop করা stash দিয়ে পার্থক্যটি পরিষ্কার হয়। একটি stash আসলে দুটি commit: একটি “index commit” (stage করা অবস্থা) আর একটি “working tree commit”, যার দুই parent — HEAD আর সেই index commit (বিস্তারিত পর্ব ৫৬-এ)। Drop করার পর:

$ git stash drop
Dropped refs/stash@{0} (7212405518b426de49689c31e3d2558d6855a0c4)
$ git fsck --unreachable
unreachable commit 1330d813af1504f305f95c343ac9423abc2c3960
unreachable blob 69cb0c298a2e0b52ee1e024cdd1b3bb542bec848
unreachable tree acbc0278ae83133fa3754529b6b242c1edf3d9fa
unreachable commit 7212405518b426de49689c31e3d2558d6855a0c4
$ git fsck
dangling commit 7212405518b426de49689c31e3d2558d6855a0c4

--unreachable চারটি object দেখাল: দুই commit, stash-এর tree আর তার ভেতরের README-র নতুন রূপের blob. সাধারণ git fsck দেখাল শুধু একটি — 7212405, কারণ বাকি তিনটিকে সে নিজেই দেখায় (index commit তার parent, tree তার snapshot, blob সেই tree-র ভেতরে)। একটি সূত্র ধরলেই পুরো স্তূপ উঠে আসে — তাই dangling তালিকাই সাধারণত যথেষ্ট।

--lost-found reflog উপেক্ষা করে। Git-এর উৎস অনুযায়ী (আর আমাদের পরীক্ষাতেও তাই দেখেছি) --lost-found চালালে fsck reflog-কে শুরুর বিন্দু ধরে না, ঠিক --no-reflogs-এর মতো। তাই এর তালিকায় সেই commit-গুলোও আসতে পারে যেগুলো আসলে reflog-এ নিরাপদে আছে — ঘাবড়াবেন না। তুলনায় সাধারণ git fsck --unreachable reflog-এ থাকা commit দেখায় না, কারণ সেগুলো এখনো “পৌঁছানো যায়”।

Stage করা কিন্তু হারানো ফাইল — dangling blob

এবার সবচেয়ে কৌতূহলজাগানো উদ্ধার। আপনি নতুন pay.js লিখে git add করেছেন, commit করেননি, তারপর ভুল করে git reset --hard. ফাইলটি ফোল্ডার থেকে উধাও — নতুন ফাইল ছিল, HEAD-এ নেই, তাই reset সেটি সরিয়ে দিয়েছে। কোনো commit নেই, তাই reflog-এ কিছু নেই। কিন্তু git add-এর মুহূর্তে Git ফাইলের লেখাটি একটি blob হিসেবে object store-এ লিখেছিল (পর্ব ৫২):

$ git add pay.js
$ git reset --hard
HEAD is now at 3db278b Update README
$ ls
README.md
cart.js
discount.js
$ git fsck --lost-found
dangling blob 83cb0fbc58b8e514b3f2e118dcfbfe4234a11bc7
$ ls .git/lost-found/*
83cb0fbc58b8e514b3f2e118dcfbfe4234a11bc7
$ cat .git/lost-found/other/83cb0fbc58b8e514b3f2e118dcfbfe4234a11bc7
function pay(method) {
  return method === "bkash" ? "ok" : "unsupported";
}

ফাইলের লেখা পুরোটাই ফিরল। কিন্তু খেয়াল করুন কী ফেরেনি: ফাইলের নাম। Blob শুধু লেখা রাখে; নাম থাকে tree-তে, আর কোনো tree তৈরিই হয়নি। তাই অনেকগুলো dangling blob থাকলে grep -l "মনে থাকা কোনো শব্দ" .git/lost-found/other/* দিয়ে খুঁজে নিতে হয়, তারপর cp করে সঠিক নামে রাখতে হয়। আর যদি add-এর পর ফাইলটি আরও বদলে থাকেন, শেষ বদলগুলো ফেরে না — ফেরে শুধু শেষবার add করা রূপ।

৭ · উদ্ধারের রেসিপি — ছয়টি পরিচিত বিপদ

সব রেসিপির প্রথম ধাপ একই, রান্নাঘরের রেসিপি খাতার “চুলা বন্ধ করুন”-এর মতো: থামুন. নতুন commit, reset, checkout বা gc নয় — প্রতিটি নতুন চলাচল reflog-এর নম্বর বদলায়, আর gc প্রমাণ মুছতে পারে। git status দেখে লিখে রাখুন কী অসংরক্ষিত আছে। তারপর:

রেসিপি ১ — মোছা branch

$ git branch -D feature/coupon
Deleted branch feature/coupon (was 3193cd0).
$ git reflog show feature/coupon
fatal: ambiguous argument 'feature/coupon': unknown revision or path not in the working tree.
…
$ git reflog | grep coupon
3db278b HEAD@{0}: checkout: moving from feature/coupon to main
3193cd0 HEAD@{1}: checkout: moving from main to feature/coupon
5f738de HEAD@{4}: checkout: moving from feature/coupon to main
3193cd0 HEAD@{5}: commit: Add coupon codes
5f738de HEAD@{6}: checkout: moving from main to feature/coupon
$ git branch feature/coupon 3193cd0
$ git log --oneline -1 feature/coupon
3193cd0 Add coupon codes

প্রথম সূত্র মোছার বার্তাতেই: (was 3193cd0). Terminal-এ সেটি না থাকলে মনে রাখুন — branch মুছলে তার নিজের reflog-ও মুছে যায় (files backend-এ; তাই git reflog show feature/coupon ব্যর্থ)। কিন্তু HEAD-এর খাতা থাকে, আর সেখানে branch-এ ঢোকা-বেরোনোর লাইনে নাম লেখা আছে। “moving from feature/coupon to main” লাইনের hash হলো main-এর, তাই সঠিক hash নিতে হয় “commit: Add coupon codes” বা “moving from main to feature/coupon” লাইন থেকে। Branch-এ কখনো checkout না করে থাকলে (যেমন অন্যের branch fetch করে মুছেছেন) HEAD-এর খাতায় কিছু থাকবে না; তখন git fsck-এর dangling commit বা remote-ই ভরসা।

রেসিপি ২ — ভুল reset --hard

$ git reset --hard HEAD~2
HEAD is now at 3457a7b Add cart total
$ git reflog show main -2
3457a7b main@{0}: reset: moving to HEAD~2
3db278b main@{1}: commit: Update README
$ git reset --hard main@{1}
HEAD is now at 3db278b Update README
$ git log --oneline -3
3db278b Update README
5f738de Add discount rule
3457a7b Add cart total

এখানে branch-এর নিজের খাতা (main@{1}) ব্যবহার করেছি, অংশ ৪-এর যুক্তিতে। Working tree-তে অসংরক্ষিত কাজ থাকলে --hard-এর আগে git branch rescue main@{1} দিয়ে commit-টিকে বাঁধুন, তারপর ভেবেচিন্তে এগোন (পর্ব ১১-এর পুরো ধাপ)।

রেসিপি ৩ — ভাঙা rebase

Rebase শেষ হওয়ার পরপরই যদি বোঝেন ভুল হয়েছে, ORIG_HEAD rebase-এর আগের মাথা দেখায় — পর্ব ১৫-এ দেখেছেন। কিন্তু মাঝখানে আরেকটি reset বা merge চললে সেটি ওপরে লেখা হয়ে যায়। তখন branch-এর খাতা দেখুন: rebase শেষে branch-এর খাতায় একটিই লাইন যোগ হয়, “rebase (finish): refs/heads/<branch> onto …”, আর তার ঠিক নিচের লাইন (<branch>@{1}) rebase-এর আগের মাথা। HEAD-এর খাতায় বরং “rebase (start)”, “rebase (pick)” ধরনের অনেক লাইন থাকে — সেখানে গুনে নম্বর বের করা ভুলপ্রবণ।

রেসিপি ৪ — drop করা stash

Stash-এর reflog-ই তার তালিকা, তাই drop বা clear করা stash-এর কোনো ref-ও নেই, reflog-ও নেই। ভরসা fsck:

$ git stash list
stash@{0}: On main: wip readme
$ git stash drop
Dropped refs/stash@{0} (7212405518b426de49689c31e3d2558d6855a0c4)
$ git fsck --unreachable | grep commit
unreachable commit 1330d813af1504f305f95c343ac9423abc2c3960
unreachable commit 7212405518b426de49689c31e3d2558d6855a0c4
$ git log -1 --format="%h %p %s" 7212405
7212405 5f738de 1330d81 On main: wip readme
$ git stash show -p 7212405518b426de49689c31e3d2558d6855a0c4
diff --git a/README.md b/README.md
index f1fcb3e..69cb0c2 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
 # Bazar app
+- cart total
$ git stash store -m "wip readme (recovered)" 7212405
$ git stash list
stash@{0}: wip readme (recovered)
Repository-র অবস্থা
git stash list
7212405 (W)
1330d81 (I)
আপনার হাতে
  1. Stash আছে। refs/stash working tree commit 7212405 (W) দেখায়; তার দুই parent — HEAD 5f738de আর index commit 1330d81 (I).
  2. git stash drop. ref আর reflog entry মুছে গেল। Git একবারই বার্তায় পুরো hash দেখায় — terminal-এ থাকলে সেটি কপি করলেই কাজ শেষ। W এখন dangling.
  3. git fsck --unreachable | grep commit. দুটি unreachable commit এলো। Repository পুরোনো হলে এখানে অনেক commit আসতে পারে — তখন git log --no-walk --format="%h %ci %s" দিয়ে সময় আর বার্তা দেখে বাছুন।
  4. সঠিকটি চেনা। git log -1 --format="%h %p %s": 7212405-এর দুই parent আর বার্তা “On main: wip readme” — এটিই W. I-এর বার্তা “index on main: …”, একটি parent; সেটি বাদ। git stash show -p-এ পুরো hash দিয়ে পরিবর্তন দেখে নিশ্চিত হলাম। (ছোট hash 7212405-এর সব অক্ষরই অঙ্ক, তাই git stash show 7212405 দিলে Git ২.৫৩ সেটিকে stash নম্বর ভেবে refs/stash@{7212405} is not a valid reference বলে — সেজন্য পুরো hash.)
  5. git stash store -m … 7212405. commit-টি আবার refs/stash-এ বসল, তালিকায় ফিরল। এখন git stash apply বা pop সাধারণ নিয়মে চলবে। সরাসরি git stash apply <পুরো hash>-ও চলে, তবে store করলে কাজ শেষ হওয়ার আগ পর্যন্ত নিরাপদে বাঁধা থাকে।

রেসিপি ৫ — add করে হারানো ফাইল

অংশ ৬-এর git fsck --lost-found: .git/lost-found/other/-এ blob-এর লেখা, নাম নিজে দিতে হবে। কাজ শেষে .git/lost-found ফোল্ডারটি মুছে দিতে পারেন; এটি শুধু একটি সুবিধার কপি।

রেসিপি ৬ — amend-এ হারানো commit

$ git commit -q --amend -m "Update README (cart, discount)"
$ git log -g -2 --format="%h %gd %gs"
a462d18 HEAD@{0} commit (amend): Update README (cart, discount)
3db278b HEAD@{1} reset: moving to HEAD
$ git branch before-amend HEAD@{1}
$ git log --oneline -1 before-amend
3db278b Update README

লক্ষ করুন, HEAD@{1}-এর কারণের লেখা “reset: moving to HEAD” (আগের রেসিপির reset --hard), “commit” নয় — কিন্তু hash-টি ঠিকই amend-এর আগের commit. কারণের লেখা বলে কীভাবে সেখানে পৌঁছেছিলেন; উদ্ধারে দরকার কোথায় ছিলেন। সেজন্য বার্তা নয়, hash আর git log দেখে নিশ্চিত হোন।

ছয়টি রেসিপি একটি খেলনা “পরিকল্পনাকারী”-তে সাজানো হলো — কোনো পরিস্থিতির নাম দিলে command-এর ক্রম ছাপে, আর অচেনা পরিস্থিতিতে অনুমান না করে মানুষকে জিজ্ঞেস করতে বলে:

// Toy recovery planner: scenario keyword -> the safe command recipe (read before you run!).
const FIRST = "git status   # stop; note what is unsaved before anything else";
const RECIPES = {
  "deleted-branch": ["git reflog | grep <branch-name>     # its own reflog died with it",
                     "git branch <branch-name> <hash>    # hash also in 'Deleted branch ... (was ...)'"],
  "reset-hard":     ["git reflog show <branch> -5        # entry 1 = tip before the reset",
                     "git branch rescue <branch>@{1}", "git reset --hard rescue   # only if tree is clean"],
  "bad-rebase":     ["git branch rescue ORIG_HEAD        # if no reset/merge ran since",
                     "git reflog show <branch> -3        # else: entry 1 = tip before 'rebase (finish)'"],
  "dropped-stash":  ["git fsck --unreachable | grep commit",
                     "git log -1 --format='%h %p %s' <hash>   # stash = 2 parents, 'On <branch>: ...'",
                     "git stash store -m 'recovered' <hash>"],
  "staged-file":    ["git fsck --lost-found              # blobs land in .git/lost-found/other/",
                     "grep -l '<text you remember>' .git/lost-found/other/*"],
  "amended":        ["git reflog -3                      # 'commit (amend)' then the old commit",
                     "git branch before-amend HEAD@{1}"],
  "never-added":    null,                                 // Git never saw it
};
function plan(scenario) {
  console.log(`# ${scenario}`);
  const steps = RECIPES[scenario];
  if (steps === undefined) return console.log("  unknown scenario - describe it to a human first\n");
  if (steps === null) return console.log("  not recoverable with Git: never added or committed.\n" +
                                         "  Try your editor's local history; commit early next time.\n");
  [FIRST, ...steps].forEach((s, i) => console.log(`  ${i + 1}. ${s}`));
  console.log("  last: git log --oneline -3 <result>   # verify, then delete rescue branches\n");
}
["dropped-stash", "reset-hard", "never-added", "force-pushed-by-colleague"].forEach(plan);

আউটপুট:

# dropped-stash
  1. git status   # stop; note what is unsaved before anything else
  2. git fsck --unreachable | grep commit
  3. git log -1 --format='%h %p %s' <hash>   # stash = 2 parents, 'On <branch>: ...'
  4. git stash store -m 'recovered' <hash>
  last: git log --oneline -3 <result>   # verify, then delete rescue branches

# reset-hard
  1. git status   # stop; note what is unsaved before anything else
  2. git reflog show <branch> -5        # entry 1 = tip before the reset
  3. git branch rescue <branch>@{1}
  4. git reset --hard rescue   # only if tree is clean
  last: git log --oneline -3 <result>   # verify, then delete rescue branches

# never-added
  not recoverable with Git: never added or committed.
  Try your editor's local history; commit early next time.

# force-pushed-by-colleague
  unknown scenario - describe it to a human first

প্রতিটি রেসিপি শুরু হয় একই নিরাপদ ধাপে (git status) আর শেষ হয় যাচাইয়ে। reset-hard-এ আগে branch বসানো, তারপর শুধু পরিষ্কার working tree-তে --hard. never-added-এর মান null — প্রোগ্রাম সৎভাবে বলে Git দিয়ে এটি ফেরে না। আর “force-pushed-by-colleague” তালিকায় নেই, তাই প্রোগ্রাম কিছু বানিয়ে না বলে থেমে যায় — একটি ভালো উদ্ধার-সহায়কের (মানুষ হোক বা AI) ঠিক এই আচরণই হওয়া উচিত। RECIPES-এ নতুন পরিস্থিতি যোগ করে চালিয়ে দেখুন।

৮ · যা ফেরে না, আর আগেভাগে নিজেকে রক্ষা

সব উদ্ধারের একটি শর্ত: জিনিসটি কোনো সময় Git-এর object store-এ ঢুকেছিল। এই সীমারেখা পরিষ্কার জানা থাকলে আতঙ্ক কমে, আর অভ্যাসও বদলায়।

যা হারিয়েছেফেরে?কোথা থেকে
Commit করা কাজ (reset, amend, rebase, branch মোছা)হ্যাঁ, সাধারণতreflog; না থাকলে fsck-এর dangling commit
Drop বা clear করা stashহ্যাঁ, gc-এর আগেfsck --unreachable
add করা কিন্তু commit না করা ফাইললেখা হ্যাঁ, নাম নাfsck --lost-found
add-এর পরের বদল, বা কখনো add না করা লেখানাGit কখনো দেখেনি; editor-এর local history-তে ভাগ্য পরীক্ষা
git clean-এ মোছা untracked ফাইলনাGit-এ কখনো ছিল না (পর্ব ১১)
gc --prune=now-এর পর অনাথ objectনা, এই repository-তেশুধু অন্য clone বা backup থেকে
অন্য কারও force push-এ server-এ মোছা commitপ্রায়ই হ্যাঁযার clone-এ আছে তার কাছ থেকে; platform-ভেদে server-এর নিজস্ব ব্যবস্থা

Server-এ reflog — আছে কি নেই?

Reflog local; push বা clone হয় না, নতুন clone-এ খাতা খালি শুরু হয়। Bare repository-তে (যেমন একটি server-এর repository) ডিফল্টভাবে reflog লেখাই হয় না — core.logAllRefUpdates-এর ডিফল্ট সাধারণ repository-তে চালু, bare-এ বন্ধ। নিজের server হলে এটি চালু করা যায়। বড় hosting platform-গুলো force push বা মোছা branch নিয়ে নিজস্ব হিসাব, activity log বা সহায়তা রাখতে পারে, কিন্তু কী থাকে, কতদিন থাকে আর কীভাবে ফেরানো যায় — তা platform-ভেদে আলাদা আর লেখার সময়ের পরে বদলাতে পারে; প্রয়োজনের আগেই নিজের platform-এর documentation পড়ে রাখুন।

আগেভাগে রক্ষার অভ্যাস

  1. ঝুঁকির কাজের আগে backup branch: git branch backup/before-rebase — দুই সেকেন্ডের কাজ, কোনো মেয়াদ নেই, gc-ও ছোঁয় না। কাজ শেষে মুছে দিন।
  2. ঘনঘন ছোট commit: নিজের branch-এ “WIP” commit লজ্জার নয়; পরে interactive rebase-এ গুছিয়ে নিন (পর্ব ১৫)। Commit করা কাজই Git বাঁচাতে পারে।
  3. Force push শুধু --force-with-lease: server-এ এমন কিছু থাকলে যা আপনি দেখেননি, push থেমে যায় (পর্ব ১৪)। দলের main-এ force push platform-এর সেটিং দিয়ে বন্ধ রাখুন (পর্ব ১৭)।
  4. Stash দীর্ঘমেয়াদি গুদাম নয়: কয়েক দিনের বেশি রাখতে হলে একটি branch-এ commit করুন। git stash clear এক আঘাতে সব মোছে।
  5. উদ্ধারের সময় gc নয়: git gc --prune=now বা git reflog expire --expire=now প্রমাণ মুছে দেয়। এগুলো ইচ্ছাকৃত পরিষ্কারের হাতিয়ার — যেমন ফাঁস হওয়া secret history থেকে সরানোর শেষ ধাপে (পর্ব ৫৭)।
  6. Script-এ ref ফাইল নয়, command: git rev-parse, git for-each-ref, git update-ref — files বা reftable, দুই backend-এই ঠিক চলে। CI pipeline-এ (পর্ব ৫৮) প্রায়ই shallow clone থাকে, যেখানে পুরো history বা reflog নেই — সেখানে এই পর্বের উদ্ধার কৌশল খাটবে না।
নাম সরানো সস্তা, object মোছা ধীর।
হারালে থামুন, খুঁজুন, branch বসান — তারপর ভাবুন।

Git-এর প্রথম দিকের version-গুলোতে (২০০৫) reflog ছিল না; ভুল reset-এর পর hash মনে না থাকলে git fsck-এর dangling তালিকা ঘাঁটাই ছিল উপায়। যতদূর জানা যায়, reflog যোগ হয় ২০০৬ সালে, মূলত Shawn O. Pearce-এর কাজে, আর পরে সাধারণ repository-তে ডিফল্টভাবে চালু করা হয়। একই সময়ের কাছাকাছি ref-এর সংখ্যা বাড়ায় packed-refs এসেছিল। পরে বহু ref-ওয়ালা server-এর প্রয়োজনে reftable-এর নকশা হয়, আর ২০২৪ সালে Git ২.৪৫-এ এটি মূল Git-এ বেছে নেওয়ার সুযোগ আসে।

আরও গভীরে: Scott Chacon ও Ben Straub-এর বিনামূল্যের বই Pro Git (২য় সংস্করণ, ২০১৪)-এর শেষ অধ্যায় “Git Internals”-এ “Maintenance and Data Recovery” অংশটি ঠিক এই পর্বের বিষয়: reflog, fsck --full আর gc. Ref-এর নাম, packed-refs-এর ফরম্যাট আর logs/ ফোল্ডারের বিবরণ আছে Git-এর gitrepository-layout(5) manual-এ; reftable-এর বিস্তারিত নকশা Git-এর technical documentation-এ। মেয়াদের সঠিক নিয়ম জানতে git help gc, git help reflog আর git help fsck — আপনার যন্ত্রের Git version-এর documentation-ই চূড়ান্ত।

পরীক্ষা ও ইন্টারভিউতে:

  • Branch মুছে ফেলেছেন, কীভাবে ফেরাবেন? — মোছার বার্তার (was …) বা git reflog-এ hash খুঁজে git branch <নাম> <hash>; branch-এর নিজের reflog মুছে যায়, HEAD-এরটা থাকে।
  • HEAD@{1} আর @{1}-এর পার্থক্য? — প্রথমটি HEAD-এর reflog, দ্বিতীয়টি বর্তমান branch-এর reflog; branch বদলালে দুটি ভিন্ন commit দেয়।
  • Dangling আর unreachable object? — unreachable মানে কোনো ref/reflog থেকে পৌঁছানো যায় না; dangling হলো তাদের মধ্যে যাদের অন্য কোনো unreachable object-ও দেখায় না।
  • git gc কি reset করা commit সঙ্গে সঙ্গে মোছে? — না; reflog entry-র মেয়াদ (ডিফল্ট unreachable-এর জন্য ৩০ দিন) আর অনাথ object-এর অনুগ্রহকাল (২ সপ্তাহ) পেরোতে হয়।
  • Drop করা stash কীভাবে ফেরাবেন? — git fsck --unreachable | grep commit, দুই parent আর “On <branch>” বার্তা দেখে বেছে git stash store বা git stash apply <hash>.

৯ · AI-এর যুগে — উদ্ধারের পরামর্শ যাচাই

কিছু হারালে মানুষ ভয় পায়, আর ভয়ের মুহূর্তে AI-এর আত্মবিশ্বাসী উত্তর খুব আকর্ষণীয়। কিন্তু উদ্ধারের ক্ষেত্রে একটি ভুল command পুরো সুযোগটাই শেষ করে দিতে পারে — বিশেষ করে “repository পরিষ্কার করুন” ধাঁচের পরামর্শ। এই বিষয়ে AI-এর পরিচিত ভুলগুলো:

AI যা বলতে পারেসমস্যাযা করবেন
“আগে git gc --prune=now দিয়ে repository পরিষ্কার করুন”ঠিক যে অনাথ object খুঁজছেন, সেটিই মুছে যায়উদ্ধার শেষ না হওয়া পর্যন্ত gc, prune, reflog expire নয়
বানানো command: git undo, git reflog --restore, git stash recoverএমন কিছু Git-এ নেই; সময় নষ্ট, বিভ্রান্তিgit help reflog, git help stash দেখে মিলিয়ে নিন
“মোছা branch-এর reflog এখনো আছে, git reflog show feature দিন”Branch মুছলে তার reflog-ও যায়HEAD-এর reflog বা fsck
HEAD@{n} আর @{n} বা @{-n} গুলিয়ে ফেলাভুল commit-এ resetআগে git log -1 <selector> দিয়ে দেখুন
“fsck --lost-found আপনার unsaved লেখা ফিরিয়ে দেবে”শুধু add করা blob ফেরে, তাও নামহীনপ্রত্যাশা ঠিক রাখুন; অভ্যাসে commit/stash
মেয়াদ নিয়ে নিশ্চিত সংখ্যা, version না বলেConfig আর version-ভেদে আলাদাgit config --get gc.reflogExpire, git help gc

AI-কে অবস্থাটা পুরো দিন, আর স্পষ্ট বলুন কোন কাজ নিষেধ। প্রতিটি command আগে একটি throwaway কপিতে চালান — পুরো .git-সহ ফোল্ডারটি অন্য জায়গায় কপি করে নিলেই হয় (clone নয়, কারণ clone reflog আর অনাথ object আনে না):

আমি Git-এ একটি জিনিস হারিয়েছি। অবস্থা: - কী ঘটেছিল: [যেমন: git stash drop দিয়েছি / branch -D দিয়েছি / reset --hard দিয়েছি] - git status, git reflog -15 আর git fsck --unreachable | grep commit এর আউটপুট নিচে দিলাম: [এখানে আউটপুট পেস্ট করুন] তুমি আমাকে উদ্ধারের ধাপগুলো বলো, এই শর্তে: ১. git gc, git prune, git reflog expire বা কোনো "পরিষ্কার" command একদম বলবে না। ২. প্রথম কাজ হবে হারানো commit-এ একটি নতুন branch বসানো, তারপর অন্য কিছু। ৩. প্রতিটি command কোন ref বা কোন এলাকা বদলাবে, এক লাইনে বলো। ৪. HEAD@{n}, @{n} বা @{-n} ব্যবহার করলে কোন খাতা পড়ছ ব্যাখ্যা করো, আর আগে git log -1 দিয়ে মিলিয়ে নিতে বলো। ৫. কোনো command বা flag নিয়ে নিশ্চিত না হলে বলো; git help দিয়ে কোনটি মিলিয়ে নেব জানাও।

AI-এর উদ্ধার-পরামর্শ চালানোর আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Git অনাথ object পেলেই মুছে ফেলে না কেন? --prune=now তো আছেই — দুই সপ্তাহ অপেক্ষার মানে কী?

প্রথম কারণ মানুষ। ভুল reset, ভুল stash drop বা ভুল branch মোছার পর মানুষ সাধারণত কয়েক ঘণ্টা বা কয়েক দিন পরে বোঝেন কী হারিয়েছে। Git যদি অনাথ object সঙ্গে সঙ্গে মুছত, তাহলে প্রতিটি ছোট ভুল স্থায়ী হতো। অনুগ্রহকাল হলো একটি ইচ্ছাকৃত “recycle bin” — জায়গা সামান্য বেশি লাগে, কিন্তু ভুল শোধরানোর সময় পাওয়া যায়। Reflog-এর ৯০ আর ৩০ দিনের মেয়াদও একই দর্শন থেকে।

দ্বিতীয় কারণ যন্ত্র, আর এটি কম পরিচিত কিন্তু গুরুত্বপূর্ণ। Git-এর অনেক কাজ কয়েক ধাপে হয়: আগে object লেখা, পরে ref বসানো। ধরুন একটি command সবে একটি commit-এর object লিখেছে, কিন্তু branch এখনো সরায়নি — ঠিক সেই মুহূর্তে অন্য একটি process gc চালালে নতুন object-টিকে “অনাথ” দেখবে। বয়সের শর্ত না থাকলে সেটি মুছে যেত, আর তারপর branch এমন একটি commit দেখাত যার অস্তিত্বই নেই — repository নষ্ট। Git-এর git gc documentation-এ স্পষ্ট লেখা আছে যে --prune=now বয়স না দেখে মোছে, আর অন্য process একই সময়ে লিখতে থাকলে corruption-এর ঝুঁকি বাড়ায়। অর্থাৎ দুই সপ্তাহের নিয়ম শুধু মানুষের ভুলের বীমা নয়, একসাথে চলা কাজের মধ্যে একটি নিরাপত্তা দেয়ালও।

এই নকশা অন্য জায়গাতেও দেখা যায়। ফাইল সিস্টেমের journaling (Operating Systems কোর্সে) আগে লেখে, পরে “চূড়ান্ত” করে; database-এর অনেক ব্যবস্থা মোছা data সঙ্গে সঙ্গে সরায় না, পরে আলাদা ধাপে পরিষ্কার করে। মূল নীতি একই: “মুছে ফেলা” আর “জায়গা খালি করা” দুটি আলাদা ঘটনা, আর দুইয়ের মাঝখানের সময়টাই নিরাপত্তা।

তাহলে --prune=now কখন? যখন সত্যিই কিছু চিরতরে সরাতে চান আর নিশ্চিত যে আর কোনো Git process চলছে না — যেমন ফাঁস হওয়া secret history থেকে সরানোর পর (পর্ব ৫৭), বা একটি বড় import-এর পর repository গোছাতে।

মূল উপলব্ধি: Git-এর ধীর মোছা কোনো দুর্বলতা নয়, নকশার সিদ্ধান্ত — মানুষের ভুল আর যন্ত্রের একসাথে চলা কাজ, দুইয়ের বিরুদ্ধেই বীমা। এই বীমা ভাঙার command জেনে-বুঝে, সঠিক মুহূর্তে।

প্র ০২প্রতি ref-এ একটি ফাইল — এত সহজ নকশা থেকে Git কেন packed-refs আর পরে reftable-এর দিকে গেল?

শুরুর নকশাটি ছিল ইউনিক্স-ধাঁচের সরলতার উদাহরণ: branch মানে একটি ফাইল, ভেতরে ৪১ বাইট। cat দিয়ে পড়া যায়, echo দিয়ে লেখা যায়, কোনো বিশেষ টুল লাগে না। ছোট project-এ আজও এটি চমৎকার কাজ করে। কিন্তু বাস্তবের repository বড় হলো। কিছু project-এ হাজারো release tag; hosting platform-গুলোতে প্রতিটি pull request-এর জন্য আলাদা ref; বড় monorepo-তে লাখের বেশি ref পর্যন্ত হতে পারে। প্রতিটি ছোট ফাইল ডিস্কে অন্তত একটি block নেয়, আর “সব tag দেখাও” মানে হাজারো ফাইল খোলা।

packed-refs এই সমস্যার প্রথম সমাধান: বেশিরভাগ ref কদাচিৎ বদলায় (পুরোনো tag কখনোই না), তাই সেগুলো এক সাজানো ফাইলে, আর ঘনঘন বদলানো কয়েকটি loose. সুন্দর আপস — কিন্তু নতুন জটিলতাও আনল: এখন একটি ref-এর মান দুই জায়গায় থাকতে পারে, “loose জেতে” নিয়ম লাগে, আর একটি ref মুছতে পুরো খাতা নতুন করে লিখতে হয়।

আরও কিছু সমস্যা ফাইল-ভিত্তিক নকশার মূলে ছিল। Windows আর macOS-এর ডিফল্ট ফাইল সিস্টেম বড়-ছোট হাতের অক্ষর আলাদা করে না, তাই Fix আর fix branch একই ফাইল হয়ে গোলমাল করে। a আর a/b নামে একসাথে ref রাখা যায় না, কারণ একটি ফাইল আর একটি ফোল্ডার একই নামে থাকতে পারে না। আর একসাথে অনেক ref বদলানো (যেমন একটি atomic push) ফাইল সিস্টেমের ওপর পুরোপুরি এক ধাক্কায় করা কঠিন।

Reftable এগুলোর উত্তরে একটি আলাদা ফরম্যাট: ref আর reflog সাজানো binary table-এ, নতুন বদল নতুন ছোট table হিসেবে যোগ হয়, আর মাঝে মাঝে table-গুলো মিলিয়ে নেওয়া হয়। যতদূর জানা যায়, নকশাটি প্রথম আসে Shawn Pearce-এর হাতে JGit-এর জন্য, আর মূল Git-এ আনার বড় কাজটি করেন GitLab-এর Patrick Steinhardt-সহ অন্যান্য contributor; Git ২.৪৫ (২০২৪) থেকে এটি বেছে নেওয়া যায়। কিন্তু এর একটি দাম আছে: ref আর cat দিয়ে পড়া যায় না। পুরোনো script, যা সরাসরি .git/refs পড়ত, ভাঙবে।

মূল উপলব্ধি: সরল নকশা দিয়ে শুরু করা ঠিক, কিন্তু মাপ বাড়লে নকশাকে বদলাতে হয়। Interface (git for-each-ref, update-ref) স্থির রেখে ভেতরের storage বদলানো — ভালো software engineering-এর একটি ক্লাসিক কৌশল, যা পর্ব ৩১-এর contract-এর ধারণার সাথে মেলে।

প্র ০৩Reflog push হয় না কেন? সবার reflog server-এ জমা থাকলে কি উদ্ধার আরও সহজ হতো না?

প্রথমে ভাবুন reflog আসলে কী লেখে: আপনার প্রতিটি checkout, প্রতিটি পরীক্ষামূলক commit, প্রতিটি reset — কাজের খসড়া খাতা। পরীক্ষার খাতার খসড়া পাতা যেমন জমা দেওয়া হয় না, তেমনি reflog-ও আপনার ব্যক্তিগত কাজের ধারা। এতে এমন commit থাকতে পারে যা আপনি ইচ্ছা করেই কখনো শেয়ার করেননি — ভুল চেষ্টা, ব্যক্তিগত নোট, এমনকি ভুলে commit করা কোনো গোপন তথ্য যা পরে amend করে সরিয়েছেন। সব push হলে “নিজের branch গুছিয়ে তারপর শেয়ার” ধারণাটাই ভেঙে যেত।

দ্বিতীয় কারণ গঠনগত। Reflog-এর entry একটি নির্দিষ্ট repository-র নির্দিষ্ট ref-এর ইতিহাস; অন্য repository-তে একই নামের ref-এর ইতিহাস সম্পূর্ণ আলাদা। আপনার main@{3} আর সহকর্মীর main@{3}-এর কোনো সম্পর্ক নেই। Distributed version control-এ (পর্ব ৭) প্রতিটি clone স্বাধীন, তাই “কার খাতা সত্য” প্রশ্নের কোনো অর্থপূর্ণ উত্তর নেই।

তাহলে server-এ কী হয়? Git-এর ডিফল্টে bare repository-তে reflog লেখা বন্ধ (core.logAllRefUpdates), কারণ সেখানে কেউ “কাজ” করে না, শুধু push আসে। নিজের server চালালে এটি চালু করে force push-এর হিসাব রাখা যায় — ছোট দলের জন্য সস্তা বীমা। বড় hosting platform-গুলো নিজস্ব ব্যবস্থায় ঘটনা লেখে বা সাময়িকভাবে পুরোনো commit রাখতে পারে, কিন্তু এর ধরন আর সময়সীমা platform-ভেদে আলাদা, তাই তার ওপর ভরসার আগে documentation পড়া দরকার।

সবচেয়ে বাস্তব উদ্ধার-উৎস তাই মানুষ: কেউ force push করে main-এর commit মুছে দিলে প্রায়ই অন্য কোনো সহকর্মীর clone-এ সেই commit-গুলো থাকে — তাঁর origin/main-এর reflog-এ বা সরাসরি branch-এ। তিনি একটি branch বসিয়ে push করলেই ফেরে। এটি distributed নকশার স্বাভাবিক সুফল।

মূল উপলব্ধি: Reflog ব্যক্তিগত খসড়া, ইতিহাস নয়। শেয়ার করা ইতিহাসের সুরক্ষা আসে অন্য পথে — protected branch, --force-with-lease, সার্ভারের নিজস্ব হিসাব আর দলের প্রতিটি clone.

প্র ০৪“কখনো add না করা পরিবর্তন ফেরে না” — এই সীমা মেনে নিয়ে একজন developer কোন অভ্যাস গড়বেন? Git কি এটি ঠিক করতে পারত না?

Git পারত — যদি প্রতিবার ফাইল সংরক্ষণের সময় একটি snapshot নিত। কিন্তু Git নকশাগতভাবে ফাইল সিস্টেম পাহারা দেয় না; আপনি যখন command দেন, শুধু তখনই দেখে। এর পেছনে যুক্তি আছে: পাহারাদার process ব্যাটারি আর CPU খায়, প্রতিটি খসড়া সংরক্ষণ repository-কে অপ্রয়োজনীয় object-এ ভরে দেয়, আর “কোনটি অর্থবহ পরিবর্তন” সেই সিদ্ধান্ত মানুষের হাতে রাখাই Git-এর দর্শন। Commit একটি সচেতন কাজ — “এই অবস্থাটা মনে রাখার মতো”।

এই শূন্যস্থান ভরায় অন্য টুল। অনেক আধুনিক editor আর IDE-তে নিজস্ব “local history” থাকে, যা প্রতিটি save-এ পুরোনো রূপ কিছুদিন রাখে; ক্লাউড সিঙ্ক বা অপারেটিং সিস্টেমের file history-ও একটি স্তর। এগুলো Git নয়, আর এদের মেয়াদ ও আচরণ টুল-ভেদে আলাদা — তবু বিপদের দিনে প্রথমে এগুলোই খুঁজে দেখার মতো।

অভ্যাসের দিক থেকে কয়েকটি নিয়ম কাজের। এক, নিজের branch-এ ঘনঘন ছোট commit — “WIP: cart validation half done” লিখতে কোনো লজ্জা নেই, পরে interactive rebase-এ গুছিয়ে নেবেন। দুই, ঝুঁকির command (reset --hard, checkout -- ., restore ., clean) চালানোর আগে git status আর git stash; stash অন্তত object store-এ ঢোকায়, আর drop হলেও কিছুদিন fsck দিয়ে ফেরে। তিন, বড় কাজের মাঝখানে git add-ও একধরনের বীমা: lost-found থেকে অন্তত শেষ add করা রূপ ফেরে।

এখানে Word বা Google Docs-এর autosave-এর সাথে তুলনা শিক্ষণীয়। সেখানে সব কিছু স্বয়ংক্রিয়, কিন্তু “কোন সংস্করণটি অর্থবহ” তা খুঁজে বের করা কঠিন। Git উল্টো: স্বয়ংক্রিয় কিছু নয়, কিন্তু যা রাখে তা সুনির্দিষ্ট, বার্তাসহ আর দলের সাথে শেয়ারযোগ্য। দুই দর্শনের trade-off বোঝা পেশাদার টুল বাছাইয়ের একটি অংশ।

মূল উপলব্ধি: Git শুধু সেটুকুই বাঁচায় যা আপনি তাকে দিয়েছেন। ঘনঘন commit আর ঝুঁকির আগে stash — এই দুই অভ্যাসই সবচেয়ে বড় উদ্ধার-কৌশল।

প্র ০৫Git-এর “প্রায় কিছুই হারায় না” স্বভাব কখন বিপদ? ফাঁস হওয়া একটি password history থেকে “মুছে ফেলা” কেন এত কঠিন?

এই পর্বের পুরোটা জুড়ে Git-এর ধীর মোছাকে আশীর্বাদ বলেছি। কিন্তু একই বৈশিষ্ট্য উল্টো দিকে কাটে: যা মুছে ফেলতে চান, তাও সহজে যায় না। ধরুন একটি database password commit হয়ে গেছে। আপনি commit --amend করলেন — পুরোনো commit reflog-এ ৩০ দিন, তারপর আরও দুই সপ্তাহ object হিসেবে। ফাইল থেকে সরিয়ে নতুন commit দিলেন — পুরোনো commit history-তে চিরকাল। Push হয়ে থাকলে প্রতিটি সহকর্মীর clone, প্রতিটি fork, CI-এর cache, এমনকি hosting platform-এর নিজস্ব cache-এ কপি থাকতে পারে।

History থেকে সত্যিই সরাতে অনেকগুলো স্তর পার হতে হয়: git filter-repo-এর মতো টুল দিয়ে প্রতিটি commit নতুন করে লেখা (সব hash বদলায়), তারপর local-এ git reflog expire --expire=now --all আর git gc --prune=now — ঠিক যে “বিপজ্জনক” command-গুলো থেকে এই পর্বে দূরে থাকতে বলেছি। তারপর force push, সব সহকর্মীকে নতুন করে clone করতে বলা, আর platform-এর cache বা fork নিয়ে আলাদা ব্যবস্থা। পুরো পদ্ধতি পর্ব ৫৭-এ।

আর সবচেয়ে গুরুত্বপূর্ণ কথা: এত কিছুর পরও ধরে নিতে হয় যে password-টি কেউ দেখে ফেলেছে। তাই সঠিক ক্রম হলো আগে password বদলানো (rotate), তারপর history পরিষ্কার। History পরিষ্কার “আর ছড়াবে না” নিশ্চিত করে, “কেউ দেখেনি” নয়।

এখানে একটি গভীর engineering শিক্ষা আছে। যে ব্যবস্থা ডেটা হারাতে দেয় না, সে ডেটা মুছতেও বাধা দেয় — নির্ভরযোগ্যতা আর মুছে ফেলার অধিকার পরস্পরবিরোধী চাপ। Backup, replication, event log — সব জায়গায় একই টানাপোড়েন (System Design কোর্সে backup আর disaster recovery). ব্যক্তিগত তথ্য রাখার আইনি দায় থাকলে এই প্রশ্ন আরও গুরুতর; নির্দিষ্ট দেশের নিয়মের জন্য আইনি পরামর্শ লাগে।

মূল উপলব্ধি: Git-এর নিরাপত্তা জাল দুই দিকেই ধরে রাখে — ভুল করে মোছা কাজও, ভুল করে রাখা গোপন তথ্যও। তাই secret কখনো commit-এ নয়; একবার গেলে প্রথম কাজ secret বদলানো।

অনুশীলন

আউটপুট অনুমান, মেয়াদের হিসাব, উদ্ধারের command-ক্রম আর দলের অভ্যাসের সমালোচনা — প্রতিটির পূর্ণ উত্তর দেওয়া আছে। নিজে চালাতে চাইলে একটি scratch ফোল্ডার ব্যবহার করুন।

  1. আউটপুট অনুমান করুন (reflog আর fsck): একটি নতুন repository-তে (ডিফল্ট config) পরপর: commit A (180ad65), commit B (98ccd74) main-এ; git switch -c topic; commit D (4fa2ed3); git switch main; git branch -D topic. এবার বলুন: (ক) git log --oneline --all কী দেখাবে? (খ) git reflog-এর পাঁচটি লাইন কী কী? (গ) git rev-parse --abbrev-ref @{-1} কী করবে? (ঘ) git fsck আর git fsck --no-reflogs কী দেখাবে? (ঙ) git rev-parse --short main@{1}?

    Git ২.৫৩-এ আসল আউটপুট (একই নির্দিষ্ট তারিখ আর লেখক দিয়ে):

    $ git log --oneline --all
    98ccd74 B
    180ad65 A
    $ git reflog
    98ccd74 HEAD@{0}: checkout: moving from topic to main
    4fa2ed3 HEAD@{1}: commit: D
    98ccd74 HEAD@{2}: checkout: moving from main to topic
    98ccd74 HEAD@{3}: commit: B
    180ad65 HEAD@{4}: commit (initial): A
    $ git rev-parse --abbrev-ref @{-1}
    fatal: ambiguous argument '@{-1}': unknown revision or path not in the working tree.
    …
    $ git fsck
    $ git fsck --no-reflogs
    dangling commit 4fa2ed319f90a136ce2474c7f693af2e57e26602
    $ git rev-parse --short main@{1}
    180ad65

    (ক) --all শুধু ref থেকে শুরু করে; D-কে কোনো ref দেখায় না, তাই নেই। (খ) HEAD-এর খাতা branch মোছায় বদলায় না — D-সহ পাঁচটি চলাচল আছে। (গ) Git খাতা পড়ে নাম “topic” পায়, কিন্তু সেই branch আর নেই, তাই resolve করতে না পেরে ব্যর্থ হয় — @{-1} নাম দেয়, hash নয়। (ঘ) সাধারণ fsck reflog-কে শুরুর বিন্দু ধরে, আর HEAD@{1} D দেখায়, তাই কিছুই dangling নয়; --no-reflogs-এ D dangling. (ঙ) main-এর নিজের খাতায় মাত্র দুটি লাইন (A, তারপর B), তাই main@{1} = A = 180ad65 — HEAD@{1}-এর (4fa2ed3) সাথে গুলিয়ে ফেলবেন না।

  2. মেয়াদের হিসাব: ডিফল্ট config (gc.reflogExpireUnreachable = ৩০ দিন, gc.pruneExpire = ২ সপ্তাহ)। ১ মার্চ সকাল ১০টায় একটি commit X বানিয়ে সেদিনই commit --amend করেছেন; X এখন শুধু HEAD-এর reflog-এ, আর তার object ফাইলটিও ১ মার্চ লেখা। Repository-তে স্বয়ংক্রিয় gc চলেছে ১০ মার্চ, ২৫ মার্চ, ২ এপ্রিল আর ২০ এপ্রিল (প্রতিবার সকাল ১০টায়)। (ক) কোন gc-তে X প্রথম মোছার যোগ্য হবে? (খ) যদি gc.reflogExpireUnreachable = never হতো? (গ) যদি amend-এর বদলে X-এর ওপর একটি branch keep থাকত?

    (ক) দুই শর্ত লাগে: reflog entry ৩০ দিনের বেশি পুরোনো (তাহলে entry ছাঁটা হয়ে X অনাথ), আর object ফাইলটি ১৪ দিনের বেশি পুরোনো।

    • ১০ মার্চ: entry-র বয়স ৯ দিন — থাকে। X reflog থেকে reachable, মোছে না।
    • ২৫ মার্চ: বয়স ২৪ দিন, এখনো ৩০-এর কম — থাকে।
    • ২ এপ্রিল: বয়স ৩২ দিন — entry ছাঁটা হয়; X অনাথ, আর object-এর বয়স ৩২ দিন > ১৪ — এই gc-তেই মোছে।

    অর্থাৎ নিশ্চিত উদ্ধারের শেষ সুযোগ ছিল ২ এপ্রিলের gc-এর আগ পর্যন্ত। (মার্চ মাস ৩১ দিনের: ১ মার্চ থেকে ২ এপ্রিল = ৩১ + ১ = ৩২ দিন।) অংশ ৫-এর JavaScript মডেলে born আর at বদলে এই হিসাব মিলিয়ে নিতে পারেন।

    (খ) Entry কখনো unreachable-নিয়মে ছাঁটা হবে না, তাই X reflog থেকে reachable থাকবে আর কোনো gc তাকে মুছবে না — যতদিন না কেউ হাতে reflog expire চালান বা entry মোছেন। (গ) X একটি ref থেকে reachable; reflog entry ৯০ দিন পর ছাঁটা হলেও commit কখনো মোছে না। Ref-ই আসল রক্ষাকর্তা।

  3. উদ্ধারের command-ক্রম: একজন সহকর্মী ক্লান্ত হয়ে তিনটি ভুল পরপর করেছেন: (১) feature/report branch-এ interactive rebase করে দুটি commit ভুলভাবে মিলিয়ে ফেলেছেন; (২) ঘাবড়ে গিয়ে git reset --hard HEAD~1 দিয়েছেন; (৩) তার আগে একটি stash ছিল, “জায়গা খালি করতে” git stash clear দিয়েছেন। কেউ gc চালায়নি। তাঁকে ধাপে ধাপে কী করতে বলবেন, আর কোন ভুল এড়াতে বলবেন?
    1. থামা আর ছবি তোলা: git status; আর কোনো commit, reset বা gc নয়। সম্ভব হলে পুরো ফোল্ডারের (.git-সহ) একটি কপি অন্য জায়গায়।
    2. Rebase-এর আগের অবস্থা: git reflog show feature/report -5. সবচেয়ে ওপরে reset-এর লাইন, তার নিচে “rebase (finish): refs/heads/feature/report onto …”, তার নিচে rebase-এর আগের মাথা — এটিই চাই। এখানে সেটি feature/report@{2} (reset আর rebase, দুই চলাচল আগে); নম্বর অনুমান নয়, খাতা দেখে।
    3. বাঁধা: git branch rescue/before-rebase feature/report@{2}, তারপর git log --oneline -5 rescue/before-rebase দিয়ে মিলিয়ে নেওয়া যে সব commit আলাদা আলাদা আছে। ORIG_HEAD এখানে ভরসা নয় — reset সেটি ওপরে লিখে দিয়েছে।
    4. Stash: git fsck --unreachable | grep commit; প্রতিটির জন্য git log -1 --format="%h %p %ci %s" <hash>. দুই parent আর “On feature/report: …” বা “WIP on …” বার্তার commit-টি stash; “index on …” বার্তারটি বাদ। তারপর git stash store -m "recovered" <hash>.
    5. Branch ফেরানো: working tree পরিষ্কার নিশ্চিত করে git switch feature/report আর git reset --hard rescue/before-rebase; চাইলে এবার সাবধানে আবার rebase.
    6. যাচাই আর পরিষ্কার: git log --oneline, git stash list, git stash show -p stash@{0}; সব ঠিক হলে rescue branch মুছে দেওয়া।

    যে ভুলগুলো এড়াবেন: HEAD@{n} দিয়ে গোনা (HEAD-এর খাতায় rebase-এর অনেক “pick” লাইন থাকে); উদ্ধারের মাঝে git gc বা git prune; আর stash-এর জন্য reflog show refs/stash খোঁজা — clear সেটিও মুছে দেয়।

  4. দলের অভ্যাসের সমালোচনা: একটি কাল্পনিক অনলাইন বাজার অ্যাপের দল একটি “repository স্বাস্থ্য নীতি” লিখেছে। প্রতিটির ঝুঁকি কী, আর কীভাবে লিখলে ভালো হতো? (১) “প্রতি শুক্রবার সবাই git reflog expire --expire=now --all && git gc --prune=now চালাবেন, যাতে repository ছোট থাকে।” (২) “অর্ধেক কাজ stash-এ রাখুন, branch বানানোর দরকার নেই; পুরোনো stash মাসে একবার git stash clear.” (৩) “Server-এ reflog আছে, তাই কেউ ভুল করে force push করলেও চিন্তা নেই।” (৪) “Deploy script cat .git/refs/heads/main পড়ে কোন commit deploy হবে ঠিক করবে।”

    (১) এটি প্রতি সপ্তাহে পুরো নিরাপত্তা জাল ছিঁড়ে ফেলা: শুক্রবারের আগে যে-কারও ভুল reset, amend বা মোছা branch চিরতরে যায়। আর অন্য Git process চলার সময় --prune=now repository নষ্টের ঝুঁকিও বাড়ায় (documentation-এর সতর্কবার্তা)। লাভ সামান্য — Git নিজেই gc --auto-তে গুছিয়ে নেয়। ভালো রূপ: “gc Git-এর ডিফল্টে চলবে; --prune=now আর reflog expire --expire=now শুধু নথিভুক্ত কারণে (যেমন secret সরানো), দলকে জানিয়ে।”

    (২) Stash-এর কোনো নাম বা বর্ণনামূলক branch নেই, push হয় না, অন্য যন্ত্রে নেই, আর clear এক আঘাতে সব মোছে — উদ্ধার শুধু fsck দিয়ে, gc-এর আগে। ভালো রূপ: “এক-দুই দিনের বেশি কাজ নিজের branch-এ WIP commit, প্রয়োজনে push; stash শুধু কয়েক ঘণ্টার জন্য; মোছার আগে git stash list আর git stash show.”

    (৩) Bare repository-তে Git ডিফল্টভাবে reflog লেখে না, আর hosting platform কী রাখে তা platform-ভেদে আলাদা — যাচাই না করা অনুমান। ভালো রূপ: “main আর release branch-এ force push platform-এ বন্ধ; নিজের branch-এ শুধু --force-with-lease; নিজস্ব server হলে core.logAllRefUpdates চালু; platform-এর উদ্ধার-ব্যবস্থা documentation দেখে লিখে রাখা।”

    (৪) pack-refs বা gc-এর পর loose ফাইল নাও থাকতে পারে (তখন script ভাঙবে বা ফাঁকা পড়বে), আর reftable repository-তে ফাইলটি শুধু একটি পাহারাদার লেখা। ভালো রূপ: “Script git rev-parse --verify refs/heads/main দিয়ে hash নেবে।”

    সাধারণ শিক্ষা: নিরাপত্তা-নীতি লিখতে হয় Git আসলে কীভাবে কাজ করে তা জেনে, আর প্রতিটি নিয়মের পাশে কারণসহ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমভুল করে git stash drop করা একটি stash ফেরানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন কোনো command বা gc না চালিয়ে থামা
  2. git fsck --unreachable | grep commit দিয়ে প্রার্থী commit খোঁজা
  3. প্রতিটির parent আর বার্তা দেখে দুই-parent-এর stash commit চেনা
  4. git stash store দিয়ে commit-টিকে আবার refs/stash-এ বসানো
  5. git stash apply করে git diff দিয়ে যাচাই

একটি repository-তে .git/packed-refs-এ লেখা 5f738de… refs/heads/main, আর .git/refs/heads/main ফাইলে লেখা 3db278b…. git rev-parse main কী দেবে?

  • 5f738de…, কারণ packed-refs প্রধান
  • একটি error — দুই জায়গায় দুই মান
  • 3db278b…, কারণ loose ref জেতে
  • যেটি নতুন সময়ের, সেটি

আপনি main-এ আছেন। HEAD-এর reflog-এর ওপরের দুই লাইন: HEAD@{0}: checkout: moving from feature to main, HEAD@{1}: checkout: moving from main to feature (তখন feature ছিল c4-এ)। main-এর reflog: main@{0} = m3, main@{1} = m2. এখন git rev-parse HEAD@{1} আর git rev-parse @{1} কী দেবে?

  • দুটিই c4
  • HEAD@{1} = c4, @{1} = m2
  • HEAD@{1} = m3, @{1} = c4
  • @{1} একটি branch-এর নাম দেবে — feature

git branch -D feature/coupon দেওয়ার পর git reflog show feature/coupon চালালে fatal: ambiguous argument আসে। Branch-টির শেষ commit কোথায় খুঁজবেন?

  • ORIG_HEAD-এ
  • .git/FETCH_HEAD-এ
  • .git/packed-refs হাতে খুলে
  • মোছার বার্তার (was …)-তে, বা git reflog (HEAD-এর খাতা)-এ

একটি stash drop করার পর git fsck --unreachable দুটি commit, একটি tree আর একটি blob দেখায়, কিন্তু শুধু git fsck দেখায় একটিমাত্র dangling commit. কেন?

  • বাকি তিনটিকে ওই commit-টিই দেখায়; dangling শুধু হারানো স্তূপের “মাথা”
  • সাধারণ git fsck বাকি তিনটি মুছে ফেলেছে
  • tree আর blob কখনো dangling হতে পারে না
  • বাকিগুলো reflog-এ আছে

ডিফল্ট config-এ একটি commit reset করে সরানো হয়েছে; সেটি এখন শুধু HEAD-এর reflog-এর একটি ১০ দিন পুরোনো entry থেকে পৌঁছানো যায়, আর তার object ফাইলও ১০ দিন পুরোনো। এখন git gc (কোনো option ছাড়া) চালালে কী হবে?

  • Commit মুছে যাবে, কারণ কোনো branch তাকে দেখায় না
  • Reflog entry ছাঁটা হবে, কিন্তু object থাকবে
  • কিছুই মুছবে না — entry ৩০ দিনের মেয়াদের মধ্যে, তাই commit সুরক্ষিত
  • Commit-টি pack-এ চলে যাবে, তাই আর ফেরানো যাবে না

একটি নতুন ফাইল pay.js লিখে git add করার পর আরও কয়েক লাইন যোগ করলেন (আর add করেননি), তারপর ভুলে git reset --hard. git fsck --lost-found কী ফেরাতে পারে?

  • ফাইলের শেষ রূপ, নামসহ
  • শেষবার add করা রূপের লেখা, hash-নামের একটি ফাইলে; নাম আর পরের বদল নয়
  • কিছুই না, কারণ reflog-এ নেই
  • শুধু commit করা থাকলে তবেই কিছু

.git/HEAD ফাইলে লেখা আছে ref: refs/heads/main. এ থেকে কোনটি নিশ্চিতভাবে বলা যায়?

  • HEAD একটি symbolic ref, main branch-এ আছেন — detached নয়
  • HEAD detached অবস্থায় আছে
  • main-এর hash অবশ্যই .git/refs/heads/main ফাইলে আছে
  • Repository-টি reftable backend ব্যবহার করছে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Ref = নাম, object = অপরিবর্তনীয় data; প্রায় সব “ধ্বংসাত্মক” command শুধু ref সরায়। Script-এ ref পড়ুন rev-parse/for-each-ref দিয়ে, ফাইল খুলে নয়।
  2. Ref থাকে loose ফাইলে বা packed-refs-এ (loose জেতে), বা reftable-এ; v1.0^{} = peeled commit.
  3. HEAD@{n} HEAD-এর খাতা, main@{n} ও @{n} branch-এর খাতা, @{-n} আগের branch-এর নাম; branch মুছলে তার খাতাও মোছে।
  4. gc মোছে শুধু যা কোনো ref বা reflog থেকে পৌঁছানো যায় না এবং সময়সীমা পেরিয়েছে (ডিফল্ট ৯০/৩০ দিন, ২ সপ্তাহ)। উদ্ধারের সময় gc নয়।
  5. git fsck --unreachable হারানো stash খোঁজে, --lost-found add করা blob ফেরায়; কখনো add না করা লেখা ফেরে না — তাই ঘনঘন commit আর backup branch.

পরের পর্বে: এই পর্বে conflict-এ থামা merge-এর MERGE_HEAD দেখেছেন। পর্ব ৫৪-এ দেখবেন merge-এর ভেতরে কী ঘটে — merge base কীভাবে খোঁজা হয়, ort strategy কী করে, diff3 ধাঁচের conflict marker কীভাবে পড়বেন আর rerere কীভাবে একই conflict দ্বিতীয়বার নিজেই মেটায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান