Git ব্রাঞ্চিং বেসিকস
এই পাঠে যা শিখবেন
- branch আসলে কী — একটি পয়েন্টার, ফাইলের কপি নয় — এবং কেন এটি এত সস্তা
- HEAD কীভাবে কাজ করে এবং নতুন কমিটে কীভাবে সরে
git branchওgit switch-এর ভূমিকার পার্থক্য- কোড দিয়ে প্রমাণ — একটি branch-এ কমিট করলে অন্য branch-এর পয়েন্টার অপরিবর্তিত থাকে
১ · Branch আসলে কী — একটি পয়েন্টার, ফাইল নয়
branchBranchএকটি হালকা, movable পয়েন্টার/লেবেল যা একটি নির্দিষ্ট কমিটকে নির্দেশ করে -- নতুন কমিট করলে সেই branch-এর পয়েন্টার স্বয়ংক্রিয়ভাবে সামনে সরে যায়। সম্পর্কে সবচেয়ে গুরুত্বপূর্ণ, মাঝে মাঝে অবাক করা তথ্যটি হলো — এটি ফাইলের কোনো কপি নয়, শুধু একটি নাম যা একটি নির্দিষ্ট কমিটকে "নির্দেশ" করে। তাই একটি নতুন branch তৈরি করা একটি O(1), প্রায় তাৎক্ষণিক অপারেশন — এতে কোনো ফাইল কপি হয় না, কোনো history ডুপ্লিকেট হয় না, শুধু বর্তমান কমিটে একটি নতুন নামের পয়েন্টার তৈরি হয়। এটি অনেক অন্য ভার্সন কন্ট্রোল সিস্টেমের ভারী branch-implementation থেকে সম্পূর্ণ ভিন্ন, এবং এই সস্তা-branching-ই git-এর ওয়ার্কফ্লোকে এত নমনীয় করে তোলে।
২ · HEAD — আপনি এখন কোথায় আছেন
HEADHEADএকটি বিশেষ পয়েন্টার যা নির্দেশ করে আপনি এখন কোন branch-এ আছেন (অথবা "detached HEAD" অবস্থায় থাকলে, ঠিক কোন কমিটে)।
নির্দেশ করে আপনি বর্তমানে কোন branch-এ কাজ করছেন (বিরল ক্ষেত্রে, একটি "detached HEAD" অবস্থায় সরাসরি একটি
নির্দিষ্ট কমিটে)। যখন একটি নতুন কমিট করা হয়, যে branch-এ বর্তমানে HEAD আছে, শুধু সেই branch-এর
পয়েন্টারই স্বয়ংক্রিয়ভাবে নতুন কমিটে সরে যায় — L30-এর git_commit মডেলের একটি
নির্ভুল সংস্করণ: আসলে সরে যায় HEAD যে branch-কে নির্দেশ করছে, সেই branch-ই।
৩ · git branch বনাম git switch
বর্তমান কমিটে একটি নতুন branch পয়েন্টার তৈরি করে — কিন্তু সেখানে সুইচ করে না, HEAD অপরিবর্তিত থাকে।
HEAD-কে একটি ভিন্ন branch-এ সরিয়ে দেয়, এবং Working Directory-কে সেই branch-এর কমিটের snapshot-এর সাথে মিলিয়ে আপডেট করে।
branching কেন এত মূল্যবান তা L29-এর ৫-জন-ডেভেলপার দৃশ্যকল্পের একটি সরাসরি উত্তর: একটি নতুন
feature বা experiment সম্পূর্ণ আইসোলেশনে করা যায়, স্থিতিশীল main
branch-কে একটুও প্রভাবিত না করে — experiment ব্যর্থ হলে শুধু সেই branch-টি বাতিল/ডিলিট করলেই হয়, বাকি
কোথাও কোনো প্রভাব পড়ে না।
একটি branch শুধুই একটি নাম আর একটি কমিট-হ্যাশের মধ্যে ম্যাপিং — {"feature-x": "c3"}-এর
মতো একটি ডিকশনারি এন্ট্রি ছাড়া আর কিছু নয়। এটি বুঝে গেলে branching, merging (L35), ও rebase
(L37)-এর প্রায় সবকিছুই অনেক সহজ হয়ে যায় — কারণ এই সবগুলো অপারেশনই মূলত এই পয়েন্টারগুলো নিয়েই কাজ করে।
৪ · কোড দিয়ে যাচাই
নিচের কোড সেলটি বাস্তব git বাইনারি চালায় না — এই ব্রাউজার-স্যান্ডবক্সে কোনো real git
ইনস্টল নেই — বরং এটি L30-এর Repo মডেল একটি branches ডিকশনারি ও একটি head
ফিল্ড দিয়ে সম্প্রসারিত করে, real git-এর ব্রাঞ্চিং অ্যালগরিদম যেভাবে কাজ করে ঠিক সেভাবেই বিশ্বস্তভাবে
সিমুলেট করছে। লক্ষ্য করুন — feature-x-এ দুটো কমিট করার পরেও, main-এ ফিরে
গিয়ে Working Directory-তে feature.py সম্পূর্ণ অনুপস্থিত দেখা যাবে।
# git-এর থ্রি-ট্রি + branching মডেল (L30 রিইউজ, branches/head যোগ) -- এই স্যান্ডবক্সে real git নেই,
# তাই এটি git-এর অভ্যন্তরীণ আচরণের একটি বিশ্বস্ত সিমুলেশন, real git চালানো নয়।
def make_repo():
return {
"working_directory": {},
"staging_area": {},
"commits": {},
"branches": {"main": None},
"head": "main",
}
def current_commit_hash(repo):
if repo["head"] in repo["branches"]:
return repo["branches"][repo["head"]]
return repo["head"] # detached HEAD -- সরাসরি একটি commit hash
def git_add(repo, filename):
repo["staging_area"][filename] = repo["working_directory"][filename]
def git_commit(repo, message):
parent = current_commit_hash(repo)
commit_hash = f"c{len(repo['commits']) + 1}"
repo["commits"][commit_hash] = {
"message": message,
"snapshot": dict(repo["staging_area"]),
"parent": parent,
}
if repo["head"] in repo["branches"]:
repo["branches"][repo["head"]] = commit_hash # শুধু বর্তমান branch-এর পয়েন্টার সরে
else:
repo["head"] = commit_hash
repo["staging_area"] = {} # বাস্তব git-এর মতোই কমিটের পর staging area খালি হয়ে যায় (L30 রিইউজ)
return commit_hash
def git_log(repo):
hashes = []
current = current_commit_hash(repo)
while current:
hashes.append(current)
current = repo["commits"][current]["parent"]
return hashes
# ---- ব্রাঞ্চিং কমান্ড ----
def git_branch(repo, name):
"""O(1): শুধু বর্তমান কমিটে একটি নতুন পয়েন্টার -- কোনো ফাইল কপি হয় না, HEAD সুইচ হয় না।"""
repo["branches"][name] = current_commit_hash(repo)
def git_switch(repo, branch_name):
"""HEAD সরায় এবং Working Directory-কে সেই branch-এর কমিটের snapshot-এ আপডেট করে।"""
if branch_name not in repo["branches"]:
raise ValueError(f"'{branch_name}' নামে কোনো branch নেই")
repo["head"] = branch_name
target_hash = repo["branches"][branch_name]
snapshot = dict(repo["commits"][target_hash]["snapshot"]) if target_hash else {}
repo["working_directory"] = dict(snapshot)
repo["staging_area"] = dict(snapshot)
# ---- ডেমো ----
repo = make_repo()
repo["working_directory"]["readme.md"] = "# প্রজেক্ট"
git_add(repo, "readme.md")
git_commit(repo, "প্রাথমিক কমিট")
print("branch তৈরির আগে:", repo["branches"])
git_branch(repo, "feature-x")
print("feature-x তৈরির পর (O(1), শুধু পয়েন্টার):", repo["branches"])
git_switch(repo, "feature-x")
repo["working_directory"]["feature.py"] = "def new_feature(): pass"
git_add(repo, "feature.py")
git_commit(repo, "ফিচার শুরু")
repo["working_directory"]["feature.py"] = "def new_feature():\n return 42"
git_add(repo, "feature.py")
git_commit(repo, "ফিচার সম্পন্ন")
print("\nfeature-x-এ ২টি কমিটের পর পয়েন্টার:")
print(" main: ", repo["branches"]["main"])
print(" feature-x: ", repo["branches"]["feature-x"])
print(" (main একচুলও নড়েনি!)")
git_switch(repo, "main")
print("\nmain-এ ফিরে যাওয়ার পর working_directory:", repo["working_directory"])
print("main-এর log (HEAD থেকে reachable):", git_log(repo))
print("অথচ repo['commits']-এ মোট কমিট সংখ্যা:", len(repo["commits"]), "(feature-x-এর ৩টিসহ)")
git_branch(repo, "feature-x") কল করার সাথে সাথেই main ও
feature-x দুটো পয়েন্টারই একই কমিটে (c1) থাকে — কোনো ফাইল ডুপ্লিকেট হয়নি। তারপর
feature-x-এ দুটো কমিট করার পরেও repo["branches"]["main"] এখনও "c1"
— কারণ git_commit-এর ভেতরে শুধু repo["head"] যে branch নির্দেশ করছে (এখানে
"feature-x"), শুধু সেই branch-এর পয়েন্টারই আপডেট হয়। main-এ ফিরে গেলে working_directory-তে
feature.py-এর কোনো চিহ্ন নেই, এবং main-এর log-এ মাত্র ১টি কমিট — যদিও repo-তে মোট ৩টি
কমিট আছে।
branching-এর পুরো শক্তি এই একটি সরল সত্যে নিহিত — প্রতিটি branch একটি স্বাধীন পয়েন্টার, আর কমিট করলে শুধু বর্তমানে-চেকআউট-করা branch-ই সরে। এই আইসোলেশনই একাধিক ডেভেলপারকে একই সময়ে একই কোডবেসে নিরাপদে, একে অপরকে প্রভাবিত না করে কাজ করতে দেয় — L35-এ এই স্বাধীন branchগুলো আবার একসাথে মিলিয়ে (merge) নেওয়ার প্রক্রিয়া দেখা যাবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
git_branch কল করলে working_directory বা commits ডিকশনারিতে কোনো পরিবর্তন হয় না কেন — এটাই কীভাবে branch তৈরিকে "O(1)" করে তোলে?
git_branch শুধু repo["branches"][name] = current_commit_hash(repo) —
একটি একক ডিকশনারি এন্ট্রি যোগ করে। এটি না কোনো ফাইলের কন্টেন্ট কপি করে, না কোনো কমিট অবজেক্ট ডুপ্লিকেট
করে — যত বড়ই repo হোক না কেন (হাজারো কমিট, হাজারো ফাইল), এই অপারেশনের খরচ সবসময় একই (একটি ডিকশনারি
insert) — এটাই "O(1)" মানে।
প্র ০২
feature-x-এ commit করার সময় main branch পয়েন্টার কেন move করে না — ঠিক কোন লাইনের কোডে এই সিদ্ধান্ত নেওয়া হচ্ছে?
git_commit-এর ভেতরে if repo["head"] in repo["branches"]: repo["branches"][repo["head"]] = commit_hash
লাইনটিই এই সিদ্ধান্ত নেয় — এটি শুধু repo["head"]-এর বর্তমান মান অনুযায়ী branch
আপডেট করে। যেহেতু git_switch(repo, "feature-x") কল করার পর repo["head"] == "feature-x",
পরের কমিটগুলো শুধু repo["branches"]["feature-x"]-কেই বদলায় — repo["branches"]["main"]
এই কোডে কখনো স্পর্শই হয় না।
প্র ০৩
main-এ ফিরে গিয়ে working_directory-তে feature.py না দেখলে, এর মানে কি ফাইলটা "মুছে" গেছে?
না, একেবারেই না। feature.py-র কন্টেন্ট এখনও feature-x branch-এর কমিট
snapshot-গুলোতে (c2, c3) নিরাপদে সংরক্ষিত আছে — git_switch(repo, "main") শুধু
working_directory-কে main-এর কমিট (c1)-এর snapshot দিয়ে প্রতিস্থাপন করেছে,
যেখানে কখনো feature.py যোগই হয়নি। আবার git_switch(repo, "feature-x") করলেই
ফাইলটি সাথে সাথে ফিরে আসবে।
অনুশীলন
-
পরীক্ষা করুন: কোড সেলে
feature-x-এ থাকা অবস্থায়git_branch(repo, "feature-x-v2")যোগ করে দুটো branch-এর পয়েন্টার প্রিন্ট করে তুলনা করুন।যেই মুহূর্তে
git_branch(repo, "feature-x-v2")কল হবে,repo["branches"]["feature-x-v2"]সেই মুহূর্তের বর্তমান কমিট হ্যাশ ধরে নেবে (যদি feature-x-এর দুটো কমিটের পরে কল করেন, তাহলে c3)। এরপর feature-x-এ আরও কমিট করলেfeature-x-v2-এর পয়েন্টার একচুলও নড়বে না — ঠিক যেভাবে main নড়েনি — কারণ প্রতিটি branch সম্পূর্ণ স্বাধীন, শুধু বর্তমানে-চেকআউট-করা branch-ই সরে। -
চিন্তা করুন: বাস্তবে ৫ জন ডেভেলপার যদি একসাথে একই branch শেয়ার করে কাজ করেন (আলাদা আলাদা branch না বানিয়ে), branching-এর আইসোলেশন সুবিধাটা কি হারিয়ে যায়? কেন বা কেন নয়?
হ্যাঁ, অনেকটাই হারিয়ে যায় — যদি সবাই একই branch-এ সরাসরি কমিট করেন, তাহলে L01-এর "৫ জন ডেভেলপার একই ফোল্ডার শেয়ার করছে" সমস্যাটার সাথে অনেকটাই মিল দেখা যায় (যদিও এখনও পুরো history ট্র্যাক হয়, যা আগের চেয়ে ভালো)। branching-এর প্রকৃত সুবিধা তখনই পূর্ণভাবে পাওয়া যায় যখন প্রতিটি ফিচার/এক্সপেরিমেন্টের নিজস্ব branch থাকে, এবং সেগুলো নিয়ন্ত্রিতভাবে merge (L35) করা হয় — এটাই M9-এ আসা কোলাবোরেশন ওয়ার্কফ্লোর ভিত্তি।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ — মার্জিং: fast-forward বনাম three-way merge — খুব শীঘ্রই।
- L30 · git init, add, commit — থ্রি-ট্রি আর্কিটেকচার পূর্বশর্ত এই পাঠের Repo মডেলই আজ branches ডিকশনারি ও head ফিল্ড দিয়ে সম্প্রসারিত হলো।
- Data Structures & Algorithms কোর্স সম্পর্কিত কোর্স commit গ্রাফ মূলত একটি tree/DAG structure — সেই কোর্সের tree ও graph traversal ধারণাগুলোর সাথে সরাসরি সম্পর্কিত।