--force বনাম --force-with-lease: বুলডোজার আর সিটবেল্ট
আগের আলোচনাগুলোতে বারবার এক লাইনে বলেছি — "force লাগলে অন্তত --force-with-lease।" এবার পুরো ব্যাখ্যা: দুটোর পার্থক্য ঠিক কোথায়, আর দ্বিতীয়টার জন্ম কেন হলো।
প্রেক্ষাপট: force-push কখন লাগে
সাধারণ git push একটা নিরাপত্তা-শর্ত মানে: রিমোটের বর্তমান অবস্থা আপনার নতুন history-র পূর্বপুরুষ (ancestor) হতে হবে — অর্থাৎ push মানে শুধু সামনে এগোনো, fast-forward। কিন্তু আপনি যদি history rewrite করে থাকেন — rebase -i দিয়ে PR গুছানো, --amend, reset — তাহলে আপনার নতুন commit-গুলো রিমোটের বংশধর নয় (সেই C বনাম C′)। Push rejected:
! [rejected] feature-x -> feature-x (non-fast-forward)
এখন রিমোটকে বলতে হবে: "তোমার কাছে যা আছে ভুলে যাও, আমারটা নাও।" এটাই force-push — আর নিজের PR branch-এ rebase-পরবর্তী এটা সম্পূর্ণ বৈধ, স্বাভাবিক কাজ। প্রশ্ন হলো কীভাবে জোর খাটাবেন।
--force: প্রশ্নহীন বুলডোজার
git push --force
এটা রিমোটকে বলে: "ওখানে যা-ই থাকুক — মুছে আমার এই hash বসাও।" কোনো শর্ত নেই, কোনো যাচাই নেই।
বিপদটা একটা নির্দিষ্ট race condition-এ লুকানো। দৃশ্যটা দেখুন:
সকাল ১০:০০ আপনি feature-x fetch করলেন — শেষ দেখা অবস্থা: commit X
সকাল ১০:৩০ আপনি লোকালে rebase করলেন (X → X′)
সকাল ১০:৪৫ সহকর্মী রাফি সেই branch-এ commit Y push করলো ← আপনি জানেন না
সকাল ১১:০০ আপনি: git push --force
ফলাফল:
রিমোট আগে: ...---X---Y (রাফির Y সহ)
রিমোট পরে: ...---X′ (Y নিঃশব্দে উধাও)
রাফির কাজ মুছে গেল — কোনো error নেই, কোনো warning নেই। আপনি জানতেও পারলেন না কিছু মুছেছেন; রাফি জানবে যখন তার পরের push/pull গোলমাল করবে। হ্যাঁ, রাফির লোকালে Y আছে আর reflog-নীতিতে উদ্ধারযোগ্য — কিন্তু সার্ভারে যদি সে-ও না রাখতো (যেমন CI-বট বা reviewer-এর suggestion-commit ছিল), খোঁজাখুঁজি কঠিন হয়ে যেত।
মূল ত্রুটিটা দার্শনিক: --force overwrite করে অন্ধভাবে — আপনার ধারণার রিমোট আর আসল রিমোট এক কি না, তা যাচাই না করেই।
--force-with-lease: শর্তসাপেক্ষ জোর
git push --force-with-lease
এটা রিমোটকে বলে: "overwrite করো — যদি branch-টা এখনো ঠিক সেই অবস্থায় থাকে যা আমি শেষবার দেখেছিলাম (আমার origin/feature-x remote-tracking ref যেখানে)। মাঝে অন্য কিছু ঘটে থাকলে বাতিল করো।"
নামের "lease"-টা এখান থেকেই — যেন আপনি branch-টার উপর একটা ইজারা নিয়েছিলেন fetch-এর সময়; ইজারার মেয়াদে অন্য কেউ হাত দিয়ে থাকলে আপনার দাবি আর খাটে না।
একই দৃশ্যে এবার:
সকাল ১১:০০ git push --force-with-lease
# ! [rejected] feature-x -> feature-x (stale info)
Push ব্যর্থ — কারণ রিমোটে এখন X নয়, Y আছে, যা আপনার remote-tracking ref-এর সাথে মেলে না। এখন আপনি:
git fetch # রাফির Y দেখুন
# Y-কে নিজের rebase-এ অন্তর্ভুক্ত করুন (যেমন rebase করে নিন)
git push --force-with-lease # এবার সফল
কারো কাজ হারালো না। এটা আসলে optimistic locking-এর চেনা প্যাটার্ন — database-এ যেমন version-column দিয়ে check-and-set করেন (Hibernate-এর @Version মনে করুন), এটা ঠিক তাই, Git ref-এর উপর: compare-and-swap। তুলনাটা interview-তে বললে ব্যাকএন্ড-প্রেক্ষাপটে চমৎকার শোনায়।
কেন এটার অস্তিত্ব — এক বাক্যে
--force বানানো হয়েছিল "আমি জানি আমি কী করছি"-র জন্য; কিন্তু বাস্তবে মানুষ জানে নিজের লোকাল অবস্থা, জানে না রিমোটে এইমাত্র কী ঘটেছে। --force-with-lease সেই জ্ঞানের ফাঁকটাই পূরণ করে — জোরটা থাকে, অন্ধত্বটা যায়।
সততার জায়গা: lease-ও ফাঁকি খেতে পারে
একটা সূক্ষ্মতা সিনিয়র-উত্তরে যোগ করার মতো। Lease-এর তুলনা হয় আপনার remote-tracking ref-এর সাথে (origin/feature-x)। কিছু টুল/IDE ব্যাকগ্রাউন্ডে auto-fetch করে — তাতে আপনার remote-tracking ref নিঃশব্দে আপডেট হয়ে যায়, রাফির Y-ও তাতে ঢুকে পড়ে, অথচ আপনি Y দেখেনইনি। তখন lease-check পাশ করে যাবে — অথচ আপনি না-দেখা কাজ মুছছেন। অর্থাৎ auto-fetch চালু থাকলে সিটবেল্টটা ঢিলে।
আরো কড়া সংস্করণ আছে:
git push --force-with-lease=feature-x:<expected-hash> # প্রত্যাশিত hash নিজে বলুন
git push --force-if-includes # (নতুন Git) যাচাই করে যে রিমোটের commit-গুলো আপনি সত্যিই দেখেছেন/অন্তর্ভুক্ত করেছেন
--force-if-includes ঠিক এই auto-fetch-ফাঁকটা বোজাতেই এসেছে — --force-with-lease-এর সাথে জুড়ে ব্যবহার করা যায়।
ব্যবহারিক নীতিমালা
| পরিস্থিতি | করণীয় |
|---|---|
| নিজের PR branch, rebase/amend-এর পর | --force-with-lease — রুটিন, নিরাপদ |
যেকোনো shared branch (main, develop) |
কোনো force-ই নয় — branch protection দিয়ে বন্ধ, ভুল শোধরাতে revert |
প্লেইন --force |
কার্যত কখনোই নয় — পেশিস্মৃতি থেকে মুছে ফেলুন |
শেষ টিপ: টাইপ করা লম্বা বলে লোকে আলসেমিতে --force লেখে — alias বানিয়ে নিন (git config --global alias.pushf "push --force-with-lease"), অজুহাতটাই শেষ।
এক লাইনে (interview-সংস্করণ)
"দুটোই non-fast-forward push-এ রিমোট overwrite করে — পার্থক্য হলো --force করে অন্ধভাবে, ফলে fetch-এর পর অন্য কেউ push করে থাকলে তার কাজ নিঃশব্দে মুছে যায়; --force-with-lease হলো optimistic-locking/compare-and-swap — overwrite তখনই যদি রিমোট ঠিক আপনার শেষ-দেখা অবস্থায় থাকে, নয়তো reject, fetch করে দেখে তারপর এগোন; এটার অস্তিত্বই এই race-condition বোজাতে — তাই নিয়ম: rewrite-পরবর্তী push-এ সবসময় --force-with-lease (auto-fetch থাকলে সাথে --force-if-includes), প্লেইন --force কখনোই না, আর shared branch-এ কোনোটাই না।"