রিমোট — clone, push, pull, fetch
এই পাঠে যা শিখবেন
- Remote রেফারেন্স কী এবং কেন এটি ডিস্ট্রিবিউটেড VCS-এর সিঙ্ক্রোনাইজেশন মেকানিজম
- clone, push, fetch, pull-এর সঠিক আচরণ এবং তাদের মধ্যে সাধারণ বিভ্রান্তির পার্থক্য
- push কেন এবং কখন প্রত্যাখ্যাত হয় — একটি বাস্তব নিরাপত্তা মেকানিজম
- Python দিয়ে দুটো আলাদা
Repoইনস্ট্যান্স ব্যবহার করে push/fetch/pull সিমুলেট ও যাচাই করা
১ · Remote কী
RemoteRemoteরিপোজিটরির অন্য একটি কপির রেফারেন্স, সাধারণত একটি শেয়ার্ড সার্ভারে হোস্ট করা — যেমন GitHub বা GitLab। হলো রিপোজিটরির আরেকটি কপির রেফারেন্স — সাধারণত GitHub বা GitLab-এর মতো একটি শেয়ার্ড সার্ভারে হোস্ট করা (দুটোই বাস্তব, ব্যাপকভাবে ব্যবহৃত উদাহরণ)। M7/L29-এ আমরা দেখেছি ডিস্ট্রিবিউটেড VCS-এ প্রতিটি লোকাল রিপো-তেই সম্পূর্ণ হিস্ট্রি থাকে — remote হলো ঠিক সেই একাধিক স্বতন্ত্র কপির মধ্যে সিঙ্ক্রোনাইজ করার প্রক্রিয়া।
২ · git clone — সম্পূর্ণ হিস্ট্রিসহ একটি নতুন কপি
git clone <url> একটি বিদ্যমান রিমোট রিপোজিটরির একটি সম্পূর্ণ নতুন লোকাল কপি তৈরি করে —
পুরো হিস্ট্রিসহ (L29-এর "প্রতিটি ক্লোনেই সম্পূর্ণ হিস্ট্রি" পয়েন্টের সরাসরি প্রয়োগ) — এবং স্বয়ংক্রিয়ভাবে
একটি remote রেফারেন্স সেট আপ করে (প্রথাগতভাবে origin নামে)।
৩ · push, fetch, pull — সঠিক পার্থক্য
git push: আপনার লোকাল commit (বর্তমান ব্রাঞ্চের) remote-এর সংশ্লিষ্ট ব্রাঞ্চে আপলোড করে — একটি গুরুত্বপূর্ণ নিরাপত্তা আচরণ: রিমোট ব্রাঞ্চে এমন commit থাকলে যা আপনার লোকালে নেই, push স্বাভাবিকভাবেই প্রত্যাখ্যাত হয় (অন্যের কাজ ভুলবশত ওভাররাইট হওয়া থেকে রক্ষা — L37-এর সোনালী নিয়মের উদ্বেগের সাথে সরাসরি সম্পর্কিত)। git fetch: remote থেকে নতুন commit/ব্রাঞ্চ ডাউনলোড করে আপনার লোকাল রিপোর "remote সম্পর্কে জ্ঞান"-এ যোগ করে — আপনার নিজের বর্তমান ওয়ার্কিং ব্রাঞ্চ একদমই স্পর্শ না করেই — অন্যরা কী করেছে দেখার একটি সত্যিকারের "নিরাপদ", অ-ধ্বংসাত্মক উপায়। git pull: এটি git fetch তারপর একটি git merge (বা কনফিগারেশন অনুযায়ী rebase) সমতুল্য — অর্থাৎ pull = fetch + ইন্টিগ্রেট, যেখানে শুধু fetch শুধুই ডাউনলোড করে (একটি সাধারণ বিভ্রান্তি এখানে স্পষ্ট করা দরকার)।
নিচের কোড সেলে আমরা L30/L34/L37-এর ঠিক একই Repo মডেলের ওপর ভিত্তি করে একটি
সম্পূর্ণ আলাদা, দ্বিতীয় Repo ইনস্ট্যান্স তৈরি করছি — সেটিই "remote"।
বাস্তব git বাইনারি/নেটওয়ার্ক এই স্যান্ডবক্সে নেই, তাই push-প্রত্যাখ্যানের নিরাপত্তা নিয়মসহ পুরো
সিঙ্ক্রোনাইজেশন আচরণ নির্ভুলভাবে Python দিয়ে সিমুলেট করা হচ্ছে।
# remote সিমুলেশন -- দুটো সম্পূর্ণ আলাদা Repo ইনস্ট্যান্স (local ও remote)।
# প্রতিটি সিমুলেটেড রিপোর নিজস্ব hash-prefix আছে, যাতে ভিন্ন ভিন্ন repo-তে স্বাধীনভাবে
# তৈরি হওয়া commit হ্যাশ কখনো একে অপরের সাথে সংঘর্ষ (collide) না করে -- বাস্তব git-এ প্রকৃত
# content-hash (SHA) ব্যবহার করে এটি প্রাকৃতিকভাবেই এড়ানো হয়।
def make_repo(prefix="c"):
return {
"working_directory": {},
"staging_area": {},
"commits": {},
"branches": {"main": None},
"head": "main",
"remote_tracking": {},
"prefix": prefix,
}
def _new_hash(repo):
return f"{repo['prefix']}{len(repo['commits']) + 1}"
def current_commit(repo):
if repo["head"] in repo["branches"]:
return repo["branches"][repo["head"]]
return repo["head"]
def git_add(repo, filename):
repo["staging_area"][filename] = repo["working_directory"][filename]
def git_commit(repo, message):
parent = current_commit(repo)
h = _new_hash(repo)
repo["commits"][h] = {"message": message, "snapshot": dict(repo["staging_area"]), "parent": parent}
repo["branches"][repo["head"]] = h
repo["staging_area"] = {}
return h
def is_ancestor(repo, ancestor_hash, descendant_hash):
current = descendant_hash
while current is not None:
if current == ancestor_hash:
return True
current = repo["commits"][current]["parent"]
return False
def git_clone(remote_repo, prefix):
return {
"working_directory": {},
"staging_area": {},
"commits": dict(remote_repo["commits"]),
"branches": dict(remote_repo["branches"]),
"head": "main",
"remote_tracking": {
f"origin/{b}": t for b, t in remote_repo["branches"].items() if t is not None
},
"prefix": prefix,
}
def git_push(local_repo, remote_repo, branch):
local_tip = local_repo["branches"].get(branch)
remote_tip = remote_repo["branches"].get(branch)
if remote_tip is not None and not is_ancestor(local_repo, remote_tip, local_tip):
return {
"success": False,
"error": f"! [rejected] {branch} -> {branch} (non-fast-forward) -- remote-এ এমন "
f"commit আছে যা আপনার local-এ নেই, আগে git pull করুন",
}
current = local_tip
while current is not None and current not in remote_repo["commits"]:
remote_repo["commits"][current] = local_repo["commits"][current]
current = local_repo["commits"][current]["parent"]
remote_repo["branches"][branch] = local_tip
return {"success": True, "new_remote_tip": local_tip}
def git_fetch(local_repo, remote_repo):
for branch, tip in remote_repo["branches"].items():
if tip is None:
continue
current = tip
while current is not None and current not in local_repo["commits"]:
local_repo["commits"][current] = remote_repo["commits"][current]
current = remote_repo["commits"][current]["parent"]
local_repo["remote_tracking"][f"origin/{branch}"] = tip
return local_repo["remote_tracking"]
def git_pull(local_repo, remote_repo, branch):
git_fetch(local_repo, remote_repo)
remote_tip = local_repo["remote_tracking"][f"origin/{branch}"]
local_tip = local_repo["branches"].get(branch)
if local_tip is None or is_ancestor(local_repo, local_tip, remote_tip):
local_repo["branches"][branch] = remote_tip
return {"type": "fast-forward", "new_tip": remote_tip}
h = _new_hash(local_repo)
local_repo["commits"][h] = {
"message": f"Merge remote-tracking branch 'origin/{branch}'",
"snapshot": dict(local_repo["commits"][remote_tip]["snapshot"]),
"parent": local_tip,
}
local_repo["branches"][branch] = h
return {"type": "merge-commit", "new_tip": h}
# --- dev_a একটি নতুন প্রজেক্ট শুরু করে origin-এ পুশ করছে ---
dev_a = make_repo("a")
origin = make_repo("o")
dev_a["working_directory"]["app.py"] = "print('v1')"
git_add(dev_a, "app.py")
git_commit(dev_a, "প্রজেক্ট শুরু")
dev_a["working_directory"]["readme.md"] = "# Project"
git_add(dev_a, "readme.md")
git_commit(dev_a, "README যোগ করা হলো")
print("dev_a -> origin প্রথম push:", git_push(dev_a, origin, "main"))
# --- dev_b ও dev_c একই মুহূর্তে origin থেকে clone করছে ---
dev_b = git_clone(origin, "b")
dev_c = git_clone(origin, "c")
print("\ndev_b ও dev_c clone-এর পর main ->", dev_b["branches"]["main"], "/", dev_c["branches"]["main"])
# --- dev_a আরেকটি commit করে আবার পুশ করছে ---
dev_a["working_directory"]["login.py"] = "def login(): pass"
git_add(dev_a, "login.py")
git_commit(dev_a, "লগইন ফিচার যোগ")
print("\ndev_a -> origin দ্বিতীয় push:", git_push(dev_a, origin, "main"))
# --- dev_b fetch করছে (শুধু জানছে, নিজের ব্রাঞ্চ এখনো বদলায়নি) ---
git_fetch(dev_b, origin)
print("\ndev_b fetch-এর পর:")
print(" dev_b['main'] ->", dev_b["branches"]["main"], "(এখনো পুরনো -- fetch ব্রাঞ্চ বদলায় না)")
print(" dev_b['origin/main'] ->", dev_b["remote_tracking"]["origin/main"], "(নতুন commit চেনা গেছে)")
# --- dev_b এবার pull করছে (fetch + merge/fast-forward) ---
pull_result = git_pull(dev_b, origin, "main")
print("\ndev_b pull-এর পর:", pull_result)
print(" dev_b['main'] ->", dev_b["branches"]["main"], "(এখন origin-এর সাথে মিলে গেছে)")
# --- dev_c fetch/pull না করেই নিজের কমিট করে সরাসরি push করার চেষ্টা করছে ---
dev_c["working_directory"]["notes.txt"] = "dev_c-এর নিজস্ব কাজ"
git_add(dev_c, "notes.txt")
git_commit(dev_c, "নিজের নোট যোগ")
print("\ndev_c-এর push (fetch/pull না করেই):", git_push(dev_c, origin, "main"))
dev_c-এর push প্রত্যাখ্যাত হয় কারণ ক্লোন করার পর সে কখনো fetch
বা pull করেনি — তার লোকাল হিস্ট্রিতে dev_a-এর দ্বিতীয় commit
("লগইন ফিচার যোগ") নেই, অথচ origin-এ এখন সেই commit-ই সবচেয়ে সাম্প্রতিক। is_ancestor
চেক ব্যর্থ হয় (origin-এর tip dev_c-এর নিজস্ব commit-চেইনের কোথাও পাওয়া যায় না), তাই push
নিরাপদে প্রত্যাখ্যাত হয় — ঠিক এভাবেই বাস্তব git অন্যের কাজ ভুলবশত মুছে যাওয়া থেকে রক্ষা করে।
Remote হলো ডিস্ট্রিবিউটেড VCS-এর একাধিক স্বতন্ত্র কপির মধ্যে সিঙ্ক্রোনাইজেশনের সেতু। push, fetch, pull তিনটিই ভিন্ন কাজ করে — এবং push-এর প্রত্যাখ্যান-নিয়ম কোনো বিরক্তিকর বাধা নয়, বরং একটি সচেতন নিরাপত্তা মেকানিজম যা কাউকে অন্যের কাজ ভুলবশত মুছে ফেলা থেকে আটকায়। পরের পাঠে (L40) আমরা দেখব কীভাবে push-করা ব্রাঞ্চ একটি পুল রিকোয়েস্টের মাধ্যমে কোড রিভিউ ও যাচাইয়ের একটি আনুষ্ঠানিক প্রক্রিয়ায় প্রবেশ করে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
fetch করার পরও কেন dev_b["branches"]["main"] তাৎক্ষণিকভাবে বদলায় না?
কারণ git_fetch ইচ্ছাকৃতভাবে শুধুমাত্র remote_tracking (আপনার নিজের রিপোতে
"origin এখন কোথায় আছে" জানার একটি আলাদা রেকর্ড) আপডেট করে এবং প্রয়োজনীয় commit অবজেক্ট ডাউনলোড করে —
কিন্তু আপনার নিজের বর্তমান ব্রাঞ্চ পয়েন্টার স্পর্শ করে না। এটিই fetch-কে "নিরাপদ" করে তোলে: এটি
আপনার ওয়ার্কিং অবস্থা পরিবর্তন না করেই আপনাকে দেখায় অন্যরা কী করেছে। ব্রাঞ্চ আসলে এগিয়ে নিতে হলে
পরবর্তীতে merge (বা pull) করতে হবে।
প্র ০২
dev_c-এর push প্রত্যাখ্যাত হলো কেন, অথচ dev_a-এর দ্বিতীয় push সফল হয়েছিল — দুটোর মধ্যে পার্থক্য কী?
dev_a তার দ্বিতীয় push করার আগে তার নিজের প্রথম push-এর commit-ই origin-এ ছিল বলে
origin-এর tip dev_a-এর নতুন commit-এর সরাসরি ancestor ছিল — is_ancestor
চেক পাশ করে। dev_c কখনো fetch/pull করেনি, তাই তার লোকাল হিস্ট্রিতে origin-এর সর্বশেষ
commit অনুপস্থিত — origin-এর tip তার commit-চেইনের কোথাও পাওয়া যায় না, তাই is_ancestor
False ফেরত দেয় এবং push প্রত্যাখ্যাত হয়।
প্র ০৩
"git pull = fetch + merge" — কোড সেলের git_pull ফাংশনটি ঠিক কীভাবে এই দুটো ধাপ ধারাবাহিকভাবে বাস্তবায়ন করে?
git_pull-এর প্রথম লাইনেই git_fetch(local_repo, remote_repo) কল করা হয়
(নতুন commit ডাউনলোড ও remote_tracking আপডেট) — তারপর সেই আপডেট হওয়া
remote_tracking রেফারেন্স ব্যবহার করে বর্তমান ব্রাঞ্চকে সেই দিকে fast-forward করা হয়
(অথবা প্রয়োজনে একটি merge commit তৈরি করা হয়)। অর্থাৎ fetch অংশটি প্রথমে ডেটা নিয়ে আসে, তারপরের
ধাপ সেটিকে বর্তমান ব্রাঞ্চে ইন্টিগ্রেট করে — ঠিক সংজ্ঞা অনুযায়ী।
অনুশীলন
-
চিন্তা করুন: যদি
dev_cpush করার আগেgit_pull(dev_c, origin, "main")কল করত, তাহলে কী ভিন্ন হতো? তার push কি তখন সফল হতো?হ্যাঁ। pull করলে প্রথমে fetch হতো (dev_a-এর "লগইন ফিচার যোগ" commit dev_c-এর লোকাল কপিতে চলে আসত), তারপর dev_c-এর main ব্রাঞ্চ সেই commit-এর দিকে fast-forward হতো (dev_c-এর নিজের কোনো আলাদা commit তখনো ছিল না, তাই ancestor সরাসরি মিলে যেত)। এরপর dev_c নিজের "নিজের নোট যোগ" commit করলে তার parent হতো origin-এর সর্বশেষ commit — এবং পরবর্তী push সফল হতো, কারণ origin-এর tip তখন dev_c-এর commit-চেইনের সরাসরি ancestor হয়ে যেত।
-
পরীক্ষা করুন: কোড সেলে
dev_c-এর commit-এর ঠিক আগে একটি লাইনgit_pull(dev_c, origin, "main")যোগ করুন, তারপর Run চেপে দেখুন শেষ push-এর ফলাফল কীভাবে বদলে যায়।শেষ push-এর ফলাফল এখন
{"success": True, ...}হবে, "! [rejected] ..." বার্তার বদলে — কারণ pull করার ফলে dev_c-এর main ব্রাঞ্চ আগে থেকেই origin-এর সর্বশেষ commit-এর ওপর fast-forward হয়ে গেছে, তাই তার পরবর্তী নিজস্ব commit origin-এর tip-কে সরাসরি ancestor হিসেবে রাখে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ — পুশ-করা ব্রাঞ্চ কীভাবে একটি পুল রিকোয়েস্টে পরিণত হয়।
- Cloud Computing & DevOps কোর্স সঙ্গী কোর্স রিমোট রিপোজিটরিতে push হওয়াটাই সাধারণত CI/CD পাইপলাইনের ট্রিগার পয়েন্ট — এই কোর্স সেই পাইপলাইনের আসল বাস্তবায়ন শেখায়।