পাঠ ৩৯ · ৫৮-এর মধ্যে · মডিউল ৯

রিমোট — clone, push, pull, fetch

Remotes — clone, push, pull & fetch
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • 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 শুধুই ডাউনলোড করে (একটি সাধারণ বিভ্রান্তি এখানে স্পষ্ট করা দরকার)।

Local Repository(dev_a / dev_b) Remote Repository(origin) git push git fetch
git push লোকাল commit রিমোটে পাঠায় (remote ডাইভার্জড থাকলে প্রত্যাখ্যাত হয়); git fetch শুধু নতুন commit ডাউনলোড করে, লোকাল ব্রাঞ্চ স্পর্শ করে না; git pull = fetch + merge/fast-forward একসাথে।

নিচের কোড সেলে আমরা L30/L34/L37-এর ঠিক একই Repo মডেলের ওপর ভিত্তি করে একটি সম্পূর্ণ আলাদা, দ্বিতীয় Repo ইনস্ট্যান্স তৈরি করছি — সেটিই "remote"। বাস্তব git বাইনারি/নেটওয়ার্ক এই স্যান্ডবক্সে নেই, তাই push-প্রত্যাখ্যানের নিরাপত্তা নিয়মসহ পুরো সিঙ্ক্রোনাইজেশন আচরণ নির্ভুলভাবে Python দিয়ে সিমুলেট করা হচ্ছে।

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 অন্যের কাজ ভুলবশত মুছে যাওয়া থেকে রক্ষা করে।
মূল কথা · Key takeaway

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 অংশটি প্রথমে ডেটা নিয়ে আসে, তারপরের ধাপ সেটিকে বর্তমান ব্রাঞ্চে ইন্টিগ্রেট করে — ঠিক সংজ্ঞা অনুযায়ী।

অনুশীলন

  1. চিন্তা করুন: যদি dev_c push করার আগে 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 হয়ে যেত।

  2. পরীক্ষা করুন: কোড সেলে 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-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
মার্জ বনাম রিবেস — কখন কোনটা