সরাসরি পর্বে যান
পর্ব ১৯ · ৭০-এর মধ্যে · মডিউল ৩

বড় repository — monorepo, submodule, subtree আর Git LFS

Large Repositories — Monorepo vs Polyrepo, Shallow and Partial Clone, Submodules, Subtrees and Git LFS

Taught in Bangla, this lesson compares monorepos and polyrepos and their trade-offs, building only what changed in a monorepo, and making large repositories manageable with shallow, partial and sparse clones. It explains submodules as pinned commits of another repository, submodule updates and common traps, alternatives such as subtrees, vendoring and package managers, and Git LFS for large binary files.

এই পর্বে যা শিখবেন

  • Monorepo আর polyrepo-র প্রকৃত trade-off — atomic change, tooling, CI-এর খরচ, access control আর মালিকানা — আর একটি কাল্পনিক কোম্পানির জন্য সিদ্ধান্ত নেওয়া
  • Dependency graph থেকে কোন project build আর test করতে হবে, তা হিসাব করা
  • Shallow, partial আর sparse clone — কোনটি কী বাদ দেয়, কখন কোনটি নেবেন, আর এর ফাঁদ
  • Submodule যোগ, clone, update আর pointer commit করা; detached HEAD, ভুলে যাওয়া update আর push না করা submodule commit-এর ফাঁদ
  • Subtree, vendoring, package manager আর Git LFS — কখন কোনটি বেছে নেবেন

১ · এক বাড়ি নাকি আলাদা ফ্ল্যাট — monorepo আর polyrepo

একটি কাল্পনিক অনলাইন বাজার অ্যাপের কথা ভাবুন। শুরুতে তিনজন developer আর একটি repository — তাতে website, payment-এর কোড আর বোতাম-কার্ডের মতো দেখতে-একরকম UI-র অংশ, সব একসাথে। দুই বছর পরে দলে ৪০ জন, তিনটি দল, দুটি backend service, একটি website, একটি admin panel, একটি Android app আর শেয়ার করা কয়েকটি library. এবার প্রশ্ন ওঠে: সব কোড কি এক repository-তে থাকবে, নাকি প্রতিটি অংশের আলাদা repository হবে?

এই পর্বের প্রশ্ন তাই শুধু Git command নয়, একটি software engineering সিদ্ধান্ত। আগের পর্বগুলোতে আপনি একটি repository-র ভেতরে কাজ শিখেছেন — পর্ব ১৪-এ clone, fetch আর push, পর্ব ১৬-এ pull request আর review, পর্ব ১৮-এ tag, SemVer আর release. এখন দেখব repository যখন অনেক বড় হয়, বা যখন একাধিক repository একে অপরের ওপর নির্ভর করে, তখন কী হয়।

পুরান ঢাকার একটি যৌথ পরিবারের বড় বাড়ির কথা ভাবুন। সবাই এক ছাদের নিচে: রান্নাঘর একটাই, বিদ্যুতের লাইন একটাই, ছাদ মেরামত করলে সবার একসাথে হয়। কেউ সিঁড়ি বদলালে সবাই সাথে সাথে টের পান। কিন্তু বাড়ি যত বড় হয়, চাবির গোছা তত ভারী, আর কে কোন ঘরের দায়িত্বে, তা লিখে রাখতে হয়। এটি monorepo. অন্যদিকে একই পরিবার আলাদা আলাদা ফ্ল্যাটে থাকলে প্রত্যেকের নিজের চাবি, নিজের নিয়ম; কিন্তু ঈদের দাওয়াত দিতে হলে প্রত্যেককে আলাদা করে খবর দিতে হয়, আর কে কবে আসবেন তা মেলানো কঠিন। এটি polyrepo. আর কোনো ফ্ল্যাটের দেয়ালে যদি অন্য বাড়ির একটি নির্দিষ্ট দিনের ছবি টাঙানো থাকে — “এই বাড়ি, ১৫ জানুয়ারির অবস্থায়” — সেটি submodule: আসল বাড়ি নয়, একটি নির্দিষ্ট মুহূর্তের ঠিকানা।

তিনভাবে কোড সাজানো

Monorepo · polyrepo with versioned packages · superproject with a submodule

Monorepo, polyrepo আর submodule-এর তুলনা ১ · Monorepo — একটি repository bazar/ (একটি ইতিহাস, একটি CI) services/payments web/shop libs/ui-kit এক commit-এ সব বদল ২ · Polyrepo — অনেক repository payments.git shop.git ui-kit.git নির্ভরতা: package ui-kit@2.3.0 (version নম্বর) ৩ · Superproject + submodule shop (superproject) README.md .gitmodules libs/ui-kit → aa11f68 ui-kit repository cb99720 aa11f68 6e47256 superproject রাখে শুধু একটি commit id (gitlink)
ছবি ১৯.১ — Monorepo-তে folder সরাসরি একে অপরকে ব্যবহার করে; polyrepo-তে নির্ভরতা চলে version নম্বর দিয়ে; submodule-এ superproject অন্য repository-র একটি নির্দিষ্ট commit-এ আটকে থাকে (ui-kit-এ নতুন cb99720 এলেও shop এখনো aa11f68-এ)।
মূল ধারণা

Monorepo মানে বহু project একটি version control repository-তে, একটি সাধারণ ইতিহাসে। এটি “এক বিশাল program” নয় — ভেতরের project-গুলো আলাদাভাবে build আর deploy হতে পারে, এমনকি আলাদা microservice হিসেবেও। Polyrepo (অনেক সময় “multi-repo” বলা হয়) মানে প্রতিটি project বা service-এর নিজস্ব repository. দুটোই বৈধ; প্রশ্নটা হলো কোন খরচ আপনার দল বহন করতে পারবে।

২ · Trade-off — কোনটি কখন সুবিধা দেয়

ধরুন libs/money library-তে টাকার হিসাব গোল করার function-এর নাম বদলাতে হবে, আর সেটি পাঁচটি project ব্যবহার করে। Monorepo-তে একজন developer একটি branch-এ library আর পাঁচটি ব্যবহারকারী একসাথে বদলে একটি pull request খোলেন; CI সব একসাথে test করে, আর merge হলে সবাই একই মুহূর্তে নতুন কোড পান। এটি atomic change. Polyrepo-তে একই কাজ হয় কয়েক ধাপে: library-তে নতুন নাম যোগ, পুরোনোটা “deprecated” রেখে নতুন version release (পর্ব ১৮-এর SemVer), তারপর পাঁচটি repository-তে আলাদা pull request দিয়ে version বাড়ানো, শেষে পুরোনো নাম মুছে আরেকটি major release. কাজটা বেশি, কিন্তু প্রতিটি দল নিজের সময়মতো বদলাতে পারে।

বিষয়MonorepoPolyrepo
একাধিক project-এ একসাথে বদলএক commit, এক review — সহজকয়েকটি release আর pull request; সময় লাগে
শেয়ার করা toolingএকই linter, formatter, build system, CI-এর নিয়ম সবার জন্যপ্রতিটি repository-তে আলাদা; সময়ের সাথে আলাদা হয়ে যায়
কোড দেখা ও খোঁজাসবাই সব কোড দেখেন, একটি search-এ সব ব্যবহার পাওয়া যায়কোথায় কোন কোড, আগে জানতে হয়
Repository-র আকারবড় হয়; clone, git status, IDE ধীর হতে পারেপ্রতিটি ছোট আর দ্রুত
CI-এর খরচসব প্রতিবার build করলে বিশাল; affected build আর cache লাগেপ্রতিটি repository শুধু নিজেরটা build করে
Access controlGit-এ সাধারণত পুরো repository পড়ার অধিকার একসাথে; folder-ভিত্তিক গোপনীয়তা কঠিনপ্রতিটি repository-র আলাদা অধিকার, সহজ
মালিকানাFolder ধরে লিখে রাখতে হয় (CODEOWNERS)Repository-ই মালিকানার সীমা
Version-এর মিলসাধারণত সবাই library-র একই (সর্বশেষ) রূপ ব্যবহার করেএকেক project একেক version-এ পড়ে থাকতে পারে

মালিকানা — CODEOWNERS

Monorepo-তে “এই folder কার দায়িত্বে” প্রশ্নের উত্তর লেখা থাকে একটি file-এ। GitHub, GitLab-এর মতো অনেক platform একটি CODEOWNERS file বোঝে: কোনো pull request ওই path-এর file বদলালে নির্দিষ্ট মানুষ বা দলকে স্বয়ংক্রিয়ভাবে reviewer করা হয়, আর পর্ব ১৬-এর protected branch-এর নিয়মে তাঁদের অনুমোদন বাধ্যতামূলকও করা যায়। File-এর জায়গা আর syntax-এর খুঁটিনাটি platform-ভেদে আলাদা; নিচের উদাহরণটি GitHub-এর ধাঁচে লেখা, যেখানে (লেখার সময়ে) শেষে মেলা লাইনটিই জেতে — .gitignore-এর মতো।

# Default owners for everything in the repository
*                     @bazar-org/platform-team

# Each team owns its folders; later lines override earlier ones
/services/payments/   @bazar-org/payments-team
/web/                 @bazar-org/web-team
/libs/money/          @bazar-org/payments-team @bazar-org/web-team
/ci/                  @bazar-org/platform-team

CODEOWNERS কে review করবেন তা ঠিক করে, কে কোড পড়তে পারবেন তা নয়। Monorepo-তে কোনো অংশ সত্যিই গোপন রাখতে হলে (ধরুন বাইরের একটি contractor দলের জন্য) সাধারণত সেটিকে আলাদা repository-তে রাখতে হয়। এখানে Conway's law মনে রাখার মতো: repository-র সীমা প্রায়ই প্রতিষ্ঠানের দলের সীমার প্রতিচ্ছবি হয়ে দাঁড়ায়, তাই দলের কাঠামো বদলালে repository-র কাঠামো নিয়েও আবার ভাবতে হয়।

বড় কোম্পানির monorepo — কী জানা যায়, কী নয়

সবচেয়ে বেশি উদ্ধৃত প্রকাশ্য বর্ণনা হলো ২০১৬ সালে Communications of the ACM-এ Google-এর দুই প্রকৌশলী Rachel Potvin আর Josh Levenberg-এর প্রবন্ধ “Why Google Stores Billions of Lines of Code in a Single Repository”. প্রবন্ধ অনুযায়ী ২০১৫ সালের দিকে সেই repository-তে প্রায় দুইশো কোটি লাইন কোড আর হাজার হাজার developer-এর কাজ ছিল। কিন্তু একটি জরুরি কথা: Google এটি Git-এ রাখে না — প্রবন্ধে বর্ণিত system-টি তাদের নিজস্ব (Piper নামে), সাথে নিজস্ব build system আর code review tool. প্রবন্ধে সুবিধার পাশাপাশি খরচও লেখা: বিশাল tooling-এ বিনিয়োগ, অপ্রয়োজনীয় নির্ভরতা বেড়ে যাওয়ার ঝুঁকি। Microsoft-ও Windows-এর কোড Git-এ আনতে Git-এর ওপর বাড়তি স্তর তৈরি করেছিল বলে তারা প্রকাশ্যে লিখেছে। তাই “বড় কোম্পানি monorepo ব্যবহার করে, তাই আমরাও করব” যুক্তিটি দুর্বল — তাদের সাথে আসে বিশাল tooling দল।

এবার সিদ্ধান্তটি একটি কাল্পনিক কোম্পানির জন্য ধাপে ধাপে নিই। লক্ষ করুন, উত্তর “পুরো monorepo” বা “পুরো polyrepo” নয় — প্রতিটি তথ্য পাল্লা একদিকে ভারী করে।

কাল্পনিক “বাজার ডট কম”: ৪০ জন developer, ৩টি দল
যে প্রশ্ন দেখা হচ্ছে
তথ্য
ঝোঁক
এখন পর্যন্ত নকশা
  1. আগে তথ্য, পরে মত। কোম্পানির সাতটি অংশ আর তিনটি দল। সিদ্ধান্তের আগে জানতে হবে কোন অংশ কোনটির ওপর নির্ভর করে, কে লেখেন, কে পড়তে পারবেন আর কে বাইরে থেকে ব্যবহার করবেন।
  2. Atomic change-এর চাহিদা। শেয়ার করা library প্রায় প্রতি sprint-এ বদলায় আর ৩-৪টি project একসাথে বদলাতে হয়। Polyrepo-তে প্রতিবার release আর কয়েকটি pull request লাগত। পাল্লা monorepo-র দিকে।
  3. এক ভাষা, এক tooling. Web আর services একই TypeScript tooling ব্যবহার করে। এক জায়গায় থাকলে linter-এর নিয়ম বা test runner-এর version একবার বদলালেই সবার হয়। আবারও monorepo.
  4. Access control আলাদা করে দেয়। Contractor দলকে পুরো monorepo দিলে payment-এর কোডও দেখা যেত — Git সাধারণ অবস্থায় folder ধরে পড়ার অধিকার দেয় না। তাই Android app আলাদা repository-তে, যা backend-এর সাথে শুধু প্রকাশ্য API দিয়ে কথা বলে।
  5. আকার সমস্যা কোডে নয়, file-এ। কোড কয়েকশো MB-ও নয়; ঝামেলা ৮ GB design file. এগুলো Git-এর সাধারণ object-এ রাখলে প্রতিটি clone-এ প্রতিটি version আসবে (অংশ ৮)। তাই Git LFS বা আলাদা storage.
  6. বাইরের ব্যবহারকারীর জন্য version. Partner কোম্পানি SDK-র নির্দিষ্ট version চায় (যেমন 2.3.0), আর আমাদের পুরো কোড তারা দেখতে পারবে না। তাই SDK আলাদা public repository, package হিসেবে প্রকাশ।
  7. ফল: hybrid. মূল কোড একটি monorepo-তে, CODEOWNERS আর affected build-সহ; Android আর SDK আলাদা repository. লক্ষ করুন, submodule লাগল না — SDK ব্যবহার হয় version-এর package হিসেবে। এই তথ্যগুলো বদলালে (ধরুন contractor চলে গেলেন) সিদ্ধান্তও আবার দেখা দরকার।

৩ · Monorepo-তে শুধু যা বদলেছে তা build করা

Monorepo-র সবচেয়ে বড় খরচ CI. প্রতিটি pull request-এ সব project build আর test করলে repository যত বড় হয়, অপেক্ষা তত লম্বা। সমাধানের মূল ধারণা সহজ: প্রতিটি project কোন project-এর ওপর নির্ভর করে, সেই dependency graph লিখে রাখুন। কোনো file বদলালে তার মালিক project, আর যারা সেই project-এর ওপর সরাসরি বা পরোক্ষভাবে নির্ভর করে — শুধু তাদেরই build আর test করুন। এর জন্য graph-এর তীর উল্টে দিয়ে হাঁটতে হয়: “কে আমাকে ব্যবহার করে?” — একে বলে reverse dependency.

রেলের সময়সূচির কথা ভাবুন। কমলাপুর থেকে ছাড়া একটি ট্রেন এক ঘণ্টা দেরি করলে কোন কোন স্টেশনের যাত্রীকে খবর দিতে হবে? সব স্টেশনকে নয় — শুধু সেই ট্রেনের পরের স্টেশনগুলোকে, আর যে সংযোগ-ট্রেন এই ট্রেনের যাত্রীর জন্য অপেক্ষা করে, তার পরের স্টেশনগুলোকেও। Affected build ঠিক এই খোঁজ: বদলের পরে “কে কে ক্ষতিগ্রস্ত” তা তীর ধরে ধরে ছড়িয়ে দেওয়া।

বদলেছে: libs/auth/token.js, docs/intro.md · reverse edge: money→payments, shop · auth→payments, catalog · ui-kit→shop, admin · catalog→admin
এখন যা হচ্ছে
queue
এ পর্যন্ত affected
বাদ
  1. মালিক খোঁজা। প্রতিটি বদলানো file-এর path যে project folder দিয়ে শুরু হয়, সেটিই মালিক (একাধিক মিললে সবচেয়ে লম্বা folder). libs/auth/token.js-এর মালিক libs/auth; docs/intro.md কোনো project-এর নয়, তাই এই নিয়মে কিছু build হবে না। libs/auth queue-তে গেল।
  2. libs/auth-কে কে ব্যবহার করে? Reverse edge বলে: services/payments আর services/catalog. দুটিই নতুন, তাই affected তালিকায় আর queue-তে যোগ হলো।
  3. services/payments. একে কোনো project ব্যবহার করে না (এটি একটি service, library নয়), তাই নতুন কিছু যোগ হলো না।
  4. services/catalog. web/admin এর ওপর নির্ভর করে — এটিই পরোক্ষ নির্ভরতা: admin সরাসরি auth ব্যবহার না করলেও catalog-এর মাধ্যমে প্রভাবিত। যোগ হলো।
  5. web/admin. একে কেউ ব্যবহার করে না। Queue খালি — খোঁজ শেষ। প্রতিটি project একবারই queue-তে ঢোকে, তাই graph-এ চক্র থাকলেও খোঁজ থামে।
  6. ফল। ৭টির মধ্যে ৪টি project build আর test হবে। প্রতিটির build আর test-এ ৬ মিনিট লাগলে আর একটার পর একটা চললে, সব build করতে ৭ × ৬ = ৪২ মিনিট, affected build-এ ৪ × ৬ = ২৪ মিনিট। আর web/shop-এর মতো project, যার কোনো input বদলায়নি, cache থেকে আগের ফল নিতে পারে।

একই হিসাব একটি ছোট program-এ, তিনটি বদলের জন্য। শেষ উদাহরণে একটি শেয়ার করা file আছে — package-lock.json সবার dependency ঠিক করে, তাই নিরাপদ নিয়ম হলো সব project build করা।

// Affected projects in a monorepo: changed files -> owning projects -> every
// project that depends on them, directly or indirectly (reverse dependency BFS).
const deps = {                       // project: the projects it depends on
  "libs/money":        [],
  "libs/auth":         [],
  "libs/ui-kit":       [],
  "services/payments": ["libs/money", "libs/auth"],
  "services/catalog":  ["libs/auth"],
  "web/shop":          ["libs/ui-kit", "libs/money"],
  "web/admin":         ["libs/ui-kit", "services/catalog"],
};
const GLOBAL = ["package-lock.json", "ci/pipeline.yml"]; // shared: affects everyone
const users = {};                    // reverse edges: project -> its dependants
for (const [p, ds] of Object.entries(deps)) for (const d of ds) (users[d] ??= []).push(p);
const owner = f => Object.keys(deps).filter(p => f.startsWith(p + "/"))
                     .sort((a, b) => b.length - a.length)[0];   // longest prefix wins
function affected(changed) {
  if (changed.some(f => GLOBAL.includes(f))) return Object.keys(deps).map(p => p.padEnd(18) + " <- shared file");
  const why = new Map(), queue = [];
  for (const f of changed) { const p = owner(f);
    if (!p) { console.log("  (no project owns " + f + ")"); continue; }
    if (!why.has(p)) { why.set(p, "changed"); queue.push(p); } }
  while (queue.length) { const p = queue.shift();            // breadth-first
    for (const u of users[p] ?? []) if (!why.has(u)) { why.set(u, "<- " + p); queue.push(u); } }
  return [...why].map(([p, w]) => p.padEnd(18) + " " + w);
}
for (const change of [["libs/auth/token.js", "docs/intro.md"],
                      ["web/shop/cart.js"], ["libs/ui-kit/button.css", "package-lock.json"]]) {
  console.log("changed: " + change.join(", "));
  for (const line of affected(change)) console.log("  build+test " + line);
}

আউটপুট:

changed: libs/auth/token.js, docs/intro.md
  (no project owns docs/intro.md)
  build+test libs/auth          changed
  build+test services/payments  <- libs/auth
  build+test services/catalog   <- libs/auth
  build+test web/admin          <- services/catalog
changed: web/shop/cart.js
  build+test web/shop           changed
changed: libs/ui-kit/button.css, package-lock.json
  build+test libs/money         <- shared file
  build+test libs/auth          <- shared file
  build+test libs/ui-kit        <- shared file
  build+test services/payments  <- shared file
  build+test services/catalog   <- shared file
  build+test web/shop           <- shared file
  build+test web/admin          <- shared file

প্রথম বদলের ফল ঠিক stepper-এর মতো: চারটি project, আর web/admin-এর কারণ <- services/catalog — পরোক্ষ পথ। দ্বিতীয় বদলে শুধু web/shop, কারণ এটি কারও dependency নয়; এখানে বাকি ছয়টি project-এর জন্য CI-এর সময় পুরোটাই বাঁচল। তৃতীয় বদলে lockfile থাকায় সাতটিই — ui-kit-এর বদল একা থাকলে হতো শুধু তিনটি (ui-kit, shop, admin). এই model-এর দুর্বলতাও আছে: মানুষ যদি deps হাতে লিখে রাখেন আর কেউ নতুন import যোগ করে লিখতে ভোলেন, তাহলে ভুল project বাদ পড়বে। বাস্তবের tool তাই graph সাধারণত build file বা import থেকে নিজেই বের করে।

Build system আর cache — tool-এর ধরন

Monorepo-র জন্য বানানো build system-গুলো এই কাজটিই বড় পরিসরে করে, সাথে আরেকটি কৌশল: build cache. কোনো কাজের (compile, test) সব input — source file, dependency-র version, command, setting — এর একটি hash বানানো হয়; একই hash আগে কোথাও চলে থাকলে আবার না চালিয়ে আগের ফল নেওয়া হয়, এমনকি দলের শেয়ার করা remote cache থেকেও। কয়েকটি পরিচিত নাম: Bazel (Google-এর ভেতরের Blaze থেকে ২০১৫ সালে open source হয় বলে জানা যায়), JavaScript জগতে Nx আর Turborepo, আরও অনেক। এগুলোর ক্ষমতা, ভাষার সমর্থন আর শেখার খরচ খুব আলাদা, আর দ্রুত বদলায় — এখানে কোনোটির সুপারিশ নয়। বাছাইয়ের সময় প্রশ্নগুলো হলো: আপনার ভাষা সমর্থন করে কি, graph নিজে বের করে কি, remote cache কোথায় থাকবে, আর দলের কেউ এটি রক্ষণাবেক্ষণ করতে পারবেন কি।

Cache-এর ফল ততটাই বিশ্বাসযোগ্য, যতটা input-এর তালিকা সম্পূর্ণ। কোনো test যদি গোপনে বর্তমান সময়, network বা কোনো environment variable-এর ওপর নির্ভর করে, আর সেটি hash-এ না থাকে, তাহলে cache পুরোনো “pass” ফিরিয়ে দিতে পারে, যদিও আসলে test ভাঙা। এজন্য এসব tool build-কে sandbox-এ, শুধু ঘোষিত input দিয়ে চালানোর চেষ্টা করে। CI pipeline-এর বিস্তারিত পর্ব ৫৮-এ।

৪ · বড় repository-তে Git — shallow, partial আর sparse clone

পর্ব ৭-এ দেখেছেন, Git distributed: সাধারণ clone প্রতিটি commit, প্রতিটি folder-এর তালিকা (tree) আর প্রতিটি file-এর প্রতিটি version (blob) নামিয়ে আনে। ছোট project-এ এটিই শক্তি — internet ছাড়াই পুরো ইতিহাস। কিন্তু বহু বছরের বড় repository-তে প্রথম clone-এ অনেক সময় আর জায়গা লাগে, অথচ বেশিরভাগ মানুষ কাজ করেন শুধু সর্বশেষ অবস্থা নিয়ে। Git তাই তিনটি আলাদা জিনিস বাদ দেওয়ার উপায় দেয়:

Shallow clone · --depth

পুরোনো commit বাদ। শুধু শেষ কয়েকটি commit আর সেগুলোর snapshot. ইতিহাসের প্রশ্নের (log, blame, merge-base) উত্তর অসম্পূর্ণ।

CI-এর একবারের build

Partial clone · --filter

ইতিহাসের সব commit আর tree আসে, কিন্তু file-এর content (blob) আসে দরকারের সময়। পুরোনো version চাইলে তখন server থেকে আনা হয়।

বড় repository-তে developer

Sparse checkout

Repository-র সব কিছু থাকতে পারে, কিন্তু working tree-তে শুধু বেছে নেওয়া folder-গুলো খোলা হয়। প্রায়ই partial clone-এর সাথে।

Monorepo-র একটি অংশে কাজ

পরের transcript-গুলোর জন্য একটি scratch monorepo বানানো হয়েছে: root-এ README.md আর চারটি folder-এ (services/payments, services/catalog, web, docs) একটি করে file; ছয়টি commit, প্রতিটিতে চারটি file-ই বদলায়। মোট object: ৬টি commit, ৩৬টি tree আর ২৫টি blob (চার file × ছয় version, আর README-র একটি) — ৬৭টি। এটি একটি local bare repository mono.git-এ রাখা, যা এখানে “server”-এর ভূমিকায়। এই পর্বের সব transcript Git ২.৫৩-এ (Windows) সত্যিই চালানো; লেখক Rahim, তারিখ নির্দিষ্ট (১৫ জানুয়ারি ২০২৬, সকাল ১০টা থেকে এক মিনিট করে বাড়ানো), আর core.autocrlf=false. লম্বা scratch path ছোট করে …/ লেখা হয়েছে।

$ git clone -q mono.git full
$ git -C full count-objects -v | grep -E '^(count|in-pack)'
count: 67
in-pack: 0
$ git clone --depth 1 mono.git shallow
Cloning into 'shallow'...
warning: --depth is ignored in local clones; use file:// instead.
done.
$ git clone --depth 1 file://$PWD/mono.git shallow
Cloning into 'shallow'...
$ cd shallow
$ git log --oneline --decorate
6799182 (grafted, HEAD -> main, origin/main, origin/HEAD) Release step 6
$ git rev-parse --is-shallow-repository
true
$ git rev-list --count HEAD
1
$ git count-objects -v | grep -E '^(count|in-pack)'
count: 0
in-pack: 12
$ git fetch -q --deepen 2
$ git log --oneline
6799182 Release step 6
335d255 Release step 5
1009df1 Release step 4
$ git fetch -q --unshallow
$ git rev-list --count HEAD
6

কয়েকটি শিক্ষা। এক: পূর্ণ clone-এ ৬৭টি object — সবই। (Local path থেকে clone-এ Git object-গুলো hard link বা কপি করে আনে, তাই সব “loose”, in-pack: 0.) দুই: সাধারণ path দিলে --depth চুপচাপ নয়, warning দিয়ে উপেক্ষা হয় — local clone-এ Git object কপি করে, network-এর protocol চালায় না। file:// URL দিলে আসল protocol চলে, আর shallow কাজ করে। তিন: grafted লেখাটি বলছে এই commit-এর parent আছে, কিন্তু এখানে নেই — ইতিহাস এখানে কাটা। মাত্র ১২টি object: ১টি commit, ৬টি tree, ৫টি blob. চার: পরে দরকার হলে --deepen দিয়ে আরও commit, আর --unshallow দিয়ে পুরো ইতিহাস আনা যায়। আরেকটি কথা: documentation অনুযায়ী --depth নিজে থেকে --single-branch-ও বোঝায় — অন্য branch আসে না।

অনেক CI service-এ repository নামানোর ধাপটি ডিফল্টভাবে shallow (platform-ভেদে আলাদা, লেখার সময়ে প্রায়ই depth 1). তখন যে কাজ ইতিহাস চায়, সেগুলো ভুল ফল দেয় বা ব্যর্থ হয়: git describe দিয়ে version বের করা (tag আগের commit-এ থাকলে পাওয়া যায় না), পর্ব ১৮-এর মতো commit থেকে changelog বানানো, দুই branch-এর merge-base খোঁজা, বা আগের অংশের affected build-এর জন্য “main-এর সাথে কী পার্থক্য” বের করা। এমন job-এ ইতিহাসের গভীরতা বাড়ান বা --unshallow করুন।

Partial clone — ইতিহাস আছে, content পরে

$ git clone --filter=blob:none file://$PWD/mono.git partial
Cloning into 'partial'...
warning: filtering not recognized by server, ignoring
$ git -C mono.git config uploadpack.allowFilter true
$ git clone --filter=blob:none file://$PWD/mono.git partial
Cloning into 'partial'...
$ cd partial
$ git config --get remote.origin.promisor
true
$ git config --get remote.origin.partialclonefilter
blob:none
$ git rev-list --objects --all --missing=print | grep -c '^?'
20
$ git checkout -q HEAD~3
$ git rev-list --objects --all --missing=print | grep -c '^?'
16

প্রথম চেষ্টায় server (এখানে local mono.git) filter বোঝেনি, তাই Git warning দিয়ে সাধারণ পূর্ণ clone করেছে। Server-এর দিকে uploadpack.allowFilter চালু করার পর partial clone হলো। বড় hosting platform-গুলো partial clone সমর্থন করে বলে জানা যায়; নিজের server-এ এই setting লাগে। Clone-এর পর origin একটি promisor remote — Git জানে, যা নেই তা ওখান থেকে চাওয়া যাবে। --missing=print অনুপস্থিত object-এর আগে ? বসায়: ২৫টি blob-এর মধ্যে ২০টি নেই — শুধু checkout-এর ৫টি এসেছে, আর সব ৬টি commit ও ৩৬টি tree আছে। HEAD~3-এ যেতেই Git চুপচাপ ৪টি blob নামিয়ে আনল (README একই, তাই ৪টি), অনুপস্থিত এখন ১৬টি। এর দাম: পুরোনো version দেখার প্রতিটি কাজে network লাগে, আর server বন্ধ থাকলে সেই কাজ ব্যর্থ হয়।

আরেকটি রূপ --filter=tree:0 (treeless clone) tree-ও পরে আনে; documentation আর hosting platform-এর লেখায় এটিকে সাধারণত একবারের CI build-এর জন্য উপযোগী বলা হয়, রোজকার developer-এর জন্য নয়, কারণ git log -- path-এর মতো কাজে বারবার tree আনতে হয়। এবার একটি model দিয়ে বড় repository-তে তিন রকম clone-এর আকার তুলনা করি।

// Clone-size model. Sizes are raw (no zlib, no delta), so real packs are smaller.
function mulberry32(a) { return function () { a |= 0; 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(19);
const FILES = 400, COMMITS = 1500, CHANGED = 3;  // 3 files change in each commit
const SNAPSHOT_TREES_KB = 900;   // every tree object of one full snapshot
const META_KB = 6;               // one later commit: commit object + changed trees
let size = Array.from({ length: FILES }, () => 2 + Math.floor(rnd() * 198)); // KB
let allBlobsKB = size.reduce((a, b) => a + b, 0);  // commit 1 stores every file
for (let c = 2; c <= COMMITS; c++) {
  for (let k = 0; k < CHANGED; k++) {
    const f = Math.floor(rnd() * FILES);
    size[f] = Math.max(1, Math.round(size[f] * (0.9 + rnd() * 0.3)));
    allBlobsKB += size[f];                          // each new version = a new blob
  }
}
const headKB = size.reduce((a, b) => a + b, 0);
const allMetaKB = SNAPSHOT_TREES_KB + (COMMITS - 1) * META_KB;
const rows = [
  ["full clone",                 allMetaKB + allBlobsKB],
  ["shallow --depth 1",          SNAPSHOT_TREES_KB + headKB],
  ["partial --filter=blob:none", allMetaKB + headKB],
];
const MB = kb => (kb / 1024).toFixed(1).padStart(7) + " MB";
console.log(`${COMMITS} commits, ${FILES} files, ${FILES + (COMMITS - 1) * CHANGED} blob versions`);
console.log(`HEAD snapshot blobs: ${MB(headKB)}   all blobs: ${MB(allBlobsKB)}`);
for (const [name, kb] of rows)
  console.log(name.padEnd(27) + MB(kb) + `  (${(100 * kb / rows[0][1]).toFixed(1)}% of full)`);

আউটপুট:

1500 commits, 400 files, 4897 blob versions
HEAD snapshot blobs:    69.4 MB   all blobs:   671.5 MB
full clone                   681.2 MB  (100.0% of full)
shallow --depth 1             70.3 MB  (10.3% of full)
partial --filter=blob:none    79.0 MB  (11.6% of full)

Model-টি ইচ্ছে করে সরল: ৪০০টি file, প্রতিটি commit-এ এলোমেলোভাবে (seed 19-এর mulberry32 PRNG, তাই প্রতিবার একই ফল) তিনটি file বদলায়, আর প্রতিটি নতুন version একটি নতুন blob. Blob-এর version ৪০০ + ১৪৯৯ × ৩ = ৪৮৯৭টি। বর্তমান snapshot মাত্র ৬৯.৪ MB, অথচ ইতিহাসের সব blob মিলিয়ে ৬৭১.৫ MB — বড় repository-র ভার আসে ইতিহাস থেকে। Shallow আর partial-এর পার্থক্য মাত্র ৮.৭ MB: সেটিই ১৪৯৯টি পুরোনো commit-এর metadata (১৪৯৯ × ৬ KB). অর্থাৎ ইতিহাসের কাঠামো সস্তা, content দামি — partial clone-এর পুরো যুক্তি এটাই। বাস্তবে Git object zlib দিয়ে compress আর পরস্পরের delta হিসেবে packfile-এ রাখে (পর্ব ৫২), তাই আসল সংখ্যা অনেক ছোট হবে; তবে তুলনার ধরনটা একই থাকে।

Sparse checkout — শুধু নিজের folder

$ git clone -q --filter=blob:none --sparse file://$PWD/mono.git sparse
$ cd sparse
$ ls
README.md
$ git sparse-checkout set services/payments
$ ls -R services | head -6
services:
payments

services/payments:
app.txt
$ git sparse-checkout list
services/payments

--sparse দিয়ে clone করলে শুরুতে শুধু root-এর file (README.md) খোলা হয়। git sparse-checkout set services/payments “cone mode”-এ (আধুনিক Git-এ ডিফল্ট) পুরো folder ধরে নিয়ম বানায়: ওই folder-এর সব কিছু, আর পথের ওপরের folder-গুলোর সরাসরি file. services/catalog, web, docs working tree-তে নেই, তবু ইতিহাসে আছে। Partial clone-এর সাথে মিলিয়ে নেওয়ায় অন্য folder-এর blob নামানোও হয়নি। Sparse checkout, git worktree আর এগুলোর ফাঁদ বিস্তারিত পর্ব ৫৬-এ।

রোজকার গতি — git maintenance আর scalar

বড় repository-তে সময়ের সাথে অসংখ্য ছোট object file আর অনেকগুলো pack জমে, ফলে git log বা fetch ধীর হয়। git maintenance (Git ২.২৯-৩০, ২০২০ সালের দিকে আসা) এই পরিষ্কারের কাজগুলো — commit-graph file লেখা, loose object গুছিয়ে pack করা, background-এ আগাম fetch — নির্দিষ্ট সময়ে নিজে থেকে চালাতে পারে:

$ git maintenance start        # register this repo and schedule background tasks
$ git maintenance run --task=commit-graph   # run one task now
$ git maintenance stop         # unregister

এখানে আউটপুট নেই, কারণ start আপনার global config-এ repository-টি নিবন্ধন করে আর OS-এর scheduler-এ (Windows-এ Task Scheduler, Linux-এ সাধারণত cron বা systemd timer, macOS-এ launchd) কাজ বসায় — এই পর্বের scratch পরীক্ষায় আমরা তা চালাইনি। Scalar হলো Microsoft-এর বড় repository-র অভিজ্ঞতা থেকে আসা একটি সহায়ক command, যা Git ২.৩৮ (২০২২) থেকে Git-এর সাথেই আসে বলে release note-এ লেখা: scalar clone একসাথে partial clone, sparse checkout, background maintenance আর বড় repository-র জন্য সুপারিশ করা কিছু setting চালু করে। Windows আর macOS-এ file-system monitor (core.fsmonitor) git status-কে দ্রুত করে। এগুলো সবই বিকল্প — ছোট project-এ এসবের কোনো দরকার নেই।

৫ · Submodule — অন্য repository-র একটি নির্দিষ্ট commit

এবার polyrepo-র দিকে ফিরি। একটি repository-র ভেতরে অন্য repository-র কোড দরকার, কিন্তু সেটি নিজের ইতিহাস আর নিজের দল নিয়ে আলাদাই থাকবে — যেমন একাধিক app-এর শেয়ার করা UI kit, বা কোনো hardware vendor-এর firmware library. Git-এর পুরোনো উত্তর submodule (২০০৭ সালের দিকে Git-এ আসে)। যে repository-র ভেতরে submodule থাকে, তাকে বলে superproject.

মূল ধারণা

Superproject submodule-এর কোনো file রাখে না। রাখে মাত্র দুটি জিনিস: (১) নিজের tree-তে একটি বিশেষ entry — mode 160000, type commit, আর submodule-এর একটি নির্দিষ্ট commit-এর hash (gitlink); (২) .gitmodules file-এ path আর URL, যাতে অন্যরা জানে কোথা থেকে clone করতে হবে। অর্থাৎ submodule একটি pin — “ui-kit, ঠিক এই commit-এ।” এটি branch অনুসরণ করে না।

Scratch setup: ui-kit.git একটি bare repository, তাতে দুটি commit (6e47256 “Add button style”, aa11f68 “Add card style”). shop.git আরেকটি bare repository, আর shop তার clone, একটি commit (5c36c98). দুটি bare repository একই folder-এ, তাই relative URL ../ui-kit.git কাজ করে — relative URL superproject-এর origin-এর সাপেক্ষে পড়া হয়।

$ git submodule add ../ui-kit.git libs/ui-kit
Cloning into '…/work/shop/libs/ui-kit'...
fatal: transport 'file' not allowed
fatal: clone of '…/work/ui-kit.git' into submodule path '…/work/shop/libs/ui-kit' failed
$ git -c protocol.file.allow=always submodule add ../ui-kit.git libs/ui-kit
Cloning into '…/work/shop/libs/ui-kit'...
done.
$ cat .gitmodules
[submodule "libs/ui-kit"]
	path = libs/ui-kit
	url = ../ui-kit.git
$ git status --short
A  .gitmodules
A  libs/ui-kit
$ git ls-files --stage libs/ui-kit
160000 aa11f68fa596ae0a5dd65c17c94ae23e045a971d 0	libs/ui-kit
$ git commit -q -m 'Add ui-kit as a submodule'
$ git ls-tree HEAD libs/
160000 commit aa11f68fa596ae0a5dd65c17c94ae23e045a971d	libs/ui-kit
$ git submodule status
 aa11f68fa596ae0a5dd65c17c94ae23e045a971d libs/ui-kit (heads/main)
$ git log --oneline
0dd4953 Add ui-kit as a submodule
5c36c98 Initial shop app

প্রথম চেষ্টাই ব্যর্থ — আর সেটি ইচ্ছাকৃত নিরাপত্তা। অক্টোবর ২০২২-এর নিরাপত্তা-release-এ (Git ২.৩৮.১ আর পুরোনো শাখাগুলোর patch) protocol.file.allow-এর ডিফল্ট হয় user: আপনি নিজে সরাসরি git clone /path দিলে local path চলবে, কিন্তু submodule-এর মতো যে clone Git নিজে থেকে করে, সেখানে file protocol বন্ধ। নিরাপত্তা-ঘোষণা (CVE-2022-39253) অনুযায়ী কারণটা ছিল: একটি ক্ষতিকর repository তার submodule-এর URL হিসেবে আপনার নিজের কম্পিউটারের একটি path দিয়ে, symbolic link-এর কৌশলে, আপনার কম্পিউটারের file নিজের clone-এ টেনে আনতে পারত — শুধু --recurse-submodules দিয়ে clone করলেই। বাস্তবের submodule সাধারণত https:// বা SSH URL-এ থাকে, সেখানে এই সমস্যা নেই। এখানে local পরীক্ষার জন্য -c protocol.file.allow=always শুধু ওই একটি command-এর জন্য দেওয়া হয়েছে; global config-এ স্থায়ীভাবে always বসানো উচিত নয়।

তারপর লক্ষ করুন: .gitmodules-এ শুধু path আর URL; index-এ libs/ui-kit একটি file নয়, mode 160000-এর একটি entry, যার hash ui-kit-এর commit aa11f68. Commit-এর পর tree-তেও একই: 160000 commit aa11f68…. git submodule status-এর লাইনের শুরুর ফাঁকা ঘর মানে “checkout করা commit আর superproject-এর রেকর্ড মিলছে”; বন্ধনীর অংশটি git describe-এর উত্তর, ওই commit-এ কোন ref আছে তার ইঙ্গিত।

অন্য কেউ clone করলে

$ git clone -q shop.git shop-a
$ cd shop-a
$ ls libs/ui-kit | wc -l
0
$ git submodule status
-aa11f68fa596ae0a5dd65c17c94ae23e045a971d libs/ui-kit
$ git -c protocol.file.allow=always submodule update --init --recursive
Submodule 'libs/ui-kit' (…/work/ui-kit.git) registered for path 'libs/ui-kit'
Cloning into '…/work/shop-a/libs/ui-kit'...
done.
Submodule path 'libs/ui-kit': checked out 'aa11f68fa596ae0a5dd65c17c94ae23e045a971d'
$ git submodule status
 aa11f68fa596ae0a5dd65c17c94ae23e045a971d libs/ui-kit (heads/main)
$ cd ..
$ git -c protocol.file.allow=always clone --recurse-submodules shop.git shop-b
Cloning into 'shop-b'...
done.
Submodule 'libs/ui-kit' (…/work/ui-kit.git) registered for path 'libs/ui-kit'
Cloning into '…/work/shop-b/libs/ui-kit'...
done.
Submodule path 'libs/ui-kit': checked out 'aa11f68fa596ae0a5dd65c17c94ae23e045a971d'
$ cd shop-b
$ git -C libs/ui-kit status | head -1
HEAD detached at aa11f68

সাধারণ clone-এ submodule-এর folder তৈরি হয়, কিন্তু খালি (০টি file). git submodule status-এর শুরুতে - মানে “initialize হয়নি”। নতুনদের সবচেয়ে পরিচিত বিভ্রান্তি এটাই — “build হচ্ছে না, ui-kit folder খালি!” সমাধান git submodule update --init --recursive: --init URL-টি .gitmodules থেকে নিজের .git/config-এ লেখে, তারপর clone করে ঠিক রেকর্ড করা commit checkout করে; --recursive submodule-এর ভেতরের submodule-ও একইভাবে আনে। অথবা শুরু থেকেই git clone --recurse-submodules. শেষ লাইনটি গুরুত্বপূর্ণ: submodule-এর ভেতরে HEAD একটি branch নয়, সরাসরি commit-এ — পর্ব ১২-এর detached HEAD. Pin মানে নির্দিষ্ট commit, তাই এটাই স্বাভাবিক। (প্রথম submodule add-এর পর shop-এ ui-kit ছিল main branch-এ — add নিজে clone করে branch-এ রাখে; পরে clone বা update করলে detached হয়।)

৬ · Submodule update আর পরিচিত ফাঁদ

ui-kit দল নতুন একটি commit push করল: cb99720 “Add badge style”. Shop-এ এটি নিজে থেকে আসবে না — pin এখনো aa11f68-এ। Pin সরানো দুই ধাপের কাজ: আগে submodule-এর ভেতরে নতুন commit checkout, তারপর superproject-এ নতুন pointer commit. Rahim কাজ করছেন shop-b clone-এ, যেখানে submodule detached:

$ cd libs/ui-kit
$ git status | head -1
HEAD detached at aa11f68
$ git pull
From …/work/ui-kit
   aa11f68..cb99720  main       -> origin/main
You are not currently on a branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

$ git fetch
$ git switch --detach origin/main
Previous HEAD position was aa11f68 Add card style
HEAD is now at cb99720 Add badge style
$ cd ../..
$ git status --short
 M libs/ui-kit
$ git submodule status
+cb99720ae131e7cbc0d1665223b22493d7302bc8 libs/ui-kit (remotes/origin/HEAD)
$ git diff
diff --git a/libs/ui-kit b/libs/ui-kit
index aa11f68..cb99720 160000
--- a/libs/ui-kit
+++ b/libs/ui-kit
@@ -1 +1 @@
-Subproject commit aa11f68fa596ae0a5dd65c17c94ae23e045a971d
+Subproject commit cb99720ae131e7cbc0d1665223b22493d7302bc8
$ git diff --submodule=log
Submodule libs/ui-kit aa11f68..cb99720:
  > Add badge style
$ git add libs/ui-kit
$ git commit -q -m 'Bump ui-kit to add badge style'
$ git submodule status
 cb99720ae131e7cbc0d1665223b22493d7302bc8 libs/ui-kit (remotes/origin/HEAD)
$ git push -q origin main

Detached HEAD-এ git pull fetch করে, কিন্তু merge করতে পারে না — কোন branch-এ merge হবে তা নেই। তাই পথ হলো fetch আর তারপর switch --detach origin/main (বা ভেতরে একটি branch বানিয়ে কাজ)। এরপর superproject-এর git status দেখায় M libs/ui-kit, আর submodule status-এর শুরুতে +: checkout করা commit superproject-এর index-এর রেকর্ডের সাথে মেলে না। সাধারণ git diff শুধু দুটি hash দেখায় — Subproject commit; --submodule=log মাঝের commit-গুলোর message দেখায়, review-এর জন্য অনেক কাজের (স্থায়ীভাবে চাইলে diff.submodule=log config). git add libs/ui-kit file নয়, নতুন commit id stage করে; commit-এর পর আবার শুরুতে ফাঁকা ঘর।

এক ধাপে একই কাজ: git submodule update --remote libs/ui-kit submodule-এর remote থেকে fetch করে tracking branch-এর (ডিফল্টে remote-এর HEAD; .gitmodules-এ branch = দিয়ে বদলানো যায়) সর্বশেষ commit checkout করে। ui-kit-এ আরও একটি commit (ab1bba7 “Add alert style”) আসার পর অন্য clone shop-এ:

$ git submodule update --remote libs/ui-kit
fatal: transport 'file' not allowed
fatal: Unable to fetch in submodule path 'libs/ui-kit'
$ git -c protocol.file.allow=always submodule update --remote libs/ui-kit
From …/work/ui-kit
   cb99720..ab1bba7  main       -> origin/main
Submodule path 'libs/ui-kit': checked out 'ab1bba7148c680202dbef10a29b9835f2b7fc19c'
$ git submodule status
+ab1bba7148c680202dbef10a29b9835f2b7fc19c libs/ui-kit (remotes/origin/HEAD)
$ git status --short
 M libs/ui-kit
$ git diff --submodule=log
Submodule libs/ui-kit cb99720..ab1bba7:
  > Add alert style
$ git submodule foreach 'git log --oneline -1'
Entering 'libs/ui-kit'
ab1bba7 Add alert style

এখানেও pointer commit হয়নি — --remote শুধু checkout করে; review করে git add আর commit আপনার কাজ। git submodule foreach প্রতিটি submodule-এর ভেতরে একটি command চালায়। এবার পুরো চক্রটি ধাপে ধাপে, superproject আর submodule-এর তিনটি জায়গা পাশাপাশি রেখে:

shop superproject · submodule libs/ui-kit · আসল Git ২.৫৩ hash
Command
.gitmodules
Superproject-এর রেকর্ড (index / HEAD)
Submodule-এ checkout
git submodule status
  1. Submodule যোগ। Git ui-kit clone করে libs/ui-kit-এ রাখল, .gitmodules বানাল, আর index-এ একটি gitlink (mode 160000) বসাল, hash aa11f68. এখনো commit হয়নি। Checkout আর index মেলে, তাই status-এর শুরু ফাঁকা।
  2. Commit. Commit 0dd4953-এর tree-তে এখন 160000 commit aa11f68. Superproject-এর ইতিহাস বলছে: “এই commit-এ shop চলে ui-kit-এর aa11f68 দিয়ে।”
  3. অন্য কম্পিউটারে সাধারণ clone. Commit-এ pin আছে, কিন্তু submodule-এর কোড আসেনি: folder খালি, status-এর শুরুতে - (initialize হয়নি)।
  4. update --init. URL নিবন্ধন, clone, আর ঠিক aa11f68 checkout — branch-এর সর্বশেষ নয়। HEAD detached. Status আবার ফাঁকা ঘরে।
  5. ui-kit এগোল। ভেতরে fetch করে cb99720-এ যাওয়া হলো। Superproject-এর রেকর্ড এখনো aa11f68, তাই status-এ + আর git status-এ M libs/ui-kit. এখন commit না করলে অন্য কেউ এই বদল পাবেন না।
  6. Pointer commit. git add libs/ui-kit নতুন hash stage করল; commit 6b39491 “Bump ui-kit to add badge style”. Push করলে এখন থেকে সবাই cb99720 পাবেন — যদি update চালান।
  7. সহকর্মীর ফাঁদ। তিনি git pull করলেন: superproject-এর রেকর্ড cb99720-এ এল, কিন্তু submodule-এর checkout পুরোনো aa11f68-এ রয়ে গেল। Status-এ +aa11f68, আর git status-এ “modified”. এ অবস্থায় git commit -a করলে তিনি ভুলবশত pin পেছনে aa11f68-এ ফিরিয়ে দেবেন!
  8. সমাধান। git submodule update রেকর্ড করা cb99720 checkout করল; সব মিলে গেল। Git ২.৫৩-এ পরীক্ষা করে দেখা গেছে, git pull --recurse-submodules দিয়ে এক ধাপেই একই ফল পাওয়া যায়।

তিনটি জায়গার এই হিসাব একটি ছোট model-এ — সেই একই ক্রম, শুধু commit-এর নাম সরল (u1, u2), আর সহকর্মীর নাম Karim:

// Submodule pointer model: a superproject commit stores ONLY a commit id for
// libs/ui-kit (a gitlink). Three places can disagree: the checked-out submodule,
// the index of the superproject, and the superproject's last commit (HEAD).
const uiKit = ["u1", "u2", "u3"];                 // ui-kit history, oldest first
const sup = { commits: [], index: null, sub: null };
function status(who, s) {                          // like `git submodule status`
  const head = s.commits.at(-1)?.ptr ?? null;
  const mark = s.sub === null ? "-" : s.sub !== s.index ? "+" : " ";
  const st = s.index !== head ? "staged pointer change" :
             s.sub !== null && s.sub !== head ? "modified: libs/ui-kit (new commits)" : "clean";
  console.log(`${who.padEnd(7)} ${mark}${s.sub ?? s.index} libs/ui-kit   status: ${st}`);
}
function commit(s, msg) { s.commits.push({ msg, ptr: s.index }); }
function clone(s) {                                // clone + submodule update --init
  const ptr = s.commits.at(-1).ptr;
  return { commits: [...s.commits], index: ptr, sub: ptr };
}
sup.sub = sup.index = uiKit[0]; commit(sup, "Add ui-kit at u1"); status("rahim", sup);
sup.sub = uiKit[1];                                // checkout u2 inside the submodule
status("rahim", sup);
let karim = clone(sup);  status("karim", karim);   // Karim still gets u1!
sup.index = sup.sub;     status("rahim", sup);     // git add libs/ui-kit
commit(sup, "Bump ui-kit to u2"); status("rahim", sup);
karim.commits = [...sup.commits]; karim.index = sup.index;  // git pull, no update
status("karim", karim);
karim.sub = karim.index; status("karim", karim);   // git submodule update
console.log(sup.commits.map(c => `${c.msg} -> pins ${c.ptr}`).join("\n"));

আউটপুট:

rahim    u1 libs/ui-kit   status: clean
rahim   +u2 libs/ui-kit   status: modified: libs/ui-kit (new commits)
karim    u1 libs/ui-kit   status: clean
rahim    u2 libs/ui-kit   status: staged pointer change
rahim    u2 libs/ui-kit   status: clean
karim   +u1 libs/ui-kit   status: modified: libs/ui-kit (new commits)
karim    u2 libs/ui-kit   status: clean
Add ui-kit at u1 -> pins u1
Bump ui-kit to u2 -> pins u2

তৃতীয় লাইনটিই মূল শিক্ষা: Rahim-এর কম্পিউটারে submodule u2-তে, অথচ Karim clone করে পেলেন u1 — কারণ pointer commit হয়নি। Rahim-এর কাছে সব ঠিক দেখায় (“আমার মেশিনে তো চলে!”), Karim-এর কাছে পুরোনো ui-kit. চতুর্থ লাইনে git add-এর পর checkout আর index মেলে, তাই চিহ্ন ফাঁকা, কিন্তু HEAD-এর সাথে পার্থক্য আছে — staged. আসল Git-এর সাথে মিল: + চিহ্ন checkout-কে index-এর সাথে তুলনা করে (documentation অনুযায়ী), আর Karim-এর pull-এর পর +u1 ঠিক আগের transcript-এর +aa11f68-এর মতো। শেষ দুই লাইন দেখায় superproject-এর ইতিহাস আসলে pin-এর ইতিহাস।

সবচেয়ে কষ্টের ফাঁদ — submodule-এর commit push করা হয়নি

Rahim submodule-এর ভেতরে নিজেই একটি বদল করলেন — একটি branch-এ commit — তারপর superproject-এ pointer commit করে push. কিন্তু ui-kit-এর commit-টি push করতে ভুলে গেলেন:

$ cd libs/ui-kit
$ git switch -q -c fix-colors
$ printf '.btn { padding: 8px 16px; color: #0a5; }\n' > button.css
$ git commit -q -am 'Change button colour'
$ cd ../..
$ git add libs/ui-kit
$ git commit -q -m 'Use new button colour'
$ git push --recurse-submodules=check origin main
The following submodule paths contain changes that can
not be found on any remote:
  libs/ui-kit

Please try

	git push --recurse-submodules=on-demand

or cd to the path and use

	git push

to push them to a remote.

fatal: Aborting.
fatal: the remote end hung up unexpectedly
$ git push -q origin main
$ cd ..
$ git -c protocol.file.allow=always clone -q --recurse-submodules shop.git shop-c
fatal: git upload-pack: not our ref b62906ce25790258dfe830b4f0cac7d874fcaaba
fatal: remote error: upload-pack: not our ref b62906ce25790258dfe830b4f0cac7d874fcaaba
fatal: Fetched in submodule path 'libs/ui-kit', but it did not contain b62906ce25790258dfe830b4f0cac7d874fcaaba. Direct fetching of that commit failed.

--recurse-submodules=check push আটকে দিয়েছে: pointer যে commit দেখাচ্ছে (b62906c), সেটি ui-kit-এর কোনো remote-এ নেই। কিন্তু সাধারণ git push কোনো যাচাই ছাড়াই superproject push করে দিল। ফল: পরের যে কেউ clone করলে superproject পান, কিন্তু submodule-এর সেই commit কোথাও নেই — not our ref. যতক্ষণ না Rahim ui-kit-এর branch push করেন, দলের সবার build ভাঙা। প্রতিরোধ: git config push.recurseSubmodules check (বা on-demand, যা আগে submodule push করে), আর CI-তে --recurse-submodules দিয়ে clone, যাতে ভাঙা pin সাথে সাথে ধরা পড়ে।

ফাঁদলক্ষণপ্রতিকার
Clone-এর পর update করা হয়নিখালি folder, status-এ -git submodule update --init --recursive, বা clone --recurse-submodules
Pull-এর পর update করা হয়নিStatus-এ +, git status-এ modified; ভুল করে পুরোনো pin commit হয়ে যায়git pull --recurse-submodules; documentation অনুযায়ী submodule.recurse=true config অনেক command-কে নিজে থেকেই recurse করায়
Detached HEAD-এ কাজ করে commitCommit কোনো branch-এ নেই, পরের update-এ “হারিয়ে” যায় (reflog-এ থাকে)কাজ শুরুর আগে ভেতরে git switch -c <branch>
Submodule-এর commit push হয়নিঅন্যদের clone-এ not our refpush.recurseSubmodules=check বা on-demand
Review-তে শুধু দুটি hashকেউ বোঝেন না pin কেন সরলgit diff --submodule=log; commit message-এ কারণ লেখা
Submodule সরানোFolder মুছলেও .gitmodules আর config-এ রয়ে যায়আধুনিক Git-এ git rm libs/ui-kit gitlink আর .gitmodules-এর অংশ দুটোই সরায় (documentation অনুযায়ী); প্রয়োজনে আগে git submodule deinit

৭ · Subtree, vendoring আর package manager — বিকল্প পথ

Submodule-এর ঝামেলা দেখে অনেক দল অন্য পথ নেয়। প্রথম বিকল্প git subtree: অন্য repository-র কোড আর ইতিহাস সরাসরি নিজের repository-র একটি folder-এ merge করা। ফলে clone করা মানুষ সাধারণ git clone-এই সব পান — কোনো বিশেষ command লাগে না। Subtree Git-এর মূল অংশ নয়, contrib folder-এর একটি script (২০১২ সালের দিকে যুক্ত); Git for Windows-এ এটি থাকে, কিন্তু কিছু Linux distribution-এ আলাদা package-এ আসে বা থাকেই না — তাই git subtree -h দিয়ে আগে দেখে নিন।

$ git subtree add --prefix=vendor/ui-kit ../ui-kit.git main
git fetch ../ui-kit.git main
From ../ui-kit
 * branch            main       -> FETCH_HEAD
Added dir 'vendor/ui-kit'
$ git log --oneline --graph
*   e2992c2 Add 'vendor/ui-kit/' from commit 'ab1bba7148c680202dbef10a29b9835f2b7fc19c'
|\
| * ab1bba7 Add alert style
| * cb99720 Add badge style
| * aa11f68 Add card style
| * 6e47256 Add button style
* 707ca54 Start site
$ ls vendor/ui-kit
alert.css
badge.css
button.css
card.css
$ git subtree pull --prefix=vendor/ui-kit ../ui-kit.git main -m 'Update ui-kit subtree'
From ../ui-kit
 * branch            main       -> FETCH_HEAD
Merge made by the 'ort' strategy.
 vendor/ui-kit/modal.css | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 vendor/ui-kit/modal.css
$ git log --oneline --graph
*   539018d Update ui-kit subtree
|\
| * 9dfe5c5 Add modal style
* | e2992c2 Add 'vendor/ui-kit/' from commit 'ab1bba7148c680202dbef10a29b9835f2b7fc19c'
|\|
| * ab1bba7 Add alert style
| * cb99720 Add badge style
| * aa11f68 Add card style
| * 6e47256 Add button style
* 707ca54 Start site

এখানে কোনো protocol.file.allow লাগেনি — subtree সাধারণ git fetch চালায়, যা আপনি নিজে দিচ্ছেন। ফলটা দেখুন: ui-kit-এর চারটি commit, একই hash-সহ (6e47256 … ab1bba7), এখন এই repository-র ইতিহাসে, আর একটি merge commit তাদের vendor/ui-kit/ folder-এ বসিয়েছে (পর্ব ১৩-এর merge). File-গুলো সাধারণ file — 160000 pointer নেই। নতুন commit আনতে subtree pull আরেকটি merge করে। --squash দিলে বাইরের পুরো ইতিহাসের বদলে একটি “Squashed 'vendor/ui-kit/' content from commit …” commit আসে; আমাদের পরীক্ষায় তাতে graph-এ শুধু দুটি নতুন commit দেখা গেছে। নিজের বদল উল্টো দিকে ফেরত পাঠাতে git subtree push আছে, তবে সেটি ইতিহাস ঘেঁটে আলাদা করে, বড় repository-তে ধীর হতে পারে।

দ্বিতীয় বিকল্প vendoring: বাইরের কোডের একটি কপি সাধারণ file হিসেবে নিজের repository-তে রাখা (প্রায়ই vendor/ বা third_party/ folder-এ), সাথে কোথা থেকে কোন version এসেছে তার একটি নোট। Subtree আসলে ইতিহাসসহ vendoring. Go-র মতো কিছু ভাষায় tool নিজেই vendoring সমর্থন করে। এর দাম হলো আপডেট হাতে করতে হয়, আর লাইসেন্সের শর্ত মানতে হয় (license file রাখা, পরিবর্তন জানানো — পর্ব ৬৩)।

তৃতীয়, আর প্রায়ই সবচেয়ে সহজ: package manager. ui-kit তার নিজের repository থেকে version নম্বরসহ package হিসেবে প্রকাশিত হয় (npm, PyPI, Maven বা প্রতিষ্ঠানের নিজস্ব registry), আর shop-এর package.json-এ লেখা থাকে "@bazar/ui-kit": "^2.3.0"; ঠিক version লেখা থাকে lockfile-এ (পর্ব ১০)। এটি submodule-এর মতোই pin, কিন্তু commit hash-এর বদলে মানুষের পড়ার মতো SemVer নম্বর, release note আর পুরো ecosystem-এর tooling-সহ।

পথPin কীভাবেClone-এ বিশেষ ধাপভালো যখনখারাপ যখন
Monorepo-র folderPin নেই — সবাই সর্বশেষনাদল এক, বদল প্রায়ই একসাথেAccess আলাদা রাখতে হয়
SubmoduleCommit hash (gitlink)হ্যাঁ: --recurse-submodulesবাইরের repository-র ঠিক commit দরকার, package নেই (firmware, বড় C/C++ library)দলের অনেকে Git-এ নতুন; ঘনঘন দুই দিকেই বদল
SubtreeMerge-এর ইতিহাসে লেখানাব্যবহারকারীদের সহজ clone চাই, আপডেট কমদুই দিকে নিয়মিত বদল পাঠাতে হয়
VendoringFolder-এ কপি + নোটনাInternet বা registry ছাড়া build, খুব স্থির dependencyপ্রায়ই আপডেট; নিরাপত্তা-patch ধরা কঠিন
Package managerSemVer + lockfileনা (install ধাপ)বেশিরভাগ application-এর libraryদুই project একসাথে দ্রুত বদলায় — প্রতিবার release লাগে

৮ · Git LFS — বড় binary file

Design দলের একটি ৩০০ KB-র ছবি file দশবার বদলালে Git-এ দশটি আলাদা blob জমে — binary file-এর ক্ষেত্রে delta compression প্রায়ই খুব কাজে আসে না, কারণ সামান্য বদলেও পুরো file-এর byte বদলে যায়। আর অংশ ৪-এ দেখেছেন, সাধারণ clone প্রতিটি version সবার কম্পিউটারে নামায়। ৫০০ MB-র ভিডিও দশবার বদলালে প্রত্যেক নতুন সদস্যকে প্রথম দিনেই কয়েক GB নামাতে হয়, অথচ তাঁর দরকার শুধু সর্বশেষটি। তার ওপর এমন file merge করা যায় না, আর লাইনভিত্তিক diff অর্থহীন।

Git LFS (Large File Storage; ২০১৫ সালে GitHub ঘোষণা করে, এখন open source extension, আলাদা install করতে হয়) সমাধান করে একটি বদল দিয়ে: Git-এর ভেতরে আসল file-এর বদলে একটি ছোট text file — LFS pointer — রাখা হয়, আর আসল content যায় একটি আলাদা LFS server-এ। পর্ব ১০-এ দেখা .gitattributes-এর filter=lfs এই কাজ চালায়: git add-এর সময় “clean” filter file-কে pointer-এ বদলায় আর content .git/lfs/objects-এ রাখে; checkout-এর সময় “smudge” filter pointer দেখে আসল file বসায়, দরকার হলে server থেকে নামিয়ে।

$ git lfs install --local
Updated Git hooks.
Git LFS initialized.
$ git lfs track '*.psd'
Tracking "*.psd"
$ cat .gitattributes
*.psd filter=lfs diff=lfs merge=lfs -text
$ git add .gitattributes design/banner.psd
$ git commit -q -m 'Add banner design'
$ git lfs ls-files
230ed06df4 * design/banner.psd
$ git show HEAD:design/banner.psd
version https://git-lfs.github.com/spec/v1
oid sha256:230ed06df482a77672cd93b6d4024053b380b8563d3a939aa5d25574772ee479
size 300000
$ git cat-file -s HEAD:design/banner.psd
131
$ ls -l design/banner.psd | awk '{print $5}'
300000
$ git check-attr -a design/banner.psd
design/banner.psd: diff: lfs
design/banner.psd: merge: lfs
design/banner.psd: text: unset
design/banner.psd: filter: lfs

এটি git-lfs ৩.৭.১ দিয়ে সত্যিই চালানো; banner.psd একটি ৩,০০,০০০ byte-এর পরীক্ষামূলক binary file. --local দেওয়া হয়েছে যাতে hook আর filter শুধু এই repository-র config-এ যায় (সাধারণ ব্যবহারে প্রতি ব্যবহারকারী একবার git lfs install). git lfs track শুধু .gitattributes-এ একটি লাইন লেখে — তাই .gitattributes নিজেও commit করতে হয়, নইলে অন্যদের কম্পিউটারে নিয়ম পৌঁছাবে না। আসল প্রমাণ git show HEAD:…: Git-এর ভেতরে file-টি মাত্র তিন লাইনের pointer — spec-এর version, content-এর SHA-256 hash (oid) আর আসল আকার (size 300000). Git-এর blob মাত্র ১৩১ byte, অথচ working tree-তে file পুরো ৩,০০,০০০ byte. check-attr দেখায় -text (line ending রূপান্তর বন্ধ) আর diff, merge, filter তিনটিই lfs. Push করলে আসল content LFS server-এ যায় — সেই অংশটি এখানে চালানো হয়নি, কারণ local পরীক্ষায় কোনো LFS server নেই।

LFS অতীত বদলায় না। git lfs track শুধু ভবিষ্যতের git add-এ কাজ করে। আগে থেকে সাধারণ blob হিসেবে commit হওয়া বড় file ইতিহাসে থেকেই যায়, আর repository হালকা হয় না। পুরোনো ইতিহাস বদলাতে git lfs migrate-এর মতো command আছে, কিন্তু সেটি সব commit-এর hash বদলে দেয় — দলের সবাইকে নতুন করে clone করতে হয়, আর force push লাগে। এমন কাজ আগে একটি throwaway clone-এ পরীক্ষা করুন আর দলকে জানিয়ে করুন।

কয়েকটি বাস্তব কথা। এক: Hosting platform-গুলো সাধারণ Git file-এর আকারে সীমা রাখে (যেমন লেখার সময়ে GitHub-এর documentation অনুযায়ী ১০০ MB-এর বড় file push আটকে যায়), আর LFS-এর storage আর bandwidth-এর জন্য আলাদা কোটা থাকে, যার বেশি হলে খরচ লাগে — সীমা platform আর plan-ভেদে আলাদা, আর বদলায়, তাই নিজের platform-এর বর্তমান documentation দেখুন। CI প্রতিটি build-এ LFS file নামালে bandwidth দ্রুত ফুরাতে পারে; দরকার না হলে GIT_LFS_SKIP_SMUDGE=1 দিয়ে শুধু pointer রেখে clone করা যায়। দুই: যে file merge হয় না (design file, 3D model), সেখানে দুইজন একসাথে বদলালে একজনের কাজ ফেলে দিতে হয়; git lfs lock দিয়ে “আমি এখন এটি বদলাচ্ছি” জানিয়ে রাখা যায়, যদি server সমর্থন করে। তিন: LFS সবসময় উত্তর নয় — build output বা বড় dataset প্রায়ই Git-এর বাইরে artifact storage-এ রাখাই ভালো, আর repository-তে থাকে শুধু সেটি কোথায় আছে তার ঠিকানা আর hash.

আরও গভীরে: Gitlink-এর mode 160000 Git-এর tree format-এরই অংশ — একটি tree entry যা blob বা tree নয়, একটি commit-এর দিকে ইঙ্গিত করে, আর Git সেই commit নিজের object store-এ খোঁজে না; এজন্যই superproject submodule-এর ইতিহাস ছাড়াই বৈধ থাকে (blob, tree আর commit object বিস্তারিত পর্ব ৫২-এ)। Partial clone-এ যে remote থেকে বাকি object আনা যায়, Git তাকে “promisor” হিসেবে চিহ্নিত করে, আর সেখান থেকে আসা pack-এর পাশে একটি .promisor file রাখে, যাতে garbage collection বুঝতে পারে কোন অনুপস্থিত object “প্রতিশ্রুত”, কোনটি সত্যিই হারানো। বড় monorepo নিয়ে সবচেয়ে উদ্ধৃত লেখা Potvin আর Levenberg-এর ২০১৬ সালের CACM প্রবন্ধ; Git-এর দিক থেকে git help gitsubmodules, git help partial-clone (technical documentation) আর Git LFS-এর pointer spec মূল সূত্র।

পরীক্ষা ও ইন্টারভিউতে:

  • Monorepo-র দুটি সুবিধা আর দুটি অসুবিধা? — সুবিধা: একাধিক project-এ atomic change, শেয়ার করা tooling; অসুবিধা: বড় repository আর CI-এর খরচ, folder-ভিত্তিক access control কঠিন।
  • Superproject submodule-এর কী রাখে? — Tree-তে mode 160000-এর একটি gitlink (একটি commit hash) আর .gitmodules-এ path ও URL; কোনো file নয়।
  • Shallow আর partial clone-এর পার্থক্য? — Shallow পুরোনো commit বাদ দেয় (ইতিহাস কাটা); partial সব commit আর tree রাখে, শুধু blob দরকারমতো আনে।
  • Clone-এর পর submodule folder খালি কেন? — সাধারণ clone submodule আনে না; git submodule update --init --recursive বা clone --recurse-submodules লাগে।
  • Git LFS কীভাবে repository ছোট রাখে? — .gitattributes-এর filter বড় file-কে version, oid আর size-এর একটি ছোট pointer-এ বদলায়; content থাকে আলাদা LFS server-এ।

৯ · AI-এর যুগে — বড় repository-র সিদ্ধান্তে AI-কে যাচাই

“Submodule আপডেট হচ্ছে না, কী করব?” বা “আমাদের কি monorepo নেওয়া উচিত?” — AI এমন প্রশ্নে দ্রুত আত্মবিশ্বাসী উত্তর দেয়। কিন্তু এই বিষয়ে AI-এর কিছু পরিচিত ভুল আছে, যার কয়েকটি দলের সবার কাজ ভেঙে দিতে পারে:

AI-এর ভুলকীভাবে ধরবেন
“Submodule ঠিক হচ্ছে না? rm -rf .git/modules করে আবার add করুন” — কোনো সতর্কতা ছাড়া, ভেতরের push না করা commit-সহ মুছে যায়আগে ভেতরে git status আর git log --branches --not --remotes দেখুন; throwaway clone-এ চেষ্টা করুন
git submodule update --remote চালিয়ে সব submodule-এর pin সর্বশেষে তুলে সরাসরি commit করতে বলাgit diff --submodule=log দেখে প্রতিটি বদল review করুন; pin সরানো একটি আলাদা, ব্যাখ্যাসহ commit
বড় file সরাতে git lfs migrate import --everything আর push --force, ইতিহাস বদলানোর কথা না বলেHash বদলায় কি না জিজ্ঞেস করুন; দলকে জানিয়ে আলাদা clone-এ আগে পরীক্ষা
অস্তিত্বহীন flag (git clone --sparse-depth, git submodule pull)git help clone, git help submodule খুলে মিলিয়ে নিন
“Google Git-এ এক বিশাল monorepo রাখে” বা বানানো সংখ্যামূল সূত্র দেখুন (২০১৬ CACM প্রবন্ধ: নিজস্ব system, Git নয়)
তিনজনের দলের জন্য জটিল build system আর remote cache-এর সুপারিশসমস্যা আগে মাপুন: CI-তে আসলে কত মিনিট যায়, কতবার একসাথে বদল হয়
Platform-এর LFS কোটা বা file-সীমার নির্দিষ্ট সংখ্যা, পুরোনো তথ্য থেকেPlatform-এর বর্তমান documentation দেখুন

নিচের prompt-এ AI-কে তথ্য দিয়ে, trade-off আর ঝুঁকিসহ উত্তর চাওয়া হয়েছে — যাতে আপনি প্রতিটি দাবি যাচাই করতে পারেন।

আমাদের দল: ১২ জন developer, ২টি দল। কোড: একটি React website, ২টি Node.js service, আর ৩টি শেয়ার করা library — এখন ৬টি আলাদা repository-তে। শেয়ার করা library বদলালে প্রায় প্রতি সপ্তাহে ৩টি repository-তে একসাথে pull request করতে হয়। CI-তে প্রতিটি repository-র build ৫-৮ মিনিট। ১. Monorepo-তে যাওয়া বনাম এখনকার মতো থাকা — দুই দিকের ৩টি করে কারণ দাও, আমাদের তথ্য ধরে। ২. Monorepo নিলে ইতিহাস হারিয়ে না ফেলে ৬টি repository কীভাবে এক জায়গায় আনা যায়? প্রতিটি Git command-এর পাশে লেখো সেটি কী বদলায়, আর কোনটি ইতিহাস বা remote বদলায়। ৩. Affected build-এর জন্য কোন ধরনের tool লাগবে? কোনো নির্দিষ্ট tool-এর নাম নিলে তার সীমাবদ্ধতাও বলো। ৪. কোনো flag বা তথ্য নিশ্চিত না হলে বলবে "নিশ্চিত নই"; বানিয়ে লিখবে না।

AI-এর পরামর্শ হাতে পেলে — চেকলিস্ট

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

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

প্র ০১Google বহু বছর ধরে প্রায় সব কোড এক repository-তে রেখেছে বলে প্রকাশ্যে লিখেছে। তাহলে সবাই কেন তা করে না?

২০১৬ সালের CACM প্রবন্ধে Potvin আর Levenberg যে সুবিধাগুলো লিখেছেন, সেগুলো বাস্তব: এক জায়গায় সব কোড থাকায় যে কেউ যেকোনো ব্যবহার খুঁজে পান; একটি library-র API বদলালে বদলকারী নিজেই সব ব্যবহারকারী এক commit-এ ঠিক করতে পারেন; কোনো project একটি library-র পুরোনো version-এ আটকে থাকে না (“diamond dependency”-র ঝামেলা কম); আর একই tooling ও নিয়ম সবার ওপর চলে। কিন্তু একই প্রবন্ধ খরচের কথাও বলে, আর সেটিই আসল শিক্ষা। Google-এর repository Git-এ নয় — প্রবন্ধে বর্ণিত নিজস্ব version control system, file system-এর মতো করে শুধু দরকারি অংশ দেখানোর ব্যবস্থা, নিজস্ব build system, বিশাল পরিসরের code search আর স্বয়ংক্রিয় refactoring tool. এগুলো বানাতে আর চালাতে আলাদা দল লাগে।

একটি ছোট বা মাঝারি প্রতিষ্ঠান সেই tooling ছাড়া শুধু “সব এক জায়গায়” নিলে monorepo-র অসুবিধাগুলোই আগে পায়: ধীর clone, প্রতিটি pull request-এ সব কিছুর test, আর কে কোন অংশের দায়িত্বে তা অস্পষ্ট। আবার Git-এর access control সাধারণত পুরো repository ধরে, তাই যে প্রতিষ্ঠানে বাইরের contractor বা আইনি কারণে আলাদা রাখতে হয় এমন কোড আছে, তাদের জন্য একক monorepo কঠিন। অন্যদিকে polyrepo-র লুকানো খরচও কম নয় — সময়ের সাথে প্রতিটি repository-র tooling আলাদা হয়ে যায়, আর শেয়ার করা library-র একটি নিরাপত্তা-patch দশটি repository-তে পৌঁছাতে সপ্তাহ লাগে।

তাই ইন্টারভিউতে ভালো উত্তর কোনো একটির পক্ষে স্লোগান নয়, প্রশ্ন: কতবার project-গুলো একসাথে বদলায়? দলগুলো কতটা স্বাধীন? Access আলাদা রাখার দরকার আছে কি? Tooling-এ বিনিয়োগের মানুষ আছে কি? অনেক প্রতিষ্ঠান মাঝামাঝি পথ নেয় — ঘনিষ্ঠভাবে জড়িত project-এর জন্য একটি monorepo, আর বাকিগুলো আলাদা।

মূল উপলব্ধি: Monorepo-র সুবিধা আসে tooling-এ বিনিয়োগের সাথে; অন্যের সাফল্যের গল্পের সাথে তাদের খরচের হিসাবটাও পড়তে হয়।

প্র ০২Git-এর “প্রত্যেকের কাছে পুরো ইতিহাস” নকশা বড় repository-তে বোঝা হয়ে দাঁড়ায় কেন, আর Git কীভাবে মানিয়ে নিয়েছে?

২০০৫ সালে Linus Torvalds Git বানিয়েছিলেন Linux kernel-এর জন্য — বড়, কিন্তু মূলত text source code-এর project. তখনকার লক্ষ্য ছিল দ্রুত branch-merge, server ছাড়া কাজ, আর content hash দিয়ে নিখুঁত অখণ্ডতা। Distributed নকশার মানে হলো প্রতিটি clone একটি পূর্ণ repository: সব commit, সব tree, সব blob. ছোট-মাঝারি project-এ এটি আশীর্বাদ — internet ছাড়াই git log, blame, bisect, আর server হারালেও যেকোনো clone থেকে সব ফিরে আসে।

কিন্তু খরচ বাড়ে ইতিহাসের সাথে, বর্তমান আকারের সাথে নয়। এই পর্বের model-এ বর্তমান snapshot ৬৯ MB, অথচ পুরো ইতিহাস ৬৮১ MB. বড় binary থাকলে অনুপাত আরও খারাপ। আরও একটি খরচ: বিশাল working tree-তে git status-কে প্রতিটি file-এর অবস্থা দেখতে হয়। Microsoft প্রকাশ্যে লিখেছে, Windows-এর কোড Git-এ আনতে তারা প্রথমে একটি virtual file system স্তর বানায়, যা শুধু ব্যবহৃত file নামাত; পরে সেই অভিজ্ঞতার অনেক কিছু Git-এর মূল অংশে ফেরত যায় বলে জানা যায়।

Git-এর মানিয়ে নেওয়া ধাপে ধাপে: shallow clone (পুরোনো, সহজ, কিন্তু ইতিহাস কাটে); partial clone আর promisor remote (২০১৮-১৯ সালের দিকে, ইতিহাসের কাঠামো রাখে কিন্তু content দরকারমতো আনে); sparse checkout-এর cone mode (দ্রুত, folder-ভিত্তিক); commit-graph file আর multi-pack-index (ইতিহাসে হাঁটা আর object খোঁজা দ্রুত); file-system monitor; আর git maintenance ও scalar, যা এগুলো একসাথে চালু করে। লক্ষ করুন, প্রায় প্রতিটি সমাধান distributed আদর্শ থেকে একটু সরে এসে server-এর ওপর নির্ভরতা বাড়ায় — partial clone-এ server বন্ধ থাকলে পুরোনো file দেখা যায় না।

মূল উপলব্ধি: প্রতিটি নকশা একটি পরিসরের জন্য সেরা; পরিসর বদলালে সমাধান আসে আংশিক আপসে — এখানে “সব কিছু সবার কাছে” থেকে “যা দরকার, যখন দরকার”-এ।

প্র ০৩Submodule-কে অনেকে “কষ্টদায়ক” বলেন। তবু Git কেন branch-এর বদলে নির্দিষ্ট commit pin করার নকশা বেছে নিল, আর কখন submodule-ই সঠিক?

Submodule যদি একটি branch অনুসরণ করত (“সবসময় ui-kit-এর main”), তাহলে superproject-এর একটি পুরোনো commit checkout করলে কোন ui-kit আসত? আজকের main — যা হয়তো সেই পুরোনো কোডের সাথে মেলেই না। অর্থাৎ superproject-এর ইতিহাস আর পুনরুৎপাদনযোগ্য থাকত না: ছয় মাস আগের release আবার build করা, বা git bisect দিয়ে bug খোঁজা অসম্ভব হতো। নির্দিষ্ট commit pin করলে superproject-এর প্রতিটি commit একটি সম্পূর্ণ, স্থির বর্ণনা — “shop-এর এই কোড, ui-kit-এর ঠিক এই কোড”। এটি lockfile-এর মতোই চিন্তা, যা পর্ব ১০-এ দেখেছেন।

কষ্ট আসে এই নকশার স্বাভাবিক ফল থেকে: তিনটি জায়গা (submodule-এর checkout, superproject-এর index, superproject-এর HEAD) আলাদা হতে পারে, আর Git ডিফল্টে সেগুলো নিজে মেলায় না — pull করলে submodule নিজে থেকে update হয় না, push করলে submodule push হয় না। নতুনদের কাছে এটি “জাদুকরীভাবে পুরোনো কোড” মনে হয়। ২০০৭ সালের দিকে submodule আসার পর থেকে Git অনেক সহায়তা যোগ করেছে — --recurse-submodules, submodule.recurse, push.recurseSubmodules, diff --submodule=log — কিন্তু ডিফল্ট আচরণ পেছনের সামঞ্জস্যের কারণে বদলায়নি।

তাহলে কখন submodule সঠিক? যখন বাইরের কোড সত্যিই আলাদা প্রকল্প, নিজের গতিতে চলে, আর আপনার দরকার তার ঠিক একটি commit — কিন্তু package হিসেবে পাওয়া যায় না। যেমন embedded project-এ chip vendor-এর SDK, C/C++ project-এ একটি library যার কোনো package manager নেই, বা documentation site-এ একটি theme. যখন দুই দিকেই প্রায় রোজ বদল হয়, বা দলের অনেকে Git-এ নতুন, তখন monorepo-র folder বা versioned package সাধারণত কম কষ্টের।

মূল উপলব্ধি: Submodule-এর কঠোরতা আসলে পুনরুৎপাদনযোগ্যতার দাম; সমস্যা নকশায় নয়, ভুল জায়গায় ব্যবহারে আর তিনটি অবস্থা মেলানোর অভ্যাস না থাকায়।

প্র ০৪Git ২০২২ সালে submodule-এর জন্য local file protocol বন্ধ করে দিল, যাতে অনেকের পরীক্ষার script ভেঙে যায়। নিরাপত্তা আর সুবিধার এই টানাপোড়েন থেকে কী শেখার আছে?

অক্টোবর ২০২২-এ Git-এর নিরাপত্তা-release (CVE-2022-39253-সহ) জানায়, local path থেকে clone-এর একটি অপ্টিমাইজেশন symbolic link অনুসরণ করত। একজন আক্রমণকারী এমন একটি repository বানাতে পারত, যার submodule-এর URL আক্রান্ত ব্যক্তির নিজের কম্পিউটারের একটি path; --recurse-submodules দিয়ে clone করলেই কৌশলে সেই কম্পিউটারের file নতুন repository-তে চলে আসতে পারত — আর পরে push হলে বাইরে। আক্রান্ত ব্যক্তি শুধু একটি সাধারণ clone চালিয়েছেন, কিছু “চালাননি”।

Git-এর সমাধান ছিল ডিফল্ট বদলানো: protocol.file.allow=user. আপনি নিজে local path দিলে কাজ করে, কিন্তু Git নিজে থেকে (যেমন submodule-এর জন্য) local path clone করবে না। এতে পরীক্ষার script, CI-এর কিছু setup আর tutorial ভেঙে যায় — এই পর্বেও আপনি সেটি দেখেছেন। তবু সিদ্ধান্তটি “secure by default” নীতির একটি ক্লাসিক উদাহরণ: বিপজ্জনক ক্ষমতা ডিফল্টে বন্ধ, আর যিনি সত্যিই চান তিনি সচেতনভাবে, সবচেয়ে ছোট পরিসরে (git -c দিয়ে একটি command-এর জন্য) চালু করবেন।

এখানে software supply chain-এর বড় শিক্ষাও আছে। Submodule, package বা build script — যেকোনো ব্যবস্থা যা অন্যের দেওয়া ঠিকানা থেকে স্বয়ংক্রিয়ভাবে কিছু আনে বা চালায়, তা আক্রমণের পথ। তাই অচেনা repository --recurse-submodules দিয়ে clone করার আগে .gitmodules পড়ে দেখা, Git নিয়মিত আপডেট রাখা, আর CI-তে ন্যূনতম অধিকার দেওয়া পেশাদার অভ্যাস। এই ধরনের সমস্যার সমাধানে প্রথম কাজ টুল আপডেট করা — নিরাপত্তা-release-এর খবর রাখুন। Git-এর নিরাপত্তা নিয়ে আরও আলোচনা পর্ব ৫৭-এ।

মূল উপলব্ধি: নিরাপদ ডিফল্ট কিছু অসুবিধা আনে, আর সেটি সচেতন আপস; পাশ কাটাতে হলে সবচেয়ে ছোট পরিসরে, কারণ বুঝে।

প্র ০৫Git LFS বড় file-এর সমস্যা মেটায়, কিন্তু Git-এর কোন মূল সুবিধা এতে ছেড়ে দিতে হয়? ভিডিও গেম বা data science দল কী করে?

Git-এর শক্তি তিনটি: প্রতিটি clone স্বয়ংসম্পূর্ণ, content-এর hash দিয়ে অখণ্ডতা, আর লাইনভিত্তিক diff-merge. বড় binary এই তিনটিকেই চাপে ফেলে — প্রতিটি version সবার কাছে যায়, delta প্রায় কাজ করে না, আর merge অর্থহীন। LFS প্রথম সুবিধাটি ছেড়ে দেয়: Git-এ থাকে শুধু pointer, আর আসল content একটি কেন্দ্রীয় server-এ। Clone ছোট হয়, কিন্তু server ছাড়া পুরোনো version দেখা যায় না, আর server-এর storage ও bandwidth-এর খরচ ফিরে আসে — এজন্যই hosting platform-গুলো LFS-এর আলাদা কোটা রাখে। অখণ্ডতা অবশ্য থাকে: pointer-এ content-এর SHA-256, তাই server ভুল file দিলে ধরা পড়ে।

যে file merge হয় না, সেখানে পুরোনো centralized version control-এর “lock-modify-unlock” ধারণা ফিরে আসে — git lfs lock. পর্ব ৭-এ দেখেছেন, Subversion-এর মতো system এই পদ্ধতি দিত। এজন্যই অনেক বড় ভিডিও গেম স্টুডিও ঐতিহাসিকভাবে Perforce-এর মতো centralized system ব্যবহার করেছে বলে প্রায়ই লেখা হয় — বিশাল art asset আর file lock সেখানে মূল সুবিধা। কিছু দল কোড Git-এ আর asset আলাদা system-এ রাখে।

Data science-এ সমস্যাটা আরেক রকম: কয়েক GB-এর dataset আর model file. সেখানে প্রায়ই dataset Git-এর বাইরে object storage-এ থাকে, আর repository-তে থাকে একটি ছোট metadata file — কোন dataset-এর কোন version, তার hash. এই কাজের জন্য আলাদা tool-ও আছে। মূল চিন্তা LFS-এর মতোই: Git-কে মানুষের লেখা ছোট text-এর জন্য রাখা, আর বড় content-কে ঠিকানা ও hash দিয়ে বাঁধা।

মূল উপলব্ধি: বড় file-এর প্রতিটি সমাধান কিছু distributed স্বাধীনতা ছেড়ে কেন্দ্রীয় storage-এ যায়; প্রশ্ন হলো কোন file-এর জন্য সেই দাম দেওয়ার মতো।

অনুশীলন

হিসাব, আউটপুট অনুমান, command-এর ক্রম আর নকশা-সমালোচনা — প্রতিটির পূর্ণ উত্তর আছে; Git-এর অংশ আসল Git ২.৫৩ দিয়ে যাচাই করা।

  1. Clone-এর আকার (হিসাব): একটি repository-তে ১০,০০০টি commit. সর্বশেষ snapshot-এর সব tree মিলিয়ে ২ MB আর সব blob মিলিয়ে ৩০০ MB. প্রথম commit বাদে প্রতিটি commit-এর commit object আর বদলানো tree মিলিয়ে গড়ে ৫ KB. ইতিহাসের সব blob-এর version মিলিয়ে (সর্বশেষগুলোসহ) ৪,৭০০ MB. Compression উপেক্ষা করুন, ১ MB = ১০২৪ KB ধরুন। (ক) পূর্ণ clone, (খ) --depth 1, (গ) --filter=blob:none clone-এ মোটামুটি কত নামবে? (ঘ) Partial clone-এ পরে পুরো ইতিহাসে git log -p চালালে কী হবে?

    প্রথম commit-এর metadata হিসেবে snapshot-এর tree (২ MB) ধরি, বাকি ৯,৯৯৯টি commit × ৫ KB = ৪৯,৯৯৫ KB = ৪৯,৯৯৫ ÷ ১০২৪ ≈ ৪৮.৮২ MB.

    • (ক) পূর্ণ clone = ২ + ৪৮.৮২ + ৪,৭০০ = ৪,৭৫০.৮২ MB (প্রায় ৪.৬৪ GB).
    • (খ) Shallow, depth 1 = একটি commit আর তার snapshot: ২ + ৩০০ = ৩০২ MB — পূর্ণের ৩০২ ÷ ৪,৭৫০.৮২ ≈ ৬.৪%.
    • (গ) Partial, blob:none = সব commit আর tree + checkout-এর blob: ২ + ৪৮.৮২ + ৩০০ = ৩৫০.৮২ MB — পূর্ণের ≈ ৭.৪%. Shallow-এর চেয়ে মাত্র ৪৮.৮২ MB বেশি, কিন্তু পুরো ইতিহাসের git log, merge-base আর blame-এর কাঠামো হাতে থাকে।
    • (ঘ) log -p-কে প্রতিটি commit-এর পুরোনো আর নতুন blob তুলনা করতে হয়, তাই Git বাকি ৪,৭০০ − ৩০০ = ৪,৪০০ MB-এর blob ধাপে ধাপে server থেকে আনবে — শেষে মোট আকার প্রায় পূর্ণ clone-এর সমান, আর অনেকগুলো আলাদা network অনুরোধে সময়ও বেশি লাগতে পারে। Partial clone সেই কাজের জন্য, যেখানে ইতিহাসের content কম লাগে।
  2. আউটপুট অনুমান — git submodule status: একটি superproject shop-এর সর্বশেষ commit submodule libs/ui-kit-কে commit cb99720-এ pin করেছে; ui-kit-এর remote-এর main-এ তার পরে আরেকটি commit ab1bba7 আছে। একজন নতুন সদস্য নিচের command-গুলো চালালেন। প্রতিটি git submodule status লাইনের শুরুর চিহ্ন আর hash কী হবে, আর শেষ git status --short কী দেখাবে?
    $ git clone -q shop.git shop-e
    $ cd shop-e
    $ git submodule status                              # (১)
    $ git -c protocol.file.allow=always submodule update --init -q
    $ git submodule status                              # (২)
    $ git -C libs/ui-kit switch -q --detach origin/main
    $ git submodule status                              # (৩)
    $ git status --short                                # (৪)
    $ git add libs/ui-kit
    $ git commit -q -m 'Bump ui-kit to add alert style'
    $ git submodule status                              # (৫)

    Git ২.৫৩-এর আসল আউটপুট:

    (১) -cb99720ae131e7cbc0d1665223b22493d7302bc8 libs/ui-kit
    (২)  cb99720ae131e7cbc0d1665223b22493d7302bc8 libs/ui-kit (cb99720)
    (৩) +ab1bba7148c680202dbef10a29b9835f2b7fc19c libs/ui-kit (heads/main)
    (৪)  M libs/ui-kit
    (৫)  ab1bba7148c680202dbef10a29b9835f2b7fc19c libs/ui-kit (heads/main)

    (১) সাধারণ clone-এ submodule initialize হয়নি — - আর রেকর্ড করা hash. (২) Update রেকর্ড করা cb99720 checkout করেছে, সর্বশেষ ab1bba7 নয় — চিহ্ন ফাঁকা। (৩) ভেতরে নতুন commit-এ গেলে checkout আর index আলাদা — +, আর hash এখন checkout করা ab1bba7. (৪) Superproject-এর চোখে submodule “modified” — দ্বিতীয় কলামে M, কারণ বদল working tree-তে, এখনো stage হয়নি। (৫) git add আর commit-এর পর আবার ফাঁকা চিহ্ন, এবার নতুন hash. বন্ধনীর অংশটি git describe-এর ফল: (২)-এ cb99720-এ কোনো branch বা tag নেই, তাই শুধু ছোট hash; (৩)-এ ab1bba7 remote-এর main-এর মাথা, তাই heads/main. এই অংশ অনুমান করা কঠিন, তাই পরীক্ষায় মূল বিষয় শুরুর চিহ্ন আর hash.

  3. Command-এর ক্রম — Git LFS: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের repository-তে design দল এখন থেকে .psd আর .mp4 file যোগ করবে। git-lfs install করা আছে। ধাপে ধাপে command লিখুন, যাতে নতুন .psd আর .mp4 LFS-এ যায়, অন্যদের clone-এ একই নিয়ম পৌঁছায়, আর commit-এর পর যাচাই করা যায় যে Git-এ সত্যিই pointer গেছে। আগে থেকে commit হওয়া একটি পুরোনো .psd-এর কী হবে?
    $ git lfs install                     # once per user (or --local for one repo)
    $ git lfs track "*.psd" "*.mp4"
    $ cat .gitattributes
    $ git add .gitattributes
    $ git add design/banner.psd
    $ git commit -m "Track design files with Git LFS"
    $ git lfs ls-files
    $ git show HEAD:design/banner.psd
    $ git check-attr -a design/banner.psd

    Git ২.৫৩ আর git-lfs ৩.৭.১-এ git lfs track "*.psd" "*.mp4" দুটি লাইনে Tracking "*.psd" আর Tracking "*.mp4" দেখায়, আর .gitattributes-এ লেখে *.mp4 filter=lfs diff=lfs merge=lfs -text আর *.psd filter=lfs diff=lfs merge=lfs -text (বর্ণানুক্রমে সাজানো)। .gitattributes commit না করলে নিয়ম শুধু আপনার কম্পিউটারে থাকবে — অন্যরা সাধারণ blob হিসেবে বড় file commit করে ফেলবেন। যাচাই: git lfs ls-files-এ file-টির নাম; git show HEAD:…-এ তিন লাইনের pointer (version https://git-lfs.github.com/spec/v1, oid sha256:…, size …); check-attr-এ filter: lfs. পুরোনো .psd যে commit-গুলোতে সাধারণ blob হিসেবে আছে, সেখানে তেমনই থাকবে; পরের বার বদলে add করলে নতুন version LFS-এ যাবে। ইতিহাস থেকেও সরাতে git lfs migrate লাগবে, যা সব hash বদলায় — দলের সাথে পরিকল্পনা করে, throwaway clone-এ আগে পরীক্ষা করে।

  4. নকশা-সমালোচনা: একটি কাল্পনিক অনলাইন বাজার অ্যাপের lead প্রস্তাব দিলেন: “(১) আমাদের ৮টি service আর ৩টি library-কে একটি platform superproject-এ submodule হিসেবে রাখব, প্রতিটি submodule .gitmodules-এ branch = main, আর প্রতি রাতে CI git submodule update --remote চালিয়ে যা আসে commit করে দেবে। (২) Clone দ্রুত করতে সব developer --depth 1 দিয়ে clone করবেন। (৩) বাইরের contractor-কে শুধু payment service-এর submodule-এর access দেব, তাহলে বাকি কোড দেখবেন না।” প্রতিটির সমস্যা আর বিকল্প বলুন।

    (১) প্রতি রাতে review ছাড়াই সব pin সর্বশেষে তোলা মানে pin-এর মূল উপকার — নির্দিষ্ট, পরীক্ষিত combination — হারানো; কোনো service-এর ভাঙা main সকালে সবার platform ভাঙবে, আর ১১টি submodule-এর তিনটি অবস্থা মেলানোর ঝামেলা প্রতিদিনের। Service-গুলো যদি প্রায়ই একসাথে বদলায়, তাহলে monorepo (affected build আর CODEOWNERS-সহ) ভালো; আলাদা থাকলে library-গুলো versioned package হিসেবে, আর service-গুলো নিজস্ব repository-তে, deploy-এর combination ঠিক হবে release-এর নিয়মে। Pin তুলতেই হলে CI একটি pull request খুলুক, test চলুক, মানুষ diff --submodule=log দেখে merge করুন।

    (২) Developer-এর shallow clone-এ git log, blame, merge-base আর bisect অসম্পূর্ণ; --depth একটিমাত্র branch-ও আনে। Shallow CI-এর একবারের build-এর জন্য উপযোগী। Developer-দের জন্য partial clone (--filter=blob:none), দরকারে sparse checkout — ইতিহাস থাকে, content দরকারমতো আসে।

    (৩) এটি আংশিক ঠিক: submodule আলাদা repository, তাই আলাদা access দেওয়া যায়, আর superproject-এর access না দিলে বাকি কোড দেখা যাবে না। কিন্তু contractor superproject clone করতে পারলে .gitmodules-এ সব URL দেখবেন, আর ভুল করে বেশি access দেওয়া সহজ। সহজ পথ: payment service-এর নিজস্ব repository, যা তিনি সরাসরি clone করেন; অন্য service-এর সাথে কথা হবে প্রকাশ্য API বা package দিয়ে। Access দেওয়ার নিয়ম ন্যূনতম অধিকারের নীতি মেনে, নিয়মিত পর্যালোচনাসহ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমSuperproject-এ submodule libs/ui-kit-কে তার remote-এর সর্বশেষ commit-এ তোলার ধাপগুলো সাজান (submodule-এর commit আগেই remote-এ আছে; ▲▼ চেপে সরান)

  1. git -C libs/ui-kit fetch
  2. git -C libs/ui-kit switch --detach origin/main
  3. git diff --submodule=log দেখে কোন commit আসছে তা যাচাই
  4. git add libs/ui-kit
  5. git commit -m "Bump ui-kit"
  6. git push (superproject)

একটি superproject-এ libs/ui-kit নামে একটি submodule আছে। Superproject-এর commit-এর tree-তে এর জন্য কী সংরক্ষিত থাকে?

  • ui-kit-এর সব file-এর blob
  • Mode 160000-এর একটি entry, যাতে submodule-এর একটি commit-এর hash
  • ui-kit-এর একটি branch-এর নাম, যেমন main
  • শুধু ui-kit repository-র URL

git submodule status-এর একটি লাইন -aa11f68… libs/ui-kit দিয়ে শুরু। এর অর্থ কী?

  • Submodule initialize হয়নি; git submodule update --init লাগবে
  • Submodule-এ রেকর্ডের চেয়ে নতুন commit checkout করা আছে
  • Submodule-এ merge conflict আছে
  • Submodule-টি superproject থেকে মুছে ফেলা হয়েছে

একটি local bare repository থেকে git clone --depth 1 /srv/repos/app.git app চালানো হলো (সাধারণ path, URL নয়)। Git ২.৫৩-এ কী ঘটে?

  • শুধু শেষ commit-সহ একটি shallow clone তৈরি হয়
  • Error দিয়ে clone ব্যর্থ হয়
  • Warning দিয়ে --depth উপেক্ষা করে পূর্ণ clone করে; shallow চাইলে file:// URL লাগে
  • সব commit আসে, কিন্তু blob বাদ পড়ে

git clone --filter=blob:none <url> (server filter সমর্থন করে) clone-এর পর repository-তে কী থাকে?

  • শুধু শেষ commit আর তার snapshot
  • শুধু commit, কোনো tree বা blob নয়, আর working tree খালি
  • সব commit, tree আর blob — সাধারণ clone-এর মতো
  • সব commit আর tree, আর শুধু checkout-এর জন্য দরকারি blob; বাকি blob দরকারমতো পরে

একটি monorepo-র dependency: web/shop → libs/ui-kit, libs/money; services/payments → libs/money, libs/auth; web/admin → libs/ui-kit, services/catalog; services/catalog → libs/auth. শুধু libs/money-এর একটি file বদলালে affected build-এ কোন project-গুলো build আর test হবে?

  • শুধু libs/money
  • libs/money, web/shop, services/payments
  • libs/money, web/shop, services/payments, web/admin
  • সব project

.gitattributes-এ *.psd filter=lfs diff=lfs merge=lfs -text আছে আর git-lfs install করা। ৩,০০,০০০ byte-এর banner.psd add আর commit করার পর git show HEAD:banner.psd কী দেখায়?

  • তিন লাইনের pointer: version https://git-lfs.github.com/spec/v1, oid sha256:…, size 300000
  • ছবির আসল ৩,০০,০০০ byte
  • একটি error, কারণ LFS file Git-এ থাকে না
  • ছবির zip করা ছোট রূপ

একজন developer submodule-এর ভেতরে একটি নতুন commit করলেন, superproject-এ pointer commit করে push করলেন, কিন্তু submodule-এর commit push করলেন না। পরে অন্য কেউ clone --recurse-submodules করলে কী হবে, আর কোন setting আগে থেকেই এটি ঠেকাত?

  • Submodule-এর আগের commit checkout হবে; কোনো setting লাগে না
  • Superproject-এর push-এর সাথে submodule-এর commit-ও নিজে থেকে চলে গেছে, তাই সব ঠিক
  • Submodule-এর fetch ব্যর্থ হবে (not our ref); push.recurseSubmodules=check push আটকাত
  • শুধু --recurse-submodules বাদ দিলেই সমস্যা মিটবে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Monorepo দেয় atomic change আর শেয়ার করা tooling, দাম বড় repository, CI-এর খরচ আর কঠিন access control; polyrepo দেয় স্বাধীনতা, দাম version আর release-এর ঝামেলা। অনেক সময় উত্তর hybrid.
  2. Monorepo-তে CI চলে affected build-এ: বদলানো file → মালিক project → reverse dependency ধরে সবাই; সাথে build cache আর CODEOWNERS.
  3. --depth পুরোনো commit বাদ দেয় (local path-এ file:// লাগে), --filter=blob:none content পরে আনে (server-এ uploadpack.allowFilter), sparse checkout শুধু কয়েকটি folder খোলে।
  4. Submodule = gitlink (160000) + .gitmodules; clone-এর পর update --init --recursive, ভেতরে detached HEAD, এগোলে git add + commit, আর submodule-এর commit আগে push (push.recurseSubmodules=check).
  5. বিকল্প: subtree (ইতিহাস merge, contrib command), vendoring, আর প্রায়ই সবচেয়ে সহজ versioned package. বড় binary-র জন্য Git LFS: pointer-এ version, oid, size; .gitattributes commit করুন, আর মনে রাখুন LFS অতীত বদলায় না।

পরের পর্বে: মডিউল ৩-এ শিখলেন দলে মিলে কোড রাখা আর বদলানো। কিন্তু কী বানাতে হবে, সেটা ঠিক হয় কীভাবে? পর্ব ২০-এ requirement engineering — stakeholder কারা, তাঁদের কাছ থেকে চাহিদা বের করার (elicitation) কৌশল, আর একটি project আদৌ করা সম্ভব কি না (feasibility).

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান