এই পর্বে যা শিখবেন
- কোন file repository-তে রাখবেন আর কোনটি নয় — কারণসহ একটি পরিষ্কার নিয়ম
.gitignore-এর প্রতিটি pattern-নিয়ম, negation, parent folder-এর ফাঁদ আর কোন ignore-তালিকা কাকে ছাপিয়ে যায়git check-ignore -v,git status --ignoredআরgit rm --cachedদিয়ে ভুল ধরা ও শোধরানো- CRLF বনাম LF,
core.autocrlf-এর তিনটি মান আর.gitattributesদিয়ে দলের জন্য একটি স্থায়ী সমাধান git config-এর স্তর,--show-origin, কাজের setting, alias আর অফিস-ব্যক্তিগত email আলাদা রাখারincludeIf
১ · কী track করবেন, কী করবেন না
একটি কাল্পনিক অনলাইন বাজার অ্যাপের দলে নতুন যোগ দিয়েছেন রহিম। প্রথম দিনেই তিনি project folder-এ git add . দিয়ে সব কিছু commit করে push করলেন। বিকেলে দলের lead জানালেন তিনটি সমস্যা: repository-তে হাজার হাজার file-এর node_modules/ folder ঢুকেছে, প্রতিটি build-এর পর dist/ folder-এর বদল নিয়ে অর্থহীন diff আসছে, আর সবচেয়ে খারাপ — database-এর password লেখা .env file-টিও এখন সবাই দেখতে পাচ্ছেন।
রহিমের ভুলটা খুব সাধারণ। পর্ব ৮-এ দেখেছেন, git add একটি file-কে working tree থেকে staging area-য় তোলে, আর commit সেটিকে repository-র স্থায়ী ইতিহাসে লিখে দেয়। কিন্তু কোন file আদৌ ইতিহাসে থাকার যোগ্য, সেই সিদ্ধান্ত Git নেয় না — নিতে হয় আপনাকে।
মায়ের রান্নাঘরের রেসিপি খাতার কথা ভাবুন। খাতায় লেখা থাকে বিরিয়ানির রেসিপি — কতটা চাল, কতটা মশলা, কতক্ষণ দমে। রান্না করা বিরিয়ানি কেউ খাতার পাতায় সংরক্ষণ করেন না; রেসিপি থাকলে যেকোনো দিন আবার রান্না করা যায়। আবার আলমারির চাবি কোথায় রাখা আছে, সেটাও কেউ রেসিপি খাতায় লেখেন না, কারণ খাতাটা আত্মীয়রাও উল্টে দেখেন। Repository হলো সেই খাতা: source code রেসিপি, build output রান্না করা খাবার, আর password হলো আলমারির চাবি।
| ধরন | উদাহরণ | কেন রাখবেন না | বদলে যা commit করবেন |
|---|---|---|---|
| Dependency | node_modules/, .venv/, vendor/ | Package manager আবার install করতে পারে; আকারে বিশাল, OS-ভেদে আলাদা হতে পারে | package.json আর lockfile (package-lock.json) |
| Build output | dist/, build/, *.o, *.class, __pycache__/ | Source থেকে যেকোনো সময় বানানো যায়; প্রতি build-এ বদলায়, diff আর conflict বাড়ায় | Source আর build script |
| Log আর অস্থায়ী file | *.log, tmp/, coverage/ | চালানোর ফল, প্রত্যেকের কম্পিউটারে আলাদা | কিছুই না |
| IDE আর editor | .vscode/, .idea/, *.swp | ব্যক্তিগত পছন্দ; একজনের setting আরেকজনের কাজে বাধা দেয় | দলের সবার জন্য একই নিয়ম হলে .editorconfig |
| OS-এর file | .DS_Store (macOS), Thumbs.db, desktop.ini (Windows) | OS নিজে বানায়, project-এর অংশ নয় | কিছুই না (এগুলো ব্যক্তিগত global ignore-এ রাখাই ভালো, অংশ ৩) |
| Secret | .env, *.pem, credentials.json | ফাঁস হলে অন্য কেউ আপনার system-এ ঢুকতে পারে | খালি মানের .env.example; আসল মান secret manager বা CI-এর গোপন setting-এ |
যা মানুষ লেখেন আর যা থেকে project আবার বানানো যায়, তা commit করুন। যা যন্ত্র বানায়, যা একজনের ব্যক্তিগত, আর যা গোপন — তা নয়। একটি বড় ব্যতিক্রম lockfile: এটি যন্ত্রের বানানো, তবু commit করা হয়, কারণ এতেই লেখা থাকে প্রতিটি dependency-র ঠিক কোন version ব্যবহার হয়েছিল। তা না থাকলে আজকের build আর ছয় মাস পরের build একই কোড থেকে আলাদা ফল দিতে পারে।
কিছু জায়গায় দলভেদে মত আলাদা। কিছু Go বা PHP দল ইচ্ছে করেই vendor/ folder commit করে, যাতে internet ছাড়াও build হয়। কিছু দল .vscode/extensions.json-এর মতো সবার কাজের একটি-দুটি file রাখে, বাকি .vscode/ বাদ দেয়। নিয়ম মুখস্থ করার চেয়ে প্রশ্নটা মনে রাখুন: “এই file কি মানুষ লেখেন, নাকি যন্ত্র বানায়? এটি কি সবার জন্য, নাকি শুধু আমার?”
.gitignore কোনো নিরাপত্তা-ব্যবস্থা নয়। এটি শুধু বলে “এই file নিজে থেকে যোগ করা হবে না” — git add -f দিলে যোগ হয়ে যায়। আর যে secret একবার commit হয়ে push হয়েছে, পরে file মুছলেও সেটি পুরোনো commit-এ থেকে যায়; যাঁরা clone করেছেন তাঁদের কাছেও। তখন প্রথম কাজ ইতিহাস ঘষামাজা নয়, secret বদলে ফেলা (নতুন password, পুরোনো key বাতিল)। ইতিহাস পরিষ্কার আর secret scanning বিস্তারিত দেখবেন পর্ব ৫৭-এ।
২ · .gitignore-এর ভাষা — pattern-এর নিয়ম
.gitignore একটি সাধারণ text file, সাধারণত project-এর root-এ থাকে আর বাকি কোডের মতোই commit করা হয় — যাতে দলের সবাই একই তালিকা পান। এর প্রতিটি লাইন একটি glob pattern. নিয়মগুলো Git-এর অফিসিয়াল gitignore documentation অনুযায়ী (git help gitignore):
- খালি লাইন কিছুই মেলায় না — শুধু পড়ার সুবিধার জন্য ফাঁক।
#দিয়ে শুরু লাইন মন্তব্য। নামের শুরুতেই সত্যিকারের#থাকলে লিখুন\#. লাইনের শেষের ফাঁকা জায়গা বাদ পড়ে, যদি না\দিয়ে আটকানো হয়। !দিয়ে শুরু হলে pattern উল্টে যায়: আগে বাদ পড়া file আবার ফেরে (negation). নামের শুরুতে সত্যিকারের!থাকলে\!.- শেষে
/থাকলে pattern শুধু folder মেলায়।logs/মেলেlogsনামের folder-এ, কিন্তুlogsনামের সাধারণ file-এ নয়। - শুরুতে বা মাঝে
/থাকলে pattern.gitignorefile-টি যে folder-এ আছে, সেখান থেকে গোনা হয় (anchored)./dist/শুধু root-এরdist;docs/*.pdfশুধু root-এরdocs-এর ভেতরে। কোনো/না থাকলে (বা শুধু শেষে থাকলে) pattern যেকোনো গভীরতায় মেলে:*.logমেলেdebug.logআরa/b/c.logদুটোতেই। *মানে/ছাড়া যেকোনো কিছু (শূন্য বা বেশি অক্ষর);?মানে/ছাড়া ঠিক একটি অক্ষর;[a-z]মানে বন্ধনীর পরিসরের একটি অক্ষর।**তিন রূপে বিশেষ: শুরুতে**/fooমানে যেকোনো folder-এর ভেতরেরfoo; শেষেabc/**মানেabc-এর ভেতরের সব কিছু, যত গভীরেই হোক; মাঝেa/**/bমানেa/b,a/x/b,a/x/y/b— শূন্য বা বেশি folder. অন্য জায়গায় পরপর দুটি তারা সাধারণ*-এর মতো আচরণ করে।
| Pattern | মেলে | মেলে না |
|---|---|---|
*.log | debug.log, logs/app.log | log.txt |
/config.local.json | root-এর config.local.json | src/config.local.json |
dist/ | dist/ folder, src/dist/ folder | dist নামের file |
temp?.txt | temp1.txt, tempA.txt | temp10.txt, temp.txt |
build[0-9].zip | build7.zip | buildA.zip |
docs/**/*.pdf | docs/manual.pdf, docs/guide/v1/manual.pdf | manual.pdf (root-এ) |
**/test-results/ | test-results/, web/test-results/ | test-results.xml |
এবার সবচেয়ে গুরুত্বপূর্ণ নিয়ম: একই file-এর ভেতরে লাইনগুলো ওপর থেকে নিচে পড়া হয়, আর যে শেষ লাইনটি path-এ মেলে, সিদ্ধান্ত তার। তার আগে একটি পরীক্ষা: path-এর কোনো parent folder কি বাদ পড়েছে? পড়লে Git সেই folder-এর ভেতরে ঢোকেই না। নিচের stepper-এ চারটি লাইনের একটি তালিকায় তিনটি path যাচাই করা হচ্ছে।
*.log · ২ !important.log · ৩ /tmp/ · ৪ debug-*.log| যাচাইয়ের path | |
| এখন দেখা হচ্ছে | |
| মিলল? | |
| এখনকার সিদ্ধান্ত |
- আগে parent folder. Path
src/important.log. Git আগে দেখেsrcfolder কি বাদ? চারটি লাইনের কোনোটিইsrc-এ মেলে না, তাই ভেতরে ঢোকা যায়। এবার file-টি নিজে। - লাইন ১ —
*.log. Pattern-এ কোনো/নেই, তাই যেকোনো গভীরতায় শুধু নামের শেষ অংশ মেলানো হয়।important.logমেলে — আপাতত সিদ্ধান্ত IGNORED. - লাইন ২ —
!important.log. এটিও মেলে, আর এটি negation. পরের লাইন আগের লাইনকে ছাপিয়ে যায়, তাই file আবার ফিরল: kept. - লাইন ৩ আর ৪।
/tmp/root-এ বাঁধা আর শুধু folder মেলায় — এটি file, নামও আলাদা।debug-*.log-এর জন্য নামdebug-দিয়ে শুরু হতে হবে। কোনোটিই মেলে না, তাই শেষ সিদ্ধান্ত লাইন ২-এর: track করা যাবে. লাইন ১ আর ২-এর ক্রম উল্টে দিলে ফলও উল্টে যেত — Git ২.৫৩-এ যাচাই করা। - দ্বিতীয় path —
tmp/important.log. এবার parent foldertmpলাইন ৩-এ মেলে, তাই পুরো folder বাদ। Git বাদ পড়া folder-এর ভেতরের তালিকাই পড়ে না, তাই লাইন ২-এর!important.logএই file পর্যন্ত পৌঁছাতে পারে না। ফল IGNORED. এটিই সেই parent folder-এর নিয়ম। - তৃতীয় path —
logs/debug-1.log.logsfolder কোনো লাইনে বাদ নয়। File-টিতে লাইন ১ (*.log) আর লাইন ৪ (debug-*.log) দুটোই মেলে; লাইন ২ মেলে না, কারণ নাম আলাদা। - শেষ মেলা লাইন জেতে। দুটি মিলের মধ্যে শেষটি লাইন ৪, তাই
git check-ignore -vকারণ হিসেবে দেখাবে.gitignore:4:debug-*.log— লাইন ১ নয়। একই ফল, কিন্তু কোন লাইন দায়ী তা জানা ভুল খোঁজার সময় কাজে লাগে।
৩ · Negation-এর ফাঁদ আর কোন তালিকা জেতে
Negation দেখতে সহজ, কিন্তু Git-এর documentation-এ একটি বাক্য সবচেয়ে বেশি ভুল বোঝা হয়: কোনো file-এর parent folder বাদ পড়লে সেই file আর ফেরানো যায় না। কারণ performance: বাদ পড়া folder-এর ভেতরে Git ঢোকেই না, তাই ভেতরের file-এর ওপর কোন ! লাইন আছে, সেটা দেখার সুযোগই আসে না।
কাজ করে না
secret/!secret/readme.md
secret/ পুরো folder বাদ দেয়; readme.md ভেতরে আটকা।
কাজ করে
private/*!private/readme.md
private/* folder নয়, ভেতরের প্রতিটি জিনিস আলাদা করে বাদ দেয়; folder খোলা থাকে।
আরও গভীরের একটি file ফেরাতে প্রতিটি স্তর খুলতে হয়। শুধু a/b/c.txt রাখতে চাইলে: a/*, !a/b/, a/b/*, !a/b/c.txt — চার লাইন। Git ২.৫৩-এ চালিয়ে দেখা গেছে, এতে a/b/c.txt থাকে, a/b/d.txt আর a/e.txt বাদ পড়ে।
একই নিয়ম নিজে বানিয়ে দেখুন
নিচের JavaScript-এ একটি ছোট matcher আছে: প্রতিটি লাইনকে regular expression-এ বদলে, আগে parent folder আর পরে file যাচাই করে, শেষ মেলা লাইনকে কারণ হিসেবে দেখায়। এটি একটি সরলীকৃত মডেল — একটিমাত্র root .gitignore ধরে নেয়, \-escape, ভেতরের folder-এর আলাদা .gitignore বা বড়-ছোট হাতের অক্ষরের নিয়ম (core.ignorecase) বোঝে না। তবু এই ১৬টি path-এ এর ফল আসল git check-ignore -v -n-এর সাথে হুবহু মিলেছে (Git ২.৫৩)।
// A SIMPLIFIED .gitignore matcher: one .gitignore at the root, no escapes.
// Blank and # lines skipped; "!" re-includes; trailing "/" = directories only;
// a "/" at the start or middle anchors to the root; *, ?, [..], ** as in gitignore.
const text = "# build output\ndist/\n*.log\n!important.log\n/config.local.json\n" +
"docs/**/*.pdf\ntemp?.txt\nbuild[0-9].zip\nsecret/\n!secret/readme.md\nprivate/*\n!private/readme.md";
const G = { "/**/": "/(?:.*/)?", "**/": "(?:.*/)?", "/**": "/.*", "**": ".*", "*": "[^/]*", "?": "[^/]" };
const rules = text.split("\n").map((line, i) => ({ line, no: i + 1 }))
.filter(r => r.line.trim() !== "" && !r.line.startsWith("#")).map(r => {
let p = r.line; const neg = p.startsWith("!"); if (neg) p = p.slice(1);
const dirOnly = p.endsWith("/"); if (dirOnly) p = p.slice(0, -1);
const anchored = p.includes("/"); if (p.startsWith("/")) p = p.slice(1);
const body = p.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\/\*\*\/|\*\*\/|\/\*\*|\*\*|\*|\?/g, t => G[t]);
return { ...r, neg, dirOnly, rx: new RegExp("^" + (anchored ? "" : "(?:.*/)?") + body + "$") };
});
const lastMatch = (path, isDir) => { // the LAST matching line decides
let hit = null;
for (const r of rules) if ((!r.dirOnly || isDir) && r.rx.test(path)) hit = r;
return hit;
};
function check(file) {
const parts = file.split("/"), f = file.padEnd(24);
for (let i = 1; i < parts.length; i++) { // parent directories first
const dir = parts.slice(0, i).join("/"), h = lastMatch(dir, true);
if (h && !h.neg) return `IGNORED ${f} parent ${dir}/ excluded by line ${h.no}: ${h.line}`;
}
const h = lastMatch(file, false);
if (!h) return `kept ${f} no pattern matches`;
return `${h.neg ? "kept " : "IGNORED"} ${f} line ${h.no}: ${h.line}`;
}
["dist/app.js", "src/dist/x.js", "debug.log", "logs/important.log", "config.local.json",
"src/config.local.json", "docs/manual.pdf", "docs/guide/v1/manual.pdf", "temp1.txt",
"temp10.txt", "build7.zip", "buildA.zip", "secret/readme.md", "private/readme.md",
"private/key.pem", "src/app.js"].forEach(p => console.log(check(p)));আউটপুট:
IGNORED dist/app.js parent dist/ excluded by line 2: dist/
IGNORED src/dist/x.js parent src/dist/ excluded by line 2: dist/
IGNORED debug.log line 3: *.log
kept logs/important.log line 4: !important.log
IGNORED config.local.json line 5: /config.local.json
kept src/config.local.json no pattern matches
IGNORED docs/manual.pdf line 6: docs/**/*.pdf
IGNORED docs/guide/v1/manual.pdf line 6: docs/**/*.pdf
IGNORED temp1.txt line 7: temp?.txt
kept temp10.txt no pattern matches
IGNORED build7.zip line 8: build[0-9].zip
kept buildA.zip no pattern matches
IGNORED secret/readme.md parent secret/ excluded by line 9: secret/
kept private/readme.md line 12: !private/readme.md
IGNORED private/key.pem line 11: private/*
kept src/app.js no pattern matches
কয়েকটি লক্ষ করার মতো লাইন। src/dist/x.js বাদ, কারণ dist/-এ শুধু শেষে /, তাই এটি যেকোনো গভীরতার dist folder মেলায়; শুধু root-এরটা চাইলে /dist/. src/config.local.json থাকল, কারণ /config.local.json root-এ বাঁধা। docs/manual.pdf বাদ, কারণ মাঝের /**/ শূন্য folder-ও মেলায়। temp10.txt থাকল, কারণ ? ঠিক একটি অক্ষর। আর মূল শিক্ষা দুটি লাইনে: secret/readme.md বাদ, কারণ parent secret/ লাইন ৯-এ বাদ পড়েছে, লাইন ১০-এর ! সেখানে পৌঁছায় না; কিন্তু private/readme.md থাকল, কারণ private/* folder-কে নয়, ভেতরের জিনিসগুলোকে বাদ দিয়েছে। Git নিজে এই দুটির জন্য দেখায় .gitignore:9:secret/ আর .gitignore:12:!private/readme.md — একই কথা।
কোন তালিকা জেতে — ignore-এর চারটি উৎস
Ignore-এর pattern শুধু একটি file থেকে আসে না। Git চারটি উৎস দেখে; ওপরের উৎস নিচেরটিকে ছাপিয়ে যায়, আর একই উৎসের ভেতরে শেষ মেলা লাইন জেতে:
- Command line-এ দেওয়া pattern — যেসব command এমন option নেয় (যেমন
git ls-files --exclude). .gitignorefile-গুলো — path-এর নিজের folder বা যেকোনো ওপরের folder-এর। কাছের (গভীরতর) folder-এর.gitignoreওপরের folder-এরটাকে ছাপিয়ে যায়। Root-এ*.csvআরdata/.gitignore-এ!keep.csvথাকলেdata/keep.csvথাকে, কিন্তুdata/other.csvআর root-এরkeep.csvবাদ — Git ২.৫৩-এ যাচাই করা।.git/info/exclude— শুধু এই clone-এর জন্য, commit হয় না। নিজের পরীক্ষার file লুকাতে ভালো।core.excludesFile— আপনার সব repository-র জন্য একটি ব্যক্তিগত তালিকা। Set না করলে Git দেখে~/.config/git/ignore(বা$XDG_CONFIG_HOME/git/ignore)..DS_Store,Thumbs.db, editor-এর swap file এখানেই রাখার জায়গা।
$ git config --global core.excludesFile ~/.gitignore_global
$ printf '.DS_Store\nThumbs.db\n*.swp\n' >> ~/.gitignore_globalকোনটি কোথায়? দলের সবার জন্য project-এর নিয়ম (dependency, build output, log, secret file) → commit করা .gitignore. শুধু আপনার এই clone → .git/info/exclude. আপনার কম্পিউটারের সব project (OS আর editor-এর file) → core.excludesFile. তবে বাস্তবে অনেক দল নতুনদের ভুল এড়াতে .DS_Store-এর মতো সাধারণ নাম project-এর .gitignore-এও রাখে — দুটো একসাথে থাকলে ক্ষতি নেই।
৪ · Git-কেই জিজ্ঞেস করুন — status --ignored আর check-ignore
অনুমান না করে Git-কে জিজ্ঞেস করাই নিরাপদ। এবার রহিমের project-টি নতুন করে, ঠিকভাবে বানাই। এই পর্বের সব transcript Git ২.৫৩-এ একটি আলাদা scratch folder-এ সত্যিই চালিয়ে নেওয়া। লেখক Rahim, email rahim@example.com, আর প্রতিটি commit-এর তারিখ নির্দিষ্ট (১৫ জানুয়ারি ২০২৬, সকাল ১০টা থেকে এক মিনিট করে বাড়ানো) — তাই একই file দিয়ে চালালে commit hash-ও একই আসার কথা। নিজের আসল config যেন না বদলায়, সেজন্য GIT_CONFIG_SYSTEM আর GIT_CONFIG_GLOBAL environment variable দিয়ে system আর global config দুটি scratch file-এ সরানো হয়েছে (Git ২.৩২ থেকে এই দুটি variable আছে)। পরীক্ষার সময় আপনিও এই কৌশল ব্যবহার করতে পারেন।
শুরুতে রহিম ভুল করে .env-সহ প্রথম commit করেছেন — অংশ ৫-এ সেটা ঠিক করব:
$ export GIT_CONFIG_SYSTEM=../etc/gitconfig GIT_CONFIG_GLOBAL=../home/.gitconfig
$ git -c init.defaultBranch=main init -q bazar
$ cd bazar
$ git config user.name "Rahim"
$ git config user.email "rahim@example.com"
$ git config core.autocrlf false
$ export GIT_AUTHOR_DATE="2026-01-15T10:00:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:00:00+06:00"
$ git add package.json src/index.js .env
$ git commit -q -m "Add app skeleton"এরপর project-এ এলো node_modules/express/index.js, dist/bundle.js, logs/app.log, debug.log, .vscode/settings.json, .DS_Store, খালি মানের .env.example — আর এই .gitignore:
# Dependencies
node_modules/
# Build output (only the top-level dist)
/dist/
# Logs
*.log
logs/
# Secrets: every .env file, except the example
.env*
!.env.example
# Editor and OS files
.vscode/
.DS_Store$ git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.example
.gitignore
nothing added to commit but untracked files present (use "git add" to track)
$ git status --ignored
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.example
.gitignore
Ignored files:
(use "git add -f <file>..." to include in what will be committed)
.DS_Store
.vscode/
debug.log
dist/
logs/
node_modules/
nothing added to commit but untracked files present (use "git add" to track)সাধারণ git status-এ মাত্র দুটি untracked file — বাকি সব ignored, তাই চুপ। --ignored দিলে লুকানোগুলোও আলাদা তালিকায় আসে। খেয়াল করুন, Git পুরো folder দেখায় (node_modules/), ভেতরের প্রতিটি file নয় — বাদ পড়া folder-এ সে ঢোকে না। আর .env কোথাও নেই! কারণ এটি আগেই commit হয়েছে, এটি tracked; বদলায়নি বলে status-এ আসেনি। এবার প্রতিটি file-এর কারণ জানতে check-ignore:
$ git check-ignore -v debug.log dist/bundle.js node_modules/express/index.js .env.example src/index.js
.gitignore:8:*.log debug.log
.gitignore:5:/dist/ dist/bundle.js
.gitignore:2:node_modules/ node_modules/express/index.js
.gitignore:13:!.env.example .env.example
$ git check-ignore -v -n debug.log .env.example src/index.js
.gitignore:8:*.log debug.log
.gitignore:13:!.env.example .env.example
:: src/index.js
$ git check-ignore -v .env; echo $?
1
$ git check-ignore -v --no-index .env
.gitignore:12:.env* .envপ্রতিটি লাইনের গঠন: উৎস-file:লাইন-নম্বর:pattern, তারপর tab, তারপর path. চারটি সূক্ষ্ম বিষয়:
.env.example-এর পাশে!.env.exampleদেখাচ্ছে — অর্থাৎ-vnegation লাইনকেও দেখায়, যদিও file-টি ignored নয়। সিদ্ধান্ত পড়ার সময় শুরুর!খেয়াল করুন।-n(--non-matching) দিলে যে path-এ কোনো pattern মেলেনি, সেটিও::চিহ্নসহ আসে; না দিলে সেগুলো চুপচাপ বাদ পড়ে।.env-এর জন্য কিছুই এলো না, exit code1(“কিছু মেলেনি”)। কারণcheck-ignoreসাধারণভাবে index-এ থাকা tracked file-কে বিবেচনাই করে না — tracked file ignore হয় না।--no-indexদিলে index না দেখে শুধু pattern মেলানো হয়: হ্যাঁ, লাইন ১২-এর.env*মেলে। অর্থাৎ নিয়ম ঠিক আছে, সমস্যা হলো file-টি আগে থেকেই tracked.
শূন্য থেকে লিখবেন না — template
প্রায় প্রতিটি ভাষা আর framework-এর জন্য তৈরি .gitignore পাওয়া যায়। সবচেয়ে পরিচিত সংগ্রহ GitHub-এর খোলা repository github/gitignore (Node, Python, Java, Unity…); GitHub-এ নতুন repository বানানোর সময় এখান থেকেই একটি template বেছে নেওয়ার সুযোগ থাকে (লেখার সময়ে; platform-ভেদে আলাদা)। Template শুরু হিসেবে চমৎকার, কিন্তু অন্ধভাবে নয় — প্রতিটি লাইন পড়ুন, নিজের project-এ না লাগলে মুছুন, আর git status --ignored দিয়ে দেখে নিন জরুরি কোনো file বাদ পড়ছে কি না।
৫ · আগেই commit হয়ে গেছে? git rm --cached
অংশ ৪-এ দেখলেন, tracked file-এর ওপর .gitignore-এর কোনো জোর নেই। Git-এর চোখে একটি file হয় tracked (index-এ আছে), নয়তো untracked; ignore-এর প্রশ্ন ওঠে শুধু untracked-এর বেলায়। তাই প্রথমে file-টিকে index থেকে সরাতে হবে, কিন্তু আপনার disk থেকে নয়। এই কাজটিই করে git rm --cached: “index থেকে মুছুন, working tree-তে রেখে দিন।”
$ git rm --cached .env
rm '.env'
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: .env
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.example
.gitignore
$ ls .env
.env
$ git add .gitignore .env.example
$ git commit -m "Stop tracking .env; add .gitignore"
[main c0873ce] Stop tracking .env; add .gitignore
3 files changed, 18 insertions(+), 1 deletion(-)
delete mode 100644 .env
create mode 100644 .env.example
create mode 100644 .gitignore
$ git ls-files
.env.example
.gitignore
package.json
src/index.js
$ git log --oneline -- .env
c0873ce Stop tracking .env; add .gitignore
0f0db3c Add app skeleton
$ git check-ignore -v .env; echo $?
.gitignore:12:.env* .env
0git status বলছে deleted: .env — commit-এর দিক থেকে file-টি মুছে যাচ্ছে, অথচ ls .env দেখাচ্ছে file-টি disk-এ অক্ষত। Commit-এর পর git ls-files (index-এর তালিকা) থেকে .env উধাও, আর এবার check-ignore সাধারণভাবেই লাইন ১২ দেখায়, exit code 0. কিন্তু শেষের আগের command-টিই আসল সতর্কবার্তা: git log -- .env এখনো দুটি commit দেখায়, আর 0f0db3c-তে password-সহ file-টি চিরকাল থেকে গেছে। git show 0f0db3c:.env লিখলেই যে কেউ পড়তে পারবেন। তাই এই password এখনই বদলাতে হবে; ইতিহাস থেকে মোছার পদ্ধতি পর্ব ৫৭-এ।
পুরো folder ভুল করে commit হলে -r লাগে। নিচের stepper-এ একটি C project-এর build/ folder-এর ঘটনা (আলাদা scratch repository, Git ২.৫৩, সময় সকাল ১১টা):
| Working tree | |
| Index | |
| শেষ commit | |
| git status --short |
- ভুলটা।
git add .দিয়েsrc/main.c-এর সাথে compile করাbuild/app.oআরbuild/app.exe-ও commit হয়ে গেছে:f6f9e41 Add main and build output. তিন এলাকাতেই file দুটি আছে। - শুধু
.gitignore-এbuild/লিখলে কী হয়? আবার compile করতেই status দেখালM build/app.o— Git এখনো বদল গুনছে, কারণ file-টি tracked. Ignore কাজ করছে না, আর এটিই প্রত্যাশিত আচরণ। git rm -r --cached build. Git লিখলrm 'build/app.exe'আরrm 'build/app.o'. Index থেকে দুটোই উঠে গেল (status-এDপ্রথম কলামে, অর্থাৎ staged মুছে ফেলা), কিন্তু working tree-তে file দুটি অক্ষত।--cachedভুলে গেলে disk থেকেও মুছে যেত।.gitignoreযোগ করে commit.git add .gitignore, তারপরgit commit -m "Stop tracking build/"→[main 7b3b3a1], “delete mode 100644 build/app.exe” আর “build/app.o”. এখনbuild/untracked আর ignored, তাই status পরিষ্কার। পরের build যত বার খুশি হোক, Git চুপ থাকবে।- সহকর্মীর কী হবে? যিনি নতুন commit-এ যাবেন (যেমন pull করে), তাঁর working tree থেকে Git
build/মুছে দেবে — কারণ commit বলছে file দুটি আর নেই। আমাদের scratch clone-এ ঠিক তা-ই হয়েছে। Build output বলে ক্ষতি নেই, আবার build করলেই হবে; কিন্তু দলকে আগে জানিয়ে দেওয়া ভালো। কোনো দরকারি config file এভাবে সরালে সবার আগে নিজের কপি রাখতে বলুন। - ইতিহাস মনে রাখে।
f6f9e41-এbuild/এখনো আছে। Build output-এর জন্য এটা শুধু জায়গার অপচয়; কিন্তু folder-এ secret থাকলে secret বদলানো আর পর্ব ৫৭-এর ইতিহাস-পরিষ্কার দুটোই লাগবে। শেষ commit-টি এখনো push না হয়ে থাকলেgit commit --amendদিয়ে সেটিকেই শুধরে নেওয়ার উপায় আছে — পর্ব ১১-এ (ভুল শোধরানো)।
নতুন .gitignore একসাথে পুরো repository-তে প্রয়োগ করার একটি পরিচিত রেসিপি: git rm -r --cached ., তারপর git add . আর commit. এটি সব file index থেকে তুলে আবার যোগ করে, ফলে নতুন নিয়মে যা ignored, সেটি বাদ পড়ে। কিন্তু সাবধান: এতে এমন untracked file-ও ঢুকে যেতে পারে যা আপনি commit করতে চাননি। Commit-এর আগে অবশ্যই git status আর git diff --cached --stat পড়ে নিন (পর্ব ৯)।
৬ · .gitattributes — Git-কে file-এর স্বভাব জানানো
.gitignore বলে কোন file Git দেখবে না; .gitattributes বলে যেগুলো দেখবে, সেগুলোর সাথে কেমন আচরণ করবে। এটিও commit করা হয়, তাই দলের সবার কম্পিউটারে একই নিয়ম চলে — ব্যক্তিগত config-এর ওপর ভরসা করতে হয় না। প্রতিটি লাইনে একটি pattern, তারপর এক বা একাধিক attribute:
# Normalize every text file to LF in the repository and on checkout
* text=auto eol=lf
# Windows batch files must keep CRLF
*.bat text eol=crlf
# Never touch these bytes
*.png binary
*.jpg binary
# Better hunk headers in diffs
*.py diff=python
# Leave these out of "git archive" release tarballs
/tests/ export-ignore
.gitattributes export-ignore
# GitHub-only hints (Linguist): hide generated code in stats and diffs
dist/** linguist-generated- চার অবস্থা: attribute set (
text), unset (-text), একটি মান (eol=lf), বা unspecified (কিছু বলা নেই — Git তখন config দেখে)।!textলিখলে আগের কোনো লাইনের দেওয়া মান মুছে unspecified করে। - Pattern-এর নিয়ম প্রায়
.gitignore-এর মতো, তবে দুটি পার্থক্য:!দিয়ে negative pattern নিষিদ্ধ (Git ২.৫৩ লিখলে বলেwarning: Negative patterns are ignored in git attributes), আর folder মেলানো pattern ভেতরের file-এ নিজে থেকে ছড়ায় না — তাইbuild/নয়, লিখতে হয়build/**. একাধিক লাইন একই file-এ মিললে, প্রতিটি attribute-এর জন্য শেষ লাইনটি জেতে। text,text=auto,eol: line ending-এর নিয়ম, পরের অংশে বিস্তারিত।text=autoমানে Git নিজে ঠিক করবে file-টি text কি না।binary: একটি macro, যার মানে-diff -merge -text— লাইনভিত্তিক diff নয়, লাইনভিত্তিক merge নয়, line ending রূপান্তর নয়। নিচের transcript-এgit check-attrঠিক এটাই দেখাবে।- Diff driver:
diff=python-এর মতো built-in নাম (python, java, cpp, html, markdown, rust ইত্যাদি) diff-এর প্রতিটি অংশের শিরোনামে ঠিক function বা class-এর নাম দেখায়। নিজের driver-ও বানানো যায়:*.docx diff=wordলিখে config-এdiff.word.textconv-এ এমন একটি program দিলে, যা file থেকে text বের করে, Word file-এরও পড়ার মতো diff মেলে। export-ignore:git archiveদিয়ে release-এর zip বা tarball বানানোর সময় এই path বাদ যায় (release নিয়ে পর্ব ১৮)।linguist-*: এগুলো Git-এর নয়, GitHub-এর Linguist library-র নিজস্ব নিয়ম — repository-র ভাষার পরিসংখ্যান আর pull request-এ generated file-এর diff লুকানো নিয়ন্ত্রণ করে (লেখার সময়ে; নাম আর আচরণ platform-ভেদে আলাদা)। Git নিজে অচেনা attribute উপেক্ষা করে।
বড় binary file (ভিডিও, design file) সামলাতে Git LFS-ও .gitattributes-এ filter=lfs diff=lfs merge=lfs -text লাইন লিখে কাজ করে; এর বিস্তারিত পর্ব ১৯-এ।
৭ · Line ending — CRLF বনাম LF
প্রতিটি text file-এ লাইনের শেষে একটি অদৃশ্য line ending থাকে। Windows-এর প্রচলিত রীতি দুটি byte: CRLF (\r\n, 0x0D 0x0A); Linux আর macOS-এর একটি: LF (\n, 0x0A). নাম দুটি এসেছে টাইপরাইটার আর teletype যুগ থেকে — “carriage return” মানে লেখার মাথা বাঁ দিকে ফেরানো, “line feed” মানে কাগজ এক লাইন ওপরে তোলা। পুরোনো Mac OS (OS X-এর আগে) আবার শুধু CR ব্যবহার করত।
একই চিঠি দুজন লিখছেন — একজন প্রতিটি লাইনের শেষে দাঁড়ি আর একটি ফাঁকা ঘর দেন, আরেকজন শুধু দাঁড়ি। পড়তে দুটো একই, কিন্তু অক্ষর গুনলে আলাদা। Git অক্ষর গোনে। তাই Windows-এ একজন file খুলে save করলেই সব লাইনের শেষ বদলে যেতে পারে, আর diff দেখায় “পুরো file বদলেছে”, যদিও আসলে একটি শব্দও বদলায়নি। তার ওপর CRLF-ওয়ালা shell script Linux-এ চালালে /bin/sh^M: bad interpreter-এর মতো অদ্ভুত error আসতে পারে — ^M হলো সেই বাড়তি \r.
Git-এর সমাধান: repository-র ভেতরে text file সবসময় LF রাখা (normalization), আর দরকার হলে শুধু working tree-তে OS-এর রীতিতে বদলানো। রূপান্তর হয় দুই জায়গায়: git add-এর সময় (working tree → index) আর checkout-এর সময় (index → working tree). কখন কী হবে, তা ঠিক করে core.autocrlf আর .gitattributes:
| Setting | add-এর সময় | checkout-এর সময় | সাধারণত কার জন্য |
|---|---|---|---|
core.autocrlf=true | CRLF → LF | LF → CRLF | Windows (Git for Windows-এর installer সাধারণত এটিই প্রস্তাব করে) |
core.autocrlf=input | CRLF → LF | কিছু নয় | Linux, macOS |
core.autocrlf=false | কিছু নয় | কিছু নয় | কিছু set না থাকলে এটিই ডিফল্ট (Linux-macOS-এ সাধারণত তাই); Windows-এ শুধু যদি সব editor LF-এ চলে আর .gitattributes থাকে |
সমস্যা হলো, core.autocrlf প্রত্যেকের নিজের config — দলের দশজনের একজন ভুল setting রাখলেই repository-তে CRLF ঢুকে পড়ে। এজন্য স্থায়ী সমাধান .gitattributes, যা repository-র সাথেই চলে। কোনো path-এ text attribute বলা থাকলে কোন file normalize হবে, সেই সিদ্ধান্ত আর core.autocrlf-এর হাতে থাকে না: * text=auto থাকলে Git যে file-কে text বলে চেনে, সেটি repository-তে LF হয়ে ঢোকে, যার কম্পিউটারেই হোক। Checkout-এর line ending আলাদা করে বেঁধে দিতে eol=lf বা eol=crlf. Text কি না বুঝতে Git দেখে file-এ NUL byte (0x00), একা CR বা অস্বাভাবিক বেশি non-printable byte আছে কি না — থাকলে binary, তখন একটি byte-ও বদলায় না।
নিচের মডেলে চারটি file (Windows editor-এর CRLF, Linux editor-এর LF, দুটো মেশানো, আর একটি PNG-এর আসল শুরুর byte) চারটি setting-এ add আর checkout করা হচ্ছে। একই চারটি file দিয়ে Git ২.৫৩-এ git cat-file আর checkout চালিয়ে byte মেলানো হয়েছে — ফল হুবহু এক।
// Line-ending model: "add" = working tree -> index, "checkout" = index -> working tree.
// Text detection like Git's auto mode: a NUL byte or a lone CR means "binary".
const FILES = [
["notes.txt", "a\r\nb\r\n"], // saved by a Windows editor (CRLF)
["app.js", "a\nb\n"], // saved by a Linux/macOS editor (LF)
["mixed.txt", "a\r\nb\n"], // both kinds in one file
["logo.png", "\x89PNG\r\n\x1a\n\0\0"], // real PNG header bytes: contains CRLF and NUL
];
const isBinary = s => s.includes("\0") || /\r(?!\n)/.test(s);
const toLF = s => s.replace(/\r\n/g, "\n");
const toCRLF = s => s.replace(/\r?\n/g, "\r\n");
const show = s => [...s].map(c => c === "\r" ? "\\r" : c === "\n" ? "\\n" :
c === "\0" ? "\\0" : c < " " || c > "~" ? "\\x" + c.charCodeAt(0).toString(16) : c).join("");
const MODES = { // [what add does, what checkout does] for TEXT files
"autocrlf=true": [toLF, toCRLF],
"autocrlf=input": [toLF, s => s],
"autocrlf=false": [s => s, s => s],
"attr text=auto eol=lf": [toLF, s => s], // .gitattributes: same on every OS
};
for (const [mode, [onAdd, onCheckout]] of Object.entries(MODES)) {
console.log(`== ${mode}`);
for (const [name, work] of FILES) {
const bin = isBinary(work);
const index = bin ? work : onAdd(work);
const back = bin ? index : onCheckout(index);
console.log(` ${name.padEnd(9)} ${bin ? "binary" : "text "} index: ${show(index).padEnd(22)} checkout: ${show(back)}`);
}
}আউটপুট:
== autocrlf=true
notes.txt text index: a\nb\n checkout: a\r\nb\r\n
app.js text index: a\nb\n checkout: a\r\nb\r\n
mixed.txt text index: a\nb\n checkout: a\r\nb\r\n
logo.png binary index: \x89PNG\r\n\x1a\n\0\0 checkout: \x89PNG\r\n\x1a\n\0\0
== autocrlf=input
notes.txt text index: a\nb\n checkout: a\nb\n
app.js text index: a\nb\n checkout: a\nb\n
mixed.txt text index: a\nb\n checkout: a\nb\n
logo.png binary index: \x89PNG\r\n\x1a\n\0\0 checkout: \x89PNG\r\n\x1a\n\0\0
== autocrlf=false
notes.txt text index: a\r\nb\r\n checkout: a\r\nb\r\n
app.js text index: a\nb\n checkout: a\nb\n
mixed.txt text index: a\r\nb\n checkout: a\r\nb\n
logo.png binary index: \x89PNG\r\n\x1a\n\0\0 checkout: \x89PNG\r\n\x1a\n\0\0
== attr text=auto eol=lf
notes.txt text index: a\nb\n checkout: a\nb\n
app.js text index: a\nb\n checkout: a\nb\n
mixed.txt text index: a\nb\n checkout: a\nb\n
logo.png binary index: \x89PNG\r\n\x1a\n\0\0 checkout: \x89PNG\r\n\x1a\n\0\0
true আর input দুটোতেই index-এ সব text file a\nb\n — repository পরিষ্কার; পার্থক্য শুধু checkout-এ (true-তে Windows-এর CRLF ফেরে)। false-এ Windows editor-এর CRLF আর মেশানো file দুটোই হুবহু repository-তে ঢুকেছে — Linux-এ থাকা সহকর্মী এগুলো দেখবেন ^M-সহ, আর পরের কেউ normalize করলে পুরো file-এর diff আসবে। PNG চারটি setting-এই অক্ষত: এর ভেতরে \r\n থাকলেও NUL byte থাকায় এটি binary. এই একটি বিষয়ই আসল বিপদ দেখায় — Git যদি ভুল করে ছবিকে text ভাবত, তাহলে \r\n বদলে ছবিটি নষ্ট হয়ে যেত; এজন্যই *.png binary লিখে রাখা নিরাপদ। মডেলটি সরলীকৃত: আসল Git-এ আরও কিছু নিয়ম আছে, যেমন text=auto-তে index-এ আগে থেকেই CRLF থাকা file-কে Git নিজে থেকে normalize করে না।
এবার রহিমের repository-তে আসল .gitattributes যোগ করি। notes.txt CRLF-এ লেখা, deploy.sh আর build.bat LF-এ, logo.png-এ PNG-এর byte:
$ cat .gitattributes
* text=auto eol=lf
*.bat text eol=crlf
*.png binary
$ git add .gitattributes notes.txt deploy.sh build.bat logo.png
warning: in the working copy of 'build.bat', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'notes.txt', CRLF will be replaced by LF the next time Git touches it
$ git ls-files --eol notes.txt deploy.sh build.bat logo.png
i/lf w/lf attr/text eol=crlf build.bat
i/lf w/lf attr/text=auto eol=lf deploy.sh
i/-text w/-text attr/-text logo.png
i/lf w/crlf attr/text=auto eol=lf notes.txt
$ git check-attr -a deploy.sh logo.png
deploy.sh: text: auto
deploy.sh: eol: lf
logo.png: binary: set
logo.png: diff: unset
logo.png: merge: unset
logo.png: text: unset
logo.png: eol: lfgit ls-files --eol প্রতিটি file-এর তিনটি তথ্য দেয়: i/ index-এ কোন line ending, w/ working tree-তে কোনটি, আর attr/ কোন attribute প্রযোজ্য। notes.txt: index-এ lf (normalize হয়ে ঢুকেছে), working tree-তে এখনো crlf — warning ঠিক এটাই বলছে: “পরের বার Git file-টি checkout করলে CRLF বদলে LF হবে।” build.bat-এর উল্টো: repository-তে LF, কিন্তু নিয়ম eol=crlf, তাই checkout-এ CRLF হবে। logo.png সবখানে -text. check-attr দেখাচ্ছে binary আসলে তিনটি unset-এর সমষ্টি; eol: lf-ও এসেছে, কারণ * লাইনটি সব file-এ মেলে, কিন্তু text unset বলে এর কোনো প্রভাব নেই। এই warning দুটি Git-এর নতুন version-এর ভাষা; পুরোনো version-এ একই সতর্কবার্তা অন্য শব্দে আসত। পুরোনো repository-তে প্রথমবার .gitattributes যোগ করলে, আগে থেকে track হওয়া file-গুলো নতুন নিয়মে আনতে git add --renormalize . চালিয়ে commit করুন (Git ২.১৬ থেকে)।
দলের জন্য: repository-তে .gitattributes-এ অন্তত * text=auto, shell script-এর জন্য eol=lf, .bat/.cmd-এর জন্য eol=crlf, আর ছবি-zip-PDF-এর জন্য binary. ব্যক্তিগতভাবে: Windows-এ core.autocrlf=true (বা false, যদি editor LF-এ চলে), Linux-macOS-এ input বা কিছুই না। .gitattributes থাকলে ব্যক্তিগত setting-এর ভুল আর repository-তে ঢুকতে পারে না।
৮ · git config-এর স্তর — কোন মান কোথা থেকে
পর্ব ৭-এ প্রথম setup-এর সময় git config --global user.name লিখেছিলেন। এই --global আসলে একটি স্তরের নাম। git config একই key একাধিক file-এ খোঁজে; প্রতিটি file একটি scope:
Git config-এর স্তর — নিচ থেকে ওপরে পড়া হয়
Read order: system → global → local → worktree → -c; the last value read wins
-c. যে মান শেষে পড়া হয়, সেটিই কার্যকর।- system — কম্পিউটারের সব ব্যবহারকারীর জন্য। Linux-এ সাধারণত
/etc/gitconfig; Git for Windows-এ Git-এর install folder-এর ভেতরেরetc/gitconfig. লিখতে প্রায়ই admin অধিকার লাগে। - global — আপনার নিজের, সব repository-র জন্য:
~/.gitconfig(বা~/.config/git/config; দুটো থাকলে দুটোই পড়া হয়,~/.gitconfigপরে)। নাম-email, editor, alias সাধারণত এখানে। - local — শুধু এই repository:
.git/config. Repository-র ভেতরেgit config key valueলিখলে, কোনো flag ছাড়া, এখানেই লেখা হয়। এটি commit হয় না। - worktree — একই repository-র একাধিক working tree থাকলে (
git worktree, পর্ব ৫৬) একটির জন্য আলাদা মান:.git/config.worktree, তবে আগেextensions.worktreeConfigচালু করতে হয়। git -c key=value command— শুধু সেই একবারের জন্য, কোনো file বদলায় না। এই পর্বের প্রথম transcript-এরgit -c init.defaultBranch=main initঠিক এটাই।
নিয়মটা একটাই: একটি মানের key-তে শেষে পড়া মান জেতে। কিছু key-তে একাধিক মান জমা হয় (যেমন remote.origin.fetch) — সেগুলো --get-all দিয়ে সব দেখা যায়। রহিমের repository-তে system file-এ core.autocrlf=true রাখা আছে (Windows installer যেমন রাখে), global-এ ব্যক্তিগত email, আর local-এ কাজের email ও core.autocrlf=false:
$ git config --list --show-origin
file:../etc/gitconfig core.autocrlf=true
file:../etc/gitconfig color.ui=auto
file:../home/.gitconfig user.name=Rahim
file:../home/.gitconfig user.email=rahim.home@example.com
file:../home/.gitconfig init.defaultbranch=main
file:../home/.gitconfig pull.rebase=true
file:.git/config core.repositoryformatversion=0
file:.git/config core.filemode=false
file:.git/config core.bare=false
file:.git/config core.logallrefupdates=true
file:.git/config core.symlinks=false
file:.git/config core.ignorecase=true
file:.git/config core.autocrlf=false
file:.git/config user.name=Rahim
file:.git/config user.email=rahim@example.com
$ git config --show-origin --get user.email
file:.git/config rahim@example.com
$ git config --show-scope --show-origin --get-all core.autocrlf
system file:../etc/gitconfig true
local file:.git/config false
$ git config get --show-origin core.autocrlf
file:.git/config false--list ঠিক পড়ার ক্রমে সব লাইন দেখায় — ওপরে system, তারপর global, শেষে local. file:../etc/gitconfig-এর মতো অদ্ভুত path এসেছে কারণ আমরা variable দিয়ে scratch file দেখিয়েছি; সাধারণ কম্পিউটারে দেখবেন file:/home/rahim/.gitconfig বা file:C:/Users/Rahim/.gitconfig-এর মতো কিছু। Key-এর নাম ছোট হাতের অক্ষরে আসে (init.defaultbranch), কারণ section আর key-এর নাম case-insensitive. core.symlinks আর core.ignorecase Windows-এ git init নিজে লেখে; Linux-এ সাধারণত দেখবেন না। user.email দুই জায়গায় আছে, --get দেখায় শেষটি — local. --show-scope (Git ২.২৬ থেকে) স্তরের নামও বলে। শেষ লাইনটি Git ২.৪৬-এ আসা নতুন রূপ git config get; পুরোনো --get রূপ এখনো চলে।
এবার একটি key ধাপে ধাপে মিলিয়ে দেখি — অফিসের project ~/work/shop-এ user.email কোথা থেকে আসে। Global config-এর শেষে একটি conditional include আছে, যার কথা অংশ ৯-এ:
| এখন পড়া হচ্ছে | |
| পাওয়া লাইন | |
| user.email এখন | |
| --show-origin |
- পড়ার তালিকা। Git ঠিক করে কোন file-গুলো পড়বে, কোন ক্রমে: system, global, local (worktree চালু নেই)। প্রতিটি file ওপর থেকে নিচে পড়া হবে; প্রতিটি key-এর জন্য শেষ পাওয়া মান মনে রাখা হবে। এখনো
user.email-এর কোনো মান নেই। - System. এখানে
core.autocrlfআরcolor.uiআছে,user.emailনেই। খোঁজা key-এর মান বদলাল না। - Global — প্রথম মান।
~/.gitconfig-এuser.email=rahim.home@example.com. এই মুহূর্তে উত্তর এটাই, উৎস~/.gitconfig. ব্যক্তিগত project-এ (যেমন~/personal/blog) উত্তর এখানেই থেমে যায়। - Conditional include. Global file-এর শেষে লেখা
[includeIf "gitdir:~/work/"]. এই repository-র.gitfolder~/work/-এর ভেতরে, তাই শর্ত মিলল, আর ঠিক এই জায়গায়~/.gitconfig-workপড়া হলো। সেখানেuser.email=rahim@work.example.com— আগের মান ঢাকা পড়ল। include-টি যদি file-এর শুরুতে,[user]-এর আগে থাকত, তাহলে পরের ব্যক্তিগত email-ই জিতত — অবস্থান গুরুত্বপূর্ণ। - Local.
.git/config-এ শুধুcore.autocrlf=false;user.emailনেই, তাই কাজের email-ই থাকল। এখানে থাকলে local-ই জিতত। - ফল। কোনো
-cদেওয়া হয়নি, তাই শেষ উত্তরrahim@work.example.com, আরgit config --show-origin --get user.emailদেখাবেfile:~/.gitconfig-work-এর পূর্ণ path. Git ২.৫৩-এ একই সাজানো file দিয়ে চালিয়ে এই ফলই পাওয়া গেছে।
একই নিয়ম একটি ছোট model-এ: প্রতিটি file ক্রমে পড়া, include এলে ঠিক সেখানে অন্য file ঢোকানো, আর শেষ মান জেতা। দুটি repository-র জন্য --list --show-origin আর কয়েকটি key-এর ফল:
// A model of how Git reads config: system file, then global, then local.
// Each file is read top to bottom and a LATER value of the same key wins.
// [includeIf "gitdir:~/work/"] splices another file in at exactly that spot.
const FILES = {
"/etc/gitconfig": [["core.autocrlf", "true"], ["color.ui", "auto"]],
"~/.gitconfig": [["user.name", "Rahim"], ["user.email", "rahim.home@example.com"],
["pull.rebase", "true"], ["includeif.gitdir:~/work/.path", "~/.gitconfig-work"]],
"~/.gitconfig-work": [["user.email", "rahim@work.example.com"], ["pull.rebase", "false"]],
"~/work/shop/.git/config": [["core.autocrlf", "false"]],
"~/personal/blog/.git/config": [["color.ui", "always"]],
};
function read(file, gitdir, out) {
for (const [key, val] of FILES[file] || []) {
out.push({ key, val, origin: file }); // --list shows the include line too
const m = key.match(/^includeif\.gitdir:(.*)\.path$/);
if (m && gitdir.startsWith(m[1])) read(val, gitdir, out); // pattern ending in "/" = prefix
}
return out;
}
function load(repo) {
const out = [], gitdir = repo + "/.git/";
for (const f of ["/etc/gitconfig", "~/.gitconfig", repo + "/.git/config"]) read(f, gitdir, out);
return out;
}
for (const repo of ["~/work/shop", "~/personal/blog"]) {
const all = load(repo);
console.log(`== in ${repo}: git config --list --show-origin`);
all.forEach(e => console.log(`file:${e.origin}\t${e.key}=${e.val}`));
console.log(`-- git config --show-origin --get <key>, last value wins`);
for (const k of ["user.email", "pull.rebase", "core.autocrlf", "color.ui"]) {
const hits = all.filter(e => e.key === k), win = hits[hits.length - 1];
console.log(`${k.padEnd(14)} file:${win.origin}\t${win.val} (${hits.length} value(s) seen)`);
}
}আউটপুট:
== in ~/work/shop: git config --list --show-origin
file:/etc/gitconfig core.autocrlf=true
file:/etc/gitconfig color.ui=auto
file:~/.gitconfig user.name=Rahim
file:~/.gitconfig user.email=rahim.home@example.com
file:~/.gitconfig pull.rebase=true
file:~/.gitconfig includeif.gitdir:~/work/.path=~/.gitconfig-work
file:~/.gitconfig-work user.email=rahim@work.example.com
file:~/.gitconfig-work pull.rebase=false
file:~/work/shop/.git/config core.autocrlf=false
-- git config --show-origin --get <key>, last value wins
user.email file:~/.gitconfig-work rahim@work.example.com (2 value(s) seen)
pull.rebase file:~/.gitconfig-work false (2 value(s) seen)
core.autocrlf file:~/work/shop/.git/config false (2 value(s) seen)
color.ui file:/etc/gitconfig auto (1 value(s) seen)
== in ~/personal/blog: git config --list --show-origin
file:/etc/gitconfig core.autocrlf=true
file:/etc/gitconfig color.ui=auto
file:~/.gitconfig user.name=Rahim
file:~/.gitconfig user.email=rahim.home@example.com
file:~/.gitconfig pull.rebase=true
file:~/.gitconfig includeif.gitdir:~/work/.path=~/.gitconfig-work
file:~/personal/blog/.git/config color.ui=always
-- git config --show-origin --get <key>, last value wins
user.email file:~/.gitconfig rahim.home@example.com (1 value(s) seen)
pull.rebase file:~/.gitconfig true (1 value(s) seen)
core.autocrlf file:/etc/gitconfig true (1 value(s) seen)
color.ui file:~/personal/blog/.git/config always (2 value(s) seen)
~/work/shop-এ include-এর শর্ত মিলেছে, তাই .gitconfig-work-এর দুটি লাইন ঠিক include লাইনের পরে বসেছে, আর user.email ও pull.rebase দুটোই অফিসের মান নিয়েছে। ~/personal/blog-এ শর্ত মেলেনি — include লাইনটি তালিকায় আছে, কিন্তু ভেতরের file পড়া হয়নি; সেখানে ব্যক্তিগত email আর pull.rebase=true. আসল Git-এর --list-ও include লাইনটি এভাবেই দেখায় (includeif.gitdir:~/work/.path=…), আর include থেকে আসা মানের উৎস হিসেবে দেখায় included file-এর path — Git ২.৫৩-এ যাচাই করা। মডেলটি সরলীকৃত: আসল Git ~-কে পূর্ণ path-এ বদলায়, gitdir:-এ glob বোঝে, আর অনেক রকম শর্ত চেনে।
৯ · কাজের setting, alias আর অফিস-ব্যক্তিগত আলাদা রাখা
নাম আর email-এর পরে কয়েকটি setting প্রায় সব পেশাদারের global config-এ দেখা যায়। প্রতিটির পূর্ণ অর্থ পরের পর্বগুলোতে আসবে; এখানে শুধু কী করে আর কেন:
| Setting | কী করে | কোথায় বিস্তারিত |
|---|---|---|
init.defaultBranch main | নতুন git init-এ প্রথম branch-এর নাম (Git ২.২৮ থেকে)। না দিলে নাম হয় master, আর Git একটি পরামর্শ-বার্তা দেখায়; Git ২.৫৩-এর বার্তা অনুযায়ী Git ৩.০-তে ডিফল্ট main হবে। | পর্ব ১২ |
pull.rebase true বা false | git pull দূরের নতুন commit আনার পর merge করবে, নাকি আপনার commit-গুলো তার ওপর rebase করবে। নতুন Git-এ দুই দিকেই নতুন commit থাকলে আর এটি set না থাকলে pull থেমে যায় আর hint: You have divergent branches… বার্তায় পছন্দ জানতে চায় (Git ২.৫৩-এ দেখা)। | পর্ব ১৪ আর ১৫ |
core.editor "code --wait" | Commit message আর interactive কাজে কোন editor খুলবে। --wait না দিলে VS Code খুলেই ফিরে আসে, আর Git খালি message পায়। | পর্ব ৮ |
color.ui auto | Terminal-এ রঙিন আউটপুট, কিন্তু file বা pipe-এ লেখার সময় রং ছাড়া। নতুন Git-এ এটিই ডিফল্ট। | পর্ব ৯ |
push.autoSetupRemote true | নতুন branch প্রথমবার git push করলেই remote-এ একই নামের branch আর tracking সম্পর্ক তৈরি (Git ২.৩৭ থেকে) — বারবার -u origin নাম লিখতে হয় না। | পর্ব ১৪ |
rerere.enabled true | একবার মেটানো conflict-এর সমাধান মনে রাখে (rerere), একই conflict আবার এলে নিজে বসায়। | পর্ব ৫৪ |
core.excludesFile | সব repository-র জন্য ব্যক্তিগত ignore-তালিকা। | অংশ ৩ |
Alias — লম্বা command-এর ডাকনাম
যে command প্রতিদিন দশবার লেখেন, তার জন্য একটি alias বানান। Alias config-এর [alias] section-এ থাকে; Git অচেনা command পেলে প্রথমে এখানে খোঁজে:
$ git config --global alias.lg "log --oneline --graph --all"
$ git lg
* 633c9bc Add .gitattributes and sample files
* c0873ce Stop tracking .env; add .gitignore
* 0f0db3c Add app skeleton
$ git config --get-regexp alias
alias.lg log --oneline --graph --all
$ cat ../home/.gitconfig
[user]
name = Rahim
email = rahim.home@example.com
[init]
defaultBranch = main
[pull]
rebase = true
[alias]
lg = log --oneline --graph --allgit config --global নিজেই global file-এর শেষে [alias] section যোগ করেছে — cat দিয়ে দেখা গেল। git lg এখন রহিমের তিনটি commit এক লাইন করে দেখায় (633c9bc হলো অংশ ৭-এর .gitattributes commit, সময় ১০:০২)। Branch থাকলে --graph রেখা টেনে দেখাবে কোনটি কোথা থেকে বেরিয়েছে (পর্ব ১২ আর ১৩)। দুটি নিয়ম মনে রাখুন: built-in command-এর নামে (যেমন log) alias বানালে কাজ করে না, Git নিজের command-ই চালায়; আর ! দিয়ে শুরু alias (যেমন alias.cleanup = "!git branch --merged | …") পুরো shell command চালায় — শক্তিশালী, তাই অন্যের দেওয়া এমন alias না বুঝে বসাবেন না।
অফিসের email অফিসের project-এ — includeIf
অনেকেই একই ল্যাপটপে অফিসের আর নিজের কাজ করেন। ভুল email-এ commit গেলে অফিসের ইতিহাসে ব্যক্তিগত ঠিকানা থেকে যায় (আর উল্টোটাও)। প্রতিটি repository-তে local email বসানো ভুলে যাওয়া সহজ; তার চেয়ে ভালো, folder দেখে নিজে থেকে বসানো:
[user]
name = Rahim
email = rahim.home@example.com
[pull]
rebase = true
# must come AFTER [user]: a later value wins
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work[user]
email = rahim@work.example.com
[pull]
rebase = falseGit ২.৫৩-এ এই দুটি file দিয়ে পরীক্ষা করা হয়েছে: ~/work/shop-এর ভেতরে git config --show-origin --get user.email দেখায় .gitconfig-work-এর পূর্ণ path আর rahim@work.example.com; ~/personal/blog-এ দেখায় ~/.gitconfig আর ব্যক্তিগত email. কয়েকটি খুঁটিনাটি: gitdir:-এর pattern / দিয়ে শেষ হলে Git নিজে শেষে ** যোগ করে, তাই ভেতরের সব গভীরতার repository মেলে; বড়-ছোট হাতের অক্ষর না মানতে gitdir/i:; branch-এর নাম দেখে শর্ত দিতে onbranch:; আর নতুন version-এ remote-এর URL দেখে hasconfig:remote.*.url:. শর্ত না মিললে Git কোনো error দেয় না — চুপচাপ বাদ দেয়, তাই --show-origin দিয়ে যাচাই করে নিন।
আরও গভীরে: Ignore আর attribute-এর পূর্ণ নিয়ম Git-এর নিজস্ব documentation-এ — git help gitignore, git help gitattributes আর git help config; Scott Chacon ও Ben Straub-এর বিনামূল্যের বই Pro Git (২য় সংস্করণ, ২০১৪)-এর “Customizing Git” অধ্যায় এগুলো উদাহরণসহ বোঝায়। ভেতরে, line ending রূপান্তর আসলে Git-এর “clean” আর “smudge” filter-এর একটি বিশেষ রূপ: object store-এ যা ঢোকে (blob) তা সবসময় normalize করা content, আর working tree-তে যা আসে তা রূপান্তরিত — blob আর object store নিয়ে পর্ব ৫২। বড় repository-তে ignore-যাচাই দ্রুত রাখতে Git core.untrackedCache আর file-system monitor (core.fsmonitor)-এর মতো ব্যবস্থাও দেয়।
পরীক্ষা ও ইন্টারভিউতে:
- একটি file
.gitignore-এ আছে, তবুgit status-এ বদল দেখাচ্ছে — কেন? — File-টি আগেই tracked;git rm --cachedদিয়ে index থেকে সরিয়ে commit করতে হবে। logs/আর!logs/keep.txt—keep.txtকি থাকবে? — না; parent folder বাদ পড়লে ভেতরের file ফেরানো যায় না, লিখতে হবেlogs/*.core.autocrlf-এরtrueআরinput-এর পার্থক্য? — দুটোই add-এ CRLF থেকে LF করে; true checkout-এ আবার CRLF করে, input করে না।- একই key system, global আর local-এ — কোনটি কার্যকর, আর কীভাবে দেখবেন? — Local (পরে পড়া হয়);
git config --show-origin --get key. .gitignoreদিয়ে কি একটি ফাঁস হওয়া password লুকানো যায়? — না; ইতিহাসে থেকে যায়, তাই আগে password বদলানো, তারপর দরকারে ইতিহাস পরিষ্কার।
১০ · AI-এর যুগে — ignore আর config-এ AI-কে যাচাই
“আমার project-এর জন্য একটি .gitignore দাও” — AI কয়েক সেকেন্ডে লম্বা একটি তালিকা দেয়, আর বেশিরভাগ সময় সেটি কাজের। কিন্তু এই বিষয়ে AI-এর কিছু পরিচিত ভুল আছে, যার কয়েকটি সরাসরি নিরাপত্তার ঝুঁকি:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
“.env .gitignore-এ যোগ করলেই secret নিরাপদ” — অথচ file-টি আগেই commit হয়েছে | git ls-files .env আর git log --oneline -- .env দেখুন; ইতিহাসে থাকলে secret বদলান |
ইতিহাস মুছতে সরাসরি git filter-branch বা git push --force, কোনো সতর্কতা ছাড়া | আগে একটি throwaway clone-এ চালান, দলকে জানান; পর্ব ৫৭-এর পদ্ধতি দেখুন |
বাদ পড়া folder-এর ভেতরে ! দিয়ে file ফেরানোর চেষ্টা (build/ + !build/keep.txt) | git check-ignore -v -n দিয়ে প্রতিটি path যাচাই করুন |
অস্তিত্বহীন command বা flag বানানো (git ignore, git config --unset-global) | git help config বা git help gitignore খুলে মিলিয়ে নিন |
Linux-এ core.autocrlf=true, বা দলকে .gitattributes ছাড়া শুধু ব্যক্তিগত setting-এ ভরসা করতে বলা | git ls-files --eol দিয়ে আসল অবস্থা দেখুন; নিয়ম repository-তে রাখুন |
Lockfile ignore করতে বলা, বা উল্টো node_modules/ commit করতে বলা | অংশ ১-এর প্রশ্ন: মানুষ লেখেন না যন্ত্র বানায়, আর lockfile-এর ব্যতিক্রম |
সাহায্য চাইতে গিয়ে আপনিই .env-এর আসল মান prompt-এ পেস্ট করলেন | AI-কে কখনো আসল secret দেবেন না; key-এর নাম রাখুন, মান মুছে দিন |
নিচের prompt-এ AI-কে প্রতিটি লাইনের কারণ লিখতে আর আপনার দেওয়া path-এ ফল আগে থেকে বলতে বলা হয়েছে — যাতে আপনি git check-ignore দিয়ে মিলিয়ে দেখতে পারেন।
AI-এর দেওয়া .gitignore বা config হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১একটি password ভুল করে push হয়ে গেছে। পরের commit-এ file মুছে .gitignore-এ যোগ করলে কি সমস্যা মিটে যায়?
না, আর এই ভুল বোঝাটাই সবচেয়ে বিপজ্জনক। Git-এর মূল নকশা হলো প্রতিটি commit একটি স্থায়ী snapshot, যার পরিচয় তার content-এর hash. পরের commit-এ file মুছলে নতুন snapshot-এ file থাকে না, কিন্তু পুরোনো snapshot অপরিবর্তিত থাকে — না হলে hash মিলত না, আর পুরো ইতিহাসের বিশ্বাসযোগ্যতাই ভেঙে পড়ত। তাই git show <পুরোনো-hash>:.env দিয়ে যে কেউ পুরোনো password পড়তে পারেন।
তার ওপর Git distributed: push হওয়া মানে remote server-এ কপি, আর যিনিই এর মধ্যে clone বা fetch করেছেন, তাঁর কম্পিউটারেও কপি। Fork, CI-এর cache, backup — কপি আরও জায়গায় ছড়াতে পারে। ইতিহাস নতুন করে লিখে (পর্ব ৫৭-এ দেখবেন কীভাবে) আপনার repository পরিষ্কার করা যায়, কিন্তু অন্যের কাছে থাকা কপি আপনি মুছতে পারেন না। Public repository-তে স্বয়ংক্রিয় bot অল্প সময়ের মধ্যেই এমন secret খুঁজে নিতে পারে — এমন ঘটনার কথা নিরাপত্তা-গবেষকেরা বিভিন্ন সময়ে লিখেছেন।
তাই পেশাদার ক্রম হলো: (১) secret বাতিল করে নতুন secret বানানো — password বদলানো, API key revoke করা; এটি মিনিটের কাজ আর এতেই আসল ঝুঁকি শেষ। (২) Log দেখে খোঁজা, ফাঁসের পর কেউ ব্যবহার করেছে কি না। (৩) তারপর দরকারে ইতিহাস পরিষ্কার আর দলকে নতুন করে clone করতে বলা। (৪) ভবিষ্যতে ঠেকাতে .gitignore, pre-commit hook-এ secret scanning, আর platform-এর push protection (যেখানে আছে)।
এখানে একটি সাধারণ software engineering শিক্ষাও আছে: যে system ইতিহাস কখনো ভোলে না, তার সুবিধা (audit, উদ্ধার) আর অসুবিধা (ভুলও ভোলে না) একই মুদ্রার দুই পিঠ। নকশার সময়েই ঠিক করতে হয় কোন তথ্য সেই ইতিহাসে ঢুকবে না।
মূল উপলব্ধি: .gitignore ভবিষ্যৎকে রক্ষা করে, অতীতকে নয়। ফাঁস হওয়া secret-এর প্রথম চিকিৎসা ইতিহাস মোছা নয়, secret বদলানো।
প্র ০২Lockfile যন্ত্রের বানানো, তবু commit করা হয় কেন? সব project-এর জন্যই কি একই নিয়ম?
package.json-এ লেখা থাকে “express-এর 4.x-এর যেকোনো version চলবে”। আজ install করলে হয়তো 4.21 আসে, ছয় মাস পরে 4.22 — আর তার নিজের dependency-গুলোও বদলে যায়। ফলে একই source code থেকে দুই সময়ে দুই রকম program তৈরি হতে পারে। একজনের কম্পিউটারে চলে, আরেকজনের কম্পিউটারে বা CI-তে ভেঙে যায় — “আমার মেশিনে তো চলে!” সমস্যার একটি বড় উৎস এটি।
Lockfile (package-lock.json, yarn.lock, poetry.lock, Cargo.lock) পুরো dependency গাছের প্রতিটি package-এর ঠিক version আর প্রায়ই content-এর hash লিখে রাখে। এটি commit থাকলে সবাই, CI আর production একই জিনিস install করে। তাই নিয়মটা “যন্ত্রের বানানো file বাদ” নয়, বরং “যা আবার বানালে একই ফল আসে না, তা রাখুন”। node_modules/ lockfile থেকে হুবহু আবার বানানো যায়, তাই বাদ; lockfile নিজে আবার বানালে ভিন্ন হতে পারে, তাই রাখা।
তবে সব ভাষার সম্প্রদায় সবসময় একমত ছিল না। Rust-এর Cargo দীর্ঘদিন পরামর্শ দিত: application-এ Cargo.lock commit করুন, কিন্তু অন্যের ব্যবহারের library-তে নয় — কারণ library ব্যবহারকারীর project তাঁর নিজের lockfile দিয়েই version ঠিক করে। ২০২৩ সালের দিকে Cargo দল এই পরামর্শ নরম করে “project-এর জন্য যা ভালো, তা-ই করুন” ধাঁচের অবস্থানে যায় বলে জানা যায়। Go-তে go.sum commit করাই রীতি, আর কিছু দল vendor/-এ পুরো dependency-র source-ও রাখে, যাতে internet বা package registry ছাড়াই build হয়।
এর পেছনে বড় একটি ধারণা হলো reproducible build — একই input থেকে সবসময় একই output. নিরাপত্তার দিক থেকেও এটি জরুরি: lockfile-এর hash থাকলে কেউ registry-তে একই version নম্বরে অন্য content বসিয়ে দিলে install ব্যর্থ হয়।
মূল উপলব্ধি: প্রশ্নটা “কে বানিয়েছে” নয়, “এটি ছাড়া কি ঠিক একই জিনিস আবার বানানো যায়?” — যায় না হলে সেটি ইতিহাসের অংশ।
প্র ০৩পৃথিবীতে একটির বদলে দুই রকম line ending কেন, আর Git সমাধানটা working tree-তে না করে repository-তে কেন করে?
ইতিহাসটা যন্ত্রের। Teletype যন্ত্রে লেখার মাথা বাঁ দিকে ফেরানো (carriage return) আর কাগজ এক লাইন তোলা (line feed) দুটি আলাদা যান্ত্রিক কাজ ছিল, তাই দুটি আলাদা সংকেত লাগত। অনেক পুরোনো system দুটোই পাঠাত — CR LF. Unix-এর নকশাকারীরা ঠিক করলেন file-এর ভেতরে একটিই অক্ষর (LF) রাখবেন, আর terminal-এ পাঠানোর সময় driver দরকারমতো CR যোগ করবে। অন্যদিকে CP/M আর পরে MS-DOS CR LF রেখে দেয়, আর Windows সেই উত্তরাধিকার পায়। পুরোনো Mac OS শুধু CR নেয়। তিনটি সিদ্ধান্তই তখনকার প্রেক্ষাপটে যুক্তিসংগত ছিল; সমস্যা হলো, file এখন সবার মধ্যে ঘোরে।
Git-এর নকশা এখানে চতুর: repository-কে একটি “সাধারণ ভাষা”-য় রাখা (LF), আর প্রতিটি কম্পিউটারের working tree-কে তার নিজের ভাষায় অনুবাদের সুযোগ দেওয়া। কারণ repository-ই সবার মিলনস্থল — diff, merge, blame সব সেখানে চলে। সেখানে যদি একই লাইন কখনো CRLF, কখনো LF হয়, তাহলে line-by-line তুলনা অর্থহীন হয়ে যায়: কেউ শুধু save করলেই পুরো file “বদলে” যায়, blame-এ আসল লেখকের নাম হারায়, আর merge-এ অকারণ conflict হয়।
দ্বিতীয় নকশা-সিদ্ধান্ত: নিয়মটা কোথায় থাকবে। শুরুতে Git ব্যক্তিগত core.autocrlf-এর ওপর ভরসা করত, কিন্তু দশজনের দলে একজন ভুল করলেই হলো। .gitattributes নিয়মকে repository-র ভেতরে নিয়ে আসে — যেমন code style-এর নিয়ম কারও মনে না রেখে linter-এর config-এ রাখা হয়। এটি একটি বড় engineering নীতি: যে নিয়ম সবার মানতে হবে, সেটি ব্যক্তির অভ্যাসে নয়, project-এর ভেতরে লিখে রাখুন। অবস্থাও বদলেছে: Windows-এর Notepad ২০১৮ সালের দিকে থেকে LF file ঠিকভাবে দেখাতে পারে বলে জানা যায়, আর প্রায় সব আধুনিক editor দুটোই বোঝে — তবু shell script বা .bat-এর মতো file-এ ঠিক byte এখনো জরুরি।
মূল উপলব্ধি: ভিন্নতা মুছে ফেলা না গেলে, একটি সাধারণ রূপ বেছে নিন আর সীমানায় অনুবাদ করুন — আর সেই নিয়ম ব্যক্তির মাথায় নয়, project-এর ভেতরে রাখুন।
প্র ০৪Git-এ “শেষে পড়া মান জেতে”। অন্য tool-ও কি একই নিয়ম মানে? না মানলে কী বিপদ?
স্তরভিত্তিক config software-এ খুব সাধারণ নকশা: সবচেয়ে সাধারণ ডিফল্ট নিচে, তার ওপর প্রতিষ্ঠান বা কম্পিউটারের নিয়ম, তার ওপর ব্যক্তির পছন্দ, তার ওপর project, আর একদম ওপরে একবারের command-line override. যত নির্দিষ্ট, তত অগ্রাধিকার। npm-এর config-ও এমন (built-in, global, user, project), CSS-এর cascade-ও মূলত এই চিন্তা, আর অনেক application-এ environment variable config file-কে ছাপিয়ে যায়।
কিন্তু “জেতা”-র নিয়ম সব জায়গায় এক নয়। OpenSSH-এর client config (~/.ssh/config) উল্টো চলে: documentation অনুযায়ী প্রতিটি parameter-এর প্রথম পাওয়া মানই ব্যবহার হয়। তাই সেখানে নির্দিষ্ট host-এর নিয়ম ওপরে আর সাধারণ Host * নিচে লিখতে হয়। Git-এর অভ্যাসে কেউ Host * ওপরে লিখলে তাঁর নির্দিষ্ট নিয়ম কখনো কাজই করবে না, আর কোনো error-ও আসবে না। একইভাবে .gitignore-এ শেষ লাইন জেতে, কিন্তু অনেক firewall-এ প্রথম মেলা নিয়ম জেতে।
এ কারণেই ভালো tool-এ “এই মান কোথা থেকে এলো?” প্রশ্নের উত্তর দেওয়ার ব্যবস্থা থাকে। Git-এ --show-origin আর --show-scope; .gitignore-এর জন্য check-ignore -v; .gitattributes-এর জন্য check-attr. এগুলো না থাকলে স্তরভিত্তিক config দ্রুত রহস্য হয়ে ওঠে — একজন বলেন “আমি তো false করেছি!”, অথচ অন্য স্তরের একটি মান সেটি ঢেকে দিচ্ছে।
Software engineering-এর দৃষ্টিতে এটি observability-র ছোট একটি উদাহরণ: system শুধু ঠিক কাজ করলেই হয় না, কেন এই কাজ করল তা জানানোর ক্ষমতাও থাকতে হয়। নিজের program-এ config-এর স্তর বানালে উৎসসহ effective config দেখানোর একটি command রাখার কথা ভাবুন।
মূল উপলব্ধি: স্তরভিত্তিক config-এ শুধু স্তর নয়, “কে জেতে” নিয়মটাও tool-ভেদে আলাদা — অনুমান না করে উৎস দেখানোর command দিয়ে যাচাই করুন।
প্র ০৫বাদ পড়া folder-এর ভেতরের file কেন ! দিয়ে ফেরানো যায় না? এটা কি Git-এর একটি ত্রুটি?
এটি ত্রুটি নয়, সচেতন একটি performance-সিদ্ধান্ত, আর documentation-এ কারণসহ লেখা আছে। git status চালালে Git-কে working tree-র প্রতিটি folder ঘুরে দেখতে হয় কোন file নতুন। একটি সাধারণ JavaScript project-এর node_modules/-এ হাজার হাজার file থাকতে পারে, অনেক গভীর folder-এ ছড়ানো। Git যদি প্রতিবার সেখানে ঢুকে প্রতিটি file-এর জন্য দেখত “কোনো ! লাইন কি একে ফেরাচ্ছে?”, তাহলে প্রতিটি status-এ অনেক সময় যেত — আর ফল প্রায় সবসময় একই হতো: সব বাদ।
তাই নিয়ম: কোনো folder বাদ পড়লে Git তার ভেতরের তালিকাই পড়ে না (file system-এ directory খোলাই হয় না)। এর দাম হলো ব্যবহারকারীর দিকের একটি ফাঁদ — build/ আর !build/keep.txt কাজ করে না। সমাধান হিসেবে Git ব্যবহারকারীকে একটি সূক্ষ্ম নিয়ন্ত্রণ দেয়: build/* লিখলে folder খোলা থাকে, ভেতরের জিনিস আলাদা করে বাদ পড়ে, আর তখন ! কাজ করে। অর্থাৎ ধীর পথ বেছে নেওয়ার ক্ষমতা থাকে আপনার হাতে, কিন্তু ডিফল্ট দ্রুত পথ।
বড় repository-তে Git আরও এগিয়েছে: core.untrackedCache চালু থাকলে Git মনে রাখে কোন folder-এর অবস্থা আগের বার কেমন ছিল আর folder-এর সময়-চিহ্ন না বদলালে আবার পড়ে না; file-system monitor (core.fsmonitor) OS-কে জিজ্ঞেস করে শুধু বদলানো path-গুলো জেনে নেয়। এগুলো নিয়মের অর্থ বদলায় না, শুধু গতি বাড়ায়। খুব বড় monorepo-র কৌশল আসবে পর্ব ১৯ আর ৫৬-এ।
নকশার শিক্ষা: সাধারণ ক্ষেত্রকে দ্রুত রাখতে বিরল ক্ষেত্রকে একটু কঠিন করা প্রায়ই ঠিক সিদ্ধান্ত — শর্ত একটাই, সীমাবদ্ধতাটা পরিষ্কার করে লেখা থাকতে হবে আর পাশ কাটানোর একটি উপায় দিতে হবে। Git এখানে দুটোই করেছে।
মূল উপলব্ধি: Parent folder-এর নিয়ম গতির জন্য নেওয়া একটি trade-off; folder/* দিয়ে আপনি ইচ্ছে করে ধীর কিন্তু নমনীয় পথ বেছে নিতে পারেন।
অনুশীলন
আউটপুট অনুমান, নিজে লেখা, command-এর ক্রম আর নকশা-সমালোচনা — প্রতিটির পূর্ণ উত্তর আসল Git দিয়ে যাচাই করা।
-
কোনগুলো ignored? (আউটপুট অনুমান) Root-এর
.gitignore:
দশটি path:*.tmp !keep.tmp /out/ cache/ docs/*.html !docs/index.html **/test-results/a.tmp,src/keep.tmp,out/app.js,src/out/app.js,src/cache/x.bin,docs/guide.html,docs/index.html,docs/api/ref.html,web/test-results/r.xml,cache/keep.tmp. প্রতিটির জন্য বলুন ignored কি না আর কোন লাইনের কারণে।Git ২.৫৩-এ
git check-ignore -v -n-এর আসল আউটপুট:.gitignore:1:*.tmp a.tmp .gitignore:2:!keep.tmp src/keep.tmp .gitignore:3:/out/ out/app.js :: src/out/app.js .gitignore:4:cache/ src/cache/x.bin .gitignore:5:docs/*.html docs/guide.html .gitignore:6:!docs/index.html docs/index.html :: docs/api/ref.html .gitignore:7:**/test-results/ web/test-results/r.xml .gitignore:4:cache/ cache/keep.tmpঅর্থাৎ ignored:
a.tmp(১),out/app.js(৩),src/cache/x.bin(৪),docs/guide.html(৫),web/test-results/r.xml(৭),cache/keep.tmp(৪). থাকে:src/keep.tmp(২-এর negation),src/out/app.js(/out/root-এ বাঁধা),docs/index.html(৬),docs/api/ref.html(docs/*.html-এর*/পার হয় না)। ফাঁদ ছিল দুটি:docs/api/ref.htmlআরcache/keep.tmp— শেষটির নাম!keep.tmp-এ মেলে, কিন্তু parentcache/বাদ পড়ায় ফেরে না। -
নিজে .gitignore লিখুন: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টাল, Python-এ লেখা। চাহিদা: (ক) যেকোনো জায়গার
__pycache__/আর.pyc/.pyo/.pydfile বাদ; (খ) root-এর.venv/বাদ; (গ) root-এরbuild/আরdist/বাদ; (ঘ).coveragefile আরhtmlcov/বাদ; (ঙ) সব.logবাদ; (চ).envআর.env.local-এর মতো সব বাদ, কিন্তু.env.exampleথাকবে; (ছ) root-এরmedia/-এ ব্যবহারকারীদের upload করা ছবি বাদ, কিন্তু খালি folder রাখারmedia/.gitkeepথাকবে, আরapp/media/logo.pngথাকবে।# Python __pycache__/ *.py[cod] /.venv/ # Build output /build/ /dist/ # Test coverage .coverage htmlcov/ # Logs *.log # Secrets .env .env.* !.env.example # User uploads: ignore the content, keep the empty folder /media/* !/media/.gitkeepমূল সিদ্ধান্তগুলো:
*.py[cod]এক লাইনে তিনটি extension..env.*.env.example-কেও বাদ দেয়, তাই পরের!লাইন — ক্রম উল্টালে কাজ করবে না। আর (ছ)-তে/media/লিখলে.gitkeepফেরানো যেত না (parent folder-এর নিয়ম), তাই/media/*; শুরুর/থাকায়app/media/ছোঁয়া হয় না। Git ২.৫৩-এ ১৩টি path দিয়ে যাচাই:app/__pycache__/views.cpython-312.pyc→ লাইন ২,.venv/lib/x.py→ লাইন ৪,.env→ লাইন ১৮,.env.local→ লাইন ১৯ (.env.*),.env.example→ লাইন ২০-এর!,media/photo.jpg→ লাইন ২৩,media/.gitkeep→ লাইন ২৪-এর!,logs/server.log→ লাইন ১৫,dist/x.whl→ লাইন ৮,htmlcov/index.html→ লাইন ১২,.coverage→ লাইন ১১; আরapp/media/logo.pngওapp/models.py-এ কোনো লাইন মেলে না (::)। -
Command-এর ক্রম: একটি repository-তে ভুল করে
.envfile আরcoverage/folder commit হয়ে push হয়ে গেছে।.gitignore-এ এখনো কিছু নেই। ধাপে ধাপে command লিখুন, যাতে দুটোই আর track না হয় কিন্তু আপনার disk-এ থেকে যায়; প্রতিটি ধাপের পর কী যাচাই করবেন বলুন। আর Git-এর বাইরে কোন কাজটি সবার আগে করতে হবে?সবার আগে, Git-এর বাইরে:
.env-এর প্রতিটি secret বাতিল করে নতুন বানানো (database password বদল, API key revoke). push হয়ে যাওয়ায় এটি আর গোপন নয়।$ printf '.env\n.env.*\n!.env.example\ncoverage/\n' >> .gitignore $ git rm --cached .env $ git rm -r --cached coverage $ git status $ git add .gitignore $ git commit -m "Stop tracking .env and coverage/" $ git ls-files .env coverage $ git check-ignore -v .env coverage/index.htmlযাচাই:
git status-এdeleted: .envআরdeleted: coverage/…“Changes to be committed”-এর নিচে থাকবে, অথচls-এ file আছে। Commit-এর পরgit ls-files .env coverageকিছুই দেখাবে না, আরcheck-ignore -vপ্রতিটির জন্য.gitignore-এর লাইন দেখাবে।--cachedভুলে গেলে disk থেকেও মুছত; commit-এর আগে টের পেলেgit restore --staged --worktree .envশেষ commit থেকে file ফিরিয়ে আনে (Git ২.৫৩-এ যাচাই করা; বিস্তারিত পর্ব ১১)। শেষে push, আর দলকে জানানো যে তাঁদের কাছ থেকেওcoverage/মুছে যাবে আর.envএখন নিজের কম্পিউটারে.env.exampleদেখে বানাতে হবে। পুরোনো commit-এ secret থেকেই গেছে; ইতিহাস পরিষ্কার পর্ব ৫৭-এ। -
নকশা-সমালোচনা: একটি দলের lead নিয়ম পাঠালেন: “(১) সবাই নিজের কম্পিউটারে
core.autocrlf=trueরাখবেন — তাহলেই line ending-এর সমস্যা থাকবে না। (২) Repository-তে শুধু.jsfile থাকবে, তাই.gitignoreহবে*আর!*.js. (৩) সবার নাম-email repository-র.git/config-এ বসানো থাকবে, তাই কাউকে কিছু set করতে হবে না।” প্রতিটি নিয়মের সমস্যা বলুন আর বিকল্প দিন।(১) দলে Linux-macOS ব্যবহারকারী থাকলে তাঁদের working tree-তে সব file CRLF হয়ে যাবে — shell script ভাঙবে। আর নিয়মটা প্রত্যেকের মনে রাখার ওপর নির্ভর করে; একজন না মানলেই repository-তে CRLF ঢোকে। বিকল্প: repository-তে
.gitattributes(* text=auto,*.sh text eol=lf, ছবির জন্যbinary), ব্যক্তিগত setting OS অনুযায়ী (অংশ ৭)।(২)
*সব folder-ও বাদ দেয়, তাইsrc/app.js-এর parentsrcবাদ পড়ে আর!*.jsসেখানে পৌঁছায় না — শুধু root-এর.jsথাকবে। আরও বড় কথা,package.json, lockfile, README,.gitignoreনিজেই বাদ পড়বে। “সব বাদ দিয়ে কয়েকটি ফেরানো”-র ধাঁচ চাইলে অন্তত!*/দিয়ে folder ফেরাতে হয়, কিন্তু এটি ভঙ্গুর। বিকল্প: যা বাদ দিতে চান সেটাই তালিকায় রাখুন (dependency, build, log, secret).(৩)
.git/configcommit-ই হয় না — অন্যের clone-এ পৌঁছাবে না। পৌঁছালেও সবাই একই নামে commit করতেন, ইতিহাসে কে কী লিখেছেন বোঝা যেত না। বিকল্প: প্রত্যেকে নিজের global config-এ নাম-email দেবেন (পর্ব ৭), অফিসের ঠিকানার জন্যincludeIf(অংশ ৯), আর দরকারে CI-তে commit-এর email যাচাই।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমGit একটি command চালানোর সময় config যে ক্রমে পড়ে, আগে থেকে পরে সাজান (শেষে পড়া মান জেতে; ▲▼ চেপে সরান)
- System —
/etc/gitconfig - Global —
~/.gitconfig - Local —
.git/config - Worktree —
.git/config.worktree(চালু থাকলে) - Command line —
git -c key=value
Repository-র root-এর .gitignore-এ একটিমাত্র লাইন: /build/. নিচের কোন path ignored হবে?
src/build/app.obuild/app.o- Root-এর
buildনামের একটি সাধারণ file build.gradle
.gitignore-এ পরপর দুটি লাইন: logs/ আর !logs/keep.txt. logs/keep.txt file-টির কী হবে?
- শেষ লাইন জেতে, তাই track করা যাবে
- Git একটি error দেখাবে
- Ignored থাকবে, কারণ parent folder বাদ পড়লে ভেতরের file ফেরানো যায় না
- শুধু অন্য একটি .gitignore-এ লিখলে ফিরবে, এখানে নয়
.env file-টি আগেই commit হয়েছে। এখন .gitignore-এ .env লেখা হলো আর file-টিতে একটি লাইন বদলানো হলো। git status কী দেখাবে, আর track বন্ধ করতে কী লাগবে?
modified: .envদেখাবে;git rm --cached .envকরে commit করতে হবে- কিছুই দেখাবে না; কাজ শেষ
deleted: .envদেখাবে;git rm .envলাগবেignored: .envদেখাবে;git add -f .envলাগবে
একটি Linux কম্পিউটারে core.autocrlf=input, আর কোনো .gitattributes নেই। Windows থেকে আসা CRLF line ending-এর একটি text file git add করে commit করা হলো, তারপর অন্য folder-এ নতুন করে checkout. Repository আর নতুন checkout-এ কী থাকবে?
- Repository-তে CRLF, checkout-এ CRLF
- Repository-তে LF, checkout-এ CRLF
- দুই জায়গাতেই হুবহু মূল file, কোনো রূপান্তর নেই
- Repository-তে LF, checkout-এও LF
একটি repository-তে user.email system config-এ a@example.com, global config-এ b@example.com, আর local .git/config-এ c@example.com. কোনো -c বা include নেই। পরের commit কোন email-এ হবে, আর কোন command উৎসসহ দেখাবে?
a@example.com;git config --system --get user.emailc@example.com;git config --show-origin --get user.emailb@example.com;git config --global --list- Git error দেবে, কারণ একই key তিন জায়গায়
.gitattributes-এ *.png binary লেখা থাকলে Git .png file-এর জন্য কোন attribute-গুলো ঠিক করে?
text eol=lf- শুধু
-text, আর file-টি LFS-এ যায় -diff -merge -textexport-ignore
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- মানুষের লেখা আর আবার বানানো যায় না এমন জিনিস commit করুন (source, build script, lockfile); dependency, build output, log, ব্যক্তিগত setting আর secret নয়।
.gitignore-এ শেষে/= শুধু folder, শুরুতে-মাঝে/= root থেকে,!= ফেরানো, শেষ মেলা লাইন জেতে; কিন্তু বাদ পড়া folder-এর ভেতরের file ফেরে না —folder/*লিখুন।- Ignore শুধু untracked file-এ কাজ করে; tracked file থামাতে
git rm --cached(folder-এ-r), আর যাচাইয়েcheck-ignore -vওstatus --ignored. - Repository-তে text file LF; দলের নিয়ম
.gitattributes-এ (* text=auto,binary), ব্যক্তিগতcore.autocrlfOS অনুযায়ী। - Config পড়া হয় system → global → local → worktree → -c, শেষ মান জেতে; উৎস দেখুন
--show-originদিয়ে, অফিসের email-এর জন্যincludeIf.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১১ · ভুল শোধরানো — restore, reset, revert, amend আর reflogপরবর্তী পর্বভুল commit বা ভুল stage — কোন command কোন এলাকা বদলায়।
- পর্ব ০৯ · History আর পার্থক্য দেখা — status, diff, log আর showআগের পর্ব
git statusআরgit diff --cachedপড়ার অভ্যাস, যা এই পর্বের প্রতিটি যাচাইয়ে লাগে। - সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- JavaScript · npm, Vite, ESLint ও Prettierপাশের ট্র্যাক
node_modules,package-lock.jsonআর build output কোথা থেকে আসে। - Python · pip আর virtual environmentপাশের ট্র্যাক
.venv/কেন commit হয় না, আর requirements file কেন হয়। - Operating Systems · Linux terminalগভীরে যান
~, path, environment variable আর shell — config file বুঝতে কাজে লাগে।