পাঠ ৩৪ · ৫৮-এর মধ্যে · মডিউল ৮
Home / Courses / Software Engineering Principles & Git / Git ব্রাঞ্চিং বেসিকস

Git ব্রাঞ্চিং বেসিকস

Git branching basics
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • 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

git branch <name>
বর্তমান কমিটে একটি নতুন branch পয়েন্টার তৈরি করে — কিন্তু সেখানে সুইচ করে না, HEAD অপরিবর্তিত থাকে।
git switch <name> (বা checkout)
HEAD-কে একটি ভিন্ন branch-এ সরিয়ে দেয়, এবং Working Directory-কে সেই branch-এর কমিটের snapshot-এর সাথে মিলিয়ে আপডেট করে।

branching কেন এত মূল্যবান তা L29-এর ৫-জন-ডেভেলপার দৃশ্যকল্পের একটি সরাসরি উত্তর: একটি নতুন feature বা experiment সম্পূর্ণ আইসোলেশনে করা যায়, স্থিতিশীল main branch-কে একটুও প্রভাবিত না করে — experiment ব্যর্থ হলে শুধু সেই branch-টি বাতিল/ডিলিট করলেই হয়, বাকি কোথাও কোনো প্রভাব পড়ে না।

c1 c2 c3 main (অপরিবর্তিত) feature-x
feature-x branch c1 থেকে তৈরি হয়ে c2, c3-এ কমিট করেছে — কিন্তু main-এর পয়েন্টার পুরোটা সময় c1-এই স্থির ছিল।
মূল অন্তর্দৃষ্টি

একটি 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 সম্পূর্ণ অনুপস্থিত দেখা যাবে।

Python
# 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-তে মোট ৩টি কমিট আছে।
মূল কথা · Key takeaway

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") করলেই ফাইলটি সাথে সাথে ফিরে আসবে।

অনুশীলন

  1. পরীক্ষা করুন: কোড সেলে 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-ই সরে।

  2. চিন্তা করুন: বাস্তবে ৫ জন ডেভেলপার যদি একসাথে একই branch শেয়ার করে কাজ করেন (আলাদা আলাদা branch না বানিয়ে), branching-এর আইসোলেশন সুবিধাটা কি হারিয়ে যায়? কেন বা কেন নয়?

    হ্যাঁ, অনেকটাই হারিয়ে যায় — যদি সবাই একই branch-এ সরাসরি কমিট করেন, তাহলে L01-এর "৫ জন ডেভেলপার একই ফোল্ডার শেয়ার করছে" সমস্যাটার সাথে অনেকটাই মিল দেখা যায় (যদিও এখনও পুরো history ট্র্যাক হয়, যা আগের চেয়ে ভালো)। branching-এর প্রকৃত সুবিধা তখনই পূর্ণভাবে পাওয়া যায় যখন প্রতিটি ফিচার/এক্সপেরিমেন্টের নিজস্ব branch থাকে, এবং সেগুলো নিয়ন্ত্রিতভাবে merge (L35) করা হয় — এটাই M9-এ আসা কোলাবোরেশন ওয়ার্কফ্লোর ভিত্তি।

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

আগের পাঠ
L33 · পরিবর্তন পূর্বাবস্থায় ফেরানো