এই পর্বে যা শিখবেন
git initকী বানায়,.gitফোল্ডারে মোটা দাগে কী থাকে, আর-b mainকেন- Working tree, staging area আর repository — তিনটি এলাকা আর তাদের মধ্যে file-এর যাতায়াত
- File-এর অবস্থা: untracked, unmodified, modified, staged — আর
git statusওgit status -sপড়া git add,add -p,add .বনামadd -A,git commit,commit -a-র ফাঁদ,git rm,git mv- একটি commit-এর ভেতরে কী থাকে, commit ID কীভাবে তৈরি হয়, আর ছোট এক-উদ্দেশ্যের commit কেন
১ · git init — একটি সাধারণ ফোল্ডার থেকে repository
পর্ব ০৭-এ (version control আর Git setup) আপনি Git install করেছেন আর নিজের নাম-email সেট করেছেন। এবার আসল কাজ। এই পর্বে আমরা একটি কাল্পনিক অনলাইন বাজার অ্যাপের ছোট একটি project শুরু করব — নাম bazar. শুরুতে এটি একটি সাধারণ ফোল্ডার। Git জানেই না এর অস্তিত্ব আছে।
একটি ফোল্ডারকে Git-এর repository বানানোর command একটিই: git init. Repository মানে সেই জায়গা, যেখানে project-এর সব file আর তাদের পুরো পরিবর্তনের ইতিহাস জমা থাকে। নিচের সব আউটপুট এই পর্বের জন্য একটি খালি scratch ফোল্ডারে আসল Git ২.৫৩ চালিয়ে নেওয়া; লম্বা path আমরা … দিয়ে ছোট করেছি।
$ git init -b main bazar
Initialized empty Git repository in …/bazar/.git/
$ cd bazar
$ git config user.name "Rahim"
$ git config user.email "rahim@example.com"
$ git config core.autocrlf false
$ ls -A
.git
$ ls .git
HEAD
config
description
hooks
info
objects
refs
$ git status
On branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)তিনটি জিনিস খেয়াল করুন। এক, git init-এর পরে ফোল্ডারে দৃশ্যমান কিছুই বদলায়নি; শুধু একটি লুকানো .git ফোল্ডার এসেছে (Linux ও macOS-এ নাম ডট দিয়ে শুরু হলে সেটি লুকানো থাকে, তাই ls -A লাগল)। দুই, git status বলছে No commits yet — repository আছে, কিন্তু ইতিহাস এখনো শূন্য। তিন, আমরা user.name আর user.email শুধু এই repository-র জন্য সেট করেছি; নিজের কম্পিউটারে সাধারণত পর্ব ০৭-এর মতো --global দিয়ে একবারই সেট করা হয়। শেষ লাইনের core.autocrlf false শুধু এই উদাহরণের জন্য — Windows আর Linux-এ line ending-এর সতর্কবার্তা যাতে আলাদা না দেখায় (বিস্তারিত পর্ব ১০-এ)।
.git ফোল্ডারে কী থাকে — মোটা দাগে
.git ফোল্ডারটিই আসল repository. আপনার project ফোল্ডারের বাকি সবকিছু শুধু “এখন যে কপিতে কাজ চলছে”। ভেতরের নামগুলোর মোটামুটি অর্থ:
| নাম | মোটা দাগে কাজ |
|---|---|
objects/ | সব file-এর বিষয়বস্তু, ফোল্ডারের তালিকা আর commit — প্রতিটি তার নিজের hash নামে রাখা (পর্ব ৫২) |
refs/ | Branch আর tag-এর নাম, প্রতিটি একটি commit-এর দিকে নির্দেশ করে (পর্ব ১২) |
HEAD | এখন আপনি কোন branch-এ আছেন, তার একটি ছোট লেখা |
config | শুধু এই repository-র সেটিং (উপরের git config এখানেই লিখেছে) |
hooks/ | নির্দিষ্ট ঘটনায় নিজে থেকে চলা script-এর জায়গা (পর্ব ৫৬) |
index | Staging area — প্রথম git add-এর পরে তৈরি হয়, তাই উপরের তালিকায় এখনো নেই |
.git ফোল্ডার হাতে বদলাবেন না, আর মুছবেন না। এটি মুছে ফেললে ফোল্ডারের file-গুলো থেকে যাবে, কিন্তু সব commit আর ইতিহাস হারিয়ে যাবে — অন্য কোথাও (যেমন একটি remote-এ, পর্ব ১৪) কপি না থাকলে আর ফেরানো যাবে না। উল্টো দিকে, একটি project-এর কপি অন্য কাউকে দিতে চাইলে .git-সহ পুরো ফোল্ডার দিলে ইতিহাসও সাথে যায়।
-b main কেন — প্রথম branch-এর নাম
প্রতিটি repository একটি branch দিয়ে শুরু হয় — branch মানে কাজের একটি ধারা, যার বিস্তারিত পর্ব ১২-এ (branch আর HEAD). কোনো সেটিং না থাকলে Git ২.৫৩ প্রথম branch-এর নাম রাখে master, আর সাথে একটি লম্বা পরামর্শ দেখায়:
$ git init demo-plain
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint: git config --global init.defaultBranch <name>
…
Initialized empty Git repository in …/demo-plain/.git/বার্তাটি নিজেই বলছে, Git ৩.০-তে এই default branch-এর নাম main হবে। বড় hosting platform-গুলোও লেখার সময়ে নতুন repository-তে main ব্যবহার করে। দুই উপায়ে আগেই main বেছে নিতে পারেন: প্রতিবার git init -b main (পুরো রূপ --initial-branch=main), অথবা একবার git config --global init.defaultBranch main. দুটোই Git ২.২৮ (২০২০) থেকে আছে; এর আগের Git-এ init-এর পরে git branch -m main দিয়ে নাম বদলাতে হতো। নামটি শুধু একটি লেবেল — master নামেও Git একই রকম কাজ করে।
২ · তিনটি এলাকা — খসড়া খাতা, বাছাই করা পাতা আর ফেয়ার কপি
Git শেখার সবচেয়ে গুরুত্বপূর্ণ ধারণা এটিই। এটি বুঝলে পরের সব command — diff, restore, reset — অনেক সহজ হয়ে যায়।
পরীক্ষার হলে একজন পরীক্ষার্থীর কথা ভাবুন। তাঁর কাছে তিনটি জিনিস। খসড়া খাতা — যেখানে তিনি যা খুশি লেখেন, কাটেন, আবার লেখেন। ফেয়ার কপির জন্য বাছাই করা উত্তর — খসড়া থেকে যে অংশগুলো তিনি ঠিক করেছেন যে খাতায় তুলবেন। আর জমা দেওয়া খাতা — একবার পরিদর্শকের হাতে গেলে সেটি স্থায়ী; পরে নতুন পাতা জোড়া যায়, কিন্তু জমা দেওয়া পাতা বদলানো যায় না। খসড়ার সব কাটাকাটি ফেয়ার কপিতে যায় না; তিনি বেছে বেছে তোলেন। Git-এর তিনটি এলাকা ঠিক এরকম।
- Working tree (খসড়া খাতা): আপনার project ফোল্ডারের সাধারণ file — editor-এ যেগুলো খোলেন আর বদলান। Git এগুলো দেখে, কিন্তু নিজে থেকে কিছু জমা রাখে না।
- Staging area বা index (বাছাই করা পাতা): পরের commit-টি ঠিক কেমন হবে, তার একটি প্রস্তুত তালিকা — প্রতিটি file-এর কোন version যাবে। এটি
.git/indexনামের একটি file. - Repository (জমা দেওয়া খাতা):
.git-এর ভেতরে সব commit. HEAD দেখায় এখন কোন commit-এর ওপর আপনি দাঁড়িয়ে আছেন — সাধারণত বর্তমান branch-এর শেষ commit.
তিনটি এলাকা আর তাদের মধ্যে যাতায়াত
Working tree → (add) → staging area → (commit) → repository
git add ফোল্ডারের বর্তমান কপি index-এ তোলে; git commit index-এর পুরো কপিকে একটি স্থায়ী commit বানায়। দাগ-কাটা পথে ফেরা যায়: git restore --staged শেষ commit-এর কপি index-এ ফেরায়, git restore index-এর কপি দিয়ে ফোল্ডারের বদল মুছে দেয় (সাবধান — এটি আপনার না-তোলা বদল ফেলে দেয়)। ফেরার পুরো গল্প পর্ব ১১-এ।একই file-এর একসাথে তিনটি আলাদা কপি থাকতে পারে: ফোল্ডারে একটি, index-এ একটি, শেষ commit-এ একটি। git status আসলে দুটি তুলনা করে বলে — index বনাম শেষ commit (কী commit-এর জন্য তৈরি) আর ফোল্ডার বনাম index (কী এখনো তোলা হয়নি)।
কথাটি কোডে দেখা যাক। নিচের ছোট মডেলে তিনটি file-এর জন্য তিনটি এলাকা রাখা আছে — W (working tree), I (index), H (HEAD commit). add ফোল্ডার থেকে index-এ কপি করে, commit পুরো index কপি করে। প্রতি ধাপে প্রতিটি file-এর পাশে git status -s-এর মতো দুই অক্ষরের সংকেত ছাপা হয় (প্রথম অক্ষর index বনাম HEAD, দ্বিতীয় অক্ষর ফোল্ডার বনাম index). এটি Git-এর আসল কোড নয়, শুধু ধারণাটির মডেল।
// Three areas: working tree (W), staging area / index (I), last commit HEAD (H).
const work = {}, index = {}; let head = {};
function state(f) { // the two letters of "git status -s"
if (!(f in index)) return "?? untracked";
const x = !(f in head) ? "A" : index[f] !== head[f] ? "M" : " ";
const y = !(f in work) ? "D" : work[f] !== index[f] ? "M" : " ";
const name = { "A ": "new, staged", "AM": "new, staged + edited again",
"M ": "modified, staged", " M": "modified, not staged",
"MM": "staged + edited again", " ": "unmodified" }[x + y];
return x + y + " " + name;
}
function show(cmd) {
console.log("$ " + cmd);
for (const f of Object.keys(work).sort())
console.log(` ${f} W=${work[f]} I=${index[f] ?? "--"} H=${head[f] ?? "--"} ${state(f)}`);
}
const add = (...fs) => fs.forEach(f => { index[f] = work[f]; });
const commit = () => { head = { ...index }; }; // commit copies the index, not W
for (const f of ["a.txt", "b.txt", "c.txt"]) work[f] = "v1";
show("create a.txt b.txt c.txt");
add("a.txt", "b.txt"); show("git add a.txt b.txt");
work["b.txt"] = "v2"; show("edit b.txt");
commit(); show('git commit -m "first"');
add("b.txt", "c.txt"); show("git add b.txt c.txt");
commit(); show('git commit -m "second"');
work["a.txt"] = "v2"; show("edit a.txt");আউটপুট:
$ create a.txt b.txt c.txt
a.txt W=v1 I=-- H=-- ?? untracked
b.txt W=v1 I=-- H=-- ?? untracked
c.txt W=v1 I=-- H=-- ?? untracked
$ git add a.txt b.txt
a.txt W=v1 I=v1 H=-- A new, staged
b.txt W=v1 I=v1 H=-- A new, staged
c.txt W=v1 I=-- H=-- ?? untracked
$ edit b.txt
a.txt W=v1 I=v1 H=-- A new, staged
b.txt W=v2 I=v1 H=-- AM new, staged + edited again
c.txt W=v1 I=-- H=-- ?? untracked
$ git commit -m "first"
a.txt W=v1 I=v1 H=v1 unmodified
b.txt W=v2 I=v1 H=v1 M modified, not staged
c.txt W=v1 I=-- H=-- ?? untracked
$ git add b.txt c.txt
a.txt W=v1 I=v1 H=v1 unmodified
b.txt W=v2 I=v2 H=v1 M modified, staged
c.txt W=v1 I=v1 H=-- A new, staged
$ git commit -m "second"
a.txt W=v1 I=v1 H=v1 unmodified
b.txt W=v2 I=v2 H=v2 unmodified
c.txt W=v1 I=v1 H=v1 unmodified
$ edit a.txt
a.txt W=v2 I=v1 H=v1 M modified, not staged
b.txt W=v2 I=v2 H=v2 unmodified
c.txt W=v1 I=v1 H=v1 unmodified
সবচেয়ে শিক্ষণীয় লাইন চতুর্থ ধাপে। b.txt-কে আমরা v1 অবস্থায় add করেছিলাম, তারপর ফোল্ডারে v2 করেছি (AM — নতুন file staged, আবার বদলানো)। প্রথম commit-এর পরে দেখুন: H=v1, কিন্তু W=v2. অর্থাৎ commit নিয়েছে index-এর v1, ফোল্ডারের v2 নয় — তাই b.txt এখন " M", modified কিন্তু staged নয়। c.txt কখনো add হয়নি বলে প্রথম commit-এ নেই, ?? থেকে গেছে। দ্বিতীয় commit-এর পরে তিন এলাকা মিলে গেছে, সব file unmodified. শেষ ধাপে a.txt ফোল্ডারে বদলাতেই আবার " M".
৩ · File-এর চার অবস্থা আর git status
Git-এর চোখে project ফোল্ডারের প্রতিটি file হয় untracked, নয়তো tracked. Tracked মানে file-টি শেষ commit-এ আছে, বা অন্তত index-এ তোলা হয়েছে। Tracked file আবার তিন রকম হতে পারে:
| অবস্থা | মানে | status -s-এ |
|---|---|---|
| Untracked | ফোল্ডারে আছে, কিন্তু Git কখনো নেয়নি — index-এও নেই, commit-এও নেই | ?? |
| Unmodified | Tracked, আর তিন কপিই একই | কিছু দেখায় না |
| Modified | ফোল্ডারের কপি index-এর কপি থেকে আলাদা | দ্বিতীয় ঘরে M: " M" |
| Staged | Index-এর কপি শেষ commit থেকে আলাদা — পরের commit-এ যাবে | প্রথম ঘরে M বা নতুন হলে A |
এক file একসাথে staged আর modified দুটোই হতে পারে — MM বা AM; তখন index-এ এক version, ফোল্ডারে আরেক। এবার আসল Git-এ দেখি। ফোল্ডারে তিনটি file লিখলাম — README.md, index.html আর app.js:
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
app.js
index.html
nothing added to commit but untracked files present (use "git add" to track)
$ git status -s
?? README.md
?? app.js
?? index.htmlলম্বা রূপে git status প্রতিটি অংশের নিচে বন্ধনীতে পরের সম্ভাব্য command-ও বলে দেয় — নতুনদের জন্য এটি দারুণ সহায়ক। ছোট রূপ git status -s (--short) প্রতি file-এ এক লাইন। এবার দুটি file add করি, তৃতীয়টি নয়:
$ git add README.md app.js
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
new file: app.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
index.htmlএখন app.js-এ একটি নতুন জিনিস ("tel") যোগ করলাম, কিন্তু আবার add করিনি:
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
new file: app.js
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: app.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
index.html
$ git status -s
A README.md
AM app.js
?? index.htmlapp.js এখন দুই জায়গায়: “Changes to be committed”-এ (index-এ পুরোনো version) আর “Changes not staged”-এ (ফোল্ডারে নতুন version). ছোট রূপে AM: প্রথম অক্ষর A — index-এ নতুন যোগ হয়েছে; দ্বিতীয় অক্ষর M — ফোল্ডারে তার পরে আবার বদলেছে। একটি ছোট খুঁটিনাটি: প্রথম commit-এর আগে unstage করার পরামর্শ git rm --cached, প্রথম commit-এর পরে সেটি হয়ে যায় git restore --staged — কারণ তখন ফেরার মতো একটি commit আছে।
নিচের stepper-এ একটি file-এর পুরো জীবনচক্র ধাপে ধাপে দেখুন। প্রতি ধাপে তিন এলাকায় app.js-এর কোন version আছে, আর git status -s কী দেখাবে:
| Working tree | |
| Staging area | |
| Repository (HEAD) | |
| git status -s | |
| অবস্থা |
- নতুন file. Editor-এ
app.jsলিখে save করলেন (version v1). File শুধু ফোল্ডারে; index-এ নেই, কোনো commit-ও নেই। Git একে untracked বলে —??. এই অবস্থায় file হারালে Git কিছুই ফেরাতে পারবে না। git add app.js. ফোল্ডারের v1-এর একটি কপি index-এ উঠল। এখন file tracked আর staged; সংকেতA(added) প্রথম ঘরে, দ্বিতীয় ঘর ফাঁকা — কারণ ফোল্ডার আর index-এর কপি একই।git commit -m "…". Index-এর পুরো কপি থেকে প্রথম commit C1 তৈরি হলো, তাতে v1. তিন এলাকাই এখন v1 — file unmodified, তাইgit status -sএর জন্য কোনো লাইনই দেখায় না।- File বদলালেন. ফোল্ডারে v2. Index আর C1-এ এখনো v1. ফোল্ডার বনাম index আলাদা, তাই দ্বিতীয় ঘরে
M:" M"— modified, কিন্তু staged নয়। এখন commit করলে কিছুই যাবে না। - আবার
git add app.js. v2 index-এ উঠল। এখন index বনাম C1 আলাদা, তাই প্রথম ঘরেM:"M "— staged. ফোল্ডার আর index একই, তাই দ্বিতীয় ঘর ফাঁকা। - আবার commit. নতুন commit C2-তে v2; C2-এর parent C1. তিন এলাকা আবার মিলে গেল — unmodified. জীবনচক্র এভাবেই ঘোরে: বদল → add → commit.
৪ · git add — কোন file, কোন অংশ, কোন ফোল্ডার
git add-এর নাম একটু বিভ্রান্তিকর। এটি শুধু “নতুন file যোগ” নয়; এর আসল অর্থ হলো এই file-এর এখনকার বিষয়বস্তু পরের commit-এর জন্য index-এ তোলা। নতুন file, বদলানো file — দুটোতেই একই command. কয়েকটি রূপ:
| Command | যা index-এ ওঠে (Git ২.x) |
|---|---|
git add app.js | শুধু ওই file-এর বর্তমান বিষয়বস্তু |
git add css/ | ওই ফোল্ডারের ভেতরের সব নতুন, বদলানো আর মুছে ফেলা file |
git add . | বর্তমান ফোল্ডার আর তার নিচের সব বদল — নতুন, বদলানো, মুছে ফেলা |
git add -A | পুরো working tree-র সব বদল, আপনি যে ফোল্ডারেই থাকুন |
git add -u | শুধু আগে থেকে tracked file-এর বদল ও মুছে ফেলা; নতুন file নয় |
git add -p | প্রতিটি বদলের টুকরো দেখে আপনি বেছে বেছে তোলেন |
git add . বনাম git add -A
পুরোনো টিউটোরিয়ালে প্রায়ই লেখা থাকে, “add . মুছে ফেলা file নেয় না, তাই -A দিন।” কথাটি Git ১.x-এর জন্য সত্যি ছিল। Git ২.০ (২০১৪) থেকে git add . মুছে ফেলা file-ও নেয়। এখন দুটির পার্থক্য শুধু পরিধিতে: . মানে “এই ফোল্ডার আর তার নিচে”, আর -A মানে “পুরো project”. Project-এর মূল ফোল্ডারে দাঁড়িয়ে দুটো একই কাজ করে। নিচে আগে README.md-এ একটি লাইন যোগ করা হয়েছে, নতুন file css/style.css আর discount.js লেখা হয়েছে; তারপর css ফোল্ডারের ভেতর থেকে add .:
$ cd css
$ git add .
$ git status -s
M ../README.md
A style.css
?? ../discount.js
$ cd ..
$ git add -A
$ git status -s
M README.md
A css/style.css
A discount.js
$ git commit -m "Add stylesheet, discount and run note"
[main 5903454] Add stylesheet, discount and run note
3 files changed, 5 insertions(+)
create mode 100644 css/style.css
create mode 100644 discount.jscss-এর ভেতর থেকে add . শুধু style.css তুলেছে (git status -s path-ও বর্তমান ফোল্ডার থেকে দেখায়, তাই ../README.md). মূল ফোল্ডারে ফিরে -A সব তুলেছে। সতর্কতা: add . বা add -A চোখ বন্ধ করে দিলে log file, password-এর file, বিশাল build ফাইল — সব উঠে যেতে পারে। তাই আগে git status দেখুন, আর যা কখনো track করবেন না তা .gitignore-এ লিখুন — পর্ব ১০ (.gitignore আর git config).
git add -p — একই file-এর অংশবিশেষ তোলা
ধরুন prices.js file-এ আপনি একসাথে দুটি আলাদা কাজ করেছেন: চালের দাম ৭০ থেকে ৭৫ টাকা করেছেন (আসল কাজ), আর একটি debug লাইন বসিয়েছেন (সাময়িক, commit-এ যাওয়ার কথা নয়)। git add prices.js দিলে দুটোই উঠবে। git add -p (--patch) file-এর বদলকে hunk-এ ভাগ করে — hunk মানে পাশাপাশি বদলানো লাইনের একটি টুকরো — আর প্রতিটির জন্য জিজ্ঞেস করে, “এটি তুলব?” নিচের stepper-এ আসল Git-এর আউটপুট দেখুন:
@@ -1,6 +1,6 @@
// prices.js - price list for the bazar
const prices = {
- chal: 70,
+ chal: 75,
dal: 120,
tel: 180,
};
@@ -14,6 +14,7 @@ function total(cart) {
}
function format(amount) {
+ console.log("debug", amount);
return amount + " taka";
}
@@ -1,6 +1,6 @@
// prices.js - price list for the bazar
const prices = {
- chal: 70,
+ chal: 75,
dal: 120,
tel: 180,
};
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? y
@@ -14,6 +14,7 @@ function total(cart) {
}
function format(amount) {
+ console.log("debug", amount);
return amount + " taka";
}
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? n
$ git status -s
MM prices.js
$ git diff --cached
…
@@ -1,6 +1,6 @@
// prices.js - price list for the bazar
const prices = {
- chal: 70,
+ chal: 75,
dal: 120,
tel: 180,
};
$ git commit -m "Raise rice price to 75 taka" [main 37c2b34] Raise rice price to 75 taka 1 file changed, 1 insertion(+), 1 deletion(-) $ git status -s M prices.js $ git restore prices.js $ git status -s (কিছু নেই — debug লাইন ফোল্ডার থেকেও মুছে গেছে)
- দুটি বদল, দুটি hunk.
git diffদেখাচ্ছে ফোল্ডারের কপি index থেকে কোথায় আলাদা। লাইন ৩-এর দাম আর লাইন ১৭-এর debug লাইন অনেক দূরে, তাই Git দুটিকে আলাদা hunk ধরেছে।@@ -1,6 +1,6 @@মানে পুরোনো file-এর লাইন ১ থেকে ৬টি লাইন, নতুনেও লাইন ১ থেকে ৬টি। - প্রথম hunk:
y.git add -pপ্রথম hunk দেখিয়ে জিজ্ঞেস করল। দামের বদলটি আমরা চাই, তাই উত্তরy(yes), অর্থাৎ হ্যাঁ। বন্ধনীর অক্ষরগুলো আরও বিকল্প:nমানে না,qমানে এখানেই বেরিয়ে যাওয়া,sমানে hunk আরও ছোট টুকরোয় ভাঙা (যদি ভাঙা যায়),eমানে হাতে সম্পাদনা,?মানে সাহায্য। এখানে তালিকায়sনেই, কারণ এই hunk-এ একটিই বদলের জায়গা, আর ভাঙার সুযোগ নেই। তালিকাটি version-ভেদেও সামান্য আলাদা হয়। - দ্বিতীয় hunk:
n. Debug লাইনটি commit-এ চাই না, তাইn. এটি ফোল্ডারে থেকেই গেল, শুধু index-এ উঠল না। তালিকায় এবারk,jনেই — এগুলো আগের বা পরের সিদ্ধান্ত-না-নেওয়া hunk-এ যায়, আর প্রথম hunk-এর সিদ্ধান্ত আগেই হয়ে গেছে; বড় হাতেরK,Jদিয়ে তবু আগের বা পরের যেকোনো hunk-এ যাওয়া যায়। MM— একই file, দুই অবস্থা। প্রথমM: index-এ দামের বদল (staged). দ্বিতীয়M: ফোল্ডারে আরও একটি বদল (debug লাইন), যা তোলা হয়নি।git diff --cached(index বনাম শেষ commit) দেখায় commit-এ ঠিক কী যাবে — শুধু দামের লাইন। Commit-এর আগে এই command চালানো একটি ভালো অভ্যাস; বিস্তারিত পর্ব ০৯-এ।- Commit-এ শুধু দাম। Commit
37c2b34-এ একটি লাইন বদল (১টি insertion, ১টি deletion). Debug লাইন তখনো ফোল্ডারে (" M").git restore prices.jsফোল্ডারের কপিকে index-এর কপিতে ফিরিয়ে debug লাইনটি ফেলে দিল। সাবধান:restoreনা-তোলা বদল চিরতরে মুছে দেয় — পর্ব ১১-এ বিস্তারিত।
add -p-এর মূল্য বোঝা যায় যখন একটি file-এ একসাথে একটি bug fix আর একটি নতুন feature-এর কাজ মিশে যায়। দুটোকে দুটি আলাদা commit-এ রাখলে পরে একটিকে ফেরানো (revert, পর্ব ১১) বা review করা (পর্ব ৪৪) অনেক সহজ। VS Code-সহ অনেক editor-এ একই কাজ হয় লাইন বেছে “Stage Selected Ranges” জাতীয় বাটনে — নাম tool-ভেদে আলাদা।
৫ · git commit — একটি commit কী কী মনে রাখে
আমাদের bazar project-এ ফিরি। app.js আর index.html-ও add করে প্রথম commit:
$ git add app.js index.html
$ git status -s
A README.md
A app.js
A index.html
$ git commit -m "Add product list page"
[main (root-commit) 8bfb77a] Add product list page
3 files changed, 7 insertions(+)
create mode 100644 README.md
create mode 100644 app.js
create mode 100644 index.html
$ git status
On branch main
nothing to commit, working tree clean
$ git log --oneline
8bfb77a Add product list pageCommit-এর আউটপুটের প্রথম লাইনটি পড়তে শিখুন: [main (root-commit) 8bfb77a] মানে branch main-এ, এটি root commit (প্রথম commit, যার আগে কিছু নেই), আর তার ID-র ছোট রূপ 8bfb77a. তারপর বার্তা, কয়টি file আর কয়টি লাইন বদলেছে। create mode 100644 মানে নতুন একটি সাধারণ file (চালানো যায় এমন file হলে 100755). এরপর git status বলছে working tree clean — তিন এলাকা মিলে গেছে।
একটি commit-এর ভেতরে উঁকি
git cat-file -p একটি Git object-এর ভেতরের লেখা হুবহু দেখায় (internals-এর বিস্তারিত পর্ব ৫২-এ, আজ শুধু উঁকি):
$ git cat-file -p HEAD
tree 9bb98eca3b3ee398a745f766c1639c8698cf6305
author Rahim <rahim@example.com> 1768449660 +0600
committer Rahim <rahim@example.com> 1768449660 +0600
Add product list page
$ git rev-parse HEAD
8bfb77a54601e9294b83f3f19c4519d36a524183
$ git rev-parse --short HEAD
8bfb77aএকটি commit আসলে এটুকুই — একটি ছোট লেখা:
tree: project-এর পুরো snapshot-এর ID — সেই মুহূর্তে index-এ থাকা সব file আর ফোল্ডারের তালিকা। শুধু পার্থক্য নয়, পুরো ছবি।parent: আগের commit-এর ID. প্রথম commit-এ এই লাইনই নেই — উপরে দেখুন। Merge commit-এ (পর্ব ১৩) একাধিক parent থাকে।- author: যিনি পরিবর্তনটি মূলত লিখেছেন, সাথে সময় (
1768449660হলো ১৯৭০ থেকে গোনা সেকেন্ড, এখানে ১৫ জানুয়ারি ২০২৬, সকাল ১০টা ১ মিনিট, ঢাকার সময়) আর সময় অঞ্চল+0600. - committer: যিনি commit-টি repository-তে বসিয়েছেন। সাধারণত author-ই; আলাদা হয় যখন কেউ অন্যের পাঠানো patch বসান, বা পরে commit নতুন করে সাজানো হয় (rebase, পর্ব ১৫)।
- বার্তা: একটি ফাঁকা লাইনের পরে commit message.
এই পুরো লেখাটিকে Git একটি hash function-এ দেয় — ঐতিহ্যগতভাবে SHA-1, আরও সঠিকভাবে বললে "commit <size>\0" শিরোনাম জুড়ে তার ওপর। ফল ৪০ অক্ষরের hexadecimal সংখ্যা, এটিই commit hash বা commit ID: 8bfb77a…. দৈনন্দিন কাজে প্রথম কয়েকটি অক্ষরই যথেষ্ট — Git সাধারণত অন্তত ৭টি দেখায়, আর repository বড় হলে দরকারমতো বেশি, যাতে দুটি commit-এর ছোট রূপ মিলে না যায়। নতুন Git-এ চাইলে git init --object-format=sha256 দিয়ে SHA-256 repository-ও বানানো যায় (তখন ID ৬৪ অক্ষরের); তবে লেখার সময়ে বেশিরভাগ repository SHA-1-এই চলে।
Author আর committer যেহেতু commit-এর লেখার অংশ, তাই আমরা এই পর্বের প্রতিটি commit-এর আগে সময় নির্দিষ্ট করে দিয়েছি (GIT_AUTHOR_DATE আর GIT_COMMITTER_DATE, প্রতিবার এক মিনিট এগিয়ে)। একই নাম, email, সময়, বার্তা আর file দিয়ে আপনি নিজে চালালে হুবহু একই hash পাবেন। সময় নির্দিষ্ট না করলে আপনার hash আলাদা আসবে — সেটাই স্বাভাবিক।
লম্বা বার্তা — editor দিয়ে commit
-m ছাড়া শুধু git commit দিলে Git আপনার editor খোলে (পর্ব ০৭-এ যেটি সেট করেছেন; না করলে প্রায়ই Vim). তাতে একটি খসড়া থাকে:
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
# modified: app.js
## দিয়ে শুরু হওয়া লাইন বাদ যায়; বার্তা ফাঁকা রেখে বন্ধ করলে commit বাতিল হয়। প্রথম লাইনে একটি ছোট সারাংশ, তারপর একটি ফাঁকা লাইন, তারপর “কেন” — এই কাঠামোয় লিখে save করে বন্ধ করলাম (terminal-এ editor খোলা থাকার সময় Git একটি “Waiting for your editor…” ধরনের বার্তা দেখাতে পারে):
$ git add app.js
$ git commit
[main be72ad5] Print each item on its own line
1 file changed, 1 insertion(+), 1 deletion(-)$ git cat-file -p HEAD
tree 9c161ecf00137cb88feb9387296a968878d635a7
parent 8bfb77a54601e9294b83f3f19c4519d36a524183
author Rahim <rahim@example.com> 1768449720 +0600
committer Rahim <rahim@example.com> 1768449720 +0600
Print each item on its own line
The list was printed as one array, which is hard to read
on a phone screen. Print one item per line instead.
$ git log -1 --format=fuller
commit be72ad5af3400d3a74cea4f824820bde9ad28056
Author: Rahim <rahim@example.com>
AuthorDate: Thu Jan 15 10:02:00 2026 +0600
Commit: Rahim <rahim@example.com>
CommitDate: Thu Jan 15 10:02:00 2026 +0600
Print each item on its own line
The list was printed as one array, which is hard to read
on a phone screen. Print one item per line instead.এবার parent 8bfb77a… লাইন এসেছে — দ্বিতীয় commit প্রথমটির দিকে নির্দেশ করছে। --format=fuller author আর committer আলাদা করে দেখায়। -m দুবার দিলেও (git commit -m "সারাংশ" -m "বিস্তারিত") Git দুটি অনুচ্ছেদ বানায়, কিন্তু লম্বা বার্তার জন্য editor-ই সুবিধাজনক। ভালো বার্তার প্রচলিত নিয়ম (৫০ অক্ষরের মধ্যে সারাংশ, আদেশসূচক ক্রিয়া, Conventional Commits) পর্ব ১৮-এ।
নিচের stepper-এ দুটি commit পাশাপাশি দেখুন — কোনটিতে কী আছে:
| Commit ID | |
| Parent | |
| Tree (snapshot) | |
| app.js-এর দ্বিতীয় লাইন | |
| Git-এর সারাংশ |
- Commit-এর আগে। তিনটি file index-এ তোলা; repository-তে কোনো commit নেই,
git statusবলছেNo commits yet. HEAD এমন একটি branch (main)-এর দিকে নির্দেশ করছে, যার এখনো কোনো commit নেই। - প্রথম commit
8bfb77a. Index-এর তিনটি file নিয়ে tree9bb98ecতৈরি হলো। Parent নেই — এটিই root commit. মোট ৭টি লাইন যোগ: README.md-এর ২টি, index.html-এর ৩টি, app.js-এর ২টি। - দ্বিতীয় commit
be72ad5. শুধুapp.js-এর দ্বিতীয় লাইন বদলেছে, তবু tree9c161ec-তে তিনটি file-ই আছে — commit সবসময় পুরো ছবি রাখে। বদলহীনREADME.mdআরindex.html-এর বিষয়বস্তু একই hash পায়, তাই Git সেগুলো দ্বিতীয়বার জমা রাখে না, শুধু আগেরটির দিকে নির্দেশ করে। Parent8bfb77a. - History একটি শিকল। Branch
mainএখনbe72ad5-এর দিকে, HEADmain-এর দিকে।be72ad5থেকে parent ধরে পেছনে গেলে8bfb77a, তারপর আর কিছু নেই।git log --onelineএই শিকলই উল্টো ক্রমে (নতুন আগে) দুই লাইনে দেখায়।
Commit ID কেন এত “স্পর্শকাতর”
Commit-এর লেখায় parent-এর ID থাকে, আর সেই লেখার hash-ই commit-এর ID. তাহলে পুরোনো একটি commit-এর একটি byte বদলালে কী হয়? নিচের মডেলে commit বানানো হয়েছে {tree, parent, author, committer, message} দিয়ে; ID হিসেবে আমরা ছোট একটি 32-bit hash, FNV-1a, ব্যবহার করেছি — এটি শুধু Git-এর SHA-1 বা SHA-256-এর জায়গায় দাঁড়ানো একটি সহজ বিকল্প, নিরাপত্তার জন্য নয়। আসল Git hash করে "commit <size>\0" আর তারপর commit-এর লেখা।
// Commit objects: {tree snapshot, parent, author, message} -> an ID from the text.
// FNV-1a 32-bit stands in for Git's SHA-1 (or SHA-256) over "commit <size>\0" + text.
function fnv1a(s) {
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
return h.toString(16).padStart(8, "0");
}
function treeId(files) { // canonical text: sorted "name content" lines
return fnv1a(Object.keys(files).sort().map(f => f + " " + files[f]).join("\n"));
}
function makeCommit(files, parent, msg, time) {
const text = "tree " + treeId(files) + "\n" + (parent ? "parent " + parent + "\n" : "") +
"author Rahim <rahim@example.com> " + time + " +0600\n" +
"committer Rahim <rahim@example.com> " + time + " +0600\n\n" + msg + "\n";
return { id: fnv1a(text), text };
}
function history(firstAppJs) {
const s1 = { "app.js": firstAppJs, "README.md": "# Bazar" };
const s2 = { ...s1, "app.js": 'items = ["chal","dal","tel"]' };
const s3 = { ...s2, "prices.js": "chal: 70" };
const c1 = makeCommit(s1, null, "Add product list", 1768449660);
const c2 = makeCommit(s2, c1.id, "Add oil to list", 1768449720);
const c3 = makeCommit(s3, c2.id, "Add price list", 1768449780);
return [c1, c2, c3];
}
const A = history('items = ["chal","dal"]');
console.log("--- first commit object (no parent line) ---\n" + A[0].text);
console.log("--- second commit object ---\n" + A[1].text);
const B = history('items = ["chal","dal"] '); // one extra space in commit 1
for (let i = 0; i < 3; i++)
console.log(`commit ${i + 1}: original ${A[i].id} one-byte change ${B[i].id}`);আউটপুট:
--- first commit object (no parent line) ---
tree dc80d4b1
author Rahim <rahim@example.com> 1768449660 +0600
committer Rahim <rahim@example.com> 1768449660 +0600
Add product list
--- second commit object ---
tree 38ba9274
parent 2934e7c2
author Rahim <rahim@example.com> 1768449720 +0600
committer Rahim <rahim@example.com> 1768449720 +0600
Add oil to list
commit 1: original 2934e7c2 one-byte change bc213395
commit 2: original f04a6621 one-byte change abfc9356
commit 3: original 140993bc one-byte change b544020f
প্রথম commit-এর লেখায় parent লাইন নেই, দ্বিতীয়টিতে parent 2934e7c2 — প্রথমটির ID. এবার শেষ তিন লাইন দেখুন: প্রথম commit-এর app.js-এ শুধু একটি space যোগ করায় তার tree বদলেছে, তাই ID 2934e7c2 থেকে bc213395. দ্বিতীয় আর তৃতীয় commit-এর tree কিন্তু একই রয়ে গেছে (তাদের app.js তো আলাদা করে লেখা), তবু তাদের ID-ও বদলেছে — কারণ তাদের লেখার parent লাইন বদলেছে। একটি পুরোনো commit বদলানো মানে তার পরের প্রতিটি commit নতুন। এজন্যই শেষ commit-এর ID-ই পুরো ইতিহাসের একটি আঙুলের ছাপ; আর এজন্যই অন্যের সাথে ভাগ করা history বদলানো (rebase, amend — পর্ব ১১ ও ১৫) সাবধানে করতে হয়।
আরও গভীরে: Git-এর object store একটি content-addressed storage — প্রতিটি file-এর বিষয়বস্তু (blob), ফোল্ডারের তালিকা (tree) আর commit তার নিজের hash নামে .git/objects-এ থাকে; পর্ব ৫২-এ এগুলো হাতে বানাব। Commit-এর parent-শিকল একটি Merkle-ধাঁচের কাঠামো, যা Ralph Merkle-এর ১৯৭০-এর দশকের hash tree-র ধারণার আত্মীয়। ২০১৭ সালে Google ও CWI Amsterdam-এর গবেষকেরা SHA-1-এর প্রথম ব্যবহারিক collision (“SHAttered”) প্রকাশ করেন; এরপর Git ২.১৩ থেকে collision-আক্রমণ ধরার ব্যবস্থাসহ একটি “hardened” SHA-1 ব্যবহার করে, আর SHA-256-এ যাওয়ার কাজ চলছে। Pro Git বইয়ের (Scott Chacon ও Ben Straub, ২য় সংস্করণ ২০১৪) “Git Basics” আর “Git Internals” অধ্যায় এই পর্বের ভালো সঙ্গী।
৬ · git commit -a — সুবিধা আর তার ফাঁদ
git commit -a (--all) মানে commit-এর আগে সব tracked file-এর বদল আর মুছে ফেলা নিজে থেকে stage করে নেওয়া। ছোট কাজে এটি সময় বাঁচায় — আলাদা git add লাগে না। কিন্তু ফাঁদটি নামেই লুকানো: “all” মানে সব tracked file, নতুন (untracked) file নয়। নিচে app.js-এ লবণ ("lobon") যোগ করা হয়েছে, আর একটি নতুন file discount.js লেখা হয়েছে; ডেভেলপার ভাবছেন দুটোই commit হবে:
$ git status -s
M app.js
?? discount.js
$ git commit -a -m "Add discount and salt"
[main 26d4771] Add discount and salt
1 file changed, 1 insertion(+), 1 deletion(-)
$ git status -s
?? discount.js
$ git show --stat --oneline HEAD
26d4771 Add discount and salt
app.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)বার্তায় লেখা “Add discount”, কিন্তু commit-এ আছে শুধু app.js (1 file changed). discount.js এখনো ??. দলের অন্য কেউ এই commit নিলে app.js হয়তো discount.js-কে খুঁজবে আর পাবে না — “আমার কম্পিউটারে তো চলছিল!” সমস্যার একটি ক্লাসিক উৎস। এরপর আমরা অংশ ৪-এর git add -A দিয়ে file-টি পরের commit-এ তুলেছিলাম।
নিচের মডেলে একটি seed-যুক্ত PRNG (mulberry32) সাতটি file-এর একটি এলোমেলো-দেখতে কিন্তু প্রতিবার একই অবস্থা বানায়, তারপর দেখায় commit -a আর “add -A, তারপর commit” — কোনটি কোন file নেয়:
// The "git commit -a" trap: which files really go into the commit?
// A seeded PRNG (mulberry32) builds a random-looking but repeatable folder.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(8);
const kinds = ["untracked (new)", "tracked, modified", "tracked, unchanged", "tracked, deleted"];
const names = ["cart.js", "login.js", "notes.md", "offer.js", "pay.js", "logo.svg", "faq.md"];
const files = names.map(name => ({ name, kind: kinds[Math.floor(rnd() * 4)] }));
// commit -a = "stage every change to files Git ALREADY tracks", then commit.
const inCommitA = f => f.kind === "tracked, modified" || f.kind === "tracked, deleted";
// add -A (or add .) first = also stage new files.
const inAddAll = f => f.kind !== "tracked, unchanged";
console.log("file state commit -a add -A + commit");
for (const f of files)
console.log(f.name.padEnd(12) + f.kind.padEnd(21) +
(inCommitA(f) ? "yes" : "no").padEnd(12) + (inAddAll(f) ? "yes" : "no"));
const lost = files.filter(f => inAddAll(f) && !inCommitA(f)).map(f => f.name);
console.log("\nleft behind by commit -a: " + (lost.join(", ") || "none"));
console.log("still shown as ?? after commit -a: " + lost.length + " file(s)");আউটপুট:
file state commit -a add -A + commit
cart.js untracked (new) no yes
login.js tracked, unchanged no no
notes.md tracked, modified yes yes
offer.js tracked, unchanged no no
pay.js tracked, unchanged no no
logo.svg tracked, modified yes yes
faq.md tracked, deleted yes yes
left behind by commit -a: cart.js
still shown as ?? after commit -a: 1 file(s)
বদলানো দুটি tracked file (notes.md, logo.svg) আর মুছে ফেলা faq.md — তিনটিই commit -a নেয়। বদলহীন file কোনো পথেই commit-এর “বদল” নয় (তবে snapshot-এ আগের মতোই থাকে)। পার্থক্য একটি জায়গায়: নতুন cart.js. commit -a একে ফেলে যায়, আর commit-এর পরে সেটি ?? হয়েই থাকে। mulberry32(8)-এর seed বদলে চালিয়ে দেখুন — যতগুলো untracked file আসবে, ততগুলোই বাদ পড়বে। নিয়মটি মনে রাখুন: commit-এর আগে একবার git status.
উল্টো ঝুঁকিও আছে। commit -a সব tracked file-এর সব বদল নেয় — অংশ ৪-এর debug লাইনের মতো যা আপনি commit করতে চাননি, সেটিও। add -p দিয়ে বেছে তোলার পরে ভুলে commit -a দিলে সেই বাছাই নষ্ট হয়ে যায়।
৭ · মুছে ফেলা, নাম বদল আর খালি ফোল্ডার
git rm — ফোল্ডার আর index দুই জায়গা থেকে
একটি tracked file সাধারণ rm বা File Explorer দিয়ে মুছলে Git সেটিকে “deleted, not staged” (" D") দেখায়; তারপর git add বা git rm দিয়ে মোছাটা stage করতে হয়। git rm দুটি কাজ একসাথে করে — ফোল্ডার থেকে file মোছে আর মোছাটা stage করে:
$ git rm discount.js
rm 'discount.js'
$ git status -s
D discount.js
$ ls
README.md
app.js
css
index.html
prices.js
$ git commit -m "Remove discount until prices are final"
[main 9610b7c] Remove discount until prices are final
1 file changed, 3 deletions(-)
delete mode 100644 discount.jsFile-টি শেষ commit থেকে বাদ গেছে, কিন্তু ইতিহাস থেকে নয় — আগের commit 5903454-এ discount.js এখনো আছে, দরকার হলে ফেরানো যাবে (পর্ব ১১)।
git rm --cached — Git থেকে বাদ, ফোল্ডারে থাকুক
ভুল করে একটি log file commit করে ফেলেছেন। File-টি নিজের কম্পিউটারে রাখতে চান, শুধু Git যেন আর track না করে। তখন --cached: শুধু index থেকে সরায়, ফোল্ডারে হাত দেয় না।
$ git add debug.log
$ git commit -m "Add debug log by mistake"
…
$ git rm --cached debug.log
rm 'debug.log'
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: debug.log
Untracked files:
(use "git add <file>..." to include in what will be committed)
debug.log
$ ls
README.md
app.js
css
debug.log
home.html
images
price-list.js
$ git commit -m "Stop tracking debug.log"
[main 8732561] Stop tracking debug.log
1 file changed, 1 deletion(-)
delete mode 100644 debug.log
$ git status -s
?? debug.logএকই file দুই তালিকায় — index থেকে “deleted” আর ফোল্ডারে “untracked”. ls দেখাচ্ছে file ফোল্ডারেই আছে। কিন্তু ?? হয়ে থাকলে কেউ আবার git add . দিয়ে তুলে ফেলতে পারেন; তাই সাথে সাথে debug.log বা *.log .gitignore-এ লেখা উচিত — পর্ব ১০।
Password বা API key ভুলে commit হলে git rm --cached যথেষ্ট নয়। এটি শুধু পরের commit থেকে file বাদ দেয়; আগের commit-এ গোপন তথ্যটি থেকেই যায়, আর repository কোথাও push হয়ে থাকলে সবাই দেখতে পারেন। প্রথম কাজ হলো সেই key বাতিল করে নতুন key নেওয়া; history পরিষ্কারের উপায় পর্ব ৫৭-এ।
git mv আর rename detection
$ git mv index.html home.html
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: index.html -> home.html
$ git commit -m "Rename index.html to home.html"
[main 14ebbe2] Rename index.html to home.html
1 file changed, 0 insertions(+), 0 deletions(-)
rename index.html => home.html (100%)মজার তথ্য: Git কোথাও “নাম বদল” লিখে রাখে না। Commit-এ থাকে শুধু snapshot — আগের snapshot-এ index.html, নতুনটিতে home.html, বিষয়বস্তু একই। git status, diff বা log দুটি snapshot তুলনা করে বলে, “একটি file গেছে, প্রায় একই রকম একটি এসেছে — নিশ্চয়ই নাম বদল।” একে বলে rename detection; ডিফল্টে বিষয়বস্তু অন্তত ৫০% মিললে Git নাম বদল ধরে ((100%) মানে হুবহু একই)। তাই git mv আসলে শুধু একটি সুবিধা — mv আর git add-এর যোগফল। সাধারণ mv করে পরে add করলেও ফল একই:
$ mv prices.js price-list.js
$ git status -s
D prices.js
?? price-list.js
$ git add -A
$ git status -s
R prices.js -> price-list.js
$ git commit -m "Rename prices.js to price-list.js"
[main ad94308] Rename prices.js to price-list.js
1 file changed, 0 insertions(+), 0 deletions(-)
rename prices.js => price-list.js (100%)add-এর আগে Git দেখাচ্ছে একটি file মুছে গেছে (" D") আর একটি নতুন এসেছে (??)। দুটিই stage হতেই সে মিলিয়ে R (renamed) বলল। একটি পরামর্শ: নাম বদল আর বিষয়বস্তুর বড় পরিবর্তন একই commit-এ না করাই ভালো — মিল ৫০%-এর নিচে নামলে Git একে “একটি মুছে আরেকটি নতুন” দেখাবে, আর পরে file-এর ইতিহাস খোঁজা কঠিন হবে।
খালি ফোল্ডার Git দেখে না — .gitkeep
Git শুধু file track করে; ফোল্ডার আসে file-এর path থেকে। তাই খালি ফোল্ডারের কোনো অস্তিত্বই Git-এর কাছে নেই:
$ mkdir images
$ git status
On branch main
nothing to commit, working tree clean
$ touch images/.gitkeep
$ git status -s
?? images/
$ git add images
$ git commit -m "Add empty images folder"
[main 809cc53] Add empty images folder
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 images/.gitkeepmkdir images-এর পরেও working tree clean. ভেতরে একটি ফাঁকা file রাখতেই ফোল্ডারটি দেখা দিল। এই file-এর প্রচলিত নাম .gitkeep — কিন্তু এটি শুধু একটি প্রথা, Git-এর কোনো বিশেষ নিয়ম নয়; যেকোনো নামেই কাজ হয়। অনেক দল ফাঁকা file-এর বদলে ছোট একটি README বা ফোল্ডারের ভেতরের সব কিছু উপেক্ষা করার একটি .gitignore রাখেন।
৮ · ছোট, এক-উদ্দেশ্যের commit — আর git log-এ এক নজর
এই পর্বের পুরো কাজের ইতিহাস এখন এরকম:
$ git log --oneline
8732561 Stop tracking debug.log
99f2ccd Add debug log by mistake
809cc53 Add empty images folder
ad94308 Rename prices.js to price-list.js
14ebbe2 Rename index.html to home.html
9610b7c Remove discount until prices are final
5903454 Add stylesheet, discount and run note
26d4771 Add discount and salt
37c2b34 Raise rice price to 75 taka
bb4928a Add price list and total
be72ad5 Print each item on its own line
8bfb77a Add product list pagegit log --oneline প্রতিটি commit-কে এক লাইনে দেখায়: ছোট ID আর বার্তার প্রথম লাইন, নতুন commit সবার ওপরে। ১২টি commit, প্রতিটির একটি কাজ — দাম বাড়ানো, নাম বদল, একটি ভুল শোধরানো। log-এর অসংখ্য বিকল্প (--graph, -p, --stat, তারিখ দিয়ে খোঁজা) পর্ব ০৯-এ (status, diff, log আর show).
একটি খারাপ ইতিহাস কল্পনা করুন: সপ্তাহের শেষে একটিমাত্র commit, বার্তা update, তাতে ৪০টি file আর দাম বদল, নতুন page, debug লাইন আর একটি bug fix — সব একসাথে। এখন দুটি বিপদ:
- ফেরানো: দাম বদলটি ভুল ছিল। ফেরাতে গেলে পুরো সপ্তাহের কাজ ফেরাতে হবে, নয়তো হাতে বেছে বেছে খুঁজতে হবে। ছোট commit হলে শুধু
37c2b34-কে revert করলেই হতো (পর্ব ১১)। - খোঁজা: কোন পরিবর্তনে bug এসেছে, Git-এর
bisectcommit ধরে ধরে binary search করে বের করতে পারে (পর্ব ৫৫)। কিন্তু উত্তর যদি হয় “৪০ file-এর ওই বিশাল commit”, তাতে প্রায় কিছুই জানা হলো না। - Review: ২০০ লাইনের এক-উদ্দেশ্যের পরিবর্তন একজন reviewer মন দিয়ে পড়তে পারেন; ২০০০ লাইনের মিশ্র পরিবর্তন সাধারণত চোখ বুলিয়ে “ঠিক আছে” পায় (code review, পর্ব ৪৪)।
একটি atomic commit একটি যুক্তিসংগত পরিবর্তন বহন করে — পুরোটা নিলে project কাজ করে, অর্ধেক নিলে নয়। মাপকাঠি লাইনের সংখ্যা নয়, উদ্দেশ্যের সংখ্যা: বার্তায় “আর” (and) লিখতে হলে সম্ভবত দুটি commit হওয়া উচিত ছিল। আমাদের 26d4771 Add discount and salt ঠিক এই ভুলের উদাহরণ — নামেই দুটি কাজ, আর একটি আবার বাদও পড়েছে।
তবে উল্টো দিকেও বাড়াবাড়ি আছে। প্রতিটি টাইপো-র জন্য আলাদা commit, “wip”, “wip2”, “fix again” — এমন ইতিহাসও পড়া কঠিন। এর সমাধান হলো নিজের branch-এ ইচ্ছেমতো ছোট commit করা, আর অন্যদের সাথে ভাগ করার আগে interactive rebase (পর্ব ১৫) দিয়ে গুছিয়ে নেওয়া, বা pull request-এ squash করা (পর্ব ১৬)। Linux kernel-এর patch জমা দেওয়ার নির্দেশিকাতেও “প্রতিটি যুক্তিসংগত পরিবর্তন আলাদা patch-এ” রাখার কথা বলা আছে — বড় open-source project-এ এটি পুরোনো অভ্যাস।
পরীক্ষা ও ইন্টারভিউতে:
- Git-এর তিনটি এলাকা কী কী? — Working tree (ফোল্ডারের file), staging area বা index (পরের commit-এর প্রস্তুত তালিকা), repository (
.git-এ জমা commit). git add-এর পরে file বদলিয়ে commit করলে কোন version যায়? — add-এর সময়ের version, কারণ commit index থেকে তৈরি হয়; নতুন বদলের জন্য আবার add লাগে।git commit -aকি নতুন file নেয়? — না; শুধু tracked file-এর বদল আর মুছে ফেলা নেয়।git rmআরgit rm --cached-এর পার্থক্য? — প্রথমটি ফোল্ডার আর index দুই জায়গা থেকে মোছে; দ্বিতীয়টি শুধু index থেকে, file ফোল্ডারে untracked হয়ে থাকে।- একটি commit-এ কী কী থাকে? — Tree (পুরো snapshot), parent (প্রথম commit-এ নেই), author, committer, সময় আর বার্তা; সব মিলিয়ে hash-ই commit ID.
৯ · AI-এর যুগে — Git command-এ AI-কে যাচাই
“আমার file commit হচ্ছে না” — এমন প্রশ্নে AI সাধারণত কয়েক সেকেন্ডে কয়েকটি command দিয়ে দেয়। বেশিরভাগ সময় কাজও হয়। কিন্তু এই পর্বের বিষয়ে AI-এর কয়েকটি পরিচিত ভুল আছে, আর তার কয়েকটি আপনার কাজ মুছে দিতে পারে:
| AI যা বলে | সমস্যা | যাচাই |
|---|---|---|
“Unstage করতে git reset --hard দিন” | Index-এর সাথে ফোল্ডারের না-তোলা বদলও মুছে যায়, ফেরত পাওয়া কঠিন | Unstage-এর জন্য git restore --staged; পর্ব ১১ |
“File আর track করবেন না? git rm দিন” | --cached ছাড়া file ফোল্ডার থেকেও মুছে যায় | git help rm, আগে একটি throwaway repo-তে চালান |
“git commit -a সব file নেবে” | নতুন file বাদ পড়ে | Commit-এর পরে git status, git show --stat |
“git add . দিয়ে সব তুলে দিন” | .env, key, log সবই উঠে যেতে পারে | আগে git status, তারপর git diff --cached |
| “Git প্রতিটি commit-এ শুধু diff রাখে” | ভুল ধারণা; commit পূর্ণ snapshot (জমা রাখার সময় Git পরে সংকোচন করে) | git cat-file -p HEAD-এ tree দেখুন |
বানানো flag, যেমন git add --new-only | এমন flag নেই; Git error দেবে, বা মিলে যাওয়া অন্য কিছু করবে | git help add বা git-scm.com-এর documentation |
AI-কে Git-এর প্রশ্ন করলে আপনার repository-র আসল অবস্থা দিন, আর ধ্বংসাত্মক command সম্পর্কে আগে সাবধান করতে বলুন:
AI-এর দেওয়া Git command চালানোর আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১অনেক version control-এ staging area নেই — সরাসরি commit হয়। Git কেন এই বাড়তি ধাপ রাখল, আর এটি কি সত্যিই দরকারি?
Subversion-এর মতো centralized ব্যবস্থায় svn commit আপনার কাজের কপির সব বদল (বা নির্দিষ্ট file-এর বদল) একবারে পাঠায়। Mercurial-ও ডিফল্টে সরাসরি commit করে; অংশবিশেষ বাছাই করতে তার আলাদা interactive ব্যবস্থা আছে। Git-এর index তাহলে কোথা থেকে এলো?
Git-এর জন্ম ২০০৫ সালে Linux kernel-এর কাজের জন্য, আর সেই দলের সংস্কৃতি ছিল email-এ patch পাঠানো। একজন maintainer-এর কাছে প্রতিদিন অসংখ্য patch আসে; প্রতিটি patch একটি যুক্তিসংগত পরিবর্তন হওয়ার কথা। তাই “কোন বদল একসাথে যাবে” সেটি সাজানোর একটি জায়গা স্বাভাবিকভাবেই দরকার ছিল। শুরুতে index ছিল মূলত ভেতরের একটি কাঠামো — কোন file-এর কোন বিষয়বস্তু, কোন সময়ে দেখা হয়েছে, তার তালিকা, যাতে বড় project-এ “কী বদলেছে” দ্রুত বের করা যায়। পরে add -p-এর মতো সুবিধা এসে এটিকে একটি ব্যবহারকারীর হাতিয়ারও বানায়।
লাভটা বাস্তব: একটি file-এ bug fix আর নতুন feature মিশে গেলে দুটিকে আলাদা commit-এ রাখা যায়; commit-এর আগে git diff --cached দিয়ে ঠিক কী যাচ্ছে তা পড়া যায়; merge conflict-এর সময় (পর্ব ১৩) index-ই মনে রাখে কোন file মীমাংসা হয়েছে। দাম হলো ধারণার জটিলতা — একই file-এর তিনটি কপি নতুনদের প্রায়ই বিভ্রান্ত করে, আর অনেক সমালোচক বলেন Git-এর UI এই জটিলতা খোলামেলা রেখে দিয়েছে। এজন্যই commit -a আছে, আর অনেক GUI index-কে প্রায় লুকিয়ে রাখে। নতুন কিছু version control (যেমন Jujutsu) ইচ্ছে করেই আলাদা staging area ছাড়া নকশা করা হয়েছে — সেখানে কাজের কপিই সবসময় একটি চলমান commit.
মূল উপলব্ধি: Staging area একটি নকশা-সিদ্ধান্ত, প্রকৃতির নিয়ম নয়। এটি “কী commit হবে” আর “ফোল্ডারে কী আছে” আলাদা করে আপনাকে পরিষ্কার ইতিহাস বানানোর ক্ষমতা দেয়, বিনিময়ে একটু বেশি ধারণা শিখতে হয়।
প্র ০২Commit যদি প্রতিবার পুরো snapshot রাখে, তাহলে ১০,০০০ commit-এর repository কি বিশাল হয়ে যাবে না? পুরোনো ব্যবস্থাগুলো কেন diff রাখত?
১৯৮০-র দশকের RCS আর তার ওপর গড়া CVS প্রতিটি file-এর ইতিহাস রাখত delta হিসেবে — একটি পূর্ণ version আর বাকিগুলো “আগের থেকে কী বদলেছে”। তখন disk ছিল দামি, তাই ধারণাটি যুক্তিসংগত। সমস্যা হলো, একটি পুরোনো version পেতে অনেকগুলো delta পরপর প্রয়োগ করতে হয়, আর “পুরো project কোনো এক মুহূর্তে কেমন ছিল” প্রশ্নের উত্তর file ধরে ধরে জোড়া দিতে হয়।
Git-এর চিন্তা উল্টো: ধারণায় প্রতিটি commit পুরো snapshot, কিন্তু জমা রাখায় চালাকি। প্রথম চালাকি content addressing — একটি file-এর বিষয়বস্তুর hash-ই তার নাম। দুটি commit-এ README.md না বদলালে দুটির tree একই hash-এর দিকে নির্দেশ করে, বিষয়বস্তু একবারই জমা থাকে। এই পর্বের দ্বিতীয় commit-এ তিনটি file-এর snapshot, কিন্তু নতুন জমা হয়েছে শুধু app.js-এর নতুন বিষয়বস্তু, একটি নতুন tree আর commit-টি। দ্বিতীয় চালাকি packfile: git gc বা push-এর সময় Git অনেক object একসাথে একটি file-এ ভরে, আর মিল থাকা object-গুলোকে একে অপরের delta হিসেবে সংকুচিত করে। অর্থাৎ delta আছে, কিন্তু সেটি জমা রাখার কৌশল, ইতিহাসের মডেল নয়।
লাভ: যেকোনো commit checkout করা দ্রুত, দুটি যেকোনো commit-এর তুলনা সহজ, branch বানানো প্রায় খরচহীন (শুধু একটি pointer, পর্ব ১২)। দাম: বিশাল binary file (ভিডিও, ডিজাইন file) প্রতিবার বদলালে প্রতিটি version আলাদা জমা হয়, কারণ সেগুলো ভালোভাবে সংকুচিত হয় না — এজন্যই Git LFS-এর মতো সমাধান (পর্ব ১৯)।
মূল উপলব্ধি: “ব্যবহারকারী কী দেখেন” (snapshot) আর “ভেতরে কীভাবে জমা থাকে” (hash দিয়ে ভাগাভাগি আর সংকোচন) — দুটি আলাদা স্তর। ভালো নকশায় সহজ মডেল উপরে থাকে, কৌশল নিচে লুকানো।
প্র ০৩একটি byte বদলালে পরের সব commit-এর ID বদলে যায় — এটি কি একটি অসুবিধা, নাকি জেনেশুনে রাখা একটি সুবিধা?
দুটোই, তবে মূলত সুবিধা। প্রতিটি commit-এর লেখায় parent-এর hash থাকায় শেষ commit-এর ID আসলে পুরো ইতিহাসের একটি সারাংশ। দুজনের কাছে একই branch-এর শেষ ID একই হলে নিশ্চিত হওয়া যায় (hash function নিরাপদ ধরে নিয়ে) যে তাঁদের পুরো ইতিহাস হুবহু এক — প্রতিটি file, প্রতিটি বার্তা, প্রতিটি সময়। disk-এর ত্রুটি বা network-এ কোনো byte নষ্ট হলে hash মিলবে না, Git সাথে সাথে ধরবে (git fsck). আর কেউ চুপিচুপি পুরোনো একটি commit বদলে দিলে পরের সব ID বদলে যাবে, তাই অন্যদের কপির সাথে মিলিয়ে সেটি ধরা পড়বে। ব্লকচেইনের hash-শিকলের ধারণাও এর আত্মীয়।
অসুবিধা দেখা যায় history নতুন করে লেখার সময়। commit --amend বা rebase (পর্ব ১১, ১৫) আসলে পুরোনো commit বদলায় না; নতুন ID-র নতুন commit বানায়। আপনার সহকর্মী পুরোনো ID-গুলোর ওপর কাজ করে থাকলে দুই ইতিহাস আলাদা হয়ে যায় — এজন্যই প্রচলিত নিয়ম, “অন্যের সাথে ভাগ করা history নতুন করে লিখবেন না”।
নিশ্চয়তাটি hash function-এর ওপর নির্ভর করে। ২০১৭ সালে প্রকাশিত “SHAttered” গবেষণায় দুটি ভিন্ন PDF দেখানো হয় যাদের SHA-1 একই — বিপুল গণনার খরচে। Git এরপর collision-চেষ্টা চেনার ব্যবস্থাসহ SHA-1 ব্যবহার শুরু করে, আর SHA-256 repository-র সমর্থন যোগ করে; পুরো ecosystem-এর রূপান্তর এখনো চলমান। মনে রাখবেন, commit-এর author নাম আর email যে কেউ যা খুশি লিখতে পারেন — hash অখণ্ডতা দেয়, পরিচয়ের প্রমাণ নয়। পরিচয়ের জন্য signed commit লাগে (পর্ব ৫৭)।
মূল উপলব্ধি: Content-addressed ইতিহাস “কী হয়েছিল” প্রশ্নে অখণ্ডতা দেয়, কিন্তু “কে করেছিল” প্রশ্নে নয়। আর ইতিহাস বদলানো কখনো “সম্পাদনা” নয় — সবসময় নতুন ইতিহাস তৈরি।
প্র ০৪Linus Torvalds-এর Git-এর প্রথম commit-এরও কোনো parent ছিল না। প্রথম commit কেন বিশেষ, আর একটি repository-তে কি একাধিক root commit থাকতে পারে?
Git-এর নিজের source code-এর repository-র সবচেয়ে পুরোনো commit সাধারণভাবে এপ্রিল ২০০৫-এর বলে উল্লেখ করা হয়, বার্তা মোটামুটি “Initial revision of "git", the information manager from hell”. এটিও একটি সাধারণ commit — tree, author, committer, বার্তা — শুধু parent লাইন নেই। এই পর্বের 8bfb77a-এর মতোই।
Root commit বিশেষ কয়েকটি কারণে। এক, history-র শিকল এখানেই শেষ; git log এখানে এসে থামে। দুই, “আগের commit-এর সাথে তুলনা” এখানে খাটে না — তাই প্রথম commit-এর git show সব file-কে নতুন দেখায়, আর অনেক command-এ “HEAD~1” (আগেরটি) খুঁজলে error আসে। তিন, প্রথম commit-এর আগে HEAD এমন একটি branch-এর দিকে নির্দেশ করে যার কোনো commit নেই (Git একে “unborn branch” বলে) — এজন্যই git status “No commits yet” বলে, আর unstage-এর পরামর্শও আলাদা।
একাধিক root commit সম্ভব, আর বাস্তবে আছে। দুটি আলাদা project-এর ইতিহাস একসাথে মেলালে (merge-এ --allow-unrelated-histories, বা subtree, পর্ব ১৯) মিলিত repository-তে দুটি root থাকে; Git-এর নিজের repository-তেও এমন কয়েকটি root আছে বলে জানা যায়, কারণ কিছু আলাদা tool পরে মেলানো হয়েছিল। git switch --orphan দিয়েও parent-হীন নতুন branch শুরু করা যায় — যেমন একটি website-এর তৈরি file আলাদা branch-এ রাখতে।
মূল উপলব্ধি: Git-এর ইতিহাস একটি শিকল নয়, একটি graph — যেখানে commit-এর parent শূন্য (root), এক (সাধারণ) বা একাধিক (merge) হতে পারে। প্রথম commit শুধু সেই graph-এর একটি প্রান্ত।
প্র ০৫একজন নতুন developer ভুলে .env file (যাতে database password) commit করে push করেছেন। তিনি git rm --cached .env দিয়ে নতুন commit করলেন। সমস্যা কি মিটল?
না। git rm --cached শুধু পরের snapshot থেকে file বাদ দেয়। আগের commit-এর tree-তে .env এখনো আছে, আর সেই commit-এর hash তার পরের সব commit-এর ভিত্তি — অর্থাৎ ইতিহাস না বদলে সেটি সরানো যায় না। যিনি repository clone করবেন, তিনি পুরোনো commit checkout করে password পড়তে পারবেন। Public repository হলে ঝুঁকি আরও বড়: স্বয়ংক্রিয় bot-গুলো public repository-তে ফাঁস হওয়া key খোঁজে বলে বিভিন্ন নিরাপত্তা রিপোর্টে বারবার বলা হয়েছে, তাই push আর অপব্যবহারের মাঝে সময় খুব কম হতে পারে।
সঠিক ক্রম, নিরাপত্তার দিক থেকে: প্রথমে secret বাতিল করে নতুন secret নেওয়া (rotate) — password বদলানো, API key revoke করা। এটিই আসল প্রতিরক্ষা, কারণ ফাঁস হওয়া তথ্য ফেরানো যায় না, শুধু অকেজো করা যায়। তারপর .env-কে .gitignore-এ যোগ (পর্ব ১০), আর একটি .env.example রাখা যাতে অন্যরা জানেন কোন setting লাগবে। এরপর দরকার হলে history থেকে file সরানো — git filter-repo-এর মতো tool দিয়ে; এতে পরের সব commit-এর ID বদলে যায় (প্র ০৩), তাই দলের সবাইকে নতুন করে clone করতে হয়, আর hosting platform-এর cache বা fork-এ পুরোনো কপি থেকে যেতে পারে। পর্ব ৫৭-এ বিস্তারিত।
প্রতিরোধ সবচেয়ে সস্তা: project শুরুর দিনেই .gitignore, commit-এর আগে git status আর git diff --cached পড়া, চোখ বন্ধ করে git add . না দেওয়া, আর pre-commit hook বা platform-এর secret scanning চালু রাখা (লেখার সময়ে বড় hosting platform-গুলো এমন সুবিধা দেয়, platform-ভেদে আলাদা)।
মূল উপলব্ধি: Git ভুলতে পারে না — সেটাই তার শক্তি, আর গোপন তথ্যের ক্ষেত্রে সেটাই বিপদ। একবার commit হওয়া secret-কে “ফাঁস হয়ে গেছে” ধরে নিয়ে আগে secret বদলান, তারপর history.
অনুশীলন
নিজের কম্পিউটারে একটি খালি scratch ফোল্ডারে চালিয়ে মিলিয়ে নিন — আসল project-এ নয়।
-
Command-এর ক্রম লিখুন: একটি নতুন ফোল্ডার
result-portal(একটি বিশ্ববিদ্যালয়ের ফলাফল পোর্টাল — কাল্পনিক)। ভেতরে আছেREADME.md,grade.jsআরnotes.txt(আপনার ব্যক্তিগত নোট)। লক্ষ্য: (ক) branchmainদিয়ে repository শুরু, (খ) প্রথম commit-এ শুধুREADME.mdআরgrade.js, (গ) তারপরgrade.js-এ দুটি বদল — একটি bug fix আর একটি নতুন feature — দুটি আলাদা commit-এ।notes.txtকখনো commit হবে না।git init -b main result-portal cd result-portal # (file তিনটি লেখা হলো) git status -s # ?? README.md ?? grade.js ?? notes.txt git add README.md grade.js # নাম ধরে — add . নয় git status -s # A README.md A grade.js ?? notes.txt git commit -m "Add grade calculator" # grade.js-এ দুটি বদল করা হলো git add -p grade.js # bug fix-এর hunk: y, feature-এর hunk: n git diff --cached # শুধু fix যাচ্ছে কি না দেখা git commit -m "Fix rounding of GPA" git add grade.js # বাকি feature-এর বদল git commit -m "Add letter grade" git log --oneline # তিনটি commitমূল বিষয়:
git add .বাcommit -aএড়ানো — প্রথমটিnotes.txtতুলে দিত। স্থায়ী সমাধান হলোnotes.txt-কে.gitignore-এ রাখা (পর্ব ১০)। দুটি বদল একই hunk-এ পড়লে (খুব কাছাকাছি লাইন)sদিয়ে ভাঙার চেষ্টা করুন, না ভাঙলেeদিয়ে হাতে সম্পাদনা। -
আউটপুট অনুমান করুন: একটি repository-তে
a.txtআরb.txtcommit করা আছে, ফোল্ডার clean. এবার পরপর চালানো হলো:echo two >> a.txt git add a.txt echo three >> a.txt git rm -q b.txt echo new > c.txt git status -sgit status -sকী দেখাবে? আর এখনgit commit -aদিলে কোন file-এর কোন পরিবর্তন commit-এ যাবে?Git ২.৫৩-এ চালিয়ে পাওয়া আসল আউটপুট:
$ git status -s MM a.txt D b.txt ?? c.txt $ git commit -a --dry-run --short M a.txt D b.txt ?? c.txtMM a.txt: index-এ “two” যোগ হওয়া version (staged, প্রথমM), ফোল্ডারে তার পরে “three”-ও (দ্বিতীয়M).D b.txt:git rmমোছাটা সরাসরি stage করেছে।?? c.txt: নতুন, untracked.--dry-runদেখাচ্ছেcommit -aকরলেa.txt-এর দুটি লাইনই (“two” আর “three”) যাবে, কারণ-aআগে সব tracked file-এর বর্তমান অবস্থা stage করে (MMথেকেM);b.txt-এর মোছা যাবে;c.txtযাবে না। শুধুgit commit(-aছাড়া) দিলে যেত শুধু “two” আরb.txt-এর মোছা। -
সমালোচনা করুন: একটি দলের
git log --oneline-এর শেষ ছয়টি লাইন:f00a1b2 final,c3d4e5f final2,9a8b7c6 update,1e2f3a4 fixed login and added dark mode and changed db password,5b6c7d8 wip,0d9e8f7 asdf. কী কী সমস্যা দেখছেন? দলের জন্য তিনটি সহজ নিয়ম প্রস্তাব করুন।সমস্যা: (১)
final,update,wip,asdf— বার্তা থেকে কিছুই বোঝা যায় না; ছয় মাস পরে কেউ জানবেন না কোন commit কী করেছে। (২)1e2f3a4-এ তিনটি অসম্পর্কিত কাজ — login fix আর dark mode আলাদাভাবে revert বা review করা যাবে না। (৩) সবচেয়ে গুরুতর: বার্তা বলছে db password বদলানো হয়েছে — password কি কোড বা config file-এ লেখা? তাহলে সেটি ইতিহাসে চিরকাল থাকবে; এখনই password rotate করে.envআর.gitignore-এ সরানো দরকার (পর্ব ৫৭)। (৪)wipআরasdfসম্ভবত অসম্পূর্ণ, project হয়তো সেখানে চলেই না।প্রস্তাবিত নিয়ম: ক) প্রতি commit-এ একটি উদ্দেশ্য; বার্তায় “and” লাগলে ভাগ করা। খ) বার্তার প্রথম লাইনে কী বদলেছে, আদেশসূচক ক্রিয়ায় (যেমন “Fix login redirect after timeout”), দরকারে নিচে কেন — বিস্তারিত নিয়ম পর্ব ১৮-এ। গ) কোনো secret কখনো repository-তে নয়; commit-এর আগে
git diff --cachedপড়া। নিজের branch-এwipচলতে পারে, কিন্তু ভাগ করার আগে গুছিয়ে নেওয়া (পর্ব ১৫)। -
Model বাড়ান: অংশ ২-এর
three_areas.js-এ একটিcommitAll()function যোগ করুন, যা আসলgit commit -a-র মতো আচরণ করবে। তারপর শেষে “edit a.txt; create d.txt; commitAll()” চালিয়ে দেখানd.txtuntracked থেকে যায়। কোন শর্তে একটি file “tracked”?const commitAll = () => { for (const f of Object.keys(index)) // tracked = index-এ আছে if (f in work) index[f] = work[f]; // বদল stage else delete index[f]; // মুছে ফেলা stage commit(); }; work["a.txt"] = "v3"; work["d.txt"] = "v1"; commitAll(); show("git commit -a");প্রত্যাশিত শেষ লাইনগুলো:
a.txt W=v3 I=v3 H=v3 unmodifiedআরd.txt W=v1 I=-- H=-- ?? untracked. মূল কথা হলো loop চলছেindex-এর key-এর ওপর,work-এর ওপর নয় — তাই নতুন file নজরেই আসে না। এই মডেলে “tracked” মানে index-এ একটি entry আছে; আসল Git-এও তাই — শেষ commit-এর সব file checkout-এর সময় index-এ থাকে, আরgit addনতুন file-কে index-এ ঢুকিয়ে tracked বানায়। (মুছে ফেলা file-এর জন্য মডেলেরshowশুধুwork-এর file দেখায়, তাই সেটি তালিকায় আসবে না।)
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নতুন project-এর প্রথম commit পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
git init -b main— repository তৈরি- Editor-এ file লেখা — file untracked
git add <file>— index-এ তোলাgit statusবাgit diff --cached— কী যাচ্ছে যাচাইgit commit -m "…"— root commit তৈরিgit log --oneline— ইতিহাসে এক লাইন
একটি Git repository-তে git add app.js চালানোর পরে app.js-এ আরও একটি লাইন যোগ করে save করা হলো, তারপর git commit -m "Update app". Commit-এ app.js-এর কোন version যাবে?
- Save করা সর্বশেষ version, নতুন লাইনসহ
git addচালানোর সময়ের version, নতুন লাইন ছাড়া- কোনোটিই নয় — Git commit বাতিল করবে
- দুই version মিলিয়ে একটি নতুন version
git status -s একটি file-এর পাশে AM দেখাচ্ছে। এর অর্থ কী?
- File-টি untracked, Git এখনো দেখেনি
- File-টিতে merge conflict হয়েছে
- File-টি commit হয়ে গেছে, পরে বদলেছে
- নতুন file staged হয়েছে, তারপর ফোল্ডারে আবার বদলানো হয়েছে
একটি repository-তে notes.md বদলানো (tracked) আর cart.js নতুন (untracked). git commit -a -m "Add cart" চালালে কী হবে?
- শুধু
notes.md-এর বদল commit হবে;cart.jsuntracked থেকে যাবে - দুটি file-ই commit হবে
- কিছুই commit হবে না, error আসবে
- শুধু
cart.jscommit হবে
git cat-file -p দিয়ে একটি repository-র একটি commit দেখা হলো। এতে tree, author, committer আর বার্তা আছে, কিন্তু parent লাইন নেই। এর সবচেয়ে সম্ভাব্য অর্থ কী?
- Commit-টি নষ্ট হয়ে গেছে
- এটি একটি merge commit
- এটি একটি root commit, যেমন repository-র প্রথম commit
- Author আর committer একই ব্যক্তি বলে parent লেখা হয়নি
একটি repository-তে খালি ফোল্ডার images/ তৈরি করে git add images আর git status চালানো হলো। কী দেখা যাবে, আর ফোল্ডারটি track করার প্রচলিত উপায় কী?
images/staged দেখাবে; আর কিছু লাগবে না- কোনো বদল দেখাবে না; ভেতরে
.gitkeepনামের একটি ফাঁকা file রেখে add করা - Error দেখাবে;
git add --empty-dirsদিতে হবে images/untracked দেখাবে;.gitignore-এ লিখলে track হবে
একটি commit করা file debug.log নিজের কম্পিউটারে রাখতে চান, কিন্তু Git যেন আর track না করে। কোন command?
git rm debug.loggit restore debug.loggit reset --hardgit rm --cached debug.log, তারপর commit
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
git init -b mainফোল্ডারে একটি.gitবানায় — সেটিই repository; হাতে বদলাবেন না, মুছবেন না।- তিন এলাকা: working tree →
git add→ staging area (index) →git commit→ repository. Commit নেয় index-এর কপি. - অবস্থা: untracked (
??), unmodified, modified (" M"), staged ("M ",A); দুই অক্ষর = index বনাম HEAD, ফোল্ডার বনাম index. - Commit = tree (পুরো snapshot) + parent + author + committer + বার্তা, তার hash-ই ID; প্রথম commit-এ parent নেই।
commit -aনতুন file নেয় না;rm --cachedফোল্ডারে রাখে; Git নাম বদল অনুমান করে; খালি ফোল্ডারে.gitkeep; আর প্রতি commit-এ একটি উদ্দেশ্য.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৯ · History আর পার্থক্য দেখা — status, diff, log আর showপরবর্তী পর্বতিন এলাকার মধ্যে পার্থক্য পড়া আর ইতিহাস খোঁজা।
- পর্ব ১১ · ভুল শোধরানো — restore, reset, revert, amend আর reflogফেরার পথছবি ৮.১-এর দাগ-কাটা তীরগুলোর পুরো গল্প।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- DSA · Hash tableপাশের ট্র্যাকHash function কীভাবে কাজ করে — Git-এর object নামের ভিত্তি।
- Operating Systems · File system interfaceপাশের ট্র্যাকFile, ফোল্ডার, path আর লুকানো file — Git যার ওপর দাঁড়িয়ে।
- Computer Networks · Cryptography-র মৌলিকগভীরে যানCryptographic hash, collision আর SHA পরিবার।