এই পর্বে যা শিখবেন
- ভালো commit-এর চারটি গুণ, আর একটি এলোমেলো commit-কে ভালো বার্তায় রূপান্তর
- ৫০/৭২ নির্দেশিকা, imperative mood, issue-এর উল্লেখ আর trailer (
-sদিয়ে sign-off) - Conventional Commits 1.0.0-এর ব্যাকরণ আর একটি ছোট commit-message linter
- Lightweight বনাম annotated tag, tag সাজানো, push, মোছা আর
git describe - Semantic Versioning 2.0.0-এর নিয়ম আর precedence, CalVer, changelog আর পুরো release process ও deprecation policy
১ · ভালো commit কী — ইতিহাস যেন পড়া যায়
পর্ব ৮-এ (প্রথম Git repository) প্রথম commit করেছেন, আর সেখানেই দেখেছেন atomic commit-এর ধারণা: একটি commit-এ একটি যুক্তিসংগত পরিবর্তন। এই পর্বে প্রশ্নটি আরেক ধাপ এগিয়ে: commit-গুলো একসাথে যখন একটি project-এর ইতিহাস হয়, আর সেই ইতিহাস থেকে যখন একটি version নম্বর, একটি release আর ব্যবহারকারীদের জন্য পরিবর্তনের তালিকা বানাতে হয় — তখন “ভালো” মানে কী?
পাড়ার মুদি দোকানের হিসাবের খাতার কথা ভাবুন। ভালো খাতায় প্রতিটি লাইন একটি লেনদেন: “১২ জানুয়ারি, করিম সাহেব, ২ কেজি চাল, ১৪০ টাকা, বাকি”। মাস শেষে মালিক খাতা দেখে বলতে পারেন কে কত বাকি রেখেছেন, কোন জিনিস বেশি বিক্রি হলো। খারাপ খাতায় লেখা: “সকালের হিসাব, ৮৭০ টাকা”। টাকা মিলেছে কি না বোঝা যায়, কিন্তু কিছু ভুল হলে খোঁজার উপায় নেই। Commit-এর ইতিহাস ঠিক এই খাতা। আর বছরের শেষে খাতা বন্ধ করে যে “হালখাতা” হয় — সেটিই release: একটি নির্দিষ্ট দিনে সব হিসাব মিলিয়ে একটি নাম দেওয়া।
অভিজ্ঞ দলগুলো ভালো commit-কে সাধারণত চারটি গুণে চেনে:
একটি যুক্তিসংগত পরিবর্তন
“ভাড়া পূর্ণ টাকায় গোল করা” — একটি উদ্দেশ্য। বার্তায় “and” লিখতে হলে সম্ভবত দুটি commit দরকার ছিল।
atomicনিজে দাঁড়াতে পারে
এই commit checkout করলে project build হয় আর test পাস করে। তাহলে revert আর bisect (পর্ব ৫৫) নিরাপদ।
build + test সবুজকেন-র ব্যাখ্যা
Diff বলে কী বদলেছে। শুধু বার্তাই বলতে পারে কেন — কোন সমস্যা, কোন বিকল্প বাদ দেওয়া হলো।
why, not whatখুঁজে পাওয়া যায়
Issue নম্বর, type আর scope থাকলে পরে git log --grep বা যন্ত্র দিয়ে খোঁজা আর গোছানো সহজ।
এই চারটি গুণ আলাদা নয়। Commit ছোট আর এক-উদ্দেশ্যের হলে “কেন” লেখা সহজ, কারণ একটিই কারণ আছে। আবার প্রতিটি commit-এ build সবুজ রাখার অভ্যাস থাকলে একটি “কাজ চলছে” ধরনের commit মূল branch-এ পৌঁছায় না। নিজের branch-এ ইচ্ছেমতো ছোট “wip” commit করা দোষের নয় — ভাগ করার আগে interactive rebase-এর fixup আর --autosquash দিয়ে সেগুলো গুছিয়ে নেওয়া যায় (পর্ব ১৫ — Rebase), কিংবা pull request merge করার সময় squash করা যায় (পর্ব ১৬ — Pull request আর fork). আর একটি পরিবর্তনের অংশগুলো আলাদা commit-এ ভাগ করার হাতিয়ার git add -p দেখেছেন পর্ব ৮-এ।
এই পর্বের সব Git আউটপুট আসল — একটি খালি পরীক্ষার folder-এ Git ২.৫৩ দিয়ে চালানো। লেখক “Rahim” (rahim@example.com), আর প্রতিটি commit ও tag-এর সময় GIT_AUTHOR_DATE/GIT_COMMITTER_DATE দিয়ে ২০২৬-০১-১৫ সকাল ১০:০০ (+06:00) থেকে এক মিনিট করে বাড়িয়ে ঠিক করা, যাতে hash আপনার যন্ত্রেও একই আসে। গল্পটি একটি কাল্পনিক আন্তঃজেলা বাসের টিকিট অ্যাপ — ঈদের আগের ভিড়ে যার ওপর অনেক চাপ। শুরুতে দুটি commit: e6e2912 (feat: add fare calculator) আর 1674d4e (docs: add README), যেখানে প্রথম release v1.0.0 দেওয়া হয়েছে।
২ · Commit message-এর গঠন — ৫০/৭২, আদেশসূচক ক্রিয়া আর trailer
একটি commit message-এর তিনটি অংশ: প্রথম লাইন (subject বা সারাংশ), একটি ফাঁকা লাইন, তারপর body. শেষে ঐচ্ছিকভাবে কিছু “Key: value” লাইন। প্রচলিত নির্দেশিকাগুলো (Git project-এর নিজের SubmittingPatches নির্দেশনা, আর ২০০৮ সালের দিকে Tim Pope-এর একটি জনপ্রিয় blog লেখা থেকে ছড়ানো “৫০/৭২” রীতি) মোটামুটি এমন:
| নিয়ম | কেন |
|---|---|
| Subject প্রায় ৫০ অক্ষরের মধ্যে (কঠিন সীমা নয়; ৭২-এর বেশি প্রায় সবাই এড়ান) | git log --oneline, platform-এর তালিকা আর email-এর subject-এ পুরোটা দেখা যায় |
| Subject-এর পরে একটি ফাঁকা লাইন | Git প্রথম অনুচ্ছেদকেই “title” ধরে; ফাঁকা লাইন না থাকলে body-ও subject হয়ে যায় |
| Body প্রায় ৭২ অক্ষরে হাতে লাইন ভাঙা | git log বার্তা ৪ ঘর সরিয়ে দেখায়; ৮০ ঘরের terminal-এ ৭২ + ৪ + কিছু ফাঁক আরামে আঁটে |
| Subject আদেশসূচক (imperative): “Add”, “Fix”, “Remove” — “Added”, “Fixes” নয় | পড়ার নিয়ম: “এই commit প্রয়োগ করলে এটি … করবে” — “If applied, this commit will add …” |
| Subject-এর শেষে দাঁড়ি (.) নেই | এটি শিরোনাম, বাক্য নয়; জায়গাও বাঁচে |
| Body-তে “কেন” আর “কী প্রভাব”; “কীভাবে” কোড নিজেই বলে | পাঁচ বছর পরের পাঠক diff দেখতে পান, কিন্তু সিদ্ধান্তের কারণ নয় |
Imperative mood নিয়ম ইংরেজি বার্তার জন্য। বাংলাভাষী দলগুলোর অনেকে — আমাদের ধারণা, বেশিরভাগই — commit message ইংরেজিতে লেখেন: কোড, টুল আর open-source অবদান ইংরেজিতে, আর দলে বিভিন্ন ভাষার মানুষ থাকতে পারেন। বাংলায় লিখলে Git-এর আপত্তি নেই (Git UTF-8 রাখে), কিন্তু কিছু পুরোনো টুল, terminal বা email ব্যবস্থায় অক্ষর ভাঙতে পারে, আর অক্ষর গোনার হিসাবও আলাদা হয়। যা-ই বাছেন, দলের নিয়মে লিখে রাখুন। এই পর্বের সব উদাহরণ ইংরেজিতে।
এলোমেলো commit থেকে ভালো বার্তা — ধাপে ধাপে
রাতের শেষে একজন developer তিনটি কাজ একসাথে commit করতে যাচ্ছিলেন: ভাড়া গোল করা, README-র একটি বানান ঠিক করা, আর ভুলে থেকে যাওয়া একটি debug লাইন। নিচের stepper-এ সেটিকে কীভাবে গোছানো হলো দেখুন:
| Commit-এ কী | |
| বার্তা | |
| Subject-এর দৈর্ঘ্য | |
| যা বাকি |
- শুরু। একটি commit-এ তিনটি আলাদা কাজ, বার্তা
updated stuff. পরে ভাড়ার হিসাবে গোলমাল হলে এই commit revert করলে README-র ঠিক করা বানানও ফিরে যাবে, আর বার্তা দেখে কেউ বুঝবেন না কোন commit-এ ভাড়া বদলেছিল। - ভাগ করুন।
git add -pদিয়ে শুধুfare.js-এর অংশ stage করে প্রথম commit, README আলাদা commit-এ। Debug লাইনটি commit-এর যোগ্যই নয় — মুছে ফেলা হলো। বার্তা এখনfixed fare rounding: এক উদ্দেশ্য, কিন্তু অতীত কাল। - আদেশসূচক ক্রিয়া, type আর scope. “If applied, this commit will round fare to whole taka” — বাক্যটি ঠিক বসে। সামনে Conventional Commits-এর
fix(fare):(অংশ ৩)। মোট ৩৫ অক্ষর, ৫০-এর মধ্যে। - কেন — body-তে। একটি ফাঁকা লাইনের পরে দুটি লাইন: কাউন্টারে পয়সার খুচরা দেওয়া যায় না, তাই ৩৮৭.৫০-এর মতো ভাড়া বিভ্রান্তি তৈরি করছিল। লাইন দুটি ৫৯ আর ৬০ অক্ষর — ৭২-এর মধ্যে। কোড কীভাবে গোল করে, তা লেখা হয়নি; সেটা diff-এই আছে।
- Issue-এর সূত্র আর দ্বিতীয় commit. শেষ অনুচ্ছেদে trailer
Refs: #17— issue tracker-এর সেই অভিযোগের নম্বর। README-র commit আলাদা:docs: fix spelling of Rajshahi in README(৪০ অক্ষর). এখন দুটি commit, প্রতিটি নিজে বোঝা যায়, আলাদা করে ফেরানো যায়।
আসল repository-তে প্রথম commit-টি এভাবে হলো (-F দিয়ে একটি ফাইল থেকে বার্তা নেওয়া; সাধারণত git commit চালালে editor খোলে, সেখানেই লেখেন):
$ cat ../msg3.txt
fix(fare): round fare to whole taka
Counters cannot give change in paisa, so a fare like 387.50
confused passengers and cashiers. Round to the nearest taka.
Refs: #17
$ git commit -F ../msg3.txt
[main 30ee2e5] fix(fare): round fare to whole taka
1 file changed, 1 insertion(+), 1 deletion(-)Issue-এর উল্লেখ আর trailer
বার্তার শেষ অনুচ্ছেদে Key: value আকারের লাইনগুলোকে Git বলে trailer — ইমেইলের নিচের স্বাক্ষর-অংশের মতো। Git নিজে কয়েকটি বোঝে, আর git interpret-trailers ও %(trailers) format দিয়ে সব পড়া যায়। প্রচলিত কয়েকটি:
Refs: #42বাFixes: #42— issue tracker-এর issue-এর সূত্র। কিছু hosting platform বার্তায়Closes #42বাFixes #42লেখা থাকলে মূল branch-এ ঢোকার পরে issue নিজে বন্ধ করে দেয় (শব্দ আর নিয়ম platform-ভেদে আলাদা, লেখার সময়ের অবস্থা দেখে নিন)।Co-authored-by: Name <email>— একসাথে বসে লেখা (pair programming) কোডে সহলেখকের নাম। Git-এর কাছে এটি শুধু লেখা; কিছু platform একে চিনে সহলেখককেও কৃতিত্ব দেখায়।Signed-off-by: Name <email>—git commit -s(--signoff) নিজের নাম দিয়ে এটি যোগ করে। অর্থ: “আমি এই কোড জমা দেওয়ার অধিকার রাখি” — Linux kernel-সহ অনেক project-এ Developer Certificate of Origin (DCO)-এ সম্মতি এভাবে জানানো হয়। এটি cryptographic স্বাক্ষর নয়; যে কেউ যেকোনো নাম লিখতে পারে। আসল স্বাক্ষর (-S, GPG/SSH) পর্ব ৫৭-এ।
# (seat.js added and staged; ../msg4.txt ends with "Refs: #42" and
# "Co-authored-by: Karim <karim@example.com>")
$ git commit -s -F ../msg4.txt
[main b6ea870] feat(seat): let passengers ask for a window seat
1 file changed, 6 insertions(+)
create mode 100644 seat.js
$ git log -1
commit b6ea870de0cc17925ea2b4a80789045caeda0704
Author: Rahim <rahim@example.com>
Date: Thu Jan 15 10:04:00 2026 +0600
feat(seat): let passengers ask for a window seat
Many passengers on long Eid trips want a window seat, and the
counter staff were choosing seats by hand. pickSeat() now takes
a wantWindow flag and falls back to any free seat.
Refs: #42
Co-authored-by: Karim <karim@example.com>
Signed-off-by: Rahim <rahim@example.com>
$ git log -1 --format="%(trailers)"
Refs: #42
Co-authored-by: Karim <karim@example.com>
Signed-off-by: Rahim <rahim@example.com>-s বার্তার শেষে Signed-off-by: Rahim <rahim@example.com> যোগ করেছে — নাম আর email এসেছে user.name/user.email config থেকে। লক্ষ করুন, এটি আগের trailer-গুলোর সাথে একই অনুচ্ছেদে বসেছে, আর %(trailers) তিনটিকেই চিনেছে। এই পর্ব তৈরির সময় প্রথম চেষ্টায় আমরা শেষ অনুচ্ছেদে Refs: #42-এর পরে Closes #42 (কোলন ছাড়া) রেখেছিলাম; তখন Git ওই অনুচ্ছেদকে trailer-এর অংশ মনে করেনি, sign-off আলাদা অনুচ্ছেদে বসিয়েছিল, আর Co-authored-by-ও trailer হিসেবে ধরা পড়েনি। কারণ Git trailer খোঁজে শুধু শেষ অনুচ্ছেদে, আর সেখানে মূলত Key: value লাইন থাকতে হয়। শিক্ষা: trailer-গুলো সবার শেষে, নিজেদের আলাদা অনুচ্ছেদে, প্রতিটি Key: value আকারে। git log বার্তার প্রতিটি লাইন ৪ ঘর সরিয়ে দেখায় — ৭২ অক্ষরের নিয়মের কারণ এখানেই।
৩ · Conventional Commits 1.0.0 — যন্ত্রও যেন পড়তে পারে
ভালো বার্তা মানুষের জন্য। কিন্তু release-এর সময় একটি প্রশ্ন যন্ত্রকেও জিজ্ঞেস করতে চাই: “শেষ release-এর পরে কি নতুন সুবিধা এসেছে? কিছু ভেঙেছে?” এজন্য বার্তার প্রথম লাইনের একটি নির্দিষ্ট ব্যাকরণ — Conventional Commits. এর ১.০.০ সংস্করণের spec (আমাদের জানা মতে ২০১৯ সালে প্রকাশ, AngularJS project-এর commit নির্দেশিকা থেকে অনুপ্রাণিত) ছোট আর নির্দিষ্ট:
[body]
[footer(s)]
- type বাধ্যতামূলক। spec নিজে শুধু দুটির অর্থ বাঁধে:
feat= নতুন সুবিধা (SemVer-এর MINOR-এর সাথে মেলে),fix= bug সারানো (PATCH). বাকি type অনুমোদিত; প্রচলিত তালিকা (Angular রীতি আর commitlint-এর প্রচলিত config থেকে):docs,refactor,test,chore,perf,ci,build,style,revert. - scope ঐচ্ছিক, বন্ধনীর ভেতরে একটি বিশেষ্য: কোডের কোন অংশ —
fix(fare):,feat(seat):. - description কোলন আর একটি space-এর ঠিক পরে; ছোট সারাংশ।
- body description-এর পরে একটি ফাঁকা লাইন দিয়ে শুরু; মুক্ত লেখা।
- footer Git trailer-এর রীতিতে (
Token: valueবাToken #value). বিশেষ footerBREAKING CHANGE: …(বড় হাতের অক্ষরে;BREAKING-CHANGEসমার্থক)। !— type/scope-এর পরে, কোলনের আগে: এটিও breaking change-এর চিহ্ন।!থাকলে footer ঐচ্ছিক; description-ই তখন কী ভাঙল তা বলে।
| Type | কখন | উদাহরণ | Release-এ প্রভাব (প্রচলিত) |
|---|---|---|---|
feat | ব্যবহারকারী দেখবেন এমন নতুন সুবিধা | feat(seat): let passengers ask for a window seat | MINOR |
fix | Bug সারানো | fix(fare): round fare to whole taka | PATCH |
perf | আচরণ একই, গতি বা মেমরি ভালো | perf(seat): stop rescanning taken seats | অনেক টুলে PATCH |
refactor | আচরণ বদলায় না এমন কোড-পুনর্গঠন | refactor(fare): extract route lookup | নেই |
docs | শুধু documentation | docs: add README | নেই |
test | Test যোগ বা ঠিক | test(fare): cover the route table | নেই |
build / ci | Build ব্যবস্থা, dependency / CI config | ci: run tests on every push | নেই |
chore | বাকি রক্ষণাবেক্ষণ | chore: update lockfile | নেই |
যেকোনো type + ! বা BREAKING CHANGE: | পুরোনো ব্যবহার ভাঙে | feat(fare)!: read fares from a route table | MAJOR |
আমাদের অ্যাপে পরের বড় পরিবর্তন: ভাড়া আর কিলোমিটার-সূত্রে নয়, রুট-ভিত্তিক তালিকা থেকে। ফলে fare(km) হয়ে গেল fare(from, to) — যে কোড পুরোনো রূপে ডাকত, সেটি ভাঙবে। বার্তায় দুইভাবেই তা বলা হলো:
feat(fare)!: read fares from a route table
Fares now come from fares.json per route instead of a per-km
formula, because the operator sets a fixed price per route.
BREAKING CHANGE: fare(km) is now fare(from, to).
দলগুলো সাধারণত এই নিয়ম যন্ত্র দিয়ে যাচাই করে — commit-এর আগে একটি Git hook-এ (পর্ব ৫৬) বা CI-তে (পর্ব ৫৮) একটি commit linter চালিয়ে। নিচে একটি ছোট linter, যা type, দৈর্ঘ্য, ফাঁকা লাইন আর imperative mood যাচাই করে। শেষ নিয়মটি নিছক অনুমান (heuristic) — খেয়াল করুন সেটি কোথায় ভুল করে:
// A tiny commit-message linter: type prefix, 50/72 lengths, blank line, imperative mood (heuristic).
const TYPES = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"];
function lint(msg) {
const lines = msg.split("\n"), subject = lines[0], problems = [];
const m = /^(\w+)(\([\w-]+\))?(!)?: (.+)$/.exec(subject);
if (!m) problems.push("no 'type(scope): description' prefix");
else if (!TYPES.includes(m[1])) problems.push(`unknown type '${m[1]}'`);
if (subject.length > 72) problems.push(`subject ${subject.length} chars (error > 72)`);
else if (subject.length > 50) problems.push(`subject ${subject.length} chars (aim for <= 50)`);
if (subject.endsWith(".")) problems.push("subject ends with '.'");
const firstWord = (m ? m[4] : subject).split(" ")[0].toLowerCase();
if (/(ed|ing)$/.test(firstWord)) problems.push(`'${firstWord}' looks non-imperative`);
if (lines.length > 1 && lines[1] !== "") problems.push("line 2 must be blank");
lines.slice(2).forEach((l, i) => {
if (l.length > 72) problems.push(`body line ${i + 3} is ${l.length} chars (> 72)`);
});
return problems;
}
const samples = [
"feat(seat): let passengers ask for a window seat",
"Fixed the bug.",
"fix: updated the fare rounding for all routes in the Dhaka division",
"docs: adding README\nexplains how to book",
"feat: embed the seat map in the ticket page",
"refactor(fare)!: drop fare(km)\n\nBREAKING CHANGE: callers must pass from and to.",
];
for (const s of samples) {
const p = lint(s);
console.log((p.length ? "FAIL " : "ok ") + JSON.stringify(s.split("\n")[0]));
p.forEach(x => console.log(" - " + x));
}আসল আউটপুট:
ok "feat(seat): let passengers ask for a window seat"
FAIL "Fixed the bug."
- no 'type(scope): description' prefix
- subject ends with '.'
- 'fixed' looks non-imperative
FAIL "fix: updated the fare rounding for all routes in the Dhaka division"
- subject 67 chars (aim for <= 50)
- 'updated' looks non-imperative
FAIL "docs: adding README"
- 'adding' looks non-imperative
- line 2 must be blank
FAIL "feat: embed the seat map in the ticket page"
- 'embed' looks non-imperative
ok "refactor(fare)!: drop fare(km)"
প্রথম আর শেষ বার্তা নিয়ম মানে; শেষটিতে ! আর BREAKING CHANGE: দুটোই আছে। Fixed the bug. তিনটি নিয়ম ভাঙে। ৬৭ অক্ষরের subject সতর্কবার্তা পায় (৭২-এর বেশি হলে error). docs: adding README-এ দ্বিতীয় লাইন ফাঁকা নয়, তাই Git পুরো দুই লাইনকেই subject ভাবত। কিন্তু embed একটি সঠিক আদেশসূচক ক্রিয়া — শুধু “ed” দিয়ে শেষ বলে linter ভুল করে আটকেছে। এটাই heuristic-এর সীমা: আসল টুলগুলো সাধারণত শব্দের তালিকা ব্যবহার করে বা এই নিয়মটি সতর্কবার্তা হিসেবে রাখে, আটকায় না। Regex-এর খুঁটিনাটি JavaScript কোর্সে — Regular expression.
৪ · Tag — lightweight আর annotated
পর্ব ১২-এ (Branch — pointer, HEAD, switch আর detached HEAD) দেখেছেন, tag হলো refs/tags/-এর নিচে থাকা একটি ref — একটি commit-এর স্থায়ী নাম, যা branch-এর মতো নতুন commit-এর সাথে সরে না। Git-এ tag দুই রকম:
Lightweight tag
git tag tested — একটি lightweight tag শুধু একটি নাম, যা সরাসরি একটি commit-এর hash রাখে। কোনো লেখক, তারিখ বা বার্তা নেই। নিজের কাজের বুকমার্ক হিসেবে সুবিধাজনক।
Annotated tag
git tag -a v1.1.0 -m "…" — annotated tag-এর জন্য Git একটি আলাদা tag object বানায়: কোন object-কে নির্দেশ করে, tagger, তারিখ আর বার্তা। Sign-ও করা যায় (-s, পর্ব ৫৭)। Release-এর জন্য এটিই।
উইন্ডো-সিটের commit b6ea870-এ দুই রকম tag-ই দিয়ে পার্থক্য দেখি:
$ git tag tested
$ git tag -a v1.1.0 -m "Release 1.1.0: window seats"
$ git cat-file -t tested
commit
$ git cat-file -t v1.1.0
tag
$ git cat-file -p v1.1.0
object b6ea870de0cc17925ea2b4a80789045caeda0704
type commit
tag v1.1.0
tagger Rahim <rahim@example.com> 1768449900 +0600
Release 1.1.0: window seats
$ git show --stat v1.1.0
tag v1.1.0
Tagger: Rahim <rahim@example.com>
Date: Thu Jan 15 10:05:00 2026 +0600
Release 1.1.0: window seats
commit b6ea870de0cc17925ea2b4a80789045caeda0704
Author: Rahim <rahim@example.com>
Date: Thu Jan 15 10:04:00 2026 +0600
feat(seat): let passengers ask for a window seat
…
seat.js | 6 ++++++
1 file changed, 6 insertions(+)git cat-file -t object-এর ধরন বলে। tested নামটি সরাসরি একটি commit-এ পৌঁছায়; v1.1.0 পৌঁছায় একটি tag object-এ। সেই object-এর ভেতরে (cat-file -p): object b6ea870… (কোন commit), type commit, নাম, tagger লাইন (Unix সময় 1768449900 = ১০:০৫, +06:00) আর বার্তা। git show v1.1.0 তাই আগে tag-এর অংশ (Tagger, Date, বার্তা) দেখায়, তারপর commit. git show tested সরাসরি commit b6ea870… দিয়ে শুরু হয় — tag-এর নিজের কোনো তথ্য নেই। Release-এ annotated tag কেন: কে কখন release দিলেন তার রেকর্ড থাকে, git describe ডিফল্টে শুধু এগুলোই দেখে, আর git push --follow-tags শুধু এগুলোই পাঠায়।
তালিকা, সাজানো, push আর মোছা
পরের কয়েকটি commit আর একটি release candidate (অংশ ৬) tag দেওয়ার পরে তালিকা:
$ git tag
tested
v1.0.0
v1.1.0
v2.0.0-rc.1
$ git tag -n
tested feat(seat): let passengers ask for a window seat
v1.0.0 Release 1.0.0
v1.1.0 Release 1.1.0: window seats
v2.0.0-rc.1 Release candidate 2.0.0-rc.1
$ git tag -l "v1.*"
v1.0.0
v1.1.0
$ git tag --sort=-v:refname
v2.0.0-rc.1
v1.1.0
v1.0.0
testedgit tag একা অক্ষরের ক্রমে সাজায়। -n annotated tag-এর বার্তা দেখায়; lightweight tested-এর জন্য commit-এর subject. -l একটি glob pattern নেয়। আর --sort=v:refname নামের সংখ্যাগুলোকে সংখ্যা হিসেবে ধরে (version sort), সামনে - দিলে উল্টো ক্রম, নতুনটি ওপরে। কিন্তু সাবধান — Git-এর version sort SemVer নয়। একটি আলাদা পরীক্ষার repository-তে একই commit-এ কয়েকটি tag দিয়ে দেখুন:
$ git tag
v1.10.0
v1.2.0
v1.9.0
v2.0.0
v2.0.0-alpha
v2.0.0-alpha.1
v2.0.0-beta
v2.0.0-rc.1
$ git tag --sort=-v:refname
v2.0.0-rc.1
v2.0.0-beta
v2.0.0-alpha.1
v2.0.0-alpha
v2.0.0
v1.10.0
v1.9.0
v1.2.0
$ git -c versionsort.suffix=- tag --sort=-v:refname
v2.0.0
v2.0.0-rc.1
v2.0.0-beta
v2.0.0-alpha.1
v2.0.0-alpha
v1.10.0
v1.9.0
v1.2.0অক্ষরের ক্রমে v1.10.0 এসেছে v1.2.0-এর আগে (“1” < “2”) — version-এর জন্য ভুল। v:refname সেটি ঠিক করে (১০ > ৯ > ২)। কিন্তু ডিফল্টে Git v2.0.0-rc.1-কে v2.0.0-এর ওপরে রেখেছে, অর্থাৎ বড় ধরেছে; SemVer-এ pre-release সবসময় মূল release-এর চেয়ে ছোট। versionsort.suffix=- config Git-কে বলে, “- দিয়ে শুরু হওয়া শেষাংশ মূল version-এর আগে বসবে” — তখন ক্রম SemVer-এর সাথে মেলে। স্থায়ীভাবে চাইলে git config versionsort.suffix -, আর ডিফল্ট সাজানো চাইলে git config tag.sort -v:refname.
Push: git push ডিফল্টে tag পাঠায় না — tag আলাদা করে পাঠাতে হয় (remote-এর বিস্তারিত পর্ব ১৪ — Remote repository). এখানে remote হলো একটি local bare repository ../origin.git:
$ git push origin main
To ../origin.git
* [new branch] main -> main
$ git push origin v1.1.0
To ../origin.git
* [new tag] v1.1.0 -> v1.1.0
$ git push origin --tags
To ../origin.git
* [new tag] tested -> tested
* [new tag] v1.0.0 -> v1.0.0
* [new tag] v2.0.0-rc.1 -> v2.0.0-rc.1
$ git tag -d tested
Deleted tag 'tested' (was b6ea870)
$ git push origin --delete tested
To ../origin.git
- [deleted] testedgit push origin v1.1.0 একটি নির্দিষ্ট tag পাঠায়। --tags সব tag পাঠায় — lightweight বুকমার্ক tested-ও চলে গেল, যা আমরা চাইনি। তাই বেশি নিরাপদ git push --follow-tags: শুধু সেই annotated tag-গুলো যায়, যেগুলো push করা commit থেকে পৌঁছানো যায় (অংশ ৯-এ ব্যবহার; git config push.followTags true দিয়ে ডিফল্ট করা যায়)। ভুল tag মুছতে দুই জায়গায়: local-এ git tag -d, remote-এ git push origin --delete <tag>.
প্রকাশিত tag সরাবেন না। v1.1.0 একবার push হয়ে গেলে কেউ সেটি fetch করেছেন, কোনো build সেটি থেকে বানানো হয়েছে। এখন সেটি মুছে অন্য commit-এ একই নাম দিলে (git tag -f) দুই জনের কাছে “v1.1.0” দুই জিনিস হবে, আর Git সাধারণ fetch-এ আগে থেকে থাকা tag নীরবে বদলায় না। Git-এর নিজের documentation (git help tag-এর “On Re-tagging” অংশ) এ ব্যাপারে কড়া সতর্ক করে। ভুল release-এর সমাধান: নতুন version, যেমন v1.1.1.
৫ · git describe আর tag দিয়ে history পড়া
Tag দেওয়া commit-এর মাঝখানের commit-গুলোর নাম কী? একটি test build-এর গায়ে “9bc3d2d” লেখার চেয়ে “v1.1.0-এর পরে ২টি commit” অনেক বেশি তথ্য দেয়। git describe ঠিক এই নাম বানায়:
# HEAD = b6ea870, exactly at v1.1.0
$ git describe
v1.1.0
# two commits later (a7e0f71, then 9bc3d2d)
$ git describe
v1.1.0-2-g9bc3d2d
$ git describe --tags
v1.1.0-2-g9bc3d2d
$ git tag -a v2.0.0-rc.1 -m "Release candidate 2.0.0-rc.1"
$ git describe
v2.0.0-rc.1
# one more commit (1afe061)
$ git describe
v2.0.0-rc.1-1-g1afe061
$ git describe --long v1.1.0
v1.1.0-0-gb6ea870
$ git describe --abbrev=0 --exclude "*-*" 1afe061
v1.1.0Tag-এর ঠিক ওপরে থাকলে শুধু tag-এর নাম। দুই commit পরে v1.1.0-2-g9bc3d2d: tag, দূরত্ব ২, আর g (“git”) + বর্তমান commit-এর ছোট hash — যাতে নামটি থেকে commit-টিও খুঁজে পাওয়া যায়। ডিফল্টে describe শুধু annotated tag দেখে; --tags দিলে lightweight-ও (এখানে tested-ও একই commit-এ ছিল, তাই ফল একই)। --long tag-এর ওপরেও পুরো রূপ দেখায় (-0-g…)। --abbrev=0 শুধু tag-এর নাম দেয়, আর --exclude "*-*" pre-release tag বাদ দেয় — “শেষ স্থিতিশীল release কোনটি?” প্রশ্নের উত্তর, অংশ ৭-এ কাজে লাগবে। কোনো tag না থাকলে describe ব্যর্থ হয় (fatal: No tags can describe …); --always দিলে তখন শুধু ছোট hash. Commit না করা বদল থাকলে --dirty শেষে -dirty যোগ করে।
Commit-এর সময়রেখা, tag আর describe-এর নাম
Newest at the top, like git log; every label is real Git 2.53 output
1afe061), তারপর changelog আর v2.0.0.দুই tag-এর মাঝের commit-এর তালিকা — release notes-এর কাঁচামাল — দেখায় range syntax A..B: B থেকে পৌঁছানো যায় কিন্তু A থেকে নয় এমন commit (পর্ব ৯ — History আর পার্থক্য দেখা)।
$ git log --oneline v1.0.0..v1.1.0
b6ea870 feat(seat): let passengers ask for a window seat
30ee2e5 fix(fare): round fare to whole taka
$ git log --oneline --decorate
1afe061 (HEAD -> main) fix(fare): add the Dhaka-Khulna route
9bc3d2d (tag: v2.0.0-rc.1) test(fare): cover the route table
a7e0f71 feat(fare)!: read fares from a route table
b6ea870 (tag: v1.1.0, tag: tested) feat(seat): let passengers ask for a window seat
30ee2e5 fix(fare): round fare to whole taka
1674d4e (tag: v1.0.0) docs: add README
e6e2912 feat: add fare calculator১.০.০ থেকে ১.১.০-এ ঠিক দুটি commit: একটি feat, একটি fix. নতুন সুবিধা এসেছে, কিছু ভাঙেনি — তাই MINOR বাড়ানো (১.০.০ → ১.১.০) সঠিক ছিল। এই যুক্তির নিয়মই পরের অংশ।
৬ · Semantic Versioning 2.0.0 — version নম্বর একটি প্রতিশ্রুতি
Semantic Versioning (SemVer) লিখেছিলেন Tom Preston-Werner; ২.০.০ সংস্করণের spec প্রকাশ হয় ২০১৩ সালের দিকে। মূল ধারণা: version নম্বর দেখেই ব্যবহারকারী বুঝবেন হালনাগাদ করলে তাঁর কোড ভাঙবে কি না। এজন্য আগে একটি public API ঘোষণা করতে হয় — documentation-এ লেখা কোন function, command বা format অন্যরা ব্যবহার করবেন। তারপর MAJOR.MINOR.PATCH:
| অংশ | কখন বাড়ে | ডানের অংশ | উদাহরণ |
|---|---|---|---|
| MAJOR | Public API-তে backward-incompatible (পুরোনো ব্যবহার ভাঙে এমন) পরিবর্তন | MINOR আর PATCH শূন্য | 1.4.2 → 2.0.0 |
| MINOR | Backward-compatible নতুন সুবিধা; কোনো অংশ deprecated ঘোষণা করলেও | PATCH শূন্য | 1.4.2 → 1.5.0 |
| PATCH | শুধু backward-compatible bug fix | — | 1.4.2 → 1.4.3 |
- সংখ্যাগুলো অ-ঋণাত্মক পূর্ণসংখ্যা, সামনে শূন্য নিষেধ:
1.02.0বৈধ নয়। প্রতিটি অংশ আলাদা সংখ্যা:1.10.0>1.9.0. - প্রকাশিত version বদলায় না: একই নম্বরে নতুন কিছু প্রকাশ করা যাবে না; ভুল হলে নতুন version.
0.y.z= প্রাথমিক উন্নয়ন: যেকোনো সময় যেকোনো কিছু বদলাতে পারে; public API স্থিতিশীল ধরা যাবে না।1.0.0থেকে public API সংজ্ঞায়িত, প্রতিশ্রুতি শুরু।- Pre-release: PATCH-এর পরে
-আর ডট দিয়ে ভাগ করা identifier:1.0.0-alpha.1,2.0.0-rc.1. Identifier-এ শুধু[0-9A-Za-z-], ফাঁকা নয়, সংখ্যা হলে সামনে শূন্য নয়। Pre-release মানে “এখনো প্রতিশ্রুতি পূর্ণ নয়”; release candidate (rc) মানে “কোনো সমস্যা না পেলে এটিই মূল release হবে”। - Build metadata:
+আর identifier:1.0.0+build.5,1.0.0-rc.1+20260115. Precedence-এ পুরো উপেক্ষিত —1.0.0+build.5আর1.0.0একই অগ্রাধিকারের। - “v” উপসর্গ:
v1.2.3নিজে semantic version নয়; tag-এর নামে “v” লাগানো একটি প্রচলিত রীতি (SemVer-এর FAQ-ও তাই বলে)। Version হলো1.2.3.
Precedence — কোন version বড়
Precedence-এর নিয়ম spec-এ ধাপে ধাপে দেওয়া: (১) MAJOR, MINOR, PATCH বাঁ থেকে সংখ্যা হিসেবে তুলনা। (২) এগুলো সমান হলে, pre-release থাকা version ছোট: 1.0.0-rc.1 < 1.0.0. (৩) দুটিতেই pre-release থাকলে identifier বাঁ থেকে একটি একটি করে: দুটিই সংখ্যা হলে সংখ্যা হিসেবে; অক্ষর থাকলে ASCII ক্রমে; সংখ্যা-identifier অক্ষরেরটির চেয়ে ছোট; আর আগের সব সমান হলে যার identifier বেশি, সে বড়। নিচের প্রোগ্রাম spec-এর এই নিয়মগুলো সরাসরি বাস্তবায়ন করে:
// SemVer 2.0.0: parse MAJOR.MINOR.PATCH[-pre][+build] and compare by the spec's precedence rules.
const N = "(0|[1-9]\\d*)", ID = "(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)";
const RE = new RegExp(`^${N}\\.${N}\\.${N}(?:-(${ID}(?:\\.${ID})*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$`);
function parse(v) {
const m = RE.exec(v);
if (!m) return null; // e.g. leading zeros or a missing part
return { nums: [+m[1], +m[2], +m[3]], pre: m[4] ? m[4].split(".") : [], build: m[5] || "" };
}
function cmpId(a, b) { // compare one pre-release identifier
const na = /^\d+$/.test(a), nb = /^\d+$/.test(b);
if (na && nb) return Number(a) - Number(b); // numbers compare as numbers
if (na !== nb) return na ? -1 : 1; // numeric identifiers are lower
return a < b ? -1 : a > b ? 1 : 0; // otherwise ASCII order
}
function compare(x, y) {
const a = parse(x), b = parse(y);
for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] - b.nums[i];
if (!a.pre.length || !b.pre.length) return b.pre.length - a.pre.length; // release wins
for (let i = 0; i < Math.min(a.pre.length, b.pre.length); i++) {
const c = cmpId(a.pre[i], b.pre[i]);
if (c) return c;
}
return a.pre.length - b.pre.length; // more identifiers = higher
}
const list = ["1.0.0", "1.10.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.2.0", "1.0.0-alpha",
"1.0.0-beta", "1.0.0-alpha.beta", "1.0.0-beta.2", "1.0.0-alpha.1", "2.0.0-rc.1"];
console.log("text sort: " + [...list].sort().join(" "));
console.log("semver sort: " + [...list].sort(compare).join(" < "));
console.log("1.0.0+build.5 vs 1.0.0 ->", compare("1.0.0+build.5", "1.0.0"));
for (const bad of ["1.02.0", "1.0", "1.0.0-01", "v1.0.0"]) console.log(bad, "->", parse(bad));আসল আউটপুট:
text sort: 1.0.0 1.0.0-alpha 1.0.0-alpha.1 1.0.0-alpha.beta 1.0.0-beta 1.0.0-beta.11 1.0.0-beta.2 1.0.0-rc.1 1.10.0 1.2.0 2.0.0-rc.1
semver sort: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 < 1.2.0 < 1.10.0 < 2.0.0-rc.1
1.0.0+build.5 vs 1.0.0 -> 0
1.02.0 -> null
1.0 -> null
1.0.0-01 -> null
v1.0.0 -> null
লেখা হিসেবে সাজালে তিনটি ভুল: 1.0.0 সবার আগে (pre-release-গুলোর চেয়ে ছোট!), beta.11 আগে beta.2-এর, আর 1.10.0 আগে 1.2.0-এর। SemVer-এর ক্রম spec-এর নিজের উদাহরণের সাথে হুবহু মেলে: alpha < alpha.1 (আগের অংশ সমান, বেশি identifier বড়), alpha.1 < alpha.beta (সংখ্যা-identifier অক্ষরেরটির চেয়ে ছোট), beta.2 < beta.11 (সংখ্যা হিসেবে), rc.1 < 1.0.0 (pre-release ছোট)। 2.0.0-rc.1 সবার বড়, কারণ MAJOR আগে দেখা হয়। Build metadata-র তুলনা 0 — সমান। আর 1.02.0, 1.0, 1.0.0-01 (সামনে শূন্যওয়ালা সংখ্যা-identifier) ও v1.0.0 বৈধ SemVer নয়।
বিকল্প: Calendar Versioning
সব software-এর “public API” থাকে না। একটি অ্যাপ, একটি website বা একটি operating system-এর ব্যবহারকারী জানতে চান “কবেকার version?” — “কী ভাঙল” নয়। এমন ক্ষেত্রে অনেকে Calendar Versioning (CalVer) ব্যবহার করেন: তারিখভিত্তিক নম্বর। যেমন Ubuntu-র 24.04 মানে ২০২৪ সালের এপ্রিল (YY.MM). calver.org নামের একটি সাইটে প্রচলিত রূপগুলো (YYYY.MM.DD, YY.MINOR ইত্যাদি) সংকলিত আছে। CalVer কোনো একক নির্দিষ্ট spec নয়, একটি রীতি — প্রতিটি project নিজের format ঘোষণা করে। আর দুটি মেশানোও যায়: বছরভিত্তিক MAJOR, বাকিটা SemVer-এর মতো। কোনটি “সঠিক”, তা নিয়ে মতভেদ আছে; প্রশ্নটি হলো আপনার ব্যবহারকারীর কাছে কোন তথ্য বেশি জরুরি (ভাবনার প্রশ্ন ৩)।
৭ · Commit থেকে পরের version
এখন দুই অংশ জোড়া লাগাই। Commit-গুলো Conventional Commits মানলে পরের version যন্ত্র নিজেই হিসাব করতে পারে: শেষ release-এর পরের সব commit দেখুন, প্রতিটির “ধাক্কা” বের করুন, আর সবচেয়ে বড়টি নিন। নিচের stepper-এ আমাদের অ্যাপের শেষ স্থিতিশীল release v1.1.0-এর পরের তিনটি commit:
| Command / commit | |
| এই commit-এর ধাক্কা | |
| এখন পর্যন্ত সবচেয়ে বড় | |
| Version |
- শেষ স্থিতিশীল release খুঁজুন।
git describe --abbrev=0 --exclude "*-*"pre-release tag বাদ দিয়ে উত্তর দেয়v1.1.0. তারপরgit log --oneline v1.1.0..HEAD— তিনটি commit:a7e0f71,9bc3d2d,1afe061. a7e0f71:feat(fare)!:—!আছে, footer-এBREAKING CHANGE:-ও।fare(km)আর চলবে না, তাই MAJOR. বাকিগুলো যা-ই হোক, ফল আর এর চেয়ে বড় হতে পারে না — তবু পুরো তালিকা দেখা ভালো অভ্যাস।9bc3d2d:test(fare):— শুধু test; ব্যবহারকারীর কাছে কিছু বদলায়নি। ধাক্কা নেই, সবচেয়ে বড় এখনো MAJOR.1afe061:fix(fare):— PATCH. MAJOR-এর চেয়ে ছোট, তাই ফল বদলায় না।- ফল:
2.0.0. MAJOR বাড়লে MINOR আর PATCH শূন্য হয়: ১.১.০ → ২.০.০ (২.১.১ নয়)। Release-এর আগে দলটিv2.0.0-rc.1দিয়েছিল; rc-র পরে1afe061এসেছে বলে মূল release হবে নতুন commit থেকে (অংশ ৯)। - যদি project
0.x-এ থাকত। SemVer বলে ০.y.z-এ “যেকোনো কিছু বদলাতে পারে” — কোনো বাঁধা নিয়ম নেই। অনেক দল একটি রীতি মানে (কিছু টুলে এটি একটি option হিসেবে আছে): ১.০.০-এর আগে breaking change MINOR বাড়ায়, feat-ও MINOR, fix PATCH. তাহলে ০.৪.২ → ০.৫.০. এটি একটি বাছাই করা নিয়ম; দলকে লিখে ঘোষণা করতে হয়।
একই নিয়ম কোডে — Conventional Commits-এর spec feat/fix/breaking-এর অর্থ বাঁধে; perf-কে PATCH ধরা অনেক টুলের প্রচলিত রীতি:
// Next version from Conventional Commit messages (feat -> minor, fix/perf -> patch, breaking -> major).
const HEADER = /^(\w+)(?:\(([^)]*)\))?(!)?: (.+)$/;
function bumpOf(msg) {
const [first, ...rest] = msg.split("\n");
const m = HEADER.exec(first);
if (!m) return 0; // not a Conventional Commit: ignored
const breaking = m[3] === "!" || rest.some(l => /^BREAKING[ -]CHANGE: /.test(l));
if (breaking) return 3;
if (m[1] === "feat") return 2;
if (m[1] === "fix" || m[1] === "perf") return 1;
return 0; // docs, test, chore, ci ... no release
}
function next(version, messages) {
let [major, minor, patch] = version.split(".").map(Number);
let bump = Math.max(0, ...messages.map(bumpOf));
if (major === 0 && bump === 3) bump = 2; // a chosen 0.x rule: breaking -> minor
if (bump === 3) return `${major + 1}.0.0`;
if (bump === 2) return `${major}.${minor + 1}.0`;
if (bump === 1) return `${major}.${minor}.${patch + 1}`;
return version + " (no release)";
}
const sinceV110 = [
"feat(fare)!: read fares from a route table\n\nBREAKING CHANGE: fare(km) is now fare(from, to).",
"test(fare): cover the route table",
"fix(fare): add the Dhaka-Khulna route",
];
console.log("1.1.0 ->", next("1.1.0", sinceV110));
console.log("1.1.0 ->", next("1.1.0", ["fix(fare): round to taka", "docs: fix typo"]));
console.log("1.1.0 ->", next("1.1.0", ["feat(seat): add window seat", "fix: null seat"]));
console.log("0.4.2 ->", next("0.4.2", ["refactor!: rename pickSeat to chooseSeat"]));
console.log("1.1.0 ->", next("1.1.0", ["docs: add README", "Fixed stuff"]));আসল আউটপুট:
1.1.0 -> 2.0.0
1.1.0 -> 1.1.1
1.1.0 -> 1.2.0
0.4.2 -> 0.5.0
1.1.0 -> 1.1.0 (no release)
প্রথম লাইন stepper-এর ফল। দ্বিতীয়টিতে fix আর docs — PATCH. তৃতীয়টিতে feat আর fix — বড়টি MINOR, তাই ১.২.০ (PATCH শূন্য)। চতুর্থটি ০.x-এর বাছাই করা নিয়ম: breaking হলেও ০.৫.০. শেষটি সতর্কবার্তা: Fixed stuff Conventional Commit নয়, তাই যন্ত্র সেটিকে উপেক্ষা করল — হয়তো সত্যিই একটি bug fix ছিল, যা release-এ গেল না। স্বয়ংক্রিয় version-এর মান commit message-এর মানের চেয়ে ভালো হতে পারে না; এজন্যই অংশ ৩-এর linter.
৮ · Changelog আর release notes
Commit-এর ইতিহাস developer-দের জন্য। ব্যবহারকারী — আরেকজন developer যিনি আপনার library ব্যবহার করেন, বা অ্যাপের মালিক — জানতে চান: “এই version-এ আমার জন্য কী বদলাল, আর হালনাগাদ করলে কী করতে হবে?” এর উত্তর দুই রূপে আসে:
- Changelog: repository-তে একটি ফাইল (সাধারণত
CHANGELOG.md), যেখানে প্রতিটি version-এর পরিবর্তন নতুন থেকে পুরোনো ক্রমে জমা হয়। - Release notes: একটি নির্দিষ্ট release-এর জন্য লেখা ঘোষণা — প্রায়ই changelog-এর অংশ, সাথে হালনাগাদের নির্দেশনা, পরিচিত সমস্যা, ধন্যবাদ। অনেক hosting platform tag-এর সাথে release পাতা বানাতে দেয়।
Changelog-এর একটি জনপ্রিয় রীতি “Keep a Changelog” (keepachangelog.com, Olivier Lacan-এর শুরু করা; আমাদের জানা মতে এর format-এর সর্বশেষ সংস্করণ ১.১.০)। মূল কথা: changelog মানুষের জন্য, যন্ত্রের জন্য নয়; প্রতিটি version-এর শিরোনামে তারিখ (ISO 8601: 2026-01-15); ওপরে একটি [Unreleased] অংশ, যেখানে পরের release-এর পরিবর্তন জমে; আর পরিবর্তনগুলো ছয় দলে: Added, Changed, Deprecated, Removed, Fixed, Security. সবচেয়ে বড় পরামর্শ: “git log-এর কপি changelog নয়” — test, chore, “typo ঠিক” ব্যবহারকারীর কোনো কাজে লাগে না।
Conventional Commits থাকলে changelog-এর খসড়া যন্ত্র বানাতে পারে। নিচের প্রোগ্রাম v1.1.0..v2.0.0-এর চারটি আসল commit আর আরও বেশি দল দেখাতে দুটি কাল্পনিক commit (বানানো hash) নিয়ে Keep a Changelog-ধাঁচের শিরোনামে সাজায়, সবার ওপরে breaking change আলাদা করে:
// Changelog generator: group Conventional Commits by type under Keep a Changelog-style headings.
const commits = [ // newest first: 4 real commits of v1.1.0..v2.0.0 + 2 imagined (made-up hashes)
["d23ad56", "docs: add changelog for 2.0.0"],
["1afe061", "fix(fare): add the Dhaka-Khulna route"],
["9bc3d2d", "test(fare): cover the route table"],
["a7e0f71", "feat(fare)!: read fares from a route table\n\nBREAKING CHANGE: fare(km) is now fare(from, to)."],
["a1b2c3d", "perf(seat): stop rescanning taken seats"],
["9f8e7d6", "feat(seat): show seat numbers on the ticket"],
];
const SECTION = { feat: "Added", fix: "Fixed", perf: "Changed", refactor: "Changed" };
function changelog(version, date, list) {
const groups = { "BREAKING CHANGES": [], Added: [], Changed: [], Fixed: [] };
for (const [hash, msg] of list.slice().reverse()) { // oldest first
const m = /^(\w+)(?:\(([^)]*)\))?(!)?: (.+)$/.exec(msg.split("\n")[0]);
if (!m) continue;
const [, type, scope, bang, desc] = m;
const note = (/^BREAKING[ -]CHANGE: (.+)$/m.exec(msg) || [])[1];
const item = `- ${scope ? "**" + scope + ":** " : ""}${desc} (${hash})`;
if (bang || note) groups["BREAKING CHANGES"].push(`- ${note || desc} (${hash})`);
if (SECTION[type]) groups[SECTION[type]].push(item); // docs, test, chore: skipped
}
const out = [`## [${version}] - ${date}`];
for (const [title, items] of Object.entries(groups))
if (items.length) out.push("", `### ${title}`, ...items);
return out.join("\n");
}
console.log(changelog("2.0.0", "2026-01-15", commits));আসল আউটপুট:
## [2.0.0] - 2026-01-15
### BREAKING CHANGES
- fare(km) is now fare(from, to). (a7e0f71)
### Added
- **seat:** show seat numbers on the ticket (9f8e7d6)
- **fare:** read fares from a route table (a7e0f71)
### Changed
- **seat:** stop rescanning taken seats (a1b2c3d)
### Fixed
- **fare:** add the Dhaka-Khulna route (1afe061)
docs আর test commit বাদ পড়েছে — ব্যবহারকারীর কাছে অর্থহীন। a7e0f71 দুই জায়গায়: breaking অংশে footer-এর লেখা (কী ভাঙল), আর Added-এ নতুন সুবিধা হিসেবে। scope মোটা হরফে, শেষে ছোট hash, যাতে আগ্রহী পাঠক commit খুঁজে পান। কিন্তু খেয়াল করুন কী নেই: পুরোনো কোড কীভাবে নতুন fare(from, to)-তে বদলাবেন। “BREAKING CHANGES” শিরোনামটিও Keep a Changelog-এর ছয় দলের বাইরে — অনেক generator এমন আলাদা অংশ রাখে, রীতিটির নিজের ছয় দলে এমন পরিবর্তন সাধারণত Changed বা Removed-এ পড়ে। যন্ত্রের খসড়া শুরু, শেষ নয়; মানুষকে হালনাগাদের নির্দেশনা যোগ করতে হয়।
বাস্তবে এই কাজের জন্য তৈরি টুল আছে। লেখার সময়ে পরিচিত দুটি ধারা: একটি ধরন (semantic-release-এর মতো) CI-তে প্রতিটি merge-এর পরে commit পড়ে version ঠিক করে, tag দেয়, changelog লিখে package প্রকাশ করে — মানুষের হাত ছাড়াই; আরেক ধরন (release-please-এর মতো) পরিবর্তনগুলো জমিয়ে একটি “release pull request” খুলে রাখে, যেখানে প্রস্তাবিত version আর changelog থাকে, আর মানুষ সেটি merge করলে release হয়। কোন টুল, কোন option আর কোন platform — বিস্তারিত বদলায়; প্রতিটির documentation দেখে নিন। নীতি একই: commit message → version → changelog → tag.
৯ · Release process আর deprecation policy
একটি release মানে শুধু একটি tag নয়। ঈদের আগে বাসের টিকিট বিক্রির কথা ভাবুন: কাউন্টার খোলার আগের রাতে ভাড়ার তালিকা চূড়ান্ত হয় (আর বদলানো যায় না), কয়েকজন কর্মী পরীক্ষামূলক টিকিট কেটে দেখেন, তারপর সকালে কাউন্টার খোলে আর বোর্ডে নোটিশ ঝোলে “এবারের ভাড়া, নতুন রুট”। Software release-এর ধাপও এমন। নিচের stepper আমাদের অ্যাপের ২.০.০ release — প্রতিটি Git বার্তা আসল:
| ধাপ | |
| Command | |
| Git-এর বার্তা | |
| git describe |
- Freeze. দল ঠিক করে ২.০.০-এ কী যাবে — এখানে রুট-ভিত্তিক ভাড়া। এরপর feature freeze: এই release-এ আর নতুন সুবিধা নয়, শুধু fix. অনেক দল এজন্য একটি release branch খোলে (যেমন
release/2.0), যাতেmain-এ পরের কাজ চলতে পারে (পর্ব ১৭ — Branching strategy). আমাদের ছোট দল সরাসরিmain-এ freeze করেছে। - Release candidate. Annotated tag
v2.0.0-rc.1, তা থেকে build — কয়েকজন কাউন্টার-কর্মী পরীক্ষা করবেন। describe এখন শুধুv2.0.0-rc.1; ওই নামেই build চেনা যাবে। - Bug পাওয়া গেল, সারানো হলো। খুলনার রুট তালিকায় ছিল না। Fix commit
1afe061; describe-এv2.0.0-rc.1-1-g1afe061— “rc.1-এর পরে একটি পরিবর্তন”। বড় fix হলেrc.2দিয়ে আবার পরীক্ষা করা নিয়ম; এই এক লাইনের ডেটা-fix-এর পরে দল সরাসরি এগোল। - Changelog. অংশ ৮-এর মতো খসড়া বানিয়ে, পড়ে, ঠিক করে commit
d23ad56. describev2.0.0-rc.1-2-gd23ad56. - Tag আর push. Annotated
v2.0.0একেবারে সেই commit-এ, যা পরীক্ষিত।--follow-tagsbranch আর তার annotated tag একসাথে পাঠায়:1afe061..d23ad56 main -> mainআর[new tag] v2.0.0. গুরুত্বপূর্ণ project-এ tag-টি sign করা হয় (পর্ব ৫৭)। - Build আর publish. CI ঠিক tag-এর commit checkout করে (detached HEAD-এ, পর্ব ১২) build artifact বানায় — package, অ্যাপের ফাইল, container image — আর প্রকাশ করে (package registry, app store, server). Artifact সবসময় tag থেকে, কারও নিজের কম্পিউটারের folder থেকে নয়; যাতে যেকোনো সময় একই জিনিস আবার বানানো যায়। Deploy-এর ধাপ পর্ব ৫৯-এ।
- Release notes আর ঘোষণা। Changelog-এর ২.০.০ অংশ, সাথে হালনাগাদের নির্দেশনা (“
fare(km)-এর জায়গায়fare(from, to)”) — hosting platform-এর release পাতায় আর ব্যবহারকারীদের কাছে। Release শেষ; পরের কাজ[Unreleased]-এ জমতে শুরু করে।
$ git commit -m "docs: add changelog for 2.0.0"
[main d23ad56] docs: add changelog for 2.0.0
1 file changed, 12 insertions(+)
create mode 100644 CHANGELOG.md
$ git tag -a v2.0.0 -m "Release 2.0.0"
$ git push --follow-tags origin main
To ../origin.git
1afe061..d23ad56 main -> main
* [new tag] v2.0.0 -> v2.0.0
$ git describe
v2.0.0
$ git tag --sort=-v:refname
v2.0.0-rc.1
v2.0.0
v1.1.0
v1.0.0
$ git -c versionsort.suffix=- tag --sort=-v:refname
v2.0.0
v2.0.0-rc.1
v1.1.0
v1.0.0
$ git ls-remote --tags origin
2db868b2a79f3683196bd8a36e9fe1745b43a94a refs/tags/v1.0.0
1674d4ed4d89b641038fddbc3d7b21bdee028dbf refs/tags/v1.0.0^{}
…
aa07a6e1c9e98f686d4b50b3f619611f8813dc70 refs/tags/v2.0.0
d23ad56f42ecd90311362bd89ef130f4dbf156b1 refs/tags/v2.0.0^{}
…--follow-tags ঠিক যা চাই তাই পাঠিয়েছে: branch-এর নতুন commit আর v2.0.0. আবার দেখুন Git-এর ডিফল্ট version sort v2.0.0-rc.1-কে ওপরে রাখে; versionsort.suffix=- দিলে মূল release ওপরে। git ls-remote --tags remote-এর প্রতিটি annotated tag দুই লাইনে দেখায়: refs/tags/v2.0.0-এর পাশে tag object-এর hash (aa07a6e…), আর refs/tags/v2.0.0^{} (“peeled”, অর্থাৎ খোসা ছাড়ানো)-এর পাশে আসল commit d23ad56…. Lightweight tag-এর শুধু একটি লাইন থাকত।
Deprecation policy — ভাঙার আগে সতর্ক করা
Deprecation মানে কোনো সুবিধা “এখনো কাজ করে, কিন্তু ভবিষ্যতে সরানো হবে” — এই ঘোষণা। রেলের সময়সূচি বদলানোর আগে স্টেশনে কয়েক সপ্তাহ নোটিশ ঝোলে; হঠাৎ বদলালে যাত্রীরা ট্রেন হারান। SemVer-এর FAQ-এর পরামর্শ: কিছু deprecated করলে documentation হালনাগাদ করুন, deprecation-টি একটি MINOR release-এ দিন, আর সরানোর কাজ পরের MAJOR-এ — যাতে ব্যবহারকারী অন্তত একটি MINOR version-এ সতর্কবার্তা দেখেন। একটি লিখিত policy-তে সাধারণত থাকে:
- কীভাবে জানানো হবে: changelog-এর Deprecated অংশ, documentation, আর চালানোর সময় সতর্কবার্তা (যেমন “fare(km) is deprecated; use fare(from, to)”).
- কতদিন: কমপক্ষে কয়টি MINOR release বা কত মাস পুরোনো রূপ চলবে, আর কোন MAJOR-এ সরানো হবে।
- বিকল্প: প্রতিটি deprecated জিনিসের পাশে নতুন পথ আর হালনাগাদের নির্দেশনা।
- সমর্থনের সময়: পুরোনো MAJOR (যেমন ১.x) কতদিন security fix পাবে। অনেক বড় project “long-term support” (LTS) version ঘোষণা করে।
আমাদের অ্যাপের দল ভুলটি করেছিল: fare(km) সরাসরি ২.০.০-এ সরানো হয়েছে, আগে deprecated না করে। ভালো পথ হতো: ১.২.০-এ fare(from, to) যোগ (MINOR) আর fare(km) deprecated, তারপর ২.০.০-এ সরানো। Web API-র ক্ষেত্রে version আর deprecation-এর আলোচনা System Design কোর্সে — REST API নকশা, আর deploy ও rollout — Deployment আর release.
আরও গভীরে: Commit message-এর রীতির শিকড় Git-এর email-ভিত্তিক কাজের ধারায়: git format-patch প্রতিটি commit-কে একটি email বানায়, যার subject হয় commit-এর প্রথম লাইন — তাই ছোট subject আর ফাঁকা লাইন। Linux kernel-এর “Submitting patches” নির্দেশিকা আর Git-এর Documentation/SubmittingPatches imperative mood আর Signed-off-by-এর নির্ভরযোগ্য উৎস; DCO-র লেখা developercertificate.org-এ। Trailer-এর নিয়ম git help interpret-trailers-এ, tag-এর git help tag-এ, আর SemVer ও Conventional Commits-এর পূর্ণ spec (সাথে BNF ব্যাকরণ) semver.org ও conventionalcommits.org-এ — ছোট, এক বসায় পড়া যায়।
পরীক্ষা ও ইন্টারভিউতে:
- Lightweight আর annotated tag-এর পার্থক্য? — lightweight শুধু commit-এর দিকে একটি ref; annotated একটি আলাদা tag object (tagger, তারিখ, বার্তা, ঐচ্ছিক স্বাক্ষর)। Release-এ annotated.
- SemVer-এ কখন MAJOR বাড়ে? — Public API-তে backward-incompatible পরিবর্তনে; তখন MINOR আর PATCH শূন্য হয়।
v1.4.0-3-g1a2b3c4মানে? —git describe: সবচেয়ে কাছের annotated tag v1.4.0, তার পরে ৩টি commit, বর্তমান commit-এর ছোট hash 1a2b3c4.- ভালো commit message কেমন? — ~৫০ অক্ষরের imperative subject, ফাঁকা লাইন, ~৭২-এ ভাঙা body যা কেন বলে, শেষে issue/trailer.
git commit -sকি commit sign করে? — না; শুধুSigned-off-bytrailer যোগ করে (DCO-তে সম্মতি)। Cryptographic স্বাক্ষর-S.
১০ · AI-এর যুগে — commit message আর release-এ AI-কে যাচাই
অনেক editor আর AI assistant এখন staged diff দেখে commit message লিখে দেয়, changelog বানায়, release command সাজেস্ট করে। সময় বাঁচে, কিন্তু এই বিষয়ে কিছু চেনা ভুল আছে:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| বার্তা শুধু diff-এর বর্ণনা (“Update fare.js”) — কেন নেই, কারণ diff-এ কারণ থাকে না | “কেন” আপনি জানেন; body-তে নিজে লিখুন, issue-এর সূত্র দিন |
একটি breaking change-কে feat বা refactor বলা, ! বাদ দেওয়া | Public API-র signature বা আচরণ বদলেছে কি না নিজে দেখুন; test-এ পুরোনো ব্যবহার চালান |
ভুল tag সারাতে git tag -f আর git push --force — প্রকাশিত tag সরানোর বিপদ না বলে | প্রকাশিত tag কখনো সরাবেন না; নতুন PATCH version দিন (git help tag, “On Re-tagging”) |
অস্তিত্বহীন flag (যেমন git tag --semver) বা ভুল আচরণের দাবি (“git push tag-ও পাঠায়”) | git help tag, git help push; একটি throwaway clone-এ চালিয়ে দেখুন |
| SemVer-এর নিয়ম নিয়ে আত্মবিশ্বাসী ভুল: “1.0.0-rc.1 > 1.0.0”, “build metadata বড় করে”, “0.x-এ MAJOR বাড়ান” | semver.org-এর spec মিলিয়ে দেখুন; অংশ ৬-এর মতো ছোট প্রোগ্রামে যাচাই |
| Changelog-এ বানানো entry বা ভুল issue নম্বর — commit-এ যা নেই | প্রতিটি লাইন git log A..B-এর একটি commit-এর সাথে মেলান |
Diff পাঠাতে গিয়ে .env, token বা গ্রাহকের তথ্য prompt-এ | পাঠানোর আগে diff পড়ুন, গোপন মান মুছুন (পর্ব ৫৭) |
নিচের prompt-এ AI-কে diff-এর সাথে “কেন”-ও দেওয়া হয়েছে, আর তার কাজ সীমিত করা হয়েছে: শুধু খসড়া, প্রতিটি সিদ্ধান্তের কারণসহ — যাতে আপনি যাচাই করতে পারেন।
AI-এর লেখা commit message বা changelog হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Commit message-এর ৫০/৭২ আর imperative mood — এগুলো কি নিছক রুচির ব্যাপার, নাকি এর পেছনে প্রযুক্তিগত ইতিহাস আছে?
ইতিহাস আছে, আর সেটা Git-এর জন্মের সাথে জড়িত। Linux kernel-এর কাজ বহু বছর ধরে email-এ চলে: একজন developer পরিবর্তনটিকে একটি patch হিসেবে mailing list-এ পাঠান, অন্যরা উত্তরে review করেন, maintainer patch প্রয়োগ করেন। ২০০৫ সালে Linus Torvalds Git বানানোর সময় এই ধারা মাথায় রেখেছিলেন। git format-patch প্রতিটি commit-কে একটি email বানায়: প্রথম লাইন হয় email-এর subject, বাকিটা email-এর শরীর। আর git am উল্টো কাজ করে — email থেকে commit. তাই subject ছোট হওয়া দরকার (mail client-এর তালিকায় আঁটতে), আর subject-এর পরে ফাঁকা লাইন — email-এর header আর body আলাদা করার মতো।
৭২ অক্ষরের কারণ terminal. পুরোনো terminal আর অনেক মানুষের অভ্যাসে লাইন ৮০ ঘরের। git log বার্তার লাইন ৪ ঘর সরিয়ে দেখায়, আর email-এ উত্তর দেওয়ার সময় উদ্ধৃতির > চিহ্ন আরও ঘর খায়। ৭২-এ ভাঙা লেখা এই সবের পরেও ৮০-র মধ্যে থাকে। Git নিজে লাইন ভাঙে না — তাই হাতে (বা editor-এর সেটিংসে) ভাঙতে হয়। ২০০৮ সালের দিকে Tim Pope-এর একটি ছোট blog লেখা এই রীতিগুলো সাধারণ developer-দের মধ্যে জনপ্রিয় করে; পরে Chris Beams-এর “How to Write a Git Commit Message” (২০১৪ সালের দিকে) সাতটি নিয়মে গুছিয়ে দেয়।
Imperative mood-এর পক্ষে Git-এর নিজের নির্দেশিকা বলে: বার্তা লিখুন যেন codebase-কে আদেশ দিচ্ছেন — “make xyzzy do frotz”, “[This patch] makes xyzzy…” নয়। কারণ Git নিজেই এই রূপে লেখে: Merge branch 'x', Revert "…". সব বার্তা একই ব্যাকরণে থাকলে git log --oneline পড়তে একটি করণীয়-তালিকার মতো লাগে।
তবে এগুলো আইন নয়। অনেক platform-এর web interface লম্বা subject ছেঁটে দেখায়, তাই ৫০ ছাড়ালে ক্ষতি সামান্য; আর বাংলায় লেখা বার্তায় “imperative” নিয়মের সরাসরি অর্থ নেই। আসল নীতি তিনটি: প্রথম লাইন একা পড়ে বোঝা যাবে, body কারণ বলবে, আর পুরো দল একই রীতি মানবে — যাতে যন্ত্র আর মানুষ দুজনেই history পড়তে পারে।
মূল উপলব্ধি: ৫০/৭২ আর imperative mood এসেছে Git-এর email-ভিত্তিক patch-ধারা আর ৮০ ঘরের terminal থেকে। প্রযুক্তি বদলালেও নীতিটি টেকে: ছোট, স্বয়ংসম্পূর্ণ শিরোনাম আর কারণসহ বিবরণ।
প্র ০২SemVer-কে বলা হয় একটি “প্রতিশ্রুতি”। কিন্তু কোন পরিবর্তন “breaking”, তা কি সবসময় পরিষ্কার? একটি bug fix-ও কি কাউকে ভাঙতে পারে?
পারে, আর এটাই SemVer-এর সবচেয়ে কঠিন প্রশ্ন। আমাদের অ্যাপের fix(fare): round fare to whole taka ভাবুন: ভাড়া আগে 387.5 আসত, এখন 388. Spec-এর ভাষায় এটি bug fix — documentation-এ বলা ছিল ভাড়া পূর্ণ টাকায় হবে। কিন্তু কেউ যদি পুরোনো ভগ্নাংশ ধরে নিয়ে একটি হিসাব-রিপোর্ট বানিয়ে থাকেন, তাঁর রিপোর্ট বদলে যাবে। তাঁর চোখে এটি breaking.
Google-এর প্রকৌশলী Hyrum Wright-এর নামে পরিচিত একটি পর্যবেক্ষণ (“Hyrum’s Law”) বলে, মোটামুটি: একটি API-র ব্যবহারকারী যথেষ্ট বেশি হলে, documentation-এ কী প্রতিশ্রুতি দেওয়া আছে তা আর গুরুত্বপূর্ণ থাকে না — system-এর যেকোনো চোখে পড়া আচরণের ওপর কেউ না কেউ নির্ভর করবেন। Error বার্তার লেখা, তালিকার ক্রম, গতি — সবই। তাহলে প্রায় যেকোনো পরিবর্তনই কাউকে ভাঙতে পারে।
SemVer এর উত্তর দেয় সংজ্ঞা দিয়ে: প্রতিশ্রুতি শুধু ঘোষিত public API-র ওপর। Spec-এর প্রথম নিয়মই হলো public API ঘোষণা করা। যা documentation-এ নেই, তার ওপর নির্ভর করা ব্যবহারকারীর ঝুঁকি। Spec-এর FAQ-ও বলে, ভুল করে breaking change একটি MINOR-এ প্রকাশ হয়ে গেলে, দ্রুত একটি নতুন MINOR দিয়ে আগের আচরণ ফিরিয়ে আনুন আর পরিবর্তনটি পরের MAJOR-এ দিন — প্রকাশিত version বদলানো যাবে না।
এর বাস্তব গুরুত্ব dependency ব্যবস্থায়। npm-এর মতো package manager-এ ^1.4.0 লিখলে মানে “১.x-এর যেকোনো নতুন version (১.৪.০ বা তার পরের) নেওয়া যাবে” — অর্থাৎ পুরো ব্যবস্থা ধরে নেয় MINOR আর PATCH কিছু ভাঙবে না। একজন maintainer একটি PATCH-এ ভুল করে কিছু ভাঙলে হাজারো project-এর build একসাথে লাল হতে পারে। এজন্যই lockfile (পর্ব ১০) ঠিক কোন version install হয়েছিল তা আটকে রাখে, আর হালনাগাদ হয় ইচ্ছে করে, test চালিয়ে। SemVer প্রতিশ্রুতি দেয়; lockfile আর test সেই প্রতিশ্রুতি যাচাই করে।
মূল উপলব্ধি: “Breaking” নির্ভর করে কী প্রতিশ্রুতি দেওয়া হয়েছে তার ওপর — তাই public API স্পষ্ট করে লিখে রাখা SemVer-এর পূর্বশর্ত। আর ব্যবহারকারী হিসেবে SemVer-কে বিশ্বাস করুন, কিন্তু lockfile আর test দিয়ে যাচাইও করুন।
প্র ০৩SemVer, CalVer আর “বাজারের নম্বর” — কোন ধরনের software-এ কোনটি মানায়? বড় project-গুলো কী বেছে নিয়েছে?
প্রশ্নটি আসলে: version নম্বর দিয়ে ব্যবহারকারীকে কোন তথ্য দিতে চান? একটি library-র ব্যবহারকারী একজন developer, যিনি জানতে চান “হালনাগাদ করলে আমার কোড ভাঙবে কি?” — SemVer ঠিক এই প্রশ্নের উত্তর। এজন্য অনেক package ecosystem (npm, Rust-এর Cargo ইত্যাদি) SemVer-কে কেন্দ্র করে dependency-র নিয়ম বানিয়েছে।
একটি operating system বা বড় অ্যাপের ব্যবহারকারীর প্রশ্ন ভিন্ন: “এটা কত পুরোনো? কতদিন সমর্থন পাবে?” Ubuntu তাই বছর আর মাস দিয়ে নাম দেয় — ২৪.০৪ মানে ২০২৪ সালের এপ্রিল; এপ্রিলের জোড়-বছরের release-গুলো দীর্ঘমেয়াদি সমর্থন (LTS) পায় বলে Ubuntu জানায়। এখানে CalVer স্বাভাবিক, কারণ OS-এ “public API ভাঙল কি না” প্রশ্নের একক উত্তর নেই — হাজারো অংশ একসাথে বদলায়।
আবার কিছু project-এর নম্বর কোনো নিয়মই মানে না। Linux kernel-এর version (যেমন ৩.০ থেকে ৬.x) SemVer নয়: kernel-এর নীতি হলো user space ভাঙা যাবে না, তাই MAJOR বাড়ানোর “breaking” অর্থ নেই। Linus Torvalds নিজে বলেছেন, MINOR সংখ্যা বেশি বড় হয়ে গেলে তিনি MAJOR বাড়ান — ২০১১ সালে ৩.০ এসেছিল Linux-এর ২০ বছর পূর্তির কাছাকাছি সময়ে। Web browser-গুলো কয়েক সপ্তাহ পর পর MAJOR বাড়ায়; সেখানে নম্বর শুধু ক্রম বোঝায়।
সমস্যা হয় তখন, যখন একটি project দেখতে SemVer-এর মতো নম্বর ব্যবহার করে কিন্তু নিয়ম মানে না — ব্যবহারকারী ভুল ধারণা করেন। তাই README-তে এক লাইনে লিখে দেওয়া ভালো: “এই project Semantic Versioning 2.0.0 মানে” বা “version-এর format YYYY.MM.PATCH”. Keep a Changelog-এর রীতিও changelog-এর শুরুতে এই ঘোষণা রাখতে বলে।
মূল উপলব্ধি: Version নম্বর একটি যোগাযোগ। Library-র জন্য SemVer (কী ভাঙল), দ্রুত বা নির্দিষ্ট সময়ে release হওয়া পণ্যের জন্য প্রায়ই CalVer (কবেকার) — আর যেটাই বাছেন, লিখে ঘোষণা করুন আর মেনে চলুন।
প্র ০৪Tag-কে বলা হয় “স্থায়ী নাম”, অথচ git tag -f দিয়ে সরানো যায়, আর platform-এ মোছাও যায়। এতে নিরাপত্তা আর নির্ভরযোগ্যতার কী ঝুঁকি?
Git-এর ভেতরে tag একটি সাধারণ ref — refs/tags/v1.1.0 নামের একটি ফাইল বা তালিকার ঘর, যাতে একটি hash লেখা। যিনি repository-তে লিখতে পারেন, তিনি এটি মুছে অন্য commit-এ বসাতে পারেন। “স্থায়ী” কথাটি তাই প্রযুক্তিগত নিশ্চয়তা নয়, একটি সামাজিক চুক্তি: “আমরা প্রকাশিত tag সরাব না”। Git-এর documentation-এর “On Re-tagging” অংশ এ নিয়ে স্পষ্ট: ভুল tag প্রকাশ হয়ে গেলে সবচেয়ে সৎ পথ হলো স্বীকার করে নতুন নাম দেওয়া; আর যদি সরাতেই হয়, সবাইকে জানাতে হবে, কারণ Git আগে fetch করা tag নিজে থেকে বদলায় না — দুই জনের কাছে একই নামে দুই commit থেকে যাবে।
নির্ভরযোগ্যতার ঝুঁকি স্পষ্ট: একটি bug report-এ “v1.1.0-এ সমস্যা” লেখা থাকলে সেটি কোন কোড, তা আর নিশ্চিত নয়। নিরাপত্তার ঝুঁকি আরও গভীর। অনেক build ব্যবস্থা আর CI config অন্য project-এর কোড tag-এর নাম দিয়ে আনে। কেউ যদি একটি জনপ্রিয় project-এর account-এ ঢুকে একটি পুরোনো tag সরিয়ে ক্ষতিকর commit-এ বসান, তাহলে যাঁরা ওই tag-এর নাম দিয়ে কোড নেন, তাঁরা পরের build-এ নীরবে ক্ষতিকর কোড পাবেন। Software supply chain-এর এমন আক্রমণ নিয়ে সাম্প্রতিক বছরগুলোতে অনেক আলোচনা হয়েছে; তাই কিছু নিরাপত্তা-নির্দেশিকা CI-তে বাইরের কোড tag-এর বদলে পুরো commit hash দিয়ে “pin” করতে বলে।
প্রতিরক্ষা কয়েক স্তরে। এক, annotated tag sign করা (git tag -s) আর ব্যবহারের আগে git tag -v দিয়ে যাচাই — তাহলে জাল tag ধরা পড়ে (পর্ব ৫৭)। দুই, hosting platform-এ tag সুরক্ষা: কিছু platform নির্দিষ্ট pattern-এর tag মোছা বা সরানো শুধু নির্দিষ্ট মানুষকে দেয় (নাম আর সুবিধা platform-ভেদে আলাদা)। তিন, release-এর artifact-এর সাথে checksum প্রকাশ, যাতে ব্যবহারকারী মিলিয়ে নিতে পারেন। চার, নিজের দলের নিয়ম: প্রকাশিত tag-এ কেউ -f দেবেন না।
মূল উপলব্ধি: Tag-এর স্থায়িত্ব আসে চুক্তি আর যাচাই থেকে, Git থেকে নয়। প্রকাশিত tag কখনো সরাবেন না, গুরুত্বপূর্ণ release sign করুন, আর বাইরের কোড নির্ভরযোগ্যভাবে আনতে hash দিয়ে pin করার কথা ভাবুন।
প্র ০৫Commit message থেকে পুরো release স্বয়ংক্রিয় করা — এর লাভ কী, আর কোথায় এটা ভুল পথে নিয়ে যেতে পারে?
লাভ বড়। হাতে release করলে মানুষ ভোলেন — changelog হালনাগাদ, tag push, ঠিক version বাছাই। আর release “ঝামেলার কাজ” হলে মানুষ সেটি কম করেন, ফলে প্রতিটি release-এ অনেক পরিবর্তন জমে, ঝুঁকি বাড়ে। Continuous delivery-র মূল যুক্তিও এটাই: ছোট, ঘন ঘন, স্বয়ংক্রিয় release নিরাপদ (পর্ব ৫৯)। Conventional Commits এখানে যন্ত্রকে একটি ভাষা দেয়: প্রতিটি commit নিজেই বলে দেয় সে MAJOR, MINOR না PATCH.
কিন্তু এতে commit message আর মুক্ত লেখা থাকে না — সেটি হয়ে যায় release ব্যবস্থার input, প্রায় একটি API. একজন ! দিতে ভুলে গেলে breaking change একটি MINOR-এ চলে যায়, আর ব্যবহারকারীর build ভাঙে। আবার কেউ সামান্য বিষয়ে ! দিলে অপ্রয়োজনীয় MAJOR, আর ব্যবহারকারী ভয়ে হালনাগাদ করেন না। আমাদের next-version প্রোগ্রামে Fixed stuff নীরবে উপেক্ষিত হয়েছিল — একটি fix release-এ যায়নি।
Squash merge আরেকটি ফাঁদ। অনেক দল pull request squash করে, আর তখন মূল branch-এ যায় একটিমাত্র commit, যার বার্তা প্রায়ই pull request-এর শিরোনাম (platform-ভেদে আলাদা; পর্ব ১৬)। ভেতরের commit-গুলো যত সুন্দরই হোক, যন্ত্র দেখবে শুধু ওই শিরোনাম। তাই এমন দল সাধারণত pull request-এর শিরোনাম lint করে। আরেকটি দিক: Conventional Commits-এর সমালোচকেরা বলেন, type-এর উপসর্গ subject-এর মূল্যবান জায়গা খায়, আর “feat না fix” বিতর্কে সময় যায়। এগুলো সত্যিকারের খরচ।
সবচেয়ে বড় সীমা: যন্ত্র “কী বদলেছে” লিখতে পারে, “ব্যবহারকারীকে এখন কী করতে হবে” নয়। একটি ভালো release note-এ হালনাগাদের ধাপ, ঝুঁকি আর প্রেক্ষাপট থাকে, যা commit-এর ছোট লাইনে আঁটে না। তাই অনেক দল মাঝামাঝি পথ নেয়: যন্ত্র version আর খসড়া বানায়, মানুষ release pull request-এ পড়ে, ঠিক করে, তবেই release.
মূল উপলব্ধি: স্বয়ংক্রিয় release-এর মান commit message-এর শৃঙ্খলার সমান। Linter আর review দিয়ে সেই শৃঙ্খলা রাখুন, আর ব্যবহারকারীর জন্য লেখা অংশে মানুষের চোখ রাখুন।
অনুশীলন
আউটপুট অনুমান, commit message নতুন করে লেখা, release-এর command আর একটি নীতির সমালোচনা — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।
-
আউটপুট অনুমান করুন: একটি repository-তে এই ছয়টি tag আছে:
v2.0.0,v2.0.0-rc.2,v1.9.9,v2.0.0-rc.10,v2.0.0-beta,v2.0.0-rc.1. (ক) SemVer 2.0.0 অনুযায়ী ছোট থেকে বড় ক্রম কী? (খ) Git ২.৫৩-এgit tag --sort=v:refnameকী ছাপবে? (গ)git -c versionsort.suffix=- tag --sort=v:refname? (ঘ)2.0.0-এর পরে commit-গুলোdocs: fix typo,fix(seat): allow last seat,feat(fare): add student discountহলে পরের version?(ক)
1.9.9 < 2.0.0-beta < 2.0.0-rc.1 < 2.0.0-rc.2 < 2.0.0-rc.10 < 2.0.0. যুক্তি: ১.৯.৯-এর MAJOR ছোট। বাকিগুলোর ২.০.০ সমান; pre-release-ওয়ালারা ছোট।beta<rc(ASCII-তে b < r).rc.2<rc.10, কারণ ২ আর ১০ সংখ্যা হিসেবে তুলনা হয়। অংশ ৬-এর প্রোগ্রামে চালালে (একটি2.0.0+exp.5যোগ করে) আসল ফল:1.9.9 < 2.0.0-beta < 2.0.0-rc.1 < 2.0.0-rc.2 < 2.0.0-rc.10 < 2.0.0 < 2.0.0+exp.5— শেষ দুটির তুলনা আসলে ০ (সমান); stable sort মূল তালিকার ক্রম রেখেছে।(খ) আর (গ) — Git ২.৫৩-এর আসল আউটপুট:
$ git tag --sort=v:refname v1.9.9 v2.0.0 v2.0.0-beta v2.0.0-rc.1 v2.0.0-rc.2 v2.0.0-rc.10 $ git -c versionsort.suffix=- tag --sort=v:refname v1.9.9 v2.0.0-beta v2.0.0-rc.1 v2.0.0-rc.2 v2.0.0-rc.10 v2.0.0ডিফল্টে Git
--এর পরের অংশকে “বাড়তি” ধরেv2.0.0-কে pre-release-গুলোর আগে রাখে — SemVer-এর উল্টো। তবেrc.2আরrc.10দুই ক্ষেত্রেই সংখ্যা হিসেবে ঠিক সাজানো। Suffix config দিলে ক্রম (ক)-এর সাথে হুবহু মেলে।(ঘ) docs → কিছু নয়, fix → PATCH, feat → MINOR; সবচেয়ে বড় MINOR → ২.১.০ (PATCH শূন্য)।
-
এই commit message-গুলো নতুন করে লিখুন (Conventional Commits, ইংরেজি, ৫০/৭২)। প্রতিটির পাশে আসল পরিবর্তনটি বন্ধনীতে দেওয়া:
(১)changes(বাসের ছাড়ার সময় ১২-ঘণ্টা থেকে ২৪-ঘণ্টা format-এ দেখানো, আর টিকিটের PDF-এ একটি বানান ভুল ঠিক)
(২)Fixed bug where app crashed.(খালি seat list পেলেpickSeat()crash করত; issue #58)
(৩)feat: Removed the old /book endpoint, use /bookings now and also updated docs for it(পুরোনো API endpoint সরানো)
(৪)wip(refund-এর হিসাবের নতুন function, অর্ধেক লেখা, test নেই)
(৫)update deps(একটি PDF library ২.x থেকে ৩.x-এ হালনাগাদ, lockfile বদল; Karim-এর সাথে বসে করা)(১) দুটি আলাদা উদ্দেশ্য → দুটি commit (
git add -p):feat(schedule): use 24-hour departure times Passengers misread 12-hour times for night buses (9:30 vs 21:30). fix(ticket): correct "Departure" typo in ticket PDFপ্রথমটি
featনাfix? আচরণ বদলেছে, নতুন সুবিধা — feat যুক্তিসংগত; দল যদি আগের format-কে bug মনে করে, fix-ও চলে। গুরুত্বপূর্ণ হলো body-র কারণ।(২) অতীত কাল, দাঁড়ি, কোনো তথ্য নেই:
fix(seat): return null when no seat is free pickSeat() threw on an empty seat list, which crashed the booking screen on fully booked Eid buses. Return null and let the caller show "sold out". Fixes: #58(৩) Endpoint সরানো মানে পুরোনো ব্যবহারকারী ভাঙবে — breaking. আর “Removed” অতীত কাল, subject ৮৪ অক্ষর, “and also” দুই কাজ (docs আলাদা হলে ভালো, তবে একই পরিবর্তনের docs একসাথে রাখাও গ্রহণযোগ্য):
feat(api)!: remove the deprecated /book endpoint /book was deprecated in 1.6.0 in favour of /bookings. BREAKING CHANGE: clients must call /bookings instead of /book.(যদি আগে deprecated না হয়ে থাকে, তাহলে বার্তা নয়, পরিকল্পনাই ভুল — আগে একটি MINOR-এ deprecate করা উচিত।)
(৪) এটি নতুন বার্তা দিয়ে সারানো যায় না: অর্ধেক কাজ, test নেই — মূল branch-এ যাওয়ার মতো commit নয়। নিজের branch-এ থাকলে চলবে; ভাগ করার আগে interactive rebase-এ (
fixup/squash) পরের commit-এর সাথে মেলান (পর্ব ১৫), আর শেষ রূপ:feat(refund): refund by hours before departure, সাথে test.(৫) MAJOR হালনাগাদ — অন্তত body-তে কারণ আর ঝুঁকি লিখুন; সহলেখক trailer-এ:
build(deps): upgrade PDF library from 2.x to 3.x 2.x no longer receives security fixes. 3.x changed the font API, so ticket.js now loads fonts through registerFont(). Co-authored-by: Karim <karim@example.com>আপনার অ্যাপের public API না বদলালে এটি আপনার version-এ breaking নয় — dependency-র MAJOR মানেই আপনার MAJOR নয়।
-
Command-এর ক্রম আর describe-এর হিসাব: একটি repository-তে শেষ annotated tag
v1.1.0, আর HEAD-এgit describeদেখাচ্ছেv1.1.0-3-g5e6f7a8. ওই তিনটি commit:feat(seat): add ladies seat filter,fix: handle empty route,ci: cache npm. (ক) “৩” আর “5e6f7a8” কী বোঝায়? (খ) পরের version কী, আর release দিতে কোন command-গুলো (changelog commit, tag, push) কোন ক্রমে? (গ) Tag দেওয়ার ঠিক পরে, আর তারপর আরও দুটি commit হলেgit describeকী দেখাবে? (ঘ) Tag-এর বার্তায় ভুল দেখা গেল, কিন্তু push এখনো হয়নি — কী করবেন? আর push হয়ে গেলে?(ক) ৩ = v1.1.0-এর পরে HEAD পর্যন্ত ৩টি commit;
g= git,5e6f7a8= HEAD-এর ছোট hash.(খ) feat → MINOR (fix আর ci ছোট) → ১.২.০. ক্রম:
git log --oneline v1.1.0..HEAD # check the 3 commits # (edit CHANGELOG.md: move [Unreleased] items under [1.2.0] - date) git add CHANGELOG.md git commit -m "docs: add changelog for 1.2.0" git tag -a v1.2.0 -m "Release 1.2.0" git push --follow-tags origin main git describe # v1.2.0(গ) Changelog commit-এর ওপর tag দিলে ঠিক পরে
v1.2.0; আরও দুটি commit হলেv1.2.0-2-g<নতুন HEAD-এর ছোট hash>— hash-টি আগে থেকে বলা সম্ভব নয়, কারণ তা commit-এর বিষয়বস্তু আর সময়ের ওপর নির্ভর করে।(ঘ) Push-এর আগে: tag শুধু আপনার কাছে, তাই
git tag -d v1.2.0আর আবারgit tag -a v1.2.0 -m "…"(বাgit tag -a -f) নিরাপদ। Push-এর পরে: অন্যরা হয়তো fetch করেছেন, build হয়ে গেছে — tag সরাবেন না। বার্তার ভুল সাধারণত সহ্য করা যায়; কোডে ভুল হলে নতুন PATCH,v1.2.1. -
নীতির সমালোচনা: একটি কাল্পনিক “bazar-pay” JavaScript library-র README-তে লেখা: “আমরা Semantic Versioning মানি। প্রতি মাসের শেষে MINOR বাড়ে। ছোট API পরিবর্তন PATCH-এ যায়, কারণ MAJOR দেখলে মানুষ ভয় পান। Release tag lightweight, আর ভুল হলে একই tag নতুন commit-এ সরিয়ে দিই। Changelog নেই — git log দেখুন। ০.x-এ থাকা অবস্থায় আমাদের API স্থিতিশীল।” কী কী সমস্যা দেখছেন, আর কীভাবে নতুন করে লিখবেন?
- “প্রতি মাসে MINOR”: SemVer-এ MINOR বাড়ে নতুন backward-compatible সুবিধা এলে, ক্যালেন্ডার দেখে নয়। সময়ভিত্তিক নম্বর চাইলে সৎভাবে CalVer ঘোষণা করুন। নতুন নিয়ম: “নতুন সুবিধা → MINOR; শুধু fix → PATCH; release যখন দরকার”।
- API পরিবর্তন PATCH-এ: এটাই SemVer-এর প্রতিশ্রুতি ভাঙা। ব্যবহারকারীর
^1.4.0নির্ভরতা নিজে থেকেই সেই PATCH নেবে, আর build ভাঙবে। নতুন নিয়ম: breaking change শুধু MAJOR-এ, আগে অন্তত একটি MINOR-এ deprecation-সহ। - Lightweight tag আর সরানো tag: কে কখন release দিলেন তার রেকর্ড নেই,
git describeডিফল্টে দেখে না, আর সরানো tag একই নামে দুই কোড তৈরি করে — নিরাপত্তার ঝুঁকিও (ভাবনার প্রশ্ন ৪)। নতুন নিয়ম: annotated (সম্ভব হলে signed) tag; প্রকাশিত tag কখনো সরানো হবে না; ভুল হলে নতুন PATCH. - Changelog নেই:
git log-এtest,choreআর ভেতরের খুঁটিনাটি — ব্যবহারকারীর কাজে লাগে না, আর হালনাগাদের নির্দেশনা থাকে না। নতুন নিয়ম: Keep a Changelog-ধাঁচেরCHANGELOG.md, প্রতিটি release-এ Deprecated/Removed অংশসহ। - “০.x-এ API স্থিতিশীল”: স্ববিরোধী। SemVer-এ ০.y.z মানে প্রাথমিক উন্নয়ন, কোনো প্রতিশ্রুতি নেই। API সত্যিই স্থিতিশীল ও অন্যরা ব্যবহার করলে ১.০.০ release দিন।
- যা যোগ করা দরকার: public API কী (documentation-এর কোন অংশ), deprecation-এর মেয়াদ, পুরোনো MAJOR কতদিন security fix পাবে, আর commit-এর রীতি (Conventional Commits + linter).
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি software release-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Release-এর scope ঠিক করে feature freeze
- Release candidate tag (যেমন
v2.0.0-rc.1) আর পরীক্ষা - পরীক্ষায় পাওয়া bug সারানো
- Changelog হালনাগাদ করে commit
- Annotated tag
v2.0.0দিয়েgit push --follow-tags - Tag থেকে build artifact বানিয়ে publish আর release notes প্রকাশ
একটি repository-তে git describe দেখাল v1.4.0-3-g1a2b3c4. এর সঠিক অর্থ কী?
- বর্তমান version হলো 1.4.3
- Tag v1.4.0-এর commit-এর hash 1a2b3c4, আর তার আগে ৩টি commit
- সবচেয়ে কাছের annotated tag v1.4.0-এর পরে ৩টি commit, আর বর্তমান commit-এর ছোট hash 1a2b3c4
- Lightweight tag v1.4.0 থেকে branch g-তে ৩টি commit
Semantic Versioning 2.0.0 অনুযায়ী 1.0.0-alpha.1 আর 1.0.0-alpha.beta-এর মধ্যে কোনটি বড়, আর কেন?
1.0.0-alpha.1, কারণ এটি ছোট লেখা1.0.0-alpha.beta, কারণ সংখ্যা-identifier অক্ষরের identifier-এর চেয়ে ছোট- দুটি সমান, কারণ pre-release অংশ তুলনায় গোনা হয় না
1.0.0-alpha.1, কারণ এর identifier বেশি
একটি library-র শেষ release 2.3.1. এরপর দুটি commit: fix(api): handle null id আর feat(api): add paging, যার footer-এ BREAKING CHANGE: list() now returns a page object. Conventional Commits আর SemVer মেনে পরের version কী?
2.3.22.4.03.3.13.0.0
একটি repository-তে git tag v1.0.0 আর git tag -a v1.1.0 -m "Release 1.1.0" চালানো হলো। এরপর git cat-file -t v1.0.0 আর git cat-file -t v1.1.0 কী ছাপবে?
commitআরtagtagআরtag- দুটিই
commit refআরtag
একজন developer git commit -s -m "fix: handle empty route" চালালেন। নিচের কোন কথাটি সঠিক?
- Commit-টি GPG key দিয়ে cryptographically sign হলো
- বার্তার শেষে
Signed-off-by: <নাম> <email>trailer যোগ হলো - Commit-এর সাথে একটি annotated tag তৈরি হলো
- Git বার্তাটিকে ৫০ অক্ষরে ছেঁটে দিল
Semantic Versioning 2.0.0-এ 0.y.z version (যেমন 0.7.3) সম্পর্কে কোন কথাটি spec-এর সাথে মেলে?
- এটি প্রাথমিক উন্নয়ন; যেকোনো কিছু যেকোনো সময় বদলাতে পারে
- ০.x-এ breaking change হলে অবশ্যই MAJOR বাড়াতে হবে
- ০.x-এ public API স্থিতিশীল আর backward-compatible থাকতে বাধ্য
- ০ দিয়ে শুরু version SemVer-এ বৈধ নয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ভালো commit: একটি যুক্তিসংগত পরিবর্তন, নিজে build ও test পাস করে, বার্তা বলে কেন। বার্তা: ~৫০ অক্ষরের imperative subject, ফাঁকা লাইন, ~৭২-এ ভাঙা body, শেষ অনুচ্ছেদে trailer (
Refs:,Co-authored-by:,-s-এরSigned-off-by:— যা স্বাক্ষর নয়)। - Conventional Commits 1.0.0:
type(scope)!: description; feat → MINOR, fix → PATCH,!বাBREAKING CHANGE:→ MAJOR; বাকি type-এর spec-এ নিজস্ব SemVer প্রভাব নেই (অনেক টুলperf-কে PATCH ধরে)। - Tag: lightweight = শুধু ref; annotated (
-a) = tag object, release-এর জন্য।git pushtag পাঠায় না —git push origin <tag>বা--follow-tags. প্রকাশিত tag সরাবেন না।git describe→tag-N-gHASH. - SemVer 2.0.0: MAJOR (ভাঙে) . MINOR (নতুন, না ভেঙে) . PATCH (fix); ডানের অংশ শূন্য হয়; ০.y.z-এ প্রতিশ্রুতি নেই;
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0;+buildতুলনায় উপেক্ষিত। Git-এরv:refnameSemVer নয় —versionsort.suffix=-. - Release: freeze → rc → fix → changelog → annotated tag → tag থেকে build ও publish → release notes; কিছু সরানোর আগে MINOR-এ deprecate, MAJOR-এ সরানো।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৯ · বড় repository — monorepo, submodule, subtree আর Git LFSপরবর্তী পর্বঅনেক project এক repository-তে, বা repository-র ভেতরে repository.
- পর্ব ১৭ · Branching strategy — Git Flow, GitHub Flow আর trunk-based developmentআগের পর্বRelease branch আর কোন ধারায় কখন release হয়।
- পর্ব ৮ · প্রথম Git repository — init, add, commit আর তিনটি এলাকাভিত্তিAtomic commit আর git add -p দিয়ে পরিবর্তন ভাগ করা।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ; signed tag পর্ব ৫৭-এ, CI/CD পর্ব ৫৮–৫৯-এ।
- System Design · Deployment আর releaseগভীরে যানTag থেকে artifact হয়ে নিরাপদ rollout.
- JavaScript · Regular expressionপাশের ট্র্যাকSemVer আর commit header parse করার regex বুঝতে।