Git — শূন্য থেকে এক্সপার্ট
১৩টা ফেজ আর ৩৯টা মডিউলের একটা সম্পূর্ণ বাংলা রোডম্যাপ — Git-এর বেসিক ভোকাবুলারি থেকে শুরু করে অবজেক্ট মডেল, রিফ ইন্টার্নালস, রিবেস/মার্জ মেকানিক্স, রিমোট কোলাবোরেশন, সিকিউরিটি এবং GitHub ওয়ার্কফ্লো পর্যন্ত — যাতে কোনো Git জ্ঞান অজানা না থাকে।
Steps
GIT — শূন্য থেকে এক্সপার্ট
ফরম্যাট: ১৩টা Phase, ৩৯টা Module। প্রতিটা মডিউলে কয়েকটা কনটেন্ট leaf (What/How/Why), তারপর একটা ল্যাব (Do), একটা ইন্টারভিউ-প্রশ্ন leaf (পরীক্ষা), আর একটা কর্নার-কেস leaf।
| লেয়ার | কী শিখবেন |
|---|---|
| What | কনসেপ্ট/কমান্ড ঠিক কী |
| How | ভেতরে কীভাবে কাজ করে (object/ref/index মডেল) |
| Why | কেন এটা দরকার, কোন সমস্যার সমাধান করে |
| Do (ল্যাব) | হাতে-কলমে করে দেখা |
| পরীক্ষা | ইন্টারভিউ-স্টাইল প্রশ্ন-উত্তর |
| কর্নার কেস | বাস্তব জীবনের গোচরা/ফুটগান |
স্কোপ
Git-এর প্রতিটা পোরসেলিন ও প্লাম্বিং কমান্ড কভার করা হয়েছে — বেসিক commit/push/pull থেকে শুরু করে object model, refs, packfiles, rebase/reflog ইন্টার্নালস, worktree/submodule, hooks, সিকিউরিটি (signing/credentials/secret removal), আর GitHub-স্পেসিফিক ওয়ার্কফ্লো (fork/PR/branch protection/Actions) পর্যন্ত। লক্ষ্য: এই জার্নি শেষ করার পর Git নিয়ে যেন কোনো প্রশ্ন অজানা না থাকে — বিগিনার থেকে প্রোডাকশন-লেভেল ট্রাবলশুটার পর্যন্ত।
মার্কার
⭐ (strength) = হ্যান্ডস-অন ল্যাব। 🔑 (gate) = Phase-শেষের চেকপয়েন্ট — পরের Phase-এ যাওয়ার আগে নিজেকে যাচাই করার জায়গা। শেষ Module 39-এ একটা চূড়ান্ত ক্যাপস্টোন আছে যেখানে পুরো জার্নির সব দক্ষতা একসাথে প্রয়োগ করতে হবে।
PHASE 0: ওরিয়েন্টেশন
Git শেখার আগে বোঝা দরকার এটা কেন এভাবে ডিজাইন করা হয়েছে — এটা শুধু "কমান্ড মুখস্থ করা" বিষয় না, এটা একটা মেন্টাল মডেল। Linus Torvalds ২০০৫ সালে Git বানিয়েছিলেন BitKeeper-এর বিকল্প হিসেবে, আর সেই ডিজাইনের প্রতিটা সিদ্ধান্তের পেছনে একটা যুক্তি আছে: distributed হতে হবে, snapshot-ভিত্তিক হতে হবে, আর দ্রুত হতে হবে। এই Phase-এ দুইটা Module — প্রথমটা ইতিহাস ও তিনটা এলাকার (working directory/staging/repo) মানসিক মডেল তৈরি করবে, দ্বিতীয়টা Git-এর configuration সিস্টেম শেখাবে যা পুরো বইয়ের বাকি অংশে বারবার কাজে লাগবে। এই দুটো মডিউল ছাড়া বাকি সব মডিউল "কেন" প্রশ্নের উত্তর ছাড়াই মুখস্থ কমান্ডের তালিকা হয়ে যাবে।
MODULE 1: ইতিহাস ও মেন্টাল মডেল
Git-কে গভীরভাবে বোঝার প্রথম ধাপ হলো এটা কোন সমস্যার সমাধান হিসেবে জন্ম নিয়েছে সেটা বোঝা। এই মডিউলে কভার হবে: centralized বনাম distributed VCS-এর পার্থক্য ও SVN-এর সীমাবদ্ধতা, Git বনাম GitHub/GitLab-এর পার্থক্য (টুল বনাম হোস্টিং সার্ভিস), প্রথম ইনস্টলেশন ও config, working directory/staging area/repository — এই তিনটা এলাকা ও তাদের মধ্যে ডেটা-প্রবাহ, এবং snapshot মডেল কেন delta/diff মডেলের চেয়ে শক্তিশালী।
VCS-এর আগে ও পরে: Centralized বনাম Distributed
সমস্যাটা কী ছিল
Git-এর আগে version control মানে ছিল CVS আর SVN (Subversion)-এর মতো centralized VCS (CVCS)। এই মডেলে একটা মাত্র central সার্ভার পুরো প্রজেক্টের ইতিহাস রাখত, আর প্রতিটা ডেভেলপারের মেশিনে থাকত শুধু current snapshot — পুরো ইতিহাস না।
এতে সমস্যা কী হতো
- সিঙ্গেল পয়েন্ট অফ ফেইলিওর — central সার্ভার ডাউন হলে কেউ commit করতে পারত না, এমনকি নিজের লোকাল history দেখতেও পারত না।
- নেটওয়ার্ক নির্ভরতা —
svn log,svn diff(পুরনো রিভিশনের সাথে) সব সময় সার্ভারে রাউন্ড-ট্রিপ লাগত। অফলাইনে কাজ প্রায় অসম্ভব। - Branch করা ছিল ভারী — SVN-এ branch মানে পুরো ডিরেক্টরি সার্ভার-সাইড কপি করা, merge করা ছিল কষ্টকর আর error-prone।
Git-এর সমাধান: Distributed VCS (DVCS)
Git-এ প্রতিটা ক্লোন একটা সম্পূর্ণ রিপোজিটরি — পুরো ইতিহাস, সব branch, সব commit, লোকাল মেশিনে থাকে। কোনো "central" সার্ভার আবশ্যক না — যেকোনো ক্লোনকে "central" হিসেবে ট্রিট করা যায় (এই জন্যই GitHub একটা hosting service মাত্র, Git-এর অংশ না)।
রিয়েল-লাইফ প্রভাব
অফলাইনে ফ্লাইটে বসে পুরো প্রজেক্ট ইতিহাস browse করা, commit করা, branch বানানো — সব সম্ভব, ইন্টারনেট লাগবে শুধু sync (push/pull/fetch) করার সময়। টিমে একজনের মেশিন নষ্ট হলেও যেকোনো clone থেকে পুরো history restore করা যায় — এটাই distributed মডেলের সবচেয়ে বড় ব্যবহারিক সুবিধা।
মনে রাখার শর্টকাট: SVN = একটা লাইব্রেরির একমাত্র বই, সবাই সেটা পড়তে লাইব্রেরিতে যেতে হয়। Git = প্রতিটা পাঠকের কাছে পুরো বইয়ের কপি।
Git বনাম GitHub/GitLab — টুল বনাম হোস্টিং সার্ভিস
সবচেয়ে সাধারণ কনফিউশন
নতুন ডেভেলপাররা প্রায়ই "Git" আর "GitHub" শব্দ দুটো একই জিনিস মনে করে ব্যবহার করেন। বাস্তবে এরা সম্পূর্ণ ভিন্ন স্তরের জিনিস।
পার্থক্য
Git হলো একটা command-line টুল/প্রোটোকল — আপনার লোকাল মেশিনে ইনস্টল হওয়া সফটওয়্যার, যেটা version control-এর আসল কাজ (commit, branch, merge, history tracking) করে। এটা সম্পূর্ণভাবে ইন্টারনেট বা কোনো নির্দিষ্ট কোম্পানির উপর নির্ভরশীল না।
GitHub, GitLab, Bitbucket হলো hosting service — এরা Git রিপোজিটরি হোস্ট করে ইন্টারনেটে, আর তার উপর অতিরিক্ত ফিচার যোগ করে যেগুলো Git নিজে দেয় না: Pull Request/Merge Request UI, Issue tracker, CI/CD pipeline, code review workflow, access control।
কীভাবে বুঝবেন এই আলাদা স্তরটা
# এটা pure Git — কোনো GitHub লাগে না
git init my-project
git add .
git commit -m "প্রথম কমিট"
git log
# এটা GitHub-নির্দিষ্ট — শুধু তখনই দরকার যখন রিমোট হোস্ট আছে
git remote add origin https://github.com/user/repo.git
git push origin main
উপরের প্রথম চারটা কমান্ড ইন্টারনেট ছাড়াই, GitHub অ্যাকাউন্ট ছাড়াই সম্পূর্ণ কাজ করে। শুধু শেষ দুটো লাইনে "GitHub" প্রাসঙ্গিক হয়।
রিয়েল-লাইফ প্রভাব
একটা কোম্পানি চাইলে GitHub একেবারেই ব্যবহার না করে নিজস্ব সার্ভারে শুধু git init --bare দিয়ে একটা রিপো হোস্ট করতে পারে (self-hosted), অথবা GitLab CE ইনস্টল করে নিজস্ব ইনফ্রাস্ট্রাকচারে চালাতে পারে — Git প্রোটোকল একই থাকে। এই জন্য "GitHub শিখব" আর "Git শিখব" — দুটো ভিন্ন লক্ষ্য, আর এই পুরো জার্নি মূলত দ্বিতীয়টার উপর ফোকাস করে।
তিনটা এলাকা: Working Directory, Staging Area (Index), Repository
Git-এর সবচেয়ে গুরুত্বপূর্ণ মেন্টাল মডেল
Git-এ প্রতিটা ফাইল তিনটা সম্ভাব্য এলাকার একটায় থাকে, আর এই তিনটা এলাকার মধ্যে ডেটার প্রবাহ বোঝাই Git বোঝার মূল ভিত্তি।
| এলাকা | কী থাকে | কোন কমান্ড এখানে কাজ করে |
|---|---|---|
| Working Directory | আপনার এডিট করা আসল ফাইল, ডিস্কে | টেক্সট এডিটর, git status |
| Staging Area (Index) | পরের commit-এ কী কী যাবে তার একটা "প্রস্তাব" | git add, git restore --staged |
| Repository (.git) | চূড়ান্ত, permanent snapshot হিসেবে commit করা ইতিহাস | git commit, git log |
ডেটা কীভাবে একটা থেকে আরেকটায় যায়
# working directory-তে একটা ফাইল এডিট করলাম
echo "hello" > app.py
# staging area-য় তোলা হলো (index-এ যোগ হলো)
git add app.py
# repository-তে permanent snapshot হিসেবে সেভ হলো
git commit -m "add app.py"
কেন staging area আলাদা করে রাখা হলো
এটাই Git-কে অনেক পুরনো VCS থেকে আলাদা করে দেয়। staging area থাকার কারণে আপনি একাধিক ফাইল এডিট করেও শুধু নির্দিষ্ট কিছু ফাইল/অংশ commit করতে পারেন — বাকিগুলো working directory-তেই থেকে যায়, পরের commit-এর জন্য অপেক্ষা করে। এটা git add -p দিয়ে আরও গ্র্যানুলার হয় — একই ফাইলের ভেতরের নির্দিষ্ট hunk-ও আলাদা করে stage করা যায়।
রিয়েল-লাইফ উদাহরণ
ধরুন আপনি একসাথে একটা bug fix আর একটা unrelated refactor করে ফেলেছেন একই সেশনে। staging area ব্যবহার করে bug fix-এর ফাইলগুলো আলাদাভাবে git add করে একটা পরিষ্কার, ফোকাসড commit বানানো যায়, refactor-টা আলাদা commit-এ যায় — ইতিহাস পরিষ্কার থাকে, git bisect বা git revert পরে সহজ হয়।
Snapshot মডেল বনাম Delta/Diff মডেল
দুই ধরনের VCS ডিজাইন দর্শন
পুরনো VCS (SVN, CVS, Mercurial-এর কিছু অংশ) সাধারণত delta-based storage ব্যবহার করে — প্রতিটা কমিটে শুধু আগের ভার্সনের সাথে পার্থক্যটুকু (diff) সংরক্ষণ করা হয়। পুরো ফাইল পুনর্গঠন করতে হলে শুরু থেকে সব delta ক্রমান্বয়ে apply করতে হয়।
Git সম্পূর্ণ ভিন্ন পথ নেয়: snapshot-based storage। প্রতিটা commit-এ Git পুরো প্রজেক্টের একটা সম্পূর্ণ "ছবি" (snapshot) রাখে — প্রতিটা ফাইলের পূর্ণ কন্টেন্টের রেফারেন্স, ডিফ না।
কীভাবে এটা efficient হয় (স্পয়লার: dedup)
শুনে মনে হতে পারে এটা জায়গা নষ্ট করবে, কিন্তু Git একটা চালাকি করে: যদি একটা ফাইল দুই commit-এর মধ্যে না বদলায়, তাহলে দুই commit-এর tree একই blob hash-কে পয়েন্ট করে — নতুন কপি তৈরি হয় না। শুধু বদলে যাওয়া ফাইলের জন্যই নতুন blob object তৈরি হয় (Phase 2-তে বিস্তারিত)।
# একই কন্টেন্ট = একই hash, তা যতবারই বা যে ফাইলেই থাকুক না কেন
git hash-object README.md
git hash-object docs/copy-of-readme.md # কন্টেন্ট identical হলে একই hash আসবে
কেন এটা গুরুত্বপূর্ণ
- দ্রুত branch switching — নতুন snapshot বানাতে delta chain apply করার দরকার নেই, শুধু tree pointer বদলে যায়।
- নির্ভরযোগ্য integrity check — প্রতিটা commit স্বয়ংসম্পূর্ণ, তাই history-র কোনো অংশ দুর্নীতিগ্রস্ত (corrupt) হলে সহজে ধরা পড়ে (SHA hash mismatch)।
- merge আর diff computation lazy — Git diff on-demand কম্পিউট করে (দুটো snapshot তুলনা করে), স্টোরেজে diff জমিয়ে রাখে না।
সারসংক্ষেপ টেবিল
| Delta-based (SVN) | Snapshot-based (Git) | |
|---|---|---|
| স্টোরেজে কী থাকে | পার্থক্য (diff chain) | পূর্ণ স্ন্যাপশট (dedup সহ) |
| পুরনো ভার্সন রিকভার | ধাপে ধাপে delta apply | সরাসরি snapshot access |
| Branch/checkout গতি | তুলনামূলক ধীর | দ্রুত |
ইনস্টলেশন ও প্রথম git config
ইনস্টলেশন
# Fedora/RHEL
sudo dnf install git
# Debian/Ubuntu
sudo apt install git
# ভার্সন চেক
git --version
প্রথম দুটো config — বাধ্যতামূলক
Git-এর প্রতিটা commit-এ একজন author আর committer থাকে (Phase 2-এ কমিট অবজেক্টের গঠনে বিস্তারিত)। এই তথ্য না দিলে Git commit করতেই দেয় না।
git config --global user.name "Rakib Hasan"
git config --global user.email "rakib@example.com"
এই তথ্য কোথায় সংরক্ষিত হয়
--global ফ্ল্যাগ ব্যবহার করলে এই কনফিগ লেখা হয় ~/.gitconfig ফাইলে, একটা প্লেইন-টেক্সট INI ফরম্যাটে:
[user]
name = Rakib Hasan
email = rakib@example.com
এই ফাইলটা যেকোনো টেক্সট এডিটর দিয়ে সরাসরি এডিট করা যায় — git config কমান্ড আসলে শুধু এই ফাইল ম্যানিপুলেট করার একটা সুবিধাজনক CLI wrapper।
যাচাই করা
git config --global --list
# user.name=Rakib Hasan
# user.email=rakib@example.com
রিয়েল-লাইফ সমস্যা যা এটা এড়ায়
কোম্পানির টিমে commit history দেখে বোঝা যায় কে কোন কোড লিখেছে (git blame, git log --author)। ভুল email দিলে GitHub-এ commit আপনার প্রোফাইলের সাথে link হবে না (attribution ভেঙে যাবে) — এই জন্য প্রজেক্ট-নির্দিষ্ট email আলাদা রাখার দরকার হয়, যেটা পরের মডিউলে includeIf দিয়ে সমাধান করা হবে।
ল্যাব: প্রথম রিপো বানিয়ে .git ফোল্ডার এক্সপ্লোর করা
লক্ষ্য
একটা নতুন Git রিপো বানিয়ে .git ফোল্ডারের ভেতরের গঠন হাতে-কলমে দেখা — যেটা Phase 2-তে গভীরভাবে ব্যাখ্যা হবে, এখানে শুধু প্রাথমিক পরিচয়।
ধাপ ১ — রিপো তৈরি
mkdir git-lab-01 && cd git-lab-01
git init
আউটপুট: Initialized empty Git repository in /path/git-lab-01/.git/
ধাপ ২ — .git এক্সপ্লোর করা
ls -la .git
দেখবেন এরকম কিছু এন্ট্রি:
HEAD config description hooks/
info/ objects/ refs/
ধাপ ৩ — প্রতিটার সংক্ষিপ্ত পরিচয়
cat .git/HEAD
# ref: refs/heads/main ← HEAD একটা branch-কে পয়েন্ট করছে
cat .git/config
# [core] section — এই রিপোর লোকাল কনফিগারেশন
ls .git/refs
# heads/ tags/ ← এখনো খালি, কারণ কোনো commit হয়নি
ধাপ ৪ — objects/ ফোল্ডার এখনো খালি কেন
find .git/objects -type f
# কিছুই আসবে না
কারণ এখনো কোনো git add বা git commit হয়নি — কোনো object তৈরি হয়নি। এটা প্রমাণ করে যে Git ফাইল ট্র্যাক শুরু করে শুধু আপনি explicitly add করলেই।
ধাপ ৫ — একটা ফাইল add করে পার্থক্য দেখা
echo "# আমার প্রথম প্রজেক্ট" > README.md
git add README.md
find .git/objects -type f
এবার একটা নতুন ফাইল দেখা যাবে .git/objects/xx/yyyy... প্যাটার্নে — এটাই আপনার প্রথম blob object, Phase 2-এ যেটা নিয়ে বিস্তারিত আলোচনা হবে।
চ্যালেঞ্জ
git init --bare test.git চালিয়ে দেখুন --bare রিপোতে working directory না থাকায় গঠন কীভাবে ভিন্ন (কোনো working tree ফাইল নেই, .git-এর কন্টেন্ট সরাসরি রুটে থাকে)।
ইন্টারভিউ প্রশ্ন — Module 1
প্র.১ — Git-কে "distributed" VCS বলা হয় কেন? উত্তর: কারণ প্রতিটা ক্লোনে পুরো প্রজেক্টের সম্পূর্ণ ইতিহাস থাকে, কোনো central সার্ভারের প্রয়োজন নেই। SVN-এর মতো centralized VCS-এ শুধু সার্ভারে পূর্ণ history থাকে, ক্লায়েন্টে শুধু current snapshot।
প্র.২ — Git আর GitHub-এর মধ্যে পার্থক্য কী? উত্তর: Git একটা version control টুল/প্রোটোকল যা লোকালি চলে ও ইন্টারনেট ছাড়াই কাজ করে। GitHub একটা hosting service যা Git রিপো হোস্ট করে এবং অতিরিক্ত ফিচার (PR, Issue, CI/CD) যোগ করে। Git ছাড়া GitHub অচল, কিন্তু GitHub ছাড়া Git পুরোপুরি চলে।
প্র.৩ — Working directory, staging area, আর repository — এই তিনটার পার্থক্য কী?
উত্তর: Working directory হলো ডিস্কে আপনার এডিটেবল ফাইল। Staging area (index) হলো পরের commit-এ কী যাবে তার প্রস্তাব। Repository (.git) হলো permanent commit history। git add staging-এ তোলে, git commit সেটাকে repository-তে স্থায়ী করে।
প্র.৪ — Git snapshot-based স্টোরেজ ব্যবহার করে, তবু কীভাবে জায়গা বাঁচায়? উত্তর: প্রতিটা commit-এ প্রতিটা ফাইলের সম্পূর্ণ snapshot রাখা হয়, কিন্তু যদি একটা ফাইল দুই commit-এর মধ্যে অপরিবর্তিত থাকে, তাহলে দুটো commit-এর tree একই blob object-কে reference করে — নতুন কপি বানায় না (content-addressable storage-এর মাধ্যমে dedup)।
প্র.৫ — git config কমান্ড কোন ফাইল ম্যানিপুলেট করে?
উত্তর: --global ফ্ল্যাগ দিলে ~/.gitconfig, --local (ডিফল্ট) দিলে .git/config, --system দিলে সিস্টেম-ওয়াইড config ফাইল — এগুলো সবই প্লেইন-টেক্সট INI ফরম্যাট, git config আসলে একটা CLI wrapper মাত্র।
প্র.৬ — SVN-এর তুলনায় Git-এ branch করা সহজ কেন? উত্তর: SVN-এ branch মানে সার্ভার-সাইড পুরো ডিরেক্টরি কপি করা (ভারী অপারেশন)। Git-এ branch মানে শুধু একটা ৪১-বাইট টেক্সট ফাইল (একটা commit hash ধরে রাখা ref) তৈরি করা — তাৎক্ষণিক ও সস্তা অপারেশন।
কর্নার কেস — Module 1
**
git configছাড়া commit করার চেষ্টা করলে Git আপনাকে আটকাবে না চিরতরে — বরং প্রথমবার একটা fallback identity (hostname-ভিত্তিক) ব্যবহার করে warning দেবে, অনেক নতুন ইউজার সেই warning উপেক্ষা করে ভুল author info নিয়ে commit করে ফেলেন। সবসময় প্রথম কাজ হিসেবেuser.name/user.emailসেট করা উচিত।.gitফোল্ডার ডিলিট করলে পুরো ইতিহাস চিরতরে হারিয়ে যায়, কিন্তু working directory-র ফাইল অক্ষত থাকে — অনেকে ভাবেন.gitডিলিট করলে সব ফাইল মুছে যাবে, বাস্তবে শুধু ভার্সন-হিস্ট্রি মুছে যায়, বর্তমান ফাইলগুলো একদম প্লেইন ফোল্ডার হিসেবে থেকে যায়।--bareরিপোতে সরাসরি ফাইল এডিট বাgit addকরা যায় না — কারণ এতে কোনো working directory নেই, শুধু.git-এর ভেতরের কন্টেন্ট। এগুলো শুধু push/pull-এর জন্য central hosting point হিসেবে ব্যবহার করার জন্য ডিজাইন করা, ডেভেলপমেন্টের জন্য না।Windows-এ ইনস্টল করা Git for Windows-এর
core.autocrlfডিফল্ট আচরণ Linux/Mac থেকে আলাদা — line-ending কনভার্সন নিয়ে cross-platform টিমে প্রথম দিকেই বিভ্রান্তি তৈরি করে (একই ফাইলে অকারণ পুরো-ফাইল diff দেখা যায়), যেটা.gitattributesদিয়ে সমাধান করতে হয়।
MODULE 2: Configuration Levels
Git-এর configuration সিস্টেম একটা layered মডেল অনুসরণ করে — system, global, local, আর worktree, প্রতিটার নিজস্ব precedence। এই মডিউলে কভার হবে: চার লেভেলের precedence, git config কমান্ডের বিভিন্ন ফ্ল্যাগ (--list --show-origin, --get, --get-regexp, --unset, --edit), গুরুত্বপূর্ণ config key (core.editor, init.defaultBranch, pull.rebase, push.default), includeIf দিয়ে কনটেক্সট-ভিত্তিক কনফিগারেশন, গ্লোবাল .gitignore, আর git help/man git-<command> কীভাবে ব্যবহার করবেন।
চার লেভেল Config ও Precedence
চারটা লেভেল
Git কনফিগারেশন চারটা ফাইলে ছড়িয়ে থাকতে পারে, প্রতিটা ভিন্ন স্কোপে:
| লেভেল | ফাইল | স্কোপ | ফ্ল্যাগ |
|---|---|---|---|
| system | /etc/gitconfig |
পুরো মেশিনের সব ইউজার | --system |
| global | ~/.gitconfig |
একটা নির্দিষ্ট ইউজারের সব রিপো | --global |
| local | .git/config |
শুধু বর্তমান রিপো | --local (ডিফল্ট) |
| worktree | .git/config.worktree |
নির্দিষ্ট worktree (multiple worktree থাকলে) | --worktree |
Precedence — কোনটা জেতে
Specificity বেশি যার, সে জেতে: worktree > local > global > system। অর্থাৎ একই key (যেমন user.email) যদি global আর local — দুই জায়গাতেই সেট থাকে, তাহলে local জিতবে সেই নির্দিষ্ট রিপোর জন্য।
# গ্লোবালি পার্সোনাল ইমেইল
git config --global user.email "personal@gmail.com"
# শুধু এই রিপোর জন্য কোম্পানির ইমেইল (local জিতবে)
cd ~/work/company-project
git config --local user.email "rakib@company.com"
কেন এই লেয়ারিং দরকার
এটা একই সাথে কনভেনিয়েন্স (global-এ একবার সেট করলেই সব রিপোতে কাজ করে) আর ফ্লেক্সিবিলিটি (যেকোনো নির্দিষ্ট রিপোর জন্য override করা যায়) দেয়। System-level কনফিগ সাধারণত sysadmin ব্যবহার করে shared মেশিনে সব ইউজারের জন্য default policy সেট করতে (যেমন core.autocrlf)।
রিয়েল-লাইফ উদাহরণ
একজন ফ্রিল্যান্স ডেভেলপার হিসেবে আপনার গ্লোবাল ইমেইল পার্সোনাল, কিন্তু ~/work/ এর ভেতরের প্রতিটা রিপোতে কোম্পানির ইমেইল ব্যবহার করতে চান — প্রতিটা রিপোতে ম্যানুয়ালি --local সেট করা ঝামেলার, এর জন্য includeIf (এই মডিউলেই পরে) স্বয়ংক্রিয় সমাধান দেয়।
git config কমান্ড: --list --show-origin, --get, --get-regexp, --unset, --edit
কোন config কোথা থেকে এলো, তা দেখা
একাধিক লেভেলে config ছড়িয়ে থাকলে confusion হতেই পারে — কোন value আসলে effective, আর সেটা কোন ফাইল থেকে এসেছে।
git config --list --show-origin
# file:/home/rakib/.gitconfig user.name=Rakib Hasan
# file:.git/config user.email=rakib@company.com
এখানে user.email local ফাইল থেকে এসেছে বোঝা যাচ্ছে সরাসরি — precedence-জনিত কনফিউশন দূর করতে এটাই সবচেয়ে দ্রুত ডায়াগনস্টিক কমান্ড।
নির্দিষ্ট একটা key পড়া
git config --get user.email
# rakib@company.com (effective value — সবচেয়ে specific লেভেল থেকে)
Pattern দিয়ে একাধিক key খোঁজা
git config --get-regexp "^user\."
# user.name Rakib Hasan
# user.email rakib@company.com
এটা কাজে লাগে যখন একটা প্রিফিক্স (যেমন সব alias.* বা url.*) দিয়ে সব সেটিং একসাথে দেখতে চান।
একটা key মুছে ফেলা
git config --unset core.editor
# --local (ডিফল্ট স্কোপ) থেকে মুছবে; --global দিলে global থেকে
সরাসরি এডিটরে config ফাইল খোলা
git config --global --edit
# core.editor-এ সেট করা এডিটরে ~/.gitconfig খুলবে
রিয়েল-লাইফ ব্যবহার
একটা CI স্ক্রিপ্টে যদি অদ্ভুত user.email দিয়ে commit হচ্ছে দেখেন, প্রথম ডায়াগনস্টিক ধাপ হওয়া উচিত git config --list --show-origin | grep email — এটা সাথে সাথে বলে দেবে কোন লেভেলের কনফিগ culprit।
গুরুত্বপূর্ণ Config Keys: core.editor, init.defaultBranch, pull.rebase, push.default
চারটা key যা প্রতিদিনের ওয়ার্কফ্লো বদলে দেয়
core.editor
Commit message লেখার সময় (যখন -m ফ্ল্যাগ দেননি) Git একটা এডিটর খোলে। ডিফল্ট প্রায়ই vi/vim, যা নতুনদের আটকে ফেলে (:wq না জানলে বের হতেই পারবে না)।
git config --global core.editor "nano"
# বা VS Code:
git config --global core.editor "code --wait"
init.defaultBranch
পুরনো Git ভার্সনে নতুন রিপোর ডিফল্ট branch নাম ছিল master। আধুনিক প্র্যাক্টিসে main প্রেফার করা হয়।
git config --global init.defaultBranch main
pull.rebase
git pull আসলে git fetch + merge (অথবা rebase) — এই বিহেভিয়ার নিয়ন্ত্রণ করে এই key।
| মান | আচরণ |
|---|---|
false (ডিফল্ট) |
fetch + merge — history-তে merge commit তৈরি হয় |
true |
fetch + rebase — history লিনিয়ার থাকে |
merges |
fetch + rebase, কিন্তু merge commit preserve করে |
git config --global pull.rebase true
push.default
git push (কোনো আর্গুমেন্ট ছাড়া) কী করবে তা নিয়ন্ত্রণ করে।
git config --global push.default simple
# simple = শুধু current branch-কে একই নামের upstream branch-এ push করে (আধুনিক ডিফল্ট)
রিয়েল-লাইফ প্রভাব
একটা টিমে সবাই pull.rebase = true সেট না করলে কিছু ডেভেলপারের history-তে অহেতুক merge commit ("Merge branch 'main' of ...") জমতে থাকে, যা git log --graph কে অগোছালো করে তোলে — এই একটামাত্র কনফিগ পুরো টিমের history-র "পরিচ্ছন্নতা" নির্ধারণ করে দেয়।
includeIf দিয়ে কনটেক্সট-ভিত্তিক Config ও গ্লোবাল .gitignore
সমস্যা: একই মেশিনে একাধিক পরিচয়
ধরুন আপনার পার্সোনাল প্রজেক্ট আর কোম্পানির প্রজেক্ট একই মেশিনে, কিন্তু commit-এ ভিন্ন ইমেইল ব্যবহার করতে চান — প্রতিটা রিপোতে ম্যানুয়ালি --local সেট করা ভুলে যাওয়ার সম্ভাবনা থাকে।
সমাধান: includeIf
~/.gitconfig-এ একটা conditional include যোগ করুন যা পাথের উপর ভিত্তি করে অন্য একটা config ফাইল লোড করে:
# ~/.gitconfig
[user]
name = Rakib Hasan
email = personal@gmail.com
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
# ~/.gitconfig-work
[user]
email = rakib@company.com
এখন ~/work/ পাথের ভেতরের যেকোনো রিপোতে স্বয়ংক্রিয়ভাবে কোম্পানির ইমেইল ব্যবহার হবে, বাকি জায়গায় পার্সোনাল ইমেইল — কোনো ম্যানুয়াল স্টেপ ছাড়াই।
যাচাই
cd ~/work/company-project
git config user.email
# rakib@company.com
cd ~/personal-project
git config user.email
# personal@gmail.com
গ্লোবাল .gitignore
প্রতিটা রিপোতে আলাদা .gitignore-এ .DS_Store, *.swp, IDE ফোল্ডার (.vscode/, .idea/) যোগ করা বিরক্তিকর — এগুলো একবার গ্লোবালি সেট করা যায়:
git config --global core.excludesFile ~/.gitignore_global
echo ".DS_Store" >> ~/.gitignore_global
echo "*.swp" >> ~/.gitignore_global
এই এন্ট্রিগুলো কোনো রিপোর .gitignore-এ কমিট হয় না — এটা সম্পূর্ণ লোকাল, পার্সোনাল-প্রেফারেন্স-লেভেলের ইগনোর নিয়ম, টিমের বাকিদের সাথে শেয়ার হয় না।
git help ও man git-<command> ব্যবহার
Git-এর built-in ডকুমেন্টেশন
প্রতিটা Git সাব-কমান্ডের জন্য একটা সম্পূর্ণ man-page-স্টাইল ডকুমেন্টেশন বিল্ট-ইন আছে — অনলাইনে সার্চ করার আগে এটাই প্রথম রিসোর্স হওয়া উচিত।
git help commit
# অথবা সমতুল্য:
man git-commit
দুটোই একই ডকুমেন্টেশন খোলে — git help <cmd> আসলে ভেতরে man git-<cmd> (বা কনফিগার করা অন্য ভিউয়ার) কল করে।
দ্রুত রেফারেন্সের জন্য শর্ট ভার্সন
git commit -h
# শুধু ফ্ল্যাগগুলোর সংক্ষিপ্ত তালিকা, পুরো man page না
সব উপলব্ধ কমান্ডের তালিকা
git help -a
# porcelain (দৈনন্দিন) ও plumbing (লো-লেভেল) — দুই ধরনের কমান্ডই লিস্ট হয়
Guide-স্টাইল ডকুমেন্টেশন
Git-এ কমান্ড-নির্দিষ্ট না এমন কিছু concept guide-ও আছে:
git help gitignore # .gitignore প্যাটার্ন সিনট্যাক্স
git help gitattributes # .gitattributes সিনট্যাক্স
git help revisions # HEAD~2, HEAD^, branch@{upstream} ইত্যাদি রেফারেন্স সিনট্যাক্স
কেন এটা গুরুত্বপূর্ণ
Stack Overflow-এর অনেক পুরনো উত্তর ভুল বা deprecated flag সাজেস্ট করে। git help <cmd> সবসময় আপনার লোকালি ইনস্টল করা এক্স্যাক্ট ভার্সনের সাথে ম্যাচ করা ডকুমেন্টেশন দেখায় — git --version চেক করে নিশ্চিত হওয়া যায় কোন ভার্সনের ডক পড়ছেন।
রিয়েল-লাইফ অভ্যাস
একটা নতুন ফ্ল্যাগ বা কমান্ড শেখার সময় git help <cmd> চালিয়ে EXAMPLES সেকশন খুঁজে পড়া — প্রায় প্রতিটা man page-এর শেষে রিয়েল-ওয়ার্ল্ড উদাহরণ থাকে যা ব্লগ পোস্টের চেয়ে বেশি নির্ভরযোগ্য, কারণ এটা সরাসরি Git মেইনটেইনারদের লেখা।
ল্যাব: includeIf সেট করে work/personal identity আলাদা করা
লক্ষ্য
includeIf ব্যবহার করে দুটো ভিন্ন ফোল্ডারে দুটো ভিন্ন Git identity স্বয়ংক্রিয়ভাবে কাজ করানো, ও তা যাচাই করা।
ধাপ ১ — ফোল্ডার স্ট্রাকচার তৈরি
mkdir -p ~/gitlab-test/work ~/gitlab-test/personal
ধাপ ২ — গ্লোবাল (পার্সোনাল) identity সেট করা
git config --global user.name "Rakib Hasan"
git config --global user.email "personal@gmail.com"
ধাপ ৩ — work-নির্দিষ্ট config ফাইল বানানো
cat > ~/.gitconfig-work << 'EOF'
[user]
email = rakib@company.com
EOF
ধাপ ৪ — includeIf যোগ করা
git config --global --edit
এডিটরে নিচের ব্লক যোগ করুন (পাথ absolute হতে হবে, শেষে / বাধ্যতামূলক):
[includeIf "gitdir:~/gitlab-test/work/"]
path = ~/.gitconfig-work
ধাপ ৫ — যাচাই
cd ~/gitlab-test/work
git init sub-project && cd sub-project
git config user.email
# আশা করা আউটপুট: rakib@company.com
cd ~/gitlab-test/personal
git init sub-project2 && cd sub-project2
git config user.email
# আশা করা আউটপুট: personal@gmail.com
ধাপ ৬ — একটা টেস্ট commit করে প্রমাণ করা
cd ~/gitlab-test/work/sub-project
touch test.txt && git add test.txt
git commit -m "test commit"
git log --format="%an <%ae>"
# Rakib Hasan <rakib@company.com>
চ্যালেঞ্জ
gitdir: এর বদলে gitdir/i: ব্যবহার করে দেখুন (case-insensitive matching) — Windows-এর মতো case-insensitive ফাইলসিস্টেমে এটা কখন দরকার হতে পারে ভাবুন।
ইন্টারভিউ প্রশ্ন — Module 2
প্র.১ — Git config-এর চারটা লেভেলের precedence অর্ডার কী? উত্তর: worktree > local > global > system। সবচেয়ে specific স্কোপ (worktree) সবার আগে জেতে, সবচেয়ে সাধারণ (system) সবার শেষে প্রযোজ্য হয় যদি অন্য কোথাও override না থাকে।
প্র.২ — একটা config value কোন ফাইল থেকে এসেছে তা কীভাবে বের করবেন?
উত্তর: git config --list --show-origin চালিয়ে — এটা প্রতিটা key-value-এর পাশে ঠিক কোন ফাইল থেকে সেটা এসেছে তা দেখায়, যা precedence-জনিত confusion দ্রুত সমাধানে সাহায্য করে।
প্র.৩ — pull.rebase কনফিগারেশন কী নিয়ন্ত্রণ করে, এবং true সেট করলে কী পরিবর্তন হয়?
উত্তর: এটা নিয়ন্ত্রণ করে git pull fetch-এর পর merge করবে নাকি rebase করবে। true সেট করলে merge commit তৈরি না হয়ে local commit গুলো remote-এর উপরে rebase হয়, ফলে history লিনিয়ার থাকে।
প্র.৪ — একজন ফ্রিল্যান্সার একই মেশিনে দুইটা ইমেইল (পার্সোনাল ও কোম্পানি) ব্যবহার করতে চান, প্রতিটা রিপোতে ম্যানুয়ালি সেট না করে। কীভাবে সমাধান করবেন?
উত্তর: ~/.gitconfig-এ includeIf "gitdir:<path>/" ব্লক ব্যবহার করে একটা নির্দিষ্ট ফোল্ডারের জন্য আলাদা config ফাইল include করে, যেখানে ভিন্ন user.email সেট থাকবে — এটা পাথের ভিত্তিতে স্বয়ংক্রিয়ভাবে কাজ করে।
প্র.৫ — গ্লোবাল .gitignore (core.excludesFile) আর প্রজেক্ট-লোকাল .gitignore-এর মধ্যে পার্থক্য কী?
উত্তর: গ্লোবাল .gitignore সম্পূর্ণ লোকাল, পার্সোনাল প্রেফারেন্স (যেমন .DS_Store, IDE ফোল্ডার), কমিট হয় না, টিমের সাথে শেয়ার হয় না। প্রজেক্ট-লোকাল .gitignore রিপোর ভেতরে কমিট হয়, পুরো টিম শেয়ার করে (যেমন node_modules/, .env)।
প্র.৬ — git help commit আর git commit -h-এর পার্থক্য কী?
উত্তর: git help commit (= man git-commit) পূর্ণ ডকুমেন্টেশন, উদাহরণসহ, দেখায়। git commit -h শুধু ফ্ল্যাগগুলোর সংক্ষিপ্ত usage তালিকা দেখায়, দ্রুত রেফারেন্সের জন্য।
কর্নার কেস — Module 2
includeIf "gitdir:"পাথে ট্রেইলিং স্ল্যাশ (/) বাদ দিলে ম্যাচ নাও হতে পারে —gitdir:~/work(স্ল্যাশ ছাড়া) সবসময় ঠিকভাবে prefix-match নাও করতে পারে; সঠিক ফরম্যাট সবসময়gitdir:~/work/।git config --unsetএকটা key-এর একাধিক ভ্যালু থাকলে (যেমন multi-value key) error দেয়, চুপচাপ প্রথমটা মুছে ফেলে না — সেক্ষেত্রে--unset-allব্যবহার করতে হয়, নাহলে "warning:has multiple values" এরর আসবে। --localconfig সেট করার আগে রিপোর ভেতরে থাকতে হবে, নাহলে "error: could not lock config file" —.git/configফাইল অস্তিত্বহীন কোনো ডিরেক্টরিতে--localচালানো ব্যর্থ হবে, এটা--global/--system-এর থেকে ভিন্ন যা যেকোনো জায়গা থেকে চালানো যায়।core.editorসেট না থাকলে Git$EDITOR/$VISUALএনভায়রনমেন্ট ভ্যারিয়েবল দেখে, তারপরও কিছু না পেলে সিস্টেম ডিফল্ট (vi) ব্যবহার করে — অনেকেcore.editorসেট না করেও অভিযোগ করেন "Git ভুল এডিটর খুলছে", আসলে সমস্যা$EDITORএনভায়রনমেন্ট ভ্যারিয়েবলে।
PHASE 1: Local Workflow বেসিকস
Phase 0-এ মেন্টাল মডেল আর কনফিগারেশন তৈরি হয়েছে, এখন দৈনন্দিন Git ব্যবহারের তিনটা মূল স্তম্ভে যাওয়ার পালা: রিপো তৈরি/ক্লোন করা, staging area ম্যানেজ করা, আর commit করা। এই Phase শেষ হলে একজন ডেভেলপার একা একটা প্রজেক্টে সম্পূর্ণ স্বাচ্ছন্দ্যে দৈনন্দিন কাজ (edit → stage → commit) করতে পারবেন — branching/merging এখনো আসেনি, সেটা পরের ফেজে। তিনটা মডিউল: Module 3 (init/clone), Module 4 (staging — status/add/diff/restore/rm/mv), Module 5 (commit — message, amend, ফ্ল্যাগ)।
MODULE 3: Repo তৈরি ও ক্লোন
এই মডিউলে কভার হবে git init (normal ও --bare, --initial-branch=), git clone (প্লেইন, নির্দিষ্ট ফোল্ডারে, --branch, --single-branch), আর shallow clone (--depth)-এর প্রাথমিক পরিচিতি — একটা রিপো "শুরু করার" সব সম্ভাব্য পথ।
git init — নরমাল রিপো ও .git-এর ভেতরে কী তৈরি হয়
কী করে
git init একটা বিদ্যমান ডিরেক্টরিকে Git রিপোজিটরিতে রূপান্তর করে — নতুন প্রজেক্ট শুরু করার সবচেয়ে সাধারণ পথ।
mkdir my-app && cd my-app
git init
ভেতরে কী ঘটে
Git একটা .git/ সাব-ডিরেক্টরি তৈরি করে যার ভেতরে থাকে খালি objects/, refs/heads/, refs/tags/, একটা HEAD ফাইল (ref: refs/heads/main কনটেন্ট সহ — এখনো কোনো branch বাস্তবে তৈরি হয়নি, শুধু HEAD একটা ভবিষ্যৎ branch-কে পয়েন্ট করছে), আর একটা ডিফল্ট config ফাইল। এই মুহূর্তে কোনো commit নেই, তাই refs/heads/main ফাইলটাও এখনো অস্তিত্বহীন — প্রথম commit না হওয়া পর্যন্ত।
init.defaultBranch প্রভাব
git config --global init.defaultBranch main
git init
git branch --show-current
# main
আগে সেট না থাকলে পুরনো Git ভার্সনে ডিফল্ট ছিল master।
বিদ্যমান প্রজেক্টে init
cd existing-project
git init
# ইতিমধ্যে থাকা সব ফাইল working directory-তে থেকে যায়, কিন্তু কিছুই ট্র্যাক করা হয়নি এখনো — সব untracked দেখাবে `git status`-এ
রিয়েল-লাইফ দৃশ্যপট
একটা লিগ্যাসি প্রজেক্ট যেটা কখনো version control-এ ছিল না, তাতে git init চালিয়ে, তারপর .gitignore লিখে, তারপর git add -A && git commit -m "initial import" করলে পুরো প্রজেক্টের ইতিহাস ট্র্যাকিং শুরু হয় — এটাই সবচেয়ে সাধারণ "onboarding" মুহূর্ত।
git init --bare এবং --initial-branch=
--bare কী
একটা normal রিপোতে working directory + .git — দুটোই থাকে। একটা bare রিপোতে শুধু .git-এর কন্টেন্টটাই থাকে, সরাসরি রুট লেভেলে — কোনো working directory নেই, কোনো checkout করা ফাইল নেই।
git init --bare project.git
ls project.git
# HEAD config description hooks/ info/ objects/ refs/
কেন bare দরকার
Bare রিপো কখনো সরাসরি এডিট করার জন্য না — এটা শুধু push/pull করার জন্য একটা কেন্দ্রীয় বিনিময়-বিন্দু (exchange point) হিসেবে ডিজাইন করা। GitHub/GitLab-এর সার্ভারে থাকা প্রতিটা রিপো আসলে একটা bare রিপো।
# সার্ভারে (bare)
git init --bare /srv/git/project.git
# ডেভেলপার মেশিনে (non-bare)
git clone user@server:/srv/git/project.git
--initial-branch= দিয়ে branch নাম নির্দিষ্ট করা
git init --initial-branch=trunk my-repo
git -C my-repo branch --show-current
# trunk
এটা init.defaultBranch config-এর চেয়ে বেশি priority পায় (এক্সপ্লিসিট ফ্ল্যাগ সবসময় config override করে) — একবারের জন্য ভিন্ন নাম দরকার হলে কাজে লাগে।
bare vs non-bare — সারসংক্ষেপ
| Bare | Non-bare | |
|---|---|---|
| Working directory | নেই | আছে |
| সরাসরি edit/commit | করা যায় না | করা যায় |
| ব্যবহার | সার্ভার-সাইড হোস্টিং | ডেভেলপমেন্ট |
.git কোথায় |
রুটে সরাসরি | .git/ সাব-ফোল্ডারে |
রিয়েল-লাইফ দৃশ্যপট
নিজস্ব প্রাইভেট Git সার্ভার সেটআপ করতে চাইলে (GitHub ছাড়া) একটা VPS-এ git init --bare /srv/git/myproject.git চালিয়ে, তারপর SSH দিয়ে সেই পাথ থেকে clone করা যায় — এটাই সবচেয়ে সহজ self-hosted Git সার্ভার।
git clone — প্লেইন, নির্দিষ্ট ফোল্ডার, --branch, --single-branch
clone কী করে
git clone একটা বিদ্যমান রিপোর সম্পূর্ণ কপি (পুরো history, সব branch/tag by default) লোকাল মেশিনে ডাউনলোড করে, স্বয়ংক্রিয়ভাবে একটা origin নামের remote সেট করে দেয়।
git clone https://github.com/user/project.git
# 'project' নামের ফোল্ডার তৈরি হবে (URL থেকে অনুমান করা নাম)
নির্দিষ্ট ফোল্ডার নামে ক্লোন
git clone https://github.com/user/project.git my-local-name
--branch দিয়ে নির্দিষ্ট branch/tag চেকআউট করা
ডিফল্টে remote-এর default branch (সাধারণত main) checkout হয়। ভিন্ন branch দরকার হলে:
git clone --branch develop https://github.com/user/project.git
# clone-এর পরপরই 'develop' branch checked out অবস্থায় থাকবে
--single-branch — শুধু একটা branch-এর history আনা
ডিফল্টে clone সব branch-এর ref আনে (যদিও শুধু একটা checkout করা থাকে)। বড় রিপোতে অপ্রয়োজনীয় ডেটা এড়াতে:
git clone --single-branch --branch develop https://github.com/user/project.git
# শুধু 'develop' branch-এর history আসবে, অন্য branch-এর ref/objects আসবে না
ভেতরে কী ঘটে
Clone আসলে তিনটা কাজ একসাথে করে: (১) পুরো .git অবজেক্ট ডেটাবেজ ডাউনলোড করে (Phase 2-এ যেটা বিস্তারিত হবে), (২) refs/remotes/origin/* তৈরি করে প্রতিটা রিমোট branch ট্র্যাক করার জন্য, (৩) working directory-তে ডিফল্ট branch-এর ফাইলগুলো checkout করে।
রিয়েল-লাইফ দৃশ্যপট
একটা বিশাল মনোরেপো (কয়েক GB history) থেকে শুধু একটা feature branch নিয়ে কাজ করতে চাইলে --single-branch --branch feature/payment দিয়ে ডাউনলোড সময় ও ডিস্ক স্পেস — দুটোই উল্লেখযোগ্যভাবে কমানো যায়।
Shallow Clone (--depth) — প্রাথমিক পরিচিতি
সমস্যা
কিছু রিপোতে বছরের পর বছরের history — লক্ষ লক্ষ commit, বিশাল object database। শুধু বর্তমান কোড দরকার হলে, পুরো history ডাউনলোড করা অপচয়।
সমাধান: --depth
git clone --depth 1 https://github.com/torvalds/linux.git
এটা শুধু সর্বশেষ ১টা commit (এবং তার সাথে সংশ্লিষ্ট tree/blob object) ডাউনলোড করে — পুরো ইতিহাস না। ফলাফল: অনেক দ্রুত clone, অনেক কম ডিস্ক স্পেস।
ট্রেড-অফ
git log
# fatal: your current branch 'main' does not have any commits yet
# (বা মাত্র ১টা commit দেখাবে — বাকি history নেই)
Shallow clone-এ git log পুরনো commit দেখাতে পারবে না, git blame সীমিত হয়ে যায়, আর branch/tag operation-এ (push করার চেষ্টা) সমস্যা হতে পারে কারণ history-র সংযোগ (parent chain) অসম্পূর্ণ।
কোথায় ব্যবহার হয়
- CI/CD pipeline — বিল্ড/টেস্টের জন্য শুধু সর্বশেষ কোড দরকার, পুরো history অপ্রয়োজনীয়। প্রায় সব CI টুল (GitHub Actions, GitLab CI) ডিফল্টে shallow clone ব্যবহার করে।
- Docker image বিল্ড — image সাইজ ছোট রাখতে।
পরে বিস্তারিত
Shallow clone-কে পরে গভীর করা যায় (git fetch --unshallow), আর এর সাথে --depth-এর পুরো ব্যাকগ্রাউন্ড মেকানিজম, .git/shallow ফাইল, আর কীভাবে এটা partial clone (--filter=blob:none)-এর সাথে সম্পর্কিত — এসব পরবর্তী ফেজে (networking/remotes) বিস্তারিতভাবে কভার হবে। এখানে শুধু concept-টা পরিচিত করানোই লক্ষ্য।
এক লাইনে
Shallow clone = "আমাকে শুধু বর্তমান অবস্থা দাও, পুরো ইতিহাস না" — গতি ও স্পেসের বিনিময়ে history-নির্ভর অপারেশন ত্যাগ করা।
ল্যাব: init, bare, clone, shallow clone হাতে-কলমে
লক্ষ্য
একটা লোকাল bare "সার্ভার" রিপো বানিয়ে, সেটাকে normal ও shallow — দুইভাবে ক্লোন করে পার্থক্য দেখা (কোনো GitHub লাগবে না, সম্পূর্ণ লোকাল)।
ধাপ ১ — একটা "সার্ভার" রিপো বানানো (bare)
mkdir -p ~/gitlab-03/server && cd ~/gitlab-03/server
git init --bare project.git
ধাপ ২ — একটা ডেভেলপার কপি বানিয়ে কিছু commit করা
cd ~/gitlab-03
git clone server/project.git dev-copy
cd dev-copy
for i in 1 2 3 4 5; do
echo "line $i" >> notes.txt
git add notes.txt
git commit -m "add line $i"
done
git push origin main
ধাপ ৩ — সাধারণ ক্লোন
cd ~/gitlab-03
git clone server/project.git full-clone
cd full-clone && git log --oneline
# ৫টা commit-ই দেখা যাবে
ধাপ ৪ — shallow clone দিয়ে পার্থক্য দেখা
cd ~/gitlab-03
git clone --depth 1 server/project.git shallow-clone
cd shallow-clone && git log --oneline
# শুধু সর্বশেষ ১টা commit দেখাবে ("add line 5")
ধাপ ৫ — shallow ক্লোনে পুরনো history নেই তা যাচাই
git log --oneline --all
# একইভাবে মাত্র ১টা commit — বাকি ৪টা আনা হয়নি
cat .git/shallow
# সেই boundary commit-এর hash দেখাবে
ধাপ ৬ — --single-branch টেস্ট
cd ~/gitlab-03/dev-copy
git checkout -b experimental
git push origin experimental
cd ~/gitlab-03
git clone --single-branch --branch main server/project.git single-branch-clone
cd single-branch-clone
git branch -a
# শুধু main-ই দেখাবে, experimental দেখাবে না
চ্যালেঞ্জ
git -C shallow-clone fetch --unshallow চালিয়ে shallow ক্লোনকে পূর্ণ history-তে রূপান্তর করুন, তারপর git log --oneline চালিয়ে যাচাই করুন সব ৫টা commit এসেছে কিনা।
ইন্টারভিউ প্রশ্ন — Module 3
প্র.১ — git init চালানোর সাথে সাথেই কি একটা commit তৈরি হয়ে যায়?
উত্তর: না। git init শুধু .git/ ডিরেক্টরি স্ট্রাকচার (খালি objects/, refs/heads/, refs/tags/, HEAD ফাইল) তৈরি করে। HEAD একটা ভবিষ্যৎ branch-কে পয়েন্ট করে থাকে, কিন্তু সেই branch ref আসলে অস্তিত্বহীন থাকে প্রথম commit না হওয়া পর্যন্ত।
প্র.২ — Bare রিপো আর normal রিপোর মধ্যে পার্থক্য কী, এবং GitHub-এর সার্ভারে কোনটা থাকে?
উত্তর: Bare রিপোতে কোনো working directory নেই, শুধু .git-এর কন্টেন্ট সরাসরি রুটে থাকে — সরাসরি এডিট করা যায় না, শুধু push/pull-এর exchange point হিসেবে কাজ করে। GitHub/GitLab-এর সার্ভারে থাকা প্রতিটা রিপো একটা bare রিপো।
প্র.৩ — git clone চালালে কয়টা কাজ ভেতরে ভেতরে ঘটে?
উত্তর: তিনটা — (১) সম্পূর্ণ object database ডাউনলোড, (২) প্রতিটা remote branch-এর জন্য refs/remotes/origin/* তৈরি, (৩) ডিফল্ট branch-এর ফাইল working directory-তে checkout। পাশাপাশি একটা origin নামের remote স্বয়ংক্রিয়ভাবে সেট হয়।
প্র.৪ — --single-branch কখন ব্যবহার করবেন?
উত্তর: যখন একটা বড় মাল্টি-branch রিপোর শুধু একটা নির্দিষ্ট branch (যেমন একটা feature branch) নিয়ে কাজ করতে চান এবং বাকি branch-এর history/objects অপ্রয়োজনীয় — এটা ডাউনলোড সময় ও ডিস্ক স্পেস কমায়।
প্র.৫ — Shallow clone (--depth 1)-এর প্রধান ট্রেড-অফ কী?
উত্তর: দ্রুত clone ও কম ডিস্ক স্পেসের বিনিময়ে পুরনো commit history অনুপস্থিত থাকে — git log সীমিত history দেখায়, git blame অসম্পূর্ণ হয়, আর branch chain অসম্পূর্ণ থাকায় কিছু অপারেশনে সমস্যা হতে পারে।
প্র.৬ — CI/CD pipeline-এ shallow clone কেন কমন প্র্যাক্টিস? উত্তর: CI বিল্ড/টেস্টের জন্য শুধু বর্তমান কোড দরকার, পুরো history অপ্রয়োজনীয়। শুধু সর্বশেষ commit আনলে clone দ্রুত হয় ও রানার রিসোর্স বাঁচে — বেশিরভাগ CI টুল এই কারণে ডিফল্টে shallow clone ব্যবহার করে।
কর্নার কেস — Module 3
একটা বিদ্যমান, non-empty ডিরেক্টরিতে
git cloneচালানো যায় না — clone সবসময় একটা নতুন ফোল্ডার তৈরি করে (অথবা বর্তমান ডিরেক্টরি সম্পূর্ণ খালি হতে হবে)। বিদ্যমান প্রজেক্টে Git যোগ করতে হলেgit initব্যবহার করতে হয়,git cloneনা।Shallow clone-এ push করার চেষ্টা করলে অনেক সময় সার্ভার-সাইড রিফিউজ করতে পারে — কিছু Git hosting policy shallow repository থেকে push গ্রহণ করে না কারণ history chain অসম্পূর্ণ থাকে;
git fetch --unshallowকরে পূর্ণ history আনার পরই নিরাপদে push করা উচিত।git initএকটা বিদ্যমান.gitফোল্ডারযুক্ত ডিরেক্টরিতে আবার চালালে এরর দেয় না, বরং idempotent — বিদ্যমান কনফিগ/history অক্ষত রেখে নিরাপদে "reinitialize" করে — অনেকে ভয় পান এটা পুরনো ইতিহাস মুছে দেবে, বাস্তবে তা হয় না।--bareরিপোতে ভুলবশতgit add/git commitচালানোর চেষ্টা করলে "fatal: this operation must be run in a work tree" এরর আসে — এটা মনে করিয়ে দেয় bare রিপো শুধু বিনিময়-বিন্দু, ডেভেলপমেন্টের জায়গা না।
MODULE 4: Staging
এই মডিউলে দৈনন্দিন staging ওয়ার্কফ্লোর প্রতিটা কমান্ড: git status (+ -s/--porcelain), git add (প্লেইন, ., -A, -u, ইন্টারেক্টিভ -p, -N), git diff বনাম git diff --staged/--cached, git restore ও git restore --staged, এবং git rm/git mv।
git status ও তার আউটপুট ফরম্যাট (-s/--porcelain)
কী করে
git status তিনটা এলাকার (working directory, staging area, repository) মধ্যে বর্তমান পার্থক্য দেখায় — কোন ফাইল modified, staged, untracked।
git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: app.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: config.py
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_feature.py
ভেতরে কী তুলনা করে
Git তিনটা তুলনা একসাথে করে: HEAD commit-এর tree বনাম index (staging area) — এটাই "Changes to be committed" সেকশন দেখায়; আর index বনাম working directory — এটাই "Changes not staged" সেকশন দেখায়। Untracked ফাইল মানে index-এই নেই।
স্ক্রিপ্টের জন্য মেশিন-পাঠযোগ্য ফরম্যাট
git status -s
# M app.py
# M config.py
# ?? new_feature.py
প্রথম কলাম staging area-র অবস্থা, দ্বিতীয় কলাম working directory-র অবস্থা। --porcelain একই আউটপুট কিন্তু ভবিষ্যৎ Git ভার্সনেও ফরম্যাট স্থির থাকার গ্যারান্টি সহ — শেল স্ক্রিপ্ট/CI-তে parse করার জন্য এটাই নিরাপদ পছন্দ, প্লেইন -s না।
রিয়েল-লাইফ ব্যবহার
একটা pre-commit hook বা CI চেকে "working directory পরিষ্কার আছে কিনা" যাচাই করতে:
if [ -n "$(git status --porcelain)" ]; then
echo "Uncommitted changes found!"
exit 1
fi
এই প্যাটার্ন প্রায় প্রতিটা deployment স্ক্রিপ্টে দেখা যায় — deploy করার আগে নিশ্চিত করা হয় কোনো uncommitted change নেই।
git add: ., -A, -u — পার্থক্য
তিনটা ভিন্ন স্কোপ
git add-এর সবচেয়ে বেশি ভুল বোঝা অংশ হলো এর বিভিন্ন ফর্মের স্কোপ ভিন্ন।
git add .
বর্তমান ডিরেক্টরি ও তার নিচের নতুন + modified ফাইল stage করে — কিন্তু বর্তমান ডিরেক্টরির বাইরে deleted ফাইল ধরে না যদি সেটা অন্য পাথে থাকে (আসলে আধুনিক Git-এ . পুরো রিপোর deletion-ও ধরে যদি বর্তমান ডিরেক্টরি রিপো রুট হয়)।
git add -A
# বা: git add --all
পুরো রিপোজিটরি জুড়ে নতুন, modified, এবং deleted — সব ধরনের পরিবর্তন stage করে, বর্তমান ডিরেক্টরি নির্বিশেষে।
git add -u
# বা: git add --update
শুধু ইতিমধ্যে ট্র্যাক করা ফাইলের modification/deletion stage করে — নতুন (untracked) ফাইল কখনো যোগ করে না।
তুলনা টেবিল
| কমান্ড | নতুন ফাইল | modified ফাইল | deleted ফাইল | স্কোপ |
|---|---|---|---|---|
git add . |
✅ | ✅ | ✅ (Git 2.x+) | বর্তমান ডিরেক্টরি ও নিচে |
git add -A |
✅ | ✅ | ✅ | পুরো রিপো |
git add -u |
❌ | ✅ | ✅ | পুরো রিপো, শুধু tracked |
রিয়েল-লাইফ সমস্যা যা এটা এড়ায়
ধরুন একটা .env.local ফাইল ভুলবশত তৈরি হয়ে গেছে যেটা কমিট করা উচিত না। git add -u ব্যবহার করলে এই নতুন ফাইলটা কখনোই স্টেজ হবে না (নিরাপদ), কিন্তু git add -A/git add . ব্যবহার করলে আপনাকে অবশ্যই .gitignore-এ সেটা যোগ করে রাখতে হবে, নাহলে ভুলবশত কমিট হয়ে যাবে।
git add -p (ইন্টারেক্টিভ patch staging) ও -N (intent-to-add)
-p: ফাইলের ভেতরের অংশ (hunk) আলাদাভাবে stage করা
একই ফাইলে দুটো unrelated পরিবর্তন করে ফেললে, পুরো ফাইল একসাথে stage করলে commit history অপরিষ্কার হয়। -p (--patch) প্রতিটা hunk আলাদাভাবে review করে stage করার সুযোগ দেয়।
git add -p app.py
diff --git a/app.py b/app.py
@@ -10,3 +10,6 @@ def process():
return result
+
+def new_helper():
+ pass
Stage this hunk [y,n,q,a,d,s,e,?]?
গুরুত্বপূর্ণ অপশন: y (stage করো), n (skip করো), s (আরও ছোট hunk-এ split করো), e (ম্যানুয়ালি hunk এডিট করো)।
রিয়েল-লাইফ ব্যবহার
একই ফাইলে একটা bug fix আর একটা debug print() স্টেটমেন্ট দুটোই যোগ হয়ে গেছে। git add -p দিয়ে শুধু bug fix hunk stage করে debug print বাদ দেওয়া যায়, ফাইল আলাদা না করেই।
-N: Intent-to-Add
একটা নতুন ফাইল তৈরি করেছেন কিন্তু এখনই সম্পূর্ণ content stage করতে চান না — শুধু Git-কে জানাতে চান "এই ফাইলটা ট্র্যাক করা হবে ভবিষ্যতে"।
git add -N new_file.py
git status -s
# AM new_file.py ← Added কিন্তু Modified হিসেবেও দেখাচ্ছে
এর পর git diff (staged না, plain diff) এই নতুন ফাইলের সম্পূর্ণ content-কেই "diff" হিসেবে দেখাতে পারবে — -N ছাড়া git diff untracked ফাইল একেবারেই দেখায় না।
কেন -N কাজে লাগে
git add -p দিয়ে একটা সম্পূর্ণ নতুন ফাইলের নির্দিষ্ট অংশ stage করতে চাইলে প্রথমে -N দিয়ে ফাইলটাকে "known" বানাতে হয়, তারপরই -p সেই ফাইলের ভেতরে hunk-লেভেল সিলেকশন দেখাতে পারে — নাহলে সম্পূর্ণ নতুন ফাইল একটা একক ব্লক হিসেবে stage হয়ে যেত।
git diff বনাম git diff --staged/--cached
দুই ধরনের diff
git diff তিনটা এলাকার মধ্যে তুলনা করতে পারে — কিন্তু ফ্ল্যাগ ছাড়া ও ফ্ল্যাগসহ ভিন্ন জিনিস দেখায়।
git diff
এটা তুলনা করে staging area (index) বনাম working directory — অর্থাৎ যে পরিবর্তনগুলো এখনো git add করা হয়নি।
git diff --staged
# সমতুল্য: git diff --cached
এটা তুলনা করে HEAD commit বনাম staging area (index) — অর্থাৎ যে পরিবর্তনগুলো পরের commit-এ যাবে।
ভিজ্যুয়ালি
HEAD (last commit) ──diff --staged──► Index (staging) ──diff (no flag)──► Working Directory
তুলনা টেবিল
| কমান্ড | তুলনা করে | দেখায় |
|---|---|---|
git diff |
Index ↔ Working Directory | যা এখনো stage হয়নি |
git diff --staged |
HEAD ↔ Index | যা commit-এ যাবে |
git diff HEAD |
HEAD ↔ Working Directory | সব পরিবর্তন (staged + unstaged একসাথে) |
রিয়েল-লাইফ ওয়ার্কফ্লো
কমিট করার ঠিক আগে সবসময় git diff --staged চালিয়ে দেখে নেওয়া উচিত ঠিক কী commit হতে যাচ্ছে — এটা accidental debug কোড বা অসম্পূর্ণ পরিবর্তন commit হয়ে যাওয়া রোধ করে।
git add app.py
git diff --staged
# শুধু app.py-তে যা stage হয়েছে তার diff দেখাবে
git commit -m "fix: null check in process()"
গুরুত্বপূর্ণ পার্থক্য মনে রাখার শর্টকাট
git diff= "আমি কী add করতে ভুলে গেছি?"git diff --staged= "আমি ঠিক কী commit করতে যাচ্ছি?"
git restore ও git restore --staged
restore কী সমস্যার সমাধান
আগে git checkout দিয়ে ফাইল রিস্টোর করা আর branch পরিবর্তন — দুটো ভিন্ন কাজের জন্য একই কমান্ড ব্যবহার হতো, যা confusing ছিল। Git 2.23+ থেকে git restore (ফাইল রিস্টোরের জন্য) আর git switch (branch পরিবর্তনের জন্য) — আলাদা করা হয়েছে।
Working directory-র পরিবর্তন বাতিল করা
git restore app.py
# working directory-তে app.py-র পরিবর্তন বাতিল, index-এর ভার্সনে ফিরিয়ে দেয়
এটা destructive — commit না করা local পরিবর্তন স্থায়ীভাবে হারিয়ে যায়।
Stage থেকে সরানো (unstage)
git add app.py
git restore --staged app.py
# app.py আবার "unstaged" অবস্থায় ফিরে যায়, কিন্তু working directory-র পরিবর্তন অক্ষত থাকে
এটা নিরাপদ — শুধু staging area থেকে সরায়, ফাইলের কন্টেন্ট বদলায় না।
দুইটা একসাথে ব্যবহার করে সম্পূর্ণ রিসেট
git restore --staged --worktree app.py
# stage থেকেও সরাবে, working directory-র পরিবর্তনও বাতিল করবে — সম্পূর্ণ HEAD অবস্থায় ফিরিয়ে দেবে
ভেতরে কী ঘটে
git restore (ফ্ল্যাগ ছাড়া) index থেকে working directory-তে ফাইল কপি করে। git restore --staged HEAD commit-এর tree থেকে index-এ ফাইল কপি করে (working directory অক্ষত রেখে)।
রিয়েল-লাইফ দৃশ্যপট
ভুলবশত git add . চালিয়ে একটা secret ফাইল (.env) stage হয়ে গেছে commit করার আগেই ধরা পড়লো:
git restore --staged .env
# stage থেকে সরে গেলো, কমিট হওয়া থেকে বাঁচলো, ফাইলটা এখনো ডিস্কে অক্ষত আছে
git rm (--cached, -r) ও git mv
git rm — ফাইল ডিলিট ও unstage একসাথে
git rm old_file.py
এটা দুটো কাজ একসাথে করে: ফাইলটা working directory থেকে ডিলিট করে, এবং সেই deletion index-এ stage করে — পরের commit-এ ফাইলটা সরানো হিসেবে রেকর্ড হবে।
--cached: শুধু ট্র্যাকিং বন্ধ করা, ফাইল অক্ষত রাখা
git rm --cached secrets.env
এটা ফাইলটাকে শুধু Git-এর ট্র্যাকিং থেকে বাদ দেয়, ডিস্কের আসল ফাইল মুছে ফেলে না। এটাই সঠিক পদ্ধতি যখন একটা ফাইল ভুলবশত ট্র্যাক হয়ে গেছে (যেমন node_modules/ বা .env) — এখন .gitignore-এ যোগ করার সাথে সাথে ব্যবহার করা হয়:
echo ".env" >> .gitignore
git rm --cached .env
git commit -m "stop tracking .env"
-r: রিকার্সিভভাবে পুরো ফোল্ডার সরানো
git rm -r old_directory/
git mv — রিনেম/মুভ
git mv old_name.py new_name.py
ভেতরে কী ঘটে (গুরুত্বপূর্ণ)
git mv old.py new.py আসলে ইন্টার্নালি সমতুল্য:
mv old.py new.py
git rm old.py
git add new.py
Git-এ কোনো explicit "rename" object/অপারেশন নেই — Git rename detect করে (content similarity বিশ্লেষণ করে) git log --follow বা git diff চালানোর সময়, storage-এ rename বলে কিছু আলাদাভাবে রেকর্ড হয় না। git mv শুধু ম্যানুয়াল দুই ধাপ (rm + add) একসাথে করার সুবিধাজনক শর্টকাট।
রিয়েল-লাইফ দৃশ্যপট
প্রজেক্ট রিস্ট্রাকচার করার সময় src/utils.py কে src/helpers/utils.py-তে সরাতে git mv src/utils.py src/helpers/utils.py — এক কমান্ডে move + stage দুটোই হয়ে যায়, আলাদা করে git add লাগে না।
ল্যাব: স্টেজিং ওয়ার্কফ্লো সম্পূর্ণ প্র্যাকটিস
লক্ষ্য
স্ট্যাটাস চেক, সিলেক্টিভ স্টেজিং, diff review, আর restore — পুরো ওয়ার্কফ্লো এক সেশনে প্র্যাকটিস করা।
ধাপ ১ — সেটআপ
mkdir ~/gitlab-04 && cd ~/gitlab-04
git init
echo "def add(a, b):\n return a + b" > calc.py
git add calc.py
git commit -m "initial calc.py"
ধাপ ২ — দুটো unrelated পরিবর্তন একসাথে করা
cat >> calc.py << 'EOF'
def subtract(a, b):
return a - b
print("DEBUG:", add(1, 2))
EOF
ধাপ ৩ — status ও diff দিয়ে যাচাই
git status -s
# M calc.py
git diff
# দুটো hunk দেখাবে — নতুন ফাংশন যোগ + debug print
ধাপ ৪ — শুধু ফাংশনটা stage করা, debug print বাদ দিয়ে
git add -p calc.py
# subtract() ফাংশনের hunk-এ 'y', debug print-এর hunk-এ 'n'
ধাপ ৫ — নিশ্চিত হওয়া কী commit হতে যাচ্ছে
git diff --staged
# শুধু subtract() ফাংশন দেখাবে, debug print দেখাবে না
ধাপ ৬ — commit করা, তারপর বাকি অংশ পরিষ্কার করা
git commit -m "add subtract function"
git restore calc.py
# debug print লাইনটা working directory থেকেও বাতিল হয়ে গেলো
git diff
# কিছুই আসবে না — পরিষ্কার
ধাপ ৭ — rm --cached প্র্যাকটিস
echo "SECRET_KEY=abc123" > .env
git add .env
git commit -m "oops committed env"
echo ".env" >> .gitignore
git rm --cached .env
git commit -m "stop tracking .env"
ls .env # ফাইলটা ডিস্কে এখনো আছে
চ্যালেঞ্জ
git mv calc.py math_ops.py চালিয়ে git status চেক করুন rename কীভাবে detect হয় (renamed: লেবেল দেখাবে)।
ইন্টারভিউ প্রশ্ন — Module 4
প্র.১ — git add -A আর git add -u-এর পার্থক্য কী?
উত্তর: git add -A পুরো রিপোতে নতুন, modified, ও deleted — সব ধরনের পরিবর্তন stage করে। git add -u শুধু ইতিমধ্যে ট্র্যাক করা ফাইলের modification/deletion stage করে, কোনো নতুন (untracked) ফাইল কখনো যোগ করে না।
প্র.২ — git diff আর git diff --staged-এর মধ্যে পার্থক্য কী?
উত্তর: git diff তুলনা করে staging area বনাম working directory (যা এখনো add করা হয়নি)। git diff --staged (= --cached) তুলনা করে HEAD commit বনাম staging area (যা পরের commit-এ যাবে)।
প্র.৩ — git restore আর git restore --staged-এর পার্থক্য বলুন।
উত্তর: git restore <file> working directory-র পরিবর্তন বাতিল করে index-এর ভার্সনে ফিরিয়ে দেয় (destructive — local change হারায়)। git restore --staged <file> শুধু staging area থেকে unstage করে, working directory-র পরিবর্তন অক্ষত রাখে (নিরাপদ)।
প্র.৪ — একটা ফাইল ভুলবশত ট্র্যাক হয়ে গেছে (যেমন .env), কিন্তু ডিস্কে ফাইলটা রাখতে চান। কীভাবে সমাধান করবেন?
উত্তর: git rm --cached .env চালিয়ে শুধু Git-এর ট্র্যাকিং থেকে বাদ দিতে হবে (ফাইল ডিস্কে অক্ষত থাকবে), তারপর .gitignore-এ .env যোগ করে commit করতে হবে যাতে ভবিষ্যতে আবার ট্র্যাক না হয়।
প্র.৫ — Git কীভাবে rename ট্র্যাক করে — git mv-এর জন্য কি আলাদা কোনো object তৈরি হয়?
উত্তর: না, Git-এ কোনো explicit rename object/অপারেশন নেই। git mv আসলে ইন্টার্নালি mv + git rm + git add-এর শর্টকাট। Rename পরে detect করা হয় content similarity বিশ্লেষণ করে (git log --follow, git diff চালানোর সময়), storage-এ আলাদাভাবে রেকর্ড থাকে না।
প্র.৬ — git add -p-এ কখন ব্যবহার করবেন, আর -N (intent-to-add) কী সমস্যা সমাধান করে?
উত্তর: -p ব্যবহার করা হয় যখন একই ফাইলে একাধিক unrelated পরিবর্তন থাকে এবং শুধু নির্দিষ্ট hunk stage করতে চান। -N একটা সম্পূর্ণ নতুন ফাইলকে "known" (কিন্তু এখনো empty-এর মতো) বানায় যাতে git diff/git add -p সেই ফাইলের ভেতরে hunk-লেভেল সিলেকশন করতে পারে — নাহলে নতুন ফাইল একটা একক ব্লক হিসেবে stage হয়।
কর্নার কেস — Module 4
git add .পুরনো Git ভার্সনে (2.x-এর আগে) deleted ফাইল ধরত না, কিন্তু আধুনিক Git-এ ধরে — এটা নিয়ে পুরনো ব্লগ পোস্ট/স্ট্যাক ওভারফ্লো উত্তর প্রায়ই ভুল তথ্য দেয়; বর্তমান Git ভার্সনেgit add .আরgit add -A(রিপো রুট থেকে চালালে) কার্যত সমতুল্য আচরণ করে।git restoredestructive — এটাrmবাcheckout ---এর মতো, কোনো "undo" নেই যদি না কমিট হয়ে থাকে — একবারgit restore file.pyচালালে working directory-র uncommitted পরিবর্তন চিরতরে হারিয়ে যায়। কমিট না করা কাজ হারানোর সবচেয়ে সাধারণ কারণ এটাই।git rmফাইলে uncommitted local পরিবর্তন থাকলে ডিফল্টে রিফিউজ করে ("error: ... has local modifications") — নিরাপত্তা ফিচার, force করতে-fলাগে, যা ভুলবশত কাজ হারানো রোধ করে।git mvআর ম্যানুয়ালmv+git add+git rm-এর ফলাফল কার্যত সম্পূর্ণ অভিন্ন — কেউ ভাবতে পারেনgit mvএকটা বিশেষ "rename tracking" object তৈরি করে, বাস্তবে দুটো পদ্ধতিই একই ফলাফল দেয়,git mvশুধু সুবিধার জন্য একটা wrapper।
MODULE 5: Commit
এই মডিউলে কমিট করার প্রতিটা কৌশল: git commit -m, -am-এর ঝুঁকি, --amend (+ --no-edit), --allow-empty, --fixup/--squash-এর প্রিভিউ, -S সাইনিং প্রিভিউ, ভালো commit message লেখার নিয়ম (৫০/৭২ রুল, imperative mood), আর -v দিয়ে diff দেখে commit করা।
git commit -m এবং -am-এর ঝুঁকি
বেসিক commit
git commit -m "fix: null pointer in process()"
এটা staging area-তে যা কিছু আছে, তার একটা নতুন commit object তৈরি করে (Phase 2-এ বিস্তারিত) — বর্তমান HEAD-কে parent হিসেবে ধরে, একটা নতুন tree snapshot বানিয়ে, HEAD-কে সেই নতুন commit-এর দিকে সরিয়ে দেয়।
-am — দুটো কাজ একসাথে, কিন্তু একটা ফাঁদসহ
git commit -am "fix bug"
# সমতুল্য: git add -u && git commit -m "fix bug"
-a ফ্ল্যাগ commit-এর আগে স্বয়ংক্রিয়ভাবে সব ট্র্যাক করা ফাইলের পরিবর্তন stage করে দেয় — কিন্তু এটা git add -u-এর সমতুল্য, git add -A-এর না।
আসল ঝুঁকি
touch brand_new_file.py # নতুন, untracked ফাইল
echo "x = 1" >> existing.py # ট্র্যাক করা ফাইলে পরিবর্তন
git commit -am "add feature"
git status
# brand_new_file.py এখনো untracked! commit-এ যায়নি
অনেক ডেভেলপার ধরে নেন -am সব পরিবর্তন ধরে ফেলবে, কিন্তু নতুন ফাইল কখনোই -a দিয়ে stage হয় না — এটা শুধু ইতিমধ্যে ট্র্যাক করা ফাইলের জন্য কাজ করে। ফলে commit-এ একটা প্রয়োজনীয় নতুন ফাইল বাদ পড়ে যেতে পারে, যা পরে বিল্ড ব্রেক করে।
নিরাপদ অভ্যাস
Commit করার আগে সবসময় git status দেখে নিশ্চিত হওয়া উচিত কোনো নতুন ফাইল বাদ পড়ছে কিনা, বিশেষ করে -am ব্যবহারের সময়।
রিয়েল-লাইফ দৃশ্যপট
একটা নতুন মডিউল ফাইল (payment_gateway.py) তৈরি করে, একটা বিদ্যমান ফাইলে সেটা import যোগ করে -am দিয়ে commit করলে — commit-এ import লাইন থাকবে কিন্তু আসল payment_gateway.py ফাইলটাই থাকবে না, ফলে CI/deployment-এ ImportError আসবে।
git commit --amend (+ --no-edit) ও --allow-empty
--amend: সর্বশেষ commit সংশোধন
git commit --amend
এটা নতুন একটা commit object তৈরি করে যা পুরনো commit-এর জায়গা প্রতিস্থাপন করে — এটা পুরনো commit-কে এডিট করে না (Git-এ commit immutable, কখনো এডিট হয় না)। ভেতরে কী ঘটে: staging area-র বর্তমান কন্টেন্ট + আগের commit message নিয়ে একটা নতুন commit object বানানো হয়, HEAD ও current branch ref সেই নতুন commit-কে পয়েন্ট করে, পুরনো commit object অরফান হয়ে যায় (reachable না, পরে git gc-তে সাফ হতে পারে)।
# ভুলে একটা ফাইল বাদ পড়েছে সর্বশেষ commit-এ
git add forgotten_file.py
git commit --amend --no-edit
# --no-edit: commit message অপরিবর্তিত রেখে শুধু কন্টেন্ট আপডেট
--no-edit vs ডিফল্ট
ডিফল্টে --amend কমিট মেসেজ এডিটরে খোলে (পরিবর্তনের সুযোগ দেয়)। --no-edit দিলে আগের মেসেজ হুবহু রেখে দেয় — শুধু কন্টেন্ট/tree বদলায়।
⚠️ সবচেয়ে গুরুত্বপূর্ণ সতর্কতা
Push করা commit কখনো amend করা উচিত না যদি না নিশ্চিত হন কেউ সেই branch থেকে pull করেনি — কারণ amend পুরনো commit hash বাতিল করে নতুন hash তৈরি করে, যা অন্যদের লোকাল history-র সাথে diverge করে দেয় (force-push আর conflict লাগবে)।
--allow-empty: খালি commit তৈরি
git commit --allow-empty -m "trigger CI rebuild"
কোনো ফাইল পরিবর্তন ছাড়াই একটা commit তৈরি করে — একই tree hash, শুধু নতুন commit metadata। ব্যবহার: CI/CD pipeline জোর করে ট্রিগার করা, deployment marker রাখা।
তুলনা টেবিল
| অপারেশন | পুরনো commit কী হয় | ব্যবহার |
|---|---|---|
--amend |
নতুন commit প্রতিস্থাপন করে, পুরনোটা অরফান | সর্বশেষ commit ঠিক করা |
--allow-empty |
নতুন commit যোগ, কোনো ফাইল পরিবর্তন ছাড়াই | CI ট্রিগার, marker commit |
--fixup ও --squash — প্রিভিউ (বিস্তারিত Interactive Rebase-এ)
সমস্যা যা এই ফ্ল্যাগ দুটো সমাধান করে
একটা বড় feature-এর উপর কাজ করার সময় ইতিমধ্যে ১০টা commit হয়ে গেছে। পরে দেখা গেলো commit #৩-এ একটা সামান্য bug ছিল। সরাসরি একটা নতুন commit "fix typo in commit 3" যোগ করলে history অগোছালো হয়ে যায় — এই সমস্যার পরিষ্কার সমাধান --fixup।
--fixup
git commit --fixup=<commit-hash>
এটা একটা বিশেষ ফরম্যাটের commit message তৈরি করে (fixup! <original commit's subject>), যা পরে git rebase -i --autosquash চালালে স্বয়ংক্রিয়ভাবে সেই টার্গেট commit-এর সাথে মিশে যায় (squash হয়) — আপনাকে ম্যানুয়ালি reorder/squash করতে হয় না।
git log --oneline
# a1b2c3 fixup! add payment validation
# 9f8e7d add error logging
# 5c4d3e add payment validation ← এটাই টার্গেট
git rebase -i --autosquash 5c4d3e~1
# rebase editor-এ fixup commit স্বয়ংক্রিয়ভাবে টার্গেটের ঠিক নিচে সাজানো ও 'fixup' অ্যাকশন প্রি-সিলেক্টেড থাকবে
--squash
git commit --squash=<commit-hash>
--fixup-এর মতোই আচরণ, কিন্তু পার্থক্য: squash commit-এর message টার্গেট commit-এর message-এর সাথে একত্রিত হয় ফাইনাল squash-এর সময় (fixup-এ টার্গেটের message-ই থেকে যায়, নতুনটা বাতিল হয়)।
এই মডিউলে শুধু প্রিভিউ কেন
--fixup/--squash-এর পূর্ণ শক্তি বোঝা যায় শুধু git rebase -i --autosquash-এর সাথে একত্রে — যেটা একটা সম্পূর্ণ আলাদা, গভীর টপিক (পরবর্তী ফেজে Interactive Rebase মডিউলে বিস্তারিত)। এখানে শুধু concept ও কমান্ড-সিনট্যাক্স পরিচিত করানো হলো।
রিয়েল-লাইফ ওয়ার্কফ্লো
PR রিভিউয়ে "commit #৩-এ এই বাগ আছে" কমেন্ট এলে, ডেভেলপাররা প্রায়ই git commit --fixup=<hash> দিয়ে ফিক্স করে রাখেন, তারপর একেবারে শেষে rebase দিয়ে সব fixup একসাথে squash করে পরিষ্কার history নিয়ে merge করেন।
-S সাইনিং প্রিভিউ (বিস্তারিত Security ফেজে)
সমস্যা: commit-এ identity spoof করা যায়
git config user.email যেকোনো ইমেইল বসিয়ে দেওয়া যায় — কেউ চাইলে অন্য কারো নামে/ইমেইলে commit বানাতে পারে, Git নিজে সেটা যাচাই করে না। এটা একটা বিশ্বাসের সমস্যা তৈরি করে বিশেষ করে ওপেন-সোর্স প্রজেক্টে।
সমাধান: GPG/SSH সাইনিং
git commit -S -m "critical security fix"
-S ফ্ল্যাগ commit object তৈরি করার সময় আপনার প্রাইভেট কী দিয়ে একটা cryptographic signature যোগ করে। এই signature commit object-এর ভেতরেই সংরক্ষিত হয় (একটা gpgsig হেডার হিসেবে) — অর্থাৎ signature নিজেই commit-এর hash-এর অংশ।
যাচাই
git log --show-signature -1
# gpg: Good signature from "Rakib Hasan <rakib@example.com>"
GitHub-এ সঠিকভাবে সাইন করা commit-এ একটা "Verified" ব্যাজ দেখা যায়।
প্রতিটা commit-এ স্বয়ংক্রিয়ভাবে সাইন করা
git config --global commit.gpgsign true
git config --global user.signingkey <KEY_ID>
কেন এখানে শুধু প্রিভিউ
সাইনিং-এর পূর্ণ সেটআপ (GPG কী তৈরি, GitHub-এ পাবলিক কী আপলোড, SSH সাইনিং বিকল্প, .gitconfig-এর gpg.format) একটা সম্পূর্ণ আলাদা Security-কেন্দ্রিক ফেজের বিষয় — সেখানে বিস্তারিত সেটআপ ও ট্রাবলশুটিং কভার হবে। এখানে শুধু concept ও কেন এটা দরকার তা বোঝাই লক্ষ্য।
রিয়েল-লাইফ প্রভাব
Linux kernel-এর মতো বড় ওপেন-সোর্স প্রজেক্টে maintainer-রা unsigned commit reject করে দেন — এটাই নিশ্চিত করে যে commit সত্যিই সেই দাবি করা ব্যক্তির কাছ থেকে এসেছে, কেউ identity spoof করেনি।
ভালো Commit Message লেখা: ৫০/৭২ রুল ও Imperative Mood
৫০/৭২ রুল
Git কমিউনিটির একটা স্বীকৃত কনভেনশন:
- প্রথম লাইন (subject) সর্বোচ্চ ৫০ ক্যারেক্টার — সংক্ষিপ্ত সারাংশ।
- খালি একটা লাইন subject আর body-র মাঝে (বাধ্যতামূলক — নাহলে অনেক টুল body-কে subject-এর অংশ ধরে নেয়)।
- Body-র প্রতিটা লাইন সর্বোচ্চ ৭২ ক্যারেক্টার — টার্মিনাল/
git logআউটপুটে wrap ছাড়া readable থাকে।
git commit -m "fix: prevent race condition in order processing" -m "
Previously, concurrent requests to the same order ID could
create duplicate charge records. This adds a row-level lock
via SELECT ... FOR UPDATE before the charge is created.
Fixes #482"
Imperative Mood (আদেশসূচক ক্রিয়া)
Subject line লেখা উচিত এমনভাবে যেন এটা একটা আদেশ/নির্দেশ — "Fix bug" (✅), "Fixed bug" বা "Fixes bug" না (❌)।
কেন: Git নিজেই এই কনভেনশন অনুসরণ করে — git merge স্বয়ংক্রিয়ভাবে যে commit message বানায় তা হলো "Merge branch 'x'"। এই নিয়মটা মনে রাখার সহজ কৌশল: subject line-এর আগে মনে মনে "If applied, this commit will..." বসিয়ে পড়ুন — "If applied, this commit will fix bug" ব্যাকরণগতভাবে ঠিক আছে, "If applied, this commit will fixed bug" না।
তুলনা
| খারাপ | ভালো |
|---|---|
| "fixed the bug" | "fix null pointer in payment handler" |
| "updates" | "update dependency versions to patch CVE-2024-XXXX" |
| "asdf wip" | "wip: partial implementation of retry logic" |
রিয়েল-লাইফ প্রভাব
git log --oneline চালালে ৫০-ক্যারেক্টার সীমা মানা commit message-গুলো এক লাইনে সুন্দরভাবে align হয়ে থাকে, পড়া সহজ হয়। ভবিষ্যতে git bisect বা git blame দিয়ে ইতিহাস খুঁজতে হলে ভালো commit message-ই একমাত্র সূত্র — খারাপ message ("fix", "update", "wip") থাকলে debugging দ্বিগুণ সময় নেয়।
git commit -v — diff দেখে কমিট করা
সমস্যা
git commit (ফ্ল্যাগ ছাড়া, এডিটর খোলে) সাধারণত শুধু একটা খালি কমিট মেসেজ এডিটর দেখায় — ঠিক কী commit হতে যাচ্ছে তা মনে করে লিখতে হয়, ভুলে যাওয়ার সম্ভাবনা থাকে।
-v ফ্ল্যাগ
git commit -v
এটা কমিট মেসেজ এডিটর খোলার সময় নিচে staged diff-টাও comment হিসেবে দেখায় — ঠিক কোন লাইনগুলো পরিবর্তন হয়েছে তা দেখতে দেখতেই মেসেজ লেখা যায়।
# Please enter the commit message for your changes...
#
# On branch main
# Changes to be committed:
# modified: payment.py
#
diff --git a/payment.py b/payment.py
index abc123..def456 100644
--- a/payment.py
+++ b/payment.py
@@ -15,6 +15,9 @@ def process_payment(order):
+ if order.amount <= 0:
+ raise ValueError("Invalid amount")
স্থায়ীভাবে সেট করা
git config --global commit.verbose true
# এখন থেকে সাধারণ 'git commit' (ফ্ল্যাগ ছাড়া, এডিটর মোডে) সবসময় diff দেখাবে
রিয়েল-লাইফ সুবিধা
একটা multi-file commit-এ ঠিক কী কী পরিবর্তন হয়েছে মনে রাখা কঠিন। -v দিয়ে diff দেখতে দেখতে একটা নির্ভুল, নির্দিষ্ট commit message লেখা যায় — যেমন "fix: reject zero-amount payments" লেখার সময় সরাসরি diff-এ দেখা if order.amount <= 0 লাইনটাই মেসেজের ভাষা নির্ধারণ করতে সাহায্য করে।
গুরুত্বপূর্ণ নোট
diff অংশটা comment (# দিয়ে শুরু) হিসেবে থাকে — সেভ করার সময় স্বয়ংক্রিয়ভাবে বাদ পড়ে যায়, actual commit message-এ যুক্ত হয় না।
ল্যাব: Commit ওয়ার্কফ্লো — amend, empty, ভালো message
লক্ষ্য
--amend, --allow-empty, আর ভালো commit message লেখার অভ্যাস — একই সেশনে প্র্যাকটিস করা।
ধাপ ১ — সেটআপ
mkdir ~/gitlab-05 && cd ~/gitlab-05
git init
git config commit.verbose true
echo "def total(items):\n return sum(items)" > cart.py
git add cart.py
git commit -m "add total function"
ধাপ ২ — ভুল ধরা পড়লো, amend দিয়ে ঠিক করা
cat >> cart.py << 'EOF'
def total(items):
if not items:
return 0
return sum(items)
EOF
git add cart.py
git commit --amend -m "add total function with empty-list guard"
git log --oneline
# একটাই commit দেখাবে — নতুন hash, পুরনোটা প্রতিস্থাপিত
ধাপ ৩ — -am-এর ঝুঁকি হাতে-কলমে দেখা
touch discount.py # নতুন, untracked ফাইল
echo "# updated" >> cart.py # tracked ফাইলে পরিবর্তন
git commit -am "add discount support"
git status
# discount.py এখনো untracked! ভুল ধরা পড়লো
git add discount.py
git commit -m "add discount.py file"
ধাপ ৪ — --allow-empty প্র্যাকটিস
git commit --allow-empty -m "chore: trigger CI rebuild"
git log --oneline
# একটা নতুন commit, কোনো ফাইল পরিবর্তন ছাড়াই
ধাপ ৫ — ৫০/৭২ রুল মেনে একটা multi-line commit
echo "def apply_discount(total, pct):\n return total * (1 - pct)" >> cart.py
git add cart.py
git commit -v -m "feat: add percentage-based discount" -m "Adds apply_discount() which reduces the cart
total by a given percentage. Percentage is expected
as a fraction (0.1 = 10%), not a whole number."
চ্যালেঞ্জ
git log -p -1 চালিয়ে দেখুন সর্বশেষ commit-এর সম্পূর্ণ diff ও message কীভাবে একসাথে দেখা যায়, তারপর git log --format="%h %s" দিয়ে শুধু hash + subject দেখুন।
ইন্টারভিউ প্রশ্ন — Module 5
প্র.১ — git commit -am কেন সবসময় নিরাপদ না?
উত্তর: -a ফ্ল্যাগ git add -u-এর সমতুল্য — শুধু ইতিমধ্যে ট্র্যাক করা ফাইলের পরিবর্তন stage করে, কোনো নতুন (untracked) ফাইল কখনো যোগ করে না। ফলে যদি একটা নতুন ফাইল প্রয়োজনীয় হয় (যেমন একটা নতুন মডিউল), সেটা commit-এ বাদ পড়ে যেতে পারে, যা পরে বিল্ড ভেঙে ফেলে।
প্র.২ — git commit --amend কি পুরনো commit-কে এডিট করে, নাকি নতুন commit তৈরি করে?
উত্তর: এটা সবসময় একটা নতুন commit object তৈরি করে যা পুরনোটাকে প্রতিস্থাপন করে — Git-এ commit immutable, কখনো in-place এডিট হয় না। পুরনো commit object অরফান হয়ে যায় (কোনো ref থেকে reachable না) এবং পরে git gc-তে সাফ হতে পারে।
প্র.৩ — Push করা commit amend করা কেন বিপজ্জনক হতে পারে? উত্তর: amend পুরনো commit hash বাতিল করে নতুন hash তৈরি করে। যদি কেউ ইতিমধ্যে সেই পুরনো commit pull করে থাকেন, তাদের লোকাল history-র সাথে diverge তৈরি হয় — সমাধানে force-push এবং সম্ভাব্য conflict লাগবে, যা টিম-ওয়ার্কে সমস্যা তৈরি করে।
প্র.৪ — --fixup আর --squash-এর পার্থক্য কী?
উত্তর: দুটোই git rebase -i --autosquash-এর সাথে ব্যবহারের জন্য বিশেষ commit তৈরি করে যা টার্গেট commit-এর সাথে মিশে যায়। পার্থক্য: --fixup-এ ফাইনাল squash-এর সময় টার্গেটের message-ই থাকে (নতুনটা বাতিল), --squash-এ দুটো message একত্রিত করে এডিট করার সুযোগ দেওয়া হয়।
প্র.৫ — git commit -S কী করে, এবং এটা কীসের সমাধান?
উত্তর: এটা commit object-এ একটা cryptographic signature (GPG/SSH) যোগ করে যা প্রমাণ করে commit সত্যিই দাবি করা ব্যক্তির কাছ থেকে এসেছে। এটা সমাধান করে identity spoofing সমস্যা — যেহেতু user.email যেকোনো মান বসানো যায়, signature ছাড়া কেউ অন্য কারো নামে commit বানাতে পারত।
প্র.৬ — Commit message-এ imperative mood ("fix bug" না "fixed bug") কেন ব্যবহার করা হয়? উত্তর: এটা Git-এর নিজস্ব কনভেনশনের সাথে সামঞ্জস্যপূর্ণ (যেমন auto-generated "Merge branch 'x'" মেসেজ)। মনে রাখার কৌশল হলো subject-এর আগে "If applied, this commit will..." বসিয়ে পড়া — imperative mood-এই বাক্যটা ব্যাকরণগতভাবে সঠিক হয়।
কর্নার কেস — Module 5
git commit --amendকরলে commit-এর author date অপরিবর্তিত থাকে, কিন্তু committer date আপডেট হয়ে যায় —git log --format="%ad %cd"দিয়ে দেখলে বোঝা যায় দুটো তারিখ ভিন্ন হয়ে গেছে; এটা মাঝেমধ্যে audit/timeline বিশ্লেষণে বিভ্রান্তি তৈরি করে।খালি staging area নিয়ে
git commit(কোনো--allow-emptyছাড়া) চালালে Git এরর দেয়, চুপচাপ কিছু করে না — "nothing to commit, working tree clean" বার্তা আসে, নতুনরা মাঝে মাঝে ভাবেন commit হয়ে গেছে, বাস্তবে কিছুই ঘটেনি।--fixup/--squashcommit তৈরি করলেই স্বয়ংক্রিয়ভাবে squash হয় না — এই commit গুলো আলাদা commit হিসেবেই history-তে থেকে যায় যতক্ষণ না ম্যানুয়ালিgit rebase -i --autosquashচালানো হয়। অনেকে ভুল করে ভাবেন--fixupচালালেই কাজ শেষ।commit.gpgsign trueগ্লোবালি সেট থাকা অবস্থায় GPG এজেন্ট চালু না থাকলে বা key expire হয়ে গেলে প্রতিটা commit ব্যর্থ হবে ("gpg failed to sign the data") — CI/CD পাইপলাইনে হঠাৎ সব commit ব্যর্থ হওয়ার একটা কমন কারণ এটাই, বিশেষ করে key rotation-এর পর।
PHASE 2: Git Internals — Object Model
এটাই এই পুরো জার্নির সবচেয়ে গুরুত্বপূর্ণ ফেজ। এখন পর্যন্ত আমরা "porcelain" কমান্ড (add, commit, status) দিয়ে কাজ করেছি — এগুলো আসলে নিচের লেয়ারে থাকা "plumbing" কমান্ড আর একটা content-addressable object ডেটাবেজের উপর সুবিধাজনক wrapper মাত্র। এই ফেজ শেষ না করে branching, merging, rebase, reflog — কোনোটাই সত্যিকার অর্থে বোঝা সম্ভব না, শুধু মুখস্থ কমান্ড থেকে যাবে। তিনটা মডিউল: Module 6 (blob/tree/commit — object model-এর ভিত্তি), Module 7 (refs, HEAD, index — এবং প্লাম্বিং কমান্ড দিয়ে হাতে একটা কমিট বানানোর সবচেয়ে গুরুত্বপূর্ণ ল্যাব, শেষে একটা checkpoint gate), Module 8 (packfiles ও মেইনটেন্যান্স — অবজেক্টগুলো ডিস্কে বাস্তবে কীভাবে সংরক্ষিত থাকে)। এই ফেজের প্রতিটা concept পরবর্তী প্রতিটা ফেজে বারবার রেফারেন্স হবে — এখানে সময় নিয়ে গভীরভাবে বোঝা বিনিয়োগযোগ্য।
MODULE 6: Blob/Tree/Commit
এই মডিউলে Git-এর object model-এর তিনটা মূল বিল্ডিং ব্লক: content-addressable storage-এর মূল ধারণা (SHA hash কন্টেন্টের ফাংশন), blob (ফাইল-কন্টেন্ট, নাম ছাড়া), tree (ডিরেক্টরি স্ন্যাপশট — নাম+মোড+hash), আর commit (tree pointer + parent + author/committer + message)। সাথে plumbing কমান্ড git hash-object, git cat-file — এগুলো দিয়ে সরাসরি object ডেটাবেজে হাত দিয়ে দেখা।
Content-Addressable Storage — SHA Hash কেন Content-এর ফাংশন
মূল ধারণা
সাধারণ ফাইলসিস্টেমে একটা ফাইলের "ঠিকানা" তার নাম আর পাথ — /home/user/app.py। Git সম্পূর্ণ ভিন্নভাবে ভাবে: content-addressable storage মানে একটা অবজেক্টের ঠিকানা নির্ধারিত হয় তার কন্টেন্টের উপর ভিত্তি করে, নাম বা লোকেশনের উপর না।
Git প্রতিটা অবজেক্টের কন্টেন্ট নিয়ে একটা SHA-1 (নতুন রিপোতে ক্রমশ SHA-256) hash কম্পিউট করে — সেই hash-ই সেই অবজেক্টের একমাত্র identifier।
echo "hello world" | git hash-object --stdin
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
কেন এটা গুরুত্বপূর্ণ: একই কন্টেন্ট = একই hash
echo "hello world" > file1.txt
echo "hello world" > file2.txt
git hash-object file1.txt
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
git hash-object file2.txt
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad ← হুবহু একই!
দুইটা সম্পূর্ণ ভিন্ন ফাইল, ভিন্ন নাম, কিন্তু identical কন্টেন্ট থাকায় Git-এর object database-এ একটাই copy সংরক্ষিত থাকবে — এটাই automatic deduplication।
প্রভাব: Integrity Verification
যেহেতু hash কন্টেন্ট-নির্ভর, একটা অবজেক্টের কন্টেন্ট একটুও বদলে গেলে (এমনকি ১ বাইট) hash সম্পূর্ণ ভিন্ন হয়ে যাবে। এর মানে Git স্বয়ংক্রিয়ভাবে corruption detect করতে পারে — git fsck এই নীতির উপরই কাজ করে (Module 8-এ বিস্তারিত)।
রিয়েল-লাইফ প্রভাব
দুইটা ভিন্ন branch-এ যদি একই ফাইল (identical কন্টেন্ট) থাকে, Git storage-এ সেটা একবারই রাখে — এই জন্যই branch তৈরি করা "সস্তা" (cheap), কপি-অন-রাইট-স্টাইল স্টোরেজ মডেলের কারণে, ফাইল সিস্টেম-লেভেল কপি না।
এক লাইনে
ফাইলনাম বলে "এই ঠিকানায় যাও"। Content hash বলে "এই কন্টেন্টটাই আমি, যেখানেই থাকি না কেন।"
Blob — শুধু Content, কোনো নাম নেই
সংজ্ঞা
Blob (Binary Large Object) হলো Git-এর object model-এর সবচেয়ে সাধারণ একক — এটা একটা ফাইলের শুধু raw content সংরক্ষণ করে। গুরুত্বপূর্ণ: blob-এ ফাইলের নাম, পাথ, বা পারমিশন কিছুই থাকে না — শুধু বাইট কন্টেন্ট।
ভেতরে কী থাকে
একটা blob অবজেক্ট আসলে একটা হেডার + কন্টেন্ট, zlib দিয়ে compress করা:
blob <size>\0<content>
echo "print('hello')" | git hash-object -t blob --stdin
# একটা 40-ক্যারেক্টার SHA-1 hash আসবে
নাম কোথায় সংরক্ষিত হয়, তাহলে?
নাম-পাথ-পারমিশন সংরক্ষিত হয় tree অবজেক্টে (পরের leaf-এ) — blob শুধু "কী কন্টেন্ট" জানে, "কোথায়/কী নামে" জানে না। এই আলাদাকরণের একটা সরাসরি ফলাফল: একই কন্টেন্টের দুইটা ভিন্ন-নামের ফাইল (config.py আর backup_config.py, একই কন্টেন্ট) — দুটোই একই blob-কে পয়েন্ট করবে, storage-এ একবারই রক্ষিত হবে।
সরাসরি দেখা
echo "version 1" | git hash-object -w --stdin
# abc123... (hash প্রিন্ট হবে, -w দিয়ে object database-এ লেখাও হবে)
git cat-file -p abc123...
# version 1
git cat-file -t abc123...
# blob
রিয়েল-লাইফ প্রভাব
Git কখনো "diff store" করে না — একটা ফাইলের প্রতিটা ভার্সন একটা সম্পূর্ণ নতুন (বা dedup করা পুরনো) blob হিসেবে থাকে। এই জন্য git show <hash>:path/to/file দিয়ে যেকোনো পুরনো commit-এর যেকোনো ফাইলের পূর্ণ কন্টেন্ট সরাসরি, তাৎক্ষণিকভাবে বের করা যায় — কোনো delta chain apply করার দরকার নেই।
সারসংক্ষেপ
Blob = শুধু "কী" (content), "কোথায়" আর "কী নামে" — এই তথ্য blob-এর দায়িত্বে না, এটা tree-র কাজ।
Tree — ডিরেক্টরি স্ন্যাপশট (নাম + মোড + Hash)
সংজ্ঞা
Tree অবজেক্ট একটা ডিরেক্টরির স্ন্যাপশট প্রতিনিধিত্ব করে — এতে থাকে এন্ট্রির একটা তালিকা, প্রতিটা এন্ট্রিতে: file mode (permission, যেমন 100644 normal file বা 100755 executable), নাম, আর hash (blob-এর জন্য, অথবা সাব-ডিরেক্টরির জন্য আরেকটা tree object-এর hash)।
গঠন দেখা
git cat-file -p HEAD^{tree}
100644 blob a1b2c3... README.md
100644 blob d4e5f6... config.py
040000 tree 7a8b9c... src
লক্ষ্য করুন: src একটা সাব-ডিরেক্টরি হওয়ায় সেটা একটা নেস্টেড tree object-কে পয়েন্ট করছে, blob-কে না। এভাবে ডিরেক্টরি স্ট্রাকচার recursively প্রতিনিধিত্ব হয় — একটা tree of trees।
কেন এই ডিজাইন
Tree-blob আলাদা করার ফলে: একটা কমিটে যদি শুধু src/app.py বদলায়, তাহলে শুধু নতুন blob (app.py-র জন্য) আর সেই পাথের tree chain-এর (src/ এবং root) নতুন tree object তৈরি হয় — বাকি অপরিবর্তিত ফাইল/ফোল্ডারের tree/blob hash অপরিবর্তিত থাকে, পুনরায় তৈরি হয় না।
রিয়েল-লাইফ দৃশ্যপট
একটা প্রজেক্টে ১০০০টা ফাইল আছে, একটা commit-এ শুধু ১টা ফাইল বদলেছে। Git এই এক commit-এ নতুন কপি বানায় না ৯৯৯টা অপরিবর্তিত ফাইলের — শুধু বদলে যাওয়া ফাইলের blob + সেই পাথ পর্যন্ত tree chain (root থেকে সেই ফাইল পর্যন্ত ডিরেক্টরির প্রতিটা tree) নতুন করে তৈরি হয়, বাকি সব পুরনো hash-ই reuse হয়।
Tree কীভাবে তৈরি হয় (প্লাম্বিং)
git write-tree
# বর্তমান staging area (index) থেকে একটা tree object তৈরি করে, hash রিটার্ন করে
এটাই git commit-এর ভেতরে ভেতরে প্রথম ধাপ — Module 7-এর ল্যাবে বিস্তারিতভাবে হাতে-কলমে দেখা যাবে।
সারসংক্ষেপ টেবিল
| Object | কী রাখে | নাম আছে? |
|---|---|---|
| Blob | ফাইল কন্টেন্ট | না |
| Tree | এন্ট্রি তালিকা (mode+নাম+hash) | হ্যাঁ, প্রতি এন্ট্রির |
Commit Object — Tree Pointer + Parent + Author/Committer + Message
সংজ্ঞা
Commit অবজেক্ট একটা নির্দিষ্ট মুহূর্তে পুরো প্রজেক্টের অবস্থার একটা রেকর্ড। এতে থাকে চারটা মূল অংশ:
git cat-file -p HEAD
tree 7a8b9c1d2e3f...
parent 5c4d3e2f1a0b...
author Rakib Hasan <rakib@example.com> 1735000000 +0600
committer Rakib Hasan <rakib@example.com> 1735000000 +0600
fix: null check in payment handler
প্রতিটা ফিল্ডের অর্থ
- tree — এই মুহূর্তের সম্পূর্ণ প্রজেক্ট স্ন্যাপশটের root tree-র hash (আগের leaf-এ ব্যাখ্যা করা)।
- parent — আগের commit-এর hash। প্রথম commit-এ parent থাকে না। Merge commit-এ একাধিক parent থাকে (Phase-পরবর্তী branching মডিউলে বিস্তারিত)।
- author — কে আসল পরিবর্তনটা লিখেছেন, ও কখন (timestamp + timezone)।
- committer — কে এই commit object-টা তৈরি করেছেন। সাধারণত author আর committer একই ব্যক্তি, কিন্তু
git commit --amend,git cherry-pick, বাgit rebase-এর পর এরা ভিন্ন হয়ে যেতে পারে — committer date/identity পরিবর্তিত হয়, author তথ্য অক্ষত থাকে। - message — commit message (subject + body)।
Author বনাম Committer — কেন আলাদা
git log --format="Author: %an %ad%nCommitter: %cn %cd" -1
Rebase বা cherry-pick-এর সময় একটা commit "replay" করা হলে, মূল লেখক (author) অপরিবর্তিত থাকেন, কিন্তু committer হয়ে যান যিনি rebase/cherry-pick চালাচ্ছেন — এভাবে Git একাধিক ধাপে হাত বদল হওয়া commit-এর পুরো ইতিহাস ট্র্যাক রাখে।
Commit hash কীভাবে চেইন তৈরি করে
প্রতিটা commit তার parent-এর hash ধারণ করায়, পুরো history একটা linked list (বা merge commit-এর ক্ষেত্রে DAG — Directed Acyclic Graph) তৈরি করে। এই চেইনটাই git log traverse করে।
রিয়েল-লাইফ প্রভাব
commit hash tree + parent + author/committer + message — সবকিছুর উপর নির্ভরশীল হওয়ায়, --amend করলে (message বা content যেকোনো একটা বদলালেও) সম্পূর্ণ নতুন hash তৈরি হয় — এটাই আগের মডিউলে দেখা "amend নতুন commit তৈরি করে" নিয়মের root cause।
git hash-object -w --stdin দিয়ে ম্যানুয়ালি একটা Blob তৈরি করা
লক্ষ্য: object ডেটাবেজে সরাসরি হাত দেওয়া
git hash-object একটা plumbing কমান্ড যা কন্টেন্ট নিয়ে hash কম্পিউট করে, ঐচ্ছিকভাবে object database-এ লেখে।
শুধু hash কম্পিউট করা (ডিস্কে লেখে না)
echo "Hello, Git internals!" | git hash-object --stdin
# f6f75a11c9fbfb814a... (hash প্রিন্ট হলো, কিন্তু .git/objects-এ কিছু লেখা হয়নি)
-w দিয়ে আসলেই object database-এ লেখা
echo "Hello, Git internals!" | git hash-object -w --stdin
# একই hash প্রিন্ট হবে
find .git/objects -type f
# .git/objects/f6/f75a11c9fbfb814a... ← নতুন blob object ফাইল তৈরি হয়েছে
কীভাবে ফাইল হিসেবে সংরক্ষিত হয় (loose object ফরম্যাট)
Hash-এর প্রথম ২ ক্যারেক্টার ফোল্ডার নাম, বাকি ৩৮ ক্যারেক্টার ফাইল নাম — এটা একটা মিলিয়ন-প্লাস অবজেক্ট থাকা রিপোতেও একটা সিঙ্গেল ফ্ল্যাট ডিরেক্টরিতে ফাইলসিস্টেম স্লো-ডাউন এড়ায়।
xxd .git/objects/f6/f75a11c9fbfb814a... | head -3
# zlib-compressed বাইনারি ডেটা দেখাবে, প্লেইন টেক্সট না
বিদ্যমান ফাইল থেকে blob তৈরি
git hash-object -w README.md
# একটা এক্সিস্টিং ফাইলের কন্টেন্ট নিয়ে blob object তৈরি করবে
এটা কেন গুরুত্বপূর্ণ শেখার জন্য
git add-এর ভেতরে ভেতরে ঠিক এই কাজটাই ঘটে — প্রতিটা staged ফাইলের জন্য hash-object -w-এর সমতুল্য একটা অপারেশন চলে, তারপর সেই hash-গুলো index-এ (staging area) রেকর্ড হয়। git add আসলে hash-object -w + index আপডেট — এই দুটো কাজের একটা porcelain wrapper।
রিয়েল-লাইফ ব্যবহার
কোনো ফাইলের কন্টেন্ট আসলেই বদলেছে কিনা (byte-for-byte) চেক করতে, দুইটা ফাইলে git hash-object চালিয়ে hash তুলনা করা — diff কমান্ডের চেয়ে দ্রুত, বাইনারি ফাইলের জন্যও কাজ করে।
git cat-file -p/-t/-s এবং --batch
cat-file: object ডেটাবেজ পড়ার মূল টুল
git cat-file object hash দিয়ে সরাসরি object database থেকে কন্টেন্ট বের করে দেখায় — Git-এর "X-ray" টুল।
-t: টাইপ জানা
git cat-file -t a1b2c3d4
# blob (অথবা: tree, commit, tag)
-p: প্রেটি-প্রিন্ট (মানুষের পড়ার উপযোগী)
git cat-file -p a1b2c3d4 # blob হলে raw content দেখাবে
git cat-file -p HEAD^{tree} # tree হলে এন্ট্রি তালিকা দেখাবে
git cat-file -p HEAD # commit হলে tree/parent/author/message দেখাবে
-s: সাইজ
git cat-file -s a1b2c3d4
# 1847 (বাইটে, compress-এর আগের আসল সাইজ)
--batch: বহু অবজেক্ট দ্রুত প্রসেস করা (স্ক্রিপ্টিং-এর জন্য)
প্রতিটা hash-এর জন্য আলাদা cat-file প্রসেস চালানো ধীরগতির (প্রতিবার নতুন প্রসেস স্পন হয়)। --batch একটা persistent প্রসেস চালু রেখে stdin দিয়ে একাধিক hash নেয়:
git rev-list --all | git cat-file --batch-check
# প্রতিটা commit hash-এর জন্য: hash type size — একলাইনে
echo "HEAD" | git cat-file --batch
# type + size + সম্পূর্ণ raw content একসাথে দেখাবে
রিয়েল-লাইফ ব্যবহার
একটা কাস্টম টুল লিখতে চাইলে যা রিপোর হাজার হাজার commit স্ক্যান করে (যেমন একটা leaked-secret স্ক্যানার), --batch/--batch-check ব্যবহার করলে প্রতিটা object-এর জন্য নতুন প্রসেস স্পন করার ওভারহেড এড়ানো যায় — বড় রিপোতে এটা ১০-১০০ গুণ দ্রুত হতে পারে single cat-file কল লুপের তুলনায়।
সারসংক্ষেপ টেবিল
| ফ্ল্যাগ | কী দেখায় |
|---|---|
-t |
object type |
-p |
pretty-printed content |
-s |
সাইজ (বাইট) |
--batch |
একাধিক object দ্রুত, persistent process |
ল্যাব: object model তিনটা লেয়ারই হাতে-কলমে তৈরি করা
লক্ষ্য
blob, tree — এই দুইটা অবজেক্ট শুধু plumbing কমান্ড দিয়ে ম্যানুয়ালি তৈরি করে দেখা, git add/git commit ছাড়াই (commit object পরের মডিউলের ল্যাবে যোগ হবে)।
ধাপ ১ — খালি রিপো
mkdir ~/gitlab-06 && cd ~/gitlab-06
git init
ধাপ ২ — দুইটা blob ম্যানুয়ালি তৈরি করা
BLOB1=$(echo "# My Project" | git hash-object -w --stdin)
BLOB2=$(echo "print('hello')" | git hash-object -w --stdin)
echo "README blob: $BLOB1"
echo "main.py blob: $BLOB2"
ধাপ ৩ — প্রতিটা যাচাই
git cat-file -t $BLOB1 # blob
git cat-file -p $BLOB1 # # My Project
git cat-file -s $BLOB2 # সাইজ বাইটে
ধাপ ৪ — একটা tree ম্যানুয়ালি তৈরি করা (mktree ব্যবহার করে)
printf "100644 blob %s\tREADME.md\n100644 blob %s\tmain.py\n" "$BLOB1" "$BLOB2" | git mktree
# একটা tree hash প্রিন্ট হবে
ধাপ ৫ — সেই tree যাচাই করা
TREE=$(printf "100644 blob %s\tREADME.md\n100644 blob %s\tmain.py\n" "$BLOB1" "$BLOB2" | git mktree)
git cat-file -p $TREE
100644 blob a1b2c3... README.md
100644 blob d4e5f6... main.py
ধাপ ৬ — dedup যাচাই করা
BLOB3=$(echo "# My Project" | git hash-object -w --stdin)
echo $BLOB1 $BLOB3
# হুবহু একই hash — কন্টেন্ট identical হওয়ায় নতুন object তৈরিই হয়নি
ধাপ ৭ — সবকিছু কতটা "invisible" তা দেখা
git status
# nothing to commit, working tree clean — অথচ .git/objects-এ ৩টা নতুন object আছে!
find .git/objects -type f | wc -l
এটাই গুরুত্বপূর্ণ শিক্ষা: object database-এ কিছু থাকা মানেই সেটা কোনো branch/HEAD থেকে reachable — এই দুটো ভিন্ন জিনিস (Module 7-এ refs আসার পর পরিষ্কার হবে)।
চ্যালেঞ্জ
git cat-file --batch-check --batch-all-objects চালিয়ে এই মুহূর্তে object database-এ কী কী object আছে তার সম্পূর্ণ তালিকা দেখুন।
ইন্টারভিউ প্রশ্ন — Module 6
প্র.১ — Content-addressable storage মানে কী, আর এটা Git-এ কীভাবে কাজ করে? উত্তর: এটা এমন একটা স্টোরেজ মডেল যেখানে একটা অবজেক্টের ঠিকানা (identifier) নির্ধারিত হয় তার কন্টেন্টের উপর ভিত্তি করে, নাম বা লোকেশনের উপর না। Git প্রতিটা অবজেক্টের কন্টেন্ট থেকে একটা SHA hash কম্পিউট করে সেটাকেই identifier হিসেবে ব্যবহার করে — একই কন্টেন্ট সবসময় একই hash দেয়।
প্র.২ — Blob অবজেক্টে ফাইলের নাম সংরক্ষিত থাকে কি? না থাকলে নাম কোথায় থাকে? উত্তর: না, blob শুধু raw ফাইল কন্টেন্ট রাখে, কোনো নাম/পাথ/পারমিশন না। ফাইলের নাম, mode, ও blob-এর রেফারেন্স সংরক্ষিত থাকে tree অবজেক্টে।
প্র.৩ — একটা tree object-এ একটা সাব-ডিরেক্টরি কীভাবে প্রতিনিধিত্ব হয়?
উত্তর: সাব-ডিরেক্টরির জন্য tree-তে একটা এন্ট্রি থাকে যার mode 040000 এবং সেটা আরেকটা tree object-এর hash-কে পয়েন্ট করে (blob-কে না) — এভাবে recursively একটা tree of trees পুরো ডিরেক্টরি স্ট্রাকচার প্রতিনিধিত্ব করে।
প্র.৪ — Commit object-এর author আর committer ফিল্ড কখন ভিন্ন হয়ে যায়?
উত্তর: git rebase, git cherry-pick, বা কেউ অন্যের patch apply করলে — মূল লেখক (author) অপরিবর্তিত থাকেন কিন্তু committer হয়ে যান যিনি সেই অপারেশনটা চালাচ্ছেন। সাধারণ git commit-এ দুটো একই থাকে।
প্র.৫ — git add আর git hash-object -w-এর সম্পর্ক কী?
উত্তর: git add ইন্টার্নালি প্রতিটা staged ফাইলের জন্য hash-object -w-এর সমতুল্য একটা অপারেশন চালায় (blob object তৈরি করে), তারপর সেই hash index (staging area)-এ রেকর্ড করে। git add মূলত এই দুই কাজের একটা porcelain wrapper।
প্র.৬ — দুইটা সম্পূর্ণ ভিন্ন-নামের ফাইলে যদি হুবহু একই কন্টেন্ট থাকে, Git-এ স্টোরেজে কী ঘটে? উত্তর: দুটোই একই blob hash পাবে (content-addressable storage-এর কারণে), তাই Git-এর object database-এ সেই কন্টেন্টের একটাই কপি থাকবে — automatic deduplication, কোনো explicit ডি-ডুপ লজিক ছাড়াই।
কর্নার কেস — Module 6
git hash-object(ফ্ল্যাগ ছাড়া,-wছাড়া) hash প্রিন্ট করে কিন্তু object database-এ কিছুই লেখে না — অনেকে ভুলে যান-wফ্ল্যাগ যোগ করতে, তারপর অবাক হন কেনgit cat-file -p <সেই hash>বলে "fatal: Not a valid object name"।git cat-file -p <hash>কাজ করার জন্য hash-টা object database-এ থাকতেই হবে, শুধু syntactically সঠিক ৪০-ক্যারেক্টার স্ট্রিং হলেই চলবে না — একটা এলোমেলো কিন্তু সঠিক ফরম্যাটের hash দিলে "fatal: Not a valid object name" আসবে যদি সেটা আসলে কখনো তৈরি না হয়ে থাকে।একই কন্টেন্টের ফাইল বিভিন্ন লাইন-এন্ডিং (CRLF vs LF)-এ থাকলে ভিন্ন hash পাবে — Windows-এ তৈরি ফাইল (CRLF) আর Linux-এ তৈরি ফাইল (LF) কন্টেন্ট "দেখতে একই" হলেও বাইট-লেভেলে ভিন্ন, তাই blob hash-ও ভিন্ন হবে। এটাই cross-platform টিমে
.gitattributes/core.autocrlfজরুরি হওয়ার মূল কারণ।শুধু object database-এ blob/tree object থাকা মানেই সেটা "নিরাপদ" না — কোনো branch/tag/HEAD থেকে reachable না হলে সেটা unreachable এবং পরবর্তী
git gcচালালে চিরতরে মুছে যেতে পারে। Blob তৈরি করা মানে এখনো commit history-র অংশ হওয়া না, এটা শুধু "রিজার্ভ করা" — Module 7-এ commit-tree দিয়ে দেখা যাবে কীভাবে সত্যিকার reachability তৈরি হয়।
MODULE 7: Refs, HEAD, Index
এই মডিউলে refs (refs/heads, refs/remotes, refs/tags) আসলে কী তা বোঝা — শুধু commit hash ধরে রাখা টেক্সট ফাইল; HEAD কী (symbolic ref বনাম detached); প্লাম্বিং কমান্ড git symbolic-ref, git update-ref, git show-ref, git for-each-ref; .git/index-এর গঠন (staging area-র বাইনারি রিপ্রেজেন্টেশন); git ls-files, git update-index; আর সবচেয়ে গুরুত্বপূর্ণ ল্যাব — git write-tree, git read-tree, git commit-tree দিয়ে সম্পূর্ণ প্লাম্বিং কমান্ড ব্যবহার করে হাতে একটা কমিট বানানো। মডিউলের শেষে একটা checkpoint gate — Phase 2 সম্পূর্ণভাবে আত্মস্থ হয়েছে কিনা তা যাচাই করার জন্য।
Refs কী — শুধু একটা Commit Hash ধরে রাখা টেক্সট ফাইল
সংজ্ঞা
একটা ref (reference) Git-এ আশ্চর্যজনকভাবে সহজ একটা জিনিস — এটা শুধু একটা ফাইল যার ভেতরে একটা ৪০-ক্যারেক্টার (SHA-1) বা ৬৪-ক্যারেক্টার (SHA-256) commit hash লেখা থাকে। এইটুকুই। কোনো জাদু নেই।
সরাসরি দেখা
cat .git/refs/heads/main
# a1b2c3d4e5f6... ← শুধু একটা commit hash, আর কিছু না
তিন ধরনের ref
| Ref টাইপ | পাথ | কী বোঝায় |
|---|---|---|
| Branch | refs/heads/<name> |
একটা লোকাল branch-এর সর্বশেষ commit |
| Remote-tracking | refs/remotes/<remote>/<name> |
সর্বশেষবার fetch/push করা remote branch-এর অবস্থা |
| Tag | refs/tags/<name> |
একটা নির্দিষ্ট commit-এ স্থায়ী বুকমার্ক (annotated tag আলাদা object, lightweight tag শুধু ref) |
কেন branch তৈরি করা এত সস্তা (instant)
git branch experimental
# ভেতরে ভেতরে যা ঘটে: .git/refs/heads/experimental ফাইল তৈরি হয়,
# HEAD যে commit-এ আছে সেই hash-টা কপি করে লেখা হয়
কোনো ফাইল কপি হয় না, কোনো tree/blob পুনরায় তৈরি হয় না — শুধু একটা ৪১-বাইট (hash + newline) টেক্সট ফাইল তৈরি হয়। এটাই ব্যাখ্যা করে Git-এ branch করা কেন SVN-এর branch করার চেয়ে হাজার গুণ দ্রুত।
Branch "মুভ" করা মানে কী
git commit -m "new change"
commit করলে আসলে HEAD যে branch-কে পয়েন্ট করছে, সেই branch ref ফাইলের ভেতরের hash আপডেট হয়ে যায় — নতুন commit-এর hash দিয়ে ওভাররাইট হয়। Branch একটা "movable pointer" মাত্র, commit-এর মতো immutable object না।
রিয়েল-লাইফ প্রভাব
git branch -d feature-x চালালে শুধু একটা টেক্সট ফাইল ডিলিট হয় — commit object গুলো object database-এ থেকেই যায় (যদি অন্য কোনো ref থেকে reachable থাকে), অন্যথায় unreachable হয়ে পরবর্তী git gc-তে সাফ হতে পারে। এই জন্যই ভুলে ডিলিট হওয়া branch প্রায়ই git reflog দিয়ে রিকভার করা যায় (কিছুদিনের মধ্যে)।
HEAD — Symbolic Ref বনাম Detached HEAD
সাধারণ অবস্থা: HEAD একটা Symbolic Ref
HEAD সাধারণত সরাসরি একটা commit hash ধারণ করে না — বরং এটা একটা branch ref-কে পয়েন্ট করে, একে বলে symbolic ref।
cat .git/HEAD
# ref: refs/heads/main
এর মানে HEAD বলছে "আমি main branch-এর সাথে আছি" — main যেখানেই যাক (নতুন commit হলে), HEAD স্বয়ংক্রিয়ভাবে সেখানেই থাকবে।
Detached HEAD: সরাসরি একটা Commit-কে পয়েন্ট করা
git checkout a1b2c3d4
cat .git/HEAD
# a1b2c3d4e5f6... ← এখন সরাসরি একটা commit hash, কোনো branch নাম না
এই অবস্থায় HEAD কোনো branch-এর সাথে যুক্ত না — এটাকে "detached" বলা হয়। কোনো branch নাম ছাড়া একটা নির্দিষ্ট commit-এ "ভ্রমণ" করা।
Detached HEAD-এ কমিট করলে কী হয় (বিপদ)
# detached অবস্থায়
echo "experiment" > test.txt
git add test.txt
git commit -m "trying something"
এই নতুন commit কোনো branch থেকে reachable না! যদি এখন git checkout main করেন, এই commit-টা "হারিয়ে" যাবে (আসলে হারায় না ঠিক তখনই — git reflog-এ থেকে যায় কিছুদিন, কিন্তু কোনো branch পয়েন্ট করছে না বলে দৃষ্টির আড়ালে চলে যায়, এবং একসময় git gc-তে unreachable হিসেবে সাফ হয়ে যেতে পারে)।
নিরাপদ ব্যবহার
git checkout a1b2c3d4
# পরীক্ষা করলাম, ফলাফল ভালো লাগলো
git switch -c new-feature
# এখন detached অবস্থার commit-টা একটা branch-এ "নোঙর" করলো, নিরাপদ হয়ে গেলো
রিয়েল-লাইফ ব্যবহার
পুরনো একটা commit-এ গিয়ে (git checkout <old-hash>) কোড কেমন ছিল দেখা, বিল্ড টেস্ট করা — এগুলো নিরাপদ যতক্ষণ না নতুন commit করেন সেই অবস্থায়। যদি সেখানে কাজ চালিয়ে যেতে হয়, সবসময় প্রথমে git switch -c <নতুন-branch-নাম> করে নেওয়া উচিত।
git symbolic-ref, update-ref, show-ref, for-each-ref
চারটা প্লাম্বিং কমান্ড যা রেফের সাথে সরাসরি কাজ করে
git symbolic-ref — HEAD কী পয়েন্ট করছে জানা/বদলানো
git symbolic-ref HEAD
# refs/heads/main
git symbolic-ref HEAD refs/heads/develop
# HEAD-কে সরাসরি develop branch-এ পয়েন্ট করানো (checkout ছাড়াই, working directory অপরিবর্তিত)
git update-ref — একটা ref-এর মান সরাসরি সেট করা
git update-ref refs/heads/main a1b2c3d4
# main branch ref-কে জোর করে নির্দিষ্ট commit hash-এ সেট করে দেয় — 'git commit' ছাড়াই
এটাই ভেতরে ভেতরে ব্যবহৃত হয় git reset --hard <commit>-এর মতো কমান্ডে — branch ref-কে নতুন hash-এ পয়েন্ট করানো।
git show-ref — সব ref-এর তালিকা ও তাদের hash
git show-ref
# a1b2c3... refs/heads/main
# d4e5f6... refs/heads/develop
# 7a8b9c... refs/tags/v1.0.0
git for-each-ref — কাস্টম ফরম্যাটে ref তালিকা (স্ক্রিপ্টিং-এর জন্য)
git for-each-ref --format='%(refname:short) %(objectname:short) %(committerdate:relative)' refs/heads/
# main a1b2c3d 2 days ago
# develop d4e5f6a 5 hours ago
রিয়েল-লাইফ ব্যবহার
একটা deployment স্ক্রিপ্ট যেটা স্বয়ংক্রিয়ভাবে সর্বশেষ tag খুঁজে বের করে:
git for-each-ref --sort=-creatordate --format='%(refname:short)' refs/tags/ | head -1
# সর্বশেষ তৈরি হওয়া tag-এর নাম, সরাসরি স্ক্রিপ্টে ব্যবহারযোগ্য
আর git update-ref ব্যবহার হয় custom Git tooling-এ যেখানে একটা branch-কে নির্দিষ্ট commit-এ পয়েন্ট করাতে হয় কিন্তু working directory স্পর্শ না করেই (যেমন একটা bare mirror রিপো আপডেট করা)।
কেন এগুলো "plumbing"
এই কমান্ডগুলো কোনো safety-check করে না (যেমন uncommitted changes আছে কিনা), কোনো human-friendly আউটপুট দেয় না — এগুলো সরাসরি নিচের লেয়ারে কাজ করে, script/automation-এর জন্য ডিজাইন করা। git branch, git checkout-এর মতো porcelain কমান্ড এদেরই wrapper, অতিরিক্ত safety আর সুবিধাজনক ইন্টারফেসসহ।
.git/index কী — Staging Area-র বাইনারি রিপ্রেজেন্টেশন
Index আসলে কী
.git/index একটা বাইনারি ফাইল — এটাই "staging area"-র প্রকৃত বাস্তব রূপ। এতে প্রতিটা tracked ফাইলের একটা এন্ট্রি থাকে: ফাইলের পাথ, mode, blob hash, ফাইল সাইজ, mtime (last modified time), আর কিছু অতিরিক্ত মেটাডেটা (stage number, flags)।
কেন এটা প্রয়োজন (working directory আর HEAD-এর মাঝে একটা "তৃতীয় স্ন্যাপশট")
Index আসলে একটা প্রস্তাবিত পরবর্তী commit-এর tree-র flat প্রতিনিধিত্ব — git write-tree এই index থেকেই একটা প্রকৃত tree object বানায় (পরের ল্যাবে হাতে-কলমে দেখা যাবে)।
বাইনারি হওয়ার কারণ (পারফরম্যান্স)
একটা টেক্সট ফাইল হলে হাজার হাজার ফাইলের রিপোতে git status চালানো ধীর হয়ে যেত — প্রতিটা ফাইলের mtime/size চেক করে working directory-র সাথে দ্রুত তুলনা করার জন্য index বাইনারি ফরম্যাটে ডিজাইন করা, যাতে O(1)-এর কাছাকাছি lookup সম্ভব হয়।
সরাসরি দেখা (raw বাইনারি না, readable আকারে)
git ls-files --stage
100644 a1b2c3d4e5f6... 0 README.md
100644 d4e5f6a7b8c9... 0 main.py
এখানে 0 হলো "stage number" — সাধারণ অবস্থায় সবসময় ০ (কনফ্লিক্ট-মুক্ত)। Merge conflict-এর সময় একই ফাইলের একাধিক এন্ট্রি stage 1/2/3 নিয়ে দেখা যায় (base/ours/theirs) — এটা পরবর্তী merge মডিউলে বিস্তারিত হবে।
রিয়েল-লাইফ প্রভাব
git status দ্রুত হওয়ার মূল কারণ এই index-এর mtime ক্যাশিং — Git প্রথমে mtime তুলনা করে (সস্তা), শুধু mtime বদলেছে মনে হলে actual content hash আবার কম্পিউট করে (ব্যয়বহুল) যাচাই করতে ফাইলটা সত্যিই বদলেছে কিনা। এই optimization-ই বিশাল রিপোতে (লক্ষ লক্ষ ফাইল) git status-কে ব্যবহারযোগ্য গতিতে রাখে।
git ls-files (-s, --others, --ignored)
Index-এর কন্টেন্ট মানুষের পড়ার উপযোগী ফরম্যাটে
git ls-files index (আর ঐচ্ছিকভাবে working directory)-র বিভিন্ন দৃশ্য দেখানোর জন্য একটা multi-purpose plumbing কমান্ড।
বেসিক — সব tracked ফাইল
git ls-files
# README.md
# main.py
# src/utils.py
-s: স্টেজ তথ্যসহ (mode, hash, stage number)
git ls-files -s
100644 a1b2c3d4e5f6789... 0 README.md
100644 d4e5f6a7b8c9012... 0 main.py
এটাই সরাসরি index-এর raw এন্ট্রি — git cat-file-এর index-ভার্সন বলা যায়।
--others: untracked ফাইল দেখা
git ls-files --others
# temp_notes.txt
# debug.log
ডিফল্টে এটা .gitignore-এ থাকা ফাইলও untracked হিসেবে দেখায়, তাই সাধারণত --exclude-standard-এর সাথে ব্যবহার হয়:
git ls-files --others --exclude-standard
# শুধু সত্যিকার untracked ফাইল, .gitignore-এ থাকা ফাইল বাদে
--ignored: কোন ফাইলগুলো .gitignore-এর কারণে বাদ পড়ছে
git ls-files --others --ignored --exclude-standard
# node_modules/package.json
# __pycache__/module.pyc
এটা ডিবাগ করতে কাজে লাগে যখন সন্দেহ হয় একটা ফাইল ভুলবশত .gitignore-এর প্যাটার্নে পড়ে যাচ্ছে।
রিয়েল-লাইফ ব্যবহার
একটা pre-commit hook যেটা নিশ্চিত করে কোনো .env-স্টাইল ফাইল ভুলবশত tracked হয়ে যায়নি:
git ls-files | grep -E '\.env$' && echo "WARNING: .env is tracked!" && exit 1
সারসংক্ষেপ
| ফ্ল্যাগ | দেখায় |
|---|---|
| (কিছু না) | সব tracked ফাইল |
-s |
mode+hash+stage সহ raw index এন্ট্রি |
--others |
untracked ফাইল |
--ignored |
.gitignore-এর কারণে বাদ পড়া ফাইল |
git update-index (--assume-unchanged, --skip-worktree)
সমস্যা: কিছু ফাইল "লোকালি বদলানো দরকার কিন্তু কমিট করা যাবে না"
কিছু ফাইল (যেমন লোকাল ডেভেলপমেন্ট config, settings.local.py) tracked থাকা দরকার (টিমের একটা টেমপ্লেট শেয়ার করার জন্য), কিন্তু প্রতিটা ডেভেলপার নিজের মতো বদলাবেন, আর সেই বদল কখনো commit হওয়া উচিত না। .gitignore এখানে কাজ করবে না কারণ ফাইলটা ইতিমধ্যে tracked।
--assume-unchanged: "এই ফাইলের দিকে তাকিও না, বদলায়নি ধরে নাও"
git update-index --assume-unchanged config/local_settings.py
এর পর থেকে git status/git diff এই ফাইলের পরিবর্তন সম্পূর্ণ উপেক্ষা করবে, যদিও ফাইলটা আসলে বদলে গেছে। ⚠️ এটা পারফরম্যান্স-অপ্টিমাইজেশন হিসেবে ডিজাইন করা (বড় ফাইল যেগুলোর mtime চেক ব্যয়বহুল), নিরাপদ "ignore" মেকানিজম হিসেবে না — Git নিজেই সতর্ক করে এটা silently override হয়ে যেতে পারে কিছু অপারেশনে।
# ফিরিয়ে আনতে
git update-index --no-assume-unchanged config/local_settings.py
--skip-worktree: আধুনিক, নির্ভরযোগ্য বিকল্প
git update-index --skip-worktree config/local_settings.py
এটা --assume-unchanged-এর চেয়ে বেশি নির্ভরযোগ্য এই নির্দিষ্ট ব্যবহারের জন্য — যখন ফাইলটা লোকালি বদলানো প্রত্যাশিত (উদ্দেশ্যমূলক), Git checkout/merge-এর সময় এই ফাইল overwrite করার চেষ্টা করবে না।
তুলনা
| ফ্ল্যাগ | মূল উদ্দেশ্য | নির্ভরযোগ্যতা |
|---|---|---|
--assume-unchanged |
পারফরম্যান্স (বিশাল ফাইল) | কম — সহজে override হয় |
--skip-worktree |
ইচ্ছাকৃত লোকাল কাস্টমাইজেশন | বেশি — এই কাজের জন্যই ডিজাইন করা |
যাচাই করা কোন ফাইল flag করা আছে
git ls-files -v | grep '^S'
# S config/local_settings.py ← 'S' মানে skip-worktree সেট আছে
রিয়েল-লাইফ দৃশ্যপট
একটা মাইক্রোসার্ভিস টেমপ্লেটে docker-compose.override.yml ফাইল কমিট করা আছে ডিফল্ট ভ্যালুসহ, কিন্তু প্রতিটা ডেভেলপার নিজের লোকাল পোর্ট/পাথ বদলাবেন — --skip-worktree দিয়ে এই ফাইলের লোকাল পরিবর্তন git status/accidental commit থেকে সুরক্ষিত রাখা যায়।
ল্যাব: write-tree, commit-tree দিয়ে সম্পূর্ণ প্লাম্বিং কমান্ডে হাতে একটা কমিট বানানো
লক্ষ্য
git add/git commit — এই দুইটা porcelain কমান্ড একবারও ব্যবহার না করে, শুধু plumbing কমান্ড (hash-object, update-index, write-tree, commit-tree, update-ref) দিয়ে একটা সম্পূর্ণ বৈধ commit তৈরি করা এবং সেটাকে একটা branch-এ reachable বানানো। এটাই object model বোঝার চূড়ান্ত প্রমাণ।
ধাপ ১ — খালি রিপো
mkdir ~/gitlab-07-plumbing && cd ~/gitlab-07-plumbing
git init
git config user.email "rakib@example.com"
git config user.name "Rakib Hasan"
ধাপ ২ — blob object তৈরি করা (hash-object)
BLOB=$(echo "# Plumbing Demo" | git hash-object -w --stdin)
echo "Blob hash: $BLOB"
ধাপ ৩ — সেই blob-কে index-এ যোগ করা (update-index) — 'git add'-এর সমতুল্য কাজ ম্যানুয়ালি
git update-index --add --cacheinfo 100644,$BLOB,README.md
git ls-files -s
# 100644 <BLOB hash> 0 README.md ← index-এ এন্ট্রি যোগ হলো
ধাপ ৪ — index থেকে একটা tree object তৈরি করা (write-tree)
TREE=$(git write-tree)
echo "Tree hash: $TREE"
git cat-file -p $TREE
# 100644 blob <BLOB hash> README.md
ধাপ ৫ — সেই tree থেকে একটা commit object তৈরি করা (commit-tree) — কোনো parent ছাড়া (প্রথম commit)
COMMIT=$(echo "Initial commit via plumbing" | git commit-tree $TREE)
echo "Commit hash: $COMMIT"
git cat-file -p $COMMIT
tree <TREE hash>
author Rakib Hasan <rakib@example.com> ...
committer Rakib Hasan <rakib@example.com> ...
Initial commit via plumbing
ধাপ ৬ — এখনো এই commit কোনো branch থেকে reachable না
git log
# fatal: your current branch 'main' does not have any commits yet
ধাপ ৭ — একটা branch ref তৈরি করে "নোঙর" করা (update-ref)
git update-ref refs/heads/main $COMMIT
git log --oneline
# <short-hash> Initial commit via plumbing ← এখন দেখা যাচ্ছে!
git status
# nothing to commit, working tree clean
ধাপ ৮ — দ্বিতীয় commit, parent-সহ (read-tree দিয়ে বিদ্যমান tree লোড করে পরিবর্তন)
BLOB2=$(echo "print('hello world')" | git hash-object -w --stdin)
git read-tree $TREE # বর্তমান index-কে সেই tree-এর অবস্থায় রিসেট
git update-index --add --cacheinfo 100644,$BLOB2,main.py
TREE2=$(git write-tree)
COMMIT2=$(echo "Add main.py via plumbing" | git commit-tree $TREE2 -p $COMMIT)
git update-ref refs/heads/main $COMMIT2
git log --oneline
# দুইটা commit, স্বাভাবিক 'git log'-এই দেখা যাচ্ছে
চ্যালেঞ্জ
git checkout main -- . চালিয়ে working directory-তে ফাইল দুইটা আসল কিনা দেখুন — পুরো এই সময়ে working directory ছিল সম্পূর্ণ খালি, শুধু object database আর index ম্যানিপুলেট করা হয়েছিল।
ইন্টারভিউ প্রশ্ন — Module 7
প্র.১ — একটা ref (যেমন refs/heads/main) আসলে কী?
উত্তর: এটা একটা সাধারণ টেক্সট ফাইল যার ভেতরে একটা commit hash লেখা থাকে — কোনো জটিল কাঠামো না। এই সরলতার কারণেই branch তৈরি করা তাৎক্ষণিক (instant) — শুধু একটা ছোট ফাইল লেখা হয়, কোনো content কপি হয় না।
প্র.২ — Symbolic ref আর detached HEAD-এর পার্থক্য কী?
উত্তর: সাধারণ অবস্থায় HEAD একটা symbolic ref — এটা refs/heads/<branch>-কে পয়েন্ট করে, তাই branch এগোলে HEAD-ও স্বয়ংক্রিয়ভাবে এগোয়। Detached HEAD-এ HEAD সরাসরি একটা commit hash ধারণ করে, কোনো branch-এর সাথে যুক্ত না — এই অবস্থায় commit করলে সেই commit কোনো branch থেকে reachable হয় না।
প্র.৩ — .git/index ফাইলটা আসলে কী প্রতিনিধিত্ব করে?
উত্তর: এটা staging area-র বাইনারি রিপ্রেজেন্টেশন — প্রস্তাবিত পরবর্তী commit-এর tree-র একটা flat তালিকা (প্রতিটা ফাইলের পাথ, mode, blob hash, mtime সহ)। git write-tree এই index থেকেই একটা প্রকৃত tree object তৈরি করে।
প্র.৪ — git update-ref কী করে, আর এটা কোন porcelain কমান্ডের ভেতরে ব্যবহৃত হয়?
উত্তর: এটা একটা ref-কে সরাসরি একটা নির্দিষ্ট commit hash-এ সেট করে দেয়, কোনো safety check বা working-directory পরিবর্তন ছাড়াই। এটা ব্যবহৃত হয় git reset --hard, git branch -f-এর মতো কমান্ডের ভেতরে।
প্র.৫ — --assume-unchanged আর --skip-worktree-এর মধ্যে কোনটা ইচ্ছাকৃত লোকাল কাস্টমাইজেশনের জন্য বেশি নির্ভরযোগ্য, এবং কেন?
উত্তর: --skip-worktree বেশি নির্ভরযোগ্য এই ব্যবহারের জন্য — এটা ঠিক এই উদ্দেশ্যেই ডিজাইন করা (ইচ্ছাকৃতভাবে tracked ফাইল লোকালি বদলানো), Git checkout/merge-এর সময় এই ফাইল overwrite করার চেষ্টা করে না। --assume-unchanged মূলত পারফরম্যান্স-অপ্টিমাইজেশনের জন্য, সহজে override/রিসেট হয়ে যেতে পারে।
প্র.৬ — git commit-এর ভেতরে ভেতরে কোন তিনটা plumbing অপারেশন ঘটে (সংক্ষেপে)?
উত্তর: (১) write-tree — index থেকে একটা tree object তৈরি, (২) commit-tree — সেই tree + বর্তমান HEAD-কে parent হিসেবে নিয়ে একটা commit object তৈরি, (৩) update-ref — বর্তমান branch ref-কে নতুন commit hash-এ আপডেট করা।
কর্নার কেস — Module 7
Detached HEAD-এ থাকা অবস্থায় করা commit তাৎক্ষণিকভাবে হারায় না, কিন্তু "reachable" থাকে না —
git reflogকিছুদিনের জন্য এই commit-এর রেফারেন্স ধরে রাখে (ডিফল্টে ৩০-৯০ দিন), কিন্তু কোনো branch-এ "নোঙর" (git switch -c) না করলে একসময়git gc-তে unreachable object হিসেবে সাফ হয়ে যেতে পারে।git update-refদিয়ে একটা branch ref-কে জোর করে অন্য commit-এ সেট করলে, working directory স্বয়ংক্রিয়ভাবে আপডেট হয় না — এটাgit reset --hard-এর থেকে ভিন্ন (যেটা ref আপডেটের পাশাপাশি working directory-ও sync করে); শুধুupdate-refচালালে working directory আর নতুন HEAD state-এর মধ্যে mismatch তৈরি হয়েgit status-এ প্রচুর "modified" ফাইল দেখাতে পারে।--assume-unchangedসেট করা একটা ফাইল remote branch থেকে merge/pull করার সময় নীরবে override/সমস্যা তৈরি করতে পারে — Git কখনো কখনো ফ্ল্যাগ উপেক্ষা করে ফাইল আপডেট করতে বাধ্য হয়, যার ফলে ঠিক কোন ফাইলে flag ছিল তা ভুলে গেলে অদ্ভুত merge আচরণ দেখা যায়।.git/indexফাইল সরাসরি টেক্সট এডিটরে খোলা বা ম্যানুয়ালি এডিট করার চেষ্টা করলে রিপো নষ্ট হয়ে যায় — এটা বাইনারি ফরম্যাট, শুধুgit update-index/git read-tree-জাতীয় কমান্ড দিয়েই নিরাপদে বদলানো উচিত, কখনো raw ফাইল হিসেবে এডিট না করে।
চেকপয়েন্ট — Phase 2 সম্পন্ন
আপনি কি পারবেন?
Phase 3-এ (branching ও merging) যাওয়ার আগে নিজেকে সৎভাবে যাচাই করুন — নিচের প্রতিটা প্রশ্নের উত্তর confidently দিতে পারলেই এগিয়ে যান:
- আপনি কি ব্যাখ্যা করতে পারবেন কেন
git commit --amendআসলে একটা নতুন commit object তৈরি করে, পুরনোটা এডিট করে না — এবং এটা কেন push করা commit-এ বিপজ্জনক? - আপনি কি হাতে-কলমে ব্যাখ্যা করতে পারবেন
git addচালানোর সময় ভেতরে ভেতরে ঠিক কোন plumbing অপারেশনগুলো ঘটে (blob তৈরি + index আপডেট)? - আপনি কি বলতে পারবেন কেন দুইটা সম্পূর্ণ ভিন্ন-নামের, ভিন্ন-পাথের ফাইল, একই কন্টেন্ট থাকলে, object database-এ একবারই সংরক্ষিত হয়?
- আপনি কি পার্থক্য করতে পারবেন branch ref (movable pointer, একটা টেক্সট ফাইল) আর commit object (immutable, permanent)-এর মধ্যে?
- আপনি কি ব্যাখ্যা করতে পারবেন detached HEAD অবস্থায় করা commit কেন "হারিয়ে যাওয়ার" ঝুঁকিতে থাকে?
- আপনি কি নিজে থেকে,
git add/git commitব্যবহার না করে, শুধুhash-object,write-tree,commit-tree,update-refদিয়ে একটা বৈধ commit তৈরি করতে পারবেন?
কেন এই ফেজ একটা বাধ্যতামূলক গেট
এই ছয়টা প্রশ্নের প্রতিটাই সরাসরি Phase 3+-এর প্রতিটা টপিকের ভিত্তি — branching মানে নতুন ref তৈরি করা, merging মানে একাধিক parent-সহ commit তৈরি করা, rebase মানে commit-কে নতুন parent দিয়ে "replay" করে নতুন hash তৈরি করা, reflog মানে ref-এর ইতিহাস ট্র্যাক করা। এই object/ref/index মডেলটা মুখস্থ না হয়ে সত্যিকার অর্থে অন্তর্গত না হলে, পরের প্রতিটা ফেজ শুধু "এই পরিস্থিতিতে এই কমান্ড টাইপ করো" ধরনের প্যাটার্ন-মেমোরাইজেশন হয়ে থাকবে, প্রকৃত বোঝাপড়া হবে না।
যদি উপরের যেকোনো একটা প্রশ্নে আটকে যান, Module 6 ও 7-এর লিফগুলো, বিশেষ করে প্লাম্বিং ল্যাবটা, আবার করে দেখুন — এগিয়ে যাওয়ার আগে এই ভিত্তিটা শক্ত হওয়া জরুরি।
MODULE 8: Packfiles & Maintenance
এই মডিউলে দেখা হবে object database বাস্তবে ডিস্কে কীভাবে efficient রাখা হয়: loose object বনাম packfile (এবং delta compression), git gc/git repack দিয়ে maintenance, git verify-pack/git index-pack/git unpack-objects দিয়ে packfile-এর ভেতরে দেখা, git count-objects দিয়ে ডায়াগনস্টিক, আর git fsck/git prune দিয়ে ইন্টেগ্রিটি চেক ও পরিষ্কার করা।
Loose Objects বনাম Packfiles — কেন Pack করা হয়, Delta Compression
Loose Object: প্রতিটা object একটা আলাদা ফাইল
এখন পর্যন্ত আমরা যা দেখেছি — git hash-object -w চালালে .git/objects/xx/yyyy... — একটা আলাদা zlib-compressed ফাইল তৈরি হয়। এটাকে বলে loose object। প্রতিটা blob/tree/commit আলাদা ফাইল।
সমস্যা: হাজার হাজার ছোট ফাইল
একটা সক্রিয় প্রজেক্টে হাজার হাজার commit মানে হাজার হাজার loose object ফাইল — প্রতিটা ফাইলের নিজস্ব filesystem overhead (inode, metadata), আর অনেক ছোট ফাইল ফাইলসিস্টেম I/O-কে ধীর করে দেয়।
সমাধান: Packfile
একটা packfile অনেকগুলো object-কে একটা মাত্র ফাইলে একসাথে সংকুচিত করে রাখে, সাথে একটা index ফাইল (.idx) যা দ্রুত lookup সম্ভব করে (কোন object কোথায় আছে সেই ফাইলের ভেতরে)।
ls .git/objects/pack/
# pack-a1b2c3....pack (আসল ডেটা)
# pack-a1b2c3....idx (দ্রুত lookup index)
Delta Compression — প্যাকিং-এর আসল জাদু
Packfile শুধু জিপ-স্টাইল কম্প্রেশন করে না — এটা similar object-দের মধ্যে delta (পার্থক্য) স্টোর করে। যদি দুইটা blob প্রায় একই রকম (যেমন একই ফাইলের দুইটা কাছাকাছি ভার্সন), Git একটাকে পূর্ণরূপে (base) রাখে, আরেকটাকে শুধু পার্থক্য (delta) হিসেবে রাখে — উল্লেখযোগ্যভাবে জায়গা বাঁচায়।
গুরুত্বপূর্ণ: এটা loose object সংরক্ষণের মূল নীতি (snapshot-based, delta না) ভঙ্গ করে না — delta compression শুধু স্টোরেজ-লেভেল অপ্টিমাইজেশন, conceptual মডেলে প্রতিটা commit এখনো একটা পূর্ণ, স্বতন্ত্র snapshot। প্রয়োজনে (cat-file -p) Git স্বচ্ছভাবে delta থেকে পূর্ণ কন্টেন্ট পুনর্গঠন করে দেয়, ইউজারের কাছে এই delta সম্পূর্ণ transparent।
তুলনা টেবিল
| Loose Object | Packfile | |
|---|---|---|
| ফাইল সংখ্যা | প্রতি object একটা ফাইল | একটা/কিছু বড় ফাইল |
| Compression | সাধারণ zlib | zlib + delta compression |
| তৈরি হয় কখন | git add/commit-এর সাথে সাথে |
git gc/git push/periodic auto-packing |
| Lookup গতি | সরাসরি ফাইল পাথ | .idx ফাইল দিয়ে বাইনারি সার্চ |
রিয়েল-লাইফ প্রভাব
Linux kernel-এর মতো বিশাল রিপো (কয়েক লাখ commit) packed অবস্থায় কয়েক GB, কিন্তু loose object হিসেবে থাকলে এটা কয়েকগুণ বড় হতো এবং clone/fetch অনেক ধীর হতো। git push/git fetch-এর সময় নেটওয়ার্কে যা পাঠানো হয় তা-ও একটা packfile — যেটা দ্রুত ট্রান্সমিশনের জন্য অপ্টিমাইজ করা।
git gc (--aggressive, --prune=now) ও git repack
git gc — Garbage Collection, কিন্তু শুধু "ময়লা সাফ" না
git gc আসলে দুটো কাজ একসাথে করে: (১) loose object-দের একত্রিত করে নতুন packfile বানায় (পারফরম্যান্স), (২) unreachable object (কোনো ref/reflog থেকে পৌঁছানো যায় না এমন object) — নির্দিষ্ট সময় (ডিফল্ট ২ সপ্তাহ) পার হয়ে গেলে মুছে ফেলে।
git gc
# Auto packing the repository...
স্বয়ংক্রিয় ট্রিগার
Git স্বয়ংক্রিয়ভাবে git gc --auto চালায় যখন loose object সংখ্যা একটা থ্রেশহোল্ড (ডিফল্ট ~৬৭০০) পার হয় — সাধারণত ম্যানুয়ালি git gc চালানোর দরকার হয় না, কিন্তু বড় অপারেশনের (বিশাল history rewrite) পর ম্যানুয়ালি চালানো উপকারী হতে পারে।
--aggressive — আরও গভীর অপ্টিমাইজেশন
git gc --aggressive
এটা delta compression-এর জন্য আরও ব্যাপক অনুসন্ধান করে (বেশি candidate base object পরীক্ষা করে) — ফলাফল ছোট packfile, কিন্তু চালাতে অনেক বেশি সময় লাগে। সাধারণত এটা মাঝেমধ্যে (মাসে একবার, বড় রিপোতে) চালানোর জন্য, নিয়মিত ওয়ার্কফ্লোর অংশ না।
--prune=now — সাথে সাথে unreachable object মুছে ফেলা
git gc --prune=now
ডিফল্ট grace period (২ সপ্তাহ) উপেক্ষা করে তাৎক্ষণিকভাবে সব unreachable object মুছে ফেলে। ⚠️ সতর্কতা: এর মানে git reflog দিয়ে অতি সাম্প্রতিক "হারানো" commit রিকভার করার সুযোগও চলে যেতে পারে যদি সেই grace period পার হওয়ার আগে prune করেন।
git repack — শুধু repacking, gc-র বাকি কাজ ছাড়া
git repack -a -d
# -a: সব object একটা নতুন packfile-এ একত্রিত করা
# -d: পুরনো redundant packfile ডিলিট করা
git gc ভেতরে git repack-ই ব্যবহার করে, কিন্তু repack সরাসরি চালালে fine-grained control পাওয়া যায় (কবে prune হবে তা আলাদাভাবে নিয়ন্ত্রণ করা যায়)।
রিয়েল-লাইফ ওয়ার্কফ্লো
একটা বড় git filter-repo/BFG অপারেশনের পর (history থেকে বড় ফাইল/secret সরানো) — পুরনো object গুলো unreachable হয়ে যায় কিন্তু ডিস্কে থেকে যায় যতক্ষণ না git gc --prune=now --aggressive চালানো হয়। রিপোর সাইজ আসলে না কমা পর্যন্ত এই ধাপ ভুলে গেলে মনে হবে history rewrite "কাজ করেনি"।
git verify-pack, git index-pack, git unpack-objects
git verify-pack — packfile-এর ইন্টেগ্রিটি ও কন্টেন্ট যাচাই
git verify-pack -v .git/objects/pack/pack-a1b2c3....idx
a1b2c3d4... commit 245 178 12
d4e5f6a7... tree 89 67 190
7a8b9c1d... blob 1847 502 257 1 d4e5f6a7...
কলামগুলো: hash, type, uncompressed size, compressed size, offset, (delta হলে) depth ও base object hash। -v দিয়ে প্রতিটা object লিস্ট হয় এবং শেষে chain integrity ভেরিফাই করা হয় — কোনো corruption থাকলে এখানে ধরা পড়বে।
git index-pack — একটা .pack ফাইল থেকে .idx রিজেনারেট করা
git index-pack pack-a1b2c3....pack
যদি কোনোভাবে .idx ফাইল হারিয়ে যায় বা করাপ্ট হয়ে যায় (কিন্তু .pack ফাইল অক্ষত), এই কমান্ড দিয়ে .pack ফাইল স্ক্যান করে আবার .idx তৈরি করা যায় — একটা রিকভারি টুল।
git unpack-objects — একটা packfile-কে আবার loose object-এ ভেঙে ফেলা
git unpack-objects < pack-a1b2c3....pack
এটা বিরল কিন্তু ডিবাগিং-এ কাজে লাগে — একটা packfile-এর ভেতরের প্রতিটা object আলাদা loose ফাইলে বের করে আনলে সেগুলো আলাদাভাবে পরীক্ষা করা সহজ হয়ে যায় (cat-file, hash যাচাই একটা একটা করে)।
রিয়েল-লাইফ দৃশ্যপট
একটা transferred/ডাউনলোড করা packfile করাপ্ট কিনা সন্দেহ হলে, প্রথমে git verify-pack -v চালিয়ে দেখা হয় — যদি ইন্টেগ্রিটি এরর আসে, git index-pack দিয়ে রিজেনারেট চেষ্টা করা যায়; সম্পূর্ণ করাপ্ট হলে সেই source থেকে আবার fetch করাই একমাত্র সমাধান।
সারসংক্ষেপ
| কমান্ড | কাজ |
|---|---|
verify-pack |
ইন্টেগ্রিটি যাচাই, কন্টেন্ট লিস্ট |
index-pack |
.pack থেকে .idx রিজেনারেট |
unpack-objects |
packfile → loose objects, ডিবাগিং-এর জন্য |
git count-objects -v — Loose বনাম Packed দেখা
দ্রুত ডায়াগনস্টিক
git count-objects -v
count: 42
size: 168
in-pack: 15230
packs: 1
size-pack: 48920
prune-packable: 3
garbage: 0
size-garbage: 0
প্রতিটা ফিল্ডের অর্থ
- count — কতগুলো loose object আছে (এখনো packfile-এ যায়নি)।
- size — সেই loose object গুলোর মোট সাইজ (KB-তে)।
- in-pack — packfile-এর ভেতরে থাকা object সংখ্যা।
- packs — কতগুলো packfile আছে।
- size-pack — packfile গুলোর মোট সাইজ (KB-তে)।
- prune-packable — এমন loose object যেগুলো ইতিমধ্যে কোনো packfile-এও আছে, তাই নিরাপদে prune করা যায়।
- garbage — করাপ্টেড বা অপ্রয়োজনীয় ফাইল object ডিরেক্টরিতে।
কেন এটা ব্যবহার করবেন
count > 0 অনেক বেশি হলে (হাজার+) বোঝায় রিপোতে অনেক নতুন কাজ হয়েছে কিন্তু সাম্প্রতিক git gc চলেনি — git status/git log-এর গতিতে সামান্য প্রভাব ফেলতে পারে।
# gc চালানোর আগে-পরে তুলনা
git count-objects -v # count: 5000+
git gc
git count-objects -v # count: 0, in-pack বৃদ্ধি পেয়েছে
রিয়েল-লাইফ ব্যবহার
একটা বড় CI সার্ভারে যেখানে একই রিপো বারবার fetch/rebuild হয়, git count-objects -v দিয়ে পিরিয়ডিক্যালি চেক করে দেখা যায় রিপো "ফুলে" যাচ্ছে কিনা (loose object জমছে কিনা), আর সেই অনুযায়ী একটা scheduled git gc cron job সেট করা যায় ডিস্ক স্পেস ও I/O পারফরম্যান্স নিয়ন্ত্রণে রাখতে।
এক-লাইন সারাংশ
git count-objects -vহলো রিপোর "স্বাস্থ্য পরীক্ষা" — কতটা ময়লা (loose object) জমেছে, packing কতটা efficient, তার একটা তাৎক্ষণিক স্ন্যাপশট।
git fsck (--lost-found) ও git prune
git fsck — Filesystem Check, object ডেটাবেজের ইন্টেগ্রিটি যাচাই
git fsck
এটা পুরো object database ট্র্যাভার্স করে যাচাই করে: প্রতিটা object-এর hash তার কন্টেন্টের সাথে মেলে কিনা (corruption ধরা), সব রেফারেন্স করা object (tree-র ভেতরের blob hash, commit-এর parent hash) আসলে অস্তিত্বশীল কিনা (dangling reference ধরা)।
git fsck --full
# dangling commit a1b2c3d4...
# dangling blob e5f6a7b8...
dangling object মানে কী
Dangling মানে object database-এ আছে, কিন্তু কোনো অন্য object থেকে reference করা না (একটা orphan commit, বা কোনো tree-তে যোগ না হওয়া blob) — সাধারণত এগুলো তৈরি হয় git reset --hard, amend, বা rebase-এর পর পুরনো commit "রেখে যাওয়ার" ফলে।
--lost-found — dangling commit উদ্ধার করা
git fsck --lost-found
ls .git/lost-found/commit/
# a1b2c3d4...
এটা প্রতিটা dangling commit-কে .git/lost-found/-এ একটা ফাইল হিসেবে রাখে, যা থেকে ম্যানুয়ালি ইনস্পেক্ট বা রিকভার করা যায়:
git show a1b2c3d4
# সেই কমিটের কন্টেন্ট দেখা যাবে
git branch recovered-work a1b2c3d4
# একটা branch দিয়ে আবার reachable বানানো
git prune — dangling/unreachable object চিরতরে মুছে ফেলা
git prune
# ডিফল্ট grace period (২ সপ্তাহ) পার হওয়া unreachable object মুছে দেয়
git prune --dry-run
# আসলে না মুছে শুধু কী মুছা হতো তার তালিকা দেখায় — নিরাপদ প্রথম ধাপ
⚠️ git gc-এর ভেতরেও prune চলে, তাই আলাদাভাবে git prune কমই সরাসরি চালানো হয় — সাধারণত git gc-এর সাথে integrated থাকে।
রিয়েল-লাইফ দৃশ্যপট: রিকভারি
একজন ডেভেলপার ভুলে git reset --hard HEAD~3 চালিয়ে ৩টা commit "হারিয়ে ফেলেছেন" (আসলে unreachable হয়ে গেছে, ডিলিট হয়নি এখনই):
git fsck --lost-found
# dangling commit <hash> ← হারানো কাজ এখানে!
git branch recovered <hash>
git reflog সাধারণত এই ধরনের রিকভারির জন্য আরও সহজ পথ (পরবর্তী ফেজে বিস্তারিত), কিন্তু git fsck --lost-found একটা fallback যখন reflog-ও কোনো কারণে সাহায্য করছে না।
সারসংক্ষেপ
| কমান্ড | কাজ |
|---|---|
git fsck |
ইন্টেগ্রিটি যাচাই, dangling object খোঁজা |
git fsck --lost-found |
dangling commit উদ্ধারযোগ্য করা |
git prune |
unreachable object স্থায়ীভাবে মুছে ফেলা |
ল্যাব: packing, count-objects, fsck দিয়ে মেইনটেন্যান্স ওয়ার্কফ্লো
লক্ষ্য
Loose object তৈরি করে, count-objects দিয়ে দেখে, gc দিয়ে pack করে, তারপর একটা dangling commit ইচ্ছাকৃতভাবে তৈরি করে fsck দিয়ে রিকভার করা।
ধাপ ১ — অনেক loose object তৈরি করা
mkdir ~/gitlab-08 && cd ~/gitlab-08
git init
for i in $(seq 1 30); do
echo "content $i" > "file$i.txt"
git add "file$i.txt"
git commit -m "add file$i.txt"
done
ধাপ ২ — packing-এর আগে অবস্থা দেখা
git count-objects -v
# count: বেশ কিছু (৩০টা commit × তিনটা object টাইপ)
# in-pack: 0 (এখনো কোনো pack হয়নি, ছোট রিপো বলে auto-gc trigger হয়নি)
ধাপ ৩ — ম্যানুয়ালি gc চালানো
git gc
git count-objects -v
# count: 0 (loose object সব pack হয়ে গেছে)
# in-pack: আগের সমান সংখ্যা
ls .git/objects/pack/
# pack-....pack এবং pack-....idx দেখা যাবে
ধাপ ৪ — packfile ইন্টেগ্রিটি যাচাই
git verify-pack -v .git/objects/pack/*.idx | tail -5
ধাপ ৫ — ইচ্ছাকৃতভাবে একটা dangling commit তৈরি করা
echo "important work" > wip.txt
git add wip.txt
git commit -m "WIP: important experiment"
git reset --hard HEAD~1
# 'important experiment' commit-টা এখন unreachable, কিন্তু object database-এ আছে
ধাপ ৬ — fsck দিয়ে খুঁজে বের করা ও রিকভার করা
git fsck --lost-found
# dangling commit <hash> দেখাবে
HASH=$(git fsck --lost-found | grep "dangling commit" | awk '{print $3}')
git show $HASH --stat
git branch recovered-wip $HASH
git log recovered-wip --oneline -1
# "WIP: important experiment" — রিকভার হয়ে গেলো!
চ্যালেঞ্জ
git prune --dry-run চালিয়ে দেখুন এখন আর কোনো (recovered-wip branch করার পর) dangling object prune-এর তালিকায় আসছে কিনা — branch তৈরি করার ফলে সেটা আবার reachable হয়ে গেছে কিনা যাচাই করুন।
ইন্টারভিউ প্রশ্ন — Module 8
প্র.১ — Loose object আর packfile-এর মধ্যে পার্থক্য কী?
উত্তর: Loose object প্রতিটা আলাদা zlib-compressed ফাইল হিসেবে .git/objects/xx/yyyy পাথে থাকে। Packfile অনেক object-কে একটা ফাইলে একত্রিত করে delta compression প্রয়োগ করে, সাথে একটা .idx ফাইল দ্রুত lookup-এর জন্য — অনেক বেশি স্পেস-এফিশিয়েন্ট।
প্র.২ — Delta compression কি Git-এর snapshot মডেল ভঙ্গ করে? উত্তর: না। Delta compression শুধু স্টোরেজ-লেভেল অপ্টিমাইজেশন — conceptually প্রতিটা commit এখনো একটা পূর্ণ, স্বতন্ত্র snapshot। প্রয়োজনে Git স্বচ্ছভাবে delta থেকে পূর্ণ কন্টেন্ট পুনর্গঠন করে দেয়, ইউজার কখনো "delta" সরাসরি দেখেন না।
প্র.৩ — git gc ঠিক কী কী কাজ করে?
উত্তর: (১) loose object-দের একত্রিত করে নতুন packfile তৈরি করে (পারফরম্যান্স), (২) grace period পার হওয়া unreachable object মুছে ফেলে (prune)। এছাড়া reflog expire এবং অন্যান্য housekeeping কাজও করে।
প্র.৪ — --prune=now ব্যবহারে কী ঝুঁকি আছে?
উত্তর: এটা ডিফল্ট grace period (সাধারণত ২ সপ্তাহ) উপেক্ষা করে তাৎক্ষণিকভাবে সব unreachable object মুছে ফেলে — এর ফলে সাম্প্রতিক ভুলবশত "হারানো" (unreachable হওয়া) commit git reflog/git fsck --lost-found দিয়ে রিকভার করার সুযোগও হারিয়ে যেতে পারে।
প্র.৫ — Dangling commit কী, এবং এটা কীভাবে তৈরি হয়?
উত্তর: Dangling commit হলো object database-এ থাকা কিন্তু কোনো ref/অন্য object থেকে reference করা না এমন একটা commit। সাধারণত git reset --hard, --amend, বা rebase-এর পর পুরনো commit reachable না থাকলে এটা ঘটে।
প্র.৬ — git fsck --lost-found দিয়ে কীভাবে একটা ভুলবশত হারানো commit রিকভার করবেন?
উত্তর: git fsck --lost-found চালিয়ে dangling commit-এর hash খুঁজে বের করে, তারপর git branch <নতুন-নাম> <hash> চালিয়ে সেটাকে একটা branch-এর মাধ্যমে আবার reachable বানিয়ে সম্পূর্ণ কাজ উদ্ধার করা যায়।
কর্নার কেস — Module 8
git gc --aggressive"আরও ভালো" মনে হলেও নিয়মিত ব্যবহারের জন্য না — এটা অনেক বেশি সময় নেয় (বড় রিপোতে ঘণ্টার হিসেবে) এবং সাধারণgit gc-এর তুলনায় সবসময় উল্লেখযোগ্যভাবে ছোট packfile না-ও দিতে পারে; বেশিরভাগ ক্ষেত্রে ডিফল্টgit gc-ই যথেষ্ট,--aggressiveকালেভদ্রে (major history cleanup-এর পর) ব্যবহার করা উচিত।git pruneসরাসরি চালানো বিপজ্জনক হতে পারে যদি সম্প্রতি কোনো অপারেশন চলমান থাকে (যেমন একটা rebase বা push মাঝপথে) — চলমান অপারেশনের অস্থায়ী object এখনো কোনো ref-এ যুক্ত না হলে সেগুলো ভুলবশত unreachable ধরে prune হয়ে যেতে পারে; এই কারণেgit gc/pruneসাধারণত অন্য কোনো Git অপারেশন চলাকালীন এড়িয়ে চলা উচিত।git count-objects-এরsizeআরsize-packফিল্ড সরাসরি ডিস্ক ইউসেজ (du) থেকে সামান্য ভিন্ন হতে পারে — filesystem block size, sparse ফাইল, আর অতিরিক্ত মেটাডেটার কারণে; সঠিক ডিস্ক ব্যবহারের জন্যdu -sh .gitব্যবহার করা ভালো,count-objects-কে শুধু object-কাউন্ট ট্রেন্ড দেখার জন্য ব্যবহার করা উচিত।git fsckডিফল্টে reflog-এ থাকা dangling commit রিপোর্ট করে না (--unreachable/--no-reflogছাড়া) — reflog নিজেই সেই object-কে সাময়িকভাবে "reachable" ধরে রাখে, তাই সাম্প্রতিক history-rewrite-এর পরপরইgit fsckচালালে প্রত্যাশিত সব dangling object নাও দেখাতে পারে, যতক্ষণ না reflog expire হয়।
PHASE 3: Branching ও Ref Model গভীরে
Phase 2 পর্যন্ত আমরা মূলত single-branch, linear ইতিহাস নিয়ে কাজ করেছি। কিন্তু বাস্তব প্রোডাকশন ওয়ার্কফ্লো — feature branch, hotfix branch, release branch — এই সবকিছুর ভিত্তি হলো Git-এর branch ও ref model। এই Phase-এ দুটো মডিউল: প্রথমে Module 9-এ branch তৈরি/মুছা/rename/tracking-এর প্রতিটা কমান্ড এবং কেন branch এত সস্তা অপারেশন সেটা ref-level-এ বোঝা হবে। তারপর Module 10-এ detached HEAD আর orphan branch — দুটো এমন state যেগুলো নতুনদের কাছে ভীতিকর মনে হয়, কিন্তু আসলে Git-এর ref model বুঝলে এগুলো সম্পূর্ণ predictable এবং নিরাপদ। এই Phase শেষে branch একটা "ম্যাজিক" মনে হবে না — এটা হবে শুধু একটা ছোট ফাইলে লেখা একটা commit hash।
MODULE 9: Branch CRUD
Branch নিয়ে দৈনন্দিন কাজ — তৈরি করা, দেখা, নাম বদলানো, মুছে ফেলা, আর remote-এর সাথে ট্র্যাক করা — এই মডিউলের মূল বিষয়। কভার হবে: git branch-এর প্রতিটা লিস্টিং ফ্ল্যাগ, ref model-এ branch আসলে কী (একটা ৪১-বাইট ফাইল), git switch -c/git checkout -b, rename/delete, tracking branches, আর কেন ২০১৯ সালে switch/restore আলাদা কমান্ড হিসেবে আনা হলো।
git branch — লিস্টিং ও ফিল্টারিং
branch তালিকা দেখা
git branch কমান্ড কোনো আর্গুমেন্ট ছাড়া চালালে শুধু local branch-গুলোর তালিকা দেখায়, বর্তমান branch-এর সামনে একটা * চিহ্ন দিয়ে। কিন্তু এর ফ্ল্যাগগুলোই আসল শক্তি:
-a— local ও remote-tracking branch দুটোই দেখায় (remotes/origin/mainসহ)।-r— শুধু remote-tracking branch দেখায়।-v/-vv— প্রতিটা branch-এর শেষ commit hash ও subject দেখায়;-vvআরও দেখায় কোন upstream branch ট্র্যাক করছে এবং ahead/behind সংখ্যা।--merged/--no-merged— বর্তমান branch-এ merge হয়ে গেছে (বা হয়নি) এমন branch ফিল্টার করে।--contains <commit>— যে branch-গুলোর ইতিহাসে একটা নির্দিষ্ট commit আছে সেগুলো দেখায়।--points-at <commit>— যে branch-এর tip ঠিক সেই commit-এ আছে (contains-এর চেয়ে সংকীর্ণ)।
রিয়েল-লাইফ ব্যবহার
PR merge হওয়ার পর স্থানীয়ভাবে জমে থাকা মৃত feature branch পরিষ্কার করার সময় সবচেয়ে বেশি ব্যবহৃত প্যাটার্ন:
git branch --merged main | grep -v '^\* main$'
# feature/login-page
# feature/old-experiment
git branch -vv
# main 7a1f9c2 [origin/main] fix: payment retry logic
# feature/login-page 3b8e0a1 [origin/feature/login-page: ahead 2] add OTP flow
-vv-এর আউটপুটে ahead 2 দেখলে বোঝা যায় লোকাল branch-এ ২টা commit আছে যেগুলো এখনো push হয়নি। --contains <hash> দিয়ে সহজেই বের করা যায় একটা নির্দিষ্ট bug-fix commit কোন কোন branch-এ ইতিমধ্যে পৌঁছেছে — release ম্যানেজমেন্টে খুবই দরকারি প্রশ্ন।
ভেতরে branch আসলে কী — একটা ৪১-বাইট ফাইল
branch একটা ডেটা স্ট্রাকচার না, একটা পয়েন্টার
Git-এ একটা branch আসলে .git/refs/heads/<name> লোকেশনে একটা সাধারণ টেক্সট ফাইল, যার ভেতরে থাকে একটামাত্র commit-এর ৪০-hex-character SHA-1 hash (নতুন SHA-256 রিপোতে ৬৪ ক্যারেক্টার) প্লাস একটা newline — মোট ৪১ বাইট।
cat .git/refs/heads/main
# 7a1f9c2e4b8d3f1a0c9e7b6d5a4c3b2a1f0e9d8c
git show-ref refs/heads/main
# 7a1f9c2e4b8d3f1a0c9e7b6d5a4c3b2a1f0e9d8c refs/heads/main
কেন এটা এত গুরুত্বপূর্ণ
Git-এর object model-এ commit, tree, blob — এই সবকিছু কন্টেন্ট-অ্যাড্রেসেবল আর ইমিউটেবল object। কিন্তু কোনো object নিজে থেকে "আমি main branch-এর tip" — এমন কিছু জানে না। branch ref-টাই সেই তথ্য ধরে রাখে — এটা একটা mutable পয়েন্টার যা কোন commit-এর দিকে নির্দেশ করছে সেটা বদলাতে থাকে।
branch তৈরি করা মানে তাই কোনো ফাইল কপি করা, কোনো tree ট্রাভার্স করা, বা কোনো নতুন object বানানো না — শুধু একটা ৪১-বাইট ফাইল লেখা। এইজন্যই:
time git branch experiment
# real 0m0.003s
কার্যত instant। এর সাথে তুলনা করুন Subversion-এর মতো সিস্টেমের সাথে, যেখানে branch মানে পুরো ডিরেক্টরি ট্রি সার্ভারে কপি করা (svn copy trunk branches/experiment) — সেটা রিপোর সাইজের সাথে সমানুপাতিক সময় নেয়। Git-এর সস্তা branch-ই কারণ কেন Git কালচারে প্রতিটা ছোট ফিচারের জন্যও আলাদা branch বানানো স্বাভাবিক অভ্যাস — কোনো storage বা সময়ের খরচ নেই।
HEAD কীভাবে এর সাথে যুক্ত
.git/HEAD ফাইলে সাধারণত থাকে ref: refs/heads/main — অর্থাৎ HEAD নিজেও সরাসরি hash ধরে না, এটা currently checked-out branch ref-কে পয়েন্ট করে। commit করলে আসলে যা ঘটে: নতুন commit object তৈরি হয়, তারপর বর্তমান branch ref (যেটা HEAD পয়েন্ট করছে) আপডেট হয়ে নতুন commit hash-টা লেখা হয়। এই দুই-স্তরের indirection (HEAD → branch ref → commit) পরের মডিউলে detached HEAD বোঝার চাবিকাঠি।
তৈরি, switch, rename, delete — সব CRUD অপারেশন
তৈরি করা
git branch feature/otp-login # branch তৈরি, কিন্তু HEAD এখনো পুরনো branch-এই থাকে
git switch -c feature/otp-login # তৈরি + সাথে সাথে switch (আধুনিক)
git checkout -b feature/otp-login # একই কাজ, পুরনো সিনট্যাক্স
-c (create) ফ্ল্যাগ ছাড়া git switch feature/otp-login চালালে existing branch-এ switch করার চেষ্টা হয়, না থাকলে error দেবে।
Rename ও Delete
git branch -m old-name new-name # rename (বর্তমান branch হলে -m শুধুই যথেষ্ট, নাম না দিলে current branch rename হয়)
git branch -d feature/done # safe delete — main-এ merge না হলে refuse করবে
git branch -D feature/abandoned # force delete — merge না হলেও মুছে ফেলবে, commit হারানোর ঝুঁকি
-d আসলে ভেতরে চেক করে branch-টা reachable কিনা HEAD বা upstream থেকে — merge না হওয়া কাজ থাকলে Git সতর্ক করে "not fully merged" বলে। -D সেই চেক বাইপাস করে — dangling commit তৈরি হতে পারে (Phase 4-এর Reflog মডিউলে রিকভারি দেখানো হবে)।
Tracking branch (upstream) সেট করা
git push -u origin feature/otp-login # push-এর সাথে upstream সেট
git branch --set-upstream-to=origin/main main # existing branch-এ পরে upstream সেট
git branch --unset-upstream # ট্র্যাকিং সম্পর্ক ভেঙে ফেলা
Tracking branch মানে local branch refs/heads/feature/otp-login-এর সাথে remote-tracking ref refs/remotes/origin/feature/otp-login-এর একটা সম্পর্ক রেকর্ড করা .git/config-এ (branch.<name>.remote ও branch.<name>.merge কী হিসেবে)। এই সম্পর্ক থাকলেই git pull/git push কোনো আর্গুমেন্ট ছাড়া কাজ করে, আর -vv আউটপুটে ahead/behead সংখ্যা দেখায়।
switch/restore আলাদা কেন হলো — ২০১৯-এর ঐতিহাসিক প্রেক্ষাপট
পুরনো সমস্যা: checkout ছিলো তিনটা ভিন্ন কাজের জন্য ওভারলোডেড
Git 2.23 (আগস্ট ২০১৯)-এর আগে git checkout একাই তিনটা সম্পূর্ণ ভিন্ন কাজ করতো:
- Branch switch করা:
git checkout main - Working directory-তে ফাইল restore করা:
git checkout -- file.py - Detached HEAD-এ যাওয়া:
git checkout <commit-hash>
এই ওভারলোডিং বিপজ্জনক ছিলো কারণ git checkout <name> — এখানে <name> একটা branch হলে switch হবে, কিন্তু একটা ফাইলের নাম হলে (ভুল টাইপ করলে) সেই ফাইলের uncommitted পরিবর্তন নিঃশব্দে মুছে working-tree ভার্সন দিয়ে replace করে দিতো — কোনো নিশ্চিতকরণ ছাড়াই।
সমাধান: দায়িত্ব ভাগ করা
Git 2.23-এ দুটো নতুন কমান্ড আনা হলো, প্রতিটার একটাই দায়িত্ব:
| কমান্ড | কাজ |
|---|---|
git switch |
শুধু branch পরিবর্তন করা (-c দিয়ে তৈরি+switch, --detach দিয়ে detached HEAD) |
git restore |
শুধু working tree/index-এ ফাইল restore করা (--staged দিয়ে unstage) |
git checkout এখনো আছে backward-compatibility-র জন্য — এটা কখনো deprecated হবে না, কারণ লক্ষ লক্ষ স্ক্রিপ্ট এর উপর নির্ভরশীল। কিন্তু নতুন workflow-এ switch/restore সুপারিশ করা হয় কারণ এগুলোর নাম থেকেই বোঝা যায় কী ঘটবে, আর accidental data-loss-এর সুযোগ কম — যেমন git restore ডিফল্টে working tree বদলায়, staged পরিবর্তনের জন্য এক্সপ্লিসিট --staged লাগবেই।
বাস্তব প্রভাব
আজকের টিউটোরিয়াল/ডকুমেন্টেশনে switch/restore বেশি দেখা যায়, কিন্তু পুরনো CI স্ক্রিপ্ট, StackOverflow উত্তর, আর অনেক টিমের runbook এখনো checkout-ই ব্যবহার করে — দুটোই জানা দরকার, একটা আরেকটাকে রিপ্লেস করেনি, শুধু নতুন, নিরাপদ বিকল্প যোগ হয়েছে।
ল্যাব: Branch CRUD হাতে-কলমে
লক্ষ্য
branch তৈরি, listing flag, rename, delete, আর tracking — সবগুলো নিজে হাতে চালিয়ে দেখা।
mkdir git-branch-lab && cd git-branch-lab && git init
echo "v1" > app.txt && git add . && git commit -m "initial commit"
# ১) তিনটা feature branch তৈরি
git switch -c feature/login
echo "login code" >> app.txt && git commit -am "add login"
git switch main
git switch -c feature/logout
echo "logout code" >> app.txt && git commit -am "add logout"
git switch main
git branch feature/unused # তৈরি হলো কিন্তু কোনো কমিট নেই
# ২) listing flags পরীক্ষা করুন
git branch -v
git branch --merged main
git branch --no-merged main
# ৩) feature/login কে main-এ merge করুন
git merge feature/login
# ৪) এখন আবার --merged চালিয়ে পার্থক্য লক্ষ্য করুন
git branch --merged main
# feature/login এখন এই তালিকায় থাকবে, feature/logout থাকবে না
# ৫) নিরাপদ ও ফোর্স ডিলিট পরীক্ষা
git branch -d feature/login # সফল হবে (merge হয়ে গেছে)
git branch -d feature/logout # refuse করবে "not fully merged"
git branch -D feature/logout # ফোর্স ডিলিট, সফল হবে
# ৬) rename করুন
git branch -m feature/unused feature/renamed
git branch -v
চ্যালেঞ্জ (নিজে করুন)
একটা bare remote সিমুলেট করুন (git init --bare ../origin.git), git remote add origin ../origin.git, git push -u origin main চালিয়ে upstream সেট করুন, তারপর git branch -vv চালিয়ে ahead/behind কলাম দেখুন। একটা নতুন লোকাল কমিট করে আবার -vv চালিয়ে দেখুন "ahead 1" কীভাবে দেখায়।
ইন্টারভিউ প্রশ্ন — Module 9
প্র.১ — একটা branch তৈরি করা কেন এত দ্রুত ও সস্তা অপারেশন?
উত্তর: কারণ branch আসলে কোনো ডেটা কপি করে না — এটা শুধু .git/refs/heads/<name> নামে একটা ছোট ফাইল লেখে যাতে একটা কমিট hash (৪০/৬৪ ক্যারেক্টার) থাকে। কোনো tree/blob object নতুন করে তৈরি হয় না, শুধু একটা পয়েন্টার সেট হয়।
প্র.২ — git branch -d আর -D-এর পার্থক্য কী?
উত্তর: -d (lowercase) একটা safe delete — branch-টা যদি বর্তমান branch/upstream-এ ইতিমধ্যে merge না হয়ে থাকে তাহলে Git refuse করবে। -D (uppercase) সেই চেক বাইপাস করে যেকোনো অবস্থায় ডিলিট করে দেয় — merge না হওয়া কমিটও হারিয়ে যেতে পারে (যদিও reflog দিয়ে কিছুদিন রিকভারি সম্ভব)।
প্র.৩ — --merged আর --no-merged-এর ব্যবহারিক উপকারিতা কী?
উত্তর: PR merge হওয়ার পর জমে থাকা মৃত local branch খুঁজে পরিষ্কার করার জন্য --merged ব্যবহার হয়, আর --no-merged দেখায় কোন কোন branch-এ এখনো unmerged কাজ পড়ে আছে — রিভিউ বা রিমাইন্ডারের জন্য দরকারি।
প্র.৪ — git switch কেন git checkout-কে সম্পূর্ণ রিপ্লেস করলো না?
উত্তর: backward compatibility রক্ষা করার জন্য — লক্ষ লক্ষ স্ক্রিপ্ট, CI পাইপলাইন, ডকুমেন্টেশন checkout ব্যবহার করে। switch/restore ২০১৯-এ (Git 2.23) যোগ হয়েছে নতুন, স্পষ্ট, নিরাপদ বিকল্প হিসেবে, কিন্তু পুরনোটা deprecated হয়নি।
প্র.৫ — tracking branch সেট করা না থাকলে কী সমস্যা হয়?
উত্তর: git pull/git push কোনো আর্গুমেন্ট ছাড়া চালানো যায় না — প্রতিবার explicit remote ও branch নাম দিতে হয় (git push origin feature/x)। tracking সেট থাকলে Git জানে কোন remote branch-এর সাথে তুলনা করে ahead/behind দেখাবে।
প্র.৬ — বর্তমানে checkout করা branch rename করা যায় কি?
উত্তর: হ্যাঁ, git branch -m new-name চালালে (পুরনো নাম না দিয়ে) বর্তমান checked-out branch-ই rename হয় এবং HEAD স্বয়ংক্রিয়ভাবে নতুন নামের ref-কে পয়েন্ট করে।
কর্নার কেস — Module 9
-aদিয়ে দেখা remote-tracking branch (remotes/origin/x) আসলে সত্যিকারের branch না — এগুলো শুধু local ক্যাশ, শেষ কবেfetchকরা হয়েছিলো তার একটা স্ন্যাপশট। remote-এ কেউ branch মুছে দিলেওgit fetch --pruneনা চালানো পর্যন্ত এটা লোকালি দেখা যেতেই থাকবে।--containsআর--points-atগুলিয়ে ফেলা সাধারণ ভুল।--contains <commit>দেখায় যেসব branch-এর ইতিহাসে commit-টা আছে (tip না-ও হতে পারে);--points-at <commit>দেখায় শুধু যেসব branch-এর tip ঠিক সেই commit-এ আছে — অনেক বেশি সংকীর্ণ ফিল্টার।git branch -dমাঝেমধ্যে merge হওয়া সত্ত্বেও refuse করে যদি merge অন্য branch-এ হয় (যেমনdevelop-এ merge হয়েছে কিন্তু আপনিmain-এ থেকে delete করছেন) — কারণ-dmerge-status চেক করে current HEAD-এর সাপেক্ষে, তাই সঠিক branch-এ checkout করে delete করা দরকার অথবা-Dলাগবে যদি নিশ্চিত হন।- rename করার পর remote branch স্বয়ংক্রিয়ভাবে rename হয় না।
git branch -m old newশুধু লোকাল ref বদলায়; remote-এ পুরনো নামেই branch থেকে যায় যতক্ষণ না ম্যানুয়ালিgit push origin :old && git push -u origin newচালানো হয়।
MODULE 10: Detached HEAD & Orphan
Detached HEAD-কে নতুনরা প্রায়ই ভয় পান — টার্মিনালে "you are in 'detached HEAD' state" মেসেজ দেখে মনে হয় কিছু ভেঙে গেছে। এই মডিউলে দেখানো হবে এটা আসলে একটা সম্পূর্ণ স্বাভাবিক, predictable state — Git-এর HEAD/ref মডেল বুঝলে এতে ভয় পাওয়ার কিছু নেই। সাথে থাকবে git switch --orphan — সম্পূর্ণ নতুন, ইতিহাসবিহীন branch তৈরির কমান্ড, যেটা বাস্তবে gh-pages-এর মতো ব্যবহার হয়।
HEAD সাধারণ অবস্থায় কী, detached অবস্থায় কী
HEAD স্বাভাবিক অবস্থায়: একটা symbolic ref
স্বাভাবিক অবস্থায় .git/HEAD ফাইলের ভেতরে থাকে:
ref: refs/heads/main
এটা একটা indirect পয়েন্টার — HEAD সরাসরি কোনো commit hash ধরে না, বরং একটা branch ref-কে পয়েন্ট করে, আর সেই branch ref তখন commit hash ধরে। এই দুই-স্তরের indirection-এর সুবিধা: commit করলে শুধু branch ref-টা আপডেট হয়, HEAD-এর বদলানোর দরকার হয় না — HEAD স্বয়ংক্রিয়ভাবে branch-এর সাথে "চলতে থাকে"।
Detached HEAD: সরাসরি একটা commit hash
যখন HEAD কোনো branch ref-এর বদলে সরাসরি একটা commit hash ধরে, তখন সেটাকে বলে detached HEAD:
7a1f9c2e4b8d3f1a0c9e7b6d5a4c3b2a1f0e9d8c
এই অবস্থায় নতুন commit করলে সেই commit কোনো branch-এর সাথে যুক্ত হয় না — এটা "floating" থাকে, শুধু HEAD থেকে reachable, অন্য কোনো ref থেকে না। যদি এই অবস্থায় অন্য branch-এ switch করে ফেলেন, সেই commit-গুলো কোনো branch থেকে পৌঁছানো যাবে না (unreachable) — কিন্তু object হিসেবে তখনো .git/objects-এ থেকে যায়, reflog-এ ট্র্যাক থাকে।
এক লাইনে পার্থক্য
স্বাভাবিক HEAD = "আমি main branch-এ আছি, main যেখানেই যাক আমিও সেখানে যাব।" Detached HEAD = "আমি ঠিক এই একটা commit-এ আছি, কোনো branch-এর সাথে যুক্ত না।"
detached HEAD কীভাবে ঘটে
তিনটা সাধারণ উপায়
git checkout 7a1f9c2 # নির্দিষ্ট commit hash-এ checkout
git checkout v1.2.0 # একটা tag-এ checkout — tag নিজেও একটা branch না, তাই detach হবে
git switch --detach main # স্পষ্টভাবে detach করে main-এর বর্তমান tip-এ
ভেতরে কী ঘটে
Git দেখে আপনি যে আর্গুমেন্ট দিয়েছেন সেটা কোনো branch নাম (refs/heads/<name>)-এর সাথে ম্যাচ করে কিনা। ম্যাচ না করলে (একটা raw hash, একটা tag, বা HEAD~3-এর মতো একটা relative reference), Git ধরে নেয় আপনি একটা নির্দিষ্ট commit-এ যেতে চাইছেন — তখন .git/HEAD-এ ref: refs/heads/... এর বদলে সরাসরি সেই commit-এর hash লিখে দেয়।
git checkout v1.2.0
# Note: switching to 'v1.2.0'.
#
# You are in 'detached HEAD' state. You can look around, make experimental
# changes and commit them, and you can discard any commits you make in this
# state without impacting any branches by switching back to a branch.
Git নিজেই এই মেসেজে বুঝিয়ে দেয়: এটা read-only exploration-এর জন্য নিরাপদ একটা মোড — একটা পুরনো release ট্যাগ চেকআউট করে দেখা, বা একটা নির্দিষ্ট commit-এ কোড কেমন ছিলো সেটা পরীক্ষা করার জন্য আদর্শ, কোনো branch তৈরি না করেই।
detached HEAD কেন 'বিপজ্জনক' মনে হয় কিন্তু আসলে না
ভয়টা কোথা থেকে আসে
নতুনরা ভয় পান কারণ মনে হয় detached অবস্থায় কমিট করলে সেটা "হারিয়ে যাবে"। আংশিক সত্য: যদি detached অবস্থায় কমিট করে তারপর সরাসরি অন্য branch-এ switch করে ফেলেন কোনো branch না বানিয়ে, সেই commit কোনো branch থেকে reachable থাকবে না।
কিন্তু আসলে ডেটা হারায় না — অন্তত সাথে সাথে না
Commit object তৈরি হওয়ার সাথে সাথে সেটা .git/objects-এ পার্মানেন্টলি সংরক্ষিত থাকে, আর git reflog HEAD-এর প্রতিটা movement ট্র্যাক রাখে — এমনকি detached অবস্থাতেও। ফলে switch করে ফেলার পরও কয়েক সপ্তাহ (ডিফল্ট gc grace period, সাধারণত ৩০-৯০ দিন) পর্যন্ত git reflog দিয়ে সেই commit hash খুঁজে বের করে নতুন branch বানিয়ে রিকভার করা সম্ভব (পুরো ওয়াকথ্রু Phase 4-এর Reflog মডিউলে)।
আসল ঝুঁকি: reflog-ও এক্সপায়ার হলে
যদি অনেক দিন পার হয়ে যায় (default gc.reflogExpireUnreachable=30 days) এবং git gc চলে, তখন সেই commit সত্যিই মুছে যেতে পারে — কারণ কোনো ref (branch/tag/reflog) থেকে reachable না এমন object-কে Git garbage collect করে। তাই ভয়টা সম্পূর্ণ ভিত্তিহীন না, কিন্তু "সাথে সাথে হারিয়ে যাবে" — এই ধারণাটা ভুল। বাস্তব অভ্যাস: detached অবস্থায় গুরুত্বপূর্ণ কাজ করলে সাথে সাথে একটা branch বানিয়ে ফেলুন।
detached অবস্থার commit বাঁচানো
সমাধান: একটা branch বানিয়ে ফেলুন
git switch --detach v1.2.0
echo "hotfix attempt" >> app.py
git commit -am "experimental fix while exploring v1.2.0"
# এখন এই commit বাঁচাতে —
git switch -c hotfix/v1.2.0-patch
# অথবা switch না করেই:
git branch hotfix/v1.2.0-patch
git branch <name> (switch ছাড়া) চালালে HEAD যেখানে আছে ঠিক সেই commit-এ একটা নতুন branch ref তৈরি হয়ে যায় — কিন্তু HEAD তখনো detached-ই থাকে যদি না switch -c/checkout -b ব্যবহার করেন। তাই safe pattern হলো switch -c — এক কমান্ডেই branch তৈরি ও তাতে attach হওয়া, যাতে ভবিষ্যতের commit স্বয়ংক্রিয়ভাবে সেই branch-এর সাথে যুক্ত হয়।
ইতিমধ্যে switch করে ফেললে
যদি কমিট করার পর ভুলে অন্য branch-এ switch করে ফেলেন, git reflog চালিয়ে "commit:" লাইনের hash খুঁজে বের করুন, তারপর git branch recovered-work <hash> দিয়ে সেই commit-কে নতুন branch-এ প্রাণ দিন। এই ফ্লো Phase 4-এর Module 16-এ বিস্তারিত ওয়াকথ্রু হবে।
git switch --orphan — সম্পূর্ণ ইতিহাসবিহীন নতুন branch
Orphan branch কী
git switch --orphan <name> এমন একটা branch তৈরি করে যার কোনো parent commit নেই — সম্পূর্ণ খালি ইতিহাস দিয়ে শুরু। বর্তমান branch-এর working tree ফাইলগুলো staged অবস্থায় থেকে যায় (যদি না --discard-changes ব্যবহার করেন), কিন্তু commit history সম্পূর্ণ আলাদা, disconnected।
git switch --orphan gh-pages
git rm -rf . # আগের সব ফাইল unstage+delete করে ফাঁকা শুরু
echo "<h1>My Site</h1>" > index.html
git add index.html
git commit -m "initial gh-pages commit"
ভেতরে কী ঘটছে
স্বাভাবিক নতুন commit তৈরি হয় তার আগের HEAD commit-কে parent হিসেবে রেখে (parent ফিল্ড commit object-এ পূরণ থাকে)। Orphan branch-এর প্রথম commit-এর parent ফিল্ড খালি — ঠিক যেমন রিপোর প্রথম commit-এর কোনো parent থাকে না। ফলে git log gh-pages আর git log main সম্পূর্ণ আলাদা, সংযোগহীন ইতিহাস দেখাবে — যদিও দুটোই একই .git রিপোজিটরির ভেতরে থাকে।
বাস্তব ব্যবহার: gh-pages branch
GitHub Pages-এ static site হোস্ট করার সময় প্রচলিত প্যাটার্ন: main branch-এ সোর্স কোড থাকে, আর gh-pages branch-এ শুধু বিল্ড-করা static HTML/CSS/JS থাকে — সম্পূর্ণ আলাদা ইতিহাস, কারণ এই দুই branch-এর কন্টেন্ট যুক্তিগতভাবে সম্পর্কহীন (একটা সোর্স, একটা আর্টিফ্যাক্ট)। orphan branch দিয়ে gh-pages শুরু করলে main-এর হাজার হাজার commit-এর ইতিহাস gh-pages-এ বহন করতে হয় না, রিপো ছোট থাকে।
ল্যাব: Detached HEAD ও Orphan Branch হাতে-কলমে
লক্ষ্য
detached HEAD-এ কাজ করে সেটা রিকভার করা, তারপর একটা orphan branch তৈরি করে দেখা।
mkdir git-detached-lab && cd git-detached-lab && git init
for i in 1 2 3; do echo "line $i" >> file.txt && git commit -am "commit $i"; done
git tag v1.0 HEAD~1 # দ্বিতীয় কমিটে একটা ট্যাগ
# ১) detached HEAD-এ যান
git switch --detach v1.0
git status # "HEAD detached at v1.0" দেখুন
# ২) একটা experimental commit করুন
echo "experimental line" >> file.txt
git commit -am "experimental change from v1.0"
git log --oneline -3
# ৩) main-এ ফিরে যান (এখনো branch বানাননি!)
git switch main
git log --oneline --all # লক্ষ্য করুন experimental commit আর কোনো branch থেকে দেখা যাচ্ছে না
# ৪) reflog দিয়ে সেই commit উদ্ধার করুন
git reflog | head -5
# HEAD@{1} থেকে hash কপি করুন, তারপর:
git branch recovered-experiment <সেই-hash>
git log --oneline recovered-experiment
# ৫) এখন orphan branch তৈরি করুন
git switch --orphan gh-pages
git rm -rf .
echo "<h1>Static Site</h1>" > index.html
git add index.html
git commit -m "initial gh-pages"
git log --oneline gh-pages # মাত্র ১টা commit, main-এর সাথে কোনো সংযোগ নেই
git log --oneline main # এখনো ৩টা commit
চ্যালেঞ্জ (নিজে করুন)
git log --graph --all --oneline চালিয়ে দেখুন gh-pages branch গ্রাফে সম্পূর্ণ বিচ্ছিন্ন root হিসেবে কীভাবে দেখানো হয়, main/recovered-experiment-এর সাথে কোনো connecting line ছাড়াই।
ইন্টারভিউ প্রশ্ন — Module 10
প্র.১ — detached HEAD আর normal HEAD-এর মধ্যে .git/HEAD ফাইলের কন্টেন্টে কী পার্থক্য?
উত্তর: Normal অবস্থায় .git/HEAD-এ থাকে ref: refs/heads/<branch-name> — একটা symbolic (indirect) পয়েন্টার। Detached অবস্থায় সরাসরি একটা commit hash থাকে, কোনো branch ref-এর রেফারেন্স ছাড়াই।
প্র.২ — detached HEAD অবস্থায় commit করে switch করে ফেললে সাথে সাথে ডেটা হারায় কি?
উত্তর: না, সাথে সাথে না। Commit object .git/objects-এ থেকে যায় এবং git reflog-এ ট্র্যাক থাকে। শুধু কোনো branch থেকে reachable না থাকায় সাধারণ git log-এ দেখা যায় না। gc.reflogExpireUnreachable (ডিফল্ট ~৩০ দিন) পার হয়ে git gc চললে তবেই সত্যিকারের ঝুঁকি তৈরি হয়।
প্র.৩ — detached HEAD অবস্থায় করা একটা commit কীভাবে স্থায়ীভাবে রক্ষা করবেন?
উত্তর: git switch -c <new-branch-name> চালিয়ে সেই মুহূর্তে একটা নতুন branch তৈরি করে সেখানে attach হয়ে যান — তাহলে ভবিষ্যতের commit-ও সেই branch-এর সাথেই যুক্ত থাকবে।
প্র.৪ — git switch --orphan আর সাধারণ git switch -c-এর পার্থক্য কী?
উত্তর: switch -c নতুন branch তৈরি করে বর্তমান HEAD commit-কে parent হিসেবে রেখে — ইতিহাস সংযুক্ত থাকে। switch --orphan সম্পূর্ণ প্যারেন্টহীন branch তৈরি করে — নতুন root commit, আগের ইতিহাসের সাথে কোনো সংযোগ থাকে না।
প্র.৫ — gh-pages branch-এর জন্য orphan branch কেন ব্যবহার করা হয়? উত্তর: কারণ gh-pages-এ থাকে বিল্ড-করা static output, যেটার সাথে source code branch (main)-এর ইতিহাসগত কোনো সম্পর্ক নেই। orphan branch দিয়ে শুরু করলে main-এর হাজার হাজার commit বহন করতে হয় না — clone দ্রুত হয়, ইতিহাস পরিষ্কার থাকে।
প্র.৬ — একটা tag checkout করলে কেন সবসময় detached HEAD হয়, কখনো নরমাল branch checkout হয় না?
উত্তর: কারণ tag নিজে refs/tags/<name>-এ থাকে, refs/heads/-এ না। Git-এর branch/HEAD মডেলে শুধু refs/heads/-এর ref-গুলোই HEAD-কে "attached" রাখতে পারে; tag একটা fixed পয়েন্টার (annotated হলে নিজেই আলাদা object), তাই checkout করলে সরাসরি সেই commit-এ detach করে দেয়।
কর্নার কেস — Module 10
git branch <name>(switch ছাড়া) চালালে HEAD detached-ই থেকে যায় — নতুন branch ref তৈরি হয় ঠিকই, কিন্তু HEAD সেই নতুন branch-এ attach হয় না, তাই পরের commit আগের মতোই detached অবস্থায় হবে যদি না আলাদা করেgit switch <name>চালান।git checkout maindetached HEAD থেকে বের করে আনার সময় সতর্কতা ছাড়াই আগের uncommitted কাজ হারাতে পারে যদি conflict থাকে — commit না করে থাকলে working tree changes carry-over করার চেষ্টা Git করে, কিন্তু conflict হলে switch block হয়ে যায়; committed change না থাকলে সবচেয়ে নিরাপদ practice হলো স্টাশ করে switch করা।- orphan branch-এর প্রথম commit-এর আগের working tree ফাইলগুলো ডিফল্টে staged অবস্থায় থেকে যায় —
git rm -rf .না চালালে পুরনো ফাইলগুলোই নতুন orphan branch-এর প্রথম commit-এ চলে আসতে পারে, যেটা gh-pages-এর মতো ব্যবহারে অনিচ্ছাকৃতভাবে পুরো সোর্স কোড লিক করে দিতে পারে। git log --alldetached HEAD-এর commit দেখাবে না যদি কোনো branch/tag/HEAD reflog থেকে reachable না হয় —--allশুধু ref-ভিত্তিক reachability দেখায়, dangling commit দেখতেgit fsck --lost-foundবাgit reflogলাগবে।
PHASE 4: Merging, Rebasing, History Rewriting
এই Phase-টাই সম্ভবত গোটা journey-র সবচেয়ে ঘনবসতিপূর্ণ ও বাস্তব-জীবনে সবচেয়ে বেশি ব্যবহৃত অংশ। ছয়টা মডিউল — merge fundamentals, conflict resolution, reset/undo model, interactive rebase, cherry-pick, আর সবশেষে reflog সেফটি নেট — একসাথে মিলে তৈরি করে "undo mastery": আপনি কখনো ভুল করবেনই (ভুল branch-এ commit, ভুল merge, ভুল rebase), আসল দক্ষতা হলো কীভাবে সেই ভুল থেকে নিরাপদে ফিরে আসা যায়। এই Phase শেষ হলে git reset --hard বা git rebase দেখে আর ভয় পাওয়ার কিছু থাকবে না — বরং এগুলো হয়ে উঠবে আপনার সবচেয়ে শক্তিশালী টুল।
MODULE 11: Merge Fundamentals
দুটো branch-এর ইতিহাস একত্র করার সবচেয়ে মৌলিক অপারেশন হলো merge। কিন্তু "merge" শব্দটার আড়ালে আসলে দুটো ভিন্ন মেকানিজম লুকিয়ে আছে — fast-forward আর 3-way merge — আর কখন কোনটা ঘটবে সেটা predictable, ম্যাজিক না। এই মডিউলে merge-base অ্যালগরিদম, --no-ff/--ff-only ফ্ল্যাগ, আর টিম ওয়ার্কফ্লোতে কোনটা কখন ব্যবহার করবেন সেটা কভার হবে।
Fast-forward বনাম 3-way Merge
Fast-forward merge
যখন target branch (যেমন main)-এর tip commit, source branch (যেমন feature/x)-এর ইতিহাসের একটা অগ্রজ (ancestor), তখন merge করার জন্য নতুন কোনো commit তৈরি করার দরকার নেই — Git শুধু main-এর ref-কে সরাসরি এগিয়ে নিয়ে feature/x-এর tip-এ বসিয়ে দেয়:
git switch main
git merge feature/x
# Updating 7a1f9c2..3b8e0a1
# Fast-forward
# app.py | 3 +++
এখানে main ref কেবল একটা পয়েন্টার আপডেট — কোনো নতুন merge commit object তৈরি হয়নি।
3-way merge
যখন দুই branch একে অপরের থেকে ডাইভার্জ করে গেছে (দুইদিকেই নতুন commit হয়েছে কোনো common ancestor-এর পর থেকে), fast-forward সম্ভব না। তখন Git একটা 3-way merge করে — তিনটা commit ব্যবহার করে: দুই branch-এর tip, আর তাদের common ancestor (merge base)। ফলাফল হিসেবে একটা নতুন merge commit তৈরি হয় যার দুটো parent থাকে।
git merge feature/y
# Merge made by the 'ort' strategy.
# utils.py | 5 +++++
ভেতরে common ancestor খোঁজা
Git প্রথমে git merge-base main feature/y চালানোর সমতুল্য একটা কাজ করে — দুই branch-এর commit graph ব্যাক-ট্রাভার্স করে সবচেয়ে recent common commit বের করে। তারপর তিনটা tree (ancestor, main-এর tree, feature/y-এর tree) তুলনা করে diff বের করে প্রতিটা ফাইলে — যেখানে দুইদিকেই একই লাইনে পরিবর্তন হয়েছে সেখানেই conflict দেখা দেয় (পরের মডিউলে বিস্তারিত)।
| শর্ত | ফলাফল |
|---|---|
| target ancestor of source | Fast-forward (নতুন commit নেই) |
| branches diverged, no conflicting lines | Automatic 3-way merge commit |
| branches diverged, conflicting lines | Merge conflict — ম্যানুয়াল রেজোলিউশন দরকার |
git merge-base — common ancestor বের করা
কী করে
git merge-base <commit1> <commit2> দুটো commit/branch-এর সবচেয়ে recent common ancestor বের করে দেয় — merge অপারেশনের ভেতরে এই কমান্ডটাই সবার আগে চলে (implicit ভাবে), কিন্তু নিজে থেকেও ম্যানুয়ালি চালানো যায়।
git merge-base main feature/y
# 7a1f9c2e4b8d3f1a0c9e7b6d5a4c3b2a1f0e9d8c
git log --oneline 7a1f9c2..feature/y # ancestor থেকে feature/y পর্যন্ত কী নতুন
git log --oneline 7a1f9c2..main # ancestor থেকে main পর্যন্ত কী নতুন
কীভাবে অ্যালগরিদমিক্যালি কাজ করে
Git-এর commit graph একটা DAG (Directed Acyclic Graph) — প্রতিটা commit তার parent(s)-কে পয়েন্ট করে। merge-base অ্যালগরিদম দুই commit থেকে ব্যাকওয়ার্ডে (parent চেইন ধরে) হাঁটে এবং প্রথম যে commit-টা উভয় দিক থেকেই reachable, সেটাই common ancestor। একাধিক merge commit থাকলে একাধিক "best" common ancestor থাকতে পারে — এই জটিল ক্ষেত্রে Git --all ফ্ল্যাগ দিয়ে সবগুলো দেখাতে পারে, অথবা recursive/ort strategy নিজে থেকে একটা virtual merge base তৈরি করে handle করে।
কোথায় ব্যবহারিকভাবে কাজে লাগে
# একটা feature branch-এ ঠিক কী কী নতুন commit আছে main-এর তুলনায়
git log --oneline $(git merge-base main feature/x)..feature/x
# rebase করার আগে দেখে নেওয়া কতগুলো commit replay হবে
git rev-list --count $(git merge-base main feature/x)..feature/x
PR রিভিউ করার সময় "এই branch-এ আসলে কী নতুন আছে main-এর তুলনায়" বোঝার জন্য এটা প্রতিদিনের কাজ — GitHub/GitLab-এর PR diff view ভেতরে ঠিক এই merge-base logic-ই ব্যবহার করে diff দেখায়, main-এর সর্বশেষ commit না দিয়ে।
--no-ff — কেন সবসময় merge commit রাখা ভালো হতে পারে
সমস্যা: fast-forward ইতিহাস থেকে branch-এর অস্তিত্ব মুছে দেয়
Fast-forward merge কোনো নতুন commit তৈরি করে না — ফলে পরে git log-এ দেখলে মনে হবে সব commit যেন সরাসরি main-এই করা হয়েছিলো, কোনো আলাদা feature branch কখনো ছিলোই না। এটা group work-এর ইতিহাস হারিয়ে ফেলে — কোন commit-গুলো একটা logical unit (একটা feature/PR) হিসেবে একসাথে এসেছিলো, সেই তথ্য চলে যায়।
সমাধান: --no-ff
git merge --no-ff feature/otp-login
এই ফ্ল্যাগ fast-forward সম্ভব হলেও জোর করে একটা merge commit তৈরি করে — ফলে git log --graph দেখতে গেলে স্পষ্ট দেখা যায় কোথায় একটা feature branch শুরু হয়ে কোথায় merge হয়েছে:
git log --graph --oneline
* 3f8a1c2 Merge branch 'feature/otp-login'
|\
| * 9d2e7b1 add OTP verification endpoint
| * 5c1a0f3 add OTP generation logic
|/
* 7a1f9c2 initial commit
টিম ওয়ার্কফ্লোতে ব্যবহারিক সুবিধা
- Revert সহজ হয় — পুরো একটা feature-কে এক merge commit হিসেবে
git revert -m 1 <merge-hash>দিয়ে সম্পূর্ণ undo করা যায় (Module 13-এ বিস্তারিত)। - PR-এর ইতিহাস সংরক্ষিত থাকে — কোন commit-গুলো একটা PR-এর অংশ ছিলো তা
git log --graph-এই দেখা যায়, external টুল ছাড়াই। - Bisect-এ সাহায্য করে — একটা বাগ কোন feature merge-এর সাথে এসেছে সেটা merge commit বাউন্ডারি দিয়ে দ্রুত আইসোলেট করা যায়।
অনেক টিম policy হিসেবে git config --global merge.ff false সেট করে রাখে, যাতে ভুলবশত fast-forward কখনো না ঘটে।
--ff-only — কেন CI-তে এটা নিরাপদ
সমস্যা: automation-এ auto-merge commit ঝুঁকিপূর্ণ
CI/CD পাইপলাইনে যখন স্বয়ংক্রিয়ভাবে main-এ merge করা হয় (যেমন একটা bot দিয়ে), যদি ইতিমধ্যে অন্য কেউ main-এ নতুন commit push করে ফেলে থাকে, একটা automatic 3-way merge conflict তৈরি করতে পারে — আর automation-এর ভেতরে conflict resolve করার কোনো মানুষ নেই। এটা pipeline-কে অনির্ধারিত অবস্থায় ফেলে দিতে পারে।
সমাধান: --ff-only
git merge --ff-only feature/x
# fatal: Not possible to fast-forward, aborting.
এই ফ্ল্যাগ Git-কে বলে: শুধুমাত্র fast-forward সম্ভব হলেই merge করো, নাহলে সম্পূর্ণ ব্যর্থ হয়ে থামো — কোনো automatic 3-way merge commit তৈরি করার চেষ্টা করবে না। এটা fail-fast আচরণ — pipeline সাথে সাথে বুঝে যায় merge সম্ভব না, আর মানুষকে জানিয়ে দেয় ম্যানুয়াল হস্তক্ষেপ দরকার।
বাস্তব ব্যবহার
# একটা release automation script-এ
git fetch origin
git switch main
git merge --ff-only origin/main # শুধু নিশ্চিত করে local main আপডেটেড, conflict নেই
# একটা "merge train" পদ্ধতিতে rebase+ff-only পদ্ধতি
git switch feature/x
git rebase origin/main # প্রথমে rebase করে main-এর উপরে replay
git switch main
git merge --ff-only feature/x # এখন guaranteed fast-forward, নিরাপদ
এই প্যাটার্ন — rebase করে তারপর --ff-only merge — অনেক টিমের "linear history" policy-র ভিত্তি, যেখানে main-এ কখনো merge commit থাকে না, সবকিছু সরল রেখায় থাকে।
ল্যাব: Fast-forward বনাম 3-way Merge হাতে-কলমে
লক্ষ্য
দুই ধরনের merge নিজের চোখে ঘটতে দেখা, আর merge-base কমান্ড ব্যবহার করা।
mkdir git-merge-lab && cd git-merge-lab && git init
echo "base" > file.txt && git add . && git commit -m "initial commit"
# ১) Fast-forward দৃশ্য
git switch -c feature/ff
echo "ff change" >> file.txt && git commit -am "ff change"
git switch main
git merge feature/ff
# লক্ষ্য করুন: "Fast-forward" মেসেজ, কোনো নতুন merge commit নেই
git log --oneline --graph
# ২) 3-way merge দৃশ্য (branch diverge করানো)
git switch -c feature/a
echo "change from a" >> file2.txt && git add . && git commit -m "feature a work"
git switch main
echo "unrelated main work" >> file3.txt && git add . && git commit -m "main work"
git merge feature/a
# লক্ষ্য করুন: একটা editor খুলবে merge commit message-এর জন্য, "Merge made by the 'ort' strategy"
git log --oneline --graph
# ৩) merge-base পরীক্ষা
git switch -c feature/b main~1
echo "b work" >> file4.txt && git add . && git commit -m "feature b work"
git merge-base main feature/b
git log --oneline $(git merge-base main feature/b)..feature/b
# ৪) --no-ff বনাম ডিফল্ট তুলনা
git switch -c feature/c
echo "c work" >> file5.txt && git commit -am "feature c work"
git switch main
git merge --no-ff feature/c
git log --graph --oneline # স্পষ্ট merge commit দেখুন, fast-forward সম্ভব থাকলেও
চ্যালেঞ্জ (নিজে করুন)
git config merge.ff false সেট করে একটা fast-forward-যোগ্য branch merge করুন — দেখুন config সেটিং ছাড়া --no-ff ফ্ল্যাগ প্রতিবার লেখা ছাড়াই একই আচরণ পাচ্ছেন কিনা।
ইন্টারভিউ প্রশ্ন — Module 11
প্র.১ — fast-forward merge কখন সম্ভব, আর কখন সম্ভব না? উত্তর: যখন target branch-এর tip commit, source branch-এর ইতিহাসের সরাসরি একটা ancestor (অর্থাৎ target-এ merge হওয়ার পর থেকে target-এ কোনো নতুন commit হয়নি), তখন fast-forward সম্ভব। দুই branch diverge করে গেলে (দুইদিকেই আলাদা commit হয়ে গেলে) fast-forward সম্ভব না, 3-way merge লাগবে।
প্র.২ — 3-way merge-এ "তিনটা" জিনিস কী কী? উত্তর: দুই branch-এর বর্তমান tip commit, আর তাদের common ancestor (merge base)। এই তিনটা tree তুলনা করে Git বুঝে কোথায় স্বয়ংক্রিয়ভাবে মার্জ করা যায় আর কোথায় conflict।
প্র.৩ — git merge-base ব্যবহারিকভাবে কোথায় কাজে লাগে?
উত্তর: একটা feature branch-এ ঠিক কী নতুন কমিট আছে main-এর তুলনায় সেটা বের করতে (git log $(git merge-base main feature)..feature), rebase-এর আগে কতগুলো commit replay হবে বোঝার জন্য, আর PR টুলে diff দেখানোর ভিত্তি হিসেবে।
প্র.৪ — --no-ff কেন অনেক টিম policy হিসেবে ব্যবহার করে?
উত্তর: এটা fast-forward সম্ভব থাকলেও জোর করে একটা merge commit তৈরি করে, যাতে ইতিহাসে স্পষ্ট থাকে কোন commit-গুলো একটা feature/PR হিসেবে একসাথে এসেছে — এটা পরবর্তীতে সেই পুরো ফিচারকে এক merge commit হিসেবে revert করা, বা bisect-এ দ্রুত আইসোলেট করা সহজ করে।
প্র.৫ — --ff-only কেন CI/CD automation-এ নিরাপদ?
উত্তর: এটা কোনো automatic 3-way merge/conflict তৈরির চেষ্টা করে না — fast-forward সম্ভব না হলে সাথে সাথে fail করে থামে। এটা fail-fast আচরণ, যাতে automation নিজে থেকে অনির্ধারিত/জটিল conflict resolve করার চেষ্টা না করে, বরং মানুষকে জানায়।
প্র.৬ — "linear history" policy বজায় রাখতে rebase আর merge কীভাবে একসাথে ব্যবহার করা হয়?
উত্তর: প্রথমে feature branch-কে git rebase origin/main দিয়ে main-এর latest tip-এর উপর replay করা হয়, তারপর git merge --ff-only feature/x দিয়ে merge করা হয় — rebase-এর পর guaranteed fast-forward সম্ভব হয়, ফলে কোনো merge commit ছাড়াই সম্পূর্ণ linear ইতিহাস বজায় থাকে।
কর্নার কেস — Module 11
git merge feature/xযখন আপনার local branch আসলে ancestor, তখনও মাঝেমধ্যে merge commit তৈরি হয় যদিmerge.ffconfigfalseসেট করা থাকে (গ্লোবাল বা লোকাল)। ডিফল্ট আচরণ ধরে নেওয়ার আগেgit config --get merge.ffচেক করুন।- একাধিক common ancestor থাকতে পারে জটিল branch topology-তে (criss-cross merge) — যখন দুই branch একে অপরকে একাধিকবার merge করে ফেলেছে। এক্ষেত্রে
git merge-base --allএকাধিক hash দেখাতে পারে, আর merge strategy (ort/recursive) নিজে থেকে একটা virtual merge base তৈরি করে হ্যান্ডল করে — ফলাফল কখনো কখনো অপ্রত্যাশিত মনে হতে পারে। --ff-onlyfail করলে সেটা কোনো error না, এটা ইচ্ছাকৃত সেফটি গার্ড — script-এ exit code চেক করে fallback logic (যেমন rebase চেষ্টা করা বা মানুষকে জানানো) রাখা উচিত, শুধু "crash" হিসেবে treat না করে।- fast-forward merge-এর পর
git log --graphদেখতে "merge হয়েছে" বোঝার কোনো উপায় নেই — যেহেতু কোনো merge commit তৈরিই হয়নি, ইতিহাস দেখতে সম্পূর্ণ linear মনে হবে, যেন branch-টা কখনো ছিলোই না।
MODULE 12: Conflict Resolution Deep-Dive
Merge conflict দেখলে অনেকের হাত-পা ঠান্ডা হয়ে যায় — কিন্তু conflict marker আসলে খুবই সরল একটা তথ্য প্রকাশ করে: "দুইদিকে একই জায়গায় ভিন্ন পরিবর্তন হয়েছে, তুমি ঠিক করো কোনটা রাখবে।" এই মডিউলে conflict marker পড়া, ours/theirs-এর বিভ্রান্তিকর দ্বৈত অর্থ (merge বনাম rebase), strategy option, rerere দিয়ে repetitive conflict এড়ানো, আর একটা হাতে-কলমে conflict তৈরি করে সমাধানের ল্যাব — সবকিছু কভার হবে।
Conflict Marker পড়া: <<<<<<<, =======, >>>>>>>
marker-গুলোর অর্থ
যখন একটা merge/rebase-এ conflict হয়, Git ফাইলের ভেতরেই দুই দিকের কন্টেন্ট মার্ক করে দেয়:
<<<<<<< HEAD
const MAX_RETRIES = 5;
=======
const MAX_RETRIES = 3;
>>>>>>> feature/tune-retries
<<<<<<< HEADথেকে=======পর্যন্ত — বর্তমান branch (যেটাতে আপনি আছেন, merge-এ HEAD) এর ভার্সন।=======থেকে>>>>>>> feature/tune-retriesপর্যন্ত — যে branch merge/rebase করা হচ্ছে তার ভার্সন।>>>>>>>লাইনের পরের নাম বলে দেয় কোন branch/commit থেকে এই অংশ এসেছে।
সমাধান করার প্রক্রিয়া
Git স্বয়ংক্রিয়ভাবে conflicted ফাইলগুলোকে working directory-তে marker-সহ রেখে দেয়, আর git status দিয়ে কোন ফাইলে conflict আছে দেখা যায়:
git status
# Unmerged paths:
# both modified: config.js
সমাধান মানে ম্যানুয়ালি ফাইল এডিট করে marker-গুলো মুছে ফেলে সঠিক ফাইনাল কন্টেন্ট রাখা (একটার, অন্যটার, বা দুটোর মিশ্রণ), তারপর git add <file> দিয়ে resolved হিসেবে mark করা, আর git commit (merge-এ) বা git rebase --continue (rebase-এ) দিয়ে এগিয়ে যাওয়া।
রিয়েল-লাইফ উদাহরণ
দুটো টিম মেম্বার একই কনফিগ ফাইলে MAX_RETRIES ভ্যালু আলাদা আলাদাভাবে টিউন করেছেন — merge করার সময় Git বুঝতে পারে না কোনটা সঠিক, তাই মানুষের সিদ্ধান্তের জন্য conflict marker রেখে থামে। সমাধান হতে পারে দুটোর একটা বেছে নেওয়া, বা দুজনের সাথে কথা বলে একটা নতুন consensus ভ্যালু ঠিক করা।
ours/theirs — merge বনাম rebase-এ উল্টো অর্থ
বিখ্যাত বিভ্রান্তি
"ours" আর "theirs" শব্দ দুটো Git-এ ব্যবহার হয়, কিন্তু merge আর rebase-এ এদের অর্থ ঠিক উল্টো — এটা experienced ডেভেলপারদেরও প্রায়ই বিভ্রান্ত করে।
Merge-এ: "ours" = আপনার বর্তমান branch
git switch main
git merge feature/x
# conflict হলে: "ours" = main (আপনি যেখানে আছেন), "theirs" = feature/x (যেটা merge করছেন)
git checkout --ours config.js # main-এর ভার্সন রাখুন
git checkout --theirs config.js # feature/x-এর ভার্সন রাখুন
Rebase-এ: "ours" = target branch, "theirs" = আপনার নিজের commit!
git switch feature/x
git rebase main
# rebase আসলে ভেতরে main-এ switch করে আপনার commit replay করে —
# তাই "ours" = main (base, যেটার উপর replay হচ্ছে), "theirs" = feature/x-এর commit (আপনারই কাজ!)
git checkout --ours config.js # এখানে main-এর ভার্সন!
git checkout --theirs config.js # এখানে আপনার নিজের feature commit-এর ভার্সন!
কেন এমন হয়
Rebase ভেতরে Git-কে আসলে বলে: "আমি temporarily main-এ চেকআউট করে আছি, এখন feature/x-এর commit-গুলো একে একে এখানে replay (cherry-pick-এর মতো) করছি।" প্রতিটা replay-এর সময় "ours" মানে বর্তমান base (main), আর "theirs" মানে যেই commit-টা apply করার চেষ্টা হচ্ছে (আপনারই পুরনো commit) — merge-এর মতো "আপনি" আর "অন্যপক্ষ"-এর ধারণা এখানে literally উল্টে যায়।
নিরাপদ অভ্যাস
নাম নিয়ে অনুমান না করে সবসময় git diff বা conflict marker-এর ভেতরের branch নাম (>>>>>>> <name>) দেখে নিশ্চিত হয়ে নিন কোনটা কী — বিশেষ করে rebase-এর সময়।
git merge --abort ও -X strategy option
merge --abort: সম্পূর্ণ ফিরে যাওয়া
git merge feature/x
# CONFLICT (content): Merge conflict in config.js
git merge --abort
--abort merge শুরু হওয়ার ঠিক আগের অবস্থায় working directory, index, ও HEAD সব ফিরিয়ে দেয় — যেন merge কখনো শুরুই হয়নি। এটা কাজ করে কারণ merge শুরু হলে .git/MERGE_HEAD ফাইলে merge-in-progress state সংরক্ষিত থাকে; --abort সেই state ব্যবহার করে reset করে।
-X ours / -X theirs: স্বয়ংক্রিয় strategy option
কখনো conflict হলে ম্যানুয়াল রেজোলিউশন না করে পুরোপুরি এক পক্ষের ভার্সন অটোমেটিক রাখতে চাইলে:
git merge -X ours feature/x # সব conflict-এ automatic ভাবে "ours" (main) জিতবে
git merge -X theirs feature/x # সব conflict-এ automatic ভাবে "theirs" (feature/x) জিতবে
সতর্কতা: -X ours/-X theirs পুরো merge strategy-র option, git checkout --ours-এর মতো per-file সিদ্ধান্ত না — এটা confusing কারণ নাম প্রায় একই, কিন্তু scope আলাদা (পুরো merge বনাম একটা ফাইল)। এছাড়া -X ours এবং git merge -s ours সম্পূর্ণ আলাদা জিনিস — -s ours (strategy) পুরো অন্য branch-এর পরিবর্তন সম্পূর্ণ উপেক্ষা করে, শুধু history-তে merge হয়েছে বলে mark করে।
রিয়েল-লাইফ ব্যবহার
Vendor/generated ফাইল merge করার সময় (যেমন lock ফাইল), যেখানে "কোনটা সঠিক" প্রশ্নটা অর্থহীন — শুধু সাম্প্রতিক ভার্সন দরকার — -X theirs দিয়ে ম্যানুয়াল কাজ এড়ানো যায়। কিন্তু business logic-এ কখনো এটা ব্যবহার করা উচিত না, কারণ নীরবে একপক্ষের কাজ হারিয়ে যেতে পারে।
merge.conflictstyle — diff3 ও zdiff3
ডিফল্ট conflictstyle-এর সীমাবদ্ধতা
ডিফল্ট merge style শুধু দুই পক্ষের ভার্সন দেখায় — কিন্তু আসল common ancestor-এ লাইনটা কেমন ছিলো সেটা দেখায় না। ফলে বোঝা কঠিন হতে পারে কে আসলে কী বদলেছে — দুটো ভার্সনই ancestor থেকে ভিন্ন হতে পারে ভিন্ন কারণে।
diff3: তৃতীয় অংশ যোগ করা
git config --global merge.conflictstyle diff3
<<<<<<< HEAD
const MAX_RETRIES = 5;
||||||| a1b2c3d (merge base)
const MAX_RETRIES = 10;
=======
const MAX_RETRIES = 3;
>>>>>>> feature/tune-retries
মাঝের ||||||| (merge base) অংশ দেখায় ancestor-এ আসল ভ্যালু কী ছিলো (১০)। এখন স্পষ্ট বোঝা যায় — আপনি ১০ থেকে ৫-এ কমিয়েছেন, অন্যপক্ষ ১০ থেকে ৩-এ কমিয়েছে। প্রসঙ্গ ছাড়া এই তথ্য অনুমান করা কঠিন ছিলো।
zdiff3: আরও পরিষ্কার (Git 2.35+)
git config --global merge.conflictstyle zdiff3
zdiff3 একই তথ্য দেখায় কিন্তু common (unchanged) লাইনগুলোকে conflict marker-এর বাইরে রেখে দেয়, ফলে conflict block ছোট ও পড়া সহজ হয় — শুধু আসল ভিন্নতাগুলো marker-এর ভেতরে থাকে।
সুপারিশ
আজকাল অনেক অভিজ্ঞ ডেভেলপার ডিফল্টভাবে zdiff3 সেট করে রাখেন — বাড়তি প্রসঙ্গের জন্য কোনো খরচ নেই, শুধু সুবিধা। এটা একটা one-time global config যা সারাজীবনের জন্য conflict resolution সহজ করে দেয়।
git rerere — বারবার একই conflict সমাধান এড়ানো
সমস্যা: একই conflict বারবার ফিরে আসা
Long-lived feature branch rebase করার সময়, একই লাইনে conflict বারবার দেখা দিতে পারে — প্রতিটা commit replay-তে যদি সেই লাইন touch হয়। প্রতিবার একই ম্যানুয়াল রেজোলিউশন পুনরাবৃত্তি করা ক্লান্তিকর এবং ভুলের ঝুঁকিপূর্ণ।
rerere: Reuse Recorded Resolution
git config --global rerere.enabled true
একবার চালু করলে, Git প্রতিটা conflict resolution-এর "before" ও "after" স্টেট রেকর্ড করে রাখে (.git/rr-cache-এ)। ভবিষ্যতে ঠিক একই conflict prefix আবার দেখা দিলে, Git স্বয়ংক্রিয়ভাবে আগের রেজোলিউশন প্রয়োগ করে — resolved marker সহ ফাইল দেখায়, শুধু review করে git add করলেই চলে।
git rebase main
# CONFLICT in config.js
# Resolved 'config.js' using previous resolution.
ভেতরে কীভাবে কাজ করে
rerere conflict marker-সহ ফাইলের একটা normalized ফিঙ্গারপ্রিন্ট (hash) রাখে "before" স্টেট হিসেবে, আর সমাধানের পরের কন্টেন্ট "after" হিসেবে। পরের বার একই fingerprint match করলে, সেই "after" কন্টেন্ট অটোমেটিক অ্যাপ্লাই হয়।
বাস্তব ব্যবহার
দীর্ঘ সময় ধরে বহুবার rebase হওয়া একটা feature branch-এ (প্রতিদিন main-এর উপর rebase করা হয়), একই merge conflict বারবার আসতে পারে যতক্ষণ না feature branch merge হয়ে যায় — rerere চালু থাকলে প্রথমবার resolve করার পর বাকি সবগুলোতে ম্যানুয়াল কাজ লাগে না। টিমে global default হিসেবে চালু রাখা একটা সাধারণ productivity বুস্ট।
git mergetool — GUI/visual conflict resolution
কী করে
git mergetool একটা কনফিগার করা external tool (VS Code, meld, vimdiff, kdiff3 ইত্যাদি) খুলে দেয় প্রতিটা conflicted ফাইলের জন্য, যেখানে three-pane (local/base/remote) ভিজ্যুয়াল view-তে conflict দেখা ও ক্লিক করে resolve করা যায় — টেক্সট marker হাতে এডিট করার বদলে।
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
git merge feature/x
# conflict হলে:
git mergetool
ভেতরে কী ঘটে
mergetool আসলে conflicted ফাইলের তিনটা ভার্সন (<file>.BASE, <file>.LOCAL, <file>.REMOTE) সাময়িক ফাইল হিসেবে তৈরি করে, external tool-কে সেই তিনটা পাস করে, আর tool বন্ধ হওয়ার পর ফলাফল ফাইলটা git add করার জন্য প্রস্তুত রাখে।
কখন ব্যবহার করবেন
- বড়, জটিল conflict যেখানে অনেকগুলো লাইন জড়িত — visual side-by-side view টেক্সট marker পড়ার চেয়ে দ্রুত বোঝা যায়।
- Binary বা structured ফাইল (যেমন JSON config) — syntax-aware tool ভুল কম করে।
- নতুন টিম মেম্বার যারা এখনো marker পড়তে স্বচ্ছন্দ না — GUI দিয়ে শেখা সহজ শুরু।
ছোট, সরল conflict-এ (এক-দুই লাইন) ম্যানুয়ালি marker এডিট করাই দ্রুততর — mergetool সব জায়গায় বাধ্যতামূলক না, প্রয়োজন অনুযায়ী বেছে নেওয়ার টুল।
ল্যাব: বাস্তব Conflict তৈরি ও হাতে-কলমে সমাধান
লক্ষ্য
ইচ্ছাকৃতভাবে একটা conflict তৈরি করে, marker পড়ে, ম্যানুয়ালি সমাধান করে সম্পূর্ণ merge শেষ করা।
mkdir git-conflict-lab && cd git-conflict-lab && git init
cat > config.js << 'EOF'
const MAX_RETRIES = 10;
const TIMEOUT_MS = 3000;
EOF
git add . && git commit -m "initial config"
# branch A: MAX_RETRIES কমানো
git switch -c feature/reduce-retries
sed -i 's/MAX_RETRIES = 10/MAX_RETRIES = 5/' config.js
git commit -am "reduce retries to 5 for faster failure"
# branch B: একই লাইন ভিন্নভাবে বদলানো
git switch main
sed -i 's/MAX_RETRIES = 10/MAX_RETRIES = 3/' config.js
git commit -am "reduce retries to 3 based on incident review"
# এখন merge করুন — conflict হবে
git merge feature/reduce-retries
# CONFLICT (content): Merge conflict in config.js
সমাধান করুন
cat config.js
# <<<<<<< HEAD
# const MAX_RETRIES = 3;
# =======
# const MAX_RETRIES = 5;
# >>>>>>> feature/reduce-retries
# const TIMEOUT_MS = 3000;
ফাইলটা এডিট করে একটা সিদ্ধান্ত নিন (ধরুন টিমে আলোচনা করে ৪ ঠিক হলো):
# marker মুছে final content রাখুন:
# const MAX_RETRIES = 4;
# const TIMEOUT_MS = 3000;
git add config.js
git commit # merge commit message এডিটর খুলবে, ডিফল্ট রেখে সেভ করুন
git log --graph --oneline
চ্যালেঞ্জ (নিজে করুন)
একই সিনারিও আবার তৈরি করুন কিন্তু git config merge.conflictstyle zdiff3 চালু করে — লক্ষ্য করুন conflict marker-এর ভেতরে এবার merge base-এর মূল ভ্যালু (১০) কীভাবে দেখা যায়, যা প্রথমবার দেখা যায়নি।
ইন্টারভিউ প্রশ্ন — Module 12
প্র.১ — conflict marker-এর তিনটা অংশ কী বোঝায়?
উত্তর: <<<<<<< HEAD থেকে ======= পর্যন্ত বর্তমান branch (HEAD)-এর ভার্সন, ======= থেকে >>>>>>> <branch> পর্যন্ত merge/rebase হওয়া branch-এর ভার্সন। >>>>>>>-এর পরের নাম বলে দেয় কোন branch থেকে সেই অংশ এসেছে।
প্র.২ — merge আর rebase-এ "ours"/"theirs"-এর অর্থ কেন উল্টে যায়? উত্তর: Merge-এ "ours" মানে আপনার বর্তমান branch, "theirs" মানে merge করা হচ্ছে যেটা। Rebase আসলে ভেতরে target branch-এ (base) গিয়ে আপনার commit-গুলো replay করে — তাই "ours" মানে base (target), "theirs" মানে replay হওয়া commit (যেটা আসলে আপনারই কাজ)। এই কারণে rebase-এ ours/theirs উল্টে যায়।
প্র.৩ — git merge -X theirs আর git checkout --theirs-এর পার্থক্য কী?
উত্তর: -X theirs পুরো merge strategy-র option — সব conflict-এ automatic ভাবে theirs জিতবে। git checkout --theirs <file> একটা নির্দিষ্ট ফাইলের জন্য ম্যানুয়ালি theirs ভার্সন নেওয়া (conflict চলাকালীন per-file সিদ্ধান্ত)।
প্র.৪ — merge.conflictstyle=diff3 কী বাড়তি তথ্য দেখায় যা ডিফল্ট style দেখায় না?
উত্তর: common ancestor (merge base)-এ লাইনটা আসলে কেমন ছিলো, ||||||| (merge base) সেকশনে। এটা বুঝতে সাহায্য করে দুই পক্ষ ঠিক কী পরিবর্তন করেছে ancestor থেকে, শুধু চূড়ান্ত দুই ভার্সন দেখে অনুমান করার বদলে।
প্র.৫ — git rerere কী সমস্যা সমাধান করে?
উত্তর: long-lived branch বারবার rebase করার সময় একই conflict বারবার ফিরে আসার সমস্যা। rerere আগের resolution রেকর্ড করে রাখে এবং একই conflict fingerprint আবার দেখা দিলে স্বয়ংক্রিয়ভাবে আগের সমাধান প্রয়োগ করে।
প্র.৬ — merge conflict-এর মাঝপথে সম্পূর্ণ বাতিল করতে চাইলে কী করবেন?
উত্তর: git merge --abort চালাতে হবে — এটা merge শুরুর আগের অবস্থায় working directory, index, HEAD সব ফিরিয়ে দেয়, যেন merge কখনো শুরুই হয়নি।
কর্নার কেস — Module 12
- rebase চলাকালীন
git merge --abortকাজ করবে না — rebase-এর জন্য আলাদা কমান্ড লাগে:git rebase --abort। দুটো ভিন্ন in-progress state (MERGE_HEADবনাম.git/rebase-merge/rebase-apply), তাই ভুল abort কমান্ড চালালে "There is no merge to abort" এরর আসবে। git add-এর পর conflict marker ফাইলের ভেতরে ভুলে থেকে গেলেও Git resolved হিসেবে ধরে নেয় — Git marker সিনট্যাক্স চেক করে না, শুধু ফাইল staged হয়েছে কিনা দেখে। ফলে অসাবধানে<<<<<<< HEADটেক্সট কোডে কমিট হয়ে যাওয়া একটা সাধারণ (embarrassing) ভুল — commit করার আগে diff রিভিউ করা ভালো অভ্যাস।- rerere রেকর্ড করা resolution ভুল হলে সেটাও পরের বার স্বয়ংক্রিয়ভাবে re-apply হবে — ভুল ফিক্স একবার রেকর্ড হয়ে গেলে বারবার ভুলভাবেই অ্যাপ্লাই হতে থাকবে যতক্ষণ না
git rerere forget <file>দিয়ে সেই এন্ট্রি মুছে ফেলা হয়। -X oursপুরো merge-এ conflict এড়িয়ে যায়, কিন্তু non-conflicting পরিবর্তন ঠিকই merge করে — এটাgit merge -s ours-এর মতো সম্পূর্ণ অন্য branch উপেক্ষা করে না; বিভ্রান্তি এড়াতে দুটোর পার্থক্য স্পষ্ট মনে রাখা জরুরি।
MODULE 13: Reset & Undo Model
git reset Git-এর সবচেয়ে শক্তিশালী কিন্তু সবচেয়ে ভুল-বোঝা কমান্ডগুলোর একটা — কারণ এর তিনটা মোড (--soft/--mixed/--hard) তিনটা ভিন্ন এলাকা বদলায়, আর ভুল মোড বেছে নিলে কাজ হারানোর ঝুঁকি থাকে। এই মডিউলে reset-এর তিন স্তর স্পষ্টভাবে আলাদা করা হবে, git revert-এর সাথে তুলনা করা হবে (কখন কোনটা নিরাপদ), আর git clean দিয়ে untracked ফাইল পরিষ্কার করা কভার হবে — প্রতিটা destructive কমান্ডের সাথে স্পষ্ট সতর্কতা সহ।
reset --soft/--mixed/--hard — তিনটা স্তরের স্পষ্ট তুলনা
Git-এর তিনটা এলাকা যা reset প্রভাবিত করে
Git-এর ওয়ার্কিং মডেলে তিনটা আলাদা স্তর আছে: HEAD/branch ref (বর্তমান commit কোথায় পয়েন্ট করছে), index/staging area (পরের commit-এ কী যাবে তার প্রস্তুতি), আর working directory (ডিস্কে থাকা প্রকৃত ফাইল)। git reset-এর তিনটা মোড ঠিক করে কতদূর পর্যন্ত এই তিনটা এলাকা বদলাবে।
| মোড | HEAD/branch ref | Index (staging) | Working directory |
|---|---|---|---|
--soft |
বদলায় | বদলায় না (আগের staged অবস্থা থাকে) | বদলায় না |
--mixed (ডিফল্ট) |
বদলায় | বদলায় (unstage হয়ে যায়) | বদলায় না |
--hard |
বদলায় | বদলায় | বদলায় — ডিস্কের ফাইলও রিপ্লেস হয় |
ব্যবহারিক পার্থক্য
git reset --soft HEAD~1
# শেষ commit-টা "undo" হলো, কিন্তু তার পরিবর্তন সব staged অবস্থায় রয়ে গেলো —
# তক্ষুনি আবার git commit চালালে প্রায় একই commit ফিরে পাবেন (হয়তো ভিন্ন মেসেজ দিয়ে)
git reset --mixed HEAD~1 # (বা শুধু git reset HEAD~1)
# commit undo, staging area খালি — পরিবর্তন working directory-তে আছে কিন্তু unstaged
git reset --hard HEAD~1
# commit undo, staging area খালি, working directory-ও আগের commit-এর অবস্থায় ফিরে যায় —
# uncommitted পরিবর্তন থাকলে তাও হারিয়ে যাবে
⚠️ সতর্কতা: --hard destructive
--hard uncommitted কাজ স্থায়ীভাবে মুছে দিতে পারে — commit না করা পরিবর্তন কোনো object হিসেবে সংরক্ষিত থাকে না (শুধু commit হওয়া object-ই .git/objects-এ থাকে, working directory-র raw পরিবর্তন না)। চালানোর আগে সবসময় git status ও git diff দিয়ে নিশ্চিত হয়ে নিন কিছু হারানোর মতো নেই, অথবা আগে git stash করে রাখুন।
pathspec দিয়ে reset — নির্দিষ্ট ফাইল unstage করা
সমস্যা: পুরো commit না, শুধু একটা ফাইল unstage করা
মাঝেমধ্যে সব ফাইল git add . দিয়ে stage করার পর বুঝতে পারেন একটা ফাইল ভুলবশত স্টেজ হয়ে গেছে, যেটা এখনই commit করতে চান না — পুরো staging area খালি করতে চান না, শুধু সেই একটা ফাইল।
সমাধান: pathspec সহ reset
git add config.js secrets.env README.md
git reset HEAD secrets.env
# অথবা আধুনিক সিনট্যাক্স:
git restore --staged secrets.env
ভেতরে কী ঘটে
pathspec ছাড়া git reset <commit> পুরো branch ref-কেই নতুন commit-এ নিয়ে যায় (আগের টেবিলের behavior)। কিন্তু pathspec (একটা ফাইলের নাম) দিলে reset সম্পূর্ণ ভিন্ন মোডে কাজ করে — branch ref একদম বদলায় না, শুধু index-এর সেই নির্দিষ্ট ফাইলের এন্ট্রিটা নির্দিষ্ট commit-এর ভার্সন দিয়ে ওভাররাইট করা হয় (ডিফল্টে HEAD-এর ভার্সন দিয়ে, যেটা কার্যকরভাবে "unstage")। working directory-র ফাইল কখনোই স্পর্শ করা হয় না — তাই এই ফর্মটা --hard-এর মতো বিপজ্জনক না, uncommitted পরিবর্তন হারানোর ঝুঁকি নেই।
রিয়েল-লাইফ ব্যবহার
.env বা secret ফাইল ভুলবশত git add .-এ চলে এলে commit হওয়ার আগেই git restore --staged .env দিয়ে বের করে ফেলা — এটা একটা daily-driver নিরাপত্তা অভ্যাস, বিশেষ করে যখন .gitignore আপডেট করতে ভুলে গেছেন।
git revert — নিরাপদভাবে ইতিহাস না বদলে undo করা
reset-এর বিপরীত দর্শন
git reset ইতিহাস মুছে/পুনর্লিখন করে — branch ref পিছনে টেনে নেয়। git revert সম্পূর্ণ ভিন্ন approach নেয়: পুরনো commit মুছে না, বরং তার বিপরীত পরিবর্তন নিয়ে একটা নতুন commit যোগ করে। ইতিহাস অক্ষত থাকে, শুধু একটা নতুন "undo" entry যুক্ত হয়।
git revert a1b2c3d
# একটা নতুন commit তৈরি হবে যা a1b2c3d-এর পরিবর্তনগুলো উল্টে দেয়
-n (--no-commit): একাধিক revert একসাথে squash করা
git revert -n commit1 commit2 commit3
# তিনটা revert-ই staged হবে কিন্তু কোনো commit হবে না
git commit -m "revert three problematic commits together"
-m (mainline): merge commit revert করা
একটা সাধারণ commit-এর একটাই parent থাকে, revert করা সহজ — সরাসরি বিপরীত diff। কিন্তু merge commit-এর দুটো parent থাকে, তাই Git জানে না কোন দিকের ইতিহাসকে "mainline" ধরে বিপরীত diff বানাবে — -m 1 বা -m 2 দিয়ে নির্দিষ্ট করতে হয় কোন parent number-কে ভিত্তি ধরা হবে (সাধারণত -m 1, কারণ parent 1 হলো যে branch-এ merge করা হয়েছিলো)।
git revert -m 1 <merge-commit-hash>
রিয়েল-লাইফ উদাহরণ
Production-এ একটা বাগযুক্ত ফিচার merge হয়ে গেছে, কিন্তু সেই কোড ইতিমধ্যে অন্যদের local branch-এ pull হয়ে গেছে — এখন reset --hard করলে অন্যদের সাথে ইতিহাস বিরোধ (diverge) তৈরি হবে। git revert -m 1 <merge-hash> দিয়ে একটা নতুন "undo" commit push করলে সবার ইতিহাস সুসংগত থাকে, কেউ force-push conflict-এ পড়ে না।
reset বনাম revert — কখন কোনটা ব্যবহার করবেন
সোনার নিয়ম
Shared/pushed history-তে কখনো
reset --hardবা history-rewriting rebase ব্যবহার করবেন না — সবসময়revertব্যবহার করুন।
কেন
reset branch ref-কে পিছনে নিয়ে যায় — commit hash-গুলো "মুছে" ফেলে (আসলে unreachable করে)। যদি সেই commit-গুলো ইতিমধ্যে remote-এ push হয়ে গিয়ে অন্যরা pull করে ফেলেছে, তাহলে তাদের local ইতিহাস আর আপনার নতুন (ছোট) ইতিহাসের সাথে diverge করে যায়। পরের push করতে গেলে force-push (--force) লাগবে, যেটা অন্যদের কাজ ওভাররাইট করে দিতে পারে — একটা টিমের জন্য বিপর্যয়কর হতে পারে।
revert কোনো পুরনো commit মুছে না, শুধু নতুন commit যোগ করে — এটা সবসময় fast-forward-বান্ধব, অন্য কারো ইতিহাসের সাথে conflict তৈরি করে না।
সিদ্ধান্তের টেবিল
| পরিস্থিতি | ব্যবহার করুন |
|---|---|
| এখনো push করেননি, নিজের লোকাল branch | reset (soft/mixed/hard, যেকোনোটা নিরাপদ) |
| push হয়ে গেছে, কেউ pull করেনি এখনো (একা কাজ করছেন) | reset + push --force-with-lease (সাবধানে) |
| push হয়ে গেছে, অন্যরা ইতিমধ্যে pull করে ফেলেছে | অবশ্যই revert |
main/shared/production branch |
সবসময় revert, কখনো reset --hard না |
--force-with-lease বনাম --force
নিজের একার branch-এ reset-এর পর push করতে হলেও --force না দিয়ে --force-with-lease ব্যবহার করা ভালো অভ্যাস — এটা চেক করে remote-এ আপনার শেষ জানা অবস্থার পর কেউ নতুন কিছু push করেছে কিনা, করলে refuse করে (নিরাপত্তা গার্ড)।
git clean — untracked ফাইল পরিষ্কার করা
কী করে
git clean working directory থেকে untracked ফাইল (যেগুলো কখনো git add হয়নি) মুছে ফেলে। এটা reset-এর সাথে সম্পর্কিত না সরাসরি, কিন্তু একই "undo/cleanup" পরিবারের কমান্ড — প্রায়ই reset --hard-এর পাশাপাশি ব্যবহার হয় একটা রিপোকে সম্পূর্ণ পরিষ্কার অবস্থায় ফেরাতে।
git clean -n # dry-run: কী কী মুছে যেত তা দেখায়, আসলে কিছু মুছে না (সবসময় প্রথমে এটা চালান!)
git clean -f # force: আসলেই মুছে ফেলে (ডিফল্টে সেফটির জন্য clean কিছু করে না -f ছাড়া)
git clean -fd # ডিরেক্টরিসহ (untracked ডিরেক্টরি, যেমন node_modules-এর ভুল কপি)
git clean -fx # .gitignore-এ থাকা ফাইলসহ (বিল্ড আর্টিফ্যাক্ট, .env, ইত্যাদিও মুছে যাবে!)
⚠️ সতর্কতা: -f এবং বিশেষত -x অত্যন্ত ধ্বংসাত্মক
-fd কোনো নিরাপত্তা নেট ছাড়াই untracked ফাইল ডিরেক্টরিসহ মুছে দেয় — এগুলো কখনো commit হয়নি, তাই git reflog-এও এদের কোনো ট্রেস নেই। একবার মুছে গেলে সম্পূর্ণ, স্থায়ীভাবে হারিয়ে যায়, filesystem-level রিকভারি টুল ছাড়া কোনো উপায় নেই।
-fx আরও বিপজ্জনক — এটা .gitignore-এ থাকা ফাইলও মুছে দেয়, যার মধ্যে থাকতে পারে .env (secrets), IDE কনফিগ, বা ম্যানুয়ালি তৈরি করা লোকাল কনফিগ ফাইল যা কখনো ইচ্ছাকৃতভাবে ট্র্যাক করা হয়নি।
নিয়ম: -f/-fd/-fx চালানোর আগে সবসময় -n দিয়ে dry-run করুন এবং আউটপুট মনোযোগ দিয়ে পড়ুন।
রিয়েল-লাইফ ব্যবহার
CI বিল্ড এজেন্টে প্রতিটা রান শুরুর আগে workspace সম্পূর্ণ পরিষ্কার নিশ্চিত করতে git clean -fdx চালানো সাধারণ প্র্যাকটিস — কিন্তু এটা শুধু ephemeral CI environment-এর জন্য নিরাপদ, ডেভেলপারের নিজের মেশিনে সাবধানে ব্যবহার করা উচিত।
ল্যাব: Reset-এর তিন স্তর হাতে-কলমে অনুভব করা
লক্ষ্য
তিনটা reset মোড ঠিক কী বদলায় তা নিজের চোখে দেখা — নিরাপদ sandbox-এ (destructive কমান্ড আছে, তাই এই ল্যাবের বাইরে সাবধানে ব্যবহার করুন)।
mkdir git-reset-lab && cd git-reset-lab && git init
echo "v1" > file.txt && git add . && git commit -m "commit 1"
echo "v2" > file.txt && git add . && git commit -m "commit 2"
echo "v3" > file.txt && git add . && git commit -m "commit 3"
git log --oneline
# ১) --soft পরীক্ষা
git reset --soft HEAD~1
git status # staged অবস্থায় "commit 3"-এর পরিবর্তন দেখুন
git log --oneline # শুধু ২টা commit দেখাবে
git diff --staged # স্টেজড ডিফ দেখুন
# পুনরায় কমিট করে ৩ নম্বরে ফিরুন
git commit -m "commit 3 again"
# ২) --mixed পরীক্ষা (ডিফল্ট)
git reset HEAD~1
git status # unstaged অবস্থায় পরিবর্তন দেখুন
git diff # working tree ডিফ (staged না)
git add . && git commit -m "commit 3 restored"
# ৩) --hard পরীক্ষা (সাবধানে!)
echo "uncommitted experiment" >> file.txt # uncommitted পরিবর্তন যোগ করুন
git status
git reset --hard HEAD~1
git status # কিছুই staged/unstaged নেই — uncommitted কাজও হারিয়ে গেছে!
cat file.txt # "v2" দেখাবে, "uncommitted experiment" হারিয়ে গেছে
# ৪) pathspec reset
echo "secret" > .env
echo "code" > app.py
git add .env app.py
git status
git restore --staged .env
git status # শুধু app.py staged, .env unstage হয়ে গেছে
চ্যালেঞ্জ (নিজে করুন)
git clean -n চালিয়ে .env ফাইলটা দেখুন (unstage করলেও এখনো untracked)। তারপর .gitignore-এ .env যোগ করে আবার git clean -n চালিয়ে দেখুন পার্থক্য — এখন এটা -x ছাড়া clean-এ দেখাবে না।
ইন্টারভিউ প্রশ্ন — Module 13
প্র.১ — reset --soft, --mixed, --hard-এর মধ্যে মূল পার্থক্য এক বাক্যে বলুন।
উত্তর: তিনটাই HEAD/branch ref বদলায়। --soft এখানেই থামে (index, working dir অপরিবর্তিত)। --mixed (ডিফল্ট) index-ও reset করে (unstage হয়) কিন্তু working dir অপরিবর্তিত রাখে। --hard তিনটাই বদলে দেয় — working directory-র ফাইলও পুরনো commit-এর অবস্থায় ফিরিয়ে দেয়, uncommitted কাজ হারানোর ঝুঁকি সহ।
প্র.২ — git reset --hard কেন কখনো কখনো "স্থায়ীভাবে" ডেটা মুছে ফেলে?
উত্তর: কারণ uncommitted পরিবর্তন কখনো কোনো Git object হিসেবে সংরক্ষিত হয় না — শুধু committed ডেটা .git/objects-এ থাকে। --hard working directory-র uncommitted পরিবর্তন ওভাররাইট করে দেয়, আর সেটার কোনো ব্যাকআপ Git-এর নিজস্ব স্টোরেজে থাকে না।
প্র.৩ — revert কেন reset-এর চেয়ে shared branch-এ নিরাপদ?
উত্তর: revert পুরনো commit মুছে না, বরং একটা নতুন commit যোগ করে যা বিপরীত পরিবর্তন প্রয়োগ করে — ইতিহাস অক্ষত থাকে, ফলে যাদের কাছে ইতিমধ্যে পুরনো commit pull হয়ে গেছে তাদের সাথে কোনো diverge/conflict তৈরি হয় না। reset branch ref পিছনে নিয়ে ইতিহাস "মুছে" ফেলে, যা shared history-তে force-push প্রয়োজন করে।
প্র.৪ — merge commit revert করতে -m ফ্ল্যাগ কেন লাগে?
উত্তর: সাধারণ commit-এর একটাই parent থাকে, বিপরীত diff স্পষ্ট। কিন্তু merge commit-এর দুটো parent — Git জানে না কোনটাকে mainline ধরে বিপরীত হিসাব করবে, তাই -m 1/-m 2 দিয়ে স্পষ্ট করতে হয়।
প্র.৫ — git clean -fdx কেন সবচেয়ে বিপজ্জনক ভ্যারিয়েন্ট?
উত্তর: -f আসল ডিলিট এক্সিকিউট করে, -d untracked ডিরেক্টরিও মুছে, আর -x .gitignore-এ থাকা ফাইলও মুছে দেয় — যার মধ্যে .env/secrets/লোকাল কনফিগ থাকতে পারে যা ইচ্ছাকৃতভাবে কখনো ট্র্যাক করা হয়নি। এবং reflog-এ এর কোনো ট্রেস থাকে না কারণ এগুলো কখনো commit-ই হয়নি।
প্র.৬ — pathspec সহ git reset HEAD <file> আর সাধারণ git reset HEAD~1-এর আচরণ কীভাবে আলাদা?
উত্তর: pathspec ছাড়া reset পুরো branch ref-কে অন্য commit-এ সরিয়ে দেয়। pathspec সহ reset branch ref একদমই বদলায় না — শুধু index-এর সেই নির্দিষ্ট ফাইলের এন্ট্রি নির্দিষ্ট commit-এর ভার্সন দিয়ে ওভাররাইট (সাধারণত unstage) করে, working directory স্পর্শ না করেই।
কর্নার কেস — Module 13
- ⚠️
git reset --hardকরার আগে stash-এ থাকা পরিবর্তনও ঝুঁকিতে না থাকলেও, uncommitted staged/unstaged পরিবর্তন সাথে সাথে হারিয়ে যায় — কমান্ড চালানোর আগেgit status/git diffদিয়ে দুবার চেক করুন, বাgit stashকরে নিরাপদ রাখুন। - ⚠️
git clean -fdreflog দিয়ে রিকভার করা যায় না — reflog শুধু commit history ট্র্যাক করে, untracked ফাইল কখনো Git object হয়নি বলে কোনো ট্রেস অবশিষ্ট থাকে না। এটাreset --hard-এর চেয়েও বেশি বিপজ্জনক এই অর্থে যে কোনো recovery path নেই। reset --mixed(ডিফল্ট) চালালে বড় ডিরেক্টরিতে সময় লাগতে পারে কারণ পুরো index পুনর্গঠন করতে হয় — বড় মনোরিপোতে এটা লক্ষণীয় ধীর হতে পারে,--softতুলনামূলক দ্রুত কারণ index স্পর্শ করে না।revertকরার সময় যদি সেই commit-এর পরিবর্তন ইতিমধ্যে অন্য commit দ্বারা ওভাররাইট হয়ে গেছে, তাহলে revert নিজেই conflict তৈরি করতে পারে — এক্ষেত্রে সাধারণ conflict resolution flow (marker এডিট,git add,git revert --continue) প্রয়োজন হয়, ঠিক merge conflict-এর মতোই।
MODULE 14: Interactive Rebase
Interactive rebase হলো Git-এর সবচেয়ে শক্তিশালী history-editing টুল — commit reorder, combine, edit, বা মুছে ফেলা সম্ভব একটা ইন্টারঅ্যাক্টিভ এডিটরের মাধ্যমে। কিন্তু এই শক্তি আসে একটা গুরুত্বপূর্ণ শর্তে: rebase আসলে ইতিহাস "সম্পাদনা" করে না, এটা নতুন commit object তৈরি করে পুরনোগুলো প্রতিস্থাপন করে — যা কেন এটাকে "history rewriting" বলা হয় তার মূল কারণ। এই মডিউলে replay মডেল, প্রতিটা rebase action, --onto-এর advanced ব্যবহার, আর rebase-এর সবচেয়ে গুরুত্বপূর্ণ নিয়ম — কখনো shared branch rebase না করা — কভার হবে।
Rebase-এর Replay মডেল — কেন এটা 'History Rewriting'
Rebase আসলে কী করে
git rebase main চালালে মনে হতে পারে বর্তমান branch-এর commit-গুলো "সরিয়ে" main-এর উপর বসানো হচ্ছে। কিন্তু ভেতরে ঘটনাটা সম্পূর্ণ ভিন্ন: Git বর্তমান branch-এর প্রতিটা commit এক এক করে main-এর সর্বশেষ tip-এর উপর নতুন করে "replay" (এক ধরনের cherry-pick) করে।
কেন নতুন hash
প্রতিটা commit object-এর hash তার কন্টেন্ট, মেসেজ, author, timestamp, এবং parent hash দিয়ে নির্ধারিত হয় (SHA-1/SHA-256 checksum)। rebase-এর পর প্রতিটা replay-করা commit-এর parent বদলে গেছে (আগে ছিলো পুরনো base, এখন নতুন base) — parent বদলালেই hash সম্পূর্ণ ভিন্ন হয়ে যায়, যদিও diff/content একই থাকতে পারে।
git log --oneline feature/x
# c3d4e5f add validation
# b2c3d4e add form
git rebase main
git log --oneline feature/x
# 9f8e7d6 add validation ← সম্পূর্ণ নতুন hash!
# 8e7d6c5 add form ← এটাও নতুন hash!
এইজন্যই "history rewriting"
পুরনো commit object (c3d4e5f, b2c3d4e) মুছে যায় না তক্ষুনি — শুধু কোনো branch থেকে reachable থাকে না (reflog-এ কিছুদিন থেকে যায়)। নতুন commit-গুলো (9f8e7d6, 8e7d6c5) সম্পূর্ণ নতুন object, একই কন্টেন্ট কিন্তু ভিন্ন পরিচয়। এই কারণেই rebase-করা branch যদি কেউ ইতিমধ্যে pull করে থাকে, তাদের কাছে পুরনো hash-ভিত্তিক ইতিহাস আর নতুন hash-ভিত্তিক ইতিহাস — দুটো সম্পূর্ণ আলাদা commit set হিসেবে দেখা দেবে, যদিও কন্টেন্ট একই — এটাই shared branch rebase না করার মূল কারণ (পরে বিস্তারিত)।
git rebase -i — pick/reword/edit/squash/fixup/drop
ইন্টারঅ্যাক্টিভ এডিটর খোলা
git rebase -i HEAD~4
এটা একটা এডিটরে সাম্প্রতিক ৪টা commit-এর তালিকা খুলে দেয়, প্রতিটার সামনে একটা action:
pick b2c3d4e add form
pick c3d4e5f add validation
pick d4e5f6a fix typo
pick e5f6a7b add tests
প্রতিটা action-এর আচরণ
| Action | কী করে |
|---|---|
pick |
commit-টা যেমন আছে তেমনই রাখা (ডিফল্ট) |
reword |
commit content একই রাখা, শুধু commit message এডিট করার সুযোগ |
edit |
সেই commit-এ rebase থামিয়ে দেওয়া — ফাইল বদলানো/git commit --amend করার সুযোগ, তারপর --continue |
squash |
এই commit-কে আগেরটার সাথে মিশিয়ে ফেলা, দুটো মেসেজ একসাথে এডিট করার সুযোগ |
fixup |
squash-এর মতোই মিশিয়ে ফেলা, কিন্তু এই commit-এর মেসেজ সম্পূর্ণ বাদ দেওয়া হয় |
drop |
commit সম্পূর্ণ বাদ দেওয়া (লাইন মুছে ফেললেও একই ফল) |
উদাহরণ: চারটা commit-কে দুটোতে কমানো
pick b2c3d4e add form
fixup c3d4e5f add validation ← form-এর সাথে নিঃশব্দে মিশে যাবে
pick d4e5f6a fix typo
squash e5f6a7b add tests ← fix typo-এর সাথে মিশবে, মেসেজ এডিট করার সুযোগ পাবেন
রিয়েল-লাইফ ব্যবহার
একটা PR review-এর আগে ৮টা "wip", "fix typo", "actually fix" commit-কে ১-২টা পরিষ্কার, অর্থবহ commit-এ কমিয়ে আনা — টিমের কাছে PR history readable রাখার জন্য একটা সাধারণ অভ্যাস।
--fixup ও --autosquash — দ্রুত ফিক্স মেশানো
সমস্যা: ম্যানুয়ালি reorder করা ঝামেলার
squash/fixup ব্যবহার করতে হলে rebase editor খুলে ম্যানুয়ালি commit reorder করে সঠিক জায়গায় action বসাতে হয় — একটু ভুল হলেই ভুল commit-এর সাথে মিশে যেতে পারে।
সমাধান: --fixup দিয়ে টার্গেট commit নির্দিষ্ট করা
git commit --fixup=b2c3d4e
# একটা নতুন commit তৈরি হবে মেসেজ "fixup! add form" দিয়ে
এই বিশেষ মেসেজ ফরম্যাট (fixup! <original subject>) Git-কে বলে দেয় এই commit কোন commit-এর সাথে মিশতে চায়।
--autosquash: rebase নিজে থেকেই সাজিয়ে দেয়
git rebase -i --autosquash HEAD~5
rebase editor খুললে fixup!/squash! prefix থাকা commit-গুলো স্বয়ংক্রিয়ভাবে তাদের টার্গেট commit-এর ঠিক পরে, সঠিক action (fixup/squash) দিয়ে বসানো অবস্থায় দেখাবে — ম্যানুয়াল reorder করার দরকার নেই, শুধু save করলেই চলে।
git config --global rebase.autosquash true # সবসময় ডিফল্টে চালু রাখা
রিয়েল-লাইফ ওয়ার্কফ্লো
git commit -am "add form" # b2c3d4e
git commit -am "add validation" # c3d4e5f
# রিভিউতে ধরা পড়লো form-এ একটা bug —
git commit --fixup=b2c3d4e -am "fix bug in form"
git rebase -i --autosquash HEAD~3
# save করলেই fixup commit স্বয়ংক্রিয়ভাবে b2c3d4e-এর সাথে মিশে যাবে
এটা PR history পরিষ্কার রাখার সবচেয়ে দ্রুত পদ্ধতি — reviewer-এর কমেন্ট অনুযায়ী ফিক্স করার পর সেটা মূল commit-এর সাথেই মিশিয়ে দেওয়া, আলাদা "fix review comment" commit না রেখে।
--onto — Branch সরানোর Advanced ব্যবহার
সমস্যা: একটা branch-এর বেস বদলাতে হবে, কিন্তু মাঝের কিছু commit বাদ দিতে হবে
ধরুন feature/b, feature/a-এর উপরে তৈরি হয়েছিলো (feature/a থেকে branch করা), কিন্তু এখন feature/a বাতিল হয়ে গেছে এবং feature/b-কে সরাসরি main-এর উপর বসাতে হবে — শুধু feature/a-এর পরের commit-গুলো (যেগুলো feature/b-তে যোগ হয়েছে) রেখে।
সমাধান: --onto
git rebase --onto main feature/a feature/b
# অর্থ: feature/a থেকে feature/b পর্যন্ত commit-গুলো নাও, main-এর উপর replay করো
সিনট্যাক্স ভাঙা
git rebase --onto <newbase> <oldbase> <branch>:
<newbase>— যেখানে replay হবে (main)<oldbase>— কোথা থেকে replay শুরু হবে, exclusive (feature/a, এর নিজের commit বাদ)<branch>— কোন branch-এর commit replay হবে (feature/b, checkout না করা থাকলেও কাজ করে)
ভিজ্যুয়াল
আগে: main ─── A1 ─── A2 (feature/a) ─── B1 ─── B2 (feature/b)
পরে: main ─┬── A1 ─── A2 (feature/a)
└── B1' ─── B2' (feature/b) ← শুধু B1, B2 replay হলো, নতুন hash সহ
অন্য বাস্তব ব্যবহার: একটা নির্দিষ্ট রেঞ্জের commit বাদ দেওয়া
git rebase --onto HEAD~5 HEAD~3 HEAD
# HEAD-এর সাম্প্রতিক ৩টা commit-এর মাঝের commit বাদ দিয়ে ৫টা আগের commit-এর উপর বসানো
--onto interactive rebase-এর সবচেয়ে সূক্ষ্ম নিয়ন্ত্রণ টুল — যখন -i দিয়ে drop/reorder যথেষ্ট না, branch-এর গোড়াই বদলাতে হয়, তখন এটাই একমাত্র পথ।
--autostash, --continue/--skip/--abort, --rebase-merges
--autostash: uncommitted কাজ থাকলেও rebase শুরু করা
সাধারণত uncommitted পরিবর্তন থাকলে rebase শুরু হতে দেয় না ("cannot rebase: You have unstaged changes")। --autostash স্বয়ংক্রিয়ভাবে rebase শুরুর আগে stash করে, rebase শেষে আবার pop করে দেয়:
git rebase --autostash main
git config --global rebase.autoStash true # ডিফল্ট করে দেওয়া ভালো অভ্যাস
conflict হলে তিনটা পথ
git rebase --continue # conflict resolve করে (add করার পর) পরের commit replay চালিয়ে যাওয়া
git rebase --skip # এই commit সম্পূর্ণ বাদ দিয়ে পরেরটায় চলে যাওয়া (এই commit-এর পরিবর্তন হারিয়ে যাবে)
git rebase --abort # পুরো rebase বাতিল, শুরুর আগের অবস্থায় সম্পূর্ণ ফিরে যাওয়া
--skip সাবধানে ব্যবহার করা উচিত — এটা conflict resolve করে না, বরং পুরো commit-টাকেই ফেলে দেয়, যা ইচ্ছাকৃত না হলে ডেটা হারানোর কারণ হতে পারে।
--rebase-merges: merge commit structure বজায় রাখা
ডিফল্ট rebase merge commit-গুলো "flatten" করে ফেলে — merge structure হারিয়ে সব commit একটা linear সিকোয়েন্সে replay হয়। --rebase-merges merge commit topology বজায় রেখে rebase করে — জটিল branch history-সহ একটা বড় feature branch rebase করতে দরকার হয় যখন সেই branch-এর ভেতরেই sub-merge আছে।
git rebase -i --rebase-merges main
এই মোডে interactive editor-এ merge action-ও দেখা যায়, যা নির্দিষ্ট করে কোথায় merge commit আবার তৈরি হবে replay-এর সময়।
রিবেসের সোনার নিয়ম — কখনো Shared Branch Rebase করবেন না
নিয়মটা
যে commit ইতিমধ্যে push হয়ে গেছে এবং অন্য কেউ pull করে ফেলেছে, সেটা কখনো rebase করবেন না।
কেন — hash বদলে যাওয়ার সরাসরি ফলাফল
আগের মডিউলে দেখা গেছে rebase প্রতিটা commit-এর নতুন hash তৈরি করে। ধরুন আপনি feature/x push করেছেন, সহকর্মী সেটা pull করে তার উপর নিজের কাজ করেছেন। এখন আপনি feature/x rebase করে আবার push (force) করলে:
- আপনার নতুন commit hash-গুলো সহকর্মীর কাছে সম্পূর্ণ অপরিচিত — তার local ইতিহাসের সাথে কোনো সংযোগ নেই।
git pullকরলে তার কাছে দেখাবে যেন হঠাৎ অনেক নতুন commit এসেছে, আর তার নিজের কাজ পুরনো (rebase-এর আগের) hash-চেইনের সাথে আটকে আছে — একটা বিশাল, বিভ্রান্তিকর conflict বা duplicate commit তৈরি হবে।- তাকে ম্যানুয়ালি
git rebase --ontoবা পুরো branch আবার তৈরি করে সমাধান করতে হবে — বেদনাদায়ক, error-prone প্রক্রিয়া।
ব্যতিক্রম: নিজের একার branch
যদি একটা feature branch শুধুমাত্র আপনি একাই কাজ করছেন (কেউ pull করেনি), rebase করে force-push করা সম্পূর্ণ নিরাপদ ও স্বাভাবিক — এটাই বরং PR history পরিষ্কার রাখার প্রস্তাবিত উপায়।
টিম নিয়ম হিসেবে
| Branch টাইপ | Rebase নিরাপদ? |
|---|---|
| নিজের feature branch, কেউ pull করেনি | ✅ হ্যাঁ |
| নিজের feature branch, PR review-এর জন্য push করা, শুধু আপনি push করছেন | ✅ সাধারণত হ্যাঁ (টিমে জানিয়ে) |
main/develop/release branch |
❌ কখনো না |
| shared branch যেখানে একাধিক জন push করছে | ❌ কখনো না |
সংক্ষেপে: rebase local/private history-এর জন্য, merge/revert public/shared history-এর জন্য।
ল্যাব: Interactive Rebase দিয়ে Commit পরিষ্কার করা
লক্ষ্য
একগুচ্ছ এলোমেলো "wip" commit-কে interactive rebase দিয়ে পরিষ্কার, অর্থবহ ইতিহাসে রূপান্তর করা, আর --fixup/--autosquash এবং --onto ব্যবহার করা।
mkdir git-rebase-lab && cd git-rebase-lab && git init
echo "base" > app.py && git add . && git commit -m "initial commit"
git commit --allow-empty -m "wip: start login feature"
git commit --allow-empty -m "wip: more login work"
git commit --allow-empty -m "fix typo in login"
git commit --allow-empty -m "add login tests"
git log --oneline
# ১) interactive rebase দিয়ে squash/fixup/reword করুন
git rebase -i HEAD~4
# এডিটরে বদলান:
# pick <hash1> wip: start login feature → reword করে "add login form"
# fixup <hash2> wip: more login work
# fixup <hash3> fix typo in login
# pick <hash4> add login tests
git log --oneline # এখন মাত্র ২টা পরিষ্কার commit থাকবে
# ২) --fixup ও --autosquash পরীক্ষা
git config --global rebase.autosquash true
echo "form code" > form.py && git add . && git commit -m "add form validation"
echo "extra fix" >> form.py
git commit --fixup=HEAD -am "small fix"
git log --oneline -3 # "fixup! add form validation" commit দেখুন
git rebase -i --autosquash HEAD~3 # save করলেই fixup স্বয়ংক্রিয়ভাবে সাজানো থাকবে
git log --oneline
# ৩) --onto পরীক্ষা
git switch -c feature/a
git commit --allow-empty -m "feature a work 1"
git commit --allow-empty -m "feature a work 2"
git switch -c feature/b
git commit --allow-empty -m "feature b work 1"
git commit --allow-empty -m "feature b work 2"
git switch main
git rebase --onto main feature/a feature/b
git log --oneline feature/b # শুধু feature b-এর ২টা commit, main-এর উপর বসানো
চ্যালেঞ্জ (নিজে করুন)
git rebase -i HEAD~2 চালিয়ে একটা commit-এ edit action সেট করুন, rebase থামলে git commit --amend দিয়ে সেই commit-এর মেসেজ ও কন্টেন্ট দুটোই বদলান, তারপর git rebase --continue দিয়ে শেষ করুন।
ইন্টারভিউ প্রশ্ন — Module 14
প্র.১ — rebase-এর পর commit hash কেন বদলে যায়, যদিও diff একই থাকে? উত্তর: commit hash কন্টেন্টের পাশাপাশি parent hash-এর উপরও নির্ভর করে (SHA checksum-এর অংশ)। rebase প্রতিটা commit-কে নতুন base-এর উপর replay করে, ফলে parent বদলে যায় — parent বদলালেই সম্পূর্ণ নতুন hash তৈরি হয়, যদিও diff/content অপরিবর্তিত থাকতে পারে।
প্র.২ — squash আর fixup-এর পার্থক্য কী?
উত্তর: দুটোই বর্তমান commit-কে আগের commit-এর সাথে মিশিয়ে দেয়। squash দুই commit-এর মেসেজ একসাথে এডিট করার সুযোগ দেয়, fixup বর্তমান commit-এর মেসেজ সম্পূর্ণ বাদ দিয়ে নিঃশব্দে মিশিয়ে ফেলে।
প্র.৩ — --autosquash কীভাবে সময় বাঁচায়?
উত্তর: git commit --fixup=<hash> দিয়ে টার্গেট commit উল্লেখ করে একটা fixup commit তৈরি করা যায়, আর --autosquash চালু থাকলে rebase editor স্বয়ংক্রিয়ভাবে সেই fixup commit-কে সঠিক টার্গেটের ঠিক পরে, সঠিক action সহ বসিয়ে দেখায় — ম্যানুয়াল reorder করার দরকার হয় না।
প্র.৪ — --onto-এর সিনট্যাক্সে তিনটা আর্গুমেন্ট কী বোঝায়?
উত্তর: git rebase --onto <newbase> <oldbase> <branch> — <newbase> হলো নতুন যেখানে replay হবে, <oldbase> হলো replay শুরুর সীমা (exclusive, এই commit বাদ), <branch> হলো কোন branch-এর commit replay হবে।
প্র.৫ — rebase-এর সোনার নিয়ম কী, আর কেন লঙ্ঘন করলে সমস্যা হয়? উত্তর: ইতিমধ্যে push হয়ে যাওয়া এবং অন্য কেউ pull করে ফেলা commit কখনো rebase করা উচিত না। লঙ্ঘন করলে rebase-এর কারণে নতুন hash তৈরি হয়, যা সহকর্মীর local ইতিহাসের সাথে সম্পূর্ণ সম্পর্কহীন হয়ে যায় — পরের pull-এ বিশাল duplicate commit/conflict তৈরি হয়।
প্র.৬ — --rebase-merges কখন প্রয়োজন হয়?
উত্তর: যখন rebase করা branch-এর ভেতরেই merge commit আছে যেগুলোর topology সংরক্ষণ করতে চান। ডিফল্ট rebase merge structure "flatten" করে সব commit-কে linear করে ফেলে; --rebase-merges সেই merge গঠন বজায় রেখে replay করে।
কর্নার কেস — Module 14
git rebase --skipconflict resolve করে না, পুরো commit-ই ফেলে দেয় — অনেকে ভুলে conflict মার্কার এডিট না করেই--skipচালান, ভেবে এটা conflict বাদ দিয়ে এগিয়ে যাবে; আসলে পুরো commit-এর পরিবর্তনই বাদ পড়ে যায়।- empty commit rebase-এর সময় নিঃশব্দে বাদ পড়ে যেতে পারে — যদি একটা commit-এর diff rebase-এর সময় খালি হয়ে যায় (যেমন আগেই একই পরিবর্তন অন্য commit-এ চলে এসেছে), ডিফল্টে Git সেটা বাদ দিয়ে দেয় (
--keep-emptyনা দিলে) — commit history-তে গণনা মিলবে না বলে বিভ্রান্তি হতে পারে। --autostashrebase conflict হলে stash pop স্বয়ংক্রিয়ভাবে না-ও ঘটতে পারে — conflict সমাধানের পরে--continue/--abortকরার পর ম্যানুয়ালিgit stash listচেক করে নিশ্চিত হওয়া উচিত stash ঠিকমতো পপ হয়েছে কিনা।- rebase চলাকালীন
git status"interactive rebase in progress" দেখালেওgit logস্বাভাবিক দেখাতে পারে বিভ্রান্তিকরভাবে — HEAD আসলে detached অবস্থায় থাকে rebase চলাকালীন (.git/rebase-merge/head-nameফাইলে আসল branch নাম সংরক্ষিত থাকে), rebase শেষ হলে তবেই branch ref আপডেট হয়ে আবার attach হয়।
MODULE 15: Cherry-pick
git cherry-pick rebase-এর "replay" মেকানিজমেরই একটা single-commit, targeted সংস্করণ — পুরো branch না, শুধু একটা নির্দিষ্ট commit অন্য branch-এ নিয়ে আসা। এই মডিউলে single/multiple/range cherry-pick, -n/-x/-m ফ্ল্যাগ, conflict flow, আর সবচেয়ে সাধারণ বাস্তব ব্যবহার — hotfix backporting — কভার হবে।
Cherry-pick: Single, Multiple, Range
একটা নির্দিষ্ট commit তুলে আনা
git cherry-pick একটা নির্দিষ্ট commit-এর diff নিয়ে বর্তমান branch-এ সেটা নতুন commit হিসেবে apply করে — অনেকটা rebase-এর একটামাত্র commit replay করার মতো, কিন্তু পুরো branch-এর বদলে ম্যানুয়ালি বেছে নেওয়া।
git switch release/v2.1
git cherry-pick a1b2c3d
# a1b2c3d-এর পরিবর্তন এখন release/v2.1-এ একটা নতুন commit হিসেবে যুক্ত হলো, নতুন hash সহ
একাধিক commit
git cherry-pick a1b2c3d e4f5g6h i7j8k9l
# তিনটা commit একের পর এক apply হবে, নিজস্ব ক্রমে
Range cherry-pick
git cherry-pick main~5..main~2
# main~5 (exclusive) থেকে main~2 (inclusive) পর্যন্ত সব commit apply হবে
সতর্কতা: A..B সিনট্যাক্স A-কে exclude করে, B-কে include করে — অনেকে ভুলে ভাবেন দুটোই inclusive। সঠিকভাবে A-সহ চাইলে A^..B ব্যবহার করতে হবে।
ভেতরে কী ঘটে
প্রতিটা cherry-pick আসলে তিনটা ধাপ: (১) সেই commit-এর tree-কে তার নিজের parent-এর tree-এর সাথে diff করে বের করা কী বদলেছে, (২) সেই diff বর্তমান working tree/index-এ apply করা, (৩) একটা নতুন commit object তৈরি করা — নতুন parent (বর্তমান HEAD), তাই নতুন hash, কিন্তু কন্টেন্ট (diff) মূল commit-এর মতোই।
রিয়েল-লাইফ উদাহরণ
main-এ একটা critical security fix commit হয়েছে, কিন্তু release/v2.1 branch অনেক আগেই main থেকে diverge করেছে — পুরো main merge করা সম্ভব না (অনেক অপ্রাসঙ্গিক পরিবর্তন চলে আসবে)। শুধু সেই একটা fix commit cherry-pick করে release/v2.1-এ আনা হয়।
-n (no-commit) ও -x (রেফারেন্স রাখা)
-n / --no-commit: শুধু apply করা, commit না
git cherry-pick -n a1b2c3d
# পরিবর্তন staged হয়ে যাবে, কিন্তু কোনো commit তৈরি হবে না
git status # "Changes to be committed"
ব্যবহার: একাধিক cherry-pick-এর ফলাফল একসাথে একটা commit-এ combine করতে চাইলে, বা apply করার পর আরও কিছু ম্যানুয়াল পরিবর্তন যোগ করে তারপর একবারে commit করতে চাইলে:
git cherry-pick -n commit1 commit2 commit3
# তিনটা diff-ই staged, এখন একসাথে commit
git commit -m "backport: combine three related fixes"
-x: মূল commit hash-এর রেফারেন্স রাখা
git cherry-pick -x a1b2c3d
এটা নতুন commit message-এর শেষে স্বয়ংক্রিয়ভাবে একটা লাইন যোগ করে:
fix null pointer in payment handler
(cherry picked from commit a1b2c3d4e5f6...)
কেন এটা ভালো প্র্যাকটিস
-x ছাড়া cherry-pick করা commit দেখতে সম্পূর্ণ স্বাধীন, নতুন commit মনে হয় — কেউ পরে জানতে পারবে না এটা কোথা থেকে এসেছে। -x দিয়ে ট্রেসেবিলিটি বজায় থাকে: git log --grep="cherry picked from" দিয়ে খুঁজে বের করা যায় কোন কোন commit backport হয়েছে, বা main-এর একটা commit release-branch-এ পৌঁছেছে কিনা যাচাই করা যায় সহজে। Open-source প্রজেক্টে (Linux kernel-সহ) এটা একটা প্রায়-বাধ্যতামূলক convention।
-m (mainline) — Merge Commit Cherry-pick করা
সমস্যা: merge commit-এর দুটো parent
সাধারণ commit-এর diff স্পষ্ট (parent-এর সাথে তুলনা)। কিন্তু merge commit-এর দুটো parent থাকে — কোন parent-এর সাপেক্ষে diff নেওয়া হবে সেটা নির্দিষ্ট না করলে Git জানে না কী apply করতে হবে।
git cherry-pick <merge-commit-hash>
# error: commit <hash> is a merge but no -m option was given.
সমাধান: -m দিয়ে mainline নির্দিষ্ট করা
git cherry-pick -m 1 <merge-commit-hash>
-m 1 মানে parent নাম্বার ১-কে ভিত্তি (mainline) ধরে diff বের করা হবে — সাধারণত parent 1 হলো যে branch-এ merge করা হয়েছিলো, আর parent 2 হলো যেটা merge করা হয়েছিলো। -m 1 দিলে merge commit-এ merge হওয়া branch-এর যোগ করা পরিবর্তনগুলোই apply হয়।
কখন প্রয়োজন হয়
একটা পুরো feature (merge commit হিসেবে main-এ এসেছে) সম্পূর্ণভাবে অন্য একটা release branch-এ backport করতে চাইলে, পুরো feature branch cherry-pick না করে শুধু সেই merge commit-টাই cherry-pick করা যায় -m 1 দিয়ে — merge-এর মধ্যে থাকা সব পরিবর্তন একসাথে একটা নতুন commit হিসেবে চলে আসবে।
সতর্কতা
merge commit cherry-pick করা জটিল হতে পারে যদি সেই merge-এর ভেতরে নিজেই conflict resolution ছিলো — -m দিয়ে বেছে নেওয়া mainline-এর ভুল দিক দিলে সম্পূর্ণ ভুল diff apply হয়ে যেতে পারে, তাই ব্যবহারের আগে git show <merge-hash> দিয়ে parent সংখ্যা ও দিক ভালোভাবে বুঝে নেওয়া জরুরি।
Conflict Flow — --continue/--abort/--skip
Cherry-pick-এও conflict হতে পারে
যেহেতু cherry-pick ভেতরে diff apply করার একটা প্রক্রিয়া (অনেকটা 3-way merge-এর মতো, commit আর তার parent-এর diff বর্তমান tree-তে apply করা), টার্গেট branch-এ একই লাইনে ভিন্ন পরিবর্তন থাকলে conflict দেখা দিতে পারে — ঠিক merge conflict-এর মতোই marker সহ।
git cherry-pick a1b2c3d
# CONFLICT (content): Merge conflict in payment.py
# error: could not apply a1b2c3d... fix null pointer
তিনটা পথ
# marker এডিট করে সমাধানের পর:
git add payment.py
git cherry-pick --continue
# এই cherry-pick সম্পূর্ণ বাদ দিয়ে (একাধিক cherry-pick চলাকালীন) পরেরটায় যাওয়া:
git cherry-pick --skip
# পুরো cherry-pick অপারেশন বাতিল, শুরুর আগের অবস্থায় ফেরা:
git cherry-pick --abort
একাধিক cherry-pick-এ conflict সিকোয়েন্স
git cherry-pick a1b2c3d e4f5g6h i7j8k9l
# a1b2c3d সফল
# e4f5g6h-এ conflict — resolve করে --continue করলে i7j8k9l-এ এগোবে
.git/CHERRY_PICK_HEAD ফাইলে চলমান cherry-pick-এর অবস্থা ট্র্যাক থাকে — git status চালালে "You are currently cherry-picking commit..." মেসেজ দেখা যায়, যা confirm করে কোন commit-এর প্রক্রিয়া চলছে।
রিয়েল-লাইফ ব্যবহার: Hotfix Backporting
সবচেয়ে সাধারণ বাস্তব দৃশ্য
একটা প্রোডাকশন incident ধরা পড়েছে — একটা critical bug। ফিক্স main branch-এ তৈরি ও merge হয়। কিন্তু production-এ এখন চলছে release/v2.1 branch, যেটা main-এর অনেক পুরনো একটা snapshot থেকে branch হয়েছিলো এবং তারপর main-এ অনেক নতুন, অসম্পূর্ণ ফিচার যোগ হয়ে গেছে যেগুলো এখনো production-এ যাওয়ার জন্য প্রস্তুত না।
# hotfix main-এ merge হয়েছে হিসেবে ধরে নিন hash: f1a2b3c
git switch release/v2.1
git cherry-pick -x f1a2b3c
# শুধু এই ফিক্স-টাই release/v2.1-এ এলো, main-এর অন্য কোনো অসম্পূর্ণ কাজ ছাড়াই
git push origin release/v2.1
# এখন হটফিক্স deploy করা যাবে production-এ, বাকি অস্থির ফিচার ছাড়াই
কেন merge না, cherry-pick
যদি এই মুহূর্তে git merge main করা হতো release/v2.1-এ, তাহলে main-এর সব অসম্পূর্ণ/অপরীক্ষিত ফিচার একসাথে release-এ চলে আসতো — একটা QA-না-করা অবস্থায় production-এ deploy করার ঝুঁকি। Cherry-pick সুনির্দিষ্টভাবে শুধু প্রয়োজনীয় একটা commit বেছে আনার সুযোগ দেয়, বাকি সব ইতিহাস থেকে আলাদা রেখে।
একাধিক release branch-এ একই fix
Multi-version সাপোর্ট করা প্রজেক্টে (যেমন একই সময়ে v2.1, v2.2, v3.0 branch maintain করা), একটা security fix সাধারণত main-এ প্রথমে যায়, তারপর -x সহ প্রতিটা active release branch-এ আলাদাভাবে cherry-pick হয় — প্রতিটাতে ট্রেসেবিলিটি বজায় রেখে।
ল্যাব: Hotfix Cherry-pick হাতে-কলমে
লক্ষ্য
একটা main branch থেকে একটা নির্দিষ্ট bugfix commit release branch-এ cherry-pick করা, conflict সহ ও ছাড়া দুটো দৃশ্যই দেখা।
mkdir git-cherry-lab && cd git-cherry-lab && git init
echo "def process(): return 1" > payment.py
git add . && git commit -m "initial payment logic"
# release branch তৈরি (পুরনো snapshot হিসেবে)
git branch release/v1.0
# main-এ এগিয়ে যান — কয়েকটা ফিচার commit + একটা bugfix
echo "def process(): return None # BUG!" > payment.py
git commit -am "feature: refactor payment (introduces bug)"
sed -i 's/return None # BUG!/return 0 # fixed null bug/' payment.py
git commit -am "fix: null pointer bug in payment processing"
FIX_HASH=$(git rev-parse HEAD)
echo "def new_feature(): pass" >> payment.py
git commit -am "feature: add unrelated new feature"
git log --oneline
# ১) শুধু fix commit release-এ backport করুন
git switch release/v1.0
git cherry-pick -x $FIX_HASH
git log --oneline # cherry-pick commit + "(cherry picked from...)" নোট সহ
cat payment.py # শুধু fix আছে, unrelated feature নেই
# ২) conflict সহ দৃশ্য তৈরি করুন
git switch main
sed -i 's/return 0 # fixed null bug/return 0 # v2 style fix/' payment.py
git commit -am "refine fix message style"
CONFLICT_HASH=$(git rev-parse HEAD)
git switch release/v1.0
sed -i 's/return 0 # fixed null bug/return -1 # release specific value/' payment.py
git commit -am "release-specific tweak"
git cherry-pick $CONFLICT_HASH
# CONFLICT হবে — marker দেখুন, সমাধান করুন
cat payment.py
# marker resolve করে:
git add payment.py
git cherry-pick --continue
চ্যালেঞ্জ (নিজে করুন)
git cherry-pick -n দিয়ে দুটো ভিন্ন commit no-commit মোডে apply করে, তারপর একসাথে একটা মাত্র commit বানিয়ে দেখুন কীভাবে দুটো আলাদা fix একটা "combined backport" commit হয়ে যায়।
ইন্টারভিউ প্রশ্ন — Module 15
প্র.১ — cherry-pick আর rebase-এর মধ্যে conceptual মিল কী? উত্তর: দুটোই "replay" মেকানিজম — একটা কমিটের diff নিয়ে নতুন parent-এর উপর নতুন commit হিসেবে apply করা। rebase পুরো branch-এর একাধিক commit ক্রমান্বয়ে replay করে, cherry-pick একটা বা নির্বাচিত কয়েকটা নির্দিষ্ট commit replay করে।
প্র.২ — -x ফ্ল্যাগ কেন ব্যবহার করা ভালো প্র্যাকটিস?
উত্তর: এটা নতুন commit message-এ "(cherry picked from commit ...)" লাইন যোগ করে ট্রেসেবিলিটি বজায় রাখে — পরে বোঝা যায় কোন commit কোথা থেকে এসেছে, যা multi-branch backport ট্র্যাক করতে গুরুত্বপূর্ণ।
প্র.৩ — merge commit cherry-pick করতে -m ফ্ল্যাগ কেন বাধ্যতামূলক?
উত্তর: merge commit-এর দুটো parent থাকে, তাই কোন parent-এর সাপেক্ষে diff নেওয়া হবে তা স্পষ্ট না করলে Git জানে না কী apply করতে হবে। -m 1/-m 2 দিয়ে mainline নির্দিষ্ট করতে হয়।
প্র.৪ — A..B range cherry-pick-এ A ইনক্লুড হয় কি?
উত্তর: না, A..B A-কে exclude করে, B পর্যন্ত (inclusive) সব commit নেয়। A-সহ চাইলে A^..B ব্যবহার করতে হয়।
প্র.৫ — hotfix backport-এ merge না করে cherry-pick করা হয় কেন? উত্তর: merge করলে source branch-এর সব ইতিহাস (সব অসম্পূর্ণ/অপরীক্ষিত ফিচার সহ) চলে আসবে টার্গেট branch-এ। cherry-pick শুধু নির্দিষ্ট, প্রয়োজনীয় একটা commit বেছে আনে, বাকি অপ্রাসঙ্গিক ইতিহাস থেকে বিচ্ছিন্ন রেখে।
প্র.৬ — -n ফ্ল্যাগ কখন কাজে লাগে?
উত্তর: যখন একাধিক commit apply করে সেগুলোকে একসাথে একটা মাত্র commit-এ combine করতে চান, বা apply করার পর অতিরিক্ত ম্যানুয়াল পরিবর্তন যোগ করে একবারে commit করতে চান — -n commit স্কিপ করে শুধু staging পর্যন্ত করে।
কর্নার কেস — Module 15
- cherry-pick করা commit এবং মূল commit — দুটোর diff একই হলেও hash সম্পূর্ণ ভিন্ন —
git log --allদুটোকেই আলাদা, স্বাধীন commit হিসেবে দেখাবে,-xছাড়া কোনো লিংক দৃশ্যমান হবে না, ফলে duplicate work মনে হতে পারে যদি ট্রেসেবিলিটি না রাখা হয়। - একই commit দুইবার cherry-pick করলে দ্বিতীয়বার conflict বা "empty commit" হতে পারে — Git ধরতে পারে diff ইতিমধ্যে apply হয়ে গেছে, তখন
git cherry-pick --allow-emptyবা--skipদরকার হতে পারে অবস্থাভেদে। git cherry-pickচলাকালীনgit status"cherry-pick in progress" দেখাবে কিন্তু HEAD detached হয় না — cherry-pick rebase-এর থেকে ভিন্ন, এটা বর্তমান branch-এই সরাসরি নতুন commit যোগ করে, তাই branch attach-ই থাকে; conflict হলে branch-ই থেকে যায় resolve করার জন্য।- range cherry-pick-এ merge commit থাকলে ডিফল্টে ব্যর্থ হবে — প্রতিটা merge commit-এর জন্য আলাদাভাবে
-mলাগবে, নাহলে পুরো range cherry-pick মাঝপথে থেমে যাবে সেই merge commit-এ এসে।
MODULE 16: Reflog সেফটি নেট
এই Phase-এর শেষ মডিউল, এবং একটা অর্থে সবচেয়ে গুরুত্বপূর্ণ — কারণ git reflog হলো সেই সেফটি নেট যা আগের সব মডিউলের (reset --hard, rebase, cherry-pick conflict, detached HEAD) ভুল থেকে উদ্ধার করার শেষ ভরসা। কভার হবে: reflog কী ও কীভাবে HEAD movement ট্র্যাক করে, HEAD@{n} সিনট্যাক্স, একটা সম্পূর্ণ recovery ওয়াকথ্রু, expiry policy, আর git fsck --lost-found — reflog-ও ব্যর্থ হলে শেষ উপায়। মডিউলের শেষে Phase 4-এর একটা checkpoint gate থাকবে।
git reflog — HEAD Movement-এর ইতিহাস
reflog কী
git reflog হলো একটা লোকাল লগ যা HEAD (এবং প্রতিটা branch) কোথায় কোথায় গিয়েছে তার প্রতিটা পদক্ষেপ রেকর্ড করে রাখে — commit, checkout, merge, rebase, reset, cherry-pick — HEAD যেকোনো কারণে move করলেই একটা entry যোগ হয়।
git reflog
# 7a1f9c2 (HEAD -> main) HEAD@{0}: commit: fix payment bug
# 3b8e0a1 HEAD@{1}: reset: moving to HEAD~1
# 9d2e7b1 HEAD@{2}: commit: add validation
# 5c1a0f3 HEAD@{3}: checkout: moving from feature/x to main
এটা commit history থেকে সম্পূর্ণ ভিন্ন জিনিস
git log দেখায় commit graph-এর ancestry — কোন commit কোন commit-এর parent। git reflog দেখায় সময়ের ক্রমানুসারে HEAD আক্ষরিকভাবে কোথায় কোথায় ছিলো — এটা একটা "activity log", ইতিহাসের গ্রাফ না। তাই একটা reset --hard-এর পর git log আর সেই "হারানো" commit দেখাবে না, কিন্তু git reflog-এ সেটা এখনো একটা entry হিসেবে থেকে যায়।
Branch-ভিত্তিক reflog আলাদা
শুধু HEAD-এরই না, প্রতিটা branch ref-এরও নিজস্ব reflog আছে:
git reflog show main
git reflog show feature/x
৯০ দিন ডিফল্ট expiry
reflog entry চিরকাল থাকে না — ডিফল্ট কনফিগ (gc.reflogExpire=90 days reachable commit-এর জন্য, gc.reflogExpireUnreachable=30 days unreachable commit-এর জন্য) অনুযায়ী পুরনো entry স্বয়ংক্রিয়ভাবে git gc চলাকালীন মুছে যায়। এইজন্যই reflog একটা সাময়িক সেফটি নেট, স্থায়ী ব্যাকআপ না।
HEAD@{n} সিনট্যাক্স
রেফারেন্সিং সিনট্যাক্স
HEAD@{n} মানে HEAD এখন যেখানে আছে তার থেকে n ধাপ পেছনের reflog entry। এটা HEAD~n (commit ancestry-তে n ধাপ পেছনে) থেকে সম্পূর্ণ ভিন্ন জিনিস — একটা সময়-ভিত্তিক (reflog), আরেকটা গ্রাফ-ভিত্তিক (parent chain)।
git reflog
# HEAD@{0}: commit: fix bug
# HEAD@{1}: reset: moving to HEAD~2
# HEAD@{2}: rebase (finish): returning to refs/heads/main
# HEAD@{3}: commit: wip work
git checkout HEAD@{1} # reset চালানোর ঠিক আগের অবস্থায় ফিরে যাওয়া
git diff HEAD@{3} HEAD@{0} # রিবেসের আগে ও পরে তুলনা
সময়-ভিত্তিক সিনট্যাক্সও সম্ভব
git checkout 'HEAD@{5 minutes ago}'
git checkout 'HEAD@{yesterday}'
git checkout 'main@{2.days.ago}'
Git প্রতিটা reflog entry-র সাথে একটা timestamp রাখে, তাই human-readable সময় দিয়েও নির্দিষ্ট মুহূর্তের অবস্থা খুঁজে বের করা যায় — "গতকাল দুপুরে কোড কেমন ছিলো" জাতীয় প্রশ্নের উত্তর দিতে দরকারি।
branch-নির্দিষ্ট রেফারেন্স
git reflog show feature/x
git checkout feature/x@{2} # feature/x branch-এর reflog-এর ২ ধাপ আগের অবস্থা
HEAD@{n} আর <branch>@{n} — দুটোই একই সিনট্যাক্স প্যাটার্ন, শুধু কোন ref-এর reflog দেখা হচ্ছে সেটা আলাদা।
ল্যাব: হারানো কমিট রিকভারি — সম্পূর্ণ ওয়াকথ্রু
দৃশ্য: ভুল করে reset --hard চালিয়ে ফেলা
mkdir git-reflog-lab && cd git-reflog-lab && git init
echo "v1" > work.txt && git add . && git commit -m "commit 1"
echo "v2" > work.txt && git commit -am "commit 2 - important feature"
echo "v3" > work.txt && git commit -am "commit 3 - another important fix"
git log --oneline
# c3d4e5f commit 3 - another important fix
# b2c3d4e commit 2 - important feature
# a1b2c3d commit 1
# ভুল করে দুই কমিট পিছিয়ে যাওয়া!
git reset --hard HEAD~2
git log --oneline
# a1b2c3d commit 1 ← আতঙ্ক! commit 2 আর 3 কোথায়?
ধাপে ধাপে রিকভারি
# ধাপ ১: reflog দেখুন
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~2
# c3d4e5f HEAD@{1}: commit: commit 3 - another important fix ← এটাই দরকার!
# b2c3d4e HEAD@{2}: commit: commit 2 - important feature
# a1b2c3d HEAD@{3}: commit: commit 1
# ধাপ ২: হারানো commit-এর hash শনাক্ত করুন (c3d4e5f, HEAD@{1})
git show c3d4e5f --stat # নিশ্চিত হন এটাই সঠিক commit
# ধাপ ৩: HEAD সেই অবস্থায় ফিরিয়ে নিন
git reset --hard HEAD@{1}
# অথবা সরাসরি hash দিয়ে:
git reset --hard c3d4e5f
git log --oneline
# c3d4e5f commit 3 - another important fix ← ফিরে এসেছে!
# b2c3d4e commit 2 - important feature
# a1b2c3d commit 1
বিকল্প: নতুন branch দিয়ে নিরাপদে রিকভার
যদি নিশ্চিত না থাকেন বর্তমান branch-এ সরাসরি reset করা ঠিক হবে কিনা, বরং একটা আলাদা branch-এ commit-টা বাঁচিয়ে রাখুন:
git branch recovered-work c3d4e5f
git log --oneline recovered-work
এই ওয়াকথ্রু-টাই প্রমাণ করে কেন Phase 4-এর প্রথম কথাটা সত্য: reset --hard "ভয়ংকর" মনে হলেও, reflog-এর কারণে এটা প্রায় সবসময় রিভার্সিবল — যতক্ষণ পর্যন্ত git gc reflog expire করে না দেয়।
git reflog expire ও Retention নিয়ন্ত্রণ
reflog চিরস্থায়ী না
ডিফল্ট কনফিগারেশন:
git config --get gc.reflogExpire # সাধারণত 90 days (reachable commit)
git config --get gc.reflogExpireUnreachable # সাধারণত 30 days (unreachable commit)
git gc (স্বয়ংক্রিয়ভাবে মাঝেমধ্যে চলে, অথবা ম্যানুয়ালি git gc চালানো যায়) এই সময়সীমা পার হওয়া reflog entry মুছে দেয়, আর তারপর সেই entry-র সাথে যুক্ত unreachable commit object-ও (যদি অন্য কোনো ref থেকে reachable না হয়) স্থায়ীভাবে মুছে ফেলা হয়।
ম্যানুয়ালি নিয়ন্ত্রণ করা
git reflog expire --expire=now --all
# সব reflog entry অবিলম্বে expire করা (বিপজ্জনক — টেস্টিং/ক্লিনআপের জন্য)
git reflog expire --expire-unreachable=now refs/heads/main
# শুধু main branch-এর unreachable entry-গুলো এক্ষুনি expire করা
দীর্ঘমেয়াদী প্রকল্পে retention বাড়ানো
কিছু টিম দীর্ঘমেয়াদী safety net চেয়ে expiry বাড়িয়ে দেয়:
git config --global gc.reflogExpire "180 days"
git config --global gc.reflogExpireUnreachable "90 days"
বাস্তব প্রভাব
মনে রাখা জরুরি: একটা "হারানো" commit রিকভার করার একটা সময়সীমা আছে। একটা ভুল করার সাথে সাথে reflog দিয়ে ঠিক করে নেওয়া সবসময় সবচেয়ে নিরাপদ — মাসের পর মাস অপেক্ষা করলে সেই সেফটি নেটও উবে যেতে পারে git gc চলার পর।
git fsck --lost-found — শেষ উপায়
যখন reflog-ও কাজে আসে না
যদি reflog-এও কোনো entry না থাকে (যেমন reflog disabled ছিলো, bare repository-তে ডিফল্টে reflog off থাকে, অথবা expire হয়ে গেছে), তবুও একটা শেষ উপায় আছে — dangling object খোঁজা।
dangling object কী
Git কখনো commit object সাথে সাথে মুছে দেয় না, এমনকি কোনো ref থেকে reachable না হলেও — যতক্ষণ না git gc চলে এবং grace period (ডিফল্ট ~২ সপ্তাহ, gc.pruneExpire) পার হয়। এই সময়ের মধ্যে সেই object "dangling" অবস্থায় ডিস্কে পড়ে থাকে — কোনো ref এটাকে পয়েন্ট করছে না, কিন্তু object database-এ এখনো আছে।
git fsck --lost-found
# dangling commit c3d4e5f1a2b3...
# dangling blob 9f8e7d6c5b4a...
রিকভারি
git show c3d4e5f1a2b3 # কন্টেন্ট দেখে যাচাই করুন এটাই আপনার হারানো commit কিনা
git branch recovered c3d4e5f1a2b3 # নতুন branch তৈরি করে বাঁচিয়ে ফেলুন
dangling blob-এর ক্ষেত্রে (যেমন git add করা কিন্তু কখনো commit না হওয়া ফাইল), সরাসরি কন্টেন্ট বের করা যায়:
git cat-file -p 9f8e7d6c5b4a > recovered-file.txt
সীমাবদ্ধতা
git fsck --lost-found শুধু তখনই কাজ করে যখন object এখনো .git/objects-এ পুরোপুরি garbage-collect হয়নি। git gc --aggressive বা অনেক দিন পার হলে এটাও ব্যর্থ হবে — এটাই একদম শেষ, সীমিত-সময়ের সেফটি নেট, প্রথম পছন্দ কখনোই না (প্রথমে সবসময় git reflog চেক করুন)।
কেন Reflog লোকাল-ওনলি — কখনো Push হয় না
মূল কথা
git reflog সম্পূর্ণভাবে লোকাল — এটা .git/logs/ ডিরেক্টরিতে সংরক্ষিত থাকে, যেটা .git/objects-এর মতো কন্টেন্ট-অ্যাড্রেসেবল shared object store না, বরং প্রতিটা developer-এর নিজস্ব মেশিনের একটা ব্যক্তিগত activity log। git push/git fetch/git clone — এসব অপারেশন কখনোই reflog ট্রান্সফার করে না।
কেন এই ডিজাইন সিদ্ধান্ত
- reflog সাইজ ও প্রাসঙ্গিকতা ব্যক্তিগত — একজন developer-এর ১০০টা
reset/checkoutmovement আরেকজনের কাছে অর্থহীন এবং অপ্রয়োজনীয় ডেটা। - প্রাইভেসি — reflog দেখায় আপনি ঠিক কী কী পরীক্ষা-নিরীক্ষা করেছেন, কতবার ভুল করেছেন, কোন detached HEAD এক্সপ্লোর করেছেন — এগুলো ব্যক্তিগত workflow তথ্য, remote-এ শেয়ার করার প্রয়োজন নেই।
- Object model-এর সাথে সামঞ্জস্যপূর্ণ না — Git-এর মূল shared ডেটা হলো commit/tree/blob object আর ref — এগুলো immutable, ডিটারমিনিস্টিক hash দিয়ে চেনা যায়। reflog একটা mutable, সময়-ক্রমিক লগ — সম্পূর্ণ ভিন্ন ধরনের ডেটা, শেয়ার করার জন্য উপযুক্ত মডেল না।
ব্যবহারিক ফলাফল
একজন সহকর্মীর মেশিনে হারানো কমিট আপনার reflog দিয়ে রিকভার করতে পারবেন না — যদি সেই commit কখনো push না হয়ে থাকে, সেটা শুধু তার মেশিনের local object store-এ আছে, আপনার কাছে কোনো ট্রেসও নেই।
এইজন্যই গুরুত্বপূর্ণ কাজ নিয়মিত push করে রাখা উচিত (এমনকি একটা draft/wip branch-এ হলেও) — reflog শুধু নিজের ভুল থেকে নিজেকে বাঁচানোর টুল, টিমের ব্যাকআপ সিস্টেম না।
ল্যাব: Reflog Expiry ও fsck --lost-found পরীক্ষা
লক্ষ্য
reflog-এর retention behavior বোঝা এবং dangling object খুঁজে বের করার প্র্যাকটিস।
mkdir git-reflog-expiry-lab && cd git-reflog-expiry-lab && git init
echo "v1" > file.txt && git add . && git commit -m "commit 1"
echo "v2" > file.txt && git commit -am "commit 2"
LOST_HASH=$(git rev-parse HEAD)
git reset --hard HEAD~1
# ১) reflog-এ এখনো commit 2 দেখা যাচ্ছে কিনা যাচাই করুন
git reflog | grep "commit 2"
# ২) branch থেকে reflog-এর সম্পর্ক ভাঙুন (dangling অবস্থা সিমুলেট)
git reflog expire --expire=now --all
git reflog | grep "commit 2" # আর দেখাবে না
# ৩) কিন্তু object এখনো object database-এ আছে কিনা দেখুন
git cat-file -t $LOST_HASH # "commit" দেখাবে — এখনো আছে!
git fsck --lost-found
# dangling commit <LOST_HASH>
# ৪) fsck দিয়ে রিকভার করুন
git show $LOST_HASH --stat
git branch recovered-via-fsck $LOST_HASH
git log --oneline recovered-via-fsck
# ৫) dangling blob-ও পরীক্ষা করুন
echo "never committed" > staged-only.txt
git add staged-only.txt
BLOB_HASH=$(git rev-parse :staged-only.txt)
git reset --hard HEAD # staged ফাইল হারিয়ে গেলো working dir থেকেও
git cat-file -p $BLOB_HASH # কিন্তু blob content এখনো উদ্ধারযোগ্য!
চ্যালেঞ্জ (নিজে করুন)
git reflog expire --expire=now --all চালানোর পর সাথে সাথে git gc --prune=now চালিয়ে দেখুন dangling commit-টা কি এবার সত্যিই permanently মুছে যায় এবং git fsck --lost-found আর কিছু দেখায় না — এটাই "reflog + gc" মিলে সত্যিকারের ডেটা ধ্বংসের শেষ ধাপ।
ইন্টারভিউ প্রশ্ন — Module 16
প্র.১ — git reflog আর git log-এর মৌলিক পার্থক্য কী?
উত্তর: git log commit ancestry (parent-child গ্রাফ) দেখায়। git reflog সময়ের ক্রমানুসারে HEAD/branch ref আক্ষরিকভাবে কোথায় কোথায় ছিলো তার activity log দেখায় — reset, checkout, rebase, commit সব movement সহ, এমনকি এখন unreachable commit-ও।
প্র.২ — git reset --hard-এর পর হারানো কমিট কীভাবে ধাপে ধাপে ফিরে পাবেন?
উত্তর: প্রথমে git reflog চালিয়ে হারানো commit-এর hash/HEAD@{n} খুঁজে বের করুন, git show <hash> দিয়ে যাচাই করুন এটাই সঠিক commit কিনা, তারপর git reset --hard <hash> (বা নিরাপদে git branch recovered <hash>) দিয়ে সেটা ফিরিয়ে আনুন বা বাঁচিয়ে রাখুন।
প্র.৩ — reflog entry কি চিরকাল থাকে?
উত্তর: না, ডিফল্টে reachable commit-এর entry ৯০ দিন (gc.reflogExpire) এবং unreachable commit-এর entry ৩০ দিন (gc.reflogExpireUnreachable) পর git gc চলাকালীন expire হয়ে যায়।
প্র.৪ — reflog-ও যদি expire হয়ে যায়, তাহলে রিকভারির আর কোনো উপায় আছে কি?
উত্তর: হ্যাঁ, git fsck --lost-found দিয়ে dangling commit/blob object খোঁজা যায় — যদি সেগুলো এখনো git gc-এর prune grace period (ডিফল্ট প্রায় ২ সপ্তাহ) পার না হয়ে থাকে। এটা একদম শেষ উপায়, নিশ্চিত না।
প্র.৫ — reflog কি push/clone-এর মাধ্যমে অন্য কারো কাছে যায়?
উত্তর: না, reflog সম্পূর্ণ লোকাল (.git/logs/-এ), কখনো push/fetch/clone-এর অংশ হয় না। এইজন্য একজনের local, un-pushed commit হারিয়ে গেলে অন্য কারো reflog দিয়ে সেটা রিকভার করা যায় না।
প্র.৬ — HEAD@{2} আর HEAD~2-এর পার্থক্য কী?
উত্তর: HEAD@{2} হলো reflog-এর ২ ধাপ আগের entry (সময়-ভিত্তিক, HEAD আক্ষরিকভাবে যেখানে ছিলো)। HEAD~2 হলো commit ancestry-তে বর্তমান commit থেকে ২ ধাপ আগের parent (গ্রাফ-ভিত্তিক)। দুটো সম্পূর্ণ ভিন্ন এবং প্রায়ই আলাদা commit নির্দেশ করে।
কর্নার কেস — Module 16
- bare repository-তে reflog ডিফল্টে বন্ধ থাকে (
core.logAllRefUpdates=falseডিফল্ট bare repo-তে) — server-side bare repo-তে reflog-নির্ভর রিকভারি আশা করলে হতাশ হতে হবে; সার্ভারে হারানো ডেটার জন্য backup/fsck-ই একমাত্র ভরসা। git reflog expire --expire=now --allটেস্টিং-এর জন্য চালালে বাস্তব প্রোডাকশন রিপোতে ভুলে চালানো বিপর্যয়কর — এটা সাথে সাথে সব reflog entry মুছে দেয়, সেফটি নেট নিজেই উবে যায়; এই কমান্ড শুধু শেখা/পরীক্ষার sandbox repo-তে চালানো উচিত।git gcস্বয়ংক্রিয়ভাবেও চলতে পারে (নির্দিষ্ট threshold-এর পর loose object সংখ্যা বেশি হলে) — মানে reflog expiry সবসময় ম্যানুয়াল কমান্ডের ফল না, দৈনন্দিন ব্যবহারেও নিঃশব্দে ঘটতে পারে, তাই "সময় থাকতে রিকভার করুন" নীতি বাস্তবিকই গুরুত্বপূর্ণ।git fsck --lost-foundফলাফল দুই ধরনের ফোল্ডারে সংরক্ষিত হয় —.git/lost-found/commit/আর.git/lost-found/other/— পুরনো Git ভার্সনে এই ফাইলে সরাসরি ফলাফল লেখা হতো; আধুনিক Git-এ টার্মিনাল আউটপুটেই hash দেখানো হয়, কিন্তু কিছু পুরনো ডকুমেন্টেশনে এই ফোল্ডার-ভিত্তিক আচরণের রেফারেন্স এখনো দেখা যায়, যা বিভ্রান্তির কারণ হতে পারে।
চেকপয়েন্ট — Phase 4 সম্পন্ন
আপনি কি পারবেন?
Phase 4 শেষ করার আগে নিজেকে সৎভাবে যাচাই করুন — এই প্রশ্নগুলোর উত্তর "হ্যাঁ, ব্যাখ্যা করতে পারি" হওয়া উচিত:
- একটা ভুল করে push করা
git reset --hardথেকে কি reflog দিয়ে ধাপে ধাপে রিকভার করতে পারবেন — reflog দেখা, সঠিক entry শনাক্ত করা, আর ফিরিয়ে আনা? - fast-forward আর 3-way merge-এর মধ্যে পার্থক্য এবং কোনটা কখন ঘটে তা কি একটা উদাহরণ দিয়ে ব্যাখ্যা করতে পারবেন?
- merge আর rebase-এ "ours"/"theirs"-এর অর্থ কেন উল্টে যায় তা কি স্পষ্টভাবে কাউকে বোঝাতে পারবেন?
reset --soft/--mixed/--hardএর মধ্যে HEAD, index, আর working directory-তে ঠিক কী প্রভাব পড়ে তা টেবিল আকারে মনে করতে পারবেন?- কেন shared/pushed branch-এ কখনো rebase বা reset --hard করা উচিত না, আর তার বদলে কী ব্যবহার করা উচিত তা কি ব্যাখ্যা করতে পারবেন?
- হাতে-কলমে একটা hotfix commit
cherry-pick -xদিয়ে একটা release branch-এ backport করতে পারবেন, প্রয়োজনে conflict সামলে?
কেন এই Phase-টা সবচেয়ে মূল্যবান
Phase 4-এর ছয়টা মডিউল একসাথে গঠন করে "undo mastery" — Git-এ ভুল করা অনিবার্য (ভুল branch-এ commit, ভুল merge, ভুল rebase, ভুল reset), কিন্তু আসল দক্ষতা হলো ভয় না পেয়ে, ঠান্ডা মাথায়, সঠিক টুল (reflog, revert, cherry-pick, --abort) দিয়ে যেকোনো ভুল থেকে ফিরে আসতে পারা। এটাই সেই একক দক্ষতা যা একজন নতুন Git ব্যবহারকারীকে একজন আত্মবিশ্বাসী, ভয়হীন Git ব্যবহারকারীতে রূপান্তরিত করে — কারণ জানা থাকে, প্রায় সবকিছুই আসলে reversible।
PHASE 5: Remote Collaboration
এতক্ষণ যা শেখা হয়েছে তার প্রায় সবটাই একটা single local repository-র ভেতরে ঘটেছে — commit, branch, merge, rebase। কিন্তু বাস্তব জীবনে Git-এর আসল শক্তি প্রকাশ পায় যখন একাধিক মানুষ, একাধিক মেশিন থেকে, একই history-তে অবদান রাখে। এই Phase-এ remote কনসেপ্ট — কীভাবে একটা local repo অন্য একটা repo-র (GitHub, GitLab, বা একটা bare repo) সাথে কথা বলে, ডেটা আদান-প্রদান করে, আর কনফ্লিক্ট এড়িয়ে টিমওয়ার্ক করে — এই পুরো মেকানিজম খোলা হবে।
তিনটা মডিউল: Module 17-এ remote কী এবং কীভাবে রেজিস্টার হয় (internally .git/config আর refs/remotes/), Module 18-এ ডেটা সিঙ্ক করার তিনটা মূল কমান্ড — fetch, pull, push — এবং তাদের বিপজ্জনক কোণাগুলো (force push!), আর Module 19-এ টিমগুলো বাস্তবে কীভাবে branch organize করে (Gitflow, trunk-based, forking) সেই প্যাটার্নগুলো। এই Phase শেষ হলে যেকোনো টিমের Git workflow-তে confidently যোগ দেওয়া যাবে।
PHASE 6: History Exploration ও Forensics
একটা repository যত পুরনো হয়, তার history তত বেশি মূল্যবান — কিন্তু শুধু তখনই, যখন সেই history থেকে তথ্য বের করার হাতিয়ার আপনার হাতে আছে। এই Phase পুরোপুরি ফরেনসিক — কে, কখন, কেন একটা লাইন বদলেছে; কোন commit-এ একটা bug ঢুকেছে; দুইটা commit-এর মধ্যে ঠিক কী পরিবর্তন হয়েছে — এই প্রশ্নগুলোর উত্তর কীভাবে সেকেন্ডে বের করা যায়।
Module 20-এ git log-এর পুরো ফ্ল্যাগ ফ্যামিলি (filtering, pickaxe search সহ), Module 21-এ show/diff/blame দিয়ে নির্দিষ্ট commit বা লাইনের গভীরে যাওয়া, আর Module 22-এ git bisect — একটা বাইনারি-সার্চ অ্যালগরিদম যা হাজার commit-এর মধ্যে থেকে ঠিক কোনটা bug ঢুকিয়েছে তা O(log n) সময়ে খুঁজে বের করে। এই তিনটা মডিউল একসাথে একটা senior engineer-কে জুনিয়র থেকে আলাদা করে দেয় — "কোথায় bug আছে" জানার চেয়ে "কীভাবে খুঁজে বের করব" জানাটাই আসল দক্ষতা।
PHASE 7: Tagging, Releases, Stashing
এই Phase দুইটা প্র্যাকটিক্যাল কিন্তু ভিন্ন সমস্যা সমাধান করে। প্রথমটা — একটা নির্দিষ্ট commit-কে "এটাই v2.0.0 রিলিজ" বলে স্থায়ীভাবে চিহ্নিত করা রাখার দরকার হয় release management-এ; Module 23-এ tag-এর দুই প্রকার (lightweight ও annotated), তাদের ভেতরের পার্থক্য, আর SemVer কনভেনশন কভার হবে।
দ্বিতীয়টা — কাজের মাঝপথে হঠাৎ context বদলাতে হলে (একটা জরুরি hotfix চলে এলো, কিন্তু আপনার working directory অগোছালো অবস্থায় আছে) commit না করেই সাময়িকভাবে কাজ সরিয়ে রাখার দরকার হয়; Module 24-এ stash — Git-এর built-in "আপাতত সরিয়ে রাখো" টুল — বিস্তারিত আলোচনা হবে, যেটা internally আসলে একটা বিশেষ ধরনের commit stack।
MODULE 17: Remotes
Git একটা distributed VCS — কোনো একটা central server ছাড়াই এটা কাজ করতে পারে, কারণ প্রতিটা clone-ই একটা সম্পূর্ণ repository (পুরো history সহ)। কিন্তু বাস্তবে টিমওয়ার্কের জন্য একটা কনভেনশনাল "কেন্দ্র" (GitHub-এ একটা repo) দরকার হয়, যেখানে সবাই push/pull করে সিঙ্ক থাকে। এই মডিউলে remote — একটা অন্য repository-র রেফারেন্স — কীভাবে যোগ, মুছে, রিনেম করা হয়, আর ভেতরে ঠিক কী স্টোর হয় (.git/config + refs/remotes/) তা কভার হবে।
Remote আসলে কী, এবং কেন দরকার
সংজ্ঞা
একটা remote হলো অন্য একটা Git repository-র একটা named reference — সাধারণত একটা URL (HTTPS বা SSH), যেখানে আপনার local repo ডেটা push (পাঠানো) বা fetch (আনা) করতে পারে। এটা মূলত একটা "address book entry" — Git নিজে remote repo-টাকে কোনো বিশেষ ক্ষমতা দেয় না, এটা শুধু আরেকটা সাধারণ Git repository যেখানে নেটওয়ার্কের মাধ্যমে পৌঁছানো যায়।
কেন দরকার
Git-এর distributed nature-এর মানে এই না যে remote লাগবে না — বরং remote হলো সেই মেকানিজম যা distributed repository-গুলোর মধ্যে synchronization সম্ভব করে। কেন্দ্রীয় SVN/CVS-এর মতো একটাই "true" copy নেই — প্রতিটা clone সমান, কিন্তু কনভেনশন হিসেবে একটা repo (GitHub-এ hosted) কে "সত্যের উৎস" (source of truth) ধরে নেওয়া হয়, আর সেটার সাথে যোগাযোগের জন্য remote লাগে।
বাস্তব সিনারিও
ধরুন আপনি (Rakib) একটা নতুন মেশিনে কাজ শুরু করছেন। git clone https://github.com/company/backend.git চালালে Git স্বয়ংক্রিয়ভাবে একটা remote তৈরি করে দেয়, নাম origin, URL সেই GitHub লিংক। এরপর থেকে git push origin main, git fetch origin — এই কমান্ডগুলো ওই URL-এর সাথে কথা বলে।
git clone https://github.com/company/backend.git
cd backend
git remote -v
# origin https://github.com/company/backend.git (fetch)
# origin https://github.com/company/backend.git (push)
একাধিক remote থাকা স্বাভাবিক
একটা repo-র একাধিক remote থাকতে পারে — যেমন origin (আপনার নিজের fork) আর upstream (মূল প্রজেক্ট)। এটা একটা single centralized server-এর ধারণার সম্পূর্ণ বিপরীত — Git-এ যত ইচ্ছা remote যোগ করা যায়, প্রতিটাই স্বাধীন একটা sync target।
git remote কমান্ড ফ্যামিলি: add/remove/rename/set-url/-v/show/prune
কমান্ড রেফারেন্স
| কমান্ড | কাজ |
|---|---|
git remote -v |
সব remote আর তাদের URL (fetch ও push আলাদা) লিস্ট করা |
git remote add <name> <url> |
নতুন remote যোগ করা |
git remote remove <name> (বা rm) |
remote মুছে ফেলা (শুধু রেফারেন্স, actual remote repo-তে কোনো প্রভাব নেই) |
git remote rename <old> <new> |
remote-এর নাম বদলানো (সংশ্লিষ্ট refs/remotes/<old>/* ও refs/remotes/<new>/*-এ move হয়) |
git remote set-url <name> <new-url> |
বিদ্যমান remote-এর URL বদলানো (যেমন HTTPS থেকে SSH-এ শিফট করা) |
git remote show <name> |
remote সম্পর্কে বিস্তারিত তথ্য — কোন branch ট্র্যাক করছে, কোনটা stale |
git remote prune <name> |
remote-এ যেসব branch আর নেই তার local refs/remotes/<name>/* এন্ট্রি মুছে ফেলা |
উদাহরণ
git remote add upstream https://github.com/original-org/backend.git
git remote -v
# origin https://github.com/rakib/backend.git (fetch)
# origin https://github.com/rakib/backend.git (push)
# upstream https://github.com/original-org/backend.git (fetch)
# upstream https://github.com/original-org/backend.git (push)
# SSH-এ শিফট করা (deploy key ব্যবহারের জন্য common)
git remote set-url origin git@github.com:rakib/backend.git
# বিস্তারিত তথ্য
git remote show origin
# * remote origin
# Fetch URL: git@github.com:rakib/backend.git
# HEAD branch: main
# Remote branches:
# main tracked
# feature/old-x stale (use 'git remote prune' to remove)
# Local branch configured for 'git pull':
# main merges with remote main
git remote prune কেন দরকার
টিমমেট যখন GitHub-এ একটা feature branch মুছে ফেলে, আপনার local repo-র refs/remotes/origin/feature-x এন্ট্রিটা সাথে সাথে মুছে যায় না — এটা "stale" হয়ে থেকে যায়। git remote prune origin (বা git fetch --prune, পরের মডিউলে) এই মৃত রেফারেন্সগুলো পরিষ্কার করে, ফলে git branch -r বা IDE-তে ভুতুড়ে পুরনো branch দেখা যায় না।
ভেতরে কী ঘটে: .git/config এন্ট্রি + refs/remotes/<name>/*
remote আসলে দুইটা জিনিসের সমষ্টি
একটা remote internally মাত্র দুইটা জিনিস দিয়ে তৈরি:
.git/config-এ একটা URL এন্ট্রি — remote-এর নাম, তার URL, আর কোন branch-গুলো fetch করতে হবে তার refspec।refs/remotes/<name>/*-এ track করা branch pointer — remote repo-র প্রতিটা branch-এর একটা local snapshot (remote-tracking branch), যেটা শুধুমাত্রfetch/pull/pushচালানোর সময় আপডেট হয়।
.git/config-এ যা দেখবেন
cat .git/config
[remote "origin"]
url = git@github.com:rakib/backend.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
লক্ষ্য করুন fetch লাইনটা — এটা একটা refspec: +refs/heads/*:refs/remotes/origin/* মানে "remote-এর refs/heads/ (তার সব local branch) fetch করে local refs/remotes/origin/-এ রাখো"। শুরুর + মানে non-fast-forward update-ও জোর করে গ্রহণ করো (force update remote-tracking ref)।
remote-tracking branch immutable read-only snapshot
refs/remotes/origin/main কোনো সাধারণ branch না — এটা সরাসরি git checkout/commit করার জন্য না (করা গেলেও detached HEAD অবস্থায় চলে যাবেন)। এটা শুধু "শেষবার fetch করার সময় remote-এর main branch কোথায় ছিল" তার একটা লোকাল ক্যাশ। git fetch চালালেই এটা আপডেট হয়; ততক্ষণ এটা পুরনো তথ্য দেখাতে থাকবে, remote-এ নতুন commit এলেও।
রিয়েল-লাইফ ইমপ্লিকেশন
দুইজন ডেভেলপার একই সময়ে একই branch-এ কাজ করলে, একজনের local refs/remotes/origin/main আরেকজনেরটার চেয়ে পুরনো হতে পারে — এটাই bug না, এটাই distributed model-এর স্বাভাবিক আচরণ। git fetch চালানো মানেই শুধু "আমার local cache আপডেট করো", কাজের ফাইল (working directory) স্পর্শ করে না।
origin/upstream নেমিং কনভেনশন ও Fork Workflow প্রিভিউ
কেন "origin" এত কমন নাম
git clone চালালে Git স্বয়ংক্রিয়ভাবে সেই remote-কে origin নাম দেয় — এটা শুধুই একটা কনভেনশন, technical requirement না। আপনি চাইলে git remote rename origin main-repo করে অন্য নাম দিতে পারতেন, সব কাজ করবে। কিন্তু পুরো Git ইকোসিস্টেম (ডকুমেন্টেশন, টুলিং, টিমমেটদের প্রত্যাশা) origin-কে ধরে নেয় "যেখান থেকে clone করা হয়েছে" — তাই এই নাম না বদলানোই ভালো প্র্যাকটিস।
কেন "upstream" নাম আসে
Fork workflow-এ (ওপেন সোর্স কনট্রিবিউশনের সাধারণ প্যাটার্ন, বিস্তারিত Phase 11-এ) আপনি একটা প্রজেক্ট GitHub-এ fork করেন (আপনার নিজের কপি তৈরি হয়), সেটা clone করেন — তখন origin হয় আপনার fork। কিন্তু মূল প্রজেক্টের নতুন commit পেতে হলে মূল রিপোকেও remote হিসেবে যোগ করতে হয় — কনভেনশন অনুযায়ী তার নাম দেওয়া হয় upstream ("যেখান থেকে আসল স্রোত আসছে", নদীর উৎসের মতো)।
git clone git@github.com:rakib/backend.git # origin = আমার fork
cd backend
git remote add upstream git@github.com:original-org/backend.git
git remote -v
# origin git@github.com:rakib/backend.git (fetch/push)
# upstream git@github.com:original-org/backend.git (fetch/push — সাধারণত push নেই)
দুই-remote মডেলের সুবিধা
originথেকে push/pull — নিজের fork-এ নিজের কাজ।upstreamথেকে শুধু fetch — মূল প্রজেক্টে নতুন কী এলো তা জানা, নিজের branch rebase করা (git rebase upstream/main)।- সাধারণত
upstream-এ সরাসরি push করার অনুমতি থাকে না — Pull Request-ই একমাত্র পথ (ঠিক এটাই ওপেন সোর্স কনট্রিবিউশন মডেলের ভিত্তি, Phase 11-এ বিস্তারিত)।
এক লাইনে
origin= "আমি যেখানে push করি",upstream= "আমি যেখান থেকে নতুন পরিবর্তন টেনে আনি" — নামটা শুধু কনভেনশন, কিন্তু এই কনভেনশন এত সর্বজনীন যে অন্য নাম ব্যবহার করলে টিমমেটরা বিভ্রান্ত হবে।
ল্যাব: একাধিক Remote সেটআপ, রিনেম, URL বদলানো, prune
লক্ষ্য
একটা repo-তে একাধিক remote যোগ করে, তাদের ম্যানেজ করে, আর stale branch prune করে হাতে-কলমে শেখা।
ধাপ ১ — দুইটা bare repo বানিয়ে সিমুলেট করা
mkdir -p ~/git-lab/origin.git ~/git-lab/upstream.git
git init --bare ~/git-lab/origin.git
git init --bare ~/git-lab/upstream.git
git clone ~/git-lab/upstream.git ~/git-lab/seed
cd ~/git-lab/seed
git commit --allow-empty -m "initial commit"
git push origin main
git push ~/git-lab/origin.git main
ধাপ ২ — লোকাল কপি ক্লোন করা এবং দুই remote যোগ করা
git clone ~/git-lab/origin.git ~/git-lab/work
cd ~/git-lab/work
git remote add upstream ~/git-lab/upstream.git
git remote -v
ধাপ ৩ — রিনেম ও URL বদলানো
git remote rename origin my-fork
git remote -v
# my-fork এখন origin-এর জায়গায়
git remote set-url my-fork ~/git-lab/origin.git # URL অপরিবর্তিত রেখে টেস্ট করুন এটা এখনো কাজ করে কিনা
ধাপ ৪ — একটা branch তৈরি করে remote-এ push, তারপর remote থেকে মুছে prune টেস্ট
git checkout -b feature/temp
git commit --allow-empty -m "temp work"
git push my-fork feature/temp
# এখন সরাসরি bare repo থেকে branch মুছে ফেলুন (সিমুলেট করছি টিমমেট মুছেছে)
git -C ~/git-lab/origin.git branch -D feature/temp
git branch -r
# my-fork/feature/temp এখনো দেখাবে — stale!
git remote prune my-fork
git branch -r
# my-fork/feature/temp এখন আর নেই
যা লক্ষ্য করবেন
.git/config ফাইলটা প্রতিটা ধাপে খুলে দেখুন (cat .git/config) — git remote rename কীভাবে [remote "..."] সেকশনের নাম বদলায় এবং সংশ্লিষ্ট branch-এর remote = লাইনও আপডেট করে দেয়।
ইন্টারভিউ প্রশ্ন — Module 17
প্র.১ — git remote add origin <url> চালালে ঠিক কী ঘটে ভেতরে?
উত্তর: .git/config-এ একটা নতুন [remote "origin"] সেকশন যোগ হয়, যাতে URL আর একটা default fetch refspec (+refs/heads/*:refs/remotes/origin/*) থাকে। কোনো ডেটা তখনই ট্রান্সফার হয় না — আসল fetch/push না চালানো পর্যন্ত refs/remotes/origin/* খালি বা তৈরি হয় না।
প্র.২ — origin আর upstream নামের মধ্যে টেকনিক্যাল পার্থক্য কী?
উত্তর: কোনো টেকনিক্যাল পার্থক্য নেই — দুটোই সাধারণ remote নাম, Git-এর কাছে সমান। পার্থক্যটা পুরোপুরি কনভেনশন: origin সাধারণত যেখান থেকে clone করা হয়েছে (আপনার push target), upstream সাধারণত মূল প্রজেক্ট (আপনি শুধু fetch করেন, push না) — fork workflow-এ ব্যবহৃত।
প্র.৩ — git remote prune origin না চালালে কী সমস্যা হতে পারে?
উত্তর: remote-এ মুছে ফেলা branch-গুলোর local refs/remotes/origin/<branch> এন্ট্রি stale থেকে যায় — git branch -r বা IDE branch list-এ মৃত branch দেখাতে থাকবে, যেটা বিভ্রান্তিকর এবং কখনো কখনো ভুল branch থেকে ভুলবশত কাজ শুরু করার কারণ হতে পারে।
প্র.৪ — git remote rename origin upstream চালালে refs/remotes/origin/* রেফারেন্সগুলোর কী হয়?
উত্তর: সবগুলো refs/remotes/upstream/*-এ move হয়ে যায়, এবং .git/config-এ সংশ্লিষ্ট [branch "..."] সেকশনের remote = লাইনও নতুন নামে আপডেট হয়ে যায় — যাতে ট্র্যাকিং সম্পর্ক ভেঙে না যায়।
প্র.৫ — একটা repo-তে remote ছাড়াই কি কমিট, branch, merge করা যায়? উত্তর: হ্যাঁ, সম্পূর্ণভাবে। remote শুধু একাধিক repository-র মধ্যে sync করার জন্য দরকার — Git-এর core object model (commit, tree, blob, branch, merge) সম্পূর্ণ local-first, কোনো remote-এর ওপর নির্ভরশীল না। এটাই "distributed" VCS হওয়ার মূল অর্থ।
প্র.৬ — git remote show origin আর git remote -v-এর মধ্যে পার্থক্য কী?
উত্তর: -v শুধু remote নাম আর URL (fetch/push) দ্রুত লিস্ট করে। show <name> নেটওয়ার্কে গিয়ে remote-এর সাথে যোগাযোগ করে বিস্তারিত তথ্য আনে — কোন branch tracked, কোনটা stale, local branch কোন remote branch-এর সাথে merge/push configured।
কর্নার কেস — Module 17
git remote removeশুধু রেফারেন্স মুছে, actual remote repo-তে কোনো প্রভাব ফেলে না। নতুনরা মাঝে মাঝে ভয় পান যে এটা GitHub-এর repo মুছে দেবে — এটা শুধু আপনার local.git/configথেকে entry আরrefs/remotes/<name>/*মুছে দেয়।- URL-এ typo থাকলে
git remote addকোনো error দেয় না — কারণ URL-এর ভ্যালিডিটি শুধু actual network operation (fetch/push) চালানোর সময় চেক হয়,addকরার সময় না। ভুল URL দিয়ে remote যোগ করলে অনেক পরে fetch করার সময় error দেখা যাবে। - একই URL দুইবার দুইটা ভিন্ন নামে remote হিসেবে যোগ করা টেকনিক্যালি বৈধ — Git কোনো বাধা দেয় না, কিন্তু এটা করলে দুইটা
refs/remotes/ট্রি একই ডেটার ডুপ্লিকেট রাখবে, disk space নষ্ট আর বিভ্রান্তির কারণ। git remote set-urlpush আর fetch URL আলাদা করে সেট করা যায় (--pushফ্ল্যাগ দিয়ে) — বিরল কিন্তু বাস্তব ব্যবহার: read-only mirror থেকে fetch করা, কিন্তু push করা অন্য একটা লেখার-অনুমতিসহ URL-এ। ডিফল্টভাবে এই দুটো URL একই থাকে।refs/remotes/origin/HEADএকটা বিশেষ সিমবলিক রেফারেন্স যা বলে দেয় remote-এর "default branch" কোনটা (যেমনmain) — এটাgit remote show origin-এর "HEAD branch" লাইনে দেখা যায়। remote-এ default branch বদলালে (GitHub সেটিংসে) local এই তথ্য নিজে থেকে আপডেট হয় না,git remote set-head origin -aদিয়ে সরাসরি রিফ্রেশ করতে হয়।
MODULE 18: Fetch/Pull/Push
এটাই দৈনন্দিন Git ব্যবহারের কেন্দ্রবিন্দু — remote-এর সাথে ডেটা আদান-প্রদান। কিন্তু তিনটা কমান্ড-এর (fetch, pull, push) মধ্যে সম্পর্ক প্রায়ই ভুল বোঝা হয় — বিশেষ করে pull যে আসলে একটা composite কমান্ড (fetch + merge/rebase) সেটা অনেকে জানেন না। এই মডিউলে প্রতিটা কমান্ডের ফ্ল্যাগ ফ্যামিলি, non-fast-forward reject-এর কারণ, আর force push-এর তিন প্রকার — --force, --force-with-lease, --force-if-includes — এর মধ্যে নিরাপত্তার পার্থক্য কভার হবে।
fetch, pull, push — তিনটা কমান্ডের এক নজরে পার্থক্য
তিনটা কমান্ডের ভূমিকা
| কমান্ড | দিক | কী করে |
|---|---|---|
git fetch |
remote → local | remote-এর নতুন commit/branch আপনার local refs/remotes/ -এ নামিয়ে আনে, working directory স্পর্শ করে না |
git pull |
remote → local | fetch + তারপর স্বয়ংক্রিয়ভাবে merge (বা --rebase দিলে rebase) — working directory-ও আপডেট হয় |
git push |
local → remote | আপনার local commit remote-এ পাঠায়, remote branch pointer এগিয়ে দেয় |
কেন এই পার্থক্য গুরুত্বপূর্ণ
git fetch সম্পূর্ণ নিরাপদ — এটা শুধু তথ্য আনে, আপনার বর্তমান কাজে কোনো হাত দেয় না। git pull তুলনামূলক ঝুঁকিপূর্ণ কারণ এটা স্বয়ংক্রিয়ভাবে merge/rebase চালিয়ে দেয় — যদি আপনার local branch-এ uncommitted পরিবর্তন থাকে বা conflict তৈরি হওয়ার সম্ভাবনা থাকে, pull মাঝপথে থেমে যেতে পারে একটা অগোছালো অবস্থায়।
রিয়েল-লাইফ সিনারিও
আপনি যদি প্রতিদিন সকালে কাজ শুরুর আগে টিমমেটদের নতুন commit দেখতে চান কিন্তু নিজের local branch-এ এখনই merge করতে না চান, git fetch চালিয়ে শুধু git log main..origin/main দিয়ে দেখে নিতে পারেন কী নতুন এসেছে — আসলে merge না করেই। এটাই অভিজ্ঞ ডেভেলপারদের pull-এর বদলে fetch + ম্যানুয়াল merge/rebase পছন্দ করার একটা বড় কারণ — নিয়ন্ত্রণ নিজের হাতে থাকে।
git fetch origin
git log --oneline main..origin/main # remote-এ নতুন কী কমিট এসেছে দেখুন
git merge origin/main # সন্তুষ্ট হলে তবেই merge করুন
git fetch গভীরে: --all, -p/--prune, --tags, --depth
মূল ফ্ল্যাগ
| ফ্ল্যাগ | কাজ |
|---|---|
git fetch origin |
শুধু origin remote থেকে নতুন ডেটা আনা |
git fetch --all |
সব configured remote থেকে একসাথে fetch করা |
git fetch -p / --prune |
fetch করার পাশাপাশি remote-এ মুছে যাওয়া branch-এর local stale refs/remotes/ এন্ট্রিও পরিষ্কার করা |
git fetch --tags |
সব tag-ও নামিয়ে আনা (ডিফল্টে commit-এর সাথে সংযুক্ত tag আসে, কিন্তু orphan tag আসে না) |
git fetch --depth=<n> |
শুধু সাম্প্রতিক <n>টা commit আনা (shallow fetch — বড় repo-তে দ্রুত CI clone-এর জন্য ব্যবহৃত) |
কেন -p/--prune গুরুত্বপূর্ণ
মুছে যাওয়া remote branch লোকালেও ক্লিন করা দরকার কারণ না হলে refs/remotes/origin/* ধীরে ধীরে ভুতুড়ে entry-তে ভরে যায় — git branch -r বা git checkout <tab-complete> করার সময় এমন branch দেখাবে যা GitHub-এ আর নেই, বিভ্রান্তির সৃষ্টি করে।
git fetch origin --prune
# From github.com:company/backend
# - [deleted] (none) -> origin/feature/old-experiment
অনেক টিম git config --global fetch.prune true সেট করে রাখেন যাতে প্রতিটা fetch/pull-এ স্বয়ংক্রিয়ভাবে prune হয়।
shallow fetch-এর বাস্তব ব্যবহার
CI/CD পাইপলাইনে পুরো history দরকার নেই, শুধু সাম্প্রতিক commit — git fetch --depth=1 দিয়ে GB-সাইজের history স্কিপ করে সেকেন্ডে clone সম্পন্ন হয়। কিন্তু shallow repo-তে git log, git blame পুরো history দেখাতে পারে না যতক্ষণ না git fetch --unshallow চালানো হয়।
git clone --depth=1 https://github.com/company/backend.git
# একটামাত্র commit — বাকি history নেই
git log --oneline
# a1b2c3d (HEAD -> main, origin/main) latest commit only
git pull = fetch + merge/rebase, এবং --ff-only
pull একটা কম্পোজিট কমান্ড
git pull আসলে দুইটা ধাপের shortcut:
git pull origin main
# ভেতরে ভেতরে ঠিক এই দুইটা কমান্ড চলে:
git fetch origin main
git merge origin/main
git pull --rebase দিলে দ্বিতীয় ধাপ merge-এর বদলে rebase হয়:
git pull --rebase origin main
# সমতুল্য:
git fetch origin main
git rebase origin/main
কেন এই ব্যাখ্যা গুরুত্বপূর্ণ
pull-কে একটা "ম্যাজিক আপডেট বাটন" হিসেবে দেখলে বিভ্রান্তি হয় — merge conflict এলে, git status "you have unmerged paths" দেখালে, বোঝা যায় না কী হচ্ছে। কিন্তু pull-কে fetch+merge হিসেবে বুঝলে, conflict resolution ঠিক আগের merge মডিউলের মতোই স্বাভাবিক মনে হয় — কারণ এটা সেই একই merge অপারেশন, শুধু fetch-এর সাথে স্বয়ংক্রিয়ভাবে চেইন করা।
--ff-only — নিরাপদ pull
git pull --ff-only
এই ফ্ল্যাগ merge commit তৈরি হতে দেয় না — শুধু তখনই pull সফল হবে যখন local branch-এ কোনো নতুন commit নেই (fast-forward সম্ভব)। যদি local branch-এ কোনো commit থাকে যা remote-এ নেই, --ff-only pull প্রত্যাখ্যান করবে এবং একটা স্পষ্ট error দেবে — অনিচ্ছাকৃত merge commit তৈরি হওয়া থেকে রক্ষা করে। অনেক টিম pull.ff = only কনফিগ ডিফল্ট করে রাখে যাতে ভুলে অগোছালো merge history তৈরি না হয়।
git config --global pull.ff only
git push গভীরে: -u tracking, --tags, --delete, --dry-run
বেসিক ও ট্র্যাকিং সেটআপ
git push origin feature/new-api
প্রথমবার একটা নতুন local branch push করলে, -u (বা --set-upstream) দিলে ভবিষ্যতে শুধু git push/git pull লিখলেই চলবে, কোন remote/branch বলতে হবে না:
git push -u origin feature/new-api
# পরবর্তী push শুধু: git push
.git/config-এ এটা [branch "feature/new-api"] সেকশনে remote = আর merge = এন্ট্রি হিসেবে সেভ হয় — ঠিক Module 17-এ দেখা .git/config স্ট্রাকচারের মতোই।
অন্যান্য গুরুত্বপূর্ণ ফ্ল্যাগ
| ফ্ল্যাগ | কাজ |
|---|---|
git push --tags |
সব local tag remote-এ push করা |
git push origin --delete feature/x |
remote-এর branch মুছে ফেলা |
git push --dry-run |
আসলে push না করে শুধু কী push হতো তা preview করা |
git push --all |
সব local branch একসাথে push করা |
--dry-run কেন উপকারী
বড়, জটিল push-এর আগে (বিশেষ করে rebase-এর পর) --dry-run দিয়ে নিশ্চিত হওয়া যায় ঠিক কোন commit push হতে যাচ্ছে, ভুলবশত অতিরিক্ত branch বা tag push হয়ে যাচ্ছে কিনা:
git push --dry-run origin main
# To github.com:company/backend.git
# a1b2c3d..e4f5g6h main -> main
remote branch মোছা
git push origin --delete feature/old-experiment
# বিকল্প সিনট্যাক্স (কম common কিন্তু কাজ করে):
git push origin :feature/old-experiment
দ্বিতীয় ফর্মটা refspec সিনট্যাক্স <src>:<dst>-এর একটা বিশেষ কেস — খালি <src> মানে "কিছু push কোরো না", যা কার্যকরভাবে destination branch মুছে ফেলে।
Force Push: --force বনাম --force-with-lease বনাম --force-if-includes
কেন force push দরকার হয়
rebase বা commit --amend-এর পর history rewrite হয়ে যায় — commit hash বদলে যায়। এই অবস্থায় সাধারণ git push reject হবে (non-fast-forward) কারণ remote branch-এর history আর আপনার local history "সরলরৈখিকভাবে এগোয়নি" — remote যা জানে তার চেয়ে ভিন্ন commit আপনার কাছে আছে। force push দিয়ে remote branch-কে জোর করে আপনার local history-তে rewrite করতে বলা হয়।
তিনটা ফ্ল্যাগের পার্থক্য
| ফ্ল্যাগ | নিরাপত্তা | চেক করে কী |
|---|---|---|
--force (-f) |
সবচেয়ে বিপজ্জনক | কিছুই চেক করে না — remote branch-কে অন্ধভাবে overwrite করে, আপনার fetch করার পর কেউ নতুন commit push করে থাকলেও তা মুছে যাবে |
--force-with-lease |
নিরাপদ | চেক করে remote branch এখনো ঠিক সেখানেই আছে যেখানে আপনি শেষবার fetch করার সময় দেখেছিলেন — কেউ মাঝখানে push করলে reject হবে |
--force-if-includes |
আরও নিরাপদ (Git 2.30+, --force-with-lease-এর সাথে ব্যবহৃত) |
নিশ্চিত করে remote-এর বর্তমান commit আপনার local reflog-এ "included" ছিল — অর্থাৎ আপনি আসলেই সেই commit দেখেছিলেন rebase করার আগে |
কেন --force-with-lease নিরাপদ
ধরুন আপনি সকাল ১০টায় origin/feature-x fetch করলেন, rebase করলেন, এবং দুপুর ২টায় push করতে চাইলেন। এর মাঝে যদি একজন টিমমেট দুপুর ১২টায় সেই branch-এ নতুন commit push করে থাকে, --force সেই commit নিঃশব্দে মুছে দেবে — ডেটা লস, কেউ টেরও পাবে না। কিন্তু --force-with-lease push করার আগে চেক করবে remote-এর বর্তমান অবস্থা আপনার শেষ fetch-এর সাথে মেলে কিনা — না মিললে push reject হবে একটা স্পষ্ট error সহ, যাতে আপনি আগে fetch করে দেখে নিতে পারেন কী বদলেছে।
git push --force-with-lease origin feature-x
# ! [rejected] feature-x -> feature-x (stale info)
# হিন্ট: fetch করে দেখুন কী পরিবর্তন হয়েছে
সাধারণ নিয়ম
শেয়ার্ড branch (main, develop)-এ কখনো force push করবেন না। নিজের feature branch-এ rebase-এর পর force push দরকার হলে সবসময়
--force-with-leaseব্যবহার করুন, খালি--forceনা।
Non-Fast-Forward Reject হওয়ার ভেতরের কারণ
error message যা সবাই দেখেছে
git push origin main
# ! [rejected] main -> main (fetch first)
# error: failed to push some refs to 'github.com:company/backend.git'
# hint: Updates were rejected because the remote contains work that you do
# hint: not have locally.
ভেতরে ঠিক কী চেক হচ্ছে
প্রতিটা branch push আসলে একটা compare-and-swap অপারেশন — remote server চেক করে: "client যে commit-কে branch-এর current tip মনে করছে (তার local refs/remotes/origin/main), সেটা কি আসলেই remote branch-এর বর্তমান tip-এর সাথে মেলে?" যদি না মেলে (কারণ মাঝে অন্য কেউ push করেছে), remote push reject করে দেয় — এটাই non-fast-forward reject।
কেন এটা প্রয়োজনীয়, বিরক্তিকর না
এই মেকানিজম ছাড়া দুইজন ডেভেলপার একই সময়ে push করলে, দ্বিতীয়জনের push প্রথমজনের commit নিঃশব্দে মুছে দিতে পারত — কারণ push মানে "remote branch pointer-কে এই নতুন commit-এ নিয়ে যাও"। non-fast-forward চেক এই সাইলেন্ট ডেটা-লস প্রতিরোধ করে, force করে যে আপনাকে আগে remote-এর নতুন commit নিজের history-তে integrate করতে হবে (merge বা rebase দিয়ে), তারপরই push করতে দেওয়া হবে।
সমাধানের স্বাভাবিক ধাপ
git fetch origin
git rebase origin/main # অথবা: git merge origin/main
git push origin main
এই মেকানিজম আর force push-এর সম্পর্ক
force push (--force/--force-with-lease) হলো এই compare-and-swap চেক ইচ্ছাকৃতভাবে বাইপাস করার উপায় — "আমি জানি remote-এ ভিন্ন কিছু আছে, তবুও আমার local history দিয়ে সেটা প্রতিস্থাপন করো"। এই জন্যই force push এত ঝুঁকিপূর্ণ — এটা ঠিক সেই সুরক্ষা যন্ত্রটাকেই নিষ্ক্রিয় করে দেয় যেটা সাধারণ push অপারেশন ডেটা-লস থেকে বাঁচানোর জন্য রাখা হয়েছিল।
ল্যাব: একই Bare Repo-তে দুই দিক থেকে Push/Pull সিমুলেশন
লক্ষ্য
দুইটা লোকাল ক্লোন একই bare repo-র সাথে সিঙ্ক করে non-fast-forward reject, fetch+rebase, আর force-with-lease হাতে-কলমে দেখা।
সেটআপ
mkdir -p ~/git-lab2 && cd ~/git-lab2
git init --bare team.git
git clone team.git dev-a
git clone team.git dev-b
cd dev-a
git commit --allow-empty -m "initial commit"
git push origin main
cd ../dev-b
git pull origin main
ধাপ ১ — dev-a থেকে push, dev-b না জেনেই push করার চেষ্টা
cd ../dev-a
echo "feature A" > a.txt && git add a.txt
git commit -m "add feature A"
git push origin main # সফল
cd ../dev-b
echo "feature B" > b.txt && git add b.txt
git commit -m "add feature B"
git push origin main
# ! [rejected] main -> main (fetch first)
ধাপ ২ — সঠিক সমাধান: fetch + rebase
git fetch origin
git rebase origin/main
git push origin main # এখন সফল, linear history বজায় থাকলো
ধাপ ৩ — force-with-lease টেস্ট
cd ../dev-a
git commit --allow-empty --amend -m "amended commit"
git push --force-with-lease origin main # সফল, কারণ dev-a-র জানা remote state ঠিক আছে
cd ../dev-b
git commit --allow-empty --amend -m "different amend"
git push --force-with-lease origin main
# ! [rejected] (stale info) — কারণ dev-b এখনো পুরনো remote state জানে
git fetch origin
git push --force-with-lease origin main # fetch-এর পর সফল
যা লক্ষ্য করবেন
--force (lease ছাড়া) দিয়ে ধাপ ৩ পুনরাবৃত্তি করলে dev-a-র commit নিঃশব্দে মুছে যাবে dev-b-র push-এ — কোনো warning ছাড়াই। এই পার্থক্যটাই --force-with-lease ব্যবহারের আসল যুক্তি।
ইন্টারভিউ প্রশ্ন — Module 18
প্র.১ — git pull আর git fetch-এর মূল পার্থক্য কী?
উত্তর: git fetch শুধু remote-এর নতুন ডেটা local refs/remotes/-এ আনে, working directory স্পর্শ করে না। git pull হলো fetch + স্বয়ংক্রিয় merge (বা --rebase দিলে rebase) — এটা working directory-ও আপডেট করে দেয়। pull আসলে একটা composite/shortcut কমান্ড।
প্র.২ — git fetch --prune কেন দরকার?
উত্তর: remote-এ কোনো branch মুছে ফেলা হলে local refs/remotes/origin/<branch> এন্ট্রি স্বয়ংক্রিয়ভাবে মোছে না — stale থেকে যায়। --prune এই মৃত রেফারেন্স পরিষ্কার করে, git branch -r তালিকা সঠিক রাখে।
প্র.৩ — --force আর --force-with-lease-এর মধ্যে পার্থক্য কী, আর কেন --force-with-lease নিরাপদ?
উত্তর: --force কোনো চেক ছাড়াই remote branch overwrite করে — মাঝে কেউ push করলে তার commit নিঃশব্দে মুছে যেতে পারে। --force-with-lease push করার আগে যাচাই করে remote branch এখনো ঠিক সেখানেই আছে যেখানে শেষবার fetch করার সময় ছিল — মাঝে অন্য কেউ push করলে reject হয়ে যায়, ডেটা লস প্রতিরোধ হয়।
প্র.৪ — non-fast-forward push reject কেন হয়, ভেতরে ঠিক কী চেক হয়? উত্তর: push একটা compare-and-swap অপারেশন — remote চেক করে client-এর জানা branch tip আসলেই remote-এর বর্তমান tip-এর সাথে মেলে কিনা। না মিললে (মাঝে অন্য কেউ push করেছে) reject হয়, যাতে সাইলেন্ট ডেটা-লস না ঘটে।
প্র.৫ — git pull --ff-only ব্যবহার করলে কী সুবিধা হয়?
উত্তর: এটা শুধু fast-forward pull সফল হতে দেয় — local-এ কোনো divergent commit থাকলে pull ব্যর্থ হয়ে স্পষ্ট error দেয়, অনিচ্ছাকৃত merge commit তৈরি হওয়া থেকে বাঁচায়। অনেক টিম এটা pull.ff = only কনফিগ করে ডিফল্ট করে রাখে।
প্র.৬ — git fetch --depth=1 (shallow clone/fetch)-এর সীমাবদ্ধতা কী?
উত্তর: শুধু সাম্প্রতিক কমিটটুকু আনে, পুরো history আসে না — তাই git log, git blame, বা পুরনো commit-এ checkout করার মতো অপারেশন সীমিত থাকে যতক্ষণ না git fetch --unshallow দিয়ে বাকি history আনা হয়। CI/CD-তে দ্রুত clone-এর জন্য উপকারী কিন্তু forensic কাজের জন্য অনুপযুক্ত।
কর্নার কেস — Module 18
git pullconflict-এ থেমে গেলে merge আর rebase-এর resolution আলাদা।pull-এর ভেতরে merge চলছে না rebase চলছে সেটা প্রথমেgit statusদিয়ে নিশ্চিত হওয়া দরকার — দুটোর abort কমান্ড আলাদা (git merge --abortবনামgit rebase --abort), ভুল কমান্ড দিলে error আসবে বা অবস্থা আরও জটিল হবে।git pushকরার সময় কোনো branch উল্লেখ না করলে,push.defaultকনফিগের ওপর নির্ভর করে আচরণ বদলায় — আধুনিক Git-এ ডিফল্টsimple(শুধু বর্তমান branch, শুধু যদি upstream সেট করা থাকে), কিন্তু পুরনো কনফিগেmatching(সব same-name branch push) সেট থাকলে অপ্রত্যাশিতভাবে একাধিক branch push হয়ে যেতে পারে।--force-with-leaseকোনো argument ছাড়া ব্যবহার করলে এটা সব branch-এর জন্য "যা শেষবার fetch করেছি" ধরে নেয় — কিন্তু যদি অন্য কোনো টুল/CI ব্যাকগ্রাউন্ডে fetch চালিয়ে রাখে (auto-fetch), lease-এর তথ্য আপনার অজান্তেই "stale" হতে পারে, push অপ্রত্যাশিতভাবে সফল বা ব্যর্থ হতে পারে।- shallow clone-এ push করলে সমস্যা হতে পারে — কারণ shallow repo-তে পূর্ণ history নেই, ফলে rebase বা history-নির্ভর অপারেশন অস্বাভাবিক আচরণ করতে পারে। shallow clone সাধারণত শুধু read-only CI ব্যবহারের জন্য, active development-এর জন্য না।
git push origin :branch-name(কোলনের আগে খালি) দিয়ে remote branch মোছা টাইপো-প্রবণ — একটা বাড়তি স্পেস বা ভুল কোলন বসালে অপ্রত্যাশিত refspec তৈরি হতে পারে।git push origin --delete branch-nameস্পষ্ট এবং নিরাপদ, তাই এটাই প্রেফার করা উচিত।
MODULE 19: টিম ওয়ার্কফ্লো প্যাটার্ন
Git নিজে কোনো "সঠিক" branching workflow বাধ্যতামূলক করে না — এটা শুধু primitive (branch, merge, rebase, tag) দেয়, বাকিটা কনভেনশন। এই মডিউলে বাস্তবে ব্যবহৃত চারটা প্রধান workflow প্যাটার্ন — feature-branch, Gitflow, trunk-based development, আর forking workflow — এবং কোন টিমের জন্য কোনটা মানানসই তা কভার হবে। এটা কোনো একক "সঠিক উত্তর" মডিউল না — বরং trade-off বোঝার মডিউল।
কেন Workflow প্যাটার্ন দরকার
সমস্যাটা কী
Git প্রযুক্তিগতভাবে যেকোনো ধরনের branch/merge প্যাটার্ন সাপোর্ট করে — কোনো নিয়ম নেই যে কবে branch তৈরি করতে হবে, কবে merge করতে হবে, কে review করবে। এই স্বাধীনতা একদিকে শক্তি, অন্যদিকে বিশৃঙ্খলার উৎস — যদি একটা টিমের সবাই ভিন্ন ভিন্ন কনভেনশন অনুসরণ করে, history অগোছালো, অনুমানযোগ্য না হয়ে যায়।
Workflow প্যাটার্ন আসলে কী
একটা workflow প্যাটার্ন হলো টিমের সম্মত নিয়মের সেট: কোন branch-এর নাম কী কনভেনশন মানবে, কখন merge করতে হবে, কোন branch থেকে release হবে, কীভাবে hotfix হ্যান্ডল হবে। এটা কোনো Git ফিচার না — এটা টিমের প্রসেস, যেটা Git-এর primitive দিয়ে বাস্তবায়িত হয়।
রিয়েল-লাইফ ইমপ্লিকেশন
একটা ৩ জনের স্টার্টআপ টিম আর একটা ৫০ জনের এন্টারপ্রাইজ টিমের প্রয়োজন সম্পূর্ণ ভিন্ন। ছোট টিমে ভারী প্রসেস (Gitflow-এর মতো একাধিক দীর্ঘস্থায়ী branch) শুধু overhead তৈরি করে; বড় টিমে হালকা প্রসেস (সরাসরি main-এ কমিট) বিশৃঙ্খলা ও প্রোডাকশন রিস্ক তৈরি করে। এই মডিউলের বাকি অংশে চারটা প্যাটার্ন দেখে বোঝা যাবে কোনটা কখন উপযুক্ত।
সাধারণ থিম
প্রায় সব আধুনিক workflow-এর মূল নীতি এক: কখনো সরাসরি main-এ কাজ করবেন না, সবসময় একটা আলাদা branch-এ কাজ করে review-এর পর merge করুন। পার্থক্য হলো তার চারপাশের কাঠামোর জটিলতায়।
Feature-Branch Workflow
কীভাবে কাজ করে
সবচেয়ে সাধারণ ও সরল প্যাটার্ন। প্রতিটা নতুন ফিচার/বাগফিক্সের জন্য main থেকে একটা নতুন branch তৈরি করা হয়, সেখানে কাজ শেষ হলে Pull Request/Merge Request-এর মাধ্যমে review করে main-এ merge করা হয়, তারপর branch মুছে ফেলা হয়।
git checkout main
git pull origin main
git checkout -b feature/user-notifications
# ... কাজ, একাধিক কমিট ...
git push -u origin feature/user-notifications
# GitHub-এ Pull Request খোলা → review → approve → merge
git checkout main
git pull origin main
git branch -d feature/user-notifications
কেন এত জনপ্রিয়
- সরল — একটাই দীর্ঘস্থায়ী branch (
main), বাকি সব ক্ষণস্থায়ী। - CI/CD-বান্ধব —
main-এ merge মানেই deploy-যোগ্য অবস্থা (যদি টিম main-কে সবসময় deployable রাখার শৃঙ্খলা মানে)। - Review গেট — প্রতিটা পরিবর্তন PR-এর মাধ্যমে যায়, code review বাধ্যতামূলক করা সহজ।
রিয়েল-লাইফ উদাহরণ
আপনার (Rakib) backend প্রজেক্টে একটা নতুন notification-retry-logic ফিচারের জন্য feature/notification-retry branch তৈরি করে কাজ করবেন, PR-এ টিমমেট review করবে, merge হবে, GitHub Actions CI/CD পাইপলাইন (এই ব্লগেরই deploy workflow-এর মতো) স্বয়ংক্রিয়ভাবে deploy করবে।
সীমাবদ্ধতা
খুব বড় টিমে (৫০+ ডেভেলপার) একা feature-branch যথেষ্ট না — release timing, hotfix urgency, আর একাধিক ভার্সন সমান্তরাল maintain করার জন্য অতিরিক্ত কাঠামো (Gitflow) দরকার হতে পারে।
Gitflow: develop/feature/release/hotfix Branches
কাঠামো
Gitflow একটা কঠোরভাবে সংজ্ঞায়িত মডেল, একাধিক দীর্ঘস্থায়ী branch নিয়ে:
| Branch | উদ্দেশ্য |
|---|---|
main |
সবসময় production-এ যা আছে ঠিক তাই — শুধু release/hotfix থেকে merge হয় |
develop |
পরবর্তী রিলিজের জন্য integration branch — সব feature branch এখানে merge হয় |
feature/* |
develop থেকে branch, কাজ শেষে develop-এ merge |
release/* |
develop থেকে branch যখন একটা রিলিজ প্রস্তুত হচ্ছে — শুধু bugfix, নতুন ফিচার না; শেষে main আর develop দুই জায়গাতেই merge |
hotfix/* |
main থেকে branch (জরুরি প্রোডাকশন বাগ ফিক্সের জন্য) — শেষে main আর develop দুই জায়গাতেই merge |
git checkout develop
git checkout -b feature/payment-retry
# ... কাজ ...
git checkout develop && git merge --no-ff feature/payment-retry
git checkout develop
git checkout -b release/2.1.0
# ... bugfix only ...
git checkout main && git merge --no-ff release/2.1.0 && git tag v2.1.0
git checkout develop && git merge --no-ff release/2.1.0
কবে ব্যবহার করা উচিত
যেসব প্রজেক্টে একাধিক ভার্সন সমান্তরাল maintain করতে হয় (যেমন desktop software, একাধিক client-এর জন্য আলাদা release cycle), অথবা release process-এ scheduled QA/staging ধাপ আছে — সেখানে Gitflow-এর কাঠামো উপকারী, কারণ release/* branch একটা স্পষ্ট "freeze" পয়েন্ট দেয়।
কবে ওভারকিল
Continuous deployment করা ওয়েব সার্ভিসে (যেখানে প্রতিদিন বহুবার production-এ deploy হয়) Gitflow-এর multi-branch overhead অপ্রয়োজনীয় জটিলতা যোগ করে — develop আর main-এর মধ্যে sync রাখা, দুই জায়গায় merge করা, এসব শুধু ধীর গতি তৈরি করে। এই কারণেই আধুনিক SaaS টিমগুলো প্রায়ই trunk-based development বেছে নেয় (পরের লিফ)।
Trunk-Based Development + Feature Flags
কাঠামো
Trunk-based development-এ একটাই দীর্ঘস্থায়ী branch — main (trunk)। ডেভেলপাররা খুব ছোট, স্বল্পস্থায়ী branch (কয়েক ঘণ্টা থেকে ১-২ দিন) তৈরি করে দ্রুত main-এ merge করে দেন — কোনো দীর্ঘস্থায়ী develop বা release branch নেই।
অসম্পূর্ণ ফিচার কীভাবে সামলানো হয়
সমস্যা: যদি সবাই ঘন ঘন main-এ merge করে, অসম্পূর্ণ ফিচার production-এ চলে যাওয়ার ঝুঁকি থাকে। সমাধান: feature flags (feature toggles) — কোড merge হয়ে যায় main-এ, কিন্তু runtime-এ একটা কনফিগ ফ্ল্যাগ দিয়ে নিয়ন্ত্রণ করা হয় ফিচারটা ব্যবহারকারীদের কাছে visible কিনা।
if feature_flags.is_enabled("new_notification_engine", user):
send_via_new_engine(user, message)
else:
send_via_legacy_engine(user, message)
এভাবে অসম্পূর্ণ বা টেস্ট-চলমান কোড production-এ deploy হয়ে গেলেও ব্যবহারকারীরা এটা দেখেন না, যতক্ষণ না flag চালু করা হয় (ধীরে ধীরে rollout, A/B test, বা instant rollback-এর জন্যও flag off করাই যথেষ্ট, নতুন deploy লাগে না)।
কেন মডার্ন high-velocity টিমগুলো এটা পছন্দ করে
- Merge conflict কম — branch যত কম দিন বাঁচে, তত কম সম্ভাবনা যে অন্যদের কাজের সাথে সাংঘর্ষিক হবে।
- Continuous Integration-এর প্রকৃত অর্থ বাস্তবায়ন — "integrate ঘন ঘন করো" নীতির আক্ষরিক প্রয়োগ, যেটা Gitflow-এর দীর্ঘস্থায়ী
feature/releasebranch-এর সাথে সাংঘর্ষিক। - Rollback দ্রুত — কোনো bug এলে নতুন deploy না করে শুধু flag off করলেই সমস্যা মিটে যায়।
- Google, Facebook, Netflix-এর মতো কোম্পানির internal practice — হাজার হাজার ইঞ্জিনিয়ার একই
main-এ দিনে বহুবার merge করেন, feature flag দিয়ে risk আলাদা করে রাখা হয়।
ট্রেড-অফ
feature flag ব্যবস্থাপনা নিজেই একটা অতিরিক্ত জটিলতা যোগ করে — পুরনো flag পরিষ্কার না করলে কোডবেস flag-এর জঞ্জালে ভরে যায় ("flag debt")। ছোট টিমে এই ইনফ্রাস্ট্রাকচার (flag management system) বানানোর খরচ যৌক্তিকতা হারাতে পারে যদি deploy frequency কম হয়।
Forking Workflow প্রিভিউ ও Workflow তুলনা
Forking Workflow — সংক্ষেপে
ওপেন সোর্স প্রজেক্টে সাধারণত কন্ট্রিবিউটরদের সরাসরি মূল repo-তে push করার অনুমতি থাকে না (নিরাপত্তা ও কোয়ালিটি কন্ট্রোলের জন্য)। তার বদলে প্রতিটা কন্ট্রিবিউটর প্রজেক্টের নিজস্ব fork (কপি) বানায়, সেখানে কাজ করে, তারপর মূল প্রজেক্টে একটা Pull Request পাঠায়। মূল প্রজেক্টের maintainer রিভিউ করে merge করেন। এটাই Module 17-এ দেখা origin/upstream দুই-remote মডেলের ভিত্তি — বিস্তারিত মেকানিক্স ও PR ইটিকেট Phase 11-এ কভার হবে।
# সংক্ষিপ্ত ছবি — বিস্তারিত Phase 11-এ
git clone git@github.com:rakib/open-source-project.git # নিজের fork
git remote add upstream git@github.com:original-org/open-source-project.git
git checkout -b fix/typo-in-docs
# ... কাজ ...
git push origin fix/typo-in-docs
# GitHub-এ "Compare & Pull Request" → upstream-এর দিকে PR
চারটা প্যাটার্নের তুলনা টেবিল
| প্যাটার্ন | দীর্ঘস্থায়ী branch | সেরা ফিট | জটিলতা |
|---|---|---|---|
| Feature-branch | শুধু main |
ছোট-মাঝারি টিম, নিয়মিত deploy | কম |
| Gitflow | main, develop, release/* |
scheduled release, একাধিক সমান্তরাল ভার্সন | বেশি |
| Trunk-based | শুধু main |
high-velocity SaaS, continuous deployment | মাঝারি (flag ইনফ্রা লাগে) |
| Forking | কন্ট্রিবিউটরের নিজস্ব fork | ওপেন সোর্স, বহিরাগত কন্ট্রিবিউটর | মাঝারি |
সিদ্ধান্ত নেওয়ার প্রশ্ন
- টিমের সবার কি একই central repo-তে push করার অনুমতি আছে? না হলে → forking।
- একাধিক ভার্সন সমান্তরাল maintain করতে হয়? হ্যাঁ হলে → Gitflow বিবেচনা করুন।
- দিনে একাধিকবার deploy হয়, দ্রুত iteration দরকার? → trunk-based + feature flags।
- উপরের কোনোটাই না, সাধারণ টিম প্রজেক্ট? → feature-branch workflow, ডিফল্ট নিরাপদ পছন্দ।
ল্যাব: একটা Feature-Branch Workflow সিমুলেট করা
লক্ষ্য
একটা ছোট repo-তে সম্পূর্ণ feature-branch workflow — branch তৈরি থেকে merge ও cleanup পর্যন্ত — হাতে-কলমে চালানো।
সেটআপ
mkdir -p ~/git-lab3 && cd ~/git-lab3
git init --bare team-repo.git
git clone team-repo.git local-repo
cd local-repo
git commit --allow-empty -m "initial commit"
git push origin main
ধাপ ১ — ফিচার branch তৈরি
git checkout -b feature/add-readme
echo "# My Project" > README.md
git add README.md
git commit -m "docs: add README"
git push -u origin feature/add-readme
ধাপ ২ — সিমুলেট করুন main-এ অন্য একটা পরিবর্তন এসেছে (টিমমেটের কাজ)
git checkout main
echo "config" > config.txt
git add config.txt
git commit -m "chore: add config"
git push origin main
ধাপ ৩ — feature branch-কে আপডেটেড main-এর সাথে sync করা (rebase দিয়ে)
git checkout feature/add-readme
git fetch origin
git rebase origin/main
git push --force-with-lease origin feature/add-readme
ধাপ ৪ — merge ও cleanup (PR merge-এর সিমুলেশন)
git checkout main
git merge --no-ff feature/add-readme -m "Merge feature/add-readme"
git push origin main
git branch -d feature/add-readme
git push origin --delete feature/add-readme
git fetch --prune
যা লক্ষ্য করবেন
git log --graph --oneline --all চালিয়ে দেখুন — --no-ff merge কীভাবে একটা স্পষ্ট merge commit রেখে দেয় যেখান থেকে বোঝা যায় ঠিক কোন commit-গুলো একসাথে একটা ফিচার হিসেবে merge হয়েছিল, এমনকি rebase-এর ফলে history linear হওয়ার পরও।
ইন্টারভিউ প্রশ্ন — Module 19
প্র.১ — Gitflow আর trunk-based development-এর মূল দার্শনিক পার্থক্য কী?
উত্তর: Gitflow দীর্ঘস্থায়ী branch (develop, release, feature) দিয়ে কাজ আলাদা রাখে এবং একটা নির্দিষ্ট release process অনুসরণ করে। Trunk-based development শুধু একটা main branch রেখে খুব ঘন ঘন, ছোট merge করে — অসম্পূর্ণ কাজ feature flag দিয়ে লুকিয়ে রাখে। একটা branch-centric, অন্যটা integration-frequency-centric।
প্র.২ — একটা ওয়েব SaaS প্রজেক্ট, দিনে ১০ বার deploy হয় — কোন workflow বেছে নেবেন এবং কেন? উত্তর: Trunk-based development + feature flags। ঘন ঘন deploy-এর সাথে Gitflow-এর দীর্ঘস্থায়ী branch এবং একাধিক-জায়গায়-merge মডেল মানানসই না — এটা গতি কমায়। ছোট, ঘন ঘন merge আর feature flag দিয়ে risk আলাদা রাখাই এই স্কেলে উপযোগী।
প্র.৩ — feature flag ব্যবহারের সবচেয়ে বড় ঝুঁকি কী? উত্তর: "Flag debt" — সময়ের সাথে সাথে পুরনো, আর প্রয়োজন নেই এমন flag কোডবেসে জমে থাকে, কোড জটিল ও পড়া কঠিন হয়ে যায়। নিয়মিত পুরনো flag পরিষ্কার করার শৃঙ্খলা না থাকলে এটা টেকনিক্যাল ঋণে পরিণত হয়।
প্র.৪ — forking workflow কেন feature-branch workflow-এর বদলে ব্যবহার করা হয় ওপেন সোর্সে? উত্তর: ওপেন সোর্স প্রজেক্টে হাজারো অপরিচিত কন্ট্রিবিউটর থাকতে পারে — সবাইকে মূল repo-তে সরাসরি push access দেওয়া নিরাপত্তা ও কোয়ালিটি ঝুঁকি। Forking প্রতিটা কন্ট্রিবিউটরকে নিজের আলাদা কপিতে কাজ করতে দেয়, শুধু maintainer-রাই মূল repo-তে merge করার ক্ষমতা রাখেন (PR review-এর মাধ্যমে)।
প্র.৫ — Gitflow-এ hotfix/* branch কেন main থেকে branch হয়, develop থেকে না?
উত্তর: hotfix জরুরি প্রোডাকশন বাগ ফিক্স করার জন্য — develop-এ হয়তো আধা-সম্পূর্ণ নতুন ফিচার আছে যা এখনো production-ready না। main থেকে branch করলে fix শুধু production-এ যা আছে তার ওপর ভিত্তি করে হয়, অপ্রাসঙ্গিক in-progress কাজ জড়ায় না। শেষে fix দুই জায়গাতেই (main ও develop) merge হয় যাতে develop-ও ফিক্সটা পায়।
প্র.৬ — একটা টিম বলছে "আমরা কখনো main-এ সরাসরি push করি না, সবসময় PR ব্যবহার করি" — এটা কোন নির্দিষ্ট workflow-এর অংশ, নাকি সাধারণ নীতি?
উত্তর: এটা প্রায় সব আধুনিক workflow-এর (feature-branch, Gitflow, trunk-based, forking) একটা সাধারণ ভিত্তি নীতি — কোড review গেট নিশ্চিত করা। নির্দিষ্ট workflow-গুলো এই নীতির ওপর ভিন্ন ভিন্ন branch-কাঠামো যোগ করে, কিন্তু "সরাসরি push না করা" নীতিটা সবগুলোতেই সাধারণত মেনে চলা হয়।
কর্নার কেস — Module 19
- Gitflow-এ
release/*branch থেকে bugfix করলে সেটাdevelop-এ manually merge না করলে হারিয়ে যায় — অনেক টিম শুধুmain-এ merge করে ভুলে যায়develop-এও merge করতে, ফলে পরবর্তী রিলিজে সেই bugfix আবার ফিরে আসে (regression)। - Trunk-based development-এ যদি feature flag ইনফ্রাস্ট্রাকচার না থাকে, টিম আসলে "gitflow ছাড়া bare trunk" চালাচ্ছে — যেটা বিপজ্জনক, কারণ অসম্পূর্ণ কাজ সরাসরি production কোডে চলে যায় কোনো toggle ছাড়াই। Trunk-based শুধুমাত্র feature flag-সহ কার্যকর, খালি "সবাই main-এ কমিট করো" যথেষ্ট না।
- ওপেন সোর্স ছাড়া প্রাইভেট কোম্পানি প্রজেক্টেও অনেকে অকারণে fork ব্যবহার করে ফেলেন — যদি সবার push access আছে (কোম্পানির নিজস্ব টিম), সাধারণ feature-branch workflow-ই যথেষ্ট, forking-এর অতিরিক্ত সিঙ্ক জটিলতা অপ্রয়োজনীয়।
- Workflow বদলানো (যেমন Gitflow থেকে trunk-based-এ মাইগ্রেশন) মাঝপথে বিশৃঙ্খলা তৈরি করতে পারে — যদি অর্ধেক টিম পুরনো কনভেনশন মেনে চলে, অর্ধেক নতুন, branch নামকরণ ও merge policy সাংঘর্ষিক হয়ে পড়ে। মাইগ্রেশনের সময় একটা স্পষ্ট কাট-অফ তারিখ ও টিম-ওয়াইড ঘোষণা জরুরি।
MODULE 20: log গভীরে
git log সবচেয়ে বেশি ব্যবহৃত কমান্ডগুলোর একটা, কিন্তু বেশিরভাগ মানুষ এর ক্ষমতার ১০ শতাংশও ব্যবহার করেন না। এই মডিউলে git log-এর পুরো ফ্ল্যাগ ফ্যামিলি — visual graph, filtering (author/date/message), diff দেখানো, আর সবচেয়ে শক্তিশালী forensic টুল pickaxe search (-S/-G) — কভার হবে। এগুলো একসাথে "কে, কখন, কী, কেন বদলেছে" প্রশ্নের উত্তর সেকেন্ডে বের করার হাতিয়ার।
git log কেন গুরুত্বপূর্ণ, ফ্ল্যাগ ফ্যামিলির পরিচিতি
সমস্যাটা কী
git log ফ্ল্যাগ ছাড়া চালালে প্রতিটা commit-এর পুরো hash, author, date, message আলাদা ব্লকে দেখায় — বড় repo-তে এটা প্রায় অকেজো। বাস্তব ব্যবহারে সবসময় নির্দিষ্ট প্রশ্নের উত্তর দরকার হয়: "কে এই ফাইলটা শেষ বদলেছে?", "গত সপ্তাহে কী কী commit এসেছে?", "কোন commit-এ এই ফাংশনটা যোগ হয়েছিল?" — প্রতিটা প্রশ্নের জন্য git log-এর আলাদা ফ্ল্যাগ কম্বিনেশন আছে।
ফ্ল্যাগ ফ্যামিলির ভাগ
| শ্রেণি | উদাহরণ ফ্ল্যাগ | কাজ |
|---|---|---|
| Visual/format | --oneline, --graph, --decorate, --pretty=format: |
আউটপুট কীভাবে দেখাবে |
| Filtering | -n, --author, --since/--until, --grep |
কোন commit-গুলো দেখাবে |
| Content diff | -p, --stat, --follow |
commit-এর সাথে কী পরিবর্তন দেখাবে |
| Content search | -S<string>, -G<regex> |
কোন commit-এ নির্দিষ্ট কোড যোগ/মুছে গেছে |
রিয়েল-লাইফ উদাহরণ
git log --oneline --author="Rakib" --since="2 weeks ago" -- src/notifications/
এই একটা কমান্ড উত্তর দেয়: "গত দুই সপ্তাহে আমি src/notifications/ ডিরেক্টরিতে কী কী commit করেছি?" — এটাই git log-এর আসল শক্তি: ফ্ল্যাগ কম্বাইন করে অত্যন্ত নির্দিষ্ট প্রশ্নের উত্তর বের করা।
কেন এটা ফরেনসিক স্কিলের ভিত্তি
পরের দুইটা মডিউলে (show/diff/blame, bisect) যে সব টুল দেখা যাবে, সেগুলোর অনেকটাই আসলে git log-এরই বিশেষায়িত রূপ বা git log-এর সাথে মিলিয়ে ব্যবহৃত হয়। git log ভালোভাবে আয়ত্ত করা মানেই বাকি ফরেনসিক টুলিং সহজ হয়ে যাওয়া।
গ্রাফ ও ভিজ্যুয়াল ফ্ল্যাগ: --oneline, --graph --all --decorate, -n
মূল ফ্ল্যাগ
| ফ্ল্যাগ | কাজ |
|---|---|
--oneline |
প্রতিটা commit একটা লাইনে (short hash + message) |
--graph |
ASCII আর্ট দিয়ে branch/merge টপোলজি দেখানো |
--all |
শুধু বর্তমান branch না, সব branch/ref দেখানো |
--decorate |
কোন commit-এ কোন branch/tag পয়েন্ট করছে তা দেখানো |
-n <count> |
সাম্প্রতিক নির্দিষ্ট সংখ্যক commit-এ সীমাবদ্ধ করা |
সবচেয়ে ব্যবহৃত কম্বো
git log --oneline --graph --all --decorate -n 15
* e4f5g6h (HEAD -> feature/retry, origin/feature/retry) fix retry backoff
* a1b2c3d add exponential backoff logic
|\
| * 9f8e7d6 (origin/main, main) hotfix: null check in notifier
* | 3c2b1a0 wip: retry counter
|/
* 7d6c5b4 initial retry skeleton
কেন --all গুরুত্বপূর্ণ
ডিফল্টে git log শুধু বর্তমান branch-এর history দেখায় — অন্য branch-এ কী হচ্ছে তা দেখা যায় না। --all দিলে local সব branch এবং remote-tracking branch (refs/remotes/*) একসাথে graph-এ দেখা যায়, ফলে বোঝা যায় বিভিন্ন branch একে অপরের সাপেক্ষে কোথায় আছে — এটা মার্জ conflict predict করা বা কোন branch পুরনো হয়ে গেছে বোঝার জন্য অমূল্য।
alias হিসেবে সেভ করা
এই কম্বো এত বেশি ব্যবহৃত হয় যে প্রায় সব অভিজ্ঞ Git ব্যবহারকারী একে alias করে রাখেন:
git config --global alias.lg "log --oneline --graph --all --decorate"
git lg -n 20
ফিল্টারিং ফ্ল্যাগ: --author, --since/--until, --grep
কমান্ড ফ্যামিলি
| ফ্ল্যাগ | কাজ |
|---|---|
--author="<pattern>" |
নির্দিষ্ট author-এর commit শুধু দেখানো (regex সাপোর্ট করে) |
--since="<date>" / --after |
নির্দিষ্ট তারিখের পর commit |
--until="<date>" / --before |
নির্দিষ্ট তারিখের আগে commit |
--grep="<pattern>" |
commit message-এ নির্দিষ্ট টেক্সট/regex খোঁজা |
উদাহরণ
# গত মাসে Rakib-এর করা সব bugfix commit
git log --author="Rakib" --since="1 month ago" --grep="fix:"
# একটা নির্দিষ্ট তারিখ রেঞ্জ
git log --since="2026-08-01" --until="2026-08-15" --oneline
--since/--until মানব-পাঠযোগ্য তারিখ পার্স করে
Git-এর তারিখ পার্সার বেশ নমনীয় — "2 weeks ago", "yesterday", "2026-08-01", "last Monday" সবই কাজ করে। এটা internally একটা "approxidate" পার্সার ব্যবহার করে যা প্রায় সব সাধারণ human-readable তারিখ ফরম্যাট বুঝতে পারে।
--grep আর -S/-G-এর মধ্যে পার্থক্য (পরের লিফে বিস্তারিত)
--grep শুধু commit message-এ সার্চ করে, কোড কনটেন্টে না। "কোন commit-এ 'refactor' লেখা মেসেজ আছে" জানতে --grep ব্যবহার হবে, কিন্তু "কোন commit-এ একটা নির্দিষ্ট function name কোডে যোগ হয়েছে" জানতে -S/-G (pickaxe) লাগবে — সম্পূর্ণ ভিন্ন সার্চ স্পেস।
একাধিক ফিল্টার কম্বাইন করা
git log --author="Rakib" --grep="notification" --since="3 weeks ago" --oneline
সব ফিল্টার AND লজিকে কাজ করে (ডিফল্টে) — সবগুলো শর্ত মিললেই commit দেখাবে। একাধিক --author দিলে অবশ্য সেগুলো OR লজিকে কাজ করে (যেকোনো একজন মিললেই দেখাবে) — এটা একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ ব্যতিক্রম।
diff দেখানোর ফ্ল্যাগ: -p, --stat, --follow
মূল ফ্ল্যাগ
| ফ্ল্যাগ | কাজ |
|---|---|
-p (--patch) |
প্রতিটা commit-এর সম্পূর্ণ diff দেখানো |
--stat |
প্রতিটা commit-এ কোন ফাইল কতটা লাইন বদলেছে তার সারাংশ (diff কনটেন্ট ছাড়া) |
--follow |
rename-এর পরেও একটা নির্দিষ্ট ফাইলের history ট্র্যাক করা |
-p বনাম --stat
git log --stat -3
commit e4f5g6h
Author: Rakib <rakib@example.com>
Date: Fri Aug 21 2026
fix retry backoff
src/retry.py | 12 +++++++-----
tests/test_retry.py | 5 +++++
2 files changed, 12 insertions(+), 5 deletions(-)
--stat দ্রুত ওভারভিউ দেয় (কতগুলো ফাইল, কত লাইন), কিন্তু -p পুরো actual diff দেখায় লাইন-বাই-লাইন — গভীর review-এর জন্য -p, দ্রুত স্ক্যানের জন্য --stat।
--follow কেন দরকার
ডিফল্টে git log <file> শুধু বর্তমান নামের ফাইলের history দেখায় — যদি ফাইলটা কখনো rename হয়ে থাকে (যেমন utils.py থেকে helpers.py), rename-এর আগের history দেখা যায় না। --follow Git-কে বলে rename detection চালিয়ে পুরনো নামের history-ও যুক্ত করে দেখাতে।
git log --follow --oneline -- src/helpers.py
# e4f5g6h rename utils.py to helpers.py
# a1b2c3d add caching logic <- এটা তখন utils.py নামে ছিল
# 7d6c5b4 initial utils functions <- এটাও utils.py নামে ছিল
গুরুত্বপূর্ণ সীমাবদ্ধতা
--follow শুধু একটা ফাইলের সাথে কাজ করে — একাধিক pathspec দিলে (git log --follow file1.py file2.py) Git error দেবে বা অপ্রত্যাশিত আচরণ করবে। এছাড়া rename detection heuristic-ভিত্তিক (content similarity threshold), তাই খুব বেশি বদলে যাওয়া ফাইল rename হিসেবে ধরা নাও পড়তে পারে।
Pickaxe Search: -S<string> বনাম -G<regex>
সমস্যাটা কী
ধরুন আপনি জানতে চান "কোন commit-এ MAX_RETRY_COUNT variable-টা যোগ বা মুছে গেছে?" — --grep কাজ করবে না কারণ এটা commit message খোঁজে, কোড না। এখানে দরকার pickaxe — কোড কনটেন্টের ভেতরে সার্চ করার ফিচার।
-S<string> — occurrence count পরিবর্তন খোঁজে
git log -S"MAX_RETRY_COUNT" --oneline
-S একটা commit দেখায় শুধু তখনই যখন সেই commit-এ MAX_RETRY_COUNT স্ট্রিং-এর সংখ্যা (occurrence count) বদলেছে — যোগ হয়েছে বা মুছে গেছে (শুধু একটা লাইনের ভেতরে সরে যাওয়া/পুনর্বিন্যাস হলে count অপরিবর্তিত থাকে, তখন -S দেখাবে না)।
-G<regex> — regex ম্যাচিং লাইনে যেকোনো পরিবর্তন খোঁজে
git log -G"retry_count\s*=\s*\d+" --oneline
-G একটা commit দেখায় যদি diff-এর কোনো যোগ/মোছা লাইন সেই regex-এর সাথে ম্যাচ করে — occurrence count বদলাক বা না বদলাক। -G আরও broad — এটা যেকোনো লাইন-লেভেল পরিবর্তন ধরে যা প্যাটার্নের সাথে মেলে।
পার্থক্য এক টেবিলে
-S |
-G |
|
|---|---|---|
| খোঁজে | স্ট্রিং occurrence count-এর পরিবর্তন | regex-ম্যাচিং লাইনে যেকোনো diff |
| Regex সাপোর্ট | --pickaxe-regex ফ্ল্যাগ দিলে |
সবসময় regex |
| ব্যবহার কেস | "এই ভ্যারিয়েবল/ফাংশন কবে যোগ/মুছে হয়েছে" | "এই প্যাটার্নের যেকোনো পরিবর্তন কবে হয়েছে" |
| False negative | লাইন পুনর্বিন্যাসে মিস করে | কম মিস করে (broader match) |
রিয়েল-লাইফ উদাহরণ
একটা production bug ট্রেস করতে গিয়ে জানা দরকার কবে একটা নির্দিষ্ট SQL query pattern যোগ হয়েছিল:
git log -S"SELECT * FROM orders" --oneline -p -- src/db/
-p-র সাথে কম্বাইন করলে সরাসরি সেই commit-এর diff-ও দেখা যায়, আলাদা করে git show চালানো লাগে না।
--pretty=format দিয়ে কাস্টম আউটপুট
কেন কাস্টম ফরম্যাট দরকার
--oneline দ্রুত কিন্তু সীমিত — author, ফুল তারিখ, বা কমা-সেপারেটেড CSV-এর মতো আউটপুট দরকার হলে --pretty=format:"..." দিয়ে সম্পূর্ণ কাস্টম টেমপ্লেট বানানো যায়।
প্লেসহোল্ডার রেফারেন্স
| প্লেসহোল্ডার | মানে |
|---|---|
%H / %h |
full / abbreviated commit hash |
%an / %ae |
author name / email |
%ad |
author date (--date= দিয়ে ফরম্যাট নিয়ন্ত্রণ করা যায়) |
%s |
commit subject (প্রথম লাইন) |
%d |
ref names (branch/tag decoration) |
উদাহরণ
git log --pretty=format:"%h | %ad | %an | %s" --date=short -5
e4f5g6h | 2026-08-21 | Rakib Hasan | fix retry backoff
a1b2c3d | 2026-08-20 | Rakib Hasan | add exponential backoff logic
9f8e7d6 | 2026-08-19 | Team Member | hotfix: null check in notifier
স্ক্রিপ্টিং-এ ব্যবহার
CI/CD পাইপলাইনে changelog স্বয়ংক্রিয় জেনারেট করার জন্য এটা খুব কাজে লাগে:
git log --pretty=format:"- %s (%h)" v1.2.0..HEAD > CHANGELOG_DRAFT.md
এই কমান্ড শেষ ট্যাগ থেকে এখন পর্যন্ত সব commit message-এর একটা bullet-list তৈরি করে দেয় — রিলিজ নোট লেখার প্রথম খসড়া হিসেবে ব্যবহারযোগ্য।
alias-এ সেভ করা
git config --global alias.changelog 'log --pretty=format:"- %s (%h)"'
ল্যাব: Pickaxe দিয়ে একটা কনফিগ ভ্যালুর ইতিহাস খুঁজে বের করা
লক্ষ্য
একটা repo তৈরি করে, ইচ্ছাকৃতভাবে একটা কনফিগ ভ্যালু কয়েকবার বদলে, -S/-G দিয়ে সেই পরিবর্তনের ইতিহাস খুঁজে বের করা।
সেটআপ
mkdir -p ~/git-lab4 && cd ~/git-lab4 && git init
echo "TIMEOUT_SECONDS = 30" > config.py
git add config.py && git commit -m "initial config"
echo "TIMEOUT_SECONDS = 30
RETRY_COUNT = 3" > config.py
git add config.py && git commit -m "add retry count"
sed -i 's/RETRY_COUNT = 3/RETRY_COUNT = 5/' config.py
git add config.py && git commit -m "bump retry count to 5"
echo "TIMEOUT_SECONDS = 60
RETRY_COUNT = 5" > config.py
git add config.py && git commit -m "increase timeout"
ধাপ ১ — -S দিয়ে কবে RETRY_COUNT যোগ/মোছা হয়েছে খোঁজা
git log -S"RETRY_COUNT" --oneline
# শুধু "add retry count" commit দেখাবে — কারণ এটাই যেখানে RETRY_COUNT প্রথম আবির্ভূত হয়েছে
# (occurrence count 0 থেকে 1 হয়েছে)
ধাপ ২ — -G দিয়ে RETRY_COUNT-এর সংখ্যা-পরিবর্তন সহ যেকোনো লাইন-বদল খোঁজা
git log -G"RETRY_COUNT = [0-9]+" --oneline
# "add retry count" এবং "bump retry count to 5" — দুইটাই দেখাবে
# কারণ দ্বিতীয়টা লাইন বদলেছে কিন্তু occurrence count বদলায়নি (তাই -S এটা মিস করত)
ধাপ ৩ — পার্থক্য যাচাই
git log -S"RETRY_COUNT" --oneline | wc -l # ১টা commit
git log -G"RETRY_COUNT" --oneline | wc -l # ২টা commit
চ্যালেঞ্জ
-p-র সাথে -G কম্বাইন করে (git log -G"RETRY_COUNT" -p -- config.py) সরাসরি দেখুন প্রতিটা matched commit-এ ঠিক কোন লাইনটা বদলেছে।
ইন্টারভিউ প্রশ্ন — Module 20
প্র.১ — --grep আর -S (pickaxe)-এর মধ্যে পার্থক্য কী?
উত্তর: --grep commit message-এ টেক্সট খোঁজে। -S (pickaxe) actual কোড কনটেন্টে একটা স্ট্রিং-এর occurrence count কোন commit-এ বদলেছে তা খোঁজে। সম্পূর্ণ ভিন্ন সার্চ স্পেস — একটা মেসেজে, আরেকটা কোডে।
প্র.২ — -S আর -G-এর মধ্যে পার্থক্য কী?
উত্তর: -S<string> শুধু তখন commit দেখায় যখন স্ট্রিং-এর occurrence count বদলেছে (যোগ/মোছা)। -G<regex> broader — regex-ম্যাচিং যেকোনো লাইনে diff হলেই দেখায়, occurrence count অপরিবর্তিত থাকলেও (যেমন লাইন পুনর্বিন্যাস বা ভ্যালু বদল)।
প্র.৩ — git log --follow-এর সীমাবদ্ধতা কী?
উত্তর: শুধু একটা ফাইলের সাথে কাজ করে (একাধিক pathspec দিলে error/অপ্রত্যাশিত আচরণ), এবং rename detection heuristic-ভিত্তিক (content similarity threshold) — খুব বেশি বদলে যাওয়া ফাইল rename হিসেবে ধরা নাও পড়তে পারে।
প্র.৪ — git log --author="A" --author="B" চালালে কী হবে?
উত্তর: A অথবা B — যেকোনো একজনের commit দেখাবে (OR লজিক)। এটা একটা ব্যতিক্রম, কারণ সাধারণত একাধিক ভিন্ন ফিল্টার (যেমন --author আর --since একসাথে) AND লজিকে কাজ করে, কিন্তু একই ফ্ল্যাগ একাধিকবার দিলে OR হয়ে যায়।
প্র.৫ — একটা changelog স্বয়ংক্রিয় জেনারেট করতে চান শেষ রিলিজ ট্যাগ থেকে এখন পর্যন্ত — কোন কমান্ড ব্যবহার করবেন?
উত্তর: git log --pretty=format:"- %s (%h)" v1.2.0..HEAD — এটা v1.2.0 ট্যাগের পর থেকে HEAD পর্যন্ত সব commit-এর subject আর short hash একটা bullet-list ফরম্যাটে দেখায়, changelog draft হিসেবে ব্যবহারযোগ্য।
প্র.৬ — --stat আর -p-এর মধ্যে কোনটা কখন ব্যবহার করবেন?
উত্তর: --stat দ্রুত ওভারভিউয়ের জন্য (কতগুলো ফাইল, কত লাইন যোগ/মোছা হয়েছে) — বড় সংখ্যক commit স্ক্যান করার সময় উপযোগী। -p পুরো actual diff দেখায় লাইন-বাই-লাইন — একটা নির্দিষ্ট commit-এর গভীর review-এর জন্য।
কর্নার কেস — Module 20
-Sস্ট্রিং-এ regex special character (যেমন.,*) থাকলে ডিফল্টে literal string হিসেবে treat হয়, regex হিসেবে না — regex হিসেবে ব্যবহার করতে চাইলে--pickaxe-regexফ্ল্যাগ যোগ করতে হবে, না হলে অপ্রত্যাশিত (কোনো) ফলাফল আসতে পারে।- merge commit-এ ডিফল্টে
-pdiff দেখায় না — কারণ merge commit-এর দুইটা parent, কোন দিক থেকে diff দেখাবে তা অস্পষ্ট।-pদিয়ে merge commit-এর diff দেখতে হলে--diff-mergesবা-mফ্ল্যাগ যোগ করতে হয়। --since/--untilকমিটের author date-এর ওপর ভিত্তি করে ফিল্টার করে, commit date-এর ওপর না — rebase-এর পরে author date অপরিবর্তিত থাকে কিন্তু commit date বদলে যায়, তাই কখনো কখনো--sinceদিয়ে ফিল্টার করলে প্রত্যাশার চেয়ে বেশি বা কম commit দেখা যেতে পারে।git log --allসব local branch/tag/remote-tracking ref দেখায়, কিন্তু stash বা reflog-only commit না —git stash list-এ যা আছে তা--all-এ দেখাবে না যদি না আলাদা করেgit log --all --source $(git rev-parse --all) refs/stashএর মতো কিছু করা হয়। এটা প্রায়ই ভুলে যাওয়া হয় যে stash আসলাদা namespace-এ থাকে।
MODULE 21: show/diff/blame
git log দিয়ে বোঝা যায় "কী কী commit হয়েছে", কিন্তু "একটা নির্দিষ্ট commit-এ ঠিক কী বদলেছে" বা "এই লাইনটা কে লিখেছিল" জানতে দরকার আরও নির্দিষ্ট টুল — git show, git diff, git blame। এই মডিউলে এই তিনটা কমান্ড, তাদের সিনট্যাক্স ভ্যারিয়েশন (two-dot vs three-dot), আর দুইটা সহায়ক কমান্ড — git shortlog (কন্ট্রিবিউটর র্যাংকিং) ও git describe (build version string) — কভার হবে।
show, diff, blame — কখন কোনটা ব্যবহার করবেন
তিনটা টুলের ভূমিকা
| কমান্ড | প্রশ্নের উত্তর দেয় |
|---|---|
git show <commit> |
একটা নির্দিষ্ট commit-এ ঠিক কী পরিবর্তন হয়েছিল? |
git diff <A> <B> |
দুইটা commit/branch/ফাইলের মধ্যে পার্থক্য কী? |
git blame <file> |
এই ফাইলের প্রতিটা লাইন কোন commit-এ, কে লিখেছিল? |
রিয়েল-লাইফ ফরেনসিক ফ্লো
একটা bug রিপোর্ট আসলে সাধারণ ফ্লো: প্রথমে git blame দিয়ে সন্দেহজনক লাইনটা কোন commit-এ এসেছে খুঁজে বের করা, তারপর git show <commit> দিয়ে সেই commit-এর পুরো প্রেক্ষাপট (message, পুরো diff, author) দেখা, প্রয়োজনে git diff <commit>^..<commit> দিয়ে আরও সূক্ষ্মভাবে ওই একটা commit-এর আগে-পরে তুলনা করা।
git blame -L 45,50 src/payment.py
# একটা সন্দেহজনক লাইনের কমিট hash পাওয়া গেলো: a1b2c3d
git show a1b2c3d
# পুরো commit context — message, author, diff
এগুলো git log-এর সাথে সম্পর্ক
git show আসলে git log -1 -p <commit>-এর সমতুল্য (একটা মাত্র commit-এর জন্য patch mode log) — আলাদা কমান্ড হলেও ভেতরে একই diff-generation মেশিনারি ব্যবহার করে। git diff ভিন্ন — এটা কোনো commit history দেখায় না, শুধু দুইটা নির্দিষ্ট point-এর মধ্যে পার্থক্য গণনা করে।
git show-এর তিন রূপ: commit, tag, commit:path
রূপ ১ — একটা commit দেখানো
git show a1b2c3d
পুরো commit metadata (author, date, message) + diff দেখায় — parent commit-এর সাথে তুলনা করে।
রূপ ২ — একটা tag দেখানো
git show v1.2.0
Annotated tag হলে প্রথমে tag object-এর নিজের তথ্য (tagger, date, message) দেখায়, তারপর যে commit-এ tag পয়েন্ট করছে তার diff। Lightweight tag হলে সরাসরি commit-এর তথ্যই দেখাবে (কারণ lightweight tag-এর নিজস্ব কোনো object নেই — Module 23-এ বিস্তারিত)।
রূপ ৩ — commit:path দিয়ে একটা নির্দিষ্ট সময়ের ফাইল দেখা
git show a1b2c3d:src/config.py
এটা diff দেখায় না — বরং সেই commit-এর মুহূর্তে src/config.py ফাইলটা ঠিক কেমন ছিল তার সম্পূর্ণ কনটেন্ট দেখায় (যেমন সেই সময় cat করলে যা দেখতেন)। এটা internally git cat-file -p দিয়ে সেই commit-এর tree object থেকে blob-টা resolve করে বের করে আনে (Phase 1-এর object model-এর সরাসরি প্রয়োগ)।
রিয়েল-লাইফ উদাহরণ
একটা পুরনো bug ডিবাগ করতে গিয়ে জানা দরকার ৩ মাস আগে একটা কনফিগ ফাইল কেমন দেখতে ছিল, বর্তমান branch checkout না করেই:
git show HEAD~50:src/config.py > /tmp/old_config.py
diff /tmp/old_config.py src/config.py
git show দিয়ে merge commit
merge commit-এ git show ডিফল্টে কম্বাইন্ড diff দেখায় (শুধু conflict resolution-এর অংশ) — পুরো diff দেখতে -m ফ্ল্যাগ লাগে, ঠিক git log -p-এর মতো একই আচরণ (Module 20-এর corner case-এ উল্লেখিত)।
Two-dot বনাম Three-dot Diff/Log সিনট্যাক্স
সিনট্যাক্স
| সিনট্যাক্স | অর্থ |
|---|---|
A..B (two-dot) |
A থেকে সরাসরি B পর্যন্ত যা কিছু বদলেছে/নতুন commit এসেছে |
A...B (three-dot) |
A আর B-এর common ancestor থেকে B পর্যন্ত (git diff-এ) অথবা A ও B উভয়ের ইউনিক commit (git log-এ) |
git diff-এ পার্থক্য
git diff main..feature-x
# main-এর বর্তমান অবস্থা আর feature-x-এর বর্তমান অবস্থার মধ্যে সরাসরি ফাইল-content পার্থক্য
git diff main...feature-x
# main আর feature-x-এর common ancestor (merge-base) থেকে feature-x পর্যন্ত পার্থক্য
# — অর্থাৎ "feature-x branch তৈরি হওয়ার পর থেকে feature-x-এ কী কী বদলেছে",
# main-এ এর মাঝে নতুন commit এলেও তা এই diff-এ আসবে না
git diff-এ আসলে two-dot আর A B (স্পেস দিয়ে, ডট ছাড়া) একই জিনিস — সরাসরি দুইটা point-এর snapshot তুলনা। Three-dot প্রথমে merge-base বের করে, তারপর সেখান থেকে তুলনা করে।
git log-এ পার্থক্য
git log main..feature-x
# শুধু feature-x-এ আছে কিন্তু main-এ নেই এমন commit
git log main...feature-x
# main এবং feature-x — দুই দিকেই যা একে অপরের নেই এমন সব commit (উভয়ের ইউনিক অংশ)
# --left-right ফ্ল্যাগ দিলে কোনটা কোন দিক থেকে এসেছে তাও দেখায়
রিয়েল-লাইফ ব্যবহার: PR review-এর আগে
একটা Pull Request review করার আগে জানতে চান "আমার feature branch-এ ঠিক কী কী নতুন কাজ আছে, main-এ এর মাঝে যা এসেছে তা বাদে":
git diff main...feature-x # শুধু feature-x-এর নিজস্ব পরিবর্তন
git log --oneline main...feature-x --left-right
# < a1b2c3d main-only commit
# > e4f5g6h feature-x-only commit
মনে রাখার শর্টকাট
Two-dot = "সরাসরি একজন থেকে আরেকজনে যাওয়ার পথ"। Three-dot = "দুইজনের শেয়ার্ড বিন্দু থেকে হিসাব"।
git diff --no-index দিয়ে non-Git ফাইল তুলনা
সমস্যাটা কী
git diff সাধারণত repository-র ভেতরের কমিট/staged/working state তুলনা করে — কিন্তু মাঝে মাঝে দুইটা সম্পূর্ণ আলাদা, Git-এর ট্র্যাকিং-এর বাইরের ফাইল তুলনা করা দরকার হয় (যেমন দুইটা আলাদা প্রজেক্টের কনফিগ ফাইল, বা কোনো ডাউনলোড করা দুইটা ভার্সনের ফাইল)।
সমাধান
git diff --no-index /path/to/file-a.txt /path/to/file-b.txt
এটা কোনো Git repository-র প্রয়োজন ছাড়াই কাজ করে (এমনকি একটা non-Git ডিরেক্টরিতেও চলবে) — শুধু Git-এর চমৎকার diff algorithm আর coloring ব্যবহার করে দুইটা ফাইল তুলনা করে, diff -u-এর একটা আরও সুন্দর বিকল্প হিসেবে।
রিয়েল-লাইফ উদাহরণ
দুইটা environment-এর কনফিগ ফাইল তুলনা করা (production বনাম staging), যেগুলো একই repo-তে নেই:
git diff --no-index config/production.env config/staging.env
diff --git a/config/production.env b/config/staging.env
index a1b2c3d..e4f5g6h 100644
--- a/config/production.env
+++ b/config/staging.env
@@ -1,3 +1,3 @@
DATABASE_HOST=prod-db.internal
-DEBUG=false
+DEBUG=true
MAX_CONNECTIONS=100
গুরুত্বপূর্ণ আচরণ
--no-index মোডে exit code অন্যান্য diff-এর মতোই — ফাইল দুইটা অভিন্ন হলে 0, ভিন্ন হলে 1 রিটার্ন করে, যা script-এ conditional check করার জন্য ব্যবহারযোগ্য। এছাড়া দুইটা ডিরেক্টরি দিলে recursively সব ফাইল তুলনা করবে।
git blame গভীরে: -L, -w, --ignore-rev/--ignore-revs-file
বেসিক ব্যবহার
git blame src/payment.py
প্রতিটা লাইনের পাশে কোন commit, কে, কবে লিখেছে তা দেখায়।
-L — লাইন রেঞ্জ সীমাবদ্ধ করা
পুরো ফাইলের blame দরকার নেই সবসময় — নির্দিষ্ট লাইন রেঞ্জে সীমাবদ্ধ করা যায়:
git blame -L 40,60 src/payment.py
git blame -L /def calculate_fee/,+15 src/payment.py # ফাংশন খুঁজে সেখান থেকে ১৫ লাইন
-w — whitespace পরিবর্তন উপেক্ষা করা
শুধু indentation/whitespace বদলানো একটা commit-কে blame-এ "আসল লেখক" হিসেবে দেখানো অনুপযুক্ত — -w সেই noise বাদ দিয়ে প্রকৃত content পরিবর্তনকারী commit দেখায়।
git blame -w src/payment.py
--ignore-rev/--ignore-revs-file — বড় reformat commit বাদ দেওয়া
একটা টিম যদি কখনো পুরো কোডবেসে একটা bulk black/prettier reformat commit চালায়, সেই commit blame-এ প্রতিটা লাইনের "লেখক" হিসেবে দেখাতে শুরু করবে — আসল ঐতিহাসিক লেখক হারিয়ে যাবে। সমাধান:
git blame --ignore-rev a1b2c3d src/payment.py
# অথবা স্থায়ীভাবে একটা ফাইলে সব reformat commit রেখে
echo "a1b2c3d" >> .git-blame-ignore-revs
git blame --ignore-revs-file .git-blame-ignore-revs src/payment.py
# পুরো টিমের জন্য স্থায়ী কনফিগ (GitHub-ও এই ফাইল অটো-ডিটেক্ট করে)
git config blame.ignoreRevsFile .git-blame-ignore-revs
রিয়েল-লাইফ প্রভাব
.git-blame-ignore-revs ফাইলটা GitHub/GitLab-এর web UI blame view-ও সম্মান করে — এটা এখন একটা de facto স্ট্যান্ডার্ড কনভেনশন বড় reformat commit-এর blame noise এড়াতে।
git shortlog -sn ও git describe --tags --always
git shortlog -sn — কন্ট্রিবিউটর র্যাংকিং
git shortlog -sn
142 Rakib Hasan
38 Team Member A
12 Team Member B
-s মানে summary (শুধু সংখ্যা, কমিট লিস্ট না), -n মানে সংখ্যা অনুযায়ী sort (বেশি থেকে কম)। এটা রিলিজ নোটে "Contributors" সেকশন বানাতে, বা কোন অংশে কে সবচেয়ে বেশি সক্রিয় বোঝার জন্য ব্যবহার হয়।
# একটা নির্দিষ্ট রেঞ্জে (শেষ রিলিজ থেকে) কন্ট্রিবিউটর
git shortlog -sn v1.2.0..HEAD
git describe --tags --always — build version string
CI/CD পাইপলাইনে প্রতিটা build-কে একটা মানুষ-পাঠযোগ্য, ইউনিক ভার্সন স্ট্রিং দেওয়া দরকার। git describe কাছের tag থেকে current commit পর্যন্ত দূরত্ব গণনা করে একটা স্ট্রিং বানায়:
git describe --tags --always
# v1.2.0-14-ga1b2c3d
এই আউটপুট ভাঙলে: v1.2.0 (সবচেয়ে কাছের ancestor tag) + -14 (সেই tag-এর পর ১৪টা commit হয়েছে) + -g (git-এর সংক্ষিপ্ত রূপ) + a1b2c3d (বর্তমান commit-এর short hash)। যদি current commit-ই একটা tag হয়, শুধু v1.2.0 দেখাবে (এক্সট্রা suffix ছাড়া)।
--always কেন গুরুত্বপূর্ণ
যদি repo-তে কোনো tag-ই না থাকে, git describe (ফ্ল্যাগ ছাড়া) error দেয়। --always দিলে tag না পেলেও fallback হিসেবে শুধু commit hash রিটার্ন করে — CI স্ক্রিপ্ট কখনো crash করবে না।
রিয়েল-লাইফ ব্যবহার: Docker image ট্যাগিং
VERSION=$(git describe --tags --always)
docker build -t myapp:$VERSION .
এভাবে প্রতিটা Docker image build-এর সাথে ঠিক কোন commit থেকে বানানো হয়েছে তার একটা ট্রেসেবল রেকর্ড থাকে — production-এ কোনো bug এলে ঠিক কোন কোড চলছিল তা image ট্যাগ থেকেই সরাসরি বোঝা যায়।
ল্যাব: blame → show ফ্লো দিয়ে একটা বাগের উৎস খোঁজা
লক্ষ্য
একটা সিমুলেটেড bug-এর উৎস git blame দিয়ে খুঁজে বের করে git show দিয়ে পুরো প্রেক্ষাপট বুঝে নেওয়া, তারপর two-dot/three-dot diff দিয়ে branch তুলনা।
সেটআপ
mkdir -p ~/git-lab5 && cd ~/git-lab5 && git init
cat > payment.py << 'EOF'
def calculate_fee(amount):
return amount * 0.02
EOF
git add payment.py && git commit -m "add fee calculation"
sed -i 's/0.02/0.05/' payment.py
git add payment.py && git commit -m "update fee rate per new policy"
echo "# payment module" >> payment.py
git add payment.py && git commit -m "add module comment"
ধাপ ১ — blame দিয়ে সন্দেহজনক লাইন খোঁজা
git blame payment.py
# ^xxxxx (author, date, line 1) def calculate_fee(amount):
# xxxxxxx (author, date, line 2) return amount * 0.05
দেখুন লাইন ২-এর commit hash কোনটা — সেটাই fee rate বদলের commit।
ধাপ ২ — show দিয়ে পুরো প্রেক্ষাপট
git show <hash-from-blame>
# পুরো diff: 0.02 -> 0.05, message: "update fee rate per new policy"
ধাপ ৩ — branch তৈরি করে two-dot/three-dot পার্থক্য দেখা
git checkout -b experiment
sed -i 's/0.05/0.10/' payment.py
git add payment.py && git commit -m "experiment: higher fee"
git checkout main
echo "logging = true" >> payment.py
git add payment.py && git commit -m "add logging flag on main"
git diff main..experiment # সরাসরি বর্তমান দুই অবস্থার তুলনা (দুটো পরিবর্তনই দেখাবে)
git diff main...experiment # শুধু experiment branch তৈরি হওয়ার পর থেকে experiment-এ কী বদলেছে
যা লক্ষ্য করবেন
main..experiment আর main...experiment-এর আউটপুট আলাদা হবে কারণ main-এ experiment branch তৈরি হওয়ার পরও নতুন commit (logging flag) এসেছে — three-dot সেটা বাদ দিয়ে শুধু experiment-এর নিজস্ব পরিবর্তন দেখাবে।
ইন্টারভিউ প্রশ্ন — Module 21
প্র.১ — git diff main..feature-আর git diff main...feature-এর পার্থক্য কী?
উত্তর: main..feature (two-dot) main আর feature-এর বর্তমান অবস্থার সরাসরি তুলনা। main...feature (three-dot) প্রথমে তাদের common ancestor (merge-base) বের করে, তারপর সেখান থেকে feature পর্যন্ত পার্থক্য দেখায় — main-এ পরে আসা নতুন commit বাদ দিয়ে।
প্র.২ — git show <commit>:<path> কী রিটার্ন করে, আর এটা git diff-এর থেকে কীভাবে আলাদা?
উত্তর: এটা ওই commit-এর মুহূর্তে সেই ফাইলের সম্পূর্ণ কনটেন্ট রিটার্ন করে (একটা diff না, পুরো স্ন্যাপশট) — internally commit-এর tree object থেকে blob resolve করে। git diff কোনো একক ফাইলের কনটেন্ট দেখায় না, দুইটা অবস্থার মধ্যে পার্থক্য দেখায়।
প্র.৩ — টিমের একটা bulk code-formatting commit git blame-কে অকেজো করে দিয়েছে — কীভাবে ঠিক করবেন?
উত্তর: সেই formatting commit-এর hash .git-blame-ignore-revs ফাইলে যোগ করে git config blame.ignoreRevsFile .git-blame-ignore-revs সেট করুন — এরপর git blame (এবং GitHub-এর web UI) সেই commit-কে স্কিপ করে আসল content-পরিবর্তনকারী commit দেখাবে।
প্র.৪ — git describe --tags --always কমান্ডের আউটপুট v1.2.0-14-ga1b2c3d-এর অর্থ কী?
উত্তর: v1.2.0 হলো সবচেয়ে কাছের ancestor tag, 14 মানে সেই tag-এর পর ১৪টা commit হয়েছে, g git-এর সংক্ষিপ্ত রূপ, a1b2c3d বর্তমান commit-এর short hash। এটা CI/CD-তে build version string বানাতে ব্যবহৃত হয়।
প্র.৫ — git diff --no-index কেন দরকার হতে পারে, যেখানে সাধারণ diff -u কমান্ডও আছে?
উত্তর: --no-index Git repository ছাড়াই দুইটা যেকোনো ফাইল/ডিরেক্টরি তুলনা করতে দেয়, Git-এর উন্নত diff algorithm ও syntax-aware coloring ব্যবহার করে — diff -u-এর তুলনায় আউটপুট পড়া সহজ এবং একই টুলিং (যেমন git diff --color-words) ব্যবহার করা যায়।
প্র.৬ — git shortlog -sn-এর -s আর -n ফ্ল্যাগ কী করে?
উত্তর: -s (summary) শুধু প্রতিটা author-এর commit সংখ্যা দেখায়, পূর্ণ commit লিস্ট না। -n সেই সংখ্যা অনুযায়ী descending order-এ sort করে — সবচেয়ে বেশি অবদানকারী শীর্ষে আসে।
কর্নার কেস — Module 21
git blameএকটা লাইনকে "সর্বশেষ কে বদলেছে" তার কমিট দেখায়, "কে মূলত লিখেছে" তা না — একটা লাইন যদি অনেকবার সামান্য বদলানো হয়ে থাকে (এমনকি শুধু একটা indentation ফিক্সেও), blame সবসময় সর্বশেষ পরিবর্তনকারীকে দেখাবে, মূল লেখককে না — এই জন্যই-wআর--ignore-revs-fileগুরুত্বপূর্ণ।git diff main...feature-এ merge-base বের করতে ব্যর্থ হলে (দুইটা branch-এর কোনো common history না থাকলে) error আসে, বিশেষ করেgit checkout --orphanদিয়ে তৈরি সম্পূর্ণ আলাদা history-র branch-এ।git showকোনো argument ছাড়া চালালে সবসময়HEADদেখায়, staged বা unstaged পরিবর্তন না — নতুনরা মাঝে মাঝে ভুলে ভাবেন এটা working directory-র পরিবর্তন দেখাবে, কিন্তু staged/unstaged diff দেখতেgit diff/git diff --stagedলাগবে।git describeট্যাগ ছাড়া repo-তে ফ্ল্যাগ ছাড়া চালালে fatal error দেয় ("No names found") —--alwaysফ্ল্যাগ ছাড়া এটা স্ক্রিপ্টে ব্যবহার করলে CI pipeline crash করতে পারে যদি কোনো কারণে tag missing থাকে (যেমন shallow clone-এ tag আনা না হলে)।
MODULE 22: bisect
কল্পনা করুন একটা bug production-এ ধরা পড়েছে, কিন্তু ঠিক কোন commit-এ এটা ঢুকেছে তা অজানা — আর মাঝে হাজার commit আছে। প্রতিটা commit ম্যানুয়ালি চেক করা অবাস্তব। git bisect এই সমস্যার সমাধান — একটা বিল্ট-ইন বাইনারি সার্চ টুল যা মাত্র log₂(n) ধাপে bug-introducing commit খুঁজে বের করে। এই মডিউলে bisect-এর ম্যানুয়াল আর অটোমেটেড (bisect run) — দুই রূপই কভার হবে।
bisect কী সমস্যা সমাধান করে
সমস্যা
একটা production bug রিপোর্ট এসেছে: "checkout page ভেঙে গেছে"। জানা যায় ২ সপ্তাহ আগে এটা ঠিক ছিল, এখন নেই — মাঝে ২০০টা commit হয়ে গেছে। কোন commit-এ bug ঢুকেছে তা খুঁজে বের করাই সমস্যা — bug-টা ঠিক হলে root cause বোঝা সহজ হয়ে যায় (ওই commit-এর diff দেখেই বোঝা যায় কী বদলেছে)।
নাইভ সমাধানের অক্ষমতা
২০০টা commit একে একে (linear search) চেক করলে গড়ে ১০০টা চেক করতে হতে পারে — প্রতিটা চেকে checkout + build + test = কয়েক মিনিট ধরলে এটা কয়েক ঘণ্টার কাজ।
git bisect-এর সমাধান
git bisect একটা binary search algorithm প্রয়োগ করে commit history-র ওপর। আপনি শুধু একটা "known good" আর একটা "known bad" commit বলে দেন, bisect মাঝখানের commit-এ checkout করে, আপনি টেস্ট করে বলেন good/bad, bisect পরের অর্ধেক অংশে আবার একই কাজ করে — যতক্ষণ না ঠিক এক commit বাকি থাকে, যেটাই bug-introducing commit।
রিয়েল-লাইফ ইমপ্যাক্ট
২০০টা commit-এর জন্য linear search-এ গড়ে ১০০টা চেক লাগত, bisect-এ মাত্র log₂(200) ≈ 8 টা চেক লাগবে — ১২ গুণেরও বেশি দ্রুত। এই পার্থক্য যত বড় হয় history, তত বেশি প্রকট হয় (২০০০ commit-এ মাত্র log₂(2000) ≈ 11 ধাপ)।
এক লাইনে
bisect একটা "20 প্রশ্নে সংখ্যা অনুমান" গেমের মতো, শুধু সংখ্যার বদলে commit history নিয়ে খেলা হয়।
বাইনারি সার্চ অ্যালগরিদম হিসেবে bisect — কেন O(log n)
অ্যালগরিদমের মূল ধারণা
ধরুন commit history-টা একটা sorted array (ধরে নেওয়া হয় bug একবার ঢোকার পর সব commit-এই থেকে যায় — monotonicity assumption, নিচে বিস্তারিত)। bisect প্রতিবার সার্চ স্পেসের ঠিক মাঝখানের commit বেছে নেয়:
good ----------------- ? ----------------- bad
[ 100 commit ] [ 100 commit ]
মাঝখানের commit টেস্ট করে good/bad বলার পর, bisect সার্চ স্পেস অর্ধেক করে ফেলে — good হলে ডান অর্ধেকে খোঁজা চলবে, bad হলে বাম অর্ধেকে।
গাণিতিক ভিত্তি
nটা commit-এর মধ্যে খুঁজতে সর্বোচ্চ ⌈log₂(n)⌉ ধাপ লাগে, কারণ প্রতিটা ধাপে সার্চ স্পেস অর্ধেক হয়ে যায়:
| Commit সংখ্যা | সর্বোচ্চ ধাপ (log₂) |
|---|---|
| 10 | 4 |
| 100 | 7 |
| 1,000 | 10 |
| 1,000,000 | 20 |
Monotonicity Assumption — গুরুত্বপূর্ণ পূর্বশর্ত
bisect ধরে নেয় bug একবার ঢোকার পর সরে যায় না (একটা single "boundary" আছে good আর bad-এর মধ্যে)। বাস্তবে যদি bug flaky হয় (মাঝে মাঝে দেখা দেয়, মাঝে মাঝে না) অথবা একটা bug ফিক্স হয়ে আবার অন্য কারণে ফিরে আসে (non-monotonic history), bisect ভুল ফলাফল দিতে পারে — এই সীমাবদ্ধতা মাথায় রাখা জরুরি।
git bisect-এর ভেতরে কী ট্র্যাক হয়
bisect internally .git/BISECT_LOG, .git/BISECT_START, আর refs/bisect/* ব্যবহার করে সার্চ স্পেসের বর্তমান অবস্থা ট্র্যাক করে — এই জন্যই git bisect মাঝপথে থামিয়ে অন্য কাজ করে পরে ফিরে এসেও চালিয়ে যাওয়া যায়, রাজ্য হারায় না।
bisect কমান্ড ফ্যামিলি: start/good/bad/skip/reset
পুরো ওয়ার্কফ্লো
git bisect start
git bisect bad # বর্তমান commit খারাপ (bug আছে)
git bisect good v1.5.0 # v1.5.0 ট্যাগে bug ছিল না
# Git এখন মাঝখানের commit-এ checkout করে দেবে
# আপনি ম্যানুয়ালি টেস্ট করে বলবেন:
git bisect good # অথবা
git bisect bad
# এভাবে চলতেই থাকবে যতক্ষণ না bisect বলে:
# "a1b2c3d is the first bad commit"
প্রতিটা সাব-কমান্ড
| কমান্ড | কাজ |
|---|---|
git bisect start |
bisect সেশন শুরু করা |
git bisect bad [<commit>] |
নির্দিষ্ট (বা বর্তমান) commit-কে "bad" হিসেবে চিহ্নিত করা |
git bisect good [<commit>] |
নির্দিষ্ট (বা বর্তমান) commit-কে "good" হিসেবে চিহ্নিত করা |
git bisect skip |
এই commit টেস্ট করা সম্ভব না (যেমন build ব্যর্থ হচ্ছে অসম্পর্কিত কারণে) — এড়িয়ে যাওয়া |
git bisect reset |
bisect সেশন শেষ করে আসল branch-এ ফিরে আসা |
git bisect log |
এখন পর্যন্ত good/bad সিদ্ধান্তের ইতিহাস দেখা |
git bisect visualize |
বর্তমান সার্চ স্পেস graph আকারে দেখা |
skip কেন দরকার
মাঝে মাঝে একটা মাঝপথের commit টেস্ট করা যায় না — হয়তো সেই commit-এ কোড কম্পাইলই হয় না (অসম্পর্কিত কারণে), অথবা সেই সময়ে একটা dependency ভাঙা ছিল। git bisect skip সেই commit বাদ দিয়ে bisect-কে পাশের একটা commit চেষ্টা করতে বলে, পুরো প্রসেস আটকে না গিয়ে।
git bisect skip
# পরবর্তী কাছের commit-এ চলে যাবে টেস্টের জন্য
git bisect reset-এর গুরুত্ব
bisect চলাকালীন repo detached HEAD অবস্থায় থাকে (একের পর এক commit checkout হচ্ছে)। কাজ শেষে git bisect reset অবশ্যই চালাতে হবে, নাহলে repo সেই detached অবস্থায় আটকে থাকবে — আসল branch-এ ফিরে আসা হবে না।
git bisect run দিয়ে সম্পূর্ণ অটোমেশন
ম্যানুয়াল bisect-এর সীমাবদ্ধতা
প্রতিটা ধাপে ম্যানুয়ালি checkout, build, test করে good/bad বলা সময়সাপেক্ষ — বিশেষ করে যদি টেস্ট প্রক্রিয়াটা সহজেই স্ক্রিপ্ট করা যায় (একটা unit test চালানো, একটা exit code চেক করা)।
bisect run — একটা স্ক্রিপ্ট দিয়ে পুরো প্রসেস অটোমেট করা
git bisect start
git bisect bad HEAD
git bisect good v1.5.0
git bisect run ./test-script.sh
test-script.sh একটা exit code রিটার্ন করবে: 0 মানে good, নন-জিরো (১২৫ ছাড়া) মানে bad, 125 মানে skip (untestable)। bisect স্বয়ংক্রিয়ভাবে প্রতিটা মাঝখানের commit-এ checkout করে স্ক্রিপ্ট চালাবে, exit code দেখে good/bad সিদ্ধান্ত নেবে, এবং শেষে সরাসরি bug-introducing commit বলে দেবে — কোনো ম্যানুয়াল হস্তক্ষেপ ছাড়াই।
রিয়েল-লাইফ স্ক্রিপ্ট উদাহরণ
#!/bin/bash
# test-script.sh
pip install -q -e . 2>/dev/null
python -m pytest tests/test_checkout.py::test_apply_discount -q
exit $?
git bisect start HEAD v1.5.0
git bisect run ./test-script.sh
# Output চলার পর:
# a1b2c3d is the first bad commit
কেন এটা এত শক্তিশালী
bisect run মানুষের বিচার-বুদ্ধি সরিয়ে সম্পূর্ণ deterministic test দিয়ে replace করে — এটা CI pipeline-এও ব্যবহার করা যায় (একটা nightly job যা automatically একটা regression-এর root cause খুঁজে বের করে টিমকে জানিয়ে দেয়)। একটা জটিল, কয়েক ঘণ্টার ম্যানুয়াল ডিবাগিং প্রসেস এভাবে কয়েক মিনিটের একটা স্বয়ংক্রিয় কমান্ডে পরিণত হয়ে যায়।
সতর্কতা
স্ক্রিপ্টটা অবশ্যই deterministic হতে হবে এবং সাইড-ইফেক্ট মুক্ত (একটা পুরনো commit-এ চালালে যেন সিস্টেমের অন্য কোনো state নষ্ট না হয়) — না হলে bisect ভুল ফলাফল দিতে পারে বা bisect সেশনই নষ্ট হয়ে যেতে পারে।
ল্যাব: ৫-৬ কমিট দূরে একটা বাগ ঢুকিয়ে bisect run দিয়ে অটো-খোঁজা
লক্ষ্য
ইচ্ছাকৃতভাবে একটা bug কয়েক commit আগে ঢুকিয়ে, একটা test script লিখে, git bisect run দিয়ে সম্পূর্ণ স্বয়ংক্রিয়ভাবে সেটা খুঁজে বের করা।
সেটআপ — একটা ছোট ফাংশন আর টেস্ট
mkdir -p ~/git-lab6 && cd ~/git-lab6 && git init
cat > calc.py << 'EOF'
def add(a, b):
return a + b
EOF
git add calc.py && git commit -m "c0: initial add function"
cat > test_calc.py << 'EOF'
from calc import add
def test_add():
assert add(2, 3) == 5
EOF
git add test_calc.py && git commit -m "c1: add test"
echo "# comment c2" >> calc.py && git add calc.py && git commit -m "c2: unrelated comment"
echo "# comment c3" >> calc.py && git add calc.py && git commit -m "c3: another comment"
# এখানে ইচ্ছাকৃতভাবে বাগ ঢোকানো হলো
sed -i 's/return a + b/return a - b/' calc.py
git add calc.py && git commit -m "c4: refactor add (accidentally broke it)"
echo "# comment c5" >> calc.py && git add calc.py && git commit -m "c5: another unrelated comment"
echo "# comment c6" >> calc.py && git add calc.py && git commit -m "c6: final comment"
ধাপ ১ — test script বানানো
cat > test-script.sh << 'EOF'
#!/bin/bash
python3 -m pytest test_calc.py -q
exit $?
EOF
chmod +x test-script.sh
ধাপ ২ — bisect run চালানো
git bisect start
git bisect bad HEAD
git bisect good c0-commit-hash # প্রথম commit-এর hash বসান (git log --oneline দিয়ে বের করুন)
git bisect run ./test-script.sh
প্রত্যাশিত আউটপুট
running ./test-script.sh
...
c4-commit-hash is the first bad commit
commit c4-commit-hash
c4: refactor add (accidentally broke it)
যা লক্ষ্য করবেন
মোট ৭টা commit-এর মধ্যে bisect মাত্র ৩টা চেক করেই bug commit খুঁজে বের করেছে (log₂(7) ≈ 3) — linear search করলে গড়ে ৩-৪টা লাগত, কিন্তু ১০০০ commit হলে এই পার্থক্য বিশাল হয়ে যেত। শেষে git bisect reset চালাতে ভুলবেন না।
ইন্টারভিউ প্রশ্ন — Module 22
প্র.১ — git bisect কেন O(log n) সময়ে bug-introducing commit খুঁজে পায়, linear search-এ O(n) কেন লাগবে?
উত্তর: bisect প্রতিটা ধাপে সার্চ স্পেস অর্ধেক করে ফেলে (একটা মাঝখানের commit টেস্ট করে good/bad অনুযায়ী বাম বা ডান অর্ধেক বাদ দেয়) — এটা ক্লাসিক বাইনারি সার্চ অ্যালগরিদম। Linear search প্রতিটা commit একে একে চেক করে, তাই গড়ে n/2 চেক লাগে।
প্র.২ — git bisect কাজ করার জন্য কোন পূর্বশর্ত (assumption) প্রয়োজন?
উত্তর: Monotonicity — bug একবার ঢোকার পর history-তে একটা single boundary তৈরি করে, বারবার আসা-যাওয়া করে না। flaky bug বা bug একবার ফিক্স হয়ে আবার ফিরে এলে (non-monotonic) bisect ভুল ফলাফল দিতে পারে।
প্র.৩ — git bisect skip কখন ব্যবহার করবেন?
উত্তর: যখন মাঝপথের একটা নির্দিষ্ট commit টেস্ট করাই সম্ভব না — যেমন সেই commit-এ কোড অসম্পর্কিত কারণে কম্পাইল হয় না, বা কোনো dependency তখন ভাঙা ছিল। এটা সেই commit বাদ দিয়ে bisect-কে পাশের একটা commit-এ চেষ্টা করতে বলে।
প্র.৪ — git bisect run স্ক্রিপ্টের exit code-এর অর্থ কী?
উত্তর: 0 মানে good (bug নেই), নন-জিরো (১২৫ বাদে) মানে bad (bug আছে), আর 125 মানে এই commit টেস্ট করা সম্ভব না (skip)। bisect run স্ক্রিপ্ট নিজে চালিয়ে exit code দেখে স্বয়ংক্রিয়ভাবে good/bad/skip সিদ্ধান্ত নেয়।
প্র.৫ — bisect সেশন চলাকালীন repo কোন অবস্থায় থাকে, আর কাজ শেষে কী করা জরুরি?
উত্তর: detached HEAD অবস্থায় থাকে, কারণ bisect একের পর এক নির্দিষ্ট commit-এ checkout করে। কাজ শেষে git bisect reset চালানো জরুরি — এটা bisect সেশন বন্ধ করে আসল branch-এ ফিরিয়ে আনে।
প্র.৬ — git bisect run-এর টেস্ট স্ক্রিপ্ট কেন deterministic হওয়া জরুরি?
উত্তর: bisect স্ক্রিপ্টের exit code-এর ওপর সম্পূর্ণ নির্ভর করে সিদ্ধান্ত নেয় — স্ক্রিপ্ট যদি নন-deterministic হয় (flaky test) বা সাইড-ইফেক্ট রেখে যায় (যেমন একটা পুরনো commit-এ চালানোর ফলে database state নষ্ট করে), bisect ভুল ফলাফল দিতে পারে বা পুরো সেশন অবিশ্বাস্য হয়ে যেতে পারে।
কর্নার কেস — Module 22
- bisect মাঝপথে থামিয়ে অন্য কাজ করলেও রাজ্য হারায় না —
.git/BISECT_LOGআরrefs/bisect/*-এ প্রগ্রেস সেভ থাকে,git bisect logদিয়ে এখন পর্যন্ত কী কী good/bad মার্ক করা হয়েছে তা দেখা যায়, এমনকি আরেকটা টার্মিনাল সেশন থেকেও। git bisect start <bad> <good>— আর্গুমেন্টের ক্রম উল্টালে ভুল ফলাফল আসবে — প্রথমে bad, তারপর good, এই ক্রম মেনে চলা জরুরি; উল্টো দিলে Git confusing error দেবে বা ভুল দিকে সার্চ করবে।- build/dependency পরিবর্তন হওয়া পুরনো commit-এ bisect চালালে false positive আসতে পারে — যদি পুরনো commit-এ একটা dependency ভিন্ন ভার্সনের ছিল যা এখন আর নেই, টেস্ট ব্যর্থ হবে bug-এর কারণে না, পরিবেশগত অসামঞ্জস্যের কারণে —
git bisect skipদিয়ে এই commit বাদ দেওয়া উচিত, ভুল commit-কে "bad" বলা ঠিক না। git bisect runস্ক্রিপ্টেgit commit/git checkoutজাতীয় কমান্ড রাখা বিপজ্জনক — bisect নিজেই checkout ম্যানেজ করে; স্ক্রিপ্টের ভেতর থেকে repo state পরিবর্তন করলে bisect-এর নিজস্ব ট্র্যাকিং এলোমেলো হয়ে যেতে পারে।
MODULE 23: Tags
একটা commit history সময়ের সাথে সামনে এগোতেই থাকে, কিন্তু কখনো কখনো একটা নির্দিষ্ট মুহূর্তকে স্থায়ীভাবে চিহ্নিত করে রাখা দরকার — "এই commit-টাই আমাদের v2.0.0 রিলিজ"। branch দিয়ে এটা করা যায় না ভালোভাবে, কারণ branch pointer নড়তে থাকে। এই কাজের জন্য Git-এর tag — একটা স্থায়ী, (সাধারণত) অপরিবর্তনীয় রেফারেন্স। এই মডিউলে lightweight বনাম annotated tag-এর ভেতরের পার্থক্য, signed tag, আর SemVer কনভেনশন কভার হবে।
Tag কী, কেন দরকার, branch থেকে পার্থক্য
সমস্যাটা কী
একটা branch pointer প্রতিটা নতুন commit-এর সাথে এগিয়ে যায় — main আজকের commit-এ পয়েন্ট করছে, কালকের নতুন commit এলে সেটাতেই সরে যাবে। কিন্তু "v2.0.0 রিলিজ ঠিক কোন commit ছিল" — এই তথ্য কখনো বদলানো উচিত না। branch দিয়ে এই স্থায়িত্ব নিশ্চিত করা যায় না।
Tag-এর সমাধান
একটা tag একটা নির্দিষ্ট commit-এ স্থায়ীভাবে আটকে থাকা একটা নাম — এটা branch-এর মতো নতুন commit-এর সাথে এগোয় না। একবার v2.0.0 ট্যাগ একটা commit-এ বসিয়ে দিলে, সেই নাম চিরকাল সেই একই commit নির্দেশ করবে (যদি না ইচ্ছাকৃতভাবে ট্যাগ মুছে আবার তৈরি করা হয়)।
রিয়েল-লাইফ উদাহরণ
git tag v2.0.0
git push origin v2.0.0
এখন যে কেউ এই repo clone করলে git checkout v2.0.0 দিয়ে ঠিক সেই মুহূর্তের কোডে ফিরে যেতে পারবে — production-এ কোন কোড চলছিল তা পুনরুৎপাদন করার জন্য এটাই standard পদ্ধতি। একটা incident-এর সময় "production-এ কোন version চলছে" জানা থাকলে সরাসরি সেই tag checkout করে সমস্যা reproduce করা যায়।
Branch বনাম Tag — এক টেবিলে
| Branch | Tag | |
|---|---|---|
| নড়াচড়া | নতুন commit এলে এগিয়ে যায় | স্থির থাকে (একবার তৈরি হলে) |
| উদ্দেশ্য | চলমান development-এর লাইন | একটা নির্দিষ্ট মুহূর্ত চিহ্নিতকরণ |
| সাধারণ ব্যবহার | feature কাজ, main development | release marking, milestone |
দুই প্রকার — সংক্ষিপ্ত প্রিভিউ
Git-এ দুই ধরনের tag আছে — lightweight (শুধু একটা pointer) আর annotated (নিজেই একটা পূর্ণাঙ্গ object, author/date/message সহ) — পরের লিফে ভেতরের পার্থক্য বিস্তারিত।
Lightweight বনাম Annotated Tag — ইন্টারনাল পার্থক্য
Lightweight Tag — শুধু একটা ref
git tag v1.0.0-lw
এটা internally .git/refs/tags/v1.0.0-lw-এ শুধু একটা commit hash লিখে রাখে — অবিকল একটা branch pointer-এর মতো, শুধু এটা নড়ে না। এর নিজস্ব কোনো metadata (কে তৈরি করেছে, কবে, কেন) নেই — সরাসরি সেই commit-এর author/date/message-ই দেখা যাবে।
Annotated Tag — একটা সম্পূর্ণ Git Object
git tag -a v1.0.0 -m "First stable release"
এটা Git-এর object database-এ (Phase 1-এর object model মনে করুন) একটা সম্পূর্ণ নতুন tag object তৈরি করে — যার নিজস্ব SHA-1/SHA-256 hash, tagger name/email, tag date, আর tag message আছে। এই tag object-টা তারপর সেই নির্দিষ্ট commit-কে পয়েন্ট করে। অর্থাৎ refs/tags/v1.0.0 আসলে tag object-কে পয়েন্ট করছে, tag object commit-কে পয়েন্ট করছে — একটা extra ইনডিরেকশন লেয়ার।
git cat-file -p v1.0.0
object a1b2c3d4e5f6...
type commit
tag v1.0.0
tagger Rakib Hasan <rakib@example.com> 1755781200 +0600
First stable release
পার্থক্য এক টেবিলে
| Lightweight | Annotated | |
|---|---|---|
| Object তৈরি হয় | না — শুধু ref | হ্যাঁ — tag object (commit-এর মতোই object database-এ) |
| Metadata (tagger, date, message) | নেই | আছে |
git describe-এর ডিফল্ট আচরণ |
ব্যবহার হয়, কিন্তু annotated প্রাধান্য পায় | প্রাধান্য পায় |
Signing সম্ভব (-s) |
না | হ্যাঁ |
| সাধারণ ব্যবহার | ব্যক্তিগত/সাময়িক মার্কার | official release |
কেন annotated tag প্রেফার করা হয়
release marking-এর মতো গুরুত্বপূর্ণ কাজে annotated tag-ই standard practice — কারণ এটা কে, কবে, কেন ট্যাগ করেছে তার একটা স্থায়ী রেকর্ড রাখে, আর signing সাপোর্ট করে (পরের লিফে)। Git নিজেও ডিফল্টে git tag -a না দিয়ে শুধু -m দিলেও annotated tag বানায়, কিন্তু -m ছাড়া git tag <name> দিলে lightweight হয়ে যায় — এই সূক্ষ্ম পার্থক্যটা নতুনরা প্রায়ই মিস করেন।
Tag ম্যানেজমেন্ট: -d, -l গ্লব প্যাটার্ন, পুরনো কমিটে ট্যাগ
ট্যাগ মোছা
git tag -d v1.0.0-beta
শুধু local ট্যাগ মোছে — remote-এ push করা থাকলে সেটা আলাদাভাবে মুছতে হবে (পরের লিফে)।
ট্যাগ লিস্ট করা, গ্লব প্যাটার্ন দিয়ে ফিল্টার
git tag -l # সব ট্যাগ
git tag -l "v1.*" # শুধু v1.x সিরিজ
git tag -l "*-beta" # শুধু beta ট্যাগ
git tag -l --sort=-v:refname # semantic version অনুযায়ী descending sort
গ্লব প্যাটার্ন বড় প্রজেক্টে (শত শত tag) নির্দিষ্ট রিলিজ সিরিজ খুঁজতে অপরিহার্য — যেমন সব v2.* রিলিজ একসাথে দেখা।
পুরনো commit-এ ট্যাগ দেওয়া
ট্যাগ সবসময় বর্তমান HEAD-এ দিতে হবে এমন না — যেকোনো পুরনো commit-এও দেওয়া যায়, hash উল্লেখ করে:
git log --oneline
# e4f5g6h fix critical bug
# a1b2c3d add feature X (এটাই ছিল সেই সময়ের রিলিজ পয়েন্ট, ভুলে ট্যাগ করা হয়নি)
git tag -a v1.1 a1b2c3d -m "Retroactively tagging v1.1 release point"
রিয়েল-লাইফ সিনারিও
একটা টিম মনে করে তারা v2.0.0 ট্যাগ দিতে ভুলে গেছে, কিন্তু জানে ঠিক কোন commit থেকে সেই রিলিজ deploy হয়েছিল (deployment log থেকে hash পাওয়া গেছে)। git tag -a v2.0.0 <that-hash> -m "..." দিয়ে retroactively সঠিক জায়গায় ট্যাগ বসানো যায় — HEAD-এ ভুল জায়গায় ট্যাগ দিতে হয় না।
ওভাররাইট প্রতিরোধ
git tag v1.0.0
# fatal: tag 'v1.0.0' already exists
বিদ্যমান ট্যাগ নাম আবার তৈরি করতে গেলে Git error দেয় — ইচ্ছাকৃতভাবে ওভাররাইট করতে -f/--force লাগবে, যেটা সতর্কতার সাথে ব্যবহার করা উচিত (কারণ কেউ আগেই সেই ট্যাগ fetch করে থাকলে তার local কপি stale হয়ে যাবে)।
Signed Tags (-s) প্রিভিউ ও git verify-tag
Signed Tag কী
git tag -s v2.0.0 -m "Signed release v2.0.0"
এটা annotated tag-এরই একটা সম্প্রসারণ — tag object-এ tagger-এর GPG (বা SSH key-based) স্বাক্ষর যোগ হয়। এটা প্রমাণ করে যে ট্যাগটা সত্যিই সেই ব্যক্তি তৈরি করেছেন যার কাছে সংশ্লিষ্ট প্রাইভেট key আছে — নাম স্পুফ করা যায় না।
কেন এটা গুরুত্বপূর্ণ (সংক্ষেপে — বিস্তারিত Security ফেজে)
ওপেন সোর্স প্রজেক্টে বা সাপ্লাই-চেইন-সংবেদনশীল সফটওয়্যারে, একটা release tag আসলেই "official maintainer" থেকে এসেছে তা যাচাই করা জরুরি — কেউ যদি repo-তে write access পেয়ে যায় (compromised account), signed tag ছাড়া সে সহজেই একটা ভুয়া "official release" ট্যাগ বসিয়ে দিতে পারত।
যাচাই করা
git verify-tag v2.0.0
gpg: Signature made ...
gpg: Good signature from "Rakib Hasan <rakib@example.com>"
git verify-tag কমান্ড tag object-এর স্বাক্ষর যাচাই করে — GPG public key ring-এ সংশ্লিষ্ট public key থাকতে হবে যাচাই সফল হতে।
রিয়েল-লাইফ প্রেক্ষাপট
Linux kernel-এর মতো বড় ওপেন সোর্স প্রজেক্টে প্রতিটা রিলিজ tag Linus Torvalds বা তার মনোনীত maintainer-দের GPG key দিয়ে signed — কেউ যদি একটা compromised mirror থেকে কোড টানে, signed tag verify করলে বোঝা যাবে সেটা আসল maintainer থেকেই এসেছে কিনা। এই বিস্তারিত মেকানিজম, key management, আর CI-তে automatic verification — এসব Phase-এর Security মডিউলে গভীরে আলোচনা হবে।
এখনকার জন্য যথেষ্ট মানসিক মডেল
Signed tag = annotated tag + cryptographic প্রমাণ "এটা আমিই বানিয়েছি"।
Tag Push করা ও রিমোট থেকে মোছা
ট্যাগ push করা — ডিফল্টে হয় না
গুরুত্বপূর্ণ: git push সাধারণ ফর্মে (কোনো ফ্ল্যাগ ছাড়া) tag push করে না — এটা ইচ্ছাকৃতভাবে আলাদা করে রাখা হয়েছে যাতে ব্যক্তিগত/সাময়িক tag ভুলবশত সবার কাছে চলে না যায়।
git push origin v2.0.0 # একটা নির্দিষ্ট tag push
git push origin --tags # সব local tag push (bulk)
--follow-tags — একটা মাঝামাঝি বিকল্প
git push --follow-tags
এটা branch commit push করার সাথে সাথে শুধু সেইসব annotated tag-ও push করে যেগুলো push হতে যাওয়া commit-গুলোর মধ্যে reachable। lightweight tag push করে না, আর অপ্রাসঙ্গিক পুরনো tag-ও push করে না — --tags (সব bulk push) আর কিছুই push না করা, এই দুইয়ের মাঝামাঝি একটা নিরাপদ ডিফল্ট।
git config --global push.followTags true # স্থায়ীভাবে ডিফল্ট করে রাখা
Remote থেকে tag মোছা
git push origin --delete v1.0.0-beta
# বিকল্প সিনট্যাক্স (refspec-based, কম common)
git push origin :refs/tags/v1.0.0-beta
মনে রাখবেন — remote থেকে tag মোছা মানে শুধু remote-এর refs/tags/-এ entry মোছা, যারা আগেই fetch করে নিয়েছে তাদের local কপিতে এখনো থেকে যাবে যতক্ষণ না তারাও নিজেরা git tag -d চালায়।
রিয়েল-লাইফ ওয়ার্কফ্লো
# একটা রিলিজ তৈরি ও প্রকাশ করা
git tag -a v3.0.0 -m "Release v3.0.0: notification engine rewrite"
git push origin v3.0.0
# ভুল ট্যাগ সংশোধন (ভুল commit-এ দিয়ে ফেলেছিলেন)
git tag -d v3.0.0
git push origin --delete v3.0.0
git tag -a v3.0.0 <correct-hash> -m "Release v3.0.0: notification engine rewrite"
git push origin v3.0.0
SemVer সংক্ষেপে — Tagging-এ কীভাবে প্রয়োগ হয়
SemVer ফরম্যাট
Semantic Versioning (MAJOR.MINOR.PATCH, যেমন v2.4.1) একটা সর্বজনীন কনভেনশন যা ভার্সন নম্বর দিয়েই বলে দেয় পরিবর্তনটা কতটা "বিপজ্জনক":
| অংশ | কখন বাড়ানো হয় | উদাহরণ |
|---|---|---|
| MAJOR | Breaking change — পুরনো API/behavior-এর সাথে অসামঞ্জস্যপূর্ণ | v1.x.x → v2.0.0 |
| MINOR | নতুন ফিচার, backward-compatible | v2.3.x → v2.4.0 |
| PATCH | শুধু bugfix, backward-compatible | v2.4.0 → v2.4.1 |
কেন এই কনভেনশন গুরুত্বপূর্ণ
একটা লাইব্রেরির user শুধু ভার্সন নম্বর দেখেই সিদ্ধান্ত নিতে পারে upgrade নিরাপদ কিনা — v2.4.0 থেকে v2.4.1-এ upgrade করলে কোড ভাঙার ঝুঁকি প্রায় নেই (শুধু bugfix), কিন্তু v1.x থেকে v2.0.0-এ upgrade করার আগে changelog পড়া উচিত (breaking change সম্ভাবনা)। dependency manager (npm, pip) SemVer রেঞ্জ (^2.4.0, ~2.4.0) ব্যবহার করে স্বয়ংক্রিয়ভাবে "নিরাপদ" আপডেট নির্ধারণ করে।
Git tagging-এ প্রয়োগ
git tag -a v2.4.1 -m "Fix null pointer in notification retry logic"
git tag -a v2.5.0 -m "Add support for SMS notifications"
git tag -a v3.0.0 -m "BREAKING: remove legacy notification API"
v prefix (যেমন v2.4.1 বনাম 2.4.1) দুটোই common, কিন্তু একটা repo-তে সামঞ্জস্যপূর্ণ থাকা উচিত — নাহলে git describe, sort, বা tooling বিভ্রান্ত হতে পারে।
Pre-release ট্যাগ
git tag -a v3.0.0-rc.1 -m "Release candidate 1 for v3.0.0"
git tag -a v3.0.0-beta.2 -m "Beta 2"
SemVer একটা optional pre-release suffix (-rc.1, -beta.2, -alpha) সাপোর্ট করে যা precedence-এ সংশ্লিষ্ট stable ভার্সনের আগে আসে (v3.0.0-rc.1 < v3.0.0) — dependency resolver এগুলোকে সাধারণত ডিফল্টে বাদ দেয় যতক্ষণ না স্পষ্টভাবে চাওয়া হয়।
রিয়েল-লাইফ ইন্টিগ্রেশন
CI/CD পাইপলাইনে একটা নতুন SemVer tag push হওয়া প্রায়ই একটা স্বয়ংক্রিয় release trigger — GitHub Actions on: push: tags: ['v*.*.*'] দিয়ে ট্যাগ push detect করে automatic build, changelog generation, আর npm/PyPI publish চালায়।
ল্যাব: Lightweight vs Annotated, Retroactive Tagging, Push/Delete
লক্ষ্য
দুই ধরনের ট্যাগ তৈরি করে internal পার্থক্য দেখা, পুরনো commit-এ ট্যাগ দেওয়া, এবং push/delete cycle সম্পূর্ণ করা।
সেটআপ
mkdir -p ~/git-lab7 && cd ~/git-lab7 && git init --initial-branch=main
git commit --allow-empty -m "c0: init"
git commit --allow-empty -m "c1: feature A"
git commit --allow-empty -m "c2: feature B"
git commit --allow-empty -m "c3: bugfix"
ধাপ ১ — দুই ধরনের ট্যাগ তৈরি ও তুলনা
git tag v1.0.0-lw # lightweight
git tag -a v1.0.0 -m "First release" # annotated
git cat-file -t v1.0.0-lw # commit
git cat-file -t v1.0.0 # tag <- ভিন্ন object type!
git show v1.0.0-lw # সরাসরি commit info
git show v1.0.0 # প্রথমে tag object info (tagger/date/message), তারপর commit info
ধাপ ২ — পুরনো commit-এ retroactive tag
git log --oneline
# ধরুন c1 (feature A)-এর hash হলো xxxxx
git tag -a v0.9.0 xxxxx -m "Retroactive tag for feature A milestone"
git show v0.9.0
ধাপ ৩ — remote সিমুলেট করে push/delete
git init --bare ~/git-lab7-remote.git
git remote add origin ~/git-lab7-remote.git
git push origin main
git push origin v1.0.0 # শুধু annotated tag push
git push origin --tags # সব ট্যাগ (lightweight-সহ) push
# ভুল করে দেওয়া ট্যাগ মোছা
git push origin --delete v1.0.0-lw
git tag -d v1.0.0-lw
চ্যালেঞ্জ
git push --follow-tags টেস্ট করুন — নতুন commit করে একটা নতুন annotated tag বসান কিন্তু --tags না দিয়ে শুধু git push --follow-tags চালান, দেখুন commit-এর সাথে সেই tag-ও push হয়ে যায় কিনা।
ইন্টারভিউ প্রশ্ন — Module 23
প্র.১ — lightweight আর annotated tag-এর ভেতরের পার্থক্য কী?
উত্তর: lightweight tag শুধু refs/tags/-এ একটা commit hash-এর pointer — কোনো নতুন object তৈরি হয় না। annotated tag Git-এর object database-এ একটা সম্পূর্ণ নতুন tag object তৈরি করে যাতে tagger, date, message থাকে, এবং সেই object commit-কে পয়েন্ট করে — একটা এক্সট্রা ইনডিরেকশন লেয়ার।
প্র.২ — git push করলে ট্যাগ push হয় না কেন — এটা কি একটা bug?
উত্তর: এটা ইচ্ছাকৃত ডিজাইন সিদ্ধান্ত — ট্যাগ প্রায়ই ব্যক্তিগত বা সাময়িক মার্কার হতে পারে, তাই ডিফল্টে push না করে ব্যবহারকারীকে explicit ভাবে git push origin <tag> বা --tags বলতে হয়, যাতে অনিচ্ছাকৃতভাবে ট্যাগ শেয়ার না হয়ে যায়।
প্র.৩ — git push --tags আর git push --follow-tags-এর পার্থক্য কী?
উত্তর: --tags সব local tag (lightweight এবং annotated, সব — প্রাসঙ্গিক-অপ্রাসঙ্গিক নির্বিশেষে) push করে। --follow-tags শুধু সেইসব annotated tag push করে যেগুলো push হওয়া commit-গুলোর মধ্যে reachable — একটা নিরাপদ, বেশি নির্বাচনী বিকল্প।
প্র.৪ — signed tag (-s) কেন annotated tag-এর ওপর ভিত্তি করে তৈরি, lightweight-এর ওপর না?
উত্তর: signing-এর জন্য একটা object দরকার যাতে cryptographic signature যোগ করা যায় (tagger info, date, message-সহ)। lightweight tag-এর নিজস্ব কোনো object নেই — শুধু একটা pointer, তাই তাতে signature যোগ করার কোনো জায়গা নেই।
প্র.৫ — SemVer-এ v2.4.0 থেকে v3.0.0-এ আপগ্রেড করার আগে একজন ডেভেলপারের কী বিশেষভাবে করা উচিত?
উত্তর: changelog/release notes পড়া, কারণ MAJOR ভার্সন বৃদ্ধি মানে breaking change — পুরনো API বা behavior-এর সাথে backward compatibility নেই। MINOR (v2.4→v2.5) বা PATCH (v2.4.0→v2.4.1) upgrade সাধারণত নিরাপদ, কিন্তু MAJOR না।
প্র.৬ — একটা ভুল commit-এ ট্যাগ দিয়ে ফেলেছেন এবং সেটা push-ও করে ফেলেছেন — সংশোধনের সঠিক ধাপ কী?
উত্তর: প্রথমে local ট্যাগ মুছুন (git tag -d v1.0.0), তারপর remote থেকে মুছুন (git push origin --delete v1.0.0), তারপর সঠিক commit hash-এ নতুন করে ট্যাগ তৈরি করে push করুন। মনে রাখতে হবে যারা আগেই fetch করেছে তাদের local কপি এখনো পুরনো থেকে যাবে যতক্ষণ না তারাও নিজে থেকে আপডেট করে।
কর্নার কেস — Module 23
git tag <name>(কোনো-a/-mছাড়া) lightweight tag তৈরি করে, কিন্তুgit tag <name> -m "..."আসলে annotated tag তৈরি করে — শুধু-mউপস্থিতিই যথেষ্ট,-aছাড়াই। এই সূক্ষ্ম নিয়ম প্রায়ই নতুনদের বিভ্রান্ত করে যারা মনে করেন-aছাড়া কখনো annotated হবে না।git describeছাড়া কোনো tag ছাড়া history-তে fatal error দেয়, কিন্তু annotated এবং lightweight উভয় ধরনের tag থাকলেgit describeডিফল্টে শুধু annotated tag খুঁজে বের করে — lightweight tag থাকলেও উপেক্ষা করে, যদি না--tagsফ্ল্যাগ যোগ করা হয় (git describe --tags)।- দুইটা ভিন্ন commit-এ একই নামে ট্যাগ থাকা যায় না, কিন্তু একই commit-এ একাধিক ট্যাগ থাকতে পারে (যেমন
v2.0.0আরstableদুটোই একই commit পয়েন্ট করতে পারে) — এটা confusion তৈরি করতে পারে যদি টিম না জানে কোন ট্যাগ "primary"। git tag -fদিয়ে বিদ্যমান ট্যাগ force-overwrite করলে remote-এ push না করা পর্যন্ত অন্য কেউ জানবে না — কিন্তু push করার সময়git push origin <tag>সাধারণত reject হবে (ট্যাগ ইতিমধ্যে remote-এ আছে) যদি না--forceযোগ করা হয় push কমান্ডেও, যেটা অত্যন্ত সতর্কতার সাথে করা উচিত কারণ যারা আগে fetch করেছে তাদের সাথে ইতিহাস বিচ্যুত হয়ে যাবে।
MODULE 24: Stash
কাজের মাঝপথে হঠাৎ context বদলাতে হয় প্রায় সবার — একটা জরুরি hotfix, একটা মিটিং-এ আগে দেখাতে হবে এমন কিছু, বা শুধু branch বদলে অন্য কিছু চেক করতে হবে — কিন্তু working directory তখন অগোছালো, commit করার মতো প্রস্তুত না। git stash এই সমস্যার সমাধান — commit না করেই কাজ সাময়িকভাবে "সরিয়ে রাখা"। এই মডিউলে stash-এর পুরো কমান্ড ফ্যামিলি, internal মেকানিজম (এটা আসলে একটা special commit stack), আর partial stash-এর সূক্ষ্মতা কভার হবে।
Stash কী সমস্যা সমাধান করে — Mid-Feature Hotfix সিনারিও
সমস্যাটা কী
আপনি একটা বড় ফিচার নিয়ে কাজ করছেন — feature/notification-batching branch-এ ৫টা ফাইল edit করা, কিন্তু কোনোটাই commit করার মতো সম্পূর্ণ না (মাঝপথে, কম্পাইলও হয়তো হবে না)। ঠিক তখনই একটা জরুরি production bug রিপোর্ট এলো, যেটা এখনই main-এ গিয়ে fix করে deploy করতে হবে।
সরাসরি branch বদলানোর সমস্যা
git checkout main
# error: Your local changes to the following files would be overwritten by checkout
Git branch বদলাতে দেবে না যদি uncommitted পরিবর্তন conflict করে বর্তমান আর target branch-এর মধ্যে। commit করাও সমাধান না — অসম্পূর্ণ, ভাঙা কোড history-তে যোগ করা ভালো প্র্যাকটিস না।
git stash-এর সমাধান
git stash push -m "wip: notification batching, mid-refactor"
git checkout main
git checkout -b hotfix/null-check
# ... hotfix করে merge/push ...
git checkout feature/notification-batching
git stash pop
stash uncommitted পরিবর্তন (staged + unstaged) সাময়িকভাবে একটা আলাদা জায়গায় সরিয়ে working directory-কে HEAD-এর মতো পরিষ্কার করে দেয় — এখন নির্দ্বিধায় branch বদলানো যায়। কাজ শেষে stash pop দিয়ে ঠিক সেই অবস্থায় ফিরে আসা যায়।
কেন এটা কমিটের চেয়ে ভালো এখানে
stash কোনো permanent history তৈরি করে না (commit history-তে দেখা যায় না) — এটা একটা সাময়িক, লোকাল-only "shelf"। hotfix শেষে ফিরে এসে আবার ঠিক যেখানে ছিলেন সেখান থেকে কাজ চালিয়ে যাওয়া যায়, কোনো অসম্পূর্ণ/ভাঙা commit history-তে না রেখেই।
stash push/pop/apply/list/show/drop/clear কমান্ড ফ্যামিলি
মূল কমান্ড রেফারেন্স
| কমান্ড | কাজ |
|---|---|
git stash / git stash push |
বর্তমান পরিবর্তন stash-এ রাখা |
git stash push -m "<message>" |
বর্ণনা-সহ stash করা |
git stash list |
সব stash-এর তালিকা দেখা |
git stash show [stash@{n}] |
একটা stash-এর সংক্ষিপ্ত diff দেখা (--stat মোডে ডিফল্ট) |
git stash show -p [stash@{n}] |
পূর্ণ diff দেখা |
git stash apply [stash@{n}] |
stash প্রয়োগ করা, কিন্তু stash list থেকে না মুছে |
git stash pop [stash@{n}] |
stash প্রয়োগ করা এবং stash list থেকে মুছে ফেলা |
git stash drop [stash@{n}] |
প্রয়োগ না করেই একটা stash মুছে ফেলা |
git stash clear |
সব stash একসাথে মুছে ফেলা (সতর্কতার সাথে!) |
apply বনাম pop
git stash apply stash@{0} # প্রয়োগ হবে, কিন্তু stash@{0} তালিকায় থেকে যাবে
git stash pop stash@{0} # প্রয়োগ হবে এবং stash@{0} মুছে যাবে
apply উপকারী যখন একই stash একাধিক branch-এ প্রয়োগ করতে চান (যেমন দুইটা ভিন্ন branch-এ একই সাময়িক পরিবর্তন টেস্ট করা) — pop করলে প্রথমবার প্রয়োগের পরই stash মুছে যাবে, দ্বিতীয়বার আর সম্ভব না।
উদাহরণ ফ্লো
git stash push -m "experiment: caching layer"
git stash list
# stash@{0}: On feature/cache: experiment: caching layer
git stash show -p stash@{0}
# পূর্ণ diff দেখাবে
git stash pop
# working directory-তে প্রয়োগ হলো, stash list থেকে মুছে গেলো
clear নিয়ে সতর্কতা
git stash clear সব stash অপ্রত্যাবর্তনযোগ্যভাবে মুছে দেয় — কোনো নিশ্চিতকরণ প্রম্পট ছাড়াই। ভুলে চালালে সব সাময়িক সংরক্ষিত কাজ হারিয়ে যেতে পারে (যদিও git fsck --unreachable দিয়ে কিছু ক্ষেত্রে reflog/dangling commit থেকে উদ্ধারের চেষ্টা করা যায় — নিশ্চয়তা নেই)।
ইন্টারনাল মেকানিজম: refs/stash + reflog-এর মতো স্ট্যাক, stash@{n} ইনডেক্সিং
Stash আসলে কী — একটা বিশেষ commit-এর স্ট্যাক
git stash internally uncommitted পরিবর্তনগুলো দিয়ে (আসলে দুই বা তিনটা) বিশেষ commit object তৈরি করে — একটা working-directory-state commit, আরেকটা index (staged) state commit, এবং সেগুলোর একটা "stash commit" যার parent হলো তৈরি হওয়ার সময়কার HEAD। এই stash commit-টা refs/stash-এ পয়েন্ট করা থাকে।
git cat-file -p refs/stash
tree ...
parent a1b2c3d4... <- HEAD যখন stash করা হয়েছিল
parent e5f6g7h8... <- index (staged) state
author Rakib Hasan ...
On feature/cache: experiment: caching layer
reflog-এর মতো একটা স্ট্যাক
একাধিকবার git stash push চালালে প্রতিবার নতুন stash commit তৈরি হয়, আর পুরনো refs/stash-এর মান refs/stash-এর reflog-এ (.git/logs/refs/stash) সরে যায় — ঠিক যেমন সাধারণ branch-এর reflog পুরনো HEAD position ট্র্যাক করে (Phase 3-এ দেখা reflog মেকানিজমের সরাসরি পুনঃব্যবহার)। stash@{0} মানে "সবচেয়ে সাম্প্রতিক stash", stash@{1} তার আগেরটা, ইত্যাদি — এই ইনডেক্সিং সরাসরি reflog entry নম্বর থেকে আসে।
git stash list
# stash@{0}: On feature/cache: latest stash
# stash@{1}: On main: older stash
# stash@{2}: On feature/x: even older stash
কেন এই মডেল গুরুত্বপূর্ণ বোঝা
যেহেতু stash আসলে commit object-ই (শুধু কোনো branch তাদের সরাসরি reference করে না), git show stash@{0} বা git log -p stash@{0} দিয়ে সরাসরি ইন্সপেক্ট করা যায় — এটা কোনো ম্যাজিক আলাদা storage না, Git-এর একই object model-এর একটা প্রয়োগ।
practical ইমপ্লিকেশন
git gc (garbage collection) সাধারণত dangling/unreachable object মুছে ফেলে, কিন্তু refs/stash আর তার reflog যতক্ষণ বিদ্যমান, ততক্ষণ stash commit-গুলো reachable থাকে, তাই সাধারণ gc-তে হারায় না — কিন্তু git stash clear চালালে সেই reference-ই মুছে যায়, তখন commit-গুলো dangling হয়ে যায় এবং পরবর্তী gc-তে মুছে যাওয়ার ঝুঁকিতে পড়ে।
Partial Stash: -p/--patch এবং push -- <path>
সমস্যাটা কী
মাঝে মাঝে working directory-র সব পরিবর্তন stash করতে চান না — শুধু নির্দিষ্ট কিছু ফাইল বা এমনকি একটা ফাইলের নির্দিষ্ট অংশ। ডিফল্ট git stash push সব uncommitted পরিবর্তন একসাথে সরিয়ে নেয়, যেটা সবসময় কাম্য না।
নির্দিষ্ট path stash করা
git stash push -- src/notifications/retry.py
এটা শুধু retry.py-এর পরিবর্তন stash করবে, বাকি সব ফাইলের পরিবর্তন working directory-তে অক্ষত থেকে যাবে। একাধিক path দেওয়া যায়:
git stash push -- src/notifications/retry.py src/notifications/config.py
-p/--patch দিয়ে হাংক-লেভেল নির্বাচন
git stash push -p
এটা git add -p-এর মতোই ইন্টারেক্টিভ মোডে প্রতিটা diff hunk দেখিয়ে জিজ্ঞেস করে stash করবেন কিনা (y/n/s split ইত্যাদি) — একটা ফাইলের ভেতরেও শুধু নির্দিষ্ট অংশ stash করা যায়, বাকি অংশ working directory-তে থেকে যায়।
রিয়েল-লাইফ উদাহরণ
আপনি একটা ফাইলে দুইটা ভিন্ন, অসম্পর্কিত পরিবর্তন করেছেন (একটা debug logging যোগ, আরেকটা আসল ফিচার লজিক) — debug logging অংশটুকু সাময়িকভাবে সরিয়ে রেখে বাকি অংশ কমিট করতে চান:
git stash push -p -m "temp: remove debug logging before commit"
# শুধু debug logging-সংক্রান্ত hunk-এ 'y' দিন, বাকিতে 'n'
git commit -am "add retry backoff logic"
git stash pop # debug logging আবার ফিরিয়ে আনুন পরে দরকার হলে
সতর্কতা
-u/--include-untracked আর -a/--all (Module-এর overview-তে উল্লেখিত) partial path selection-এর সাথে কম্বাইন করা যায়, কিন্তু জটিল হয়ে উঠতে পারে — untracked নতুন ফাইল partial stash-এ অন্তর্ভুক্ত করতে চাইলে path স্পষ্টভাবে উল্লেখ করাই নিরাপদ।
git stash branch <name> — Conflict এড়িয়ে সরাসরি নতুন Branch
সমস্যাটা কখন হয়
git stash pop মাঝে মাঝে conflict দেয় — যদি stash করার পর থেকে বর্তমান branch-এ এমন পরিবর্তন এসেছে যা stash-এর পরিবর্তনের সাথে সাংঘর্ষিক। এই অবস্থায় pop merge conflict marker রেখে যায়, যেটা resolve করতে হয় ঠিক normal merge conflict-এর মতোই — মাঝে মাঝে বিরক্তিকর, বিশেষ করে যদি stash পুরনো হয়ে গিয়ে থাকে branch অনেকদূর এগিয়ে যাওয়ায়।
git stash branch সমাধান
git stash branch new-feature-from-stash
এই একটা কমান্ড তিনটা কাজ করে:
- stash যে commit-এ তৈরি হয়েছিল, সেই commit থেকে একটা নতুন branch তৈরি করে ও checkout করে।
- সেই নতুন branch-এ stash apply করে।
- সফল হলে stash drop করে দেয়।
কেন এটা conflict এড়ায়
যেহেতু নতুন branch তৈরি হচ্ছে ঠিক সেই commit থেকে যেখানে stash তৈরি হয়েছিল (বর্তমান branch-এর এগিয়ে যাওয়া history থেকে না), stash-এর পরিবর্তন প্রয়োগ করার সময় বর্তমান branch-এর পরবর্তী কোনো commit-এর সাথে সাংঘর্ষিক হওয়ার সুযোগই নেই — একদম "ক্লিন স্লেট"-এ প্রয়োগ হচ্ছে।
রিয়েল-লাইফ উদাহরণ
git stash list
# stash@{0}: On main: wip experimental caching (২ সপ্তাহ পুরনো!)
git stash branch experiment/caching stash@{0}
# main branch এখন অনেক এগিয়ে গেছে, সরাসরি pop করলে অনেক conflict হতো
# কিন্তু এই কমান্ড দিয়ে একটা পরিষ্কার নতুন branch-এ (পুরনো commit থেকে) stash প্রয়োগ হলো
# এখন প্রয়োজনে main-এর সাথে rebase/merge করে conflict resolve করা যাবে normal workflow-এ
কখন ব্যবহার করবেন
যখন একটা পুরনো stash pop করতে গিয়ে জটিল conflict আসছে, বা stash-এর কাজটা আসলে একটা পূর্ণাঙ্গ feature হিসেবে গড়ে তোলার যোগ্য মনে হচ্ছে — সরাসরি একটা branch-এ রূপান্তর করে সেখান থেকে normal development চালিয়ে যাওয়া অনেক পরিষ্কার সমাধান।
ল্যাব: Mid-Feature Hotfix সিমুলেশন
লক্ষ্য
একটা বাস্তব সিনারিও সিমুলেট করা — ফিচার কাজের মাঝপথে জরুরি hotfix, stash দিয়ে কাজ বাঁচিয়ে branch বদলানো, hotfix করে ফিরে এসে কাজ চালিয়ে যাওয়া।
সেটআপ
mkdir -p ~/git-lab8 && cd ~/git-lab8 && git init --initial-branch=main
echo "def send_notification(): pass" > notifier.py
git add notifier.py && git commit -m "initial notifier"
git checkout -b feature/batching
ধাপ ১ — অসম্পূর্ণ ফিচার কাজ (কমিট করার মতো প্রস্তুত না)
cat >> notifier.py << 'EOF'
def batch_notifications(items):
# TODO: এখনো অসম্পূর্ণ, ভাঙা কোড
for item in items
pass
EOF
ধাপ ২ — জরুরি bug রিপোর্ট আসলো, main-এ যেতে হবে
git checkout main
# error: Your local changes to the following files would be overwritten by checkout
ধাপ ৩ — stash দিয়ে কাজ বাঁচানো
git stash push -m "wip: batching feature, incomplete for loop"
git checkout main
ধাপ ৪ — hotfix করা ও merge
git checkout -b hotfix/notifier-crash
sed -i 's/def send_notification(): pass/def send_notification():\n print("sending")/' notifier.py
git add notifier.py
git commit -m "hotfix: fix notifier crash"
git checkout main
git merge --no-ff hotfix/notifier-crash -m "Merge hotfix/notifier-crash"
git branch -d hotfix/notifier-crash
ধাপ ৫ — ফিচার branch-এ ফিরে stash পুনরুদ্ধার
git checkout feature/batching
git stash list
git stash pop
cat notifier.py # আগের অসম্পূর্ণ কাজ ফিরে এসেছে, সাথে hotfix-ও (rebase করলে) merge হতে পারে
চ্যালেঞ্জ
ধাপ ৩-এর আগে git stash push -p ব্যবহার করে দেখুন শুধু batch_notifications ফাংশনের অংশটুকু stash করা যায় কিনা, বাকি ফাইল অক্ষত রেখে।
ইন্টারভিউ প্রশ্ন — Module 24
প্র.১ — git stash internally ঠিক কী করে — কোথায় ডেটা সংরক্ষিত হয়?
উত্তর: uncommitted পরিবর্তন (staged ও unstaged) দিয়ে বিশেষ commit object তৈরি হয় (working-tree state ও index state আলাদাভাবে), এবং একটা "stash commit" refs/stash-এ পয়েন্ট করে রাখা হয়। একাধিক stash refs/stash-এর reflog-এ স্ট্যাকের মতো সংরক্ষিত থাকে — stash@{0}, stash@{1} ইত্যাদি ইনডেক্স সেই reflog entry থেকেই আসে।
প্র.২ — git stash apply আর git stash pop-এর পার্থক্য কী, কখন apply prefer করবেন?
উত্তর: pop প্রয়োগ করে stash list থেকে মুছে দেয়, apply প্রয়োগ করে কিন্তু stash list-এ রেখে দেয়। একই stash একাধিক branch-এ প্রয়োগ করতে চাইলে apply ব্যবহার করা উচিত, কারণ pop প্রথমবারই stash মুছে ফেলবে।
প্র.৩ — git stash push -- <path> আর git stash push -p-এর মধ্যে পার্থক্য কী?
উত্তর: -- <path> নির্দিষ্ট ফাইল/ফাইলসমূহ পুরোপুরি stash করে, বাকি ফাইল অক্ষত থাকে। -p/--patch আরও সূক্ষ্ম — একটা ফাইলের ভেতরেও নির্দিষ্ট hunk/অংশ বেছে stash করতে দেয়, ইন্টারেক্টিভভাবে।
প্র.৪ — git stash branch <name> কেন সরাসরি git stash pop-এর চেয়ে নিরাপদ যখন stash অনেক পুরনো?
উত্তর: git stash branch stash তৈরি হওয়ার সময়কার commit থেকে একটা নতুন branch তৈরি করে সেখানে stash প্রয়োগ করে — বর্তমান (এগিয়ে যাওয়া) branch-এর সাথে সরাসরি conflict হওয়ার সুযোগ নেই, কারণ প্রয়োগ হচ্ছে ঠিক সেই পুরনো commit-এর ওপর যেখান থেকে stash এসেছিল।
প্র.৫ — git stash clear চালানোর পর কি stash করা কাজ পুনরুদ্ধার করা সম্ভব?
উত্তর: গ্যারান্টিসহ না — refs/stash মুছে যাওয়ার পর stash commit-গুলো dangling হয়ে যায়। git gc চালানোর আগে git fsck --unreachable/--dangling দিয়ে কিছু ক্ষেত্রে খুঁজে পাওয়া সম্ভব হতে পারে, কিন্তু নির্ভরযোগ্য উপায় না — তাই clear চালানোর আগে সতর্ক থাকা জরুরি।
প্র.৬ — stash কি টিমমেটদের সাথে শেয়ার করা যায় (push করা যায় remote-এ)?
উত্তর: না, সরাসরি না। stash কোনো branch না, এটা শুধু local refs/stash-এ থাকে — এটা push করার কোনো native কমান্ড নেই। শেয়ার করতে চাইলে git stash show -p > patch.diff দিয়ে একটা patch ফাইল বানিয়ে পাঠানো যায়, বা git stash branch দিয়ে একটা আসল branch-এ রূপান্তর করে push করা যায়।
কর্নার কেস — Module 24
git stashডিফল্টে untracked (নতুন, কখনো add করা হয়নি) ফাইল stash করে না — শুধু tracked ফাইলের পরিবর্তন। নতুন ফাইল-সহ stash করতে-u/--include-untrackedলাগবে, আর.gitignore-এ থাকা ফাইলও stash করতে-a/--allলাগবে (আরও agresive, সাধারণত-uযথেষ্ট)।git stash popconflict দিলে এবং resolve করার পরও stash list থেকে entry মুছে যায় না স্বয়ংক্রিয়ভাবে — conflict হলেpopমাঝপথে থেমে যায়, resolve করার পর ম্যানুয়ালিgit stash dropচালাতে হয় (Git নিশ্চিত করতে চায় না যে resolve সঠিকভাবে হয়েছে, তাই automatically drop করে না)।- stash@{n} ইনডেক্স স্থির না — প্রতিটা নতুন
stash push/pop/drop-এর পর সব ইনডেক্স শিফট হয় —stash@{2}আজ যা বোঝাচ্ছে, একটা নতুন stash push করার পর তাstash@{3}হয়ে যাবে। স্ক্রিপ্টে hardcodedstash@{n}রেফারেন্স রাখা বিপজ্জনক। - একই ফাইলে stash করার সময় এবং pop করার সময়ের মাঝে যদি bare
git stash(branch-independent) ভিন্ন branch-এ pop করা হয়, ফাইল স্ট্রাকচার সম্পূর্ণ ভিন্ন থাকলে conflict বা এমনকি ভুল জায়গায় ফাইল তৈরি হতে পারে — stash কোনো branch-aware সেফটি চেক করে না, ব্যবহারকারীর দায়িত্ব বুঝে প্রয়োগ করা কোন branch-এ করছেন।
PHASE 8: Advanced Workspace Tools
এতদিন আমরা একটা সময়ে একটামাত্র working directory নিয়ে কাজ করেছি, আর একটামাত্র repository নিয়ে। বাস্তবের বড় প্রজেক্টে এই দুইটা ধারণাই ভেঙে যায়: একই সময়ে দুইটা branch-এ কাজ করা লাগে (একটায় feature, আরেকটায় জরুরি hotfix), একটা repository আরেকটা repository-কে dependency হিসেবে টেনে আনে (submodule/subtree), আর repository নিজেই এত বড় হয়ে যায় (মনোরেপো, গিগাবাইট গিগাবাইট asset) যে পুরোটা ক্লোন করাই অবাস্তব হয়ে দাঁড়ায়।
এই Phase-এ তিনটা মডিউল — Worktree (একই repo-র একাধিক working copy), Submodules/Subtrees (repo-র ভেতরে repo), আর বড় Repo/Monorepo টুলিং (shallow/partial clone, sparse-checkout, maintenance, Git LFS)। এগুলো হলো সেই টুলসেট যেটা একজন soloist ডেভেলপারকে একটা large-team, large-scale ইঞ্জিনিয়ারিং সংস্কৃতির জন্য প্রস্তুত করে।
MODULE 25: Worktree
একটা .git directory, কিন্তু একাধিক working directory — এটাই git worktree-র মূল প্রতিশ্রুতি। কভার হবে: worktree কীভাবে object database শেয়ার করে, add/list/remove/lock/unlock/prune কমান্ডগুলো, আর branch switch না করেই সমান্তরালভাবে একাধিক branch-এ কাজ করার বাস্তব সমস্যা সমাধান।
git worktree — একই সময়ে একাধিক Working Directory
সমস্যাটা কী
স্বাভাবিক Git workflow-এ একটা repository ক্লোন করলে একটামাত্র working directory পাওয়া যায় — যে কোনো মুহূর্তে সেখানে একটামাত্র branch checked out থাকতে পারে। feature/payment branch-এ কাজ করার সময় হঠাৎ একটা production hotfix লিখতে হলে সাধারণত git stash করে main-এ checkout করতে হয় — কাজের মাঝখানে বাধা, আর ভুলে অসম্পূর্ণ stash রয়ে যাওয়ার ঝুঁকি।
git worktree এই সমস্যা সমাধান করে: একই .git object database শেয়ার করে একাধিক working directory তৈরি করা যায়, প্রতিটাতে আলাদা branch checked out থাকতে পারে — সম্পূর্ণ সমান্তরালে, কোনো stash/switch ছাড়াই।
মূল কমান্ড
git worktree add ../myrepo-hotfix hotfix/urgent-bug
git worktree list
git worktree remove ../myrepo-hotfix
রিয়েল-লাইফ উদাহরণ
আপনি feature/payment-gateway branch-এ অর্ধেক কাজ করা অবস্থায় Slack-এ জানানো হলো production-এ একটা critical bug আছে। Stash না করে:
git worktree add ../myrepo-hotfix main
cd ../myrepo-hotfix
git checkout -b hotfix/null-pointer
# ফিক্স করুন, কমিট করুন, push করুন
cd ../myrepo # আগের feature কাজ ঠিক যেভাবে ছিল সেভাবেই আছে
দুইটা টার্মিনাল ট্যাব, দুইটা ফোল্ডার, একই repository — কোনো context-switch penalty নেই।
এক লাইনে
Worktree = একটা repo, একাধিক "physical" working copy, শেয়ার্ড history।
ইন্টারনাল মেকানিজম: .git/worktrees ও শেয়ার্ড Object Database
ভেতরে কী ঘটে
মূল worktree ক্লোন করার সময় .git একটা সাধারণ ডিরেক্টরি থাকে যেখানে objects, refs, HEAD, index — সব একসাথে থাকে। git worktree add চালালে Git একটা নতুন linked worktree তৈরি করে — নতুন ফোল্ডারে একটা .git ফাইল (ডিরেক্টরি না) থাকে যেটা মূল .git/worktrees/<name>/ ডিরেক্টরির দিকে পয়েন্ট করে:
$ cat ../myrepo-hotfix/.git
gitdir: /path/to/myrepo/.git/worktrees/myrepo-hotfix
কী শেয়ার্ড, কী আলাদা
| শেয়ার্ড (সব worktree জুড়ে একটাই) | প্রতিটা worktree-র নিজস্ব |
|---|---|
objects/ (commits, trees, blobs) |
HEAD |
refs/ (branches, tags) |
index (staging area) |
config |
working directory-র ফাইলগুলো |
| reflog (মূলত শেয়ার্ড) | per-worktree config (worktree-scope, Module 28) |
যেহেতু objects শেয়ার্ড, একটা worktree-তে কমিট করা object সাথে সাথে অন্য worktree থেকেও দেখা যায় (git log)। কিন্তু একই branch দুইটা worktree-তে একসাথে checkout করা যায় না — Git এটা ব্লক করে দেয় ("already checked out" error), কারণ দুইটা আলাদা index থাকলেও branch pointer একটাই।
prune, lock, unlock
git worktree lock ../myrepo-hotfix --reason "external drive, may be offline"
git worktree unlock ../myrepo-hotfix
git worktree prune # ম্যানুয়ালি ডিলিট হওয়া worktree ফোল্ডারের রেফারেন্স পরিষ্কার করে
prune দরকার হয় যখন worktree ফোল্ডার সরাসরি rm -rf দিয়ে মুছে ফেলা হয়েছে (সঠিক উপায় git worktree remove), কারণ Git-এর মেটাডেটায় তখনও সেটার রেকর্ড থেকে যায়।
কেন এই মডেল — Branch Switch-এর ব্যয় এড়ানো
git checkout/switch বনাম Worktree
Branch পাল্টানোর সাধারণ পদ্ধতি (git switch other-branch) সস্তা মনে হলেও বাস্তবে কয়েকটা ব্যয় আছে:
- Build/dependency cache invalidation —
node_modules,.venv, compiled artifact আলাদা branch-এ ভিন্ন হতে পারে; switch করলে আবার rebuild/reinstall লাগতে পারে। - অসম্পূর্ণ কাজ হারানোর ঝুঁকি — stash করা জিনিস ভুলে যাওয়া একটা সাধারণ ভুল।
- IDE state হারানো — খোলা ফাইল, breakpoint, running dev server — সব রিসেট হয়ে যায়।
Worktree দিয়ে প্রতিটা branch-এর একটা আলাদা ফিজিক্যাল ফোল্ডার থাকে বলে এই সব state স্বাধীনভাবে টিকে থাকে।
বাস্তব ব্যবহারের দৃশ্য
- Hotfix বনাম feature — production bug ফিক্স করতে করতে feature branch untouched থাকে।
- Code review — একজন সহকর্মীর PR local-এ চালিয়ে দেখতে চান, কিন্তু নিজের running dev server বন্ধ করতে চান না:
git worktree add ../review-pr-42 origin/feature-x। - দীর্ঘ CI/build সময়ের বিরুদ্ধে সমান্তরাল কাজ — একটা worktree-তে ভারী build চলছে, আরেকটাতে normal কাজ চলছে।
- Bisect করার সময় —
git bisectচালানো worktree-তে ইতিহাস জুড়ে checkout হতে থাকে; আলাদা worktree-তে bisect চালালে মূল working directory অক্ষত থাকে।
ট্রেড-অফ
- ডিস্কে extra working-tree ফাইল থাকে (objects শেয়ার্ড বলে বড় সমস্যা না, কিন্তু build artifact প্রতিটাতে আলাদা)।
- কিছু টুল (পুরনো IDE, কিছু git GUI) worktree সম্পূর্ণভাবে বুঝে না — সবসময় প্রথম-শ্রেণীর সাপোর্ট থাকে না।
কমান্ড রেফারেন্স: add, list, remove, lock, unlock, prune
সম্পূর্ণ কমান্ড সেট
# নতুন worktree, নতুন branch সহ
git worktree add -b feature/new-ui ../myrepo-ui origin/main
# বিদ্যমান branch দিয়ে
git worktree add ../myrepo-hotfix hotfix/urgent
# detached HEAD-এ (কোনো branch বাইন্ড না করে, শুধু দেখার জন্য)
git worktree add --detach ../myrepo-inspect v2.3.0
# তালিকা দেখা
git worktree list
# /path/to/myrepo abcd123 [main]
# /path/to/myrepo-hotfix ef01234 [hotfix/urgent]
# মুছে ফেলা (working directory ও worktree metadata দুটোই সরায়)
git worktree remove ../myrepo-hotfix
# অসম্পূর্ণ পরিবর্তন থাকলেও জোর করে সরানো
git worktree remove --force ../myrepo-hotfix
# lock/unlock — external/removable media-তে worktree থাকলে prune থেকে সুরক্ষা
git worktree lock ../myrepo-usb --reason "USB drive"
git worktree unlock ../myrepo-usb
# ম্যানুয়ালি মুছে ফেলা ফোল্ডারের স্টেল metadata পরিষ্কার
git worktree prune -v
গুরুত্বপূর্ণ ফ্ল্যাগ
| ফ্ল্যাগ | কাজ |
|---|---|
-b <name> |
নতুন branch তৈরি করে সেই branch দিয়ে worktree শুরু করা |
--detach |
কোনো branch-এ bind না করে নির্দিষ্ট কমিট/ট্যাগে worktree খোলা |
--force |
একই branch দুইবার checkout করার ব্লক ওভাররাইড (সতর্কতার সাথে) |
-v (prune) |
কোন worktree entry মোছা হলো তা verbose আউটপুটে দেখানো |
Bare repository-র সাথে worktree
কখনো কখনো মূল রিপোতে কোনো checkout না রেখে (git clone --bare) সব কাজ worktree দিয়ে করা হয় — এটা এমন টিমে জনপ্রিয় যেখানে "প্রতিটা branch একটা আলাদা ফোল্ডার" নীতি মানা হয়, root ফোল্ডারে কোনো active branch থাকে না, শুধু worktree-গুলোই আসল কাজের জায়গা।
ল্যাব: সমান্তরাল Feature ও Hotfix Worktree
লক্ষ্য
একটা repository-তে দুইটা worktree তৈরি করে দেখানো যে objects শেয়ার্ড কিন্তু working state স্বাধীন।
ধাপ
- একটা টেস্ট repo বানান এবং কিছু কমিট দিন:
mkdir /tmp/wt-lab && cd /tmp/wt-lab && git init
echo "v1" > app.txt && git add . && git commit -m "init"
- একটা feature branch তৈরি করুন এবং কিছু অসম্পূর্ণ (uncommitted) পরিবর্তন রেখে দিন:
git checkout -b feature/payment
echo "half-written payment code" >> app.txt
(এখানে কমিট করবেন না — এটাই আসল সমস্যা যা worktree সমাধান করে)
- নতুন টার্মিনালে হটফিক্সের জন্য একটা worktree তৈরি করুন
mainথেকে:
git worktree add ../wt-lab-hotfix main
cd ../wt-lab-hotfix
cat app.txt # শুধু "v1" দেখাবে — feature-এর অসম্পূর্ণ কাজ এখানে নেই
- হটফিক্স করে কমিট করুন:
git checkout -b hotfix/typo
echo "v1-fixed" > app.txt
git commit -am "fix typo"
- মূল worktree-তে ফিরে যাচাই করুন feature-এর অসম্পূর্ণ কাজ এখনো অক্ষত:
cd /tmp/wt-lab
git status # app.txt মডিফায়েড দেখাবে, feature-এর কাজ অক্ষত
git log --all --oneline --graph # hotfix কমিট দেখা যাচ্ছে, একই object DB শেয়ার্ড
- পরিষ্কার করুন:
git worktree list
git worktree remove ../wt-lab-hotfix
যা লক্ষ্য করবেন
git log --all মূল worktree থেকে চালালেও hotfix-এর কমিট দেখা যায় — কারণ objects/refs শেয়ার্ড। কিন্তু app.txt-এর uncommitted content দুই worktree-তে সম্পূর্ণ আলাদা।
ইন্টারভিউ প্রশ্ন — Module 25
প্র.১ — git worktree কী সমস্যা সমাধান করে যেটা git stash + git checkout দিয়ে করা যেত না?
উত্তর: Stash/checkout পদ্ধতিতে একটামাত্র working directory-তে কাজ করতে হয় বলে build artifact, IDE state, running dev server — সব বাধাগ্রস্ত হয়। Worktree প্রতিটা branch-এর জন্য আলাদা ফিজিক্যাল ফোল্ডার দেয়, তাই সমান্তরালভাবে কাজ চালানো যায় কোনো state হারানো ছাড়াই।
প্র.২ — দুইটা worktree কি একটা object কে দুইবার ডিস্কে সংরক্ষণ করে?
উত্তর: না। সব worktree একই .git/objects শেয়ার করে — শুধু HEAD, index, আর working directory-র ফাইলগুলো প্রতিটা worktree-র নিজস্ব।
প্র.৩ — একই branch কি দুইটা worktree-তে একসাথে checkout করা যায়? উত্তর: না, Git এটা ব্লক করে ("already checked out")। কারণ branch pointer একটাই, দুই জায়গায় একসাথে থাকলে কোন worktree-র commit "সত্যি" তা নিয়ে দ্বন্দ্ব হতো।
প্র.৪ — git worktree remove আর ম্যানুয়ালি rm -rf দিয়ে worktree ফোল্ডার মোছার মধ্যে পার্থক্য কী?
উত্তর: git worktree remove worktree ফোল্ডার এবং .git/worktrees/<name> metadata দুটোই পরিষ্কার করে। rm -rf দিয়ে সরাসরি ফোল্ডার মুছলে metadata স্টেল থেকে যায় — git worktree prune চালিয়ে সেটা পরিষ্কার করতে হয়।
প্র.৫ — git worktree lock কেন দরকার হতে পারে?
উত্তর: যদি worktree কোনো removable/external ড্রাইভে থাকে যেটা মাঝে মাঝে unmount থাকতে পারে, lock করলে git worktree prune ভুল করে সেটাকে "স্টেল" ভেবে মুছে ফেলে না।
প্র.৬ — Worktree ব্যবহারের একটা ট্রেড-অফ বলুন। উত্তর: প্রতিটা worktree-র নিজস্ব build artifact/dependency (node_modules, venv) থাকতে পারে, ফলে ডিস্ক ব্যবহার বাড়ে; কিছু পুরনো IDE/GUI টুল worktree পুরোপুরি সাপোর্ট করে না।
কর্নার কেস — Module 25
git worktree addকরার পর একই branch অন্য জায়গায় checkout করার চেষ্টা করলে এরর আসবে — সমাধান: হয়--detachব্যবহার করুন, নয়তো নতুন branch তৈরি করুন।Worktree ফোল্ডার সরাসরি সরিয়ে ফেললে (
mv) Git বিভ্রান্ত হয় —.gitফাইলের ভেতরের পাথ পুরনো লোকেশনে পয়েন্ট করে থাকে। সমাধান:git worktree move <worktree> <new-path>ব্যবহার করুন, ম্যানুয়ালিmvনয়।মূল repository (
.gitডিরেক্টরিসহ) মুছে ফেললে সব linked worktree ভেঙে পড়ে — যেহেতু objects/refs মূল.git-এই থাকে, linked worktree কোনোভাবেই স্বাধীন নয়।Prune স্বয়ংক্রিয়ভাবে চলে না — বেশ কিছু কমান্ড (
git worktree list) মাঝেমধ্যে স্টেল entry auto-prune করলেও নির্ভরযোগ্যভাবে সবসময় হয় না; CI বা script-এ ম্যানুয়ালিgit worktree pruneচালানো নিরাপদ অভ্যাস।Detached HEAD worktree-তে কমিট করলে সেই কমিট কোনো branch-এ থাকে না — worktree সরিয়ে ফেললে বা
git gcচললে সেই কমিট harvest (গার্বেজ কালেক্ট) হয়ে যেতে পারে যদি কোনো ref (branch/tag) দিয়ে reachable না থাকে।.git/worktrees/-এর ভেতরে জমে থাকা স্টেল entry রিপোজিটরির সাইজ বাgit worktree list-এর আউটপুট নোংরা করে দিতে পারে — নিয়মিতgit worktree prune -vচালানো একটা ভালো hygiene অভ্যাস, বিশেষ করে CI/স্ক্রিপ্টেড workflow-এ যেখানে worktree বারবার তৈরি-মুছা হয়।
MODULE 26: Submodules & Subtrees
একটা repository-র ভেতরে আরেকটা সম্পূর্ণ স্বাধীন repository রেফারেন্স করার দুইটা পদ্ধতি — git submodule (pointer-ভিত্তিক) আর git subtree (merge-ভিত্তিক)। কভার হবে: .gitmodules-এর গঠন, submodule-এর ইন্টারনাল mechanism (gitlink), সাধারণ ব্যথার পয়েন্ট, subtree workflow, আর দুটোর মধ্যে কখন কোনটা বেছে নেবেন।
git submodule — Repository-র ভেতরে Repository
সমস্যাটা কী
ধরুন আপনার মূল প্রজেক্ট একটা শেয়ার্ড internal library (common-utils) ব্যবহার করে, যেটা নিজেই আলাদাভাবে ভার্শন হয়। কোড কপি-পেস্ট করলে আপডেট trace করা কঠিন হয়ে যায়। Submodule সমাধান দেয়: মূল repository-তে পুরো library কোড না রেখে, শুধু একটা নির্দিষ্ট কমিটের রেফারেন্স রাখা হয়।
যোগ করা
git submodule add https://github.com/org/common-utils.git libs/common-utils
git commit -m "Add common-utils submodule"
এটা তৈরি করে একটা .gitmodules ফাইল:
[submodule "libs/common-utils"]
path = libs/common-utils
url = https://github.com/org/common-utils.git
ক্লোন করার সময়
git clone --recurse-submodules https://github.com/org/main-app.git
# অথবা আগে থেকে ক্লোন করা থাকলে:
git submodule update --init --recursive
রিয়েল-লাইফ উদাহরণ
একটা মাইক্রোসার্ভিস আর্কিটেকচারে shared protobuf schema definitions একটা আলাদা repository-তে রাখা হয়, আর প্রতিটা service সেটাকে submodule হিসেবে টেনে আনে নির্দিষ্ট commit-এ পিন করে — যাতে schema-র breaking change হঠাৎ সব service-এ ছড়িয়ে না পড়ে, প্রতিটা service ইচ্ছাকৃতভাবে আপডেট করলেই কেবল নতুন schema পায়।
ইন্টারনাল মেকানিজম: Gitlink ও Detached HEAD
Gitlink (mode 160000)
মূল repository-র tree object-এ submodule একটা বিশেষ entry হিসেবে সংরক্ষিত থাকে — সাধারণ blob (mode 100644) বা tree (mode 040000) না, বরং একটা gitlink (mode 160000), যেটা submodule repository-র একটা নির্দিষ্ট commit SHA পয়েন্ট করে:
$ git ls-tree HEAD libs/
160000 commit e3f1a2b... libs/common-utils
অর্থাৎ মূল repository আসলে submodule-এর কোড রাখে না, শুধু "এই ঠিকানায় এই commit checkout করো" — এই তথ্যটুকু রাখে। এই কারণেই .gitmodules-এ URL, আর tree-তে exact commit SHA — দুটো মিলিয়ে submodule সম্পূর্ণভাবে resolve হয়।
Detached HEAD কেন
git submodule update submodule ফোল্ডারের ভেতরে ঠিক সেই pinned commit-এ checkout করে — কোনো branch attach না করেই, কারণ submodule-এর দৃষ্টিকোণ থেকে এটা একটা নির্দিষ্ট বিন্দু, "branch-এর সর্বশেষ" না। তাই ভেতরে ঢুকলে দেখবেন:
$ cd libs/common-utils && git status
HEAD detached at e3f1a2b
নতুনরা এখানে ভুল করে সরাসরি কমিট করে ফেলেন — সেই কমিট কোনো branch-এ না থাকায় সহজেই হারিয়ে যেতে পারে।
Submodule-এর ভেতরের .git
আধুনিক Git-এ submodule ফোল্ডারের ভেতরে সরাসরি .git ডিরেক্টরি না রেখে, মূল repo-র .git/modules/<name>/-এ প্রকৃত ডেটা রাখা হয় এবং submodule ফোল্ডারে শুধু একটা .git ফাইল (gitdir পয়েন্টার) থাকে — অনেকটা worktree-র মতোই কৌশল।
কমান্ড রেফারেন্স: init, update, status, sync, deinit
সম্পূর্ণ জীবনচক্র
# যোগ করা
git submodule add <url> <path>
# প্রথমবার ক্লোন করার পর init + fetch + checkout
git submodule init
git submodule update
# অথবা একসাথে:
git submodule update --init --recursive
# বর্তমান অবস্থা দেখা (pinned SHA বনাম চেকড-আউট SHA)
git submodule status
# e3f1a2b libs/common-utils (heads/main)
# -e3f1a2b মানে init হয়নি, +e3f1a2b মানে pinned SHA থেকে এগিয়ে গেছে
# submodule-এর ভেতরে গিয়ে সর্বশেষ commit-এ আপডেট করে মূল repo-তে সেই নতুন SHA পিন করা
cd libs/common-utils && git checkout main && git pull
cd ../.. && git add libs/common-utils && git commit -m "Bump common-utils"
# .gitmodules-এ URL পাল্টালে local config-এও সিঙ্ক করা
git submodule sync --recursive
# সম্পূর্ণ সরিয়ে ফেলা
git submodule deinit -f libs/common-utils
git rm libs/common-utils
rm -rf .git/modules/libs/common-utils
প্রতিটা submodule-এ একসাথে কমান্ড চালানো
git submodule foreach 'git checkout main && git pull'
--recurse-submodules ফ্ল্যাগ কোথায় কোথায়
clone, pull, checkout, fetch — এসব কমান্ডেও --recurse-submodules যোগ করা যায় যাতে submodule-ও একইসাথে সিঙ্ক থাকে। git config --global submodule.recurse true সেট করলে ডিফল্ট আচরণ হিসেবে সবসময় সক্রিয় থাকে।
সাধারণ ব্যথার পয়েন্ট (Pain Points)
কেন submodule-কে "ব্যথাদায়ক" বলা হয়
১. Detached HEAD-এ ভুল করে কমিট করা। submodule ফোল্ডারে ঢুকে সরাসরি কোড পরিবর্তন করে commit করলে সেটা কোনো branch-এ না থাকায় পরে git submodule update চালালে (যেটা pinned SHA-তে ফিরিয়ে আনে) সেই কাজ হারিয়ে যেতে পারে যদি push করা না থাকে।
২. ভুলে পুরনো commit reference রয়ে যাওয়া। submodule-এর ভেতরে নতুন কমিট করে push করার পর, মূল repo-তে git add libs/common-utils && git commit না করলে মূল repo এখনো পুরনো SHA পয়েন্ট করতে থাকে — টিমমেট pull করলেও পুরনো ভার্শনই পাবে।
৩. টিমমেট submodule update ভুলে যাওয়া। কেউ মূল repo pull করলে submodule pointer বদলে যেতে পারে, কিন্তু git pull ডিফল্টভাবে submodule কোড আপডেট করে না — শুধু pointer আপডেট করে। ফলে submodule ফোল্ডারে পুরনো কোড থেকে যায়, "কাজ করছে না" বলে বাগ রিপোর্ট আসে যেটা আসলে stale submodule।
প্রশমনের উপায়
git config --global submodule.recurse true # pull/checkout-এ অটো-রিকার্স
CI pipeline-এও --recurse-submodules clone flag বাধ্যতামূলক করা, আর README-তে স্পষ্ট নির্দেশনা রাখা — এই দুটো টিম-লেভেল ব্যথা অনেকটা কমায়।
git subtree — Submodule-এর বিকল্প
Subtree কী
git subtree submodule-এর বিপরীত দর্শন নেয়: বহিরাগত repository-র কোড আসলেই মূল repository-র history-তে merge করে দেওয়া হয় — কোনো আলাদা pointer/gitlink না, পুরো কোড সরাসরি tree-তে বসে যায়।
# যোগ করা (squash করে, পুরো বহিরাগত history না টেনে)
git subtree add --prefix=libs/common-utils \
https://github.com/org/common-utils.git main --squash
# আপডেট আনা
git subtree pull --prefix=libs/common-utils \
https://github.com/org/common-utils.git main --squash
# পরিবর্তন ফেরত পাঠানো (contribute back)
git subtree push --prefix=libs/common-utils \
https://github.com/org/common-utils.git main
# আলাদা করে নতুন repo হিসেবে বের করে আনা
git subtree split --prefix=libs/common-utils -b extracted-branch
তুলনা টেবিল: Submodule বনাম Subtree
| দিক | Submodule | Subtree |
|---|---|---|
| কোড সংরক্ষণ | শুধু pointer (gitlink), কোড আলাদা repo-তে | পুরো কোড মূল repo-র history-তে merge হয়ে যায় |
| ক্লোন করা | --recurse-submodules লাগবে, নাহলে ফাঁকা ফোল্ডার |
কিছুই লাগে না, git clone করলেই সব কোড আসে |
| শেখার বক্ররেখা | বেশি (detached HEAD, .gitmodules, sync পদক্ষেপ) |
কম (মূলত normal git commands) |
| repo সাইজ | ছোট থাকে (কোড আলাদা) | বড় হয় (পুরো ইতিহাস মিশে যায়) |
| আপস্ট্রিমে contribute ফেরত | সহজ (submodule নিজেই একটা normal repo) | তুলনামূলক জটিল (subtree push) |
| কোন কমিটে পিন করা আছে তা স্পষ্ট | খুব স্পষ্ট (exact SHA) | কম স্পষ্ট, history দেখে বুঝতে হয় |
কখন কোনটা
- বহিরাগত কোড কম পরিবর্তনশীল, ভার্শন-পিন করা দরকার, একাধিক প্রজেক্ট শেয়ার করে → Submodule
- বহিরাগত কোড প্রায়ই edit করতে হবে সরাসরি মূল repo-র ভেতর থেকেই, বা টিমকে
.gitmodulesশেখানোর ঝক্কি এড়াতে চান → Subtree
ল্যাব: Submodule যোগ করা ও ভাঙা-গড়া
লক্ষ্য
একটা submodule যোগ করে, ইচ্ছাকৃতভাবে "stale submodule" সমস্যা তৈরি করে, তারপর ঠিক করা।
ধাপ
- দুইটা টেস্ট repo বানান:
mkdir /tmp/lib-repo && cd /tmp/lib-repo && git init
echo "v1" > lib.txt && git add . && git commit -m "lib v1"
mkdir /tmp/main-repo && cd /tmp/main-repo && git init
echo "app" > app.txt && git add . && git commit -m "init app"
- submodule যোগ করুন:
git submodule add /tmp/lib-repo libs/mylib
git commit -m "Add mylib submodule"
git ls-tree HEAD libs/ # মোড 160000 লক্ষ্য করুন
- Submodule-এর উৎসে (
lib-repo) নতুন কমিট দিন — কিন্তু মূল repo-তে আপডেট করবেন না:
cd /tmp/lib-repo
echo "v2" > lib.txt && git commit -am "lib v2"
- "stale submodule" সমস্যা পুনরায় তৈরি করুন — main-repo-তে ফিরে দেখুন এখনো v1 pinned:
cd /tmp/main-repo
cat libs/mylib/lib.txt # এখনো "v1" দেখাবে, যদিও উৎসে v2 আছে
git submodule status # pinned SHA বনাম upstream SHA-র পার্থক্য বোঝা
- সঠিকভাবে আপডেট করুন এবং মূল repo-তে নতুন pointer commit করুন:
cd libs/mylib && git checkout main && git pull
cd ../.. && git add libs/mylib && git commit -m "Bump mylib to v2"
- যাচাই করুন gitlink SHA বদলেছে:
git ls-tree HEAD libs/
চ্যালেঞ্জ
git submodule deinit -f libs/mylib চালিয়ে submodule সম্পূর্ণ সরিয়ে দেখুন .gitmodules ও .git/config-এ কী পরিবর্তন হয়।
ইন্টারভিউ প্রশ্ন — Module 26
প্র.১ — Submodule মূল repository-তে ঠিক কী সংরক্ষণ করে — পুরো কোড, নাকি অন্য কিছু?
উত্তর: পুরো কোড না, শুধু একটা gitlink entry (mode 160000) যেটা submodule repository-র একটা নির্দিষ্ট commit SHA পয়েন্ট করে। .gitmodules ফাইলে URL সংরক্ষিত থাকে, আর tree-তে SHA — দুটো মিলিয়ে resolve হয়।
প্র.২ — git clone করার পর submodule ফোল্ডার খালি দেখাচ্ছে কেন?
উত্তর: কারণ git clone ডিফল্টভাবে submodule init/checkout করে না। --recurse-submodules দিয়ে ক্লোন করতে হবে, অথবা পরে git submodule update --init --recursive চালাতে হবে।
প্র.৩ — submodule ফোল্ডারের ভেতরে গিয়ে git status চালালে "HEAD detached at ..." কেন দেখায়?
উত্তর: submodule কোনো branch-এর "সর্বশেষ" অবস্থা না, একটা নির্দিষ্ট pinned commit — তাই checkout হয় detached HEAD-এ। ভেতরে সরাসরি কমিট করলে সেটা কোনো branch-এ না থাকায় হারিয়ে যাওয়ার ঝুঁকি থাকে।
প্র.৪ — "stale submodule" সমস্যাটা কীভাবে ঘটে এবং কীভাবে এড়াবেন?
উত্তর: টিমমেট git pull করলে শুধু submodule-এর pointer আপডেট হয়, কোড না — git submodule update আলাদা করে চালাতে হয়। git config --global submodule.recurse true সেট করে এটা স্বয়ংক্রিয় করা যায়।
প্র.৫ — Submodule আর Subtree-এর মধ্যে মূল স্থাপত্যগত পার্থক্য কী? উত্তর: Submodule পয়েন্টার-ভিত্তিক — কোড আলাদা থাকে, শুধু commit reference রাখা হয়। Subtree merge-ভিত্তিক — বহিরাগত কোড সরাসরি মূল repo-র history-তে মিশে যায়, আলাদা init/update ধাপ লাগে না ক্লোনের সময়।
প্র.৬ — কোন পরিস্থিতিতে subtree submodule-এর চেয়ে ভালো পছন্দ?
উত্তর: যখন বহিরাগত কোড ঘন ঘন এডিট করতে হবে সরাসরি মূল repo-র ভেতর থেকেই, অথবা টিমকে .gitmodules/detached-HEAD workflow শেখানোর জটিলতা এড়াতে চান — এবং repository সাইজ বাড়া গ্রহণযোগ্য।
কর্নার কেস — Module 26
.gitmodules-এ URL পাল্টালে শুধু ফাইল এডিট করাই যথেষ্ট না —git submodule syncনা চালালে local.git/config-এ পুরনো URL রয়ে যায়, পরেরfetchপুরনো ঠিকানাতেই যাবে।Submodule-এর ভেতরে
git branch -dচালালে মূল repo-র pinned commit নষ্ট হয় না, কিন্তু সেই commit যদি কোনো ref থেকে unreachable হয়ে যায় এবং submodule-এর নিজস্বgit gcচলে, সেই কমিট হারিয়ে যেতে পারে।Relative URL ব্যবহার করলে submodule fork করা সহজ হয় কিন্তু বিভ্রান্তিকর হতে পারে —
.gitmodules-এ../common-utils.git-এর মতো relative path GitHub/GitLab-এ fork workflow-এ কাজে লাগে, কিন্তু ভিন্ন hosting-এ মাইগ্রেট করলে ভেঙে যেতে পারে।Subtree-তে
--squashনা দিলে বহিরাগত repo-র পুরো commit history মূল repo-তে ঢুকে যায় —git logহঠাৎ শত শত অপরিচিত কমিটে ভরে যায়, blame/bisect জটিল হয়ে ওঠে।--squashদিয়ে একটামাত্র merge commit রাখা সাধারণত নিরাপদ পছন্দ।git submodule updateডিফল্টভাবে merge/rebase strategy ছাড়াই checkout করে — যদি submodule-এর ভেতরে লোকাল uncommitted পরিবর্তন থাকে,updateব্যর্থ হয়ে যায় বা সেগুলো silently overwrite করার ঝুঁকি তৈরি করে;--merge/--rebaseফ্ল্যাগ যোগ করলে আচরণ স্পষ্ট হয়।
MODULE 27: বড় Repo / Monorepo
হাজার হাজার কমিট, গিগাবাইট গিগাবাইট বাইনারি অ্যাসেট, শত শত ফোল্ডারের একটা মনোরেপো — পুরো জিনিস প্রতিটা ডেভেলপারের মেশিনে সম্পূর্ণ ক্লোন করা অবাস্তব হয়ে যায়। কভার হবে: shallow ও partial clone (lazy fetch), sparse-checkout (দরকারি অংশটুকু চেকআউট), git maintenance (background housekeeping), আর Git LFS (বড় বাইনারি ফাইল ম্যানেজমেন্ট)। এই মডিউল শেষে একটা Phase 8-এর checkpoint gate আছে।
Shallow Clone: `--depth` ও পরে `git fetch --unshallow`
সমস্যাটা কী
একটা repository-র ১০ বছরের ইতিহাস, লাখো কমিট — কিন্তু CI pipeline-এর দরকার শুধু সর্বশেষ কোড, পুরো ইতিহাস না। পুরো ক্লোন করলে সময় ও ব্যান্ডউইথ অপচয় হয়।
Shallow Clone
git clone --depth 1 https://github.com/org/huge-repo.git
এটা শুধু সর্বশেষ ১টা কমিট (আর তার সাথে যুক্ত tree/blob object) fetch করে। ফলাফল একটা truncated history — পুরনো কমিটে git log/git blame গেলে "shallow" boundary-তে থেমে যাবে।
$ git log --oneline
a1b2c3d Latest commit
# তার আগে আর কিছু নেই — history truncated
পরে পুরো ইতিহাস দরকার হলে
git fetch --unshallow
এটা shallow repository-কে সম্পূর্ণ, normal repository-তে রূপান্তর করে — বাকি সব commit history fetch করে আনে।
রিয়েল-লাইফ উদাহরণ
CI pipeline-এ যেখানে শুধু build/test করতে হবে, --depth 1 clone সেকেন্ডের পার্থক্য তৈরি করে বড় monorepo-তে। কিন্তু যদি সেই pipeline-এ git blame বা tag-ভিত্তিক changelog তৈরি করতে হয়, shallow clone কাজ করবে না — সেক্ষেত্রে --depth বাড়াতে হয় বা fetch --unshallow করতে হয়।
সতর্কতা
Shallow repository থেকে push করা বা নতুন clone বানানো (clone from shallow) কিছু সীমাবদ্ধতা আনে — সব Git server শুরুর দিকে shallow fetch/push পুরোপুরি সমর্থন করত না, আধুনিক Git/GitHub-এ এটা মূলত ঠিকঠাক কাজ করে।
Partial Clone: `--filter=blob:none` / `--filter=tree:0`
Shallow বনাম Partial — পার্থক্য
Shallow clone সময়ের গভীরতা (কতগুলো পুরনো commit) কমায়। Partial clone ভিন্ন মাত্রায় কাজ করে — পুরো commit history রাখে, কিন্তু blob (ফাইল content) বা tree object lazily, প্রয়োজন হলেই fetch করে।
--filter=blob:none
git clone --filter=blob:none https://github.com/org/huge-repo.git
সব commit ও tree object আসে (তাই পুরো history নেভিগেট করা যায়, git log স্বাভাবিক কাজ করে), কিন্তু blob (actual ফাইল content) আসে না — যখন git checkout কোনো ফাইলের content দরকার হয়, তখন সার্ভার থেকে on-demand fetch হয়।
--filter=tree:0
আরও আক্রমণাত্মক — tree object-ও শুরুতে আসে না, শুধু commit object। git log-এর মতো কমান্ডও কিছু ক্ষেত্রে lazy fetch ট্রিগার করবে।
রিয়েল-লাইফ উদাহরণ
একটা মনোরেপোতে ১০০+ সার্ভিস আছে, কিন্তু একজন ডেভেলপার শুধু একটা সার্ভিসে কাজ করেন। --filter=blob:none দিয়ে ক্লোন করলে পুরো commit graph/history ঠিকঠাক দেখা যায় (navigation স্বাভাবিক), কিন্তু ডিস্কে সব ফাইলের content নামানো হয় না — sparse-checkout-এর সাথে মিলিয়ে ব্যবহার করলে এটা সবচেয়ে কার্যকর।
$ git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
$ cd monorepo
$ git sparse-checkout set services/payment
ট্রেড-অফ
- Lazy fetch-এর জন্য প্রতিটা নতুন blob দরকার হলে নেটওয়ার্ক কল লাগে — অফলাইনে কাজ করলে সমস্যা হতে পারে যদি blob আগে থেকে cache না থাকে।
- সব Git hosting provider (self-hosted পুরনো সার্ভার) partial clone protocol সমর্থন নাও করতে পারে।
Sparse-Checkout: Cone Mode দিয়ে শুধু দরকারি ফোল্ডার
সমস্যাটা কী
মনোরেপোতে হাজার হাজার ফোল্ডার — একজন ডেভেলপারের দরকার হয়তো মাত্র ২-৩টা। পুরো working directory-তে সব ফাইল উপস্থিত থাকলে IDE indexing ধীর, ডিস্ক স্পেস অপচয়, আর git status স্লো হয়ে যায়।
Cone Mode Sparse-Checkout
git sparse-checkout init --cone
git sparse-checkout set services/payment services/notification
git sparse-checkout list
--cone মোড (আধুনিক, দ্রুত) ডিরেক্টরি-ভিত্তিক প্যাটার্ন ব্যবহার করে — শুধু নির্দিষ্ট করা ফোল্ডার এবং root-এর ফাইলগুলো working directory-তে থাকে, বাকি সব "প্রেজেন্ট কিন্তু hidden" থাকে (object database-এ আছে, working tree-তে নেই)।
$ ls
services/ README.md
$ ls services/
payment/ notification/
# services/inventory/, services/auth/ ইত্যাদি working tree-তে অনুপস্থিত
যোগ/বাদ দেওয়া
git sparse-checkout add services/inventory # আরেকটা ফোল্ডার যোগ
git sparse-checkout disable # সম্পূর্ণ working tree ফিরিয়ে আনা
Cone বনাম Non-cone মোড
পুরনো (non-cone) মোড pattern-ভিত্তিক ছিল (.gitignore-এর মতো glob pattern) — নমনীয় কিন্তু ধীর, বড় repo-তে পারফরম্যান্স সমস্যা হতো। Cone mode ডিরেক্টরি prefix-ভিত্তিক match করে বলে অনেক দ্রুত, আর এটাই আধুনিক Git-এর সুপারিশকৃত ডিফল্ট (git sparse-checkout init --cone)।
রিয়েল-লাইফ উদাহরণ
একটা কোম্পানির ১৫০টা মাইক্রোসার্ভিসের মনোরেপো — payment-team-এর ডেভেলপাররা --filter=blob:none --sparse clone করে শুধু services/payment আর shared libs/ ফোল্ডার sparse-checkout করেন। ক্লোন সময় মিনিট থেকে সেকেন্ডে নেমে আসে, IDE indexing দ্রুত হয়।
`git maintenance` — ব্যাকগ্রাউন্ড Auto-GC ও Prefetch
সমস্যাটা কী
বড় repository-তে ম্যানুয়ালি git gc চালানো ভুলে যাওয়া হয়, ফলে loose object জমে repository ধীর হয়ে যায়। আবার git gc নিজেই ভারী অপারেশন — কাজের মাঝখানে হঠাৎ চালু হলে বিরক্তিকর।
git maintenance কী করে
git maintenance start
এটা background-এ (scheduled task/cron বা systemd timer, প্ল্যাটফর্ম অনুযায়ী) নিয়মিত কিছু maintenance task চালানোর ব্যবস্থা করে:
| Task | কাজ | ফ্রিকোয়েন্সি (ডিফল্ট) |
|---|---|---|
gc |
পুরনো/অপ্রয়োজনীয় object পরিষ্কার, repack | সাপ্তাহিক |
commit-graph |
কমিট গ্রাফ ফাইল আপডেট (দ্রুত log/merge-base) |
দৈনিক |
prefetch |
ব্যাকগ্রাউন্ডে remote থেকে নতুন object আগেভাগে নামানো | ঘণ্টাভিত্তিক |
loose-objects |
ছোট loose object-গুলো pack-এ একত্র করা | দৈনিক |
incremental-repack |
ধাপে ধাপে repack, একবারে ভারী না করে | দৈনিক |
বন্ধ করা
git maintenance stop
git maintenance unregister # কনফিগ থেকে এই repo সরিয়ে দেওয়া
রিয়েল-লাইফ উদাহরণ
একটা মনোরেপোতে যেখানে শত শত ডেভেলপার প্রতিদিন fetch করেন, prefetch টাস্ক ব্যাকগ্রাউন্ডে remote object আগে থেকে নামিয়ে রাখে বলে সকালে git pull করলে ফলাফল প্রায় সাথে সাথেই আসে — ব্যান্ডউইথ ব্যবহারও সারাদিন জুড়ে সমানভাবে বিতরণ হয়, একটা peak সময়ে না।
কেন এটা ম্যানুয়াল git gc-এর চেয়ে ভালো
ম্যানুয়াল git gc একবারে সব কাজ করতে গিয়ে ভারী হয়ে যায় এবং ডেভেলপারের কাজের সময় ট্রিগার হলে বিরক্তিকর। git maintenance-এর incremental, scheduled পদ্ধতি ছোট ছোট ধাপে কাজ ভাগ করে দেয়, idle সময়ে চালায়।
Git LFS: বড় বাইনারি ফাইল ম্যানেজমেন্ট
সমস্যাটা কী
Git ডিজাইন করা হয়েছে টেক্সট-ভিত্তিক সোর্স কোডের delta compression-এর জন্য। একটা ৫০MB ভিডিও ফাইল বা PSD ডিজাইন ফাইল সরাসরি কমিট করলে — প্রতিটা পরিবর্তনে পুরো ফাইলের নতুন কপি object database-এ জমা হয় (বাইনারি ফাইলে effective delta compression হয় না), repository-র সাইজ দ্রুত গিগাবাইটে পৌঁছে যায়, প্রতিটা ক্লোন ভারী হয়ে যায়।
Git LFS (Large File Storage) সমাধান
LFS আসল বাইনারি ফাইল repository-র বাইরে (একটা আলাদা LFS server/storage-এ) রাখে, আর Git history-তে শুধু একটা ছোট pointer ফাইল রাখে:
git lfs install
git lfs track "*.psd"
git add .gitattributes
git add design.psd
git commit -m "Add design file via LFS"
.gitattributes-এ যোগ হয়:
*.psd filter=lfs diff=lfs merge=lfs -text
Git history-তে design.psd-র জায়গায় আসলে যা কমিট হয়:
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214...
size 52428800
মূল কমান্ড
git lfs push origin main # LFS object আলাদা করে আপলোড
git lfs pull # actual content নামানো (checkout-এর সময় pointer resolve)
git lfs ls-files # কোন ফাইলগুলো LFS-ট্র্যাকড তা দেখা
কেন বড় বাইনারি সরাসরি Git-এ রাখা উচিত না
- Delta compression কাজ করে না — প্রতিটা সংস্করণ প্রায় পুরোটাই নতুন করে সংরক্ষিত হয়।
- ক্লোন সময় ও সাইজ বিস্ফোরিত হয় — সবাইকে পুরো ইতিহাস (সব পুরনো ভার্শনসহ) ডাউনলোড করতে হয়, LFS-এ শুধু বর্তমান দরকারি ভার্শন আনা যায়।
git gc/repack ধীর হয়ে যায় — বড় বাইনারি object নিয়ে repack করা ব্যয়বহুল।
LFS দিয়ে ইতিহাসে শুধু ছোট pointer থাকে, তাই এই সব সমস্যা এড়ানো যায়।
ল্যাব: Sparse-Checkout ও Partial Clone একসাথে
লক্ষ্য
একটা মাল্টি-ফোল্ডার "মনোরেপো" সিমুলেট করে partial clone + sparse-checkout দিয়ে শুধু একটা অংশ কাজে লাগানো।
ধাপ
- একটা bare "monorepo" বানান একাধিক ফোল্ডার সহ:
mkdir /tmp/mono-source && cd /tmp/mono-source && git init
mkdir -p services/payment services/notification services/inventory libs/common
echo "payment code" > services/payment/main.py
echo "notif code" > services/notification/main.py
echo "inv code" > services/inventory/main.py
echo "shared code" > libs/common/utils.py
git add . && git commit -m "monorepo initial structure"
- Partial clone করুন (blob:none filter সহ, sparse ফ্ল্যাগসহ):
cd /tmp
git clone --filter=blob:none --sparse /tmp/mono-source mono-clone
cd mono-clone
ls # শুধু root-level কী আছে দেখুন
- Cone mode sparse-checkout সেট করুন শুধু
services/paymentআরlibs/common-এর জন্য:
git sparse-checkout init --cone
git sparse-checkout set services/payment libs/common
ls services/ # শুধু payment/ দেখাবে, notification/ ও inventory/ অনুপস্থিত
- যাচাই করুন
git log/history স্বাভাবিক কাজ করছে (partial clone-এ commit metadata সবসময় থাকে):
git log --oneline
- আরেকটা ফোল্ডার যোগ করুন এবং দেখুন lazy blob fetch ঘটছে:
git sparse-checkout add services/notification
ls services/ # এখন notification/ ও দেখাবে
- পরিষ্কার করুন:
git sparse-checkout disable # পুরো working tree ফিরিয়ে আনা
চ্যালেঞ্জ
git lfs install করে একটা বড় (ডামি) ফাইল *.bin প্যাটার্নে ট্র্যাক করে কমিট করুন, তারপর .gitattributes আর actual commit-এ pointer ফাইলের বিষয়বস্তু পরীক্ষা করুন।
ইন্টারভিউ প্রশ্ন — Module 27
প্র.১ — Shallow clone আর partial clone-এর মধ্যে মূল পার্থক্য কী?
উত্তর: Shallow clone (--depth) commit history-র গভীরতা কমায় — পুরনো কমিট আসেই না। Partial clone (--filter=blob:none) পুরো commit/tree history রাখে কিন্তু blob content lazily fetch করে — history navigation স্বাভাবিক থাকে, শুধু ফাইল content প্রয়োজনে আসে।
প্র.২ — Sparse-checkout cone mode কেন non-cone মোডের চেয়ে বেশি সুপারিশ করা হয়? উত্তর: Cone mode ডিরেক্টরি prefix-ভিত্তিক match করে বলে বড় repository-তে অনেক দ্রুত পারফর্ম করে। Non-cone মোড glob-pattern ভিত্তিক, নমনীয় কিন্তু বড় স্কেলে ধীর।
প্র.৩ — git maintenance start ম্যানুয়াল git gc চালানোর চেয়ে কীভাবে ভালো?
উত্তর: এটা কাজকে ছোট, নির্ধারিত-সময়ের টাস্কে ভাগ করে (gc, commit-graph, prefetch, repack) এবং idle সময়ে ব্যাকগ্রাউন্ডে চালায় — ফলে ডেভেলপারের কাজের মাঝে হঠাৎ ভারী gc চালানোর বিরক্তি এড়ানো যায়, আর prefetch আগে থেকেই নতুন object নামিয়ে রাখে বলে pull দ্রুত হয়।
প্র.৪ — Git-এ বড় বাইনারি ফাইল সরাসরি কমিট করা কেন সমস্যাজনক?
উত্তর: বাইনারি ফাইলে কার্যকর delta compression হয় না, প্রতিটা পরিবর্তনে প্রায় পুরো ফাইল নতুন করে সংরক্ষিত হয় — repository সাইজ দ্রুত বাড়ে, ক্লোন সময় ও gc/repack সময় বেড়ে যায়।
প্র.৫ — Git LFS ইতিহাসে আসলে কী সংরক্ষণ করে?
উত্তর: আসল ফাইল content না — একটা ছোট pointer ফাইল (oid/sha256 hash আর size) যেটা LFS storage-এ প্রকৃত content-এর দিকে নির্দেশ করে। .gitattributes-এ filter=lfs নির্ধারণ করে দেয় কোন প্যাটার্নের ফাইল LFS দিয়ে হ্যান্ডল হবে।
প্র.৬ — একটা টিম একটা বিশাল মনোরেপোতে দ্রুত onboard হতে চায় — কোন তিনটা টুল একসাথে ব্যবহার করবেন?
উত্তর: --filter=blob:none (partial clone, শুধু দরকারি blob lazily fetch), --sparse + sparse-checkout set (শুধু দরকারি ফোল্ডার working tree-তে), আর git maintenance start (background prefetch/gc যাতে পারফরম্যান্স দীর্ঘমেয়াদে ভালো থাকে)।
কর্নার কেস — Module 27
Shallow clone থেকে push করলে বা কিছু পুরনো Git hosting-এ সমস্যা হতে পারে — কিছু সার্ভার shallow repository থেকে আসা push সম্পূর্ণভাবে গ্রহণ করে না। আধুনিক GitHub/GitLab-এ এটা মূলত ঠিকঠাক কাজ করে, কিন্তু নিজস্ব হোস্টেড পুরনো সার্ভারে যাচাই করে নেওয়া ভালো।
git fetch --unshallowপুরো ইতিহাস আনতে অনেক সময় নিতে পারে — বড় repository-তে এটা প্রায় পুরো ক্লোনের সমান খরচ, তাই CI-তে shallow clone ব্যবহার করে পরে unshallow করার প্ল্যান থাকলে বিবেচনা করে নেওয়া উচিত এটা আসলেই দরকার কিনা।Sparse-checkout সক্রিয় থাকা অবস্থায়
git checkout <branch>করলে working tree-তে অনুপস্থিত ফোল্ডারগুলো branch পাল্টানোর পরও অনুপস্থিতই থাকে — নতুনরা ভাবেন ফাইল হারিয়ে গেছে, আসলে সেগুলো এখনো sparse-checkout প্যাটার্নের বাইরে।LFS bandwidth quota — GitHub-এর মতো hosting-এ LFS storage ও bandwidth-এর আলাদা quota থাকে (সাধারণ repository storage থেকে ভিন্ন), অতিরিক্ত ব্যবহার হলে আলাদা বিলিং প্রযোজ্য হতে পারে — এটা অনেক টিম দেরিতে আবিষ্কার করে।
কেউ যদি
.gitattributesসেটআপের আগেই বড় ফাইল সরাসরি কমিট করে ফেলে, পরে LFS ট্র্যাক করা সেই পুরনো কমিটকে রিট্রোঅ্যাক্টিভলি বদলায় না — ইতিমধ্যে করা কমিটগুলো এখনো পুরনোভাবে (non-LFS) সংরক্ষিত থাকবে, ইতিহাস থেকে সরাতেgit lfs migrateবা history rewrite (Module 33) লাগবে।
চেকপয়েন্ট — Phase 8 সম্পন্ন
আপনি কি পারবেন?
- একই সময়ে দুইটা branch-এ কাজ করার দরকার হলে
git worktree addদিয়ে branch switch/stash ছাড়াই সমান্তরাল working directory তৈরি করতে পারবেন, এবং ব্যাখ্যা করতে পারবেন কেন objects/refs শেয়ার্ড কিন্তু HEAD/index প্রতিটা worktree-র নিজস্ব? .gitmodulesফাইলের গঠন এবং gitlink (mode 160000) মেকানিজম ব্যাখ্যা করতে পারবেন, আর submodule-এর "stale reference" ও "detached HEAD" সমস্যাগুলো চিনে ঠিক করতে পারবেন?- Submodule আর subtree-র মধ্যে একটা বাস্তব সিদ্ধান্ত নিতে পারবেন — কখন pointer-ভিত্তিক পদ্ধতি ভালো, কখন merge-ভিত্তিক পদ্ধতি?
- একটা বিশাল মনোরেপোর জন্য shallow clone, partial clone (
--filter=blob:none), আর cone-mode sparse-checkout — এই তিনটা কৌশল মিলিয়ে একজন ডেভেলপারের onboarding সময় কমাতে পারবেন? - ব্যাখ্যা করতে পারবেন কেন বড় বাইনারি ফাইল সরাসরি Git-এ না রেখে Git LFS দিয়ে পরিচালনা করা উচিত, এবং
.gitattributes-এ কীভাবে সেটা রেজিস্টার হয়?
এক লাইনে সারমর্ম
এই Phase-এর সব টুল একটা জিনিসেরই প্রতিফলন: স্কেল। Worktree সমাধান করে "একই সময়ে একাধিক জায়গায় কাজ", submodule/subtree সমাধান করে "repo-র ভেতরে repo", আর shallow/partial clone + sparse-checkout + LFS সমাধান করে "repo এত বড় যে পুরোটা কারো দরকারও নেই, সামর্থ্যও নেই"। এই টুলিং-ই একজন সোলো ডেভেলপারের ওয়ার্কফ্লোকে একটা বড়-টিম/মনোরেপো ওয়ার্কফ্লোর জন্য প্রস্তুত করে তোলে।
PHASE 9: Customization, Hooks, Automation
Git একটা সাধারণ টুল থেকে একটা personalized, automated ওয়ার্কফ্লো ইঞ্জিনে রূপান্তরিত হয় তিনটা জিনিসের মাধ্যমে: alias/config দিয়ে নিজের ভাষায় শর্টকাট বানানো, .gitignore/.gitattributes দিয়ে Git-কে বলে দেওয়া কোন ফাইল কীভাবে ট্রিট করতে হবে, আর hooks দিয়ে নির্দিষ্ট মুহূর্তে স্বয়ংক্রিয় স্ক্রিপ্ট চালানো।
এই Phase-এ তিনটা মডিউল — Aliases & Config, gitignore/gitattributes, আর Hooks। এগুলো "দিনে দিনে ব্যবহারযোগ্যতা" বাড়ানোর টুল — যেগুলো শেখার পর একটা টিমের পুরো কমিট-quality gate নিজেই বানিয়ে ফেলতে পারবেন, কোনো তৃতীয় পক্ষের CI না চালিয়েও।
MODULE 28: Aliases & Config
Git-এর alias.* config দিয়ে যেকোনো লম্বা কমান্ডকে ছোট, ব্যক্তিগত শর্টকাটে রূপান্তর করা যায় — এমনকি শেল কমান্ডও চালানো যায় ! প্রিফিক্স দিয়ে। কভার হবে: pure git-subcommand alias বনাম shell-command alias, worktree-scope config, includeIf রিক্যাপ, --get-regexp/--unset-all, আর কিছু জনপ্রিয় প্রোডাক্টিভিটি alias।
git alias — Pure Subcommand বনাম Shell Command (`!`)
দুই ধরনের Alias
Git alias সেট করা হয় git config দিয়ে alias.<name> কী-তে:
git config --global alias.co checkout
git config --global alias.st status
এরপর git co main চললে আসলে git checkout main-ই চলে। এটা pure subcommand alias — Git নিজেই এটাকে অভ্যন্তরীণভাবে resolve করে, তাই এখানে শুধু Git-এর নিজস্ব সাব-কমান্ড আর তাদের আর্গুমেন্ট বসানো যায়।
! দিয়ে Shell Command Alias
আরও শক্তিশালী রূপ — ! প্রিফিক্স দিলে পুরো লাইনটা একটা শেল কমান্ড হিসেবে চলে, যেকোনো বাহ্যিক টুল, pipe, বা একাধিক কমান্ড চেইন করা যায়:
git config --global alias.undo '!git reset --soft HEAD~1'
git config --global alias.last '!git log -1 HEAD --stat'
git config --global alias.cleanup '!git branch --merged | grep -v "\*\|main\|master" | xargs -n 1 git branch -d'
পার্থক্য সারাংশ
| দিক | Pure subcommand | Shell (!) |
|---|---|---|
| সিনট্যাক্স | alias.co = checkout |
alias.undo = !git reset --soft HEAD~1 |
| কী চালাতে পারে | শুধু Git সাব-কমান্ড | যেকোনো শেল কমান্ড, pipe, external টুল |
| কারেন্ট ডিরেক্টরি | সবসময় repository root থেকে সাপেক্ষে চলে | শেলের বর্তমান working directory থেকে চলে (সাবধান!) |
| আর্গুমেন্ট পাস | স্বয়ংক্রিয়ভাবে appended হয় | $1, $2 ইত্যাদি ম্যানুয়ালি হ্যান্ডল করতে হয় |
রিয়েল-লাইফ সতর্কতা
Shell alias git-এর "সবসময় repo root-এ চলে" গ্যারান্টি হারায় — সাবফোল্ডারে থেকে চালালে ভিন্ন আচরণ করতে পারে যদি script-এ relative path থাকে। এই কারণে জটিল shell alias-এ cd "$(git rev-parse --show-toplevel)" দিয়ে root-এ ফিরে যাওয়া একটা ভালো অভ্যাস।
Worktree-Scope Config ও `includeIf` রিক্যাপ
Config Scope-এর স্তর (রিক্যাপ)
Git config চারটা স্তরে থাকতে পারে, precedence অনুযায়ী (নিচেরটা সবচেয়ে বেশি প্রাধান্য পায়):
system → global → local (repo) → worktree
Worktree-Scope Config (--worktree)
Module 25-এ শেখা git worktree ব্যবহারে, প্রতিটা linked worktree সাধারণত একই local config শেয়ার করে — কিন্তু কখনো কখনো প্রতিটা worktree-র জন্য আলাদা config দরকার হয় (যেমন ভিন্ন user.email, বা ভিন্ন core.sparseCheckout সেটিং)। এর জন্য দরকার extensions.worktreeConfig সক্রিয় করা:
git config extensions.worktreeConfig true
git config --worktree user.email "hotfix-bot@company.com"
এতে করে .git/worktrees/<name>/config.worktree নামে একটা আলাদা ফাইলে সেটিং সংরক্ষিত হয়, যেটা শুধু সেই নির্দিষ্ট worktree-তে প্রযোজ্য।
includeIf রিক্যাপ
আগে শেখা includeIf কন্ডিশনাল কনফিগারেশনের একটা শক্তিশালী প্যাটার্ন — ডিরেক্টরি-ভিত্তিক ভিন্ন identity ব্যবহারের জন্য:
# ~/.gitconfig
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
--get-regexp ও --unset-all
# regexp দিয়ে একগুচ্ছ কনফিগ কী দেখা
git config --get-regexp alias\.*
git config --get-regexp user\.*
# একটা কী-র multiple value একসাথে সরানো (যেমন একাধিক remote push url)
git config --unset-all remote.origin.pushurl
--get-regexp বিশেষভাবে কাজে লাগে যখন সব alias এক নজরে দেখতে চান, অথবা ডিবাগ করার সময় বুঝতে চান কোন কনফিগ স্তর থেকে একটা ভ্যালু আসছে (git config --show-origin-এর সাথে মিলিয়ে)।
জনপ্রিয় প্রোডাক্টিভিটি Alias উদাহরণ
কেন Alias গুরুত্বপূর্ণ
প্রতিদিন টাইপ করা দীর্ঘ কমান্ড ছোট শর্টকাটে নামিয়ে আনলে কগনিটিভ লোড কমে, ভুল টাইপো কমে, আর দলের মধ্যে শেয়ার করা alias set একটা "টিম কনভেনশন"ও হতে পারে।
জনপ্রিয় কিছু উদাহরণ
# সুন্দর গ্রাফ-সহ লগ
git config --global alias.lg "log --graph --oneline --decorate --all"
# শর্ট checkout
git config --global alias.co checkout
# শেষ কমিট বাতিল করে পরিবর্তনগুলো staged রাখা
git config --global alias.undo '!git reset --soft HEAD~1'
# staged ও unstaged দুটোই একসাথে দেখা
git config --global alias.st status
# amend, মেসেজ না বদলে
git config --global alias.amend 'commit --amend --no-edit'
# merged হয়ে যাওয়া local branch একসাথে পরিষ্কার
git config --global alias.cleanup '!git branch --merged main | grep -v "\*\|main" | xargs -n 1 git branch -d'
# কোন ফাইল কোন কমিটে সবচেয়ে বেশি পরিবর্তিত হয়েছে
git config --global alias.churn "log --format=format: --name-only | egrep -v '^$' | sort | uniq -c | sort -rg | head -20"
# stash সহ দ্রুত sync
git config --global alias.sync '!git stash && git pull --rebase && git stash pop'
টিম-লেভেল Alias শেয়ার করা
~/.gitconfig-এ ব্যক্তিগত alias রাখার পাশাপাশি, একটা repo-লেভেল .gitconfig ফাইল তৈরি করে git config --local include.path ../.gitconfig-team দিয়ে টিমের কমন alias শেয়ার করা যায় — প্রতিটা ডেভেলপারকে ম্যানুয়ালি কপি-পেস্ট করতে হয় না।
সতর্কতা
খুব বেশি কাস্টম alias টিমের নতুন সদস্যদের জন্য "hidden magic" তৈরি করতে পারে — onboarding doc-এ alias-গুলো ডকুমেন্ট করা ভালো অভ্যাস, বিশেষ করে shell (!) alias যেগুলো standard Git আচরণ থেকে ভিন্ন।
ল্যাব: নিজের Alias সেট তৈরি করা
লক্ষ্য
কয়েকটা alias তৈরি করে, তাদের ভিন্নতা (pure বনাম shell) হাতে-কলমে বোঝা।
ধাপ
- একটা pure subcommand alias তৈরি করুন:
git config --global alias.st "status -sb"
git st
- একটা shell alias তৈরি করুন যেটা আর্গুমেন্ট নেয়:
git config --global alias.new-branch '!f() { git checkout -b "$1" && git push -u origin "$1"; }; f'
git new-branch feature/test-alias
- যাচাই করুন কোথা থেকে alias চালু হচ্ছে সেটা directory-independent কিনা — একটা সাবফোল্ডারে গিয়ে চালান:
mkdir -p src/nested && cd src/nested
git st # pure subcommand alias সবসময় repo-root সাপেক্ষে ঠিকঠাক কাজ করে
- সব alias এক নজরে দেখুন:
git config --get-regexp alias\.
git lgতৈরি করুন এবং grap-friendly লগ দেখুন:
git config --global alias.lg "log --graph --oneline --decorate --all"
git lg
- একটা ভুল alias সরান:
git config --global --unset alias.new-branch
চ্যালেঞ্জ
extensions.worktreeConfig সক্রিয় করে একটা worktree-স্কোপড user.email সেট করুন (Module 25-এর worktree ল্যাবের সাথে মিলিয়ে), তারপর যাচাই করুন সেই worktree-তে করা কমিটে ভিন্ন email ব্যবহৃত হচ্ছে কিনা git log --format='%ae' দিয়ে।
ইন্টারভিউ প্রশ্ন — Module 28
প্র.১ — pure subcommand alias আর ! দিয়ে shell alias-এর মধ্যে সবচেয়ে গুরুত্বপূর্ণ ফাংশনাল পার্থক্য কী?
উত্তর: Pure subcommand alias সবসময় repository root-এর সাপেক্ষে predictably চলে এবং শুধু Git সাব-কমান্ড চালাতে পারে। Shell alias (!) শেলের বর্তমান working directory থেকে চলে (সাবফোল্ডার থেকে চালালে ভিন্ন আচরণ হতে পারে) এবং যেকোনো বাহ্যিক কমান্ড/pipe চালাতে পারে।
প্র.২ — worktree-scope config কখন দরকার হয়?
উত্তর: যখন একই repository-র বিভিন্ন worktree-তে ভিন্ন কনফিগারেশন দরকার (যেমন হটফিক্স worktree-তে ভিন্ন user.email, বা ভিন্ন sparse-checkout সেটিং), extensions.worktreeConfig true সক্রিয় করে git config --worktree দিয়ে সেট করতে হয়।
প্র.৩ — git config --get-regexp কী কাজে লাগে?
উত্তর: একগুচ্ছ related কনফিগ কী (যেমন সব alias, বা সব user.* কী) এক নজরে regexp প্যাটার্ন দিয়ে দেখার জন্য — ডিবাগিং বা audit-এর সময় কাজে লাগে।
প্র.৪ — includeIf "gitdir:~/work/" প্যাটার্নের বাস্তব ব্যবহার কী?
উত্তর: ডিরেক্টরি-ভিত্তিক শর্তসাপেক্ষে ভিন্ন Git config লোড করা — যেমন ~/work/ ফোল্ডারের ভেতরের সব repository-তে অফিসের user.email স্বয়ংক্রিয়ভাবে ব্যবহার হবে, ~/personal/-এ ব্যক্তিগত ইমেইল — ম্যানুয়ালি প্রতিটা repo-তে সেট করার দরকার নেই।
প্র.৫ — git config --unset-all কখন --unset-এর চেয়ে বেশি দরকারি?
উত্তর: যখন একটা কনফিগ কী-র একাধিক value থাকে (যেমন multi-value remote.origin.pushurl বা একাধিক alias-জাতীয় সেটিং যেখানে multiple entries জমে গেছে) — --unset শুধু প্রথম match সরায় (বা একাধিক match থাকলে error দেয়), --unset-all সব একসাথে সরায়।
প্র.৬ — টিমে shell (!) alias শেয়ার করার সময় কী সতর্কতা রাখা উচিত?
উত্তর: এগুলো "hidden magic" তৈরি করতে পারে — নতুন টিম সদস্য বুঝবে না alias আসলে কী করছে। onboarding doc-এ ডকুমেন্ট করা এবং ধ্বংসাত্মক shell কমান্ড (যেমন force-delete জাতীয়) alias-এ রাখার আগে দুইবার ভাবা উচিত।
কর্নার কেস — Module 28
Shell alias-এ quote/escape ভুল হলে নীরবে ভুল আচরণ করতে পারে — বিশেষ করে
grep -v "\*\|main"-এর মতো প্যাটার্নে shell-এর নিজস্ব escaping আর regex escaping একসাথে সামলাতে হয়, ভুল হলে alias "কাজ করছে" মনে হলেও ভুল ফাইল/branch টার্গেট করতে পারে।alias.co-এর মতো নাম Git-এর নিজস্ব ভবিষ্যৎ সাব-কমান্ডের সাথে সংঘর্ষ করতে পারে — যদি ভবিষ্যতে Git নিজেইcoনামে কোনো নতুন সাব-কমান্ড যোগ করে, ব্যক্তিগত alias সেটার সাথে সাংঘর্ষিক হতে পারে (যদিও এখনো এমন কিছু হয়নি, এটা একটা সচেতনতার পয়েন্ট)।--globalআর--localউভয় জায়গায় একই alias সেট করা থাকলে--local-টাই জেতে — precedence ভুলে গেলে "আমি তো global-এ change করলাম, কাজ করছে না কেন" ধরনের বিভ্রান্তি হয়।git config --list --show-originদিয়ে কোন স্তর থেকে ভ্যালু আসছে যাচাই করা উচিত।worktree-scope config সক্রিয় করার আগে (
extensions.worktreeConfig)git config --worktreeচালালে নীরবেlocalscope-এ গিয়ে বসতে পারে — এক্সটেনশন সক্রিয় আছে কিনা আগে নিশ্চিত করা জরুরি, নাহলে "worktree-specific" ভেবে যা সেট করলেন তা আসলে পুরো repo-তে প্রযোজ্য হয়ে গেল।
MODULE 29: gitignore/gitattributes
.gitignore বলে দেয় কোন ফাইল Git-এর নজরেই আসবে না, আর .gitattributes বলে দেয় যে ফাইলগুলো ট্র্যাক করা হচ্ছে সেগুলোর সাথে ঠিক কী আচরণ করা হবে (line-ending normalization, diff/merge কৌশল, archive-এ অন্তর্ভুক্তি)। কভার হবে: pattern সিনট্যাক্স, global ignore, check-ignore/check-attr ডিবাগিং টুল, cross-platform CRLF/LF সমস্যা, custom diff/merge driver, আর GitHub linguist hint।
.gitignore প্যাটার্ন সিনট্যাক্স
মূল সিনট্যাক্স নিয়ম
# কমেন্ট লাইন
*.log # যেকোনো এক্সটেনশন match (glob)
/build # শুধু root-level build ফোল্ডার, সাবফোল্ডারের build/ না
node_modules/ # trailing slash = শুধু ডিরেক্টরি, ফাইল না
**/temp # যেকোনো গভীরতায় temp নামের ফোল্ডার/ফাইল
!important.log # negation — উপরের প্যাটার্নে match হলেও এটাকে un-ignore করা
গুরুত্বপূর্ণ খুঁটিনাটি
| প্যাটার্ন | অর্থ |
|---|---|
*.env |
সব ডিরেক্টরিতে যেকোনো .env এক্সটেনশনের ফাইল |
/*.env |
শুধু root ডিরেক্টরির .env ফাইল |
logs/ |
trailing slash — নিশ্চিত করে এটা ডিরেক্টরি pattern, একই নামের ফাইল match হবে না |
**/*.pyc |
যেকোনো গভীরতার সব .pyc ফাইল |
!logs/keep.log |
logs/ ইগনোর করা থাকলেও ভেতরের একটা নির্দিষ্ট ফাইল রাখতে চাইলে — কিন্তু সতর্কতা: প্যারেন্ট ডিরেক্টরি পুরোপুরি ইগনোর হলে negation কাজ নাও করতে পারে |
Negation-এর সীমাবদ্ধতা
যদি একটা ডিরেক্টরি সম্পূর্ণ ইগনোর করা থাকে (logs/), তার ভেতরের কোনো নির্দিষ্ট ফাইল !logs/keep.log দিয়ে un-ignore করার চেষ্টা কাজ করবে না — কারণ Git ইগনোর করা ডিরেক্টরির ভেতরে প্রবেশই করে না ফাইল-লেভেল প্যাটার্ন চেক করার জন্য। এই ক্ষেত্রে ডিরেক্টরি-লেভেল প্যাটার্ন এড়িয়ে ফাইল-লেভেল প্যাটার্ন ব্যবহার করতে হয় (logs/* এবং তারপর !logs/keep.log)।
রিয়েল-লাইফ উদাহরণ
একটা Python প্রজেক্টে সাধারণ .gitignore:
__pycache__/
*.pyc
.venv/
.env
*.egg-info/
dist/
.pytest_cache/
.env ইগনোর করা বিশেষভাবে গুরুত্বপূর্ণ — Module 33-তে দেখা যাবে এই ফাইল ভুলে কমিট হয়ে গেলে কী মারাত্মক পরিণতি হতে পারে (secret leak)।
Global .gitignore ও `git check-ignore -v` ডিবাগিং
Global .gitignore
প্রতিটা প্রজেক্টের নিজস্ব .gitignore-এ OS/editor-specific ফাইল (.DS_Store, .idea/, *.swp) বারবার যোগ করার বদলে একটা global ignore ফাইল সেট করা যায়:
git config --global core.excludesfile ~/.gitignore_global
~/.gitignore_global:
.DS_Store
.idea/
*.swp
.vscode/settings.json
এটা সব repository-তে প্রযোজ্য, কিন্তু প্রজেক্ট-স্পেসিফিক .gitignore (যেটা repo-তে কমিট হয়ে টিমের সবার সাথে শেয়ার হয়) থেকে আলাদা রাখা ভালো — global-এ শুধু ব্যক্তিগত টুল/OS artifact রাখা উচিত, প্রজেক্ট-নির্দিষ্ট জিনিস (node_modules/, dist/) প্রজেক্টের .gitignore-এই রাখা উচিত যাতে পুরো টিম উপকৃত হয়।
git check-ignore -v — কেন একটা ফাইল ইগনোর হচ্ছে
একটা সাধারণ বিভ্রান্তি: "আমি .gitignore-এ এন্ট্রি দিলাম, কিন্তু ফাইলটা তবু ট্র্যাক হচ্ছে/হচ্ছে না কেন?" — check-ignore -v ঠিক কোন লাইন, কোন ফাইল থেকে match হচ্ছে তা দেখায়:
$ git check-ignore -v config/secrets.env
.gitignore:12:*.env config/secrets.env
আউটপুট ফরম্যাট: <source-file>:<line-number>:<pattern> <matched-file>। এটা দেখায় ঠিক কোন .gitignore (root, global, নাকি nested ফোল্ডারের) আর কোন লাইন responsible।
যদি কোনো আউটপুট না আসে
তার মানে ফাইলটা কোনো ignore প্যাটার্নে match করছে না — যদি তবুও git status-এ না দেখায়, সম্ভবত ফাইলটা ইতিমধ্যে ট্র্যাকড (একবার git add হয়ে গেলে .gitignore তার উপর প্রভাব ফেলে না, git rm --cached লাগবে)।
ইতিমধ্যে ট্র্যাকড ফাইল ইগনোর করা
git rm --cached config/secrets.env
echo "config/secrets.env" >> .gitignore
git commit -m "Stop tracking secrets.env"
এটা ফাইলটা working directory থেকে মোছে না, শুধু Git-এর ট্র্যাকিং থেকে সরায় — কিন্তু পুরনো commit history-তে এটা এখনো থেকে যায় (Module 33-এ history rewrite প্রসঙ্গ)।
.gitattributes: text=auto ও EOL Normalization
সমস্যাটা কী
Windows লাইন-এন্ড করে \r\n (CRLF) দিয়ে, Linux/macOS করে \n (LF) দিয়ে। একটা মিশ্র-প্ল্যাটফর্ম টিমে কেউ Windows-এ এডিট করলে পুরো ফাইলের line-ending বদলে গিয়ে diff-এ শত শত "পরিবর্তিত" লাইন দেখাতে পারে, যদিও আসল কোড অপরিবর্তিত।
text=auto সমাধান
# .gitattributes
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
text=auto বলে দেয়: Git নিজে বুঝে নেবে ফাইলটা টেক্সট কিনা, আর repository-তে (internal storage) সবসময় LF দিয়ে normalize করে রাখবে, কিন্তু checkout করার সময় platform-এর native line-ending-এ রূপান্তর করবে (core.autocrlf সেটিং অনুযায়ী)।
নির্দিষ্ট ফাইল টাইপের জন্য জোরপূর্বক eol নির্ধারণ করা যায় — শেল স্ক্রিপ্ট সবসময় LF (Windows-এ CRLF শেল স্ক্রিপ্ট ভেঙে দেয়), Windows batch ফাইল সবসময় CRLF।
binary মার্কার
*.png binary আসলে shortcut for -text -diff — বলে দেয় এই ফাইলে line-ending normalization বা text diff প্রযোজ্য না, পুরোটাই raw বাইনারি হিসেবে ট্রিট করা হবে।
রিয়েল-লাইফ উদাহরণ
একটা টিমে কিছু ডেভেলপার Windows, কিছু macOS ব্যবহার করেন। .gitattributes-এ * text=auto না থাকলে, Windows ডেভেলপার একটা ফাইল সেভ করার পরপরই পুরো ফাইলের CRLF-এ রূপান্তরিত diff দেখাতে পারে PR-এ — রিভিউয়ার আসল পরিবর্তন খুঁজে পাবেন না শত শত noise লাইনের মধ্যে। .gitattributes দিয়ে এটা root-এই প্রতিরোধ করা যায়, প্রতিটা ডেভেলপারের ব্যক্তিগত core.autocrlf সেটিং-এর উপর নির্ভর না করে (কারণ ব্যক্তিগত সেটিং ভুলে ভিন্ন হতে পারে, .gitattributes repo-তে কমিট হয়ে সবার জন্য একই নিয়ম প্রয়োগ করে)।
Custom Diff/Merge Driver, export-ignore, Linguist Hints
Custom Diff Driver
কিছু ফাইল টাইপে ডিফল্ট line-based diff অর্থহীন — যেমন Jupyter notebook (.ipynb, JSON-ভিত্তিক) বা Word ফাইল। কাস্টম driver রেজিস্টার করা যায়:
# .gitattributes
*.ipynb diff=ipynb
# .git/config বা global config
[diff "ipynb"]
textconv = jupyter nbconvert --to script --stdout
এখন git diff একটা .ipynb ফাইলে raw JSON না দেখিয়ে, প্রথমে সেটাকে প্লেইন স্ক্রিপ্টে রূপান্তর করে তারপর diff দেখাবে — মানুষের পড়ার উপযোগী।
Custom Merge Driver
Lock ফাইল বা generated ফাইলে (যেমন package-lock.json) স্বয়ংক্রিয় merge policy সেট করা যায় — যেমন সবসময় "ours" বা "theirs" নেওয়া, বা কাস্টম স্ক্রিপ্ট চালানো:
*.generated.go merge=ours
git config merge.ours.driver true
export-ignore — Archive-এ ফাইল বাদ দেওয়া
git archive (রিলিজ tarball বানানোর কমান্ড) দিয়ে যখন সোর্স প্যাকেজ করা হয়, ডেভেলপমেন্ট-অনলি ফাইল (টেস্ট, CI config) বাদ দেওয়া যায়:
# .gitattributes
.github/ export-ignore
tests/ export-ignore
*.test.js export-ignore
git archive --format=tar.gz -o release.tar.gz HEAD
# উপরের প্যাটার্নে ম্যাচ হওয়া ফাইল tar-এ অন্তর্ভুক্ত হবে না
Linguist Hints — GitHub ভাষা ডিটেকশন
GitHub repository-র "Languages" bar (কোন ভাষায় কত শতাংশ কোড) Linguist লাইব্রেরি দিয়ে হিসাব হয়, যেটা .gitattributes hint সম্মান করে:
vendor/* linguist-vendored
docs/*.md linguist-documentation
generated/* linguist-generated
*.min.js linguist-generated=true
এতে vendor করা থার্ড-পার্টি কোড বা auto-generated ফাইল ভাষার পরিসংখ্যানে ভুলভাবে গণনা না হয় — একটা প্রজেক্ট মূলত Python-এ লেখা হলেও bundled JS library-র কারণে "70% JavaScript" দেখানোর ভুল এড়ানো যায়।
`git check-attr` — Attribute ডিবাগিং
সমস্যাটা কী
.gitattributes-এ একাধিক লাইন, একাধিক ফোল্ডারে nested .gitattributes ফাইল থাকতে পারে (ঠিক .gitignore-এর মতো, প্রতিটা সাবডিরেক্টরিতে আলাদা attribute ওভাররাইড সম্ভব) — একটা নির্দিষ্ট ফাইলের জন্য শেষমেশ কোন attribute effective সেটা বোঝা কঠিন হতে পারে।
git check-attr সমাধান
git check-attr -a path/to/file.ipynb
# path/to/file.ipynb: diff: ipynb
# path/to/file.ipynb: text: unset
git check-attr text eol diff -- src/script.sh
# src/script.sh: text: set
# src/script.sh: eol: lf
# src/script.sh: diff: unset
-a ফ্ল্যাগ দিয়ে সব attribute একসাথে দেখা যায়, নাহলে নির্দিষ্ট attribute নাম দিয়ে জিজ্ঞেস করা যায়।
রিয়েল-লাইফ ব্যবহার
একটা CI pipeline ফেইল করছে বলছে "line ending mismatch" — ডিবাগ করতে:
git check-attr text eol -- problematic-file.sh
এই কমান্ড অবিলম্বে দেখাবে কোন eol rule প্রযোজ্য হচ্ছে (বা কোনোটাই না), যা root cause খুঁজে বের করতে সাহায্য করে — হয়তো .gitattributes-এ pattern ভুল লেখা হয়েছে বা কোনো nested .gitattributes override করে দিয়েছে।
--cached ফ্ল্যাগ
git check-attr --cached -a file.txt
Index-এ (staged অবস্থায়) থাকা attribute দেখতে চাইলে — working directory-র বর্তমান .gitattributes না, বরং staged commit-এর সময়কার attribute যাচাই করতে কাজে লাগে।
ল্যাব: CRLF সমস্যা রিপ্রোডিউস ও ফিক্স
লক্ষ্য
.gitattributes ছাড়া CRLF/LF সমস্যা তৈরি করে, তারপর ঠিক করে সমস্যাটা হাতে-কলমে বোঝা।
ধাপ
- একটা টেস্ট repo বানান,
core.autocrlfবন্ধ রেখে:
mkdir /tmp/eol-lab && cd /tmp/eol-lab && git init
git config core.autocrlf false
printf "line1\nline2\nline3\n" > script.sh
git add script.sh && git commit -m "init with LF"
- ফাইলটা CRLF-এ রূপান্তর করে "অন্য প্ল্যাটফর্মের এডিট" সিমুলেট করুন:
sed -i 's/$/\r/' script.sh
git diff --stat # লক্ষ্য করুন পুরো ফাইল "পরিবর্তিত" দেখাচ্ছে যদিও কনটেন্ট একই
- এই পরিবর্তন revert করুন এবং
.gitattributesযোগ করে সমাধান করুন:
git checkout -- script.sh
echo "*.sh text eol=lf" > .gitattributes
git add .gitattributes && git commit -m "Enforce LF for shell scripts"
- আবার CRLF রূপান্তর করে দেখুন এবার কী হয়:
sed -i 's/$/\r/' script.sh
git check-attr eol -- script.sh # eol: lf দেখাবে
git diff script.sh # Git normalize করে LF হিসেবে ট্রিট করবে commit-এর সময়
- যাচাই করুন
git check-ignore -vকীভাবে কাজ করে:
echo "*.log" >> .gitignore
touch debug.log
git check-ignore -v debug.log
চ্যালেঞ্জ
একটা .gitattributes লাইন যোগ করুন যেটা *.png binary মার্ক করে, তারপর git check-attr -a দিয়ে যাচাই করুন diff ও text attribute কী মান নিচ্ছে।
ইন্টারভিউ প্রশ্ন — Module 29
প্র.১ — .gitignore-এ একটা ইতিমধ্যে ট্র্যাকড ফাইলের এন্ট্রি যোগ করলে সেই ফাইল কেন ইগনোর হয় না?
উত্তর: .gitignore শুধু আনট্র্যাকড ফাইলের জন্য কাজ করে। একবার git add হয়ে গেলে Git সেই ফাইলকে ট্র্যাক করে ফেলেছে — .gitignore-এ যোগ করলেও প্রভাব পড়ে না। git rm --cached <file> দিয়ে ট্র্যাকিং বন্ধ করতে হবে।
প্র.২ — একটা ডিরেক্টরি সম্পূর্ণ ইগনোর করা থাকলে (logs/), তার ভেতরের একটা নির্দিষ্ট ফাইল negation (!logs/keep.log) দিয়ে কেন un-ignore করা যায় না?
উত্তর: ডিরেক্টরি সম্পূর্ণ ইগনোর হলে Git সেই ডিরেক্টরির ভেতরে ফাইল-লেভেল প্যাটার্ন চেক করার জন্য প্রবেশই করে না। সমাধান: ডিরেক্টরি-লেভেল প্যাটার্নের বদলে logs/* ব্যবহার করে তারপর নির্দিষ্ট ফাইল negate করা।
প্র.৩ — text=auto কী করে এবং কেন এটা মিশ্র-প্ল্যাটফর্ম টিমে গুরুত্বপূর্ণ?
উত্তর: এটা Git-কে বলে repository-র internal storage-এ সবসময় LF দিয়ে normalize রাখতে, কিন্তু checkout-এর সময় platform-নেটিভ line-ending ব্যবহার করতে। এটা না থাকলে Windows/Linux/macOS ডেভেলপারদের মধ্যে সেভ করা ফাইলে পুরো-ফাইল "পরিবর্তিত" দেখানোর মতো noise তৈরি হয়।
প্র.৪ — export-ignore কীসের জন্য ব্যবহার করা হয়?
উত্তর: git archive দিয়ে রিলিজ প্যাকেজ বানানোর সময় নির্দিষ্ট ফাইল/ফোল্ডার (যেমন টেস্ট, CI config) বাদ দেওয়ার জন্য — যাতে ডিস্ট্রিবিউটেড প্যাকেজে শুধু প্রোডাকশন-প্রাসঙ্গিক ফাইল থাকে।
প্র.৫ — GitHub-এর "Languages" পরিসংখ্যান ভুল দেখাচ্ছে (bundled third-party JS-কে মূল ভাষা হিসেবে গণনা করছে) — কীভাবে ঠিক করবেন?
উত্তর: .gitattributes-এ linguist-vendored বা linguist-generated hint যোগ করে সংশ্লিষ্ট ফোল্ডার/ফাইল প্যাটার্ন মার্ক করতে হবে — Linguist তখন সেগুলো ভাষা-পরিসংখ্যান থেকে বাদ দেবে।
প্র.৬ — git check-ignore -v আর git check-attr — এই দুটো ডিবাগ টুলের মধ্যে কাজের পার্থক্য কী?
উত্তর: check-ignore -v বলে দেয় একটা ফাইল কেন ইগনোর হচ্ছে (কোন .gitignore ফাইল, কোন লাইন, কোন প্যাটার্ন)। check-attr বলে দেয় একটা ফাইলের জন্য কোন .gitattributes attribute (text, eol, diff, merge, ইত্যাদি) effective — সম্পূর্ণ ভিন্ন দুইটা কনসার্ন, একটা "ট্র্যাকিং হবে কিনা", আরেকটা "ট্র্যাকড ফাইলের সাথে কীভাবে আচরণ হবে"।
কর্নার কেস — Module 29
Nested
.gitignore/.gitattributesফাইল সাবডিরেক্টরিতে root-এর নিয়ম override করতে পারে — একটা সাবফোল্ডারের নিজস্ব.gitattributesথাকলে সেখানকার ফাইলের জন্য সেই লোকাল ফাইলই প্রাধান্য পায়, root-এরটা না;git check-attrদিয়ে যাচাই না করলে এটা বিভ্রান্তিকর হতে পারে।core.autocrlf=trueআর.gitattributes-এরtext=autoএকসাথে থাকলে conflicting আচরণ হতে পারে — সাধারণত.gitattributes-ভিত্তিক নিয়ন্ত্রণ বেশি নির্ভরযোগ্য (কারণ এটা repo-তে কমিট হয়ে সবার জন্য একই), ব্যক্তিগতcore.autocrlfসেটিং-এর উপর নির্ভর করা উচিত না টিম প্রজেক্টে।.gitignoreপ্যাটার্নে অনিচ্ছাকৃতভাবে খুব ব্যাপক match হয়ে যাওয়া — যেমনconfig(স্ল্যাশ ছাড়া) লিখলে এটা যেকোনো গভীরতারconfigনামের ফাইল বা ফোল্ডার — দুটোই match করবে, হয়তোsrc/utils/config.py-ও ভুলবশত ইগনোর হয়ে যেতে পারে যদি নাম মিলে যায়।Binary ফাইলে
text=autoভুলবশত প্রয়োগ হলে ফাইল করাপ্ট হয়ে যেতে পারে — Git যদি একটা বাইনারি ফাইলকে ভুল করে টেক্সট মনে করে (heuristic ভুল করলে) এবং line-ending normalize করার চেষ্টা করে, ফাইলের বাইনারি ডেটা নষ্ট হয়ে যেতে পারে। তাই বাইনারি এক্সটেনশনের জন্য স্পষ্টভাবেbinaryমার্ক করা নিরাপদ, heuristic-এর উপর পুরোপুরি নির্ভর না করা।
MODULE 30: Hooks
Git hooks হলো নির্দিষ্ট git অপারেশনের ঠিক আগে বা পরে স্বয়ংক্রিয়ভাবে চলা স্ক্রিপ্ট — কমিট, পুশ, চেকআউট, মার্জ-এর মতো ঘটনার সাথে যুক্ত। কভার হবে: ক্লায়েন্ট-সাইড হুক (pre-commit থেকে post-merge পর্যন্ত), সার্ভার-সাইড হুক সংক্ষেপে, core.hooksPath দিয়ে টিমে হুক শেয়ার করা, আর husky/pre-commit ফ্রেমওয়ার্কের ভূমিকা।
ক্লায়েন্ট-সাইড হুক: ট্রিগার-পয়েন্ট ও ব্যবহার
Hook কী
প্রতিটা Git repository-র .git/hooks/ ফোল্ডারে কিছু স্যাম্পল স্ক্রিপ্ট থাকে (.sample এক্সটেনশনসহ, নিষ্ক্রিয়) — এক্সটেনশন বাদ দিয়ে executable বানালেই সেগুলো নির্দিষ্ট মুহূর্তে স্বয়ংক্রিয়ভাবে চলে।
ক্লায়েন্ট-সাইড হুকের তালিকা ও ট্রিগার-পয়েন্ট
| Hook | কখন চলে | টিপিক্যাল ব্যবহার |
|---|---|---|
pre-commit |
git commit চালানোর সাথে সাথে, মেসেজ লেখার আগে |
Lint, format check, বড় ফাইল ব্লক করা |
prepare-commit-msg |
commit message editor খোলার আগে | ডিফল্ট মেসেজ টেমপ্লেট বসানো (যেমন ticket ID auto-prefix) |
commit-msg |
মেসেজ লেখা শেষে, commit ফাইনালাইজ হওয়ার আগে | মেসেজ ফরম্যাট যাচাই (Conventional Commits নিয়ম) |
post-commit |
কমিট সম্পূর্ণ হওয়ার পর | নোটিফিকেশন পাঠানো, লোকাল লগ আপডেট |
pre-push |
git push চালানোর সাথে সাথে, ডেটা সার্ভারে যাওয়ার আগে |
টেস্ট চালানো, protected branch-এ সরাসরি push ব্লক করা |
pre-rebase |
git rebase শুরু হওয়ার আগে |
নির্দিষ্ট branch rebase করা থেকে বিরত রাখা |
post-checkout |
git checkout/switch সম্পূর্ণ হওয়ার পর |
dependency ইনস্টল রিমাইন্ডার, submodule sync |
post-merge |
git merge সম্পূর্ণ হওয়ার পর |
dependency ফাইল বদলেছে কিনা চেক করে auto npm install |
রিয়েল-লাইফ উদাহরণ
commit-msg হুক দিয়ে টিমে বাধ্যতামূলক করা যায় প্রতিটা commit message Conventional Commits ফরম্যাট মেনে চলবে (feat:, fix:, chore: প্রিফিক্স) — এটা পরবর্তীতে স্বয়ংক্রিয় changelog generation বা semantic versioning-এর ভিত্তি হয়।
#!/bin/sh
# .git/hooks/commit-msg
commit_msg=$(cat "$1")
if ! echo "$commit_msg" | grep -qE "^(feat|fix|chore|docs|refactor|test)(\(.+\))?: "; then
echo "Error: Commit message must start with feat:/fix:/chore:/docs:/refactor:/test:"
exit 1
fi
Exit code নন-জিরো হলে সংশ্লিষ্ট Git অপারেশন বাতিল হয়ে যায় — এটাই hook-কে একটা "gate" বানায়।
সার্ভার-সাইড হুক: pre-receive, update, post-receive
ক্লায়েন্ট-সাইড বনাম সার্ভার-সাইড
ক্লায়েন্ট-সাইড হুক ডেভেলপারের নিজের মেশিনে চলে — সহজে বাইপাস করা যায় (--no-verify, বা হুক-ই না থাকা)। সার্ভার-সাইড হুক চলে Git সার্ভারে (যেমন self-hosted GitLab/Gitea/bare repo সার্ভার), যেখানে push আসার পর — এগুলো বাইপাস করা কঠিন, কারণ ডেভেলপারের মেশিনের নিয়ন্ত্রণে নেই।
তিনটা মূল সার্ভার-সাইড হুক
| Hook | কখন চলে | ব্যবহার |
|---|---|---|
pre-receive |
push আসার পর, কোনো ref আপডেট হওয়ার আগে | পুরো push batch validate করা (সব কমিট, সব ref) — নন-জিরো exit করলে পুরো push প্রত্যাখ্যাত |
update |
প্রতিটা ref-এর জন্য আলাদাভাবে চলে | নির্দিষ্ট branch-এ ভিন্ন নিয়ম (যেমন main-এ force-push ব্লক, feature/*-এ অনুমতি) |
post-receive |
push সম্পূর্ণ গৃহীত হওয়ার পর | CI ট্রিগার, deployment, Slack নোটিফিকেশন |
রিয়েল-লাইফ উদাহরণ
একটা self-hosted Git সার্ভারে pre-receive হুক দিয়ে নিশ্চিত করা যায় কেউ যেন সরাসরি main branch-এ push করতে না পারে (branch protection যদি platform-নেটিভ ফিচার হিসেবে না থাকে), অথবা প্রতিটা commit-এ GPG signature আছে কিনা যাচাই করা যায় (Module 31-এর সাথে সংযোগ)।
GitHub/GitLab-এ সার্ভার-সাইড হুক
GitHub.com-এ সরাসরি সার্ভার-সাইড হুক স্ক্রিপ্ট লেখার সুযোগ নেই (এটা managed platform) — এর বদলে branch protection rules, required status checks, ও webhook/GitHub Actions দিয়ে একই উদ্দেশ্য অর্জন করা হয়। সার্ভার-সাইড হুক মূলত self-hosted Git সার্ভার (bare repo, GitLab self-managed, Gitea) প্রসঙ্গে প্রাসঙ্গিক।
`core.hooksPath` — কাস্টম হুক ডিরেক্টরি ও টিমে শেয়ারিং
সমস্যাটা কী
.git/hooks/ ফোল্ডার কমিট হয় না — এটা .git-এর ভেতরে, যেটা ক্লোন করার সময় কপি হয় না (প্রতিটা ক্লোনে শুধু .sample ফাইলগুলো ডিফল্টভাবে থাকে)। ফলে একটা টিমের সবাইকে একই hook ব্যবহার করাতে চাইলে, প্রতিটা ডেভেলপারকে ম্যানুয়ালি হুক স্ক্রিপ্ট কপি করে executable করতে হতো — ভুলে যাওয়া সহজ।
core.hooksPath সমাধান
git config core.hooksPath .githooks
এটা Git-কে বলে দেয় .git/hooks/-এর বদলে repository-র ভেতরের .githooks/ ফোল্ডার (যেটা normal ফাইলের মতো কমিট করা যায়) থেকে হুক খুঁজতে:
.githooks/
├── pre-commit
├── commit-msg
└── pre-push
chmod +x .githooks/*
git add .githooks/
git commit -m "Add shared team hooks"
এখন যে কেউ repository ক্লোন করার পর শুধু একবার git config core.hooksPath .githooks চালালেই (বা একটা setup script দিয়ে স্বয়ংক্রিয় করলে) পুরো টিম একই hook ব্যবহার করবে।
Onboarding স্বয়ংক্রিয় করা
অনেক প্রজেক্ট একটা setup.sh বা package.json-এর postinstall স্ক্রিপ্টে এই কনফিগারেশন লাইন যোগ করে রাখে, যাতে নতুন ডেভেলপার npm install বা make setup চালালেই hooks স্বয়ংক্রিয়ভাবে সক্রিয় হয়ে যায় — কাউকে ম্যানুয়ালি মনে রাখতে হয় না।
সতর্কতা
core.hooksPath লোকাল config-এ সেট হয় (.git/config), এটা নিজে কমিট হয় না — তাই প্রতিটা ডেভেলপারকে অন্তত একবার এই কমান্ড চালাতে হয়, অথবা প্রজেক্টের README/setup script-এ এটা ডকুমেন্ট/অটোমেট করতে হয়।
husky/pre-commit ফ্রেমওয়ার্ক — কেন কাঁচা Hook-এর বদলে
কাঁচা Git Hook-এর সীমাবদ্ধতা
- ভাষা-নির্দিষ্ট নয় — শেল স্ক্রিপ্ট লিখতে হয়, cross-platform (Windows/Linux/macOS) সামঞ্জস্য নিজে সামলাতে হয়।
- একাধিক hook চেইন করা (যেমন একটা
pre-commit-এ lint + format + secret-scan — তিনটা আলাদা টুল) ম্যানুয়ালি অর্কেস্ট্রেট করতে হয়। - Version control আর distribution (নতুন ডেভেলপারের মেশিনে hook সক্রিয় করা) নিজে থেকে হয় না,
core.hooksPathম্যানুয়ালি সেট করতে হয়।
Husky (JavaScript ইকোসিস্টেম)
npm install husky --save-dev
npx husky init
echo "npx lint-staged" > .husky/pre-commit
Husky ভেতরে core.hooksPath ব্যবহার করেই কাজ করে, কিন্তু npm install-এর সময় স্বয়ংক্রিয়ভাবে সেটআপ হয়ে যায় (package.json-এর prepare script দিয়ে) — টিমের কাউকে ম্যানুয়াল কনফিগারেশন মনে রাখতে হয় না। প্রায়ই lint-staged-এর সাথে জোড়া লাগানো হয়, যা শুধু staged ফাইলে lint/format চালায় (পুরো প্রজেক্টে না, তাই দ্রুত)।
pre-commit (Python ইকোসিস্টেম, ভাষা-নিরপেক্ষ)
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 24.1.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: detect-private-key
pip install pre-commit
pre-commit install
এই ফ্রেমওয়ার্ক ভাষা-নিরপেক্ষ — YAML দিয়ে declarative ভাবে hook chain সংজ্ঞায়িত করা যায়, community-maintained hook (secret detection, large-file check ইত্যাদি) সরাসরি ব্যবহার করা যায় নিজে স্ক্রিপ্ট না লিখেই।
কখন কাঁচা Hook যথেষ্ট, কখন ফ্রেমওয়ার্ক দরকার
একটামাত্র সাধারণ চেক (যেমন commit message ফরম্যাট) হলে কাঁচা shell script যথেষ্ট। একাধিক ভাষা/টুল চেইন করতে হলে, বা community hook পুনঃব্যবহার করতে চাইলে, ফ্রেমওয়ার্ক ব্যবহার করাই ভালো — maintenance ও onboarding অনেক সহজ হয়ে যায়।
ল্যাব: বড় ফাইল ব্লক করা pre-commit হুক লেখা
লক্ষ্য
একটা pre-commit hook লিখুন যা ৫MB-এর বেশি সাইজের কোনো ফাইল কমিট হতে দেবে না।
ধাপ
- একটা টেস্ট repo বানান:
mkdir /tmp/hook-lab && cd /tmp/hook-lab && git init
.git/hooks/pre-commitফাইল তৈরি করুন:
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
MAX_SIZE=5242880 # 5MB in bytes
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
for file in $staged_files; do
if [ -f "$file" ]; then
size=$(wc -c < "$file")
if [ "$size" -gt "$MAX_SIZE" ]; then
echo "Error: '$file' is $(($size / 1024 / 1024))MB — exceeds 5MB limit."
echo "Consider using Git LFS for large binary files (see Module 27)."
exit 1
fi
fi
done
exit 0
EOF
chmod +x .git/hooks/pre-commit
- একটা ছোট ফাইল দিয়ে যাচাই করুন commit স্বাভাবিক কাজ করছে:
echo "small file" > small.txt
git add small.txt
git commit -m "add small file" # সফল হবে
- একটা ৬MB ডামি ফাইল বানিয়ে hook যাচাই করুন:
dd if=/dev/zero of=big-file.bin bs=1M count=6
git add big-file.bin
git commit -m "add big file" # ব্লক হবে, hook error দেখাবে
--no-verifyদিয়ে hook বাইপাস করে দেখুন (বুঝুন এটা কতটা সহজে এড়ানো যায় — এই জন্যই ক্লায়েন্ট-সাইড hook একা যথেষ্ট নিরাপত্তা না):
git commit --no-verify -m "bypass hook"
চ্যালেঞ্জ
.githooks/ ফোল্ডারে এই স্ক্রিপ্ট সরিয়ে core.hooksPath সেট করুন যাতে এটা টিম-শেয়ারযোগ্য হয়ে যায়, এবং কমিট করুন যাতে অন্য কেউ ক্লোন করলে (core.hooksPath সেট করার পর) একই সুরক্ষা পায়।
ইন্টারভিউ প্রশ্ন — Module 30
প্র.১ — pre-commit আর commit-msg হুকের মধ্যে পার্থক্য কী?
উত্তর: pre-commit চলে কমিট প্রক্রিয়া শুরুর সাথে সাথে, কমিট মেসেজ লেখার আগে — সাধারণত staged content (lint, format, বড় ফাইল) যাচাই করতে ব্যবহৃত হয়। commit-msg চলে মেসেজ লেখা শেষে, ফাইনালাইজ হওয়ার ঠিক আগে — মেসেজের বিষয়বস্তু/ফরম্যাট যাচাইয়ের জন্য।
প্র.২ — ক্লায়েন্ট-সাইড হুক কেন "নিরাপত্তার গ্যারান্টি" হিসেবে যথেষ্ট না?
উত্তর: ডেভেলপার নিজের মেশিনে হুক থাকা/না-থাকা নিয়ন্ত্রণ করতে পারেন, এবং git commit --no-verify দিয়ে সহজেই বাইপাস করা যায়। সত্যিকারের এনফোর্সমেন্টের জন্য সার্ভার-সাইড হুক বা CI pipeline-এর মতো এমন জায়গায় চেক দরকার যেটা ডেভেলপারের নিয়ন্ত্রণের বাইরে।
প্র.৩ — core.hooksPath কী সমস্যা সমাধান করে?
উত্তর: .git/hooks/ ফোল্ডার কমিট হয় না বলে ডিফল্টভাবে hook টিমের সবার মধ্যে শেয়ার হয় না। core.hooksPath দিয়ে একটা কমিট-যোগ্য ফোল্ডার (যেমন .githooks/) নির্দিষ্ট করা যায়, যেটা repository-র সাথেই ভার্শন-কন্ট্রোল্ড থাকে।
প্র.৪ — pre-receive আর update সার্ভার-সাইড হুকের মধ্যে পার্থক্য কী?
উত্তর: pre-receive পুরো push batch (সব ref একসাথে) নিয়ে একবার চলে, batch-ওয়াইড validation-এর জন্য উপযুক্ত। update প্রতিটা ref-এর জন্য আলাদাভাবে চলে, তাই ref-নির্দিষ্ট নিয়ম (যেমন শুধু main-এ force-push ব্লক) প্রয়োগ করা সহজ।
প্র.৫ — Husky বা pre-commit ফ্রেমওয়ার্ক ব্যবহার করার সুবিধা কাঁচা shell script hook-এর তুলনায় কী?
উত্তর: এগুলো hook chain declarative ভাবে সংজ্ঞায়িত করা, community-maintained চেক পুনঃব্যবহার করা, আর npm install/pre-commit install-এর মাধ্যমে স্বয়ংক্রিয় distribution — এই সুবিধা দেয়, cross-platform সামঞ্জস্য ও maintenance অনেক সহজ করে।
প্র.৬ — একটা post-merge হুকের বাস্তব ব্যবহার কী হতে পারে?
উত্তর: merge-এর পর package.json/requirements.txt বদলেছে কিনা চেক করে স্বয়ংক্রিয়ভাবে npm install/pip install চালানোর রিমাইন্ডার বা অটো-ট্রিগার — যাতে ডেভেলপার ভুলে পুরনো dependency নিয়ে কাজ চালিয়ে না যান।
কর্নার কেস — Module 30
git commit --no-verifyশুধুpre-commitআরcommit-msgহুক বাইপাস করে,post-commit-এর মতো non-blocking hook এখনো চলবে (কারণ সেগুলো এমনিতেই কমিট আটকাতে পারে না, exit code চেক করা হয় না)।Hook স্ক্রিপ্ট executable bit না থাকলে নীরবে skip হয়ে যায় — কোনো error message না দিয়েই। নতুনরা ভাবেন hook "কাজ করছে না", আসলে
chmod +xকরা ভুলে গেছেন।Windows-এ shebang (
#!/bin/sh) লাইন সরাসরি কাজ নাও করতে পারে — Git for Windows-এ Git Bash দিয়ে চললেও, কিছু পরিবেশে (native cmd/PowerShell hook চালানো) শেল স্ক্রিপ্ট hook cross-platform issue তৈরি করতে পারে। এই কারণে Husky-র মতো ফ্রেমওয়ার্ক Node.js-ভিত্তিক ক্রস-প্ল্যাটফর্ম wrapper ব্যবহার করে।core.hooksPathসেট করা থাকলে.git/hooks/-এর ভেতরের hook সম্পূর্ণ ইগনোর হয়ে যায় — কেউ যদি ভুলে দুই জায়গায় hook রাখেন (পুরনো.git/hooks/pre-commitআর নতুন.githooks/pre-commit), শুধুcore.hooksPath-এ নির্দেশিত ফোল্ডারেরটাই চলবে, অন্যটা নীরবে উপেক্ষিত হবে — ডিবাগ করার সময় বিভ্রান্তিকর।git commit --amendবা interactive rebase-এর সময়pre-commit/commit-msgহুক প্রতিটা পুনর্লিখিত কমিটে আবার চলে — যদি hook ধীরগতির (যেমন পুরো টেস্ট স্যুট চালায়), একটা বড় interactive rebase-এ বহুবার একই hook চলে সময় অনেক বেড়ে যেতে পারে; ভারী চেক শুধুpre-push-এ রাখা এই সমস্যা এড়ানোর একটা কৌশল।
PHASE 10: Security ও Repository Hygiene
Git-এর শেষ Phase ঘোরে দুইটা প্রশ্নকে কেন্দ্র করে: "এই কমিটটা সত্যিই কি যে দাবি করছে সেই ব্যক্তি করেছে?" (signing, credentials), আর "যদি ভুল করে কিছু সংবেদনশীল ডেটা কমিট হয়ে যায়, তাহলে কী করব?" (secret removal)। এই দুইটাই এমন বিষয় যা বেশিরভাগ ডেভেলপার শেখেন শুধুমাত্র একটা ইনসিডেন্ট ঘটার পর — এই Phase-এর লক্ষ্য সেই ইনসিডেন্টের আগেই প্রস্তুত থাকা।
তিনটা মডিউল — Signing (GPG/SSH দিয়ে কমিট/ট্যাগ ভেরিফাই করা), Credentials (PAT/SSH/credential helper), আর সিক্রেট রিমুভাল (leaked secret থেকে পুনরুদ্ধার, history rewrite, secret rotation)। শেষ মডিউলে একটা Phase-শেষের checkpoint gate আছে।
MODULE 31: Signing
একটা কমিট বা ট্যাগ সত্যিই যে ব্যক্তি দাবি করছে তার তৈরি কিনা — এই প্রশ্নের উত্তর দেয় cryptographic signing। কভার হবে: GPG signing সেটআপ, -S ফ্ল্যাগ, নতুন SSH-ভিত্তিক signing, git log --show-signature, verify-commit/verify-tag, আর GitHub-এর "Verified" ব্যাজ কীভাবে এসব থেকেই আসে।
GPG Commit/Tag Signing সেটআপ
সমস্যাটা কী
Git commit-এর author/committer ফিল্ড শুধু একটা টেক্সট স্ট্রিং (user.name, user.email) — যা যেকেউ নিজের .gitconfig-এ নিজের ইচ্ছামতো সেট করতে পারে। কেউ চাইলে আরেকজনের নাম-ইমেইল দিয়ে কমিট তৈরি করে impersonate করতে পারে। Cryptographic signing এই সমস্যা সমাধান করে — একটা প্রাইভেট কী দিয়ে কমিট/ট্যাগে ডিজিটাল স্বাক্ষর যোগ করা হয়, যেটা শুধু সংশ্লিষ্ট পাবলিক কী দিয়েই ভেরিফাই করা যায়।
GPG সেটআপ
# GPG কী জেনারেট করা (না থাকলে)
gpg --full-generate-key
# তৈরি কী-র ID খুঁজে বের করা
gpg --list-secret-keys --keyid-format=long
# Git-কে সেই কী ব্যবহার করতে বলা
git config --global user.signingkey <KEY_ID>
# সব কমিট স্বয়ংক্রিয়ভাবে সাইন করা
git config --global commit.gpgsign true
# সব ট্যাগ স্বয়ংক্রিয়ভাবে সাইন করা
git config --global tag.gpgsign true
রিয়েল-লাইফ উদাহরণ
একটা ওপেন-সোর্স প্রজেক্টে মেইনটেইনাররা প্রায়ই GPG signing বাধ্যতামূলক করেন — বিশেষ করে release ট্যাগে (v1.0.0)। এতে ডাউনলোডকারী নিশ্চিত হতে পারে ট্যাগটা প্রকৃত মেইনটেইনার তৈরি করেছেন, কোনো compromised mirror বা supply-chain attack দিয়ে ইনজেক্ট করা কোড না।
কেন এটা user.name/user.email-এর চেয়ে শক্তিশালী
Plain-text author field কেউ জাল করতে পারলেও, প্রাইভেট কী ছাড়া কারো পক্ষে একটা বৈধ signature জাল করা ক্রিপ্টোগ্রাফিকভাবে অসম্ভব (কী compromise না হলে)। এটাই signing-কে trust-এর ভিত্তি বানায়, plain metadata না।
`-S` ফ্ল্যাগ ও `git tag -s`
Per-Commit Signing (-S)
commit.gpgsign true গ্লোবালি সেট না করে, নির্দিষ্ট কমিটে ম্যানুয়ালি সাইন করা যায়:
git commit -S -m "Critical security fix"
Signed Tag (git tag -s)
git tag -s v2.0.0 -m "Release version 2.0.0"
সাধারণ (lightweight) ট্যাগের বিপরীতে, signed tag একটা পূর্ণাঙ্গ tag object তৈরি করে যাতে tagger info, মেসেজ, আর signature — সব সংরক্ষিত থাকে।
কমিট অবজেক্টে signature ঠিক কোথায় থাকে
signed commit-এর raw object দেখলে বোঝা যায়:
$ git cat-file -p HEAD
tree 4b825dc...
parent a1b2c3d...
author Rakib Hasan <rakib@example.com> 1700000000 +0600
committer Rakib Hasan <rakib@example.com> 1700000000 +0600
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE...
-----END PGP SIGNATURE-----
Critical security fix
gpgsig একটা হেডার ফিল্ড হিসেবে commit object-এর ভেতরেই এমবেডেড থাকে — অর্থাৎ signature commit-এর content hash-এর অংশ; কমিটের কোনো কিছু (মেসেজ, tree, parent) সামান্যতম বদলালেও পুরনো signature অবৈধ হয়ে যাবে।
প্রতিটা কমিটে ম্যানুয়ালি -S ভুলে যাওয়ার সমাধান
commit.gpgsign true গ্লোবাল কনফিগে সেট করে রাখলে প্রতিটা কমিটে ফ্ল্যাগ মনে রাখতে হয় না — এটাই বাস্তবে বেশিরভাগ টিমের পছন্দ।
SSH-Based Signing (`gpg.format=ssh`) — GPG-এর বিকল্প
কেন SSH Signing
GPG কী ম্যানেজমেন্ট (key generation, expiry, web-of-trust, keyserver) অনেক ডেভেলপারের কাছে জটিল মনে হয়। যেহেতু বেশিরভাগ ডেভেলপারের কাছে ইতিমধ্যে একটা SSH কী আছে (Module 32-এ দেখা যাবে — GitHub push authentication-এর জন্য), Git ২.৩৪+ ভার্শন থেকে সেই একই SSH কী দিয়ে commit/tag signing করার সুবিধা যোগ হয়েছে।
সেটআপ
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
# সাধারণ push-এর জন্য ব্যবহৃত কী-ই signing-এর জন্যও ব্যবহৃত হবে
git config --global commit.gpgsign true
Allowed Signers ফাইল
GPG-তে keyserver/web-of-trust দিয়ে পাবলিক কী ভেরিফাই হয়, কিন্তু SSH-এ এরকম কোনো কেন্দ্রীয় ব্যবস্থা নেই — তাই লোকালি একটা "allowed signers" ফাইল দরকার হয় ভেরিফিকেশনের জন্য:
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
~/.ssh/allowed_signers ফাইলের ফরম্যাট:
rakib@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
রিয়েল-লাইফ উদাহরণ
একজন ডেভেলপার যিনি ইতিমধ্যে GitHub-এ SSH কী দিয়ে push করেন, GPG শেখার বাড়তি ওভারহেড ছাড়াই — শুধু দুই লাইন কনফিগ দিয়ে সেই একই কী দিয়ে signed commit করতে পারেন। GitHub নিজেও ২০২২ সাল থেকে SSH-signed commit-কে "Verified" হিসেবে দেখায়, যদি সংশ্লিষ্ট পাবলিক কী GitHub প্রোফাইলে "Signing Key" হিসেবে যোগ করা থাকে।
GPG বনাম SSH signing — সংক্ষিপ্ত তুলনা
| দিক | GPG | SSH |
|---|---|---|
| সেটআপ জটিলতা | বেশি (key generation, keyserver) | কম (বিদ্যমান SSH কী পুনর্ব্যবহার) |
| Key distribution | Keyserver / GitHub profile | GitHub profile / allowed_signers ফাইল |
| Web of trust | সমর্থিত | নেই |
ভেরিফিকেশন: `--show-signature`, `verify-commit`, `verify-tag`
একটা কমিটের Signature দেখা
git log --show-signature -1
আউটপুটে দেখা যাবে:
commit a1b2c3d...
gpg: Signature made Mon 22 Aug 2026 10:00:00 AM +06
gpg: Good signature from "Rakib Hasan <rakib@example.com>" [ultimate]
Author: Rakib Hasan <rakib@example.com>
...
নির্দিষ্ট কমিট/ট্যাগ ভেরিফাই করা
git verify-commit HEAD
git verify-tag v2.0.0
Exit code 0 মানে signature বৈধ, নন-জিরো মানে অবৈধ বা কী পাওয়া যায়নি — এটা স্ক্রিপ্ট/CI-তে automate করার জন্য সুবিধাজনক:
if ! git verify-commit HEAD 2>/dev/null; then
echo "Warning: unsigned or invalid commit signature!"
fi
সম্ভাব্য আউটপুট অবস্থা
| আউটপুট | অর্থ |
|---|---|
Good signature |
Signature বৈধ, কী trusted |
Can't check signature: No public key |
Signature আছে কিন্তু ভেরিফায়ারের কাছে সংশ্লিষ্ট পাবলিক কী নেই |
BAD signature |
Commit content পরিবর্তিত হয়েছে signing-এর পর (rebase/amend), বা signature জাল |
কোনো gpg: লাইন নেই |
কমিটটা আদৌ সাইন করা হয়নি |
রিয়েল-লাইফ ব্যবহার — CI-তে বাধ্যতামূলক signing যাচাই
একটা pre-receive সার্ভার-সাইড হুক (Module 30) বা CI step দিয়ে নিশ্চিত করা যায় প্রতিটা incoming কমিট সাইন করা এবং একটা trusted কী দিয়ে — সেনসিটিভ প্রজেক্টে (যেমন ওপেন-সোর্স সিকিউরিটি লাইব্রেরি) এটা সাপ্লাই-চেইন অ্যাটাক প্রতিরোধের একটা স্তর।
GitHub-এর "Verified" ব্যাজ কোথা থেকে আসে
ব্যাজ কীভাবে হিসাব হয়
GitHub একটা commit-এর পাশে সবুজ "Verified" ব্যাজ দেখায় যখন:
- Commit-টা cryptographically signed (GPG অথবা SSH format-এ)।
- সংশ্লিষ্ট পাবলিক কী সেই GitHub অ্যাকাউন্টের প্রোফাইলে যোগ করা আছে (Settings → SSH and GPG keys)।
- Signature সেই পাবলিক কী দিয়ে সফলভাবে ভেরিফাই হয়।
- (GPG-র ক্ষেত্রে) committer email কী-র সাথে যুক্ত ভেরিফায়েড ইমেইলের একটার সাথে মেলে।
গুরুত্বপূর্ণ ভুল ধারণা
"Verified" ব্যাজ মানে এই না যে কমিটের কন্টেন্ট নিরাপদ বা bug-free — এটা শুধু প্রমাণ করে কমিটটা সেই ব্যক্তির প্রাইভেট কী দিয়ে সাইন করা হয়েছে যার নামে দাবি করা হচ্ছে। এটা identity verification, code review না।
"Unverified" কেন দেখাতে পারে যদিও আপনি নিজেই কমিট করেছেন
- GitHub-এর ওয়েব ইন্টারফেস দিয়ে করা কমিট (যেমন সরাসরি ব্রাউজারে ফাইল এডিট) GitHub নিজেই সাইন করে, এটা স্বয়ংক্রিয়ভাবে "Verified" দেখায়।
- লোকাল কমিট signing ছাড়া করলে GitHub-এ "Unverified" লেবেল দেখাবে না (শুধু সাইন করা কমিটেই ব্যাজ দেখানো হয়) — তবে কখনো কখনো email mismatch হলে explicit "Unverified" লেবেলও দেখাতে পারে।
রিয়েল-লাইফ গুরুত্ব
একটা ওপেন-সোর্স প্রজেক্টে maintainer-রা প্রায়ই দাবি করেন সব merge commit signed হতে হবে — এটা প্রমাণ রাখে ঠিক কে merge অনুমোদন করেছে, বিশেষ করে যখন একাধিক maintainer আছে এবং accountability গুরুত্বপূর্ণ (কোনো malicious code ইনজেকশন হলে ট্রেস করা সহজ হয়)।
ল্যাব: SSH দিয়ে Commit Signing সেটআপ ও ভেরিফাই
লক্ষ্য
GPG ছাড়াই বিদ্যমান SSH কী দিয়ে commit signing সেটআপ করে ভেরিফাই করা।
ধাপ
- SSH কী না থাকলে একটা তৈরি করুন (থাকলে skip করুন):
ssh-keygen -t ed25519 -C "test-signing-key" -f ~/.ssh/id_ed25519_test -N ""
- Git-কে SSH signing মোডে সেট করুন:
mkdir -p /tmp/sign-lab && cd /tmp/sign-lab && git init
git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519_test.pub
git config commit.gpgsign true
- Allowed signers ফাইল তৈরি করুন (ভেরিফিকেশনের জন্য):
echo "test@example.com $(cat ~/.ssh/id_ed25519_test.pub)" > /tmp/allowed_signers
git config gpg.ssh.allowedSignersFile /tmp/allowed_signers
git config user.email "test@example.com"
git config user.name "Test Signer"
- একটা signed কমিট করুন:
echo "hello" > file.txt
git add file.txt
git commit -m "First signed commit"
- ভেরিফাই করুন:
git log --show-signature -1
git verify-commit HEAD
echo "exit code: $?" # 0 মানে সফল
- এখন কমিট মেসেজ পরিবর্তন করে (amend) দেখুন signature কী হয়:
git commit --amend -m "Modified message"
git verify-commit HEAD # নতুন signature সহ এখনো valid, কিন্তু এটা নতুন signature, পুরনোটা না
চ্যালেঞ্জ
allowed_signers ফাইল থেকে এন্ট্রি মুছে ফেলুন এবং আবার git verify-commit HEAD চালিয়ে দেখুন "no public key" জাতীয় এরর আসে কিনা — এটা বোঝায় signature ভেরিফিকেশন সবসময় সঠিক allowed_signers তালিকার উপর নির্ভরশীল।
ইন্টারভিউ প্রশ্ন — Module 31
প্র.১ — Git commit-এ signing কেন দরকার, যখন user.name/user.email দিয়েই author info থাকে?
উত্তর: user.name/user.email শুধু প্লেইন টেক্সট, যেকেউ নিজের .gitconfig-এ যেকোনো নাম-ইমেইল বসিয়ে impersonate করতে পারে। Signing একটা cryptographic প্রমাণ যোগ করে যে কমিটটা নির্দিষ্ট প্রাইভেট কী-র মালিক তৈরি করেছেন — জাল করা ক্রিপ্টোগ্রাফিকভাবে অসম্ভব।
প্র.২ — SSH-ভিত্তিক signing (gpg.format=ssh) GPG-এর তুলনায় কী সুবিধা দেয়?
উত্তর: বেশিরভাগ ডেভেলপারের কাছে ইতিমধ্যে GitHub push authentication-এর জন্য SSH কী থাকে — নতুন করে GPG key generation/keyserver/web-of-trust শেখার দরকার নেই, একই কী দিয়ে signing করা যায়, সেটআপ অনেক সহজ।
প্র.৩ — git verify-commit কমান্ডের exit code কীভাবে ব্যবহারিক?
উত্তর: এটা 0 রিটার্ন করে যদি signature বৈধ হয়, নন-জিরো হলে অবৈধ/অনুপস্থিত — CI script বা pre-receive hook-এ এটা conditional check হিসেবে ব্যবহার করে স্বয়ংক্রিয়ভাবে unsigned/invalid কমিট ব্লক করা যায়।
প্র.৪ — GitHub-এর "Verified" ব্যাজ কী প্রমাণ করে, আর কী প্রমাণ করে না? উত্তর: এটা প্রমাণ করে কমিট cryptographically signed এবং সংশ্লিষ্ট পাবলিক কী দাবি করা GitHub অ্যাকাউন্টের সাথে যুক্ত (identity verification)। এটা প্রমাণ করে না যে কমিটের কোড নিরাপদ, বাগ-মুক্ত, বা রিভিউ করা হয়েছে (code quality/security-এর সাথে সম্পর্কহীন)।
প্র.৫ — git commit --amend করার পর signed commit-এর signature-এ কী হয়?
উত্তর: পুরনো signature অবৈধ হয়ে যায় কারণ commit content (message/tree/parent) বদলে যাওয়ায় নতুন commit object তৈরি হয়, নতুন hash — commit.gpgsign true সেট থাকলে amend স্বয়ংক্রিয়ভাবে নতুন করে সাইন করে দেয়, নাহলে unsigned থেকে যাবে।
প্র.৬ — SSH signing ভেরিফাই করার জন্য "allowed signers" ফাইল কেন দরকার, GPG-তে যা লাগে না?
উত্তর: GPG-তে keyserver/web-of-trust দিয়ে কেন্দ্রীয়ভাবে পাবলিক কী distribute/ভেরিফাই করা যায়। SSH-এ এরকম কেন্দ্রীয় pubkey infrastructure নেই, তাই লোকালি (বা GitHub-এর মতো প্ল্যাটফর্মে) কোন ইমেইল কোন পাবলিক কী-র মালিক তা ম্যাপ করে রাখা allowed_signers ফাইলের প্রয়োজন হয়।
কর্নার কেস — Module 31
commit.gpgsign trueসেট করা থাকলে কিন্তু GPG agent/SSH agent চালু না থাকলে প্রতিটা কমিট ব্যর্থ হয়ে যায় পাসফ্রেজ চাওয়ার সময় — বিশেষ করে headless/CI পরিবেশে এটা confusing এরর দেয় ("gpg failed to sign the data"), সমাধান: agent forwarding বাGPG_TTYএনভায়রনমেন্ট ভ্যারিয়েবল সঠিকভাবে সেট করা।Rebase করলে সব কমিটের hash বদলে যায়, ফলে পুরনো signature-ও নতুন করে করতে হয় —
git rebaseস্বয়ংক্রিয়ভাবেcommit.gpgsign trueথাকলে নতুন করে সাইন করে, কিন্তু কনফিগ না থাকলে rebase-করা কমিট unsigned থেকে যায়, টিমের নিয়ম না জানলে অবাক হতে পারেন।কী expire হয়ে গেলে পুরনো signature "Good signature" দেখানো বন্ধ করে দিতে পারে — GPG কী-তে expiry date থাকলে, কী মেয়াদ পার হওয়ার পর পুরনো signed commit ভেরিফাই করলে warning আসতে পারে, যদিও কমিটটা তখন বৈধভাবেই সাইন করা হয়েছিল।
একই ইমেইল একাধিক কী-তে যুক্ত থাকলে ভেরিফিকেশন বিভ্রান্তিকর হতে পারে — GitHub প্রোফাইলে একাধিক signing key যোগ করা থাকলে, কোন কমিট কোন কী দিয়ে সাইন করা তা ট্র্যাক করা কঠিন হতে পারে যদি পুরনো/revoked কী কখনো ব্যবহার হয়ে থাকে।
MODULE 32: Credentials
Git সার্ভারে authenticate করার প্রধান দুইটা পদ্ধতি — HTTPS + PAT (Personal Access Token), আর SSH কী। কভার হবে: কখন কোনটা ব্যবহার করবেন, credential helper (cache, store, OS-specific manager), SSH কী তৈরি ও GitHub-এ যোগ করা, git credential fill/approve/reject প্লাম্বিং লেয়ার, আর ডিস্কে বনাম মেমোরিতে credential ক্যাশ করার নিরাপত্তা ট্রেড-অফ।
PAT বনাম SSH Key — কখন কোনটা
দুইটা পদ্ধতির মূল পার্থক্য
Personal Access Token (PAT) HTTPS প্রোটোকলের মাধ্যমে authenticate করে — অনেকটা পাসওয়ার্ডের মতো ব্যবহৃত হয় (GitHub এখন পুরনো পাসওয়ার্ড-ভিত্তিক auth সম্পূর্ণ বন্ধ করে দিয়েছে, PAT-ই একমাত্র HTTPS বিকল্প)।
SSH Key পাবলিক-প্রাইভেট কী জোড়া ব্যবহার করে — প্রাইভেট কী কখনো নেটওয়ার্কে যায় না, শুধু challenge-response দিয়ে প্রমাণ করা হয় প্রাইভেট কী-র মালিকানা।
তুলনা টেবিল
| দিক | PAT (HTTPS) | SSH Key |
|---|---|---|
| প্রোটোকল | HTTPS | SSH |
| Firewall-friendly | হ্যাঁ (সাধারণত পোর্ট 443 খোলা থাকে, কর্পোরেট নেটওয়ার্কে বাধা কম) | কখনো কখনো পোর্ট 22 ব্লক থাকে |
| Expiry | সহজে সেট করা যায় (৩০/৬০/৯০ দিন), স্বয়ংক্রিয়ভাবে মেয়াদ শেষ | ম্যানুয়ালি রিভোক না করলে অনির্দিষ্টকাল বৈধ |
| Scope নিয়ন্ত্রণ | Fine-grained PAT দিয়ে নির্দিষ্ট repo/permission-এ সীমাবদ্ধ করা যায় | সাধারণত পুরো অ্যাকাউন্টের সব repo-তে প্রযোজ্য |
| একাধিক মেশিনে ব্যবহার | প্রতিটা মেশিনে আলাদা টোকেন সহজে তৈরি/রিভোক করা যায় | প্রতিটা মেশিনের জন্য আলাদা কী-জোড়া তৈরি করা ভালো অভ্যাস |
| CI/CD-তে ব্যবহার | সাধারণ (secret হিসেবে ইনজেক্ট করা সহজ) | সম্ভব, কিন্তু কী ম্যানেজমেন্ট একটু জটিল |
রিয়েল-লাইফ সুপারিশ
দৈনন্দিন লোকাল ডেভেলপমেন্টে SSH কী সাধারণত বেশি সুবিধাজনক — একবার সেটআপ করলে বারবার auth করতে হয় না, expire হয় না। CI/CD pipeline-এ Fine-grained PAT ভালো পছন্দ — নির্দিষ্ট মেয়াদ, নির্দিষ্ট scope, আর সহজে rotate/revoke করা যায় যদি leak হয়ে যায় (Module 33-এর সাথে সরাসরি সংযোগ)।
Credential Helper: cache, store, OS-Specific Manager
সমস্যাটা কী
HTTPS দিয়ে প্রতিবার push/pull করলে ইউজারনেম-পাসওয়ার্ড (PAT) বারবার চাওয়া বিরক্তিকর। Credential helper এই তথ্য মনে রাখার ব্যবস্থা করে, ভিন্ন ভিন্ন নিরাপত্তা-স্তরে।
cache — সাময়িক মেমোরি-ভিত্তিক ক্যাশ
git config --global credential.helper cache
git config --global credential.helper 'cache --timeout=3600'
Credential একটা ব্যাকগ্রাউন্ড ডেমন প্রসেসের মেমোরিতে (ডিস্কে না) নির্দিষ্ট সময়ের জন্য (ডিফল্ট ১৫ মিনিট) রাখা হয়, তারপর স্বয়ংক্রিয়ভাবে মুছে যায়।
store — স্থায়ী প্লেইনটেক্সট ডিস্ক সংরক্ষণ
git config --global credential.helper store
Credential ~/.git-credentials-এ প্লেইনটেক্সট সংরক্ষিত হয়, মেয়াদ ছাড়াই। সুবিধাজনক কিন্তু নিরাপত্তার দৃষ্টিকোণ থেকে ঝুঁকিপূর্ণ — কেউ সেই ফাইল পড়তে পারলে সরাসরি credential পেয়ে যায়।
OS-নেটিভ Credential Manager (সুপারিশকৃত)
# macOS
git config --global credential.helper osxkeychain
# Windows
git config --global credential.helper manager
# Linux (libsecret, GNOME Keyring/KWallet ইন্টিগ্রেশন প্রয়োজন)
git config --global credential.helper libsecret
এগুলো OS-এর নিজস্ব এনক্রিপ্টেড ক্রেডেনশিয়াল স্টোরে সংরক্ষণ করে — store-এর মতো স্থায়ী, কিন্তু প্লেইনটেক্সট না, OS-লেভেল এনক্রিপশন ও access control দিয়ে সুরক্ষিত।
সারাংশ — কোনটা বেছে নেবেন
| Helper | নিরাপত্তা | সুবিধা |
|---|---|---|
cache |
ভালো (মেমোরি, সাময়িক) | মাঝারি (সময় ফুরালে আবার চাইবে) |
store |
দুর্বল (প্লেইনটেক্সট, স্থায়ী) | সর্বোচ্চ |
| OS-native manager | সর্বোচ্চ (encrypted) | সর্বোচ্চ |
বেশিরভাগ আধুনিক পরিস্থিতিতে OS-নেটিভ manager-ই সেরা পছন্দ — নিরাপত্তা ও সুবিধা দুটোই পাওয়া যায়।
SSH কী জেনারেট করা ও GitHub-এ যোগ করা
ধাপে ধাপে SSH কী সেটআপ
# আধুনিক, দ্রুত, নিরাপদ Ed25519 অ্যালগরিদম (পুরনো RSA-এর বদলে সুপারিশকৃত)
ssh-keygen -t ed25519 -C "rakib.hasan@example.com"
# Enter চাপলে ~/.ssh/id_ed25519 (প্রাইভেট) ও ~/.ssh/id_ed25519.pub (পাবলিক) তৈরি হবে
# পাসফ্রেজ সেট করা (ঐচ্ছিক কিন্তু সুপারিশকৃত — extra security layer)
# SSH agent-এ কী যোগ করা (পাসফ্রেজ বারবার না চাওয়ার জন্য)
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# পাবলিক কী কপি করা
cat ~/.ssh/id_ed25519.pub
GitHub-এ যোগ করা
- GitHub → Settings → SSH and GPG keys → New SSH key
- পাবলিক কী (
.pubফাইলের কন্টেন্ট) পেস্ট করা — কখনো প্রাইভেট কী শেয়ার করা যাবে না - যাচাই করা:
ssh -T git@github.com
# Hi rakib-hasan! You've successfully authenticated...
Remote URL SSH ফরম্যাটে থাকতে হবে
git remote set-url origin git@github.com:MonadWizard/blog.git
# HTTPS ফরম্যাট (https://github.com/...) হলে SSH কী কাজে আসবে না, PAT লাগবে
একাধিক GitHub অ্যাকাউন্টের জন্য একাধিক কী
# ~/.ssh/config
Host github.com-personal
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
Host github.com-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
git clone git@github.com-work:company/repo.git
এই কৌশল দিয়ে একই মেশিনে ব্যক্তিগত ও অফিসের GitHub অ্যাকাউন্ট আলাদা কী দিয়ে পরিচালনা করা যায়, includeIf-এর সাথে মিলিয়ে সঠিক user.email-ও স্বয়ংক্রিয়ভাবে প্রযোজ্য হয়।
`git credential fill/approve/reject` — প্লাম্বিং লেয়ার
Credential System-এর ভেতরের স্তর
credential.helper সেটিং যা ঘটায় তার নিচে একটা প্লাম্বিং লেয়ার আছে — git credential কমান্ড, যেটা সরাসরি ব্যবহার করেও দেখা যায় কীভাবে credential resolve হয়।
git credential fill
$ printf "protocol=https\nhost=github.com\n" | git credential fill
protocol=https
host=github.com
username=rakib-hasan
password=ghp_xxxxxxxxxxxxxxxxxxxx
এটা configured helper(গুলো) জিজ্ঞেস করে দেখে কোনো সংরক্ষিত credential আছে কিনা — থাকলে ফেরত দেয়, না থাকলে interactive prompt (বা ব্যর্থতা) হতে পারে।
git credential approve
Push/pull সফল হওয়ার পর Git স্বয়ংক্রিয়ভাবে এটা কল করে যাতে সফল credential সংরক্ষিত হয় (helper অনুযায়ী cache/store/OS-manager-এ):
printf "protocol=https\nhost=github.com\nusername=rakib-hasan\npassword=ghp_xxx\n" | git credential approve
git credential reject
Authentication ব্যর্থ হলে (যেমন টোকেন revoke হয়ে গেছে) Git এটা কল করে যাতে ভুল/পুরনো credential helper থেকে মুছে ফেলা হয় — নাহলে পরের চেষ্টায় একই ভুল credential আবার ব্যবহৃত হতো:
printf "protocol=https\nhost=github.com\nusername=rakib-hasan\n" | git credential reject
কেন এই প্লাম্বিং লেয়ার জানা দরকার
কাস্টম credential helper স্ক্রিপ্ট লেখার সময় (যেমন কোম্পানির নিজস্ব secret vault-এর সাথে ইন্টিগ্রেশন), ঠিক এই তিনটা অপারেশন (get/store/erase — যেগুলো fill/approve/reject দিয়ে টেস্ট করা যায়) বাস্তবায়ন করতে হয়। এটা বোঝা মানে বোঝা যে credential helper আসলে stdin/stdout দিয়ে key=value ফরম্যাটে যোগাযোগ করা একটা সাধারণ external প্রোগ্রাম, কোনো জাদু না।
ডিস্কে বনাম মেমোরিতে ক্যাশ করার নিরাপত্তা ট্রেড-অফ
মূল প্রশ্ন: সুবিধা বনাম ঝুঁকি
Credential যত বেশি সময়/স্থায়ীভাবে সংরক্ষিত থাকে, ব্যবহার তত সুবিধাজনক — কিন্তু compromise হলে ক্ষতিও তত বেশি।
ঝুঁকির স্তর
credential.helper store(প্লেইনটেক্সট ডিস্কে) — সবচেয়ে ঝুঁকিপূর্ণ। যদি মেশিন compromise হয় (malware, চুরি হওয়া ল্যাপটপ, ভুল ফাইল পারমিশন),~/.git-credentialsফাইল সরাসরি পড়ে টোকেন/পাসওয়ার্ড বের করে ফেলা যায় — কোনো এনক্রিপশন নেই।cache(মেমোরি, সাময়িক) — মাঝারি ঝুঁকি। মেমোরি ডাম্প এক্সেস করা আক্রমণকারীর জন্য কঠিন (root/admin access লাগে সাধারণত), আর নির্দিষ্ট সময় পর স্বয়ংক্রিয়ভাবে মুছে যায় — exposure window সীমিত।- OS-নেটিভ manager (Keychain/Credential Manager/libsecret) — সবচেয়ে নিরাপদ ব্যবহারযোগ্য বিকল্প। ডেটা এনক্রিপ্টেড, OS-লেভেল access control (কখনো কখনো biometric/password re-prompt) প্রয়োজন হতে পারে সংবেদনশীল অপারেশনে।
রিয়েল-লাইফ সিদ্ধান্ত নেওয়ার নীতি
- শেয়ার্ড/multi-user মেশিন (যেমন office workstation একাধিক ব্যবহারকারীর) →
storeকখনোই না,cacheবা OS-manager। - ব্যক্তিগত, এনক্রিপ্টেড ডিস্কযুক্ত ল্যাপটপ → OS-নেটিভ manager আদর্শ পছন্দ (ডিস্ক এনক্রিপশন + OS keychain-এর দ্বৈত সুরক্ষা)।
- CI/CD রানার (ephemeral) → প্রতিটা রান শেষে পরিবেশ মুছে যায় বলে সাধারণত secret manager থেকে সরাসরি environment variable-এ ইনজেক্ট করা হয়, credential helper caching relevant না।
একটা গুরুত্বপূর্ণ নীতি
যেকোনো caching mechanism-ই "যদি ডিভাইস compromise হয়" পরিস্থিতির জন্য একটা অতিরিক্ত আক্রমণ-তল (attack surface) তৈরি করে — এই কারণেই PAT-এর জন্য সংক্ষিপ্ত মেয়াদ (৩০-৯০ দিন) এবং সর্বনিম্ন প্রয়োজনীয় scope নির্ধারণ করা caching mechanism যতই নিরাপদ হোক, একটা গুরুত্বপূর্ণ দ্বিতীয় স্তরের প্রতিরক্ষা।
ল্যাব: Credential Helper পরিবর্তন ও প্লাম্বিং টেস্ট
লক্ষ্য
বিভিন্ন credential helper-এর আচরণ পরীক্ষা করা এবং plumbing কমান্ড হাতে-কলমে চালানো।
ধাপ
- বর্তমান helper কনফিগ দেখুন:
git config --get-all credential.helper
- একটা সাময়িক cache helper সেট করুন (৩০ সেকেন্ড টাইমআউট, টেস্টের জন্য ছোট রাখা হলো):
git config --global credential.helper 'cache --timeout=30'
- plumbing দিয়ে ম্যানুয়ালি একটা ফেক credential approve করুন:
printf "protocol=https\nhost=example-test.com\nusername=testuser\npassword=testpass123\n" | git credential approve
- সাথে সাথে
fillদিয়ে যাচাই করুন এটা ফেরত আসছে কিনা:
printf "protocol=https\nhost=example-test.com\n" | git credential fill
# username=testuser, password=testpass123 দেখাবে
- ৩০ সেকেন্ড অপেক্ষা করে আবার
fillচালান — cache টাইমআউট হয়ে গেছে কিনা দেখুন:
sleep 31
printf "protocol=https\nhost=example-test.com\n" | git credential fill
# এবার কিছু ফেরত আসবে না, অথবা ইন্টারেক্টিভ প্রম্পট দেখাবে
rejectদিয়ে ম্যানুয়ালি মুছে ফেলা টেস্ট করুন:
printf "protocol=https\nhost=example-test.com\nusername=testuser\n" | git credential reject
চ্যালেঞ্জ
আপনার OS অনুযায়ী নেটিভ credential manager (osxkeychain/manager/libsecret) সেট করে দেখুন store-এর তুলনায় কী পার্থক্য অনুভব করেন — এবং ~/.git-credentials ফাইল (যদি আগে store ব্যবহার করে থাকেন) খুলে দেখুন সেখানে প্লেইনটেক্সটে কী আছে।
ইন্টারভিউ প্রশ্ন — Module 32
প্র.১ — PAT আর SSH কী-র মধ্যে দৈনন্দিন লোকাল ডেভেলপমেন্টে কোনটা সাধারণত বেশি সুবিধাজনক, এবং কেন? উত্তর: SSH কী — একবার সেটআপ করলে expire হয় না, বারবার প্রম্পট আসে না, আর প্রাইভেট কী কখনো নেটওয়ার্কে যায় না (challenge-response মেকানিজম)। PAT-এর মেয়াদ থাকে বলে নিয়মিত রিফ্রেশ করতে হতে পারে।
প্র.২ — credential.helper store কেন নিরাপত্তার দৃষ্টিকোণ থেকে ঝুঁকিপূর্ণ?
উত্তর: এটা credential ~/.git-credentials-এ প্লেইনটেক্সটে, মেয়াদ ছাড়াই সংরক্ষণ করে। মেশিন compromise হলে বা ফাইল পারমিশন ভুল হলে সরাসরি টোকেন/পাসওয়ার্ড পড়ে ফেলা যায়, কোনো এনক্রিপশন স্তর নেই।
প্র.৩ — git credential approve আর git credential reject-এর কাজ কী?
উত্তর: approve সফল authentication-এর পর credential সংশ্লিষ্ট helper-এ সংরক্ষণ করতে বলে (cache/store/OS-manager অনুযায়ী)। reject ব্যর্থ authentication-এর পর পুরনো/ভুল credential মুছে ফেলতে বলে, যাতে পরের চেষ্টায় একই ভুল তথ্য পুনরায় ব্যবহৃত না হয়।
প্র.৪ — একটা শেয়ার্ড, একাধিক-ব্যবহারকারীর মেশিনে কোন credential helper এড়িয়ে চলা উচিত এবং কেন?
উত্তর: store (প্লেইনটেক্সট, স্থায়ী) এড়ানো উচিত — একাধিক ব্যবহারকারীর অ্যাক্সেস থাকা মেশিনে যেকেউ ফাইল পড়ে ফেলতে পারে। cache (সাময়িক, মেমোরি) বা OS-নেটিভ encrypted manager বেশি নিরাপদ।
প্র.৫ — CI/CD pipeline-এ credential caching মেকানিজম (cache/store) কেন সাধারণত ব্যবহার করা হয় না? উত্তর: CI রানার সাধারণত ephemeral (প্রতিটা রান শেষে পরিবেশ মুছে যায়) — caching-এর কোনো সুবিধা নেই কারণ পরের রানে কিছুই টিকে থাকবে না। এর বদলে সরাসরি secret manager থেকে environment variable-এ credential ইনজেক্ট করা হয়।
প্র.৬ — একই মেশিনে একাধিক GitHub অ্যাকাউন্ট (ব্যক্তিগত ও অফিস) ব্যবহার করার সময় SSH কী কীভাবে ম্যানেজ করবেন?
উত্তর: প্রতিটা অ্যাকাউন্টের জন্য আলাদা SSH কী-জোড়া তৈরি করে ~/.ssh/config-এ আলাদা Host alias (যেমন github.com-personal, github.com-work) সংজ্ঞায়িত করে প্রতিটার জন্য IdentityFile নির্দিষ্ট করতে হয়, আর remote URL-এ সেই alias ব্যবহার করতে হয়।
কর্নার কেস — Module 32
HTTPS remote URL-এ SSH কী কোনো কাজে আসে না —
git remote -vদিয়ে চেক না করে "SSH কী সেট করলাম, তবু পাসওয়ার্ড চাইছে" বলে বিভ্রান্ত হওয়া একটা সাধারণ ভুল। Remote URLhttps://-দিয়ে শুরু হলে PAT/credential helper লাগবে, SSH কী প্রযোজ্য না যতক্ষণ না URLgit@github.com:...ফরম্যাটে বদলানো হয়।GitHub-এ password-based HTTPS authentication বন্ধ, কিন্তু পুরনো cached password এখনো
~/.git-credentials-এ থাকতে পারে — নতুন করে PAT দিয়ে replace না করা পর্যন্ত পুরনো, এখন-অকার্যকর পাসওয়ার্ড ব্যবহার করার চেষ্টা করে বারবার ব্যর্থ হতে পারে,git credential rejectবাstoreফাইল ম্যানুয়ালি এডিট করে পুরনো এন্ট্রি সরাতে হয়।PAT-এর scope অতিরিক্ত ব্যাপক দিলে (যেমন পুরো অ্যাকাউন্টের সব repo, admin scope) একটা leak হলে ক্ষতির পরিধি অনেক বড় হয়ে যায় — সবসময় ন্যূনতম প্রয়োজনীয় scope (fine-grained PAT দিয়ে নির্দিষ্ট repo, নির্দিষ্ট permission) নির্ধারণ করা উচিত।
ssh-addদিয়ে agent-এ কী যোগ করা রিবুট করলে হারিয়ে যায় (persist করে না ডিফল্টে) — প্রতিবার নতুন সেশনে পাসফ্রেজ চাইতে থাকলে বিরক্তিকর মনে হতে পারে; macOS-এ Keychain ইন্টিগ্রেশন (ssh-add --apple-use-keychain) বা Linux-এkeychainটুল দিয়ে এটা persist করানো যায়।
MODULE 33: সিক্রেট রিমুভাল
একটা .env ফাইল বা API key ভুলে কমিট হয়ে push হয়ে গেছে — এটা প্রতিটা ডেভেলপারের ক্যারিয়ারে অন্তত একবার ঘটা একটা দুঃস্বপ্ন। কভার হবে: git filter-repo দিয়ে ইতিহাস থেকে সিক্রেট মুছে ফেলা, BFG Repo-Cleaner বিকল্প, পুরনো git filter-branch কেন এড়ানো উচিত, force-push + team re-clone প্রোটোকল, আর সবচেয়ে গুরুত্বপূর্ণ — কেন secret rotation বাধ্যতামূলক (শুধু history rewrite যথেষ্ট না)। মডিউল শেষে Phase 10-এর checkpoint gate আছে।
লিকড সিক্রেট: বাস্তব দৃশ্য ও কেন এটা ভয়ংকর
দৃশ্যটা
একজন ডেভেলপার লোকালি টেস্ট করার জন্য .env ফাইলে একটা প্রোডাকশন AWS access key বা Stripe API key লিখলেন। .gitignore-এ .env যোগ করতে ভুলে গেলেন (বা ভুলে git add -A চালালেন) — ফাইলটা কমিট হয়ে গেল, push-ও হয়ে গেল GitHub-এ, হয়তো একটা পাবলিক repository-তে।
কেন এটা শুধু "ফাইল মুছে ফেলাই যথেষ্ট" না
git rm .env
git commit -m "Remove .env"
git push
এটা যথেষ্ট না — কারণ পুরনো কমিটে সেই ফাইল এখনো object database-এ পুরোপুরি অক্ষত আছে। যে কেউ:
git log --all --full-history -- .env
git show <old-commit-sha>:.env
চালিয়ে সেই পুরনো কমিট থেকে সিক্রেট বের করে ফেলতে পারবে — এমনকি ফাইল বর্তমান branch-এ না থাকলেও, ইতিহাসে থেকে যাওয়া কমিট থেকে অ্যাক্সেসযোগ্য।
কেন এটা "সাধারণ বাগ"-এর চেয়ে গুরুত্বর
- স্বয়ংক্রিয় স্ক্যানার সব সময় নজর রাখছে — GitHub-এর নিজস্ব secret scanning, আর বহু বট পাবলিক রিপোজিটরি ক্রল করে known API key প্যাটার্ন খোঁজে, মিনিটের মধ্যে exploit হতে পারে।
- ইতিমধ্যে ফর্ক/ক্যাশ হয়ে থাকতে পারে — GitHub-এর নিজস্ব কমিট ক্যাশ, বা কেউ ইতিমধ্যে repository ফর্ক/ক্লোন করে থাকতে পারে যেটার উপর আপনার কোনো নিয়ন্ত্রণ নেই।
- পাবলিক CI log-এও leak হতে পারে — যদি
.envbuild/test-এ ব্যবহৃত হয় এবং কোনো debug output-এ প্রিন্ট হয়ে যায়।
প্রথম পদক্ষেপ (ক্রম গুরুত্বপূর্ণ)
- অবিলম্বে সেই key/token রোটেট (invalidate) করুন — এটাই সবচেয়ে জরুরি পদক্ষেপ, ইতিহাস পরিষ্কার করার আগেই।
- তারপর ইতিহাস থেকে সিক্রেট মুছে ফেলার প্রক্রিয়া শুরু করুন (
git filter-repo, পরের leaf-এ)।
`git filter-repo` — ইতিহাস থেকে সিক্রেট পার্মানেন্টলি মোছা
কেন filter-repo
git filter-repo হলো Git প্রজেক্ট নিজেই সুপারিশ করা আধুনিক টুল history rewrite করার জন্য — পুরনো git filter-branch-এর চেয়ে দ্রুত, নিরাপদ, আর সহজ সিনট্যাক্স।
ইনস্টল
pip install git-filter-repo
# অথবা: brew install git-filter-repo
নির্দিষ্ট ফাইল পুরো ইতিহাস থেকে মোছা
git filter-repo --path .env --invert-paths
--invert-paths মানে "এই পাথ বাদে সব রাখো" — অর্থাৎ .env-কে ইতিহাসের প্রতিটা কমিট থেকে সরিয়ে ফেলা হয়, শুধু বর্তমান commit থেকে না।
নির্দিষ্ট স্ট্রিং/প্যাটার্ন প্রতিস্থাপন করা (ফাইল না মুছে শুধু সিক্রেট ভ্যালু রিডাক্ট করা)
echo "AKIAIOSFODNN7EXAMPLE==>REDACTED" > replacements.txt
git filter-repo --replace-text replacements.txt
এটা ইতিহাসের সব কমিটে সেই নির্দিষ্ট স্ট্রিং খুঁজে REDACTED দিয়ে প্রতিস্থাপন করে দেয় — পুরো ফাইল না মুছেও সংবেদনশীল অংশ সরানো যায়।
গুরুত্বপূর্ণ সতর্কতা
# fresh clone-এ চালানো সবচেয়ে নিরাপদ — কোনো uncommitted কাজ থাকা অবস্থায় চালাবেন না
git clone https://github.com/org/repo.git repo-clean
cd repo-clean
git filter-repo --path .env --invert-paths
filter-repo চালানোর পর প্রতিটা কমিটের SHA বদলে যায় (rewrite করা মানেই নতুন commit object, নতুন hash) — এটা একটা সম্পূর্ণ ইতিহাস পুনর্লিখন, যার জন্য পরের leaf-এ আলোচিত force-push + team re-clone প্রোটোকল বাধ্যতামূলক।
BFG Repo-Cleaner — বিকল্প টুল
BFG কী
BFG Repo-Cleaner হলো git filter-repo-র আরেকটা জনপ্রিয় বিকল্প, বিশেষভাবে ডিজাইন করা দ্রুত ও সহজ সাধারণ ব্যবহারের ক্ষেত্রে (বড় ফাইল বা পাসওয়ার্ড মোছা) — যদিও filter-repo-র মতো নমনীয় নয়, সিনট্যাক্স সহজ হওয়ায় অনেকে এখনো পছন্দ করেন।
ইনস্টল ও ব্যবহার
BFG একটা Java jar ফাইল হিসেবে আসে:
# নির্দিষ্ট নামের ফাইল পুরো ইতিহাস থেকে মোছা
java -jar bfg.jar --delete-files secrets.env repo.git
# নির্দিষ্ট টেক্সট প্যাটার্ন রিপ্লেস করা (একটা ফাইলে প্যাটার্ন তালিকা দিয়ে)
java -jar bfg.jar --replace-text passwords.txt repo.git
# ১MB-এর বেশি বড় ফাইল মোছা (Module 27-এর LFS প্রসঙ্গের সাথে সংযুক্ত)
java -jar bfg.jar --strip-blobs-bigger-than 1M repo.git
BFG সবসময় একটা bare mirror clone-এ কাজ করে:
git clone --mirror https://github.com/org/repo.git
java -jar bfg.jar --delete-files secrets.env repo.git
cd repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
filter-repo বনাম BFG — তুলনা
| দিক | git filter-repo |
BFG Repo-Cleaner |
|---|---|---|
| স্পীড | দ্রুত | দ্রুত (কিছু ক্ষেত্রে filter-repo-র চেয়েও দ্রুত সাধারণ কাজে) |
| নমনীয়তা | অনেক বেশি (পাইথন কলব্যাক, জটিল path/commit ফিল্টারিং) | সীমিত (শুধু ফাইল ডিলিট/টেক্সট রিপ্লেস/সাইজ ফিল্টার) |
| শেখার সহজতা | মাঝারি | সহজ |
| Git প্রজেক্টের অফিসিয়াল সুপারিশ | হ্যাঁ | না, কিন্তু ব্যাপকভাবে ব্যবহৃত ও নির্ভরযোগ্য |
| নির্ভরতা | Python | Java |
রিয়েল-লাইফ সিদ্ধান্ত
সাধারণ কেস (একটা নির্দিষ্ট ফাইল বা একটা নির্দিষ্ট string প্যাটার্ন মুছতে চান) — দুটোই সমানভাবে ভালো, টিমের পরিচিতি অনুযায়ী বেছে নিন। জটিল, কাস্টম rewrite logic দরকার হলে filter-repo-র পাইথন API বেশি নমনীয়তা দেয়।
`git filter-branch` — Deprecated/Legacy, কেন এড়িয়ে চলবেন
filter-branch কী ছিল
Git-এর নিজস্ব বিল্ট-ইন পুরনো history-rewrite টুল — git filter-repo আসার আগে এটাই ছিল স্ট্যান্ডার্ড উপায়:
# পুরনো, এখন অ-সুপারিশকৃত পদ্ধতি — শুধু রেফারেন্সের জন্য
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
কেন Git নিজেই এখন এটা ব্যবহার না করতে বলে
- ভয়ংকর ধীর — বড় repository-তে প্রতিটা কমিট, প্রতিটা ফাইলের জন্য আলাদা শেল প্রসেস স্পন করে, ঘণ্টার পর ঘণ্টা সময় লাগতে পারে।
- সহজেই ভুল করার সুযোগ — জটিল, ত্রুটি-প্রবণ সিনট্যাক্স (
--index-filter,--tree-filterইত্যাদির পার্থক্য বোঝা কঠিন), ভুল ব্যবহারে ইতিহাস আরও নষ্ট হয়ে যেতে পারে। - পুরনো রেফারেন্স নীরবে থেকে যাওয়ার ঝুঁকি — সঠিকভাবে
refs/original/পরিষ্কার না করলে "মুছে ফেলা" সিক্রেট আসলে এখনো reachable থেকে যায়।
Git-এর নিজস্ব ডকুমেন্টেশনের সতর্কবার্তা
Git-এর অফিসিয়াল ম্যানুয়াল পেজেই স্পষ্টভাবে লেখা আছে: "filter-branch has a plethora of pitfalls... git filter-repo is recommended" — অর্থাৎ Git নিজেই তার ব্যবহারকারীদের filter-repo-র দিকে নির্দেশ করছে।
এই মডিউলে filter-branch কেন উল্লেখ করা হলো তাহলে
শুধু ঐতিহাসিক/রেফারেন্স প্রেক্ষাপটে — পুরনো ব্লগ পোস্ট, স্ট্যাক ওভারফ্লো উত্তর, বা লিগ্যাসি ডকুমেন্টেশনে এখনো filter-branch উদাহরণ দেখা যায়। সেগুলো পড়ে বিভ্রান্ত হবেন না — বাস্তবে সবসময় git filter-repo বা BFG ব্যবহার করা উচিত, filter-branch না।
Force-Push + পুরো টিমের Re-clone প্রোটোকল
কেন এই ধাপ অনিবার্য
History rewrite (filter-repo/BFG) করার পর, লোকাল rewritten repository আর remote-এর পুরনো ইতিহাস সম্পূর্ণ ভিন্ন — কমিট SHA সব বদলে গেছে। Remote-এ পাঠাতে force push ছাড়া উপায় নেই, আর তারপর প্রতিটা টিমমেটের লোকাল কপি পুরনো (এখন "বাতিল") ইতিহাসের সাথে সম্পূর্ণ diverged হয়ে যাবে।
ধাপে ধাপে প্রোটোকল
# ১. rewrite সম্পন্ন হওয়ার পর, সব branch/tag force push
git push origin --force --all
git push origin --force --tags
# ২. সব টিমমেটকে অবিলম্বে জানানো — এটা communication-এর দিক থেকে সবচেয়ে গুরুত্বপূর্ণ ধাপ
প্রতিটা টিমমেটের করণীয় (পুরনো লোকাল কপি দিয়ে চালিয়ে যাওয়া বিপজ্জনক)
# পুরনো লোকাল repository সম্পূর্ণ ফেলে দিয়ে ফ্রেশ ক্লোন করাই সবচেয়ে নিরাপদ
mv old-repo old-repo-backup # সতর্কতাবশত ব্যাকআপ, পরে মুছে ফেলা
git clone git@github.com:org/repo.git
# যদি কেউ ফ্রেশ ক্লোন না করে পুরনো কপি চালিয়ে যেতে চান (সুপারিশকৃত না):
git fetch origin
git reset --hard origin/main
# সতর্কতা: uncommitted/unpushed local কাজ থাকলে হারিয়ে যাবে
কেন "শুধু fetch + reset" ঝুঁকিপূর্ণ
পুরনো, বাতিল ইতিহাসে করা কোনো লোকাল কমিট (push হয়নি এমন) থাকলে সেটা নতুন ইতিহাসের সাথে merge/rebase করার চেষ্টা বিপর্যয়কর জট তৈরি করতে পারে — পুরনো (ফাঁস হওয়া সিক্রেটসহ) commit আবার নতুন ইতিহাসে ফিরে আসার ঝুঁকিও থাকে। এই কারণে ফ্রেশ ক্লোন-ই সবচেয়ে নিরাপদ সুপারিশ।
Branch protection ও PR-এর প্রভাব
Force-push করার আগে GitHub-এ branch protection rule (যদি main-এ force-push ব্লক করা থাকে) সাময়িকভাবে শিথিল করতে হতে পারে, আর ওপেন থাকা সব pull request-এর base rewrite হয়ে যাওয়ায় conflict/rebase প্রয়োজন হবে — এটাও টিমকে আগেভাগে জানানো জরুরি।
Secret Rotation — কেন History Rewrite একাই যথেষ্ট না
সবচেয়ে গুরুত্বপূর্ণ নীতি এই মডিউলের
ইতিহাস থেকে সিক্রেট মুছে ফেলা মানেই সেই সিক্রেট আবার নিরাপদ হয়ে যাওয়া না।
কেন
একবার push হয়ে যাওয়া সিক্রেট ইতিমধ্যে অনেক জায়গায় "বেরিয়ে" যেতে পারে, যেগুলোর উপর আপনার কোনো নিয়ন্ত্রণ নেই:
- GitHub-এর নিজস্ব ক্যাশ/সার্চ ইনডেক্স — কিছুক্ষণের জন্য হলেও পুরনো কমিট ক্যাশ থেকে অ্যাক্সেসযোগ্য থাকতে পারে।
- যেকেউ ইতিমধ্যে clone/fork করে থাকতে পারেন — তাদের লোকাল কপিতে পুরনো ইতিহাস, এবং তাই সিক্রেট, অক্ষত থেকে যায়। আপনার force-push তাদের ইতিমধ্যে-ডাউনলোড-করা কপিকে প্রভাবিত করে না।
- স্বয়ংক্রিয় স্ক্যানার/বট ইতিমধ্যে scrape করে থাকতে পারে — পাবলিক রিপোজিটরি ক্রল করা বট (malicious এবং legitimate, দুই ধরনের) push হওয়ার মিনিটের মধ্যে ডেটা সংগ্রহ করে ফেলতে পারে।
- CI log, third-party integration log-এও কপি থেকে যেতে পারে যেগুলো history rewrite স্পর্শ করে না।
একমাত্র নির্ভরযোগ্য সমাধান: Rotation
যে key/token/password leak হয়েছে সেটাকে সংশ্লিষ্ট প্রোভাইডারে (AWS, Stripe, database, ইত্যাদি) গিয়ে সম্পূর্ণ invalidate/revoke করে নতুন একটা ইস্যু করা। এরপরই leak হওয়া পুরনো credential কার্যত মূল্যহীন — সেটা যেখানেই ছড়িয়ে থাকুক না কেন, আর কাজ করবে না।
সঠিক ক্রম (গুরুত্বপূর্ণ)
১. Rotation (সবার আগে, অবিলম্বে) — leaked key invalidate করা
২. History rewrite (filter-repo/BFG) — ভবিষ্যতে clone/browse করা লোকজনের জন্য cleanup
৩. Force-push + team re-clone প্রোটোকল
৪. Root cause analysis — কীভাবে leak হলো, কীভাবে ভবিষ্যতে প্রতিরোধ করা যায় (secret scanning, pre-commit hook)
Rotation-কে "১" নম্বরে রাখা হয়েছে ইচ্ছাকৃতভাবে — এটাই একমাত্র পদক্ষেপ যা প্রকৃতপক্ষে ঝুঁকি নিষ্ক্রিয় করে; বাকি সব cleanup ও hygiene পদক্ষেপ, নিরাপত্তার আসল গ্যারান্টি না।
Secret-Scanning টুল: GitHub Secret Scanning, gitleaks, truffleHog
কেন প্রতিরোধ সবসময় নিরাময়ের চেয়ে ভালো
উপরের সব পদক্ষেপ (rotation, history rewrite) একটা ইনসিডেন্টের পরে করণীয়। এই leaf-এ সংক্ষেপে দেখা যাবে কোন টুলগুলো সিক্রেট কমিট হওয়ার আগে বা সাথে সাথেই ধরে ফেলার চেষ্টা করে।
GitHub Secret Scanning (Push Protection)
GitHub-এ বিল্ট-ইন ফিচার — পাবলিক (এবং GitHub Advanced Security সহ প্রাইভেট) রিপোজিটরিতে push হওয়া কন্টেন্ট স্বয়ংক্রিয়ভাবে স্ক্যান করে পরিচিত API key প্যাটার্ন (AWS, Stripe, Google Cloud, ইত্যাদি শত শত প্রোভাইডার) খুঁজে বের করে। Push protection সক্রিয় থাকলে সিক্রেট-সদৃশ কন্টেন্ট থাকা push-ই ব্লক করে দেয়, কমিট হওয়ার আগেই।
gitleaks
একটা ওপেন-সোর্স, স্ট্যান্ডঅ্যালোন CLI টুল যা regex প্যাটার্ন দিয়ে সিক্রেট ডিটেক্ট করে — লোকাল pre-commit hook (Module 30-এর সাথে সংযোগ) বা CI pipeline-এ ইন্টিগ্রেট করা যায়:
gitleaks detect --source . --verbose
.pre-commit-config.yaml-এ যোগ করে প্রতিটা কমিটের আগে স্বয়ংক্রিয়ভাবে চালানো যায়।
truffleHog
আরেকটা জনপ্রিয় ওপেন-সোর্স টুল, যেটা শুধু বর্তমান কোড না, পুরো Git history স্ক্যান করতে পারে উচ্চ-এনট্রপি স্ট্রিং (যেগুলো key/token হওয়ার সম্ভাবনা বেশি) খুঁজে বের করার জন্য — এমন সিক্রেটও ধরতে পারে যেগুলো বহু আগে কমিট হয়ে এখন আর বর্তমান ফাইলে নেই কিন্তু ইতিহাসে রয়ে গেছে।
রিয়েল-লাইফ স্তরায়িত প্রতিরক্ষা (Defense in Depth)
স্তর ১: pre-commit hook (gitleaks) — ডেভেলপারের মেশিনে, কমিট হওয়ার আগেই ধরা
স্তর ২: GitHub push protection — সার্ভার-সাইড, push হওয়ার সময় ব্লক
স্তর ৩: CI pipeline scan (truffleHog/gitleaks periodic scan) — যদি প্রথম দুই স্তর এড়িয়ে যায়
স্তর ৪: rotation + filter-repo প্রোটোকল — সবকিছু ব্যর্থ হলে ইনসিডেন্ট রেসপন্স
কোনো একক স্তরই নিখুঁত না — এই কারণেই layered approach গুরুত্বপূর্ণ।
ল্যাব: সিক্রেট লিক সিমুলেট করে সম্পূর্ণ Cleanup
লক্ষ্য
একটা সম্পূর্ণ leaked-secret ইনসিডেন্ট সিমুলেট করা: ভুলে কমিট → আবিষ্কার → filter-repo দিয়ে cleanup → যাচাই।
ধাপ
- একটা টেস্ট repo বানান এবং "সিক্রেট" কমিট করুন:
mkdir /tmp/secret-lab && cd /tmp/secret-lab && git init
echo "app config" > app.py
git add app.py && git commit -m "init"
echo "AWS_SECRET_KEY=AKIAIOSFODNN7EXAMPLEFAKE123" > .env
git add .env && git commit -m "add env config"
echo "more app code" >> app.py
git add app.py && git commit -m "more work"
- নিশ্চিত করুন সিক্রেট সত্যিই ইতিহাসে আছে:
git log --all --oneline -- .env
git log -p --all -- .env | grep AKIA
.env-কে বর্তমান থেকে সরালেই যথেষ্ট না — তা যাচাই করুন:
git rm .env
git commit -m "remove .env"
git log -p --all -- .env | grep AKIA # এখনো পুরনো কমিটে পাওয়া যাবে!
git-filter-repoদিয়ে সম্পূর্ণ ইতিহাস থেকে মোছা (ইনস্টল না থাকলেpip install git-filter-repo):
git filter-repo --path .env --invert-paths --force
- যাচাই করুন সিক্রেট সম্পূর্ণ চলে গেছে:
git log --all --oneline -- .env # কিছুই দেখাবে না
git log -p --all | grep AKIA # কিছুই মিলবে না
- (সিমুলেশন) force-push প্রোটোকল লিখুন — বাস্তব remote না থাকায় শুধু কমান্ড নোট করুন:
# git push origin --force --all
# git push origin --force --tags
# → টিমকে জানানো, সবাইকে fresh clone করতে বলা
চ্যালেঞ্জ
gitleaks detect --source . (ইনস্টল থাকলে) চালিয়ে দেখুন এটা cleanup-এর আগে/পরে repository-তে কী রিপোর্ট করে।
ইন্টারভিউ প্রশ্ন — Module 33
প্র.১ — একটা .env ফাইল ভুলে কমিট ও push হয়ে গেছে বলে জানতে পারলেন — প্রথম পদক্ষেপ কী হওয়া উচিত?
উত্তর: প্রথমেই ইতিহাস পরিষ্কার করা না — বরং সেই ফাইলে থাকা key/token অবিলম্বে সংশ্লিষ্ট প্রোভাইডারে গিয়ে rotate/invalidate করা। কারণ history rewrite leak-কে নিরাপদ করে না, শুধু ভবিষ্যতে browse/clone করা মানুষের কাছ থেকে লুকায়।
প্র.২ — git rm .env && git commit করলেই কেন যথেষ্ট না?
উত্তর: এটা শুধু বর্তমান/ভবিষ্যৎ commit থেকে ফাইল সরায়, কিন্তু পুরনো কমিটে ফাইলটা object database-এ সম্পূর্ণ অক্ষত থাকে — git show <old-sha>:.env দিয়ে যে কেউ এখনো এটা পুনরুদ্ধার করতে পারবে।
প্র.৩ — git filter-repo কেন git filter-branch-এর চেয়ে সুপারিশ করা হয়?
উত্তর: filter-branch অনেক ধীর (প্রতিটা কমিটে আলাদা শেল প্রসেস স্পন করে), জটিল ও ত্রুটি-প্রবণ সিনট্যাক্স, আর ভুলভাবে ব্যবহার করলে পুরনো রেফারেন্স নীরবে থেকে যাওয়ার ঝুঁকি থাকে। Git-এর নিজস্ব ডকুমেন্টেশনই এখন filter-repo সুপারিশ করে — দ্রুত, নিরাপদ, সহজ সিনট্যাক্স।
প্র.৪ — History rewrite করার পর টিমমেটরা কেন git pull না করে ফ্রেশ ক্লোন করবেন?
উত্তর: Rewrite-এর ফলে সব commit SHA বদলে যায়, পুরনো লোকাল ইতিহাস remote-এর নতুন ইতিহাসের সাথে সম্পূর্ণ diverged। pull/merge চেষ্টা করলে বিপর্যয়কর conflict হতে পারে, এমনকি (unpushed local commit থাকলে) পুরনো, leaked সিক্রেটসহ কমিট আবার নতুন ইতিহাসে ফিরে আসার ঝুঁকিও থাকে — ফ্রেশ ক্লোন সবচেয়ে নিরাপদ।
প্র.৫ — leaked secret আগে থেকেই কোথায় কোথায় "ছড়িয়ে" থাকতে পারে যেখানে history rewrite পৌঁছায় না? উত্তর: GitHub-এর নিজস্ব ক্যাশ/ইনডেক্স, ইতিমধ্যে হওয়া fork/clone-এর লোকাল কপি, স্বয়ংক্রিয় স্ক্যানার বট যেগুলো push হওয়ার সাথে সাথে scrape করে ফেলে, এবং CI/third-party integration log।
প্র.৬ — Secret leak প্রতিরোধে একটা layered (defense-in-depth) approach-এ কোন কোন টুল/স্তর থাকা উচিত? উত্তর: লোকাল pre-commit hook-এ gitleaks-এর মতো স্ক্যানার, GitHub push protection সার্ভার-সাইড ব্লক হিসেবে, CI pipeline-এ truffleHog/gitleaks periodic scan, আর সবশেষে ইনসিডেন্ট ঘটলে rotation + filter-repo প্রোটোকল রেসপন্স হিসেবে — কোনো একটা স্তর একা যথেষ্ট না।
কর্নার কেস — Module 33
git filter-repoডিফল্টভাবে একটা fresh clone আশা করে, বিদ্যমান কাজ-চলা repository-তে সরাসরি চালাতে--forceলাগে — এটা ইচ্ছাকৃত সেফটি গার্ড, কারণ rewrite ধ্বংসাত্মক এবং অপরিবর্তনযোগ্য (backup ছাড়া), তাই টুলটা ব্যবহারকারীকে বাড়তি সতর্ক করতে চায়।Reflog-এও পুরনো, "মুছে ফেলা" কমিটের রেফারেন্স থেকে যেতে পারে — শুধু
filter-repo/BFG চালালেই যথেষ্ট না,git reflog expire --expire=now --all && git gc --prune=now --aggressiveনা চালালে পুরনো object আসলে ডিস্ক থেকেও সম্পূর্ণ মোছে না, যদিও সাধারণgit log-এ আর দেখা যায় না।Fork করা repository-তে rewrite প্রভাব ফেলে না — মূল repository-তে
filter-repoচালিয়ে force-push করলেও, ইতিমধ্যে হওয়া প্রতিটা fork-এ পুরনো, সিক্রেটসহ ইতিহাস অক্ষত থেকে যায় — এই কারণেই rotation বাদ দিয়ে শুধু rewrite-এর উপর নির্ভর করা বিপজ্জনক (আগের leaf-এর মূল নীতি)।Push protection মাঝে মাঝে "false positive" দিয়ে বৈধ push ব্লক করতে পারে — যেমন টেস্ট/ডকুমেন্টেশনে ইচ্ছাকৃতভাবে রাখা example key (
AKIAIOSFODNN7EXAMPLE-এর মতো AWS-এর নিজস্ব official example key) মাঝে মাঝে সিক্রেট স্ক্যানার ট্রিগার করতে পারে — GitHub-এ এই ধরনের ক্ষেত্রে ম্যানুয়ালি "allow" করার অপশন থাকে, কিন্তু প্রতিটা allow-এর যৌক্তিকতা যাচাই করে নেওয়া উচিত যাতে প্রকৃত leak ভুলে allow না হয়ে যায়।
চেকপয়েন্ট — Phase 10 সম্পন্ন
আপনি কি পারবেন?
- GPG এবং SSH — দুই পদ্ধতিতেই commit/tag signing সেটআপ করতে পারবেন, এবং ব্যাখ্যা করতে পারবেন GitHub-এর "Verified" ব্যাজ ঠিক কী প্রমাণ করে (আর কী প্রমাণ করে না — কোড কোয়ালিটি না, শুধু identity)?
- PAT এবং SSH কী-র মধ্যে একটা বাস্তব পরিস্থিতিতে সঠিক পছন্দ করতে পারবেন, এবং credential caching-এর নিরাপত্তা ট্রেড-অফ (মেমোরি বনাম ডিস্ক বনাম OS-নেটিভ manager) ব্যাখ্যা করতে পারবেন?
- একটা leaked secret ইনসিডেন্টে সঠিক ক্রমে পদক্ষেপ নিতে পারবেন — rotation প্রথমে, তারপর
git filter-repo/BFG দিয়ে history cleanup, তারপর force-push + team re-clone প্রোটোকল? - ব্যাখ্যা করতে পারবেন কেন শুধু ইতিহাস থেকে সিক্রেট মুছে ফেলাই যথেষ্ট না — fork/cache/scraper-এর কারণে rotation ছাড়া leak প্রকৃতপক্ষে নিষ্ক্রিয় হয় না?
git filter-branchকেন এখন deprecated/legacy বিবেচিত হয় এবং কেনgit filter-repo/BFG-ই আধুনিক সুপারিশ তা ব্যাখ্যা করতে পারবেন?- একটা layered secret-scanning কৌশল ডিজাইন করতে পারবেন (pre-commit hook + push protection + CI scan) যা প্রথমেই leak প্রতিরোধ করে, ইনসিডেন্ট রেসপন্সের উপর নির্ভর না করে?
এক লাইনে সারমর্ম
এই Phase-এর আসল শিক্ষা কোনো একটামাত্র কমান্ড না — এটা একটা চলমান discipline। Signing আর credential hygiene প্রতিদিনের অভ্যাস, এক-কালীন সেটআপ না। সিক্রেট লিক প্রতিরোধ (scanning, hooks) সবসময় নিরাময়ের (rotation, history rewrite) চেয়ে সস্তা ও নিরাপদ। Repository hygiene কোনো একবার "সম্পন্ন করা" কাজ না — এটা একটা টিমের প্রতিদিনের ইঞ্জিনিয়ারিং সংস্কৃতির অংশ হয়ে থাকা উচিত।
PHASE 11: GitHub-নির্দিষ্ট ওয়ার্কফ্লো
এতদূর যা শেখা হয়েছে তার প্রায় সবটাই pure git — লোকাল রিপোজিটরি, অবজেক্ট মডেল, branch/merge/rebase, hooks। কিন্তু বাস্তব টিমওয়ার্কের একটা বড় অংশ ঘটে git-এর বাইরে, GitHub-এর প্ল্যাটফর্ম লেয়ারে — fork, Pull Request, branch protection, GitHub Actions। এই চারটার একটাও git commit বা core git অবজেক্ট না; এগুলো GitHub-এর সার্ভার-সাইড ফিচার, git-এর ওপর তৈরি একটা workflow layer মাত্র। এই Phase-এ বারবার মনে করিয়ে দেওয়া হবে কোনটা git, কোনটা শুধু GitHub — কারণ GitLab/Bitbucket-এ গেলে এই লেয়ারটাই বদলে যায়, কিন্তু নিচের git ইতিমধ্যে যা শেখা হয়েছে তা অপরিবর্তিত থাকে।
MODULE 34: Fork ও Upstream Sync
GitHub-এ fork একটা প্ল্যাটফর্ম-লেভেল কনসেপ্ট — git নিজে fork বলে কিছু জানে না, এটা শুধু clone-remote-branch-এর ওপর একটা GitHub UI/API কনভেনশন। এই মডিউলে fork আসলে কী, নিজের ফর্কে upstream remote যোগ করা, নিয়মিত sync রাখার কৌশল, আর দীর্ঘদিন sync না করলে কী সমস্যা হয় (stale fork) কভার হবে।
Fork কী — এবং কেন এটা git-এর কনসেপ্ট না
সংজ্ঞা
Fork হলো GitHub-এ একটা রিপোজিটরির নিজের অ্যাকাউন্টে একটা সম্পূর্ণ কপি তৈরি করা — সব branch, commit, tag সহ। কিন্তু গুরুত্বপূর্ণ কথা: git-এর কোনো git fork কমান্ড নেই, এবং git-এর অবজেক্ট মডেলে (commit/tree/blob/tag) "fork" নামে কিছু নেই। Fork সম্পূর্ণভাবে GitHub (এবং GitLab-এ "fork", Bitbucket-এ একইরকম ফিচার)-এর একটা সার্ভার-সাইড, প্ল্যাটফর্ম-লেভেল কনভেনশন।
ভেতরে আসলে কী হয়
GitHub-এ fork করলে টেকনিক্যালি একটা নতুন repository তৈরি হয় আপনার অ্যাকাউন্টের নিচে, কিন্তু GitHub ইন্টারনালি একটা "fork network" ব্যবহার করে — একই organization-এর সব fork একই underlying git object storage শেয়ার করে (deduplication-এর জন্য), শুধু ref (branch/tag) আলাদা থাকে। এই জন্যই একটা বিশাল repo fork করা প্রায় সাথে সাথেই হয়ে যায় — পুরো ডেটা কপি হয় না, শুধু একটা নতুন namespace আর ref সেট তৈরি হয়।
কেন দরকার
Open-source প্রজেক্টে আপনার কাছে সরাসরি push access থাকে না। Fork আপনাকে একটা নিজস্ব কপি দেয় যেখানে স্বাধীনভাবে push করতে পারবেন, তারপর মূল প্রজেক্টে একটা Pull Request পাঠাতে পারবেন — মূল প্রজেক্টের owner সেই পরিবর্তন review করে merge করবেন কিনা সিদ্ধান্ত নেবেন।
রিয়েল-লাইফ উদাহরণ
আপনি (Rakib) যদি একটা জনপ্রিয় Django প্যাকেজে একটা bug fix পাঠাতে চান — সরাসরি সেই repo-তে push access পাবেন না। GitHub-এ "Fork" বাটনে ক্লিক করলে github.com/rakib/django-package নামে একটা কপি তৈরি হবে আপনার অ্যাকাউন্টে; সেটা clone করে, ফিক্স করে, push করে, তারপর মূল django-package repo-তে একটা PR পাঠাবেন।
এক লাইনে
Fork = GitHub-এর তৈরি একটা কনভেনিয়েন্স লেয়ার, যাতে write-access ছাড়াই কারো কোডে অবদান রাখা যায় — git নিজে এই কনসেপ্ট সম্পর্কে কিছুই জানে না, git শুধু repository আর remote বোঝে।
Fork Clone করে Upstream Remote যোগ করা
ধাপ
GitHub-এ fork করার পর, সেটাই এখন আপনার "origin" — নিজের কপি:
git clone https://github.com/rakib/django-package.git
cd django-package
git remote -v
# origin https://github.com/rakib/django-package.git (fetch)
# origin https://github.com/rakib/django-package.git (push)
মূল (upstream) প্রজেক্টের সাথে সংযোগ রাখতে একটা দ্বিতীয় remote যোগ করা হয়:
git remote add upstream https://github.com/original-org/django-package.git
git remote -v
# origin https://github.com/rakib/django-package.git (fetch/push)
# upstream https://github.com/original-org/django-package.git (fetch/push)
কনভেনশন কেন এমন
origin = আপনার নিজের fork, যেখানে push করার অধিকার আছে।
upstream = মূল প্রজেক্ট, যেখান থেকে শুধু fetch করা হয় (সাধারণত push access নেই, দরকারও নেই — PR-ই একমাত্র পথ)।
এটা শুধু নামকরণের কনভেনশন — git-এর কাছে দুটোই সাধারণ remote, বিশেষ কোনো "upstream" টাইপ নেই। কিন্তু পুরো ওপেন-সোর্স কমিউনিটি এই নামকরণ মেনে চলে, তাই অন্য কারো instructions অনুসরণ করা সহজ হয়।
যাচাই
git fetch upstream
git branch -r
# origin/main
# upstream/main
দুটো main branch দেখা যাবে — একটা আপনার fork-এর, একটা মূল প্রজেক্টের। এই দুটো স্বাধীনভাবে diverge করতে পারে, পরের leaf-এ এদের sync রাখার উপায় দেখা হবে।
Fork-কে Upstream-এর সাথে Sync রাখা
সমস্যা
আপনি fork করার পর মূল প্রজেক্টে নতুন commit আসতে থাকে, কিন্তু আপনার fork-এর main branch সেই commit পায় না — কারণ fork করাটা একটা one-time snapshot, চলমান sync না।
সমাধান — fetch + merge/rebase
git checkout main
git fetch upstream
git merge upstream/main
# অথবা, লিনিয়ার হিস্ট্রি চাইলে:
git rebase upstream/main
git push origin main
ভেতরে কী হচ্ছে
git fetch upstream শুধু upstream-এর নতুন commit অবজেক্ট ডাউনলোড করে আর upstream/main remote-tracking ref আপডেট করে — আপনার লোকাল main-কে স্পর্শ করে না। git merge upstream/main তারপর সেই নতুন commit-গুলো আপনার লোকাল main-এ ইন্টিগ্রেট করে (দরকার হলে একটা merge commit তৈরি করে)। git push origin main সেই আপডেটেড history আপনার fork-এ পাঠায়।
GitHub UI শর্টকাট
GitHub-এর repo পেজে fork-এর ওপরে "Sync fork" বাটন থাকে — এটা ঠিক এই একই fetch + merge (fast-forward হলে) সার্ভার-সাইডে করে দেয়। CLI দিয়ে: gh repo sync rakib/django-package।
রিয়েল-লাইফ উদাহরণ
দুই সপ্তাহ ধরে একটা feature branch নিয়ে কাজ করার আগে, প্রতিদিন main sync রাখা উচিত — নাহলে PR তৈরির সময় বিশাল conflict দেখা দেবে, যা পরের leaf-এ কভার হবে।
Stale Fork সমস্যা এবং এড়ানোর উপায়
সমস্যাটা কী
একটা fork মাসের পর মাস sync না করলে, upstream-এর সাথে সেটার history ব্যাপকভাবে diverge করে যায় — একে বলে stale fork। GitHub এই অবস্থায় repo পেজে "This branch is 214 commits behind, 3 commits ahead" এই ধরনের সতর্কবার্তা দেখায়।
কেন এটা ভয়াবহ
- নতুন PR তৈরি করলে GitHub পুরনো আর নতুন history-র মধ্যে merge base খুঁজতে গিয়ে বিশাল, অপ্রাসঙ্গিক diff দেখাতে পারে — রিভিউয়ার বুঝতেই পারবেন না আসল পরিবর্তন কোনটা।
- Conflict resolution জটিল হয়ে যায় — শত শত commit-এর ফারাক থাকলে একই লাইনে বহুবার পরিবর্তন হয়ে থাকতে পারে।
- File deletion/rename-এর মতো বড় পরিবর্তন upstream-এ হলে, merge/rebase দুটোই কঠিন হয়ে যায়।
সমাধান কৌশল
- নিয়মিত sync — নতুন feature branch শুরু করার আগে সবসময়
mainsync করুন। - ছোট-জীবিত feature branch — branch যত বেশিদিন বাঁচে, diverge তত বাড়ে; তাড়াতাড়ি PR পাঠিয়ে merge করে ফেলাই ভালো।
- চরম stale হয়ে গেলে — কখনো কখনো পুরনো fork ডিলিট করে নতুন করে fork করাই দ্রুততর সমাধান, বিশেষত যদি আপনার fork-এ কোনো নিজস্ব unmerged কাজ না থাকে।
রিয়েল-লাইফ উদাহরণ
Rakib যদি একটা ওপেন-সোর্স Django প্যাকেজে ৬ মাস আগে fork করে রেখে দেন, আর এখন একটা নতুন bug fix পাঠাতে চান — সরাসরি পুরনো fork-এ কাজ শুরু করলে বিশাল conflict-এর মুখোমুখি হবেন। প্রথমে git fetch upstream && git merge upstream/main && git push origin main চালিয়ে fork আপ-টু-ডেট করে নেওয়াই সঠিক প্রথম পদক্ষেপ।
ল্যাব: একটা Repo Fork করে Upstream Sync করা
লক্ষ্য
একটা পাবলিক GitHub repo fork করে, upstream remote যোগ করে, এবং fork-কে upstream-এর সাথে sync রাখার পুরো cycle হাতে-কলমে অনুশীলন করা।
ধাপ ১ — Fork করুন
GitHub-এ যেকোনো ছোট পাবলিক প্রজেক্ট (উদাহরণ: octocat/Hello-World) খুলে উপরের-ডানে "Fork" বাটনে ক্লিক করুন। এটা আপনার অ্যাকাউন্টে একটা কপি তৈরি করবে।
ধাপ ২ — Clone ও Remote সেটআপ
git clone https://github.com/<আপনার-ইউজারনেম>/Hello-World.git
cd Hello-World
git remote add upstream https://github.com/octocat/Hello-World.git
git remote -v
ধাপ ৩ — Sync করুন
git fetch upstream
git log --oneline main..upstream/main # upstream-এ কী নতুন আছে দেখুন
git merge upstream/main
git push origin main
ধাপ ৪ — একটা ফিচার branch দিয়ে PR সিমুলেট করুন
git checkout -b add-my-note
echo "Rakib was here" >> README
git add README
git commit -m "docs: add a note"
git push origin add-my-note
GitHub-এ যান, "Compare & pull request" বাটন দেখবেন — এটাই আপনার fork থেকে মূল repo-তে (বা নিজের fork-এর মধ্যেই) PR তৈরির পথ।
যাচাই
git log --oneline --graph --all চালিয়ে দেখুন origin/main, upstream/main, আর আপনার add-my-note branch — তিনটার সম্পর্ক কেমন দেখাচ্ছে।
চ্যালেঞ্জ
git remote remove upstream করে দেখুন git fetch upstream কী error দেয় — বুঝুন কেন remote name গুলো শুধু লোকাল aliases।
ইন্টারভিউ প্রশ্ন — Module 34
প্র.১ — Fork কি git-এর একটা ফিচার?
উত্তর: না। Fork সম্পূর্ণভাবে GitHub (বা GitLab/Bitbucket)-এর একটা প্ল্যাটফর্ম-লেভেল কনভেনশন — git core-এ fork নামে কোনো কমান্ড বা অবজেক্ট নেই। git শুধু repository, remote, branch বোঝে।
প্র.২ — origin আর upstream remote-এর মধ্যে টেকনিক্যাল পার্থক্য কী?
উত্তর: টেকনিক্যালি কোনো পার্থক্য নেই — দুটোই সাধারণ named remote। origin/upstream শুধু কমিউনিটি-কনভেনশন নামকরণ: origin = আপনার fork (push access আছে), upstream = মূল প্রজেক্ট (সাধারণত শুধু fetch)।
প্র.৩ — Fork sync না রাখলে কী সমস্যা হয়? উত্তর: Stale fork তৈরি হয় — upstream-এর সাথে history ব্যাপকভাবে diverge করে, ফলে নতুন PR-এ বিশাল অপ্রাসঙ্গিক diff দেখা যায় এবং conflict resolution জটিল হয়ে যায়।
প্র.৪ — git fetch upstream চালানোর পর git branch -r কী দেখাবে যদি আপনার একটাই main branch থাকে?
উত্তর: দুটো remote-tracking branch — origin/main এবং upstream/main — একই নামের হলেও এরা আলাদা ref, স্বাধীনভাবে diverge করতে পারে।
প্র.৫ — GitHub-এর "Sync fork" বাটন ভেতরে কী করে?
উত্তর: সার্ভার-সাইডে মূলত fetch + fast-forward merge করে upstream-এর নতুন commit fork-এর branch-এ নিয়ে আসে — gh repo sync কমান্ড এর CLI সমতুল্য।
কর্নার কেস — Module 34
- Fork করার সময় শুধু default branch কপি হয় না — সব branch/tag কপি হয়, কিন্তু GitHub UI default-এ শুধু default branch দেখায়; বাকি branch দেখতে branch dropdown-এ যেতে হয়।
upstreamremote-এ push করার চেষ্টা করলে সাধারণত permission denied পাবেন — এটা প্রত্যাশিতই, কারণ contribute করার একমাত্র পথ হলো নিজের fork-এ push করে PR পাঠানো।- Fork-এর নিজস্ব Issues/PR/Actions সেটিংস মূল repo থেকে স্বাধীন — fork-এ GitHub Actions ডিফল্টে বন্ধ থাকতে পারে privacy/security কারণে (arbitrary code execution আটকাতে); PR থেকে workflow রান করাতে হলে "Approve and run" আলাদাভাবে করতে হয়।
git merge upstream/mainমাঝেমধ্যে conflict দেয় যদি আপনি নিজের fork-এরmain-এই সরাসরি commit করে থাকেন — best practice হলো fork-এরmain-কে কখনো সরাসরি স্পর্শ না করা, সব কাজ ফিচার branch-এ করা,mainশুধু upstream sync-এর জন্য রাখা।- একটা fork ডিলিট করলে তার ওপর ভিত্তি করে তৈরি open PR বন্ধ হয়ে যায় না সবসময় — merge হওয়ার আগে fork ডিলিট করলে GitHub সতর্ক করে, কারণ PR-এর সোর্স branch হারিয়ে যাবে।
MODULE 35: Pull Request
Pull Request (PR) হলো GitHub-এর কোড রিভিউ ও ইন্টিগ্রেশন ওয়ার্কফ্লোর কেন্দ্রবিন্দু — একটা branch-এর পরিবর্তন আরেকটা branch-এ মার্জ করার জন্য একটা রিভিউযোগ্য, আলোচনাযোগ্য অনুরোধ। এই মডিউলে PR তৈরি, review process, তিনটা merge strategy-র (merge commit/squash/rebase-merge) history-র ওপর প্রভাব, issue auto-closing, আর gh CLI দিয়ে পুরো workflow টার্মিনাল থেকে চালানো কভার হবে।
Pull Request কী এবং কীভাবে তৈরি করবেন (Draft PR সহ)
সংজ্ঞা
Pull Request (PR) হলো GitHub-এর একটা রিভিউযোগ্য অনুরোধ: "আমার এই branch-এর commit-গুলো তোমার এই branch-এ merge করো"। এটাও git-এর কনসেপ্ট না — git-এ শুধু branch merge করার কমান্ড আছে (git merge), কিন্তু "একটা merge-এর জন্য রিভিউ/আলোচনা/অনুমোদন চাওয়া" — এই পুরো ওয়ার্কফ্লো GitHub-এর সংযোজন।
কীভাবে তৈরি করবেন
git checkout -b fix-login-bug
# ... পরিবর্তন করে commit ...
git push origin fix-login-bug
তারপর GitHub-এ গিয়ে "Compare & pull request" ক্লিক করুন, অথবা CLI দিয়ে:
gh pr create --title "Fix login bug" --body "Fixes #45" --base main --head fix-login-bug
Draft PR
কাজ এখনো সম্পূর্ণ না হলে Draft PR তৈরি করা যায় (gh pr create --draft) — এটা রিভিউয়ারকে জানায় "এখনো মার্জযোগ্য না, কিন্তু আগে থেকে দেখে ফিডব্যাক দিতে পারো"। Draft PR-এ status check চলে, কিন্তু merge বাটন disabled থাকে যতক্ষণ না "Ready for review" করা হয়।
ভেতরে কী থাকে
একটা PR প্রযুক্তিগতভাবে দুটো ref-এর মধ্যে একটা diff (base...head), plus metadata (title, description, reviewers, labels, linked issues) যা GitHub নিজের ডাটাবেজে রাখে — git repo-র ভেতরে PR-এর কোনো commit/object থাকে না, PR merge হলে যা তৈরি হয় তা হলো একটা সাধারণ merge/squash/rebase commit।
রিয়েল-লাইফ উদাহরণ
Rakib একটা bug fix করে PR পাঠালেন — টিমের সিনিয়র ডেভেলপার কোড রিভিউ করে দুটো লাইনে কমেন্ট করলেন, Rakib ফিক্স করে নতুন commit push করলেন (একই PR আপডেট হলো), অনুমোদনের পর merge হলো।
Review Request ও Suggested Changes — এগুলোও Commit হিসেবেই আসে
Review request
PR-এর সাইডবারে নির্দিষ্ট ব্যক্তি/টিমকে reviewer হিসেবে যোগ করা যায় (gh pr create --reviewer username বা UI থেকে)। Reviewer approve/request changes/comment — এই তিনটার একটা দিতে পারেন।
Suggested changes
Reviewer PR-এর diff-এ সরাসরি একটা কোড-স্নিপেট প্রস্তাব করতে পারেন ("Suggest change" বাটন) — এটা দেখতে একটা বিশেষ markdown কোড ব্লকের মতো। PR author "Commit suggestion" ক্লিক করলে GitHub সেই পরিবর্তনটাকে একটা সাধারণ commit হিসেবে PR branch-এ push করে দেয় — reviewer-এর নামে না, বরং সাধারণত committer হিসেবে যিনি "commit suggestion" ক্লিক করলেন তার নামে (অথবা GitHub-এর web-commit bot হিসেবে)।
গুরুত্বপূর্ণ বোঝাপড়া
এখানে কোনো জাদু নেই — suggested change accept করা মানে ভেতরে ভেতরে ঠিক তাই ঘটছে যা আপনি লোকালি করলে করতেন: একটা নতুন commit তৈরি হচ্ছে যা সেই ফাইলের সেই লাইন বদলে দিচ্ছে, এবং সেই commit push হয়ে PR branch-এ যোগ হচ্ছে। GitHub UI শুধু এই প্রক্রিয়াটা এক-ক্লিকে করে দেয়।
রিয়েল-লাইফ উদাহরণ
# Suggested change accept করার পর লোকালি sync করতে:
git fetch origin
git checkout fix-login-bug
git pull origin fix-login-bug
Reviewer-এর "commit suggestion" যদি লোকাল branch-এর সাথে diverge করে (আপনি একইসাথে লোকালি নতুন commit করেছেন), git pull merge/rebase conflict দেখাতে পারে — ঠিক যেমন সাধারণ দুই ডেভেলপারের কাজ diverge করলে হয়।
প্রসেস টেবিল
| অ্যাকশন | ফলাফল |
|---|---|
| Approve | PR-এ green checkmark, merge করার অনুমতি (protection rule অনুযায়ী) |
| Request changes | merge ব্লক হতে পারে (protection rule-এ required হলে) |
| Comment only | কোনো merge-ব্লকিং প্রভাব নেই, শুধু আলোচনা |
তিনটা Merge Strategy: Merge Commit বনাম Squash বনাম Rebase-merge
তিনটা অপশন
GitHub PR মার্জ করার সময় তিনটা স্ট্র্যাটেজি অফার করে — এগুলো আগের ফেজে শেখা git merge/git rebase-এরই সার্ভার-সাইড প্রয়োগ, শুধু GitHub একটা বাটনের পেছনে সেটা চালায়।
| স্ট্র্যাটেজি | ভেতরে কী কমান্ড চলে | History-তে প্রভাব |
|---|---|---|
| Merge commit | git merge --no-ff feature |
সব individual commit বজায় থাকে + একটা নতুন merge commit যোগ হয়; history-তে branch-এর shape দেখা যায় |
| Squash and merge | git merge --squash feature + একটা commit |
PR-এর সব commit একটা মাত্র commit-এ মিশে যায় main-এ; branch history-র বিস্তারিত হারিয়ে যায় কিন্তু main পরিষ্কার/লিনিয়ার থাকে |
| Rebase and merge | git rebase feature (main-এর ওপর) + fast-forward |
প্রতিটা commit আলাদা থাকে, কিন্তু নতুন hash দিয়ে main-এর টিপে replay হয়; কোনো merge commit তৈরি হয় না, history সম্পূর্ণ লিনিয়ার |
কোনটা কখন
- Merge commit: আপনি feature branch-এর development history (কোন commit কবে হয়েছে) সংরক্ষণ করতে চান, অথবা বড় feature-এর জন্য "এই সব commit একসাথে একটা feature ছিল" — এই গ্রুপিং দরকার।
- Squash: ফিচার branch-এ অনেক ছোট/messy "wip", "fix typo" commit থাকলে, main-এ একটামাত্র পরিষ্কার commit চান।
- Rebase-merge: সম্পূর্ণ লিনিয়ার history চান (কোনো merge commit নেই), কিন্তু individual commit-ও রাখতে চান।
ঝুঁকি
Rebase-merge আর squash দুটোই commit hash বদলে দেয় — যদি অন্য কেউ ইতিমধ্যে সেই feature branch থেকে pull করে থাকে, তার লোকাল history diverge করে যাবে (আগের ফেজে শেখা "rewriting shared history" সমস্যার ঠিক পুনরাবৃত্তি, শুধু PR-merge লেভেলে)।
রিয়েল-লাইফ উদাহরণ
অনেক টিম convention হিসেবে "squash and merge" default রাখে — এতে main-এর history প্রতি PR-এ একটা commit, git log main স্ক্যান করা সহজ হয়, আর git bisect ব্যবহার করাও সহজ হয় কারণ প্রতিটা commit একটা সম্পূর্ণ, deployable ইউনিট।
Issue Linking — closes #123, fixes #123 কীভাবে Auto-close ট্রিগার করে
সিনট্যাক্স
PR-এর description বা কোনো commit message-এ নির্দিষ্ট keyword + issue নম্বর লিখলে GitHub সেই issue-কে PR-এর সাথে লিংক করে দেয়:
closes #123
fixes #123
resolves #123
কীভাবে কাজ করে
এটা সম্পূর্ণ GitHub-সাইড টেক্সট-পার্সিং ফিচার — git-এর কোনো ভূমিকা নেই এখানে, commit message-এ শুধু plain text থাকে। GitHub PR description/commit message parse করে এই keyword + #number প্যাটার্ন খোঁজে, এবং সেই অনুযায়ী issue-এর সাথে একটা রেফারেন্স তৈরি করে। PR merge হওয়ার মুহূর্তে (শুধু default branch-এ merge হলে) — লিংক করা issue স্বয়ংক্রিয়ভাবে বন্ধ (closed) হয়ে যায়।
গুরুত্বপূর্ণ শর্ত
- Keyword কাজ করে শুধু default branch-এ merge হলে — অন্য কোনো branch-এ merge করলে auto-close ট্রিগার হয় না।
- Cross-repository linking সম্ভব:
closes org/other-repo#123। - শুধু
#123লিখলে (কোনো keyword ছাড়া) — শুধু একটা রেফারেন্স তৈরি হয়, auto-close হয় না।
রিয়েল-লাইফ উদাহরণ
git commit -m "fix: null pointer in login handler
Fixes #45"
git push origin fix-login-bug
PR তৈরি হলে GitHub এই commit message স্ক্যান করে issue #45-কে সাইডবারে "Linked issues" হিসেবে দেখাবে, এবং PR merge হওয়ার সাথে সাথেই issue #45 স্বয়ংক্রিয়ভাবে বন্ধ হয়ে যাবে — টিমকে ম্যানুয়ালি issue ট্র্যাকার আপডেট করতে হয় না।
gh দিয়ে
gh issue create --title "Login bug" # issue #45 তৈরি
gh pr create --body "Fixes #45"
gh CLI দিয়ে PR ও Issue ম্যানেজ করা
কেন gh CLI
gh (GitHub CLI) ব্রাউজারে না গিয়ে টার্মিনাল থেকেই পুরো GitHub workflow চালানোর টুল — PR তৈরি, review, merge, issue ম্যানেজমেন্ট সবকিছু।
মূল কমান্ড
gh auth login # প্রথমবার অথেনটিকেট করা
gh pr create --title "Fix bug" --body "Fixes #45" --base main
gh pr list # খোলা PR-এর তালিকা
gh pr view 12 # PR #12-এর বিস্তারিত
gh pr view 12 --web # ব্রাউজারে খোলা
gh pr checkout 12 # PR #12-এর branch লোকালি checkout করা
gh pr diff 12 # PR-এর diff দেখা
gh pr review 12 --approve # অনুমোদন
gh pr merge 12 --squash # squash merge করা
gh issue create --title "Bug: crash on login"
gh issue list --label bug
gh issue close 45
রিয়েল-লাইফ উদাহরণ
একজন রিভিউয়ার হিসেবে Rakib প্রতিটা PR ব্রাউজারে খুলে দেখার বদলে:
gh pr list
gh pr checkout 12
# লোকালি টেস্ট চালানো, IDE-তে দেখা
gh pr review 12 --comment --body "টেস্ট প্যাস করেছে, একটা মাইনর সাজেশন আছে"
এভাবে পুরো review cycle টার্মিনাল আর editor-এর মধ্যেই থেকে যায়, context switching কমে।
CI/স্ক্রিপ্টিং-এ সুবিধা
gh স্ক্রিপ্টেবল — shell script-এ বসিয়ে automated release note তৈরি, bulk PR তৈরি, বা bot-এর মতো আচরণ করানো যায়:
gh pr create --title "chore: dependency bump" --body "Automated" --label automated
ল্যাব: gh CLI দিয়ে সম্পূর্ণ PR Workflow
লক্ষ্য
gh CLI ব্যবহার করে issue তৈরি থেকে PR merge পর্যন্ত সম্পূর্ণ চক্র চালানো, ব্রাউজার স্পর্শ না করে।
ধাপ ১ — Setup
gh auth login
cd my-fork-repo
gh issue create --title "Add CONTRIBUTING.md" --body "প্রজেক্টে contribution guide নেই"
# আউটপুট থেকে issue নম্বর নোট করুন, ধরুন #7
ধাপ ২ — Branch ও কাজ
git checkout -b add-contributing-guide
echo "# Contributing" > CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "docs: add CONTRIBUTING.md
Closes #7"
git push origin add-contributing-guide
ধাপ ৩ — PR তৈরি ও পর্যবেক্ষণ
gh pr create --title "Add CONTRIBUTING.md" --body "Closes #7" --base main
gh pr view --web # ব্রাউজারে PR দেখুন, issue #7 লিংক হয়েছে কিনা যাচাই করুন
gh pr checks # কোনো CI status check থাকলে দেখুন
ধাপ ৪ — Merge
gh pr merge --squash --delete-branch
যাচাই
gh issue view 7
# Status: Closed
issue #7 স্বয়ংক্রিয়ভাবে বন্ধ হয়ে গেছে দেখুন — Closes #7 keyword-এর কারণে।
চ্যালেঞ্জ
--merge, --squash, --rebase — তিনটা ফ্ল্যাগ দিয়ে তিনবার একই ধরনের ছোট PR merge করে git log --oneline --graph দিয়ে main branch-এর history-র পার্থক্য পর্যবেক্ষণ করুন।
ইন্টারভিউ প্রশ্ন — Module 35
প্র.১ — PR কি git-এর একটা অবজেক্ট? উত্তর: না। PR হলো GitHub-এর ডাটাবেজে রাখা metadata (branch reference + description + reviewers) — git repo-তে PR নামে কোনো commit/object থাকে না। PR merge হলে যা তৈরি হয় তা একটা স্বাভাবিক merge/squash/rebase commit।
প্র.২ — Squash merge আর rebase merge-এর মধ্যে পার্থক্য কী? উত্তর: Squash সব commit একটাতে মিশিয়ে main-এ একটা commit হিসেবে যোগ করে (individual commit history হারায়)। Rebase-merge প্রতিটা commit আলাদা রাখে কিন্তু main-এর টিপে নতুন hash দিয়ে replay করে, merge commit ছাড়াই — লিনিয়ার history পাওয়া যায়, individual commit-ও থাকে।
প্র.৩ — Suggested change accept করলে কমিট কার নামে হয়? উত্তর: সাধারণত যিনি "Commit suggestion" ক্লিক করেন (PR author) অথবা GitHub-এর web-commit bot হিসেবে — reviewer-এর নামে সরাসরি commit হয় না, কিন্তু co-author হিসেবে ট্র্যাক থাকতে পারে।
প্র.৪ — closes #123 কখন issue বন্ধ করে না?
উত্তর: যদি PR default branch ছাড়া অন্য কোনো branch-এ merge হয়, অথবা PR merge না হয়ে শুধু close করা হয় (merge ছাড়া) — auto-close ট্রিগার হবে না।
প্র.৫ — একটা টিমের PR-এ অনেক "wip", "fix typo" commit থাকে — কোন merge strategy বেছে নেবেন এবং কেন? উত্তর: Squash and merge — এতে main branch-এ প্রতিটা PR একটা পরিষ্কার, একক commit হিসেবে যোগ হয়, messy intermediate commit-গুলো main-এর history-কে দূষিত করে না।
কর্নার কেস — Module 35
- Draft PR-এও CI status check চলে, কিন্তু merge বাটন সবসময় disabled থাকে যতক্ষণ না "Ready for review" করা হয় — অনেকে ভাবেন draft মানে CI স্কিপ হয়, তা না।
closes #123একই PR-এ একাধিকবার লিখলে সমস্যা নেই, কিন্তু ভিন্ন ভিন্ন keyword দিয়ে ভিন্ন repo-র issue রেফারেন্স করলে (closes org/repo-a#1,closes org/repo-b#2) — cross-repo auto-close permission-নির্ভর, সবসময় কাজ নাও করতে পারে।- Rebase-merge করলে commit hash বদলে যায় — যদি কেউ ইতিমধ্যে সেই feature branch থেকে লোকালি pull করে থাকে, পরে
mainথেকে সেই branch delete হয়ে গেলে তাদের লোকাল branch "orphaned" কমিট নিয়ে থেকে যাবে —git fetch --pruneচালিয়ে পরিষ্কার করা দরকার। - Squash merge PR-এর সব author-এর commit metadata হারিয়ে ফেলতে পারে যদি সতর্ক না থাকা হয় — GitHub ডিফল্টে সব contributor-কে
Co-authored-by:ট্রেইলার হিসেবে squash commit message-এ যোগ করে, কিন্তু ম্যানুয়ালি এডিট করলে ভুলে মুছে যেতে পারে। gh pr mergeলোকাল branch অটো-ডিলিট করে না যদি না--delete-branchফ্ল্যাগ দেওয়া হয় — merge হয়ে যাওয়ার পরও পুরনো ফিচার branch লোকালি ও রিমোটে থেকে যেতে পারে,git branch -dআলাদা করে করতে হয়।
MODULE 36: Branch Protection ও গভর্নেন্স
একটা টিমে সবাইকে সরাসরি main branch-এ push করতে দেওয়া বিপর্যয়কর — branch protection rule এই ঝুঁকি নিয়ন্ত্রণ করে। এই মডিউলে required review/status check, linear history এনফোর্স করা, CODEOWNERS দিয়ে পাথ-ভিত্তিক বাধ্যতামূলক রিভিউয়ার, আর merge queue দিয়ে বড় টিমে race condition এড়ানো কভার হবে — এগুলো সবই GitHub-এর গভর্নেন্স লেয়ার, git-এর না।
Required Reviews ও Required Status Checks
সমস্যা যা এটা সমাধান করে
কোনো নিয়ম ছাড়া, যেকোনো collaborator সরাসরি untested, unreviewed কোড main-এ push করে দিতে পারে। Branch protection rule GitHub-এর একটা রিপো-সেটিং যা নির্দিষ্ট branch-এ (সাধারণত main) push/merge-এর ওপর শর্ত আরোপ করে — এটাও সম্পূর্ণ প্ল্যাটফর্ম-লেভেল, git-এর নিজের কোনো "protected branch" ধারণা নেই।
Required reviews
কনফিগার করা যায় ন্যূনতম কতজন approve দিলে merge সম্ভব হবে (Require approvals, সংখ্যা সেট করা যায়, যেমন ২ জন)। "Dismiss stale approvals" অপশন চালু থাকলে নতুন commit push হলে আগের approval বাতিল হয়ে যায় — পুরনো কোড দেখে approve করা কেউ, নতুন পরিবর্তন না দেখেই merge হতে দেওয়া আটকায়।
Required status checks
CI pipeline (GitHub Actions বা অন্য কোনো external CI) থেকে আসা status (pass/fail) merge-এর পূর্বশর্ত করা যায়। উদাহরণ: Require status checks to pass চালু করে test, lint, build — এই তিনটা job pass না হলে merge বাটন disabled থাকবে, এমনকি সব review approve হলেও।
রিয়েল-লাইফ উদাহরণ
Rakib-এর টিমে main branch protection rule আছে: ২ জন approval + pytest status check pass বাধ্যতামূলক। একজন জুনিয়র ডেভেলপার ভুল করে একটা ব্রেকিং চেঞ্জ push করলেও, CI ফেইল হওয়ায় merge বাটন লক থাকে — প্রোডাকশনে যাওয়ার আগেই সমস্যা ধরা পড়ে।
টেবিল
| সেটিং | কী আটকায় |
|---|---|
| Require approvals (N) | পর্যাপ্ত রিভিউ ছাড়া merge |
| Dismiss stale reviews | নতুন commit-এর পর পুরনো approval দিয়ে merge |
| Require status checks | CI ফেইল অবস্থায় merge |
| Require branches up to date | পুরনো base-এর ওপর merge (আগে rebase/sync বাধ্যতামূলক) |
Linear History রিকোয়ারমেন্ট — Rebase-merge বা Squash বাধ্যতামূলক করা
সেটিং
Branch protection-এ "Require linear history" চালু করলে GitHub সেই branch-এ merge commit তৈরি করে এমন merge (সাধারণ merge commit strategy) সম্পূর্ণ নিষিদ্ধ করে দেয় — শুধু squash অথবা rebase-merge অনুমোদিত, কারণ এই দুটোই main-এর history-কে সবসময় একটা সরল রেখা (linear) রাখে, কোনো branch-point/merge-point তৈরি হয় না।
কেন কেউ এটা চায়
git log mainস্ক্যান করা সহজ হয় — কোনো জটিল branch টপোলজি নেই, একটার পর একটা commit।git bisectঅনেক দ্রুত ও পরিষ্কার হয় — merge commit-এর কারণে bisect মাঝে মাঝে বিভ্রান্তিকর ফলাফল দেয় (কোন দিকে branch অনুসরণ করা উচিত)।- Revert করা সহজ — merge commit revert করা (
git revert -m) সবসময় একটু জটিল (কোন parent দিকে যাবে বাছাই করতে হয়); লিনিয়ার history-তে সাধারণgit revertযথেষ্ট।
ট্রেড-অফ
Linear history রিকোয়ারমেন্ট থাকলে "এই সব commit একসাথে একটা feature ছিল" — এই গ্রুপিং তথ্য হারিয়ে যায় (স্কোয়াশ করলে) বা কমিট hash বদলে যায় (rebase করলে) — Module 39-এ merge-vs-rebase ফ্রেমওয়ার্কে এই ট্রেড-অফ আরও বিস্তারিত।
রিয়েল-লাইফ উদাহরণ
একটা লাইব্রেরি প্রজেক্ট যেখানে প্রতিটা commit-কে স্বাধীনভাবে git bisect দিয়ে টেস্ট করা যেতে হবে (প্রতিটা commit build/test pass করা আবশ্যক) — সেখানে "Require linear history" + squash merge কনভেনশন আদর্শ, কারণ main-এর প্রতিটা commit একটা সম্পূর্ণ, স্বনির্ভর PR-এর প্রতিনিধিত্ব করে।
# Protection চালু থাকলে এই কমান্ড GitHub সার্ভারে reject হবে:
git push origin main # সরাসরি merge commit সহ push
CODEOWNERS ফাইল — নির্দিষ্ট পাথের বাধ্যতামূলক রিভিউয়ার
ফাইল লোকেশন ও ফরম্যাট
.github/CODEOWNERS (অথবা repo root, বা docs/-এ) — একটা প্লেইন টেক্সট ফাইল যেখানে path pattern-এর সাথে GitHub username/team ম্যাপ করা থাকে:
# .github/CODEOWNERS
*.py @rakib
/infra/ @rakib @devops-team
/src/payments/ @payments-team
*.md @docs-team
কীভাবে কাজ করে
PR তৈরি হওয়ার সাথে সাথে, PR-এ যে ফাইলগুলো পরিবর্তিত হয়েছে সেগুলোর path CODEOWNERS-এর প্যাটার্নের সাথে ম্যাচ করে, এবং GitHub স্বয়ংক্রিয়ভাবে সেই owner-দের reviewer হিসেবে যোগ করে দেয়। Branch protection-এ "Require review from Code Owners" চালু থাকলে, ম্যাচ করা owner-এর approval ছাড়া merge সম্ভব হয় না — এমনকি অন্য যেকোনো ২ জন approve করলেও।
রিয়েল-লাইফ উদাহরণ
Rakib-এর টিমে payments মডিউলে পরিবর্তন হলে অবশ্যই @payments-team-এর কারো রিভিউ লাগবে, কিন্তু docs/-এ পরিবর্তন হলে @docs-team-ই যথেষ্ট — একজন জুনিয়র ডেভেলপার payment লজিকে পরিবর্তন করলেও সিনিয়র পেমেন্ট ইঞ্জিনিয়ারের চোখ এড়িয়ে merge হতে পারবে না।
কর্নার কেস
সবচেয়ে নিচের ম্যাচিং প্যাটার্ন জিতে যায় (last-match-wins), gitignore-এর মতোই। তাই general pattern আগে, specific pattern পরে লেখা উচিত:
*.py @rakib
/src/payments/*.py @payments-team # এটা জিতবে payments ফোল্ডারের জন্য
Merge Queue — বড় টিমে Race Condition এড়ানো
সমস্যা যা এটা সমাধান করে
কল্পনা করুন একসাথে ৫টা PR "Ready to merge" অবস্থায় — সবগুলোই individually CI pass করেছে main-এর current state-এর বিপরীতে। কিন্তু একটার পর একটা merge হতে থাকলে, প্রতিটা merge main-কে বদলে দিচ্ছে — শেষের PR-গুলো আসলে আর যাচাই করা state-এর বিপরীতে merge হচ্ছে না। দুটো individually-safe PR একসাথে merge হলে ব্রেক করতে পারে (একজন একটা ফাংশনের সিগনেচার বদলালো, আরেকজন সেই ফাংশন নতুনভাবে কল করলো — কেউই একা এটা ধরতে পারবে না)।
Merge Queue কীভাবে সমাধান করে
Merge queue চালু থাকলে, PR merge করার অনুরোধ এলে GitHub সাথে সাথে merge করে না — বরং queue-তে রাখে, প্রতিটা PR-কে queue-তে থাকা আগের সব PR-সহ speculative ভাবে merge করে আবার CI চালায়। শুধু সেই combined state pass করলেই আসল merge হয়। এভাবে "একা একা ঠিক, একসাথে ভুল" সমস্যা ধরা পড়ে merge হওয়ার আগেই।
রিয়েল-লাইফ উদাহরণ
৫০+ ডেভেলপারের একটা মনোরিপোতে দিনে শত শত PR merge হয় — merge queue ছাড়া, main ব্রেক হওয়ার ঝুঁকি অনেক বেশি, কারণ CI প্রতিটা PR নিজের branch-এর বিপরীতে চালায়, চূড়ান্ত merge-এর মুহূর্তের main-এর বিপরীতে না।
ট্রেড-অফ
- Merge queue-তে প্রতিটা PR-এর জন্য আবার পুরো CI চালাতে হয় — CI cost/সময় বাড়ে।
- ছোট টিমে (কম concurrent PR) এই ওভারহেড অপ্রয়োজনীয় — merge queue মূলত বড় টিম/high-velocity রিপোর জন্য কার্যকর।
সংক্ষেপে
Merge queue = প্রতিটা merge-কে "সর্বশেষ সত্যিকারের main"-এর বিপরীতে যাচাই করার একটা সিরিয়ালাইজেশন মেকানিজম, parallel-merge race condition এড়াতে।
ল্যাব: Branch Protection Rule ও CODEOWNERS সেটআপ
লক্ষ্য
একটা টেস্ট রিপোতে branch protection rule আর CODEOWNERS কনফিগার করে বাস্তবে merge ব্লক হতে দেখা।
ধাপ ১ — CODEOWNERS তৈরি
mkdir -p .github
cat > .github/CODEOWNERS << 'EOF'
*.py @<আপনার-গিটহাব-ইউজারনেম>
EOF
git add .github/CODEOWNERS
git commit -m "chore: add CODEOWNERS"
git push origin main
ধাপ ২ — Branch Protection Rule চালু করা
GitHub repo → Settings → Branches → Add rule → branch name pattern main:
- Require a pull request before merging ✅
- Require approvals: 1 ✅
- Require review from Code Owners ✅
- Require status checks to pass (যদি কোনো Actions workflow থাকে) ✅
- Require linear history ✅
ধাপ ৩ — টেস্ট PR
git checkout -b test-protection
echo "print('hi')" > test.py
git add test.py
git commit -m "test: add test.py"
git push origin test-protection
gh pr create --title "Test protection" --body "Testing" --base main
লক্ষ্য করুন: merge বাটন disabled, কারণ CODEOWNERS-এর .py owner (আপনি নিজেই) এখনো approve করেননি।
ধাপ ৪ — সরাসরি push করে দেখুন ব্লক হয় কিনা
git checkout main
echo "x = 1" >> test.py
git add test.py
git commit -m "direct commit"
git push origin main
# error: GH006: Protected branch update failed
যাচাই
Error message-টা পড়ুন — GitHub ঠিক কোন rule-এর কারণে push আটকালো তা বলে দেয়।
ইন্টারভিউ প্রশ্ন — Module 36
প্র.১ — Branch protection rule কি git-এর নিজস্ব ফিচার? উত্তর: না, এটাও সম্পূর্ণ GitHub-সাইড সেটিং। git নিজে কোনো branch-কে "protected" হিসেবে চিহ্নিত করে না — সব validation GitHub-এর সার্ভারে push গ্রহণ করার সময় হয়।
প্র.২ — "Dismiss stale approvals" কেন গুরুত্বপূর্ণ? উত্তর: এটা ছাড়া, কেউ একটা পুরনো ভার্সন approve করার পর PR author নতুন (potentially সমস্যাযুক্ত) commit push করলেও, পুরনো approval দিয়েই merge হয়ে যেতে পারে — নতুন পরিবর্তন আসলে কেউ রিভিউ করেনি।
প্র.৩ — CODEOWNERS-এ দুটো প্যাটার্ন একই ফাইলের সাথে ম্যাচ করলে কোনটা প্রযোজ্য হয়? উত্তর: ফাইলের সবচেয়ে নিচের (last) ম্যাচিং লাইন জিতে যায় — gitignore-এর মতোই "last match wins" নিয়ম।
প্র.৪ — Merge queue না থাকলে কী ধরনের বাগ চুপচাপ merge হয়ে যেতে পারে? উত্তর: দুটো individually-passing PR যেগুলো একসাথে merge হলে সংঘর্ষ করে (যেমন একজন ফাংশন সিগনেচার বদলায়, আরেকজন পুরনো সিগনেচার দিয়ে কল করে) — প্রতিটা PR নিজের branch-এর বিপরীতে CI পাস করেছে, কিন্তু চূড়ান্ত combined state-এর বিপরীতে না।
প্র.৫ — "Require linear history" চালু থাকলে কোন merge strategy ব্যবহারকারীরা reject হবে? উত্তর: সাধারণ "Create a merge commit" স্ট্র্যাটেজি — শুধু squash আর rebase-merge অনুমোদিত থাকবে, কারণ এই দুটোই কখনো merge commit তৈরি করে না।
কর্নার কেস — Module 36
- Repo admin ডিফল্টে branch protection rule bypass করতে পারে, যদি না "Include administrators" আলাদা করে চালু করা হয় — অনেকে ভাবেন rule সবার জন্য সমান, আসলে না।
- CODEOWNERS ফাইলে টাইপো (ভুল username/team) থাকলে GitHub নীরবে সেই লাইন ইগনোর করে, কোনো error দেখায় না — PR-এ owner assign না হওয়া পর্যন্ত বোঝা কঠিন।
- Force-push সক্ষম থাকলে branch protection-এর "linear history" আর "required review" বাইপাস করা প্রায় অসম্ভব করার জন্য "Do not allow force pushes" আলাদাভাবে চালু করতে হয় — শুধু required review চালু করলেই force-push আটকায় না।
- Merge queue-তে ঢোকা একটা PR যদি অন্য একটা PR-এর কারণে ফেইল করে, সেটা queue থেকে বাদ পড়ে যায় এবং আবার সাবমিট করতে হয় — এটা মাঝেমধ্যে বিভ্রান্তিকর মনে হয়, মনে হয় নিজের PR-এই সমস্যা।
- "Require status checks" চালু করার সময় নির্দিষ্ট check-এর নাম দিতে হয়, আর সেই workflow অন্তত একবার চলে থাকা লাগে — নাহলে GitHub-এর dropdown-এ সেই check নামটাই দেখা যায় না, ফলে নতুন workflow যোগ করার পর branch protection সেটিংসে গিয়ে আবার সেটা enable করে দিতে ভুলে যাওয়া একটা সাধারণ ভুল।
MODULE 37: GitHub Actions বেসিক
GitHub Actions হলো GitHub-এর হোস্টেড CI/CD সিস্টেম — repo-তে ঘটা ইভেন্টে অটোমেটেড workflow ট্রিগার করে। এই মডিউলে workflow YAML-এর বেসিক গঠন, ট্রিগার টাইপ, secrets/GITHUB_TOKEN, আর একটা মিনিমাল CI পাইপলাইন কভার হবে, সাথে local git hooks বনাম hosted Actions-এর পার্থক্য স্পষ্ট করা হবে। এই মডিউল শেষে Phase 11-এর একটা checkpoint আছে — team-collaboration দক্ষতার পুরো আর্কটা এখানেই সম্পূর্ণ হয়।
Workflow YAML-এর বেসিক গঠন
ফাইল লোকেশন
GitHub Actions workflow ফাইল থাকে .github/workflows/*.yml-এ — প্রতিটা .yml ফাইল একটা স্বতন্ত্র workflow।
বেসিক স্ট্রাকচার
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: pytest
প্রতিটা কী-এর মানে
- name — workflow-এর নাম, GitHub UI-তে দেখায়।
- on — কোন ইভেন্টে ট্রিগার হবে (পরের leaf-এ বিস্তারিত)।
- jobs — এক বা একাধিক job, প্রতিটা একটা আলাদা virtual machine (runner)-এ চলে, ডিফল্টে parallel।
- runs-on — কোন OS/runner-এ চলবে (
ubuntu-latest,windows-latest,macos-latest, বা self-hosted)। - steps — একটা job-এর ভেতরে ধারাবাহিক ধাপ;
usesদিয়ে reusable action চালানো যায় (যেমনactions/checkout— repo-র কোড চেকআউট করে),runদিয়ে shell command চালানো যায়।
actions/checkout কেন প্রয়োজন
গুরুত্বপূর্ণ বোঝাপড়া: runner ভার্চুয়াল মেশিন খালি অবস্থায় শুরু হয় — আপনার repo-র কোড সেখানে অটোমেটিক থাকে না। actions/checkout@v4 স্টেপটা আসলে ভেতরে git clone/git fetch চালিয়ে সেই commit-টা checkout করে যেটার জন্য workflow ট্রিগার হয়েছে।
রিয়েল-লাইফ উদাহরণ
Rakib-এর Django প্রজেক্টে প্রতিটা push-এ pytest অটোমেটিক চালানোর জন্য ওপরের মতো একটা মিনিমাল workflow যথেষ্ট — এটাই সবচেয়ে সাধারণ ব্যবহার।
ট্রিগার: push, pull_request, schedule, workflow_dispatch
চারটা সাধারণ ট্রিগার
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: "0 3 * * *" # প্রতিদিন রাত ৩টায় (UTC)
workflow_dispatch: # ম্যানুয়াল ট্রিগার
inputs:
environment:
description: "Deploy target"
required: true
পার্থক্য ও ব্যবহার
- push — কোনো branch-এ commit push হলে চলে। CI (test/lint/build)-এর জন্য সবচেয়ে সাধারণ।
- pull_request — PR খোলা/আপডেট হলে চলে; push-এর চেয়ে গুরুত্বপূর্ণ পার্থক্য — এটা PR-এর merge commit-এর সিমুলেশনের বিপরীতে চলে (base + head-এর একটা temporary merge), তাই base branch-এর সাথে সংঘর্ষ ধরা পড়ে যা শুধু push ট্রিগারে ধরা পড়ত না।
- schedule — cron সিনট্যাক্সে নির্দিষ্ট সময়ে চলে; nightly build, dependency scan, স্টেল issue ক্লিনআপের জন্য ব্যবহৃত।
- workflow_dispatch — GitHub UI বা
gh workflow run-এর মাধ্যমে ম্যানুয়ালি ট্রিগার করা; deployment workflow-এর জন্য common, যেখানে কেউ ইচ্ছাকৃতভাবে বাটনে ক্লিক করে ডিপ্লয় শুরু করবে।
রিয়েল-লাইফ উদাহরণ
gh workflow run deploy.yml -f environment=staging
এটা workflow_dispatch ট্রিগার করবে, environment ইনপুট সহ — Rakib যদি staging-এ ম্যানুয়াল ডিপ্লয় চালাতে চান কোনো নতুন কোড push না করেই।
কর্নার কেস
একই ফাইলে একাধিক ট্রিগার থাকতে পারে (on: [push, pull_request]), কিন্তু তখন সতর্ক থাকতে হয় — একই কমিটের জন্য দুইবার (push দিয়ে এবং pull_request দিয়ে) workflow চলতে পারে, ডুপ্লিকেট CI রান আর খরচ বাড়িয়ে দেয়।
Secrets ও GITHUB_TOKEN
Secrets কী
API key, database password-এর মতো sensitive value plaintext-এ workflow YAML-এ লেখা যাবে না (repo public/private যাই হোক) — GitHub-এ Settings → Secrets and variables → Actions-এ encrypted secret সংরক্ষণ করা হয়, workflow থেকে রেফারেন্স করা হয়:
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
run: ./deploy.sh
GITHUB_TOKEN — স্বয়ংক্রিয়ভাবে তৈরি
প্রতিটা workflow run-এর জন্য GitHub স্বয়ংক্রিয়ভাবে একটা সাময়িক GITHUB_TOKEN তৈরি করে দেয় (আলাদা করে সেট করতে হয় না) — এটা repo-র বিরুদ্ধে API কল করার (issue কমেন্ট করা, PR-এ লেবেল যোগ করা, রিলিজ তৈরি করা) অনুমতি দেয়, run শেষ হলেই token বাতিল হয়ে যায়।
steps:
- name: Comment on PR
run: gh pr comment ${{ github.event.number }} --body "CI passed!"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
নিরাপত্তা সতর্কতা
- Secret কখনো run কমান্ডে সরাসরি লগ করবেন না (
echo $API_KEY) — GitHub log-এ mask করার চেষ্টা করে, কিন্তু নিশ্চিত না, বিশেষত string manipulation-এর পর। - Fork থেকে আসা PR-এ secrets ডিফল্টে উপলব্ধ না (pull_request ট্রিগারে) — এটা ইচ্ছাকৃত নিরাপত্তা ব্যবস্থা, যাতে কেউ ম্যালিশাস PR পাঠিয়ে আপনার secrets চুরি করতে না পারে। pull_request_target ব্যবহার করলে secrets পাওয়া যায়, কিন্তু এটা সতর্কতার সাথে ব্যবহার করতে হয়।
রিয়েল-লাইফ উদাহরণ
Rakib-এর প্রজেক্টে production deploy workflow-এ AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY secret হিসেবে রাখা আছে — কোড রিভিউতে বা git log-এ এই ভ্যালু কখনো দৃশ্যমান হয় না।
Git Hooks বনাম GitHub Actions — লোকাল vs হোস্টেড, কখন কোনটা
মৌলিক পার্থক্য
| Git Hooks (আগের ফেজে শেখা) | GitHub Actions | |
|---|---|---|
| চলে কোথায় | ডেভেলপারের নিজের মেশিনে, লোকালি | GitHub-এর হোস্টেড/self-hosted runner-এ, সার্ভারে |
| কে বাইপাস করতে পারে | যে কেউ --no-verify দিয়ে, বা hook ফাইলই ডিলিট করে | কেউ না (branch protection-এর সাথে মিলিয়ে ব্যবহার করলে) |
| কখন চলে | লোকাল git অপারেশনের মুহূর্তে (commit, push, ইত্যাদি) | Repo-তে ইভেন্ট ঘটলে (push, PR, schedule) |
| distribution | .git/hooks/-এ, ডিফল্টে repo-র সাথে sync হয় না (Husky-জাতীয় টুল লাগে) | .github/workflows/-এ, সরাসরি repo-র সাথে version-controlled |
| নির্ভরযোগ্যতা (enforcement) | দুর্বল — লোকাল, বাইপাসযোগ্য | শক্তিশালী — কেন্দ্রীয়, branch protection দিয়ে বাধ্যতামূলক করা যায় |
কখন কোনটা
- Git hooks: দ্রুত, লোকাল ফিডব্যাক চান (commit করার আগেই lint error ধরা), অথবা offline/দ্রুত ইটারেশনে কাজ করছেন। কিন্তু এটা কখনো নিরাপত্তা/গভর্নেন্স গ্যারান্টি হতে পারে না — যে কেউ বাইপাস করতে পারে।
- GitHub Actions: টিম-ওয়াইড, বাধ্যতামূলক এনফোর্সমেন্ট চান (সবার কোড অবশ্যই টেস্ট পাস করবে, কেউ বাইপাস করতে পারবে না) — branch protection-এর "required status check"-এর সাথে মিলিয়ে ব্যবহার করলে এটাই আসল গেটকিপার।
সেরা প্র্যাকটিস
দুটোই একসাথে ব্যবহার করা — pre-commit hook দিয়ে দ্রুত লোকাল ফিডব্যাক (lint, format), আর GitHub Actions দিয়ে চূড়ান্ত, বাইপাস-অযোগ্য enforcement (test suite, security scan)। এভাবে ডেভেলপার দ্রুত ফিডব্যাক পান, আর টিম নিশ্চিত থাকে কিছুই ফাঁক দিয়ে গলে যাবে না।
রিয়েল-লাইফ উদাহরণ
Rakib-এর টিমে pre-commit hook black/ruff চালায় (লোকাল, দ্রুত), আর GitHub Actions পুরো pytest suite + mypy type-check চালায় (ধীর কিন্তু বাধ্যতামূলক, branch protection দিয়ে merge ব্লক করে ফেইল হলে)।
ল্যাব: Push হলে টেস্ট রান করা একটা মিনিমাল CI Workflow
লক্ষ্য
একটা কাজ করা GitHub Actions workflow তৈরি করা যা push-এ অটোমেটিক টেস্ট চালায়, এবং সেটাকে branch protection-এর সাথে যুক্ত করে merge গেটকিপার বানানো।
ধাপ ১ — একটা সাধারণ টেস্ট তৈরি করুন
mkdir -p tests
cat > tests/test_basic.py << 'EOF'
def test_addition():
assert 1 + 1 == 2
EOF
pip install pytest
ধাপ ২ — Workflow ফাইল লিখুন
mkdir -p .github/workflows
cat > .github/workflows/ci.yml << 'EOF'
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pytest
- run: pytest
EOF
ধাপ ৩ — Push করে দেখুন
git add .github/workflows/ci.yml tests/
git commit -m "ci: add pytest workflow"
git push origin main
GitHub repo-র "Actions" ট্যাবে গিয়ে workflow রান হতে দেখুন — সবুজ চেকমার্ক এলে সফল।
ধাপ ৪ — ইচ্ছাকৃতভাবে ভাঙুন
git checkout -b break-test
echo "def test_fail(): assert False" >> tests/test_basic.py
git add tests/test_basic.py
git commit -m "test: intentionally broken test"
git push origin break-test
gh pr create --title "Break test" --base main
PR-এ লাল ক্রস দেখুন — এবং যদি আগের মডিউলে branch protection-এ "require status checks" চালু করে থাকেন, merge বাটন disabled থাকবে।
যাচাই
Actions ট্যাবে ব্যর্থ run-এর লগ খুলে দেখুন ঠিক কোন assertion ফেইল করেছে — real-life debugging-এর ঠিক এই প্রসেসটাই অনুসরণ করা হয়।
ইন্টারভিউ প্রশ্ন — Module 37
প্র.১ — GitHub Actions workflow ফাইল কোথায় রাখতে হয়, এবং কেন সেই লোকেশন? উত্তর: .github/workflows/*.yml-এ — GitHub এই নির্দিষ্ট পাথ স্ক্যান করে workflow খুঁজে বের করে; এটা একটা কনভেনশন, git-এর নিজস্ব কোনো নিয়ম না।
প্র.২ — push আর pull_request ট্রিগারের মধ্যে মূল পার্থক্য কী? উত্তর: pull_request ট্রিগার base ও head branch-এর একটা temporary merge-এর বিপরীতে চলে, তাই merge conflict/সংঘর্ষ যা শুধু push-এ ধরা পড়ত না তা এখানে ধরা পড়ে।
প্র.৩ — GITHUB_TOKEN কোথা থেকে আসে, এটা কি ম্যানুয়ালি সেট করতে হয়? উত্তর: না, GitHub প্রতিটা workflow run-এর জন্য স্বয়ংক্রিয়ভাবে একটা সাময়িক token তৈরি করে দেয়, run শেষে সেটা বাতিল হয়ে যায়।
প্র.৪ — একটা fork থেকে আসা PR-এ secrets ডিফল্টে উপলব্ধ থাকে না কেন? উত্তর: নিরাপত্তার জন্য — যদি secrets উপলব্ধ থাকত, কেউ একটা ম্যালিশাস PR পাঠিয়ে workflow-এর মধ্যে secret চুরির চেষ্টা করতে পারত।
প্র.৫ — Git hooks বাইপাসযোগ্য, তাহলে টিম-ওয়াইড বাধ্যতামূলক এনফোর্সমেন্টের জন্য কী ব্যবহার করা উচিত? উত্তর: GitHub Actions-এর required status check, branch protection rule-এর সাথে মিলিয়ে — এটা সার্ভার-সাইড, কেউ বাইপাস করতে পারে না, যেখানে local hook --no-verify দিয়ে সহজেই এড়ানো যায়।
প্র.৬ — schedule আর workflow_dispatch ট্রিগারের পার্থক্য কী? উত্তর: schedule cron-ভিত্তিক স্বয়ংক্রিয় সময়ে চলে (nightly build), workflow_dispatch একজন মানুষ ম্যানুয়ালি UI/CLI থেকে ট্রিগার করে (deployment)।
কর্নার কেস — Module 37
- runs-on: ubuntu-latest মানে প্রতিটা রান একই ইমেজ পাবে এই গ্যারান্টি নেই — GitHub মাঝেমধ্যে "latest"-কে নতুন Ubuntu ভার্সনে পয়েন্ট করে বদলে দেয়, ফলে পুরনো workflow হঠাৎ ব্রেক করতে পারে; production pipeline-এ নির্দিষ্ট ভার্সন পিন করা (ubuntu-22.04) নিরাপদ।
- Secrets মাস্কিং শুধু exact-match লগ আউটপুটে কাজ করে — secret ভ্যালুকে base64-encode বা substring করে প্রিন্ট করলে GitHub সেটা মাস্ক নাও করতে পারে, ফলে দুর্ঘটনাক্রমে leak হতে পারে।
- pull_request_target ট্রিগার secrets দেয়, কিন্তু base branch-এর কোড checkout করে, PR-এর কোড না — অনেকে ভুল করে PR-এর head কোড checkout করে ফেলেন actions/checkout@v4 দিয়ে ref সেট করে, যা একটা গুরুতর নিরাপত্তা ঝুঁকি তৈরি করে (ম্যালিশাস PR কোড secrets-সহ চলতে পারে)।
- Workflow ফাইলে সিনট্যাক্স এরর থাকলে GitHub সাইলেন্টলি পুরো workflow স্কিপ করে দিতে পারে, কোনো স্পষ্ট নোটিফিকেশন ছাড়াই — "Actions" ট্যাবে গিয়ে workflow run history চেক করাই একমাত্র উপায় বোঝার।
চেকপয়েন্ট — Phase 11 সম্পন্ন
নিজেকে যাচাই করুন
Phase 11 শেষ — এটাই ছিল জার্নির "team practice" আর্কের চূড়ান্ত ধাপ: pure git দক্ষতার (আগের ১০ Phase) ওপর platform-level collaboration fluency যোগ করা। নিচের প্রশ্নগুলো নিজেকে জিজ্ঞেস করুন:
- আমি কি স্পষ্টভাবে বলতে পারি fork, PR, branch protection, GitHub Actions — এই চারটার একটাও core git কনসেপ্ট না, এগুলো GitHub-এর প্ল্যাটফর্ম লেয়ার?
- আমি কি নিজে fork করে, upstream remote যোগ করে, নিয়মিত sync রাখতে পারি এবং stale fork কেন সমস্যা তৈরি করে তা ব্যাখ্যা করতে পারি?
- আমি কি merge commit, squash, আর rebase-merge — তিনটা PR merge strategy-র history-র ওপর প্রভাব একজন সহকর্মীকে ব্যাখ্যা করতে পারি?
- আমি কি required review, required status check, CODEOWNERS, আর merge queue দিয়ে একটা টিমের জন্য একটা যুক্তিসঙ্গত governance policy ডিজাইন করতে পারি?
- আমি কি একটা মিনিমাল GitHub Actions CI workflow শূন্য থেকে লিখতে পারি, এবং git hooks বনাম Actions-এর মধ্যে কখন কোনটা ব্যবহার করা উচিত তা ব্যাখ্যা করতে পারি?
- আমি কি secrets আর GITHUB_TOKEN-এর নিরাপদ ব্যবহার বুঝি, বিশেষত fork থেকে আসা PR-এর ক্ষেত্রে সতর্কতা?
উপরের প্রতিটায় "হ্যাঁ" বলতে পারলে, আপনি এখন শুধু একজন git ব্যবহারকারী না — একজন পূর্ণাঙ্গ GitHub-নেটিভ টিম কন্ট্রিবিউটর। বাকি আছে শুধু Phase 12 — বাস্তব জরুরি অবস্থার প্লেবুক আর চূড়ান্ত ক্যাপস্টোন।
PHASE 12: ট্রাবলশুটিং প্লেবুক ও ক্যাপস্টোন
এটাই জার্নির শেষ Phase — প্রথমে বাস্তব প্রোডাকশন প্যানিক মুহূর্তের জন্য একটা ready-made playbook (ভুল branch, force-push দুর্ঘটনা, corrupt repo, diverged history), তারপর merge-vs-rebase-এর একটা টিম-লেভেল সিদ্ধান্ত ফ্রেমওয়ার্ক, একটা সম্পূর্ণ কমান্ড চিটশিট, আর সবশেষে একটা ক্যাপস্টোন — যেখানে conflict resolution, interactive rebase, reflog recovery, secret removal, আর PR workflow একসাথে, একটা সিমুলেটেড বাস্তব repo-তে প্রয়োগ করতে হবে। এটাই পুরো ৩৯ মডিউলের চূড়ান্ত পরীক্ষা।
MODULE 38: ইমার্জেন্সি সিনারিও ওয়াকথ্রু
বাস্তব প্রোডাকশন কাজে git নিয়ে সবচেয়ে বেশি প্যানিক হয় ঠিক এই মুহূর্তগুলোতে — ভুল branch-এ commit, মাঝপথে conflict, ভুল force-push, CRLF সমস্যা, detached HEAD, corrupt repo, বা diverged history। এই মডিউলে সাতটা বাস্তব ইমার্জেন্সি সিনারিও প্রতিটাকে সমস্যা → ডায়াগনোসিস → সমাধান → ভেরিফিকেশন — এই ফরম্যাটে ধাপে ধাপে সমাধান করা হবে, যাতে আসল প্যানিক মুহূর্তে মাথা ঠান্ডা রেখে সঠিক কমান্ড মনে করা যায়।
সিনারিও ১: ভুল Branch-এ Commit করে ফেলা
সমস্যা
আপনি main-এ থাকা অবস্থায় ভুলে একটা feature-এর কাজ করে commit করে ফেলেছেন — অথচ এটা feature/login-এ হওয়ার কথা ছিল।
ডায়াগনোসিস
git log --oneline -3
git branch --show-current
# main <- ভুল জায়গায় আছেন
সমাধান — commit এখনো push হয়নি
git branch feature/login # বর্তমান commit-সহ নতুন branch তৈরি
git reset --hard origin/main # main-কে আগের অবস্থায় ফিরিয়ে আনা
git checkout feature/login # সঠিক branch-এ চলে যাওয়া
ভেতরে কী হচ্ছে
git branch feature/login কমান্ড শুধু বর্তমান HEAD পয়েন্ট করা commit-এ একটা নতুন ref তৈরি করে — কোনো ডেটা কপি হয় না, commit অবজেক্ট আগে থেকেই আছে, শুধু একটা নতুন নাম যোগ হলো। তারপর main-কে origin/main-এ reset করে দিলে main ref আবার আগের commit-এ ফিরে যায় — ভুল commit-টা এখন শুধু feature/login-এ বেঁচে থাকে।
যদি একাধিক commit ভুল জায়গায় হয়ে থাকে
git branch feature/login # সব commit-সহ branch তৈরি
git reset --hard origin/main
যদি commit ইতিমধ্যে push হয়ে গেছে
git checkout -b feature/login
git push origin feature/login
git checkout main
git reset --hard origin/main # লোকাল main ঠিক করা
# কিন্তু remote main-এ যদি push হয়ে থাকে, সেটা আলাদাভাবে revert/force-push দরকার — সতর্ক থাকুন, এটা shared history
ভেরিফিকেশন
git log --oneline main
git log --oneline feature/login
main এখন পরিষ্কার, feature/login-এ কাজ নিরাপদ।
সিনারিও ২: মাঝপথে Conflict প্যানিক
সমস্যা
git merge বা git rebase চালানোর পর টার্মিনালে conflict marker (<<<<<<<, =======, >>>>>>>) দেখে প্যানিক — মনে হয় সব নষ্ট হয়ে গেছে।
ডায়াগনোসিস — মাথা ঠান্ডা রাখার প্রথম ধাপ
git status
# both modified: src/app.py
git status সবসময় বলে দেয় কোন ফাইলে conflict আছে — প্যানিক না করে প্রথমে এটা পড়ুন।
সমাধানের ধাপ
- প্রতিটা conflicted ফাইল খুলুন,
<<<<<<< HEADথেকে=======পর্যন্ত আপনার (বা current branch-এর) ভার্সন,=======থেকে>>>>>>>পর্যন্ত incoming ভার্সন — কোনটা রাখবেন সিদ্ধান্ত নিন (দুটোই, একটা, বা মিশ্রণ)। - Marker লাইনগুলো সম্পূর্ণ মুছে ফেলুন।
git add src/app.py- Merge-এর ক্ষেত্রে:
git commit(merge commit সম্পূর্ণ হবে)। Rebase-এর ক্ষেত্রে:git rebase --continue।
যদি পুরোপুরি আটকে যান
git merge --abort # merge-এর ক্ষেত্রে, conflict শুরুর আগের অবস্থায় ফিরে যাওয়া
git rebase --abort # rebase-এর ক্ষেত্রে
এই দুটো কমান্ড সম্পূর্ণ নিরাপদ — কোনো ডেটা হারায় না, শুধু conflict resolution প্রক্রিয়া বাতিল করে আগের clean অবস্থায় ফিরিয়ে দেয়। এটাই সবচেয়ে গুরুত্বপূর্ণ শেখা: প্যানিক করার আগে মনে রাখুন --abort সবসময় আছে।
ভেরিফিকেশন
git status # "nothing to commit, working tree clean" বা conflict-মুক্ত অবস্থা
git diff origin/main # ফলাফল প্রত্যাশিত কিনা যাচাই
মূল শিক্ষা
Conflict মানে ডেটা হারানো না — git শুধু বলছে "দুটো পরিবর্তন একই জায়গায়, তুমি সিদ্ধান্ত নাও"। --abort সবসময় একটা নিরাপদ প্রস্থান পথ।
সিনারিও ৩: Force-push-এ টিমমেটের কাজ Overwrite — রিকভারি প্রোটোকল
সমস্যা
আপনি git push --force origin main চালিয়েছেন, আর পরে বুঝলেন এটা টিমমেটের নতুন commit মুছে দিয়েছে — remote-এ তাদের কাজ এখন আর দেখা যাচ্ছে না।
ডায়াগনোসিস
git reflog show origin/main # লোকাল reflog-এ remote-tracking-এর ইতিহাস (যদি আগে fetch করা থাকে)
তবে আসল উদ্ধারের জায়গা হলো যার commit হারিয়ে গেছে তার লোকাল reflog, কারণ reflog সম্পূর্ণ লোকাল — remote force-push সেই ব্যক্তির লোকাল .git/logs/HEAD-কে স্পর্শ করে না।
রিকভারি প্রোটোকল
ধাপ ১ — যার কাজ হারিয়েছে তাকে সাথে সাথে জিজ্ঞেস করুন:
git reflog
# তাদের হারানো commit hash খুঁজে বের করুন
ধাপ ২ — সেই commit থেকে একটা recovery branch তৈরি করুন (তাদের মেশিনে):
git branch recovered-work <হারানো-commit-hash>
git push origin recovered-work
ধাপ ৩ — main-এ সঠিকভাবে merge করুন:
git checkout main
git pull origin main
git merge recovered-work
git push origin main
যদি কারো লোকাল reflog-ও না থাকে
GitHub-এ ৯০ দিন পর্যন্ত "unreachable" commit maintain করতে পারে server-side (garbage collection-এর আগে) — GitHub Support-কে যোগাযোগ করা শেষ অবলম্বন, তবে গ্যারান্টি নেই।
ভবিষ্যতে প্রতিরোধ
git push --force-with-lease origin main
--force-with-lease remote-এ আপনার শেষবার fetch করা অবস্থার সাথে তুলনা করে — যদি remote-এ এর মাঝে অন্য কেউ নতুন commit push করে থাকে, push reject হয়ে যাবে, পুরনো blind --force-এর মতো silently overwrite করবে না। Branch protection-এ "Do not allow force pushes" চালু রাখাই সবচেয়ে নিরাপদ, যা আগের মডিউলে শেখা হয়েছে।
ভেরিফিকেশন
git log --oneline main | grep <হারানো-commit-এর-অংশ>
সিনারিও ৪: CRLF/LF Cross-platform সমস্যা
সমস্যা
Windows টিমমেট আর Linux/Mac টিমমেট একই repo-তে কাজ করছেন — git diff পুরো ফাইল বদলে গেছে দেখাচ্ছে যদিও শুধু এক লাইন পরিবর্তন করা হয়েছে, অথবা git status অকারণে সব ফাইল "modified" দেখাচ্ছে।
ডায়াগনোসিস
git diff --stat
# হাজার হাজার লাইন পরিবর্তিত দেখাচ্ছে, যদিও আপনি একটা শব্দ বদলেছেন
file src/app.py
# ASCII text, with CRLF line terminators <- এখানেই সমস্যা
Windows লাইন-এন্ডিং হিসেবে \r\n (CRLF) ব্যবহার করে, Linux/Mac শুধু \n (LF)। git character-by-character diff করে, তাই একটা ফাইলের সব লাইনের এন্ডিং বদলে গেলে git পুরো ফাইলকেই "বদলে গেছে" মনে করে।
সমাধান — .gitattributes
cat > .gitattributes << 'EOF'
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
EOF
git add .gitattributes
git add --renormalize .
git commit -m "fix: normalize line endings"
text=auto git-কে বলে দেয় টেক্সট ফাইল সনাক্ত করে repo-তে সবসময় LF-এ normalize করে রাখতে (checkout-এর সময় প্রয়োজনে OS-নির্দিষ্ট এন্ডিং-এ কনভার্ট করবে)। git add --renormalize . ইতিমধ্যে থাকা ফাইলগুলোতেও এই নিয়ম প্রয়োগ করে।
core.autocrlf — লোকাল সেটিং হিসেবে বিকল্প
git config --global core.autocrlf true # Windows-এ: checkout-এ LF→CRLF, commit-এ CRLF→LF
git config --global core.autocrlf input # Linux/Mac-এ: commit-এ CRLF→LF, checkout-এ কিছু করে না
.gitattributes বেশি নির্ভরযোগ্য কারণ এটা repo-র সাথে commit হয় — সব টিমমেট একই নিয়ম পায়, ব্যক্তিগত core.autocrlf সেটিং-এর ওপর নির্ভর করে না।
ভেরিফিকেশন
git diff --stat # এখন শুধু আসল পরিবর্তিত লাইন দেখাবে
সিনারিও ৫: Detached HEAD-এ কাজ করে ফেলার পর Commit বাঁচানো
সমস্যা
git checkout a1b2c3d # একটা নির্দিষ্ট পুরনো commit checkout করেছেন
# You are in 'detached HEAD' state
এই অবস্থায় কয়েক ঘণ্টা কাজ করে কয়েকটা commit করে ফেলেছেন — এখন বুঝলেন কোনো branch এই commit-গুলো ট্র্যাক করছে না, git checkout main করলে এগুলো "হারিয়ে" যাবে বলে মনে হচ্ছে।
ডায়াগনোসিস
git status
# HEAD detached at a1b2c3d
git log --oneline -5
# নতুন commit-গুলো এখানে দেখা যাচ্ছে, কিন্তু কোনো branch নাম নেই
কেন এগুলো আসলে হারায়নি (এখনো)
Detached HEAD state-এ করা commit সম্পূর্ণ বৈধ git commit অবজেক্ট — শুধু কোনো branch ref সেগুলোকে পয়েন্ট করছে না। যতক্ষণ git checkout করে অন্য কোথাও সরে না যাচ্ছেন, HEAD নিজেই সেই commit পয়েন্ট করে আছে। কিন্তু সরে গেলে, এই commit-গুলো "unreachable" হয়ে যাবে (কোনো branch/tag থেকে পৌঁছানো যাবে না) — reflog ছাড়া হারিয়ে যাওয়ার মতোই।
সমাধান — সরে যাওয়ার আগেই
git branch rescue-work
# অথবা সরাসরি:
git checkout -b rescue-work
এখন এই commit-গুলো rescue-work branch-এর অংশ, নিরাপদ।
যদি ইতিমধ্যে সরে গেছেন
git reflog
# a1b2c3d HEAD@{3}: commit: my detached work
git branch rescue-work a1b2c3d
ভেরিফিকেশন
git log --oneline rescue-work -5
git branch --show-current # rescue-work
মূল শিক্ষা
Detached HEAD মানে "ভয়ংকর অবস্থা" না — এটা শুধু "এই মুহূর্তে কোনো branch label নেই" মানে। commit করার আগেই git checkout -b <নাম> দিয়ে একটা branch তৈরি করে ফেলাই সবচেয়ে নিরাপদ অভ্যাস।
সিনারিও ৬: Corrupt Repo রিপেয়ার (fsck + repack)
সমস্যা
git status
# error: object file .git/objects/4a/f2c1... is empty
# fatal: loose object 4af2c1... is corrupt
ডিস্ক ফুল হয়ে যাওয়া, হঠাৎ পাওয়ার লস, বা ফাইল সিস্টেম সমস্যার কারণে .git ডিরেক্টরির ভেতরের object ফাইল corrupt হতে পারে।
ডায়াগনোসিস — git fsck
git fsck --full
# error: object file ... is empty
# missing blob 8f3e21...
# dangling commit 9a1b2c...
git fsck (filesystem check) পুরো object database ঘুরে দেখে — প্রতিটা tree/commit/blob-এর checksum যাচাই করে, broken reference, corrupt/missing object, বা dangling (কোনো ref থেকে reachable না এমন) object চিহ্নিত করে।
সমাধান-এর ধাপ
যদি একটা রিমোট/টিমমেটের কাছে ভালো কপি থাকে (সবচেয়ে সহজ পথ):
cd ..
git clone <remote-url> repo-fresh
# corrupt repo থেকে যেকোনো uncommitted/unpushed কাজ ম্যানুয়ালি কপি করে আনুন
যদি corruption ছোট (কিছু loose object) এবং backup না থাকে:
git fsck --full --unreachable # কোন object-গুলো এখনো উদ্ধারযোগ্য দেখুন
git prune # সত্যিকারের অব্যবহৃত অবজেক্ট পরিষ্কার (সতর্কতার সাথে)
git gc --aggressive # loose object-গুলো নতুন করে packfile-এ সংকুচিত করে
git repack -a -d # সব object আবার একটা packfile-এ পুনর্গঠন
repack সব loose ও pack object পুনরায় পড়ে নতুন, consistent packfile তৈরি করার চেষ্টা করে — কিছু ধরনের হালকা corruption এতে সেরে যেতে পারে যদি অন্তর্নিহিত ডেটা এখনো আংশিক পড়া যায়।
ভেরিফিকেশন
git fsck --full
# কোনো error না এলে repo সুস্থ
git log --oneline -5 # history স্বাভাবিকভাবে দেখা যাচ্ছে কিনা
মূল শিক্ষা
Corrupt repo-র সবচেয়ে নির্ভরযোগ্য "ফিক্স" হলো একটা ভালো রিমোট/ক্লোন থেকে আবার শুরু করা — fsck/repack দিয়ে ইন-প্লেস রিপেয়ার শুধু তখনই কাজে দেয় যখন ভালো কপি হাতের কাছে নেই।
সিনারিও ৭: Diverged Branch — Merge/Rebase/Force সিদ্ধান্ত গাছ
সমস্যা
git pull origin main
# hint: You have divergent branches and need to specify how to reconcile them
আপনার লোকাল main-এ ২টা নতুন commit আছে (push হয়নি), আর remote main-এও ৩টা নতুন commit (অন্য কেউ push করেছে) — দুই দিকেই ইতিহাস এগিয়েছে, একটা কমন পূর্বপুরুষ থেকে diverge করেছে।
ডায়াগনোসিস
git fetch origin
git log --oneline --graph --left-right main...origin/main
# < আপনার ২টা commit
# > remote-এর ৩টা commit
সিদ্ধান্ত গাছ
diverged branch পাওয়া গেছে
|
+-- আপনার commit কি ইতিমধ্যে অন্য কেউ pull করেছে/দেখেছে?
| +-- হ্যাঁ (shared) -> merge ব্যবহার করুন (rewrite করবেন না)
| | git merge origin/main
| | git push origin main
| |
| +-- না (শুধু আপনার লোকাল, কেউ দেখেনি)
| -> rebase করুন (পরিষ্কার, লিনিয়ার history)
| git rebase origin/main
| git push origin main
|
+-- আপনি কি নিশ্চিত remote-এর commit-গুলো ভুল/অপ্রয়োজনীয়?
-> এটা বিরল ও বিপজ্জনক কেস
যাচাই করুন commit-গুলো আসলে কী, টিমের সাথে কথা বলুন
নিশ্চিত হলেই শুধু: git push --force-with-lease
(কখনো blind --force ব্যবহার করবেন না — সিনারিও ৩ দেখুন)
কেন এই সিদ্ধান্ত গুরুত্বপূর্ণ
Merge ব্যবহার করলে দুই দিকের commit-ই অক্ষত থাকে, একটা নতুন merge commit দিয়ে একত্রিত হয় — shared history-র জন্য নিরাপদ (rewrite হয় না)। Rebase আপনার commit-গুলোকে remote-এর টিপে replay করে, নতুন hash তৈরি করে — যদি আপনার commit ইতিমধ্যে কেউ দেখে ফেলে থাকে, rebase তাদের জন্য history rewrite সমস্যা তৈরি করবে (আগের ফেজের "golden rule"-এর পুনরাবৃত্তি)।
ভেরিফিকেশন
git log --oneline --graph -10
git status # "Your branch is up to date with origin/main"
ল্যাব: পাঁচটা ইমার্জেন্সি সিনারিও নিজে সিমুলেট করে সমাধান করা
লক্ষ্য
একটা টেস্ট repo-তে ইচ্ছাকৃতভাবে সমস্যা তৈরি করে, নিজে ডায়াগনোসিস আর সমাধান করে হাতে-কলমে মুখস্থ করা।
সেটআপ
mkdir git-emergency-lab && cd git-emergency-lab
git init
echo "v1" > file.txt && git add . && git commit -m "initial"
সিমুলেশন ১ — ভুল branch-এ commit
# main-এ থেকেই কাজ করুন, পরে branch + reset --hard দিয়ে ঠিক করুন (সিনারিও ১ অনুসরণ করুন)
সিমুলেশন ২ — Detached HEAD
FIRST=$(git log --oneline | tail -1 | cut -d' ' -f1)
git checkout $FIRST
echo "detached work" >> file.txt
git commit -am "work done in detached HEAD"
# এখন rescue-work branch দিয়ে উদ্ধার করুন (সিনারিও ৫)
সিমুলেশন ৩ — CRLF সমস্যা
printf "line1\r\nline2\r\n" > windows-file.txt
git add windows-file.txt && git commit -m "add windows file"
# .gitattributes দিয়ে normalize করুন (সিনারিও ৪)
সিমুলেশন ৪ — Diverged branch (দুইটা ক্লোন ব্যবহার করে)
cd .. && git clone git-emergency-lab clone-b
cd git-emergency-lab && echo "a" >> file.txt && git commit -am "from repo A"
cd ../clone-b && echo "b" >> file.txt && git commit -am "from repo B"
git push origin main # প্রথমবার সফল
cd ../git-emergency-lab && git push origin main # rejected - diverged!
git pull origin main # decision tree অনুসরণ করে সমাধান করুন
যাচাই
প্রতিটা সিমুলেশনের পর git log --oneline --graph --all চালিয়ে history-র shape পর্যবেক্ষণ করুন — বুঝুন প্রতিটা সমাধান কমান্ড ঠিক কীভাবে graph বদলে দিলো।
চ্যালেঞ্জ
git fsck --full চালিয়ে দেখুন এই ল্যাব repo-তে কোনো dangling commit আছে কিনা (থাকবে — detached HEAD আর reset-করা commit থেকে) — এগুলো git gc চালানোর আগে পর্যন্ত উদ্ধারযোগ্য থাকে।
ইন্টারভিউ প্রশ্ন — Module 38
প্র.১ — git merge --abort আর git rebase --abort কী গ্যারান্টি দেয়? উত্তর: দুটোই conflict resolution প্রক্রিয়া শুরুর আগের অবস্থায় সম্পূর্ণ নিরাপদে ফিরিয়ে দেয় — কোনো ডেটা হারায় না। Conflict-এ প্যানিক করার বদলে এটাই প্রথম মনে রাখা উচিত নিরাপদ প্রস্থান পথ।
প্র.২ — Force-push দুর্ঘটনায় টিমমেটের কাজ হারিয়ে গেলে সবচেয়ে নির্ভরযোগ্য উদ্ধার উৎস কী? উত্তর: যার কাজ হারিয়েছে তার লোকাল reflog — কারণ reflog সম্পূর্ণ লোকাল, remote force-push তাদের .git/logs/HEAD-কে স্পর্শ করে না। GitHub-এর server-side dangling commit retention (সীমিত সময়ের) একটা fallback।
প্র.৩ — --force আর --force-with-lease-এর পার্থক্য কী? উত্তর: --force blindly remote overwrite করে, remote-এ কী আছে যাচাই না করেই। --force-with-lease আপনার শেষ fetch-করা অবস্থার সাথে তুলনা করে — এর মাঝে remote-এ নতুন কিছু push হলে reject করে দেয়, দুর্ঘটনাজনিত overwrite প্রতিরোধ করে।
প্র.৪ — CRLF/LF সমস্যার স্থায়ী সমাধান .gitattributes কেন core.autocrlf-এর চেয়ে ভালো? উত্তর: .gitattributes repo-র সাথে commit হয়, তাই সব টিমমেট (OS নির্বিশেষে) একই নিয়ম পায়। core.autocrlf ব্যক্তিগত লোকাল সেটিং — কেউ ভুলে সেট না করলে সমস্যা ফিরে আসে।
প্র.৫ — Detached HEAD-এ করা commit কখন সত্যিই হারানোর ঝুঁকিতে পড়ে? উত্তর: যখন সেই commit-গুলো কোনো branch/tag থেকে reachable না এবং git gc চলে যায় (সাধারণত কয়েক সপ্তাহ পর, gc.pruneExpire অনুযায়ী) — তার আগ পর্যন্ত reflog দিয়ে উদ্ধারযোগ্য।
প্র.৬ — Diverged branch-এ merge বেছে নেবেন নাকি rebase, কীসের ওপর নির্ভর করে? উত্তর: মূল প্রশ্ন — commit-গুলো কি already shared/দেখা হয়ে গেছে অন্যদের সাথে? হ্যাঁ হলে merge (rewrite করবেন না); শুধু নিজের লোকাল, কেউ দেখেনি হলে rebase (পরিষ্কার লিনিয়ার history)।
কর্নার কেস — Module 38
- git reset --hard চালানোর সময় uncommitted পরিবর্তন (staged বা unstaged) সতর্কতা ছাড়াই স্থায়ীভাবে মুছে যায় — commit করা কাজের জন্য reflog নিরাপত্তা জাল আছে, কিন্তু কখনো commit না হওয়া কাজের জন্য কোনো reflog entry নেই, উদ্ধার প্রায় অসম্ভব।
- git gc স্বয়ংক্রিয়ভাবে চলতে পারে (git gc --auto) কিছু কমান্ডের পরে, এবং এটা reflog-এর মেয়াদ-উত্তীর্ণ entry ও dangling commit স্থায়ীভাবে মুছে দিতে পারে — তাই "হারানো" commit উদ্ধারের চেষ্টা যত দ্রুত করা যায় ততই ভালো, দেরি করলে gc চলে গিয়ে থাকতে পারে।
- .gitattributes যোগ করার পর git add --renormalize . না চালালে বিদ্যমান ফাইলে প্রভাব পড়ে না — শুধু নতুন commit থেকে কার্যকর হবে, পুরনো ফাইল আগের line-ending নিয়েই থেকে যাবে।
- git fsck-এর "dangling commit" রিপোর্ট মানেই সমস্যা না — normal ওয়ার্কফ্লোতেও (rebase, amend, reset) dangling object তৈরি হয়, এগুলো git gc-এর জন্য প্রত্যাশিত, ভয় পাওয়ার কিছু নেই যতক্ষণ না আপনি ইচ্ছাকৃতভাবে কিছু উদ্ধার করতে চাইছেন।
MODULE 39: Merge vs Rebase Framework ও ক্যাপস্টোন
পুরো জার্নির শেষ মডিউল। প্রথমে merge বনাম rebase নিয়ে একটা টিম-কনভেনশন ডিসিশন গাইড, তারপর ৩৯ মডিউলের সবচেয়ে গুরুত্বপূর্ণ কমান্ডগুলোর একটা কম্প্যাক্ট চিটশিট, তারপর একটা ক্যাপস্টোন — যেখানে conflict resolution, interactive rebase, reflog recovery, secret removal, আর branch-protected PR merge — সবকিছু একসাথে একটা সিমুলেটেড repo-তে প্রয়োগ করতে হবে। শেষে একটা ছোট বিদায়ী নোট দিয়ে পুরো যাত্রার সমাপ্তি।
Merge বনাম Rebase — টিম কনভেনশন ডিসিশন গাইড
প্রশ্নটা ভুলভাবে করা হয়
"Merge ভালো নাকি rebase ভালো" — এটা ভুল প্রশ্ন। সঠিক প্রশ্ন: "আমাদের টিমের জন্য কোন কনভেনশন সবচেয়ে কম ঘর্ষণ আর সবচেয়ে বেশি স্বচ্ছতা দেয়?" — উত্তর টিমের আকার, রিলিজ প্রসেস, আর history-কে কীভাবে ব্যবহার করা হবে তার ওপর নির্ভর করে।
ট্রেড-অফ টেবিল
| বিবেচনা | Merge (commit) | Rebase |
|---|---|---|
| History shape | Branch/merge point দেখা যায় (গ্রাফ জটিল হতে পারে) | সরল রেখা (linear) |
| Commit hash | অপরিবর্তিত থাকে | পুনর্লিখিত হয় (নতুন hash) |
| Shared branch-এ নিরাপদ? | হ্যাঁ, সবসময় | না — rewritten history অন্যদের জন্য সমস্যা তৈরি করে |
| git bisect-এ সুবিধা | মাঝে মাঝে বিভ্রান্তিকর (কোন parent অনুসরণ করবে) | পরিষ্কার, প্রতিটা commit ধারাবাহিক |
| Revert করা | জটিল (-m ফ্ল্যাগ লাগে merge commit-এর জন্য) | সহজ, সাধারণ revert |
| দলীয় ইতিহাস (কে কবে কী করলো) | সংরক্ষিত থাকে | হারিয়ে যায় (commit তারিখ থাকলেও branch topology হারায়) |
| CI/CD-তে "কী deploy হলো" ট্র্যাক করা | branch অনুযায়ী জটিল | প্রতিটা commit deployable ইউনিট হলে সহজ |
সিদ্ধান্তের নিয়ম
- কখনো shared/public branch rebase করবেন না — এটা একটা অলঙ্ঘনীয় নিয়ম (golden rule), টিম যেকোনো কনভেনশন বেছে নিক না কেন।
- ছোট, দ্রুত-চলা টিম, প্রচুর ছোট PR → squash + linear history রিকোয়ারমেন্ট ভালো ফিট — main পরিষ্কার থাকে।
- বড় ফিচার, দলগত history গুরুত্বপূর্ণ (কে কোন অংশ করলো ট্র্যাক করতে হয়) → merge commit strategy রাখুন, individual commit বজায় থাকুক।
- ব্যক্তিগত ফিচার branch-এ নিজের কাজ পরিষ্কার করা (push করার আগে) → interactive rebase সবসময় নিরাপদ, কারণ এটা এখনো "আপনার একার" history।
রিয়েল-লাইফ উদাহরণ
Rakib-এর টিম "squash and merge" + "require linear history" কনভেনশন বেছে নিয়েছে কারণ তাদের main-এর প্রতিটা commit একটা deploy-able ইউনিট হওয়া দরকার (production incident হলে git bisect দ্রুত root cause খুঁজে বের করতে হয়) — এটা তাদের নির্দিষ্ট প্রয়োজনের জন্য সঠিক সিদ্ধান্ত, সব টিমের জন্য universal সত্য না।
সম্পূর্ণ কমান্ড চিটশিট — পুরো জার্নির সারাংশ
সবচেয়ে গুরুত্বপূর্ণ কমান্ডসমূহ — এক নজরে
ব্যবহারের নিয়ম
এই টেবিল মুখস্থ করার জন্য না — বরং "কোন সমস্যায় কোন কমান্ড" এই ম্যাপিং দ্রুত মনে করার জন্য একটা রেফারেন্স। প্রতিটা সারির "ফেজ" কলাম দেখে, প্রয়োজনে সেই ফেজের বিস্তারিত leaf-এ ফিরে গিয়ে internals/context আবার পড়ে নিন।
ক্যাপস্টোন: এক Repo-তে সব দক্ষতা
দৃশ্যপট
আপনি (Rakib) একটা টিম প্রজেক্টে feature/payment-retry branch নিয়ে কাজ করছেন। এই একটা সিমুলেশনে জার্নির প্রায় প্রতিটা দক্ষতা একসাথে প্রয়োগ করতে হবে — বাস্তব প্রোডাকশন কাজের ঠিক এইরকমই এলোমেলো ক্রমে সমস্যা আসে।
ধাপ ১ — Merge Conflict সমাধান
feature/payment-retry-কে main-এর সাথে sync করতে গিয়ে conflict পেলেন payment_service.py-তে:
git fetch origin
git rebase origin/main
# CONFLICT (content): Merge conflict in payment_service.py
git status দিয়ে conflicted ফাইল দেখুন, মার্কার resolve করুন, git add payment_service.py, তারপর git rebase --continue।
ধাপ ২ — Interactive Rebase দিয়ে History পরিষ্কার
এই branch-এ ৮টা messy commit ("wip", "fix typo", "actually fix it") জমে আছে — PR পাঠানোর আগে পরিষ্কার করুন:
git rebase -i HEAD~8
সংশ্লিষ্ট "wip"/"fix typo" commit-গুলো squash/fixup করে ২-৩টা অর্থবহ commit-এ নামিয়ে আনুন।
ধাপ ৩ — ভুলে হারানো Commit Reflog দিয়ে উদ্ধার
Interactive rebase-এর মাঝে ভুলবশত একটা গুরুত্বপূর্ণ commit drop করে ফেলেছেন। প্যানিক না করে:
git reflog
# খুঁজে বের করুন rebase শুরুর আগের commit hash
git cherry-pick <হারানো-commit-hash>
ধাপ ৪ — Leaked Secret সরানো
Rebase-এর সময় লক্ষ্য করলেন কয়েক commit আগে ভুলে একটা AWS_SECRET_KEY কমিট হয়ে গেছে:
pip install git-filter-repo
echo "AKIA...REDACTED==>REMOVED" > replacements.txt
git filter-repo --replace-text replacements.txt --force
এর পর অবশ্যই সেই key AWS কনসোলে rotate/revoke করতে হবে — history থেকে মোছাই যথেষ্ট না, কারণ key ইতিমধ্যে exposed হয়ে গেছে।
ধাপ ৫ — Branch-Protected PR Workflow দিয়ে Merge
git push origin feature/payment-retry --force-with-lease # history rewrite হয়েছে বলে force-with-lease দরকার
gh pr create --title "Add payment retry logic" --body "Closes #88" --base main
Branch protection rule অনুযায়ী: CODEOWNERS approval + CI status check pass দরকার। রিভিউ পেয়ে, CI সবুজ হওয়ার পর:
gh pr merge --squash --delete-branch
সমাপ্তি
এই একটা ওয়ার্কফ্লোতে conflict resolution, interactive rebase, reflog recovery, history rewriting নিরাপত্তা, আর প্ল্যাটফর্ম-লেভেল গভর্নেন্স — সবকিছু একসাথে এলো। এটাই বাস্তব সিনিয়র ডেভেলপারের দৈনন্দিন git — কোনো একটা কমান্ড না, বরং সঠিক মুহূর্তে সঠিক কমান্ড বেছে নেওয়ার বিচারবুদ্ধি।
বিদায়ী নোট — যাত্রা শেষ, শেখা চলবেই
অভিনন্দন
৩৯টা মডিউল, ১৩টা Phase, শূন্য থেকে শুরু করে git-এর অবজেক্ট মডেল, ref, index, branching/merging/rebasing-এর ইন্টার্নালস, history rewriting, বড় repo অপ্টিমাইজেশন, GitHub-নেটিভ টিম ওয়ার্কফ্লো, আর সবশেষে একটা সম্পূর্ণ বাস্তব-সিনারিও ক্যাপস্টোন — এই পুরো পথ পাড়ি দেওয়ার জন্য অভিনন্দন। এখন আপনি (Rakib) শুধু git ব্যবহারকারী না, git-কে ভেতর থেকে বোঝা একজন প্র্যাকটিশনার।
পরবর্তী শেখার পথ
- git-scm.com/doc — অফিসিয়াল রেফারেন্স, প্রতিটা কমান্ডের সম্পূর্ণ flag ডকুমেন্টেশন; নতুন কোনো flag দেখলে এখানেই প্রথম চেক করুন।
- Pro Git বই (Scott Chacon ও Ben Straub, git-scm.com/book-এ ফ্রি) — এই জার্নির অনেক ইন্টারনালস (object model) এই বই থেকেই অনুপ্রাণিত; আরও গভীরে যেতে চাইলে "Git Internals" চ্যাপ্টার আবার পড়া ভালো।
- নিজের প্রজেক্টে প্রয়োগ — শেখা সম্পূর্ণ হয় প্রয়োগে। পরবর্তী রিয়েল প্রজেক্টে ইচ্ছাকৃতভাবে git bisect, interactive rebase, বা branch protection rule ব্যবহার করে দেখুন।
- অন্য হোস্টিং প্ল্যাটফর্ম — GitLab/Bitbucket-এর নিজস্ব fork/MR/pipeline সিস্টেম আছে; এই জার্নিতে যেভাবে "কোনটা git, কোনটা প্ল্যাটফর্ম" আলাদা করে শেখানো হয়েছে, সেই মানসিকতা যেকোনো নতুন প্ল্যাটফর্মে দ্রুত মানিয়ে নিতে সাহায্য করবে।
শেষ কথা
git কমান্ড মুখস্থ করা লক্ষ্য না — git কীভাবে ভাবে (commit = snapshot, branch = pointer, history = DAG) এটা বুঝে গেলে, যেকোনো নতুন কমান্ড বা ফ্ল্যাগ দেখামাত্র বোঝা যায় এটা আসলে কী করছে। শুভকামনা, Rakib।