পাঠ ৪৩ · ৫৮-এর মধ্যে · মডিউল ৯
Home / Courses / Software Engineering Principles & Git / Git ট্যাগ ও ভার্সনিং

Git ট্যাগ, রিলিজ ও সিমান্টিক ভার্সনিং

Git tags, releases & semantic versioning
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • Git ট্যাগ ব্রাঞ্চ থেকে ঠিক কীভাবে আলাদা এবং কেন এটি "অপরিবর্তনীয়"
  • ট্যাগ কীভাবে একটি নির্দিষ্ট কমিটকে স্থায়ীভাবে একটি রিলিজ হিসেবে চিহ্নিত করে
  • সিমান্টিক ভার্সনিংয়ের প্রমিত MAJOR.MINOR.PATCH নিয়ম এবং "নিচের সংখ্যা রিসেট" রুল
  • বাস্তব parse_semver ও bump_version ফাংশন লিখে সঠিকভাবে ভার্সন বাম্প করা

১ · Git ট্যাগ কী — ব্রাঞ্চ থেকে ঠিক কীভাবে আলাদা

Git ট্যাগGit Tagএকটি নির্দিষ্ট কমিটের দিকে নির্দেশকারী পয়েন্টার, ব্রাঞ্চের মতোই দেখতে, কিন্তু অপরিবর্তনীয় — নতুন কমিট এলেও ট্যাগ সামনে এগোয় না। M8/L34-এ আমরা দেখেছিলাম ব্রাঞ্চ আসলে একটি হালকা, চলমান পয়েন্টার — নতুন কমিট হলে সেই ব্রাঞ্চ পয়েন্টার স্বয়ংক্রিয়ভাবে সামনে এগিয়ে যায়। Git ট্যাগ দেখতে একই রকম — একটি নির্দিষ্ট কমিট হ্যাশের দিকে নির্দেশ করা একটি নাম — কিন্তু একটি মৌলিক, গুরুত্বপূর্ণ পার্থক্য আছে: একটি ব্রাঞ্চ পয়েন্টার এগিয়ে যাওয়ার জন্য প্রত্যাশিত, কাজ চলতে থাকলে; কিন্তু একটি ট্যাগ একই কমিটে চিরকাল স্থির থাকার জন্য প্রত্যাশিত — এটি ইতিহাসে একটি স্থায়ী রেফারেন্স পয়েন্ট চিহ্নিত করে।

c1 c2 c3 c4 c5 v1.0.0 ট্যাগ — স্থির main — HEAD, চলমান
c1 থেকে c5 পর্যন্ত কমিট ইতিহাস এগিয়ে চলার সময়ও v1.0.0 ট্যাগ চিরকাল c3-তেই স্থির থাকে — কিন্তু main ব্রাঞ্চ পয়েন্টার প্রতিটি নতুন কমিটের সাথে সাথে এগিয়ে যায়।

ট্যাগ সবচেয়ে বেশি ব্যবহৃত হয় সফটওয়্যার রিলিজ মার্ক করতে — "এই ঠিক এই কমিটটিই আমরা v2.1.0 হিসেবে ব্যবহারকারীদের কাছে পাঠিয়েছিলাম।" এই তথ্যটি স্থায়ীভাবে সংরক্ষণ করা গুরুত্বপূর্ণ, কারণ মাসখানেক পরে যদি একটি বাগ রিপোর্ট আসে "ভার্সন ২.১.০-এ এই সমস্যা আছে," দলটি ঠিক সেই মুহূর্তের কোড নিশ্চিতভাবে ফিরে দেখতে পারে — ব্রাঞ্চ পয়েন্টার দিয়ে এটি সম্ভব নয়, কারণ ব্রাঞ্চ ততক্ষণে আরও অনেক নতুন কমিটে এগিয়ে গেছে।

২ · সিমান্টিক ভার্সনিং (SemVer) — একটি প্রমিত ভাষা

একটি ভার্সন নম্বর যেমন 2.1.3 শুধু একটি নির্বিচার সংখ্যা নয় — সিমান্টিক ভার্সনিং একে তিনটি অংশে ভাগ করে, প্রতিটি অংশের একটি নির্দিষ্ট, প্রমিত অর্থ সহ:

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

একটি গুরুত্বপূর্ণ, প্রায়ই ভুলে যাওয়া নিয়ম: উঁচু অংশ বাড়ালে নিচের সব অংশ ০-তে রিসেট হয়ে যায়। অর্থাৎ 2.1.3 থেকে একটি নতুন MINOR রিলিজে যাওয়া মানে 2.2.0 — 2.2.3 নয়! PATCH অংশটিও ০-তে ফিরে যায়, কারণ এটি একটি সম্পূর্ণ নতুন MINOR লাইনের শুরু। একইভাবে একটি MAJOR বাম্প 3.0.0 দেয় — MINOR ও PATCH দুটোই রিসেট হয়। এই নিয়মটি নিচের KaTeX সূত্রে প্রকাশ করা যায়:

$$\text{minor bump: } (M, m, p) \rightarrow (M, m+1, 0)$$

$$\text{major bump: } (M, m, p) \rightarrow (M+1, 0, 0)$$

কেন এই প্রমিত স্কিম ব্যবহারিকভাবে গুরুত্বপূর্ণ

SemVer অন্য ডেভেলপার বা স্বয়ংক্রিয় টুলকে শুধু ভার্সন নম্বর দেখেই একটি আপগ্রেডের ঝুঁকি বুঝতে দেয় — কোনো চেঞ্জলগ না পড়েই। একটি MAJOR বাম্প সংকেত দেয় "কোড পরিবর্তনের প্রস্তুতি নিন," আর একটি PATCH বাম্প সংকেত দেয় "নিশ্চিন্তে আপগ্রেড করুন, কোনো কোড পরিবর্তন লাগবে না।" এই প্রেডিক্টেবিলিটিই SemVer-কে ব্যাপকভাবে গৃহীত করে তুলেছে।

নিচের কোড সেলে আমরা একটি বাস্তব parse_semver ও bump_version ফাংশন লিখব, এবং নিশ্চিত করব যে "রিসেট" রুলটি সঠিকভাবে প্রয়োগ হচ্ছে — বিশেষ করে (2, 1, 3)-এর MINOR বাম্প ঠিক (2, 2, 0) দেয়, (2, 2, 3) নয়।

Python
# সিমান্টিক ভার্সনিং পার্স করা এবং সঠিক রিসেট-রুল অনুযায়ী ভার্সন বাম্প করা।

def parse_semver(version_string):
    major, minor, patch = version_string.split(".")
    return (int(major), int(minor), int(patch))

def bump_version(version_tuple, bump_type):
    major, minor, patch = version_tuple
    if bump_type == "major":
        return (major + 1, 0, 0)          # MINOR ও PATCH উভয়ই রিসেট
    elif bump_type == "minor":
        return (major, minor + 1, 0)      # শুধু PATCH রিসেট
    elif bump_type == "patch":
        return (major, minor, patch + 1)  # কিছুই রিসেট হয় না
    else:
        raise ValueError(f"অজানা bump_type: {bump_type!r}")

start = parse_semver("2.1.3")
print(f"শুরুর ভার্সন: {start[0]}.{start[1]}.{start[2]}\n")

for bump_type in ("patch", "minor", "major"):
    new_version = bump_version(start, bump_type)
    print(f"{bump_type:>5} বাম্প -> {new_version[0]}.{new_version[1]}.{new_version[2]}")

# রিসেট রুল যাচাই -- হাতে-হিসাব করা প্রত্যাশিত ফলাফলের সাথে তুলনা
assert bump_version(start, "minor") == (2, 2, 0), "minor বাম্পে PATCH রিসেট হওয়া উচিত ছিল"
assert bump_version(start, "major") == (3, 0, 0), "major বাম্পে MINOR ও PATCH দুটোই রিসেট হওয়া উচিত ছিল"
print("\nসব assertion পাস করেছে -- রিসেট রুল সঠিকভাবে প্রয়োগ হয়েছে।")

    
লক্ষ্য করুন তিনটি বাম্পই একই শুরুর ভার্সন (2, 1, 3) থেকে গণনা করা হয়েছে, যাতে পার্থক্যগুলো সরাসরি তুলনা করা যায়। minor বাম্পে ফলাফল 2.2.0 — 1 থেকে 2-এ বেড়েছে MINOR, আর PATCH-এর 3 সম্পূর্ণ হারিয়ে গিয়ে 0-তে ফিরে গেছে। এটিই সবচেয়ে বেশি ভুল হওয়া অংশ — অনেকে ভুল করে 2.2.3 আশা করেন, কিন্তু SemVer-এর নিয়ম অনুযায়ী তা ভুল।
মূল কথা · Key takeaway

ট্যাগ ও ব্রাঞ্চ দুটোই কমিট-পয়েন্টার, কিন্তু ভিন্ন উদ্দেশ্যে — ব্রাঞ্চ চলমান কাজের জন্য, ট্যাগ স্থায়ী ঐতিহাসিক রেফারেন্সের জন্য। সিমান্টিক ভার্সনিং সেই রেফারেন্স পয়েন্টগুলোকে একটি প্রমিত, অর্থবহ নামকরণ স্কিম দেয় — যাতে শুধু সংখ্যা দেখেই একটি আপগ্রেডের ঝুঁকি বোঝা যায়, এবং উঁচু অংশ বাড়ানোর সময় নিচের অংশ সবসময় ০-তে রিসেট হয়।

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

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

প্র ০১ কেউ যদি একটি রিলিজের জন্য ট্যাগের বদলে শুধু একটি সাধারণ ব্রাঞ্চ ব্যবহার করে (যেমন release-2.1.0 নামের একটি ব্রাঞ্চ), তাহলে ভবিষ্যতে কী সমস্যা হতে পারে?

সমস্যা হলো, একটি ব্রাঞ্চ পয়েন্টার এগিয়ে যাওয়ার জন্য তৈরি — কেউ ভুলবশত (বা ইচ্ছাকৃতভাবে) সেই release-2.1.0 ব্রাঞ্চে আরও নতুন কমিট যোগ করে ফেলতে পারে, যার ফলে "v2.1.0 হিসেবে ঠিক কোন কোডটি রিলিজ হয়েছিল" — এই প্রশ্নের উত্তর অস্পষ্ট হয়ে যায়। ট্যাগ ব্যবহার করলে এই ঝুঁকি থাকে না, কারণ ট্যাগ একটি নির্দিষ্ট কমিটে চিরকাল স্থির থাকার জন্যই তৈরি — এটি ভুলবশত এগিয়ে যাওয়ার সুযোগই নেই।

প্র ০২ একটি লাইব্রেরি 1.4.2 থেকে 2.0.0-তে আপগ্রেড হয়েছে দেখে, শুধু ভার্সন নম্বর থেকেই একজন ডেভেলপার কী অনুমান করতে পারেন?

MAJOR অংশ (1 থেকে 2) বেড়েছে, যার মানে SemVer অনুযায়ী এতে অসামঞ্জস্যপূর্ণ/ ব্রেকিং পরিবর্তন থাকতে পারে — ডেভেলপারের উচিত সরাসরি আপগ্রেড করার আগে সতর্ক থাকা এবং চেঞ্জলগ/মাইগ্রেশন গাইড দেখা, কারণ তাদের বিদ্যমান কোড এই নতুন ভার্সনে সরাসরি কাজ নাও করতে পারে। যদি এটি বরং 1.5.0 হতো (MINOR বাম্প), তাহলে নতুন ফিচার আছে কিন্তু বিদ্যমান কোড ভাঙার আশঙ্কা কম — এবং 1.4.3 (PATCH বাম্প) হলে নির্ভয়ে আপগ্রেড করা যেত।

প্র ০৩ উপরের কোড সেলে bump_version((2, 1, 3), "patch") কেন (2, 1, 4) দেয়, কিন্তু কোনো সংখ্যাই রিসেট হয় না — কেন প্যাচ বাম্প রিসেট রুলের ব্যতিক্রম?

কারণ PATCH ইতিমধ্যেই সবচেয়ে নিচের, সবচেয়ে ছোট অংশ — এর নিচে রিসেট করার মতো আর কোনো সংখ্যা নেই। রিসেট রুলের উদ্দেশ্য হলো: একটি উঁচু অংশ বাড়ানো মানে "একটি নতুন সিরিজ শুরু হলো," তাই তার নিচের সব অংশ শূন্য থেকে আবার শুরু হওয়া উচিত। যেহেতু PATCH কোনো অংশের "উপরে" নয়, তাই এটি শুধু নিজেই ১ বৃদ্ধি পায়, অন্য কিছু প্রভাবিত হয় না।

অনুশীলন

  1. চিন্তা করুন: একটি টিম 0.x.y ভার্সন সিরিজে (যেমন 0.9.2) দীর্ঘদিন ধরে কাজ করছে, এখনো 1.0.0-তে পৌঁছায়নি। SemVer-এর প্রথাগত ব্যাখ্যা অনুযায়ী 0.x.y-এর অর্থ কী হতে পারে, এবং কেন একটি টিম ইচ্ছাকৃতভাবে 1.0.0-এ পৌঁছাতে দেরি করতে পারে?

    SemVer-এর প্রথাগত ব্যাখ্যায় 0.x.y মানে "এখনো ডেভেলপমেন্ট/অস্থিতিশীল পর্যায়ে" — পাবলিক API যেকোনো সময়ে ব্রেকিং পরিবর্তন হতে পারে, এমনকি MINOR বাম্পেও, কারণ 1.0.0-এ না পৌঁছানো পর্যন্ত "স্থিতিশীল, প্রতিশ্রুতিবদ্ধ API" থাকার নিশ্চয়তা নেই। একটি টিম ইচ্ছাকৃতভাবে 1.0.0 দেরি করতে পারে যখন তারা এখনো API ডিজাইন নিয়ে পরীক্ষা-নিরীক্ষা করছে এবং প্রকাশ্যে প্রতিশ্রুতি দিতে প্রস্তুত নয় যে ভবিষ্যতে সবকিছু ব্যাকওয়ার্ড-কম্প্যাটিবল থাকবে।

  2. পরীক্ষা করুন: উপরের কোড সেলে start-এর বদলে parse_semver("9.9.9") ব্যবহার করে তিনটি বাম্প টাইপই আবার চালান। major বাম্পের ফলাফল কী হয়, এবং তা কি রিসেট রুলকে এখনো সঠিকভাবে অনুসরণ করে?

    bump_version((9, 9, 9), "major") দেবে (10, 0, 0) — MAJOR অংশ 9 থেকে 10-এ বেড়েছে (দুই-অঙ্কের সংখ্যায় পরিণত হওয়া সম্পূর্ণ স্বাভাবিক, SemVer-এ কোনো সংখ্যার উপরের সীমা নেই), এবং MINOR ও PATCH দুটোই ঠিক আগের নিয়ম মেনে 0-তে রিসেট হয়েছে। এটি নিশ্চিত করে ফাংশনটি যেকোনো শুরুর মান থেকেই সঠিকভাবে কাজ করে, শুধু ছোট সংখ্যার উদাহরণেই সীমাবদ্ধ নয়।

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

আগের পাঠ
ট্রাংক-বেসড ডেভেলপমেন্ট ও ফিচার ফ্ল্যাগ