পাঠ ৪১ · ৫৮-এর মধ্যে · মডিউল ৯

Git Flow ব্রাঞ্চিং স্ট্র্যাটেজি

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

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

  • Git Flow-এর পাঁচ ধরনের ব্রাঞ্চ ও প্রতিটির নির্দিষ্ট ভূমিকা
  • ফিচার ও রিলিজ ব্রাঞ্চ ঠিক কোথা থেকে শুরু হয় এবং কোথায় merge হয়
  • Git Flow কখন উপযুক্ত, কখন এটি অপ্রয়োজনীয়ভাবে ভারী
  • Python দিয়ে একটি বাস্তবসম্মত start_feature/finish_feature/start_release/finish_release সিকোয়েন্স সিমুলেট ও যাচাই করা

১ · Git Flow কী

Git FlowGit FlowVincent Driessen-এর প্রস্তাবিত একটি নির্দিষ্ট, স্ট্রাকচার্ড ব্রাঞ্চিং স্ট্র্যাটেজি যেখানে main, develop, feature, release ও hotfix ব্রাঞ্চের প্রতিটির একটি সুনির্দিষ্ট ভূমিকা আছে। হলো একটি সুনির্দিষ্ট, ব্যাপকভাবে পরিচিত ব্রাঞ্চিং কনভেনশন — মূলত Vincent Driessen প্রস্তাব করেছিলেন (একটি বাস্তব ঐতিহাসিক উৎস, উল্লেখ করার মতো)। এটি M8-এর ব্রাঞ্চিং/মার্জিং মেকানিক্সের ওপর ভিত্তি করে বিভিন্ন ধরনের ব্রাঞ্চের জন্য নির্দিষ্ট ভূমিকা নির্ধারণ করে দেয়, যাতে একটি টিমের সবাই একই কনভেনশন অনুসরণ করে।

২ · পাঁচটি ব্রাঞ্চ টাইপ

main / master
সবসময় প্রোডাকশন-রেডি, রিলিজড কোড ধরে রাখে।
develop
সম্পন্ন ফিচার জমা হওয়ার কেন্দ্রীয় ইন্টিগ্রেশন ব্রাঞ্চ, দুই রিলিজের মাঝখানে।
feature/*
একটি চলমান ফিচারের জন্য একটি — develop থেকে branch, সম্পন্ন হলে develop-এ merge।
release/*
রিলিজ প্রস্তুতির জন্য — develop থেকে branch, চূড়ান্ত টেস্ট/বাগফিক্সের পর main ও develop উভয়েতেই merge।
hotfix/*
জরুরি প্রোডাকশন ফিক্সের জন্য — সরাসরি main থেকে branch, main ও develop উভয়েতেই merge।

৩ · কখন উপযুক্ত, কখন ভারী

সৎ ট্রেড-অফ: এই স্ট্রাকচার্ড, একাধিক-ব্রাঞ্চ মডেল সেই প্রজেক্টগুলোর জন্য ভালো কাজ করে যেখানে শিডিউল্ড, ভার্সন-নাম্বারড রিলিজ আছে (যেমন ইনস্টল-করা সফটওয়্যার যার নির্দিষ্ট ভার্সন নম্বর থাকে) — কিন্তু কন্টিনিউয়াস ডিপ্লয়মেন্ট করা প্রজেক্টের জন্য এটি সত্যিই অপ্রয়োজনীয়ভাবে ভারী (M9/L42-এর ট্রাংক-বেসড ডেভেলপমেন্ট, একটি হালকা বিকল্প, সেখানে বিস্তারিত দেখব)। Git Flow-কে সব প্রজেক্টের জন্য "একমাত্র সঠিক" পদ্ধতি হিসেবে দেখানো ঠিক নয়।

main — সবসময় production-ready রিলিজড কোড develop — ফিচার ইন্টিগ্রেশনের কেন্দ্রীয় ব্রাঞ্চ feature/* — develop থেকে শুরু, develop-এ merge release/* — develop থেকে শুরু, main+develop-এ merge hotfix/* — main থেকে শুরু, main+develop-এ merge
প্রতিটি ব্রাঞ্চ টাইপের একটি সুনির্দিষ্ট শুরু-বিন্দু ও merge-গন্তব্য আছে — এই স্পষ্টতাই Git Flow-এর মূল প্রস্তাব।

নিচের কোড সেলে L30/L34/L37-এর ঠিক একই Repo মডেলের ওপর ভিত্তি করে একটি GitFlowRepo-স্টাইল সিমুলেশন লেখা হয়েছে — start_feature/finish_feature এবং start_release/finish_release ফাংশনসহ। বাস্তব git বাইনারি এই স্যান্ডবক্সে নেই, তাই এই সবকিছু commit/branch/merge-এর প্রকৃত অভ্যন্তরীণ আচরণের একটি নির্ভুল Python সিমুলেশন।

Python
# Git Flow সিমুলেশন -- L30/L34/L37-এর ঠিক একই Repo মডেলের ওপর ভিত্তি করে।

def make_repo():
    return {
        "working_directory": {}, "staging_area": {}, "commits": {},
        "branches": {"main": None}, "head": "main",
    }

def _new_hash(repo):
    return f"c{len(repo['commits']) + 1}"

def current_commit(repo):
    if repo["head"] in repo["branches"]:
        return repo["branches"][repo["head"]]
    return repo["head"]

def git_add(repo, filename):
    repo["staging_area"][filename] = repo["working_directory"][filename]

def git_commit(repo, message):
    parent = current_commit(repo)
    h = _new_hash(repo)
    repo["commits"][h] = {"message": message, "snapshot": dict(repo["staging_area"]), "parent": parent}
    repo["branches"][repo["head"]] = h
    repo["staging_area"] = {}
    return h

def is_ancestor(repo, ancestor_hash, descendant_hash):
    current = descendant_hash
    while current is not None:
        if current == ancestor_hash:
            return True
        current = repo["commits"][current]["parent"]
    return False

def merge_into(repo, source_branch, target_branch):
    source_tip = repo["branches"][source_branch]
    target_tip = repo["branches"][target_branch]
    if target_tip is None or is_ancestor(repo, target_tip, source_tip):
        repo["branches"][target_branch] = source_tip
        return "fast-forward"
    if is_ancestor(repo, source_tip, target_tip):
        return "already-up-to-date"
    h = _new_hash(repo)
    repo["commits"][h] = {
        "message": f"Merge {source_branch} into {target_branch}",
        "snapshot": dict(repo["commits"][source_tip]["snapshot"]),
        "parent": target_tip,
        "parents": [target_tip, source_tip],
    }
    repo["branches"][target_branch] = h
    return "merge-commit"

def commit_on(repo, branch, message, files):
    repo["head"] = branch
    for name, content in files.items():
        repo["working_directory"][name] = content
        git_add(repo, name)
    return git_commit(repo, message)

def start_feature(repo, name):
    branch = f"feature/{name}"
    repo["branches"][branch] = repo["branches"]["develop"]
    return branch

def finish_feature(repo, name):
    branch = f"feature/{name}"
    result = merge_into(repo, branch, "develop")
    del repo["branches"][branch]
    return result

def start_release(repo, version):
    branch = f"release/{version}"
    repo["branches"][branch] = repo["branches"]["develop"]
    return branch

def finish_release(repo, version):
    branch = f"release/{version}"
    r1 = merge_into(repo, branch, "main")
    r2 = merge_into(repo, branch, "develop")
    del repo["branches"][branch]
    return r1, r2

def log_branch(repo, branch):
    messages = []
    current = repo["branches"][branch]
    while current is not None:
        messages.append(repo["commits"][current]["message"])
        current = repo["commits"][current]["parent"]
    return list(reversed(messages))


repo = make_repo()
repo["branches"]["develop"] = None

commit_on(repo, "main", "প্রজেক্ট শুরু", {"app.py": "v0"})
repo["branches"]["develop"] = repo["branches"]["main"]

start_feature(repo, "login")
commit_on(repo, "feature/login", "লগইন ফিচার যোগ", {"login.py": "v1"})
print("feature/login শেষ:", finish_feature(repo, "login"))

start_feature(repo, "signup")
commit_on(repo, "feature/signup", "সাইনআপ ফিচার যোগ", {"signup.py": "v1"})
print("feature/signup শেষ:", finish_feature(repo, "signup"))

start_release(repo, "1.0")
commit_on(repo, "release/1.0", "রিলিজ ১.০-এর জন্য ভার্সন বাম্প", {"VERSION": "1.0"})
print("release/1.0 শেষ (main, develop):", finish_release(repo, "1.0"))

print("\nচূড়ান্ত ব্রাঞ্চ পয়েন্টার:")
for b, tip in repo["branches"].items():
    print(f"  {b:16s} -> {tip}")

print("\nmain-এর কমিট হিস্ট্রি (পুরনো -> নতুন):")
for m in log_branch(repo, "main"):
    print("  -", m)

print("\ndevelop-এর কমিট হিস্ট্রি (পুরনো -> নতুন):")
for m in log_branch(repo, "develop"):
    print("  -", m)

main_has_both_features = (
    "লগইন ফিচার যোগ" in log_branch(repo, "main")
    and "সাইনআপ ফিচার যোগ" in log_branch(repo, "main")
)
develop_has_release_bump = "রিলিজ ১.০-এর জন্য ভার্সন বাম্প" in log_branch(repo, "develop")

print("\nmain-এ কি দুটো ফিচারই পৌঁছেছে?", main_has_both_features)
print("develop কি রিলিজের ভার্সন-বাম্প commit-ও পেয়েছে?", develop_has_release_bump)

    
এই সিমুলেশনে প্রতিটি merge fast-forward হয়েছে (কারণ কোনো ব্রাঞ্চ কখনো একই সাথে develop-এর সমান্তরাল অগ্রগতি থেকে আলাদাভাবে diverge করেনি) — তাই main ও develop শেষে একদম একই commit চেইন শেয়ার করে। এই কারণেই log_branch(repo, "main")-এ দুটো ফিচার commit-ই দেখা যায় এবং develop-এও রিলিজ ভার্সন-বাম্প commit দেখা যায় — কমিট হিস্ট্রি (parent চেইন ধরে হাঁটা) দিয়ে এটি সরাসরি যাচাই করা হয়েছে, কেবল দাবি করে নয়।
মূল কথা · Key takeaway

Git Flow প্রতিটি ব্রাঞ্চ টাইপের জন্য একটি স্পষ্ট, পূর্বনির্ধারিত ভূমিকা দেয় — শিডিউল্ড রিলিজ-চক্র থাকা প্রজেক্টে এই স্পষ্টতা মূল্যবান। কিন্তু এটি একমাত্র বিকল্প নয় — পরের পাঠে (L42) আমরা ট্রাংক-বেসড ডেভেলপমেন্ট দেখব, যা কন্টিনিউয়াস ডিপ্লয়মেন্ট প্রজেক্টের জন্য একটি ইচ্ছাকৃতভাবে হালকা বিকল্প প্রস্তাব করে।

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

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

প্র ০১ কেন release/* ব্রাঞ্চ শেষে develop-এও merge করা হয়, শুধু main-এ নয়?

কারণ রিলিজ প্রস্তুতির সময় প্রায়ই শেষ মুহূর্তের বাগফিক্স/টেস্ট-সংশোধন release ব্রাঞ্চে করা হয় — যদি সেই পরিবর্তনগুলো শুধু main-এ merge করা হয়, develop সেগুলো কখনোই পাবে না, এবং পরবর্তী রিলিজে সেই একই বাগ আবার ফিরে আসতে পারে। develop-এও merge করলে নিশ্চিত হয় ভবিষ্যতের সব ফিচার-ওয়ার্ক এই শেষ-মুহূর্তের ফিক্সগুলোর ওপর ভিত্তি করে চলবে।

প্র ০২ হটফিক্স ব্রাঞ্চ কেন সরাসরি main থেকে শুরু হয়, develop থেকে নয়?

কারণ হটফিক্স একটি জরুরি প্রোডাকশন সমস্যার সমাধান — এবং develop-এ প্রায়ই এমন অসম্পূর্ণ/এখনো-রিলিজ-না-হওয়া ফিচার-ওয়ার্ক থাকতে পারে যা এখনো প্রোডাকশনে যাওয়ার জন্য প্রস্তুত নয়। যদি হটফিক্স develop থেকে শুরু হতো, সেই ফিক্সের সাথে অনিচ্ছাকৃতভাবে অসম্পূর্ণ কোডও প্রোডাকশনে চলে যেতে পারত। main থেকে শুরু করলে হটফিক্সটি শুধুই বর্তমান প্রোডাকশন কোডের ওপর ভিত্তি করে তৈরি হয়, অতিরিক্ত কোনো কিছু জড়ায় না।

প্র ০৩ কোড সেলে log_branch(repo, "main") এবং log_branch(repo, "develop") এই উদাহরণে identical কেন হলো — এটা কি সবসময় হবে?

না, সবসময় হবে না — এই নির্দিষ্ট উদাহরণে identical হয়েছে কারণ প্রতিটি merge fast-forward ছিল (main বা develop কখনোই independently এগিয়ে যায়নি, তাই তাদের মধ্যে কখনো প্রকৃত ডাইভার্জেন্স হয়নি)। বাস্তবে, যদি main-এ সরাসরি একটি হটফিক্স commit হতো যা তখনও develop-এ merge করা হয়নি, অথবা develop-এ নতুন ফিচার commit হতো যা এখনো কোনো রিলিজে যায়নি, তাহলে দুই ব্রাঞ্চের log_branch ফলাফল ভিন্ন হতো।

অনুশীলন

  1. চিন্তা করুন: আপনার টিম প্রতিদিন একাধিকবার প্রোডাকশনে ডিপ্লয় করে (কন্টিনিউয়াস ডিপ্লয়মেন্ট)। Git Flow-এর release/* ব্রাঞ্চ প্রক্রিয়া এই ধরনের টিমের জন্য কেন ঘর্ষণ (friction) তৈরি করতে পারে?

    কন্টিনিউয়াস ডিপ্লয়মেন্টে প্রতিটি ছোট পরিবর্তন দ্রুত প্রোডাকশনে যাওয়ার কথা — কিন্তু Git Flow-এর release/* ধাপ একটি অতিরিক্ত, ম্যানুয়াল "রিলিজ প্রস্তুতি" পর্যায় যোগ করে (ব্রাঞ্চ তৈরি, চূড়ান্ত টেস্ট, main+develop উভয়েতে merge) যা প্রতিটি ছোট পরিবর্তনের জন্য অপ্রয়োজনীয় ওভারহেড তৈরি করে। এই কারণেই M9/L42-এর ট্রাংক-বেসড ডেভেলপমেন্ট (সরাসরি এক ট্রাংকে ঘন ঘন কমিট, ফিচার ফ্ল্যাগ দিয়ে অসম্পূর্ণ কাজ লুকানো) এই ধরনের প্রজেক্টের জন্য বেশি স্বাভাবিক ফিট।

  2. পরীক্ষা করুন: কোড সেলে finish_release(repo, "1.0") কল করার পর একটি তৃতীয় ফিচার (start_feature/commit_on/finish_feature) যোগ করুন, তারপর আবার log_branch(repo, "develop") প্রিন্ট করে দেখুন এতে নতুন commit-টি যুক্ত হয়েছে কিনা।

    হ্যাঁ, নতুন ফিচার commit-টি এখন log_branch(repo, "develop")-এর তালিকায় সবচেয়ে নতুন (তালিকার শেষে) এন্ট্রি হিসেবে যুক্ত হবে, কারণ finish_feature সবসময় develop-এর বর্তমান tip-এর ওপর merge করে — এই ক্ষেত্রে সেটি রিলিজ ১.০-এর ভার্সন-বাম্প commit-এর পরের অবস্থান। main-এর হিস্ট্রি তখনও অপরিবর্তিত থাকবে, যতক্ষণ না একটি নতুন রিলিজ সেই ফিচারটিকেও main-এ নিয়ে আসে।

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

আগের পাঠ
পুল রিকোয়েস্ট ও কোড রিভিউ ওয়ার্কফ্লো