পাঠ ৫৮ · ৫৮-এর মধ্যে · মডিউল ১৩
Home / Courses / Software Engineering Principles & Git / প্রজেক্ট ম্যানেজমেন্ট ও চূড়ান্ত প্রকল্প

ক্যাপস্টোন — একটি সম্পূর্ণ প্রজেক্ট Git ওয়ার্কফ্লো সহ সিমুলেট করা

Capstone — simulating an end-to-end project with a Git workflow
১৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • এই কোর্সের বিভিন্ন মডিউলের টুকরোগুলো বাস্তবে একসাথে কীভাবে কাজ করে, বিচ্ছিন্নভাবে নয়
  • কেন একটি বাস্তব ফিচার-ডেভেলপমেন্ট চক্রে প্রায়ই একটি সত্যিকারের ৩-ওয়ে মার্জ প্রয়োজন হয়, fast-forward নয়
  • SOLID/DIP ডিজাইন কীভাবে সরাসরি ইউনিট টেস্ট লেখা সহজ করে তোলে
  • একটি সম্পূর্ণ, সাত-ধাপের প্রজেক্ট লগ যা প্রতিটি ধাপের ফলাফল সত্যিকারের কোড-চালিত গণনা থেকে দেখায়

১ · এই ক্যাপস্টোন কী করে

এই পাঠে নতুন কোনো তত্ত্ব নেই — বরং এই কোর্সের ৫৭টি পাঠে শেখা প্রতিটি বড় টুকরো একটি একক, বাস্তব দৃশ্যপটে একসাথে বসানো হয়েছে: একটি ছোট টিম একটি "Task Manager" অ্যাপ্লিকেশনে একটি ট্যাগিং ফিচার যোগ করছে। নিচের ফ্লোচার্টে সাতটি ধাপ দেখানো হয়েছে — প্রতিটি ধাপ কোন মডিউলের উপকরণ পুনর্ব্যবহার করছে তা কোডেই বন্ধনীতে উল্লেখ করা আছে।

১. ইউজার স্টোরি ও AC (M3/L13) ২. SOLID ডিজাইন + DI (M4/L16-L18) ৩. ফিচার ব্রাঞ্চ + কমিট (M7-M9) ৪. TDD ইউনিট টেস্ট (M10/L45-L46) ৫. পুল রিকোয়েস্ট + রিভিউ (M9/L40) ৬. CI পাইপলাইন (M12/L53) ৭. চূড়ান্ত ২-প্যারেন্ট মার্জ (M8/L35)
সাতটি ধাপ, প্রতিটি একটি আগের মডিউলের নির্দিষ্ট উপকরণ পুনর্ব্যবহার করে — এই কোর্সের প্রকৃত সিন্থেসিস।
Git সিমুলেশন নোট: এই ব্রাউজার-স্যান্ডবক্সে কোনো real git বাইনারি নেই। নিচের কোড সেলের Git ওয়ার্কফ্লো অংশ (ধাপ ৩ ও ৭) M7-M9-এ প্রতিষ্ঠিত Repo/branches/ commits/parents মডেলের একটি from-scratch সিমুলেশন — real git-এর প্রকৃত অ্যালগরিদম (fast-forward চেক, merge-base খোঁজা, দুই-প্যারেন্ট মার্জ কমিট তৈরি) যথাযথভাবে অনুসরণ করে, কিন্তু সম্পূর্ণ Python-এ বাস্তবায়িত, কারণ এই স্যান্ডবক্সে real git বাইনারি চালানো সম্ভব নয়।

২ · সম্পূর্ণ সাত-ধাপ সিমুলেশন

নিচের একটি একক কোড সেলে পুরো দৃশ্যপট চলে — একটি ইউজার স্টোরি থেকে শুরু করে একটি চূড়ান্ত, মার্জড অবস্থা পর্যন্ত। প্রতিটি সংখ্যা (টেস্ট ফলাফল, মার্জ কমিটের parents, PR-এর অ্যাপ্রুভাল-গেট) কোডেই সত্যিকারের গণনা থেকে আসে।

Python
# ============================================================
# ক্যাপস্টোন -- একটি "Task Manager"-এ ট্যাগিং ফিচার যোগ করার
# সম্পূর্ণ, শেষ-থেকে-শুরু প্রজেক্ট সিমুলেশন (M3, M4, M7-M9, M10, M12)
# ============================================================

print("=" * 70)
print("ধাপ ১ -- ইউজার স্টোরি ও অ্যাকসেপ্টেন্স ক্রাইটেরিয়া (M3/L13)")
print("=" * 70)

class UserStory:
    def __init__(self, role, goal, reason, acceptance_criteria):
        self.role = role
        self.goal = goal
        self.reason = reason
        self.acceptance_criteria = acceptance_criteria

    def format_story(self):
        return f"একজন {self.role} হিসেবে, আমি চাই {self.goal}, যাতে {self.reason}।"

story = UserStory(
    role="প্রজেক্ট ম্যানেজার",
    goal="প্রতিটি টাস্ককে 'urgent' ট্যাগ দিয়ে চিহ্নিত করতে পারি",
    reason="জরুরি কাজগুলো দ্রুত আলাদা করে চেনা যায়",
    acceptance_criteria=[
        "AC1: add_tag(task, 'urgent') কল করলে টাস্কে ট্যাগ যুক্ত হয়",
        "AC2: get_tasks_by_tag('urgent') শুধু 'urgent' ট্যাগযুক্ত টাস্কগুলো রিটার্ন করে",
        "AC3: একই ট্যাগ দুইবার যোগ করলে ডুপ্লিকেট তৈরি হয় না",
    ],
)
print(story.format_story())
for ac in story.acceptance_criteria:
    print(f"  - {ac}")


print()
print("=" * 70)
print("ধাপ ২ -- SOLID-কমপ্লায়েন্ট ক্লাস ডিজাইন, DIP-সহ (M4/L16-L18)")
print("=" * 70)

class TaskRepository:  # abstract ইন্টারফেস -- DIP-এর abstraction
    def save(self, task):
        raise NotImplementedError
    def all(self):
        raise NotImplementedError

class InMemoryTaskRepository(TaskRepository):  # concrete ইমপ্লিমেন্টেশন
    def __init__(self):
        self._tasks = []
    def save(self, task):
        self._tasks.append(task)
    def all(self):
        return list(self._tasks)

class TaskService:  # high-level মডিউল -- শুধু abstraction-এর উপর নির্ভরশীল
    def __init__(self, repository: TaskRepository):  # constructor-এর মাধ্যমে dependency injection
        self.repository = repository

    def add_task(self, title, tags=None):
        task = {"title": title, "tags": set(tags or [])}
        self.repository.save(task)
        return task

    def add_tag(self, task, tag):
        task["tags"].add(tag)  # set ব্যবহারে AC3 (ডুপ্লিকেট প্রতিরোধ) স্বয়ংক্রিয়ভাবে পূরণ হয়

    def get_tasks_by_tag(self, tag):
        return [t for t in self.repository.all() if tag in t["tags"]]

print("TaskService(repository) -- কংক্রিট InMemoryTaskRepository constructor দিয়ে injected হয়েছে।")
print("(SRP: storage ও business logic আলাদা ক্লাসে। DIP: TaskService কোনো কংক্রিট ক্লাসকে সরাসরি জানে না।)")


print()
print("=" * 70)
print("ধাপ ৩ -- Git ওয়ার্কফ্লো: ফিচার ব্রাঞ্চ, কমিট, ও main-এর সমান্তরাল অগ্রগতি (M7-M9)")
print("=" * 70)

def make_repo():
    return {"commits": {}, "branches": {"main": None}, "head": "main"}

def current_commit(repo):
    ref = repo["head"]
    return repo["branches"][ref] if ref in repo["branches"] else ref

def git_commit(repo, message, snapshot):
    parent = current_commit(repo)
    parents = [parent] if parent else []
    chash = f"c{len(repo['commits']) + 1}"
    repo["commits"][chash] = {"message": message, "snapshot": dict(snapshot), "parents": parents}
    repo["branches"][repo["head"]] = chash
    return chash

def git_branch(repo, name):
    repo["branches"][name] = current_commit(repo)

def git_switch(repo, name):
    repo["head"] = name

def is_ancestor(repo, maybe_ancestor, start_hash):
    stack = [start_hash]
    while stack:
        cur = stack.pop()
        if cur == maybe_ancestor:
            return True
        if cur is None:
            continue
        stack.extend(repo["commits"][cur]["parents"])
    return False

def find_merge_base(repo, hash_a, hash_b):
    ancestors_a = set()
    stack = [hash_a]
    while stack:
        cur = stack.pop()
        if cur is None or cur in ancestors_a:
            continue
        ancestors_a.add(cur)
        stack.extend(repo["commits"][cur]["parents"])
    queue = [hash_b]
    seen = set()
    while queue:
        cur = queue.pop(0)
        if cur in ancestors_a:
            return cur
        if cur is None or cur in seen:
            continue
        seen.add(cur)
        queue.extend(repo["commits"][cur]["parents"])
    return None

def git_merge(repo, source_branch, message):
    target_branch = repo["head"]
    target_hash = repo["branches"][target_branch]
    source_hash = repo["branches"][source_branch]
    if is_ancestor(repo, target_hash, source_hash):
        repo["branches"][target_branch] = source_hash
        return source_hash, "fast-forward"
    base = find_merge_base(repo, target_hash, source_hash)
    merged_snapshot = {**repo["commits"][target_hash]["snapshot"], **repo["commits"][source_hash]["snapshot"]}
    chash = f"c{len(repo['commits']) + 1}"
    repo["commits"][chash] = {"message": message, "snapshot": merged_snapshot, "parents": [target_hash, source_hash]}
    repo["branches"][target_branch] = chash
    return chash, f"three-way merge (merge base={base}, 2 parents)"

repo = make_repo()
c1 = git_commit(repo, "প্রাথমিক প্রজেক্ট স্ক্যাফোল্ড", {"app.py": "# base app"})
print(f"  {c1} -- main-এ প্রাথমিক কমিট")

git_branch(repo, "feature/task-tags")
git_switch(repo, "feature/task-tags")
c2 = git_commit(repo, "TaskRepository ও TaskService ক্লাস যোগ (DIP-সহ)", {"task_service.py": "class TaskRepository: ..."})
c3 = git_commit(repo, "add_tag() ও get_tasks_by_tag() মেথড ইমপ্লিমেন্ট", {"task_service.py": "... add_tag ..."})
print(f"  {c2}, {c3} -- feature/task-tags-এ দুটি কমিট (main থেকে ব্রাঞ্চ করে)")

# রিভিউ প্রক্রিয়া চলাকালীন, অন্য একজন ডেভেলপারের কাজ ইতিমধ্যে main-এ মার্জ হয়ে গেছে
git_switch(repo, "main")
c4 = git_commit(repo, "README আপডেট (রিভিউ চলাকালীন অন্য ডেভেলপারের কাজ)", {"README.md": "..."})
print(f"  {c4} -- main স্বাধীনভাবে এগিয়ে গেছে রিভিউ চলাকালীন")
print(f"  ফলাফল: main({c4}) ও feature/task-tags({c3}) উভয়েই {c1} থেকে ডাইভার্জড -- সত্যিকারের ৩-ওয়ে মার্জ প্রয়োজন হবে।")


print()
print("=" * 70)
print("ধাপ ৪ -- TDD-স্টাইল ইউনিট টেস্ট স্যুট (M10/L45-L46)")
print("=" * 70)

def test_add_tag_adds_tag():
    repository = InMemoryTaskRepository()
    service = TaskService(repository)
    task = service.add_task("রিপোর্ট লেখা")
    service.add_tag(task, "urgent")
    assert "urgent" in task["tags"], "AC1 ব্যর্থ: ট্যাগ যোগ হয়নি"

def test_get_tasks_by_tag_filters_correctly():
    repository = InMemoryTaskRepository()
    service = TaskService(repository)
    t1 = service.add_task("A", tags=["urgent"])
    service.add_task("B", tags=[])
    result = service.get_tasks_by_tag("urgent")
    assert result == [t1], "AC2 ব্যর্থ: শুধু urgent ট্যাগযুক্ত টাস্ক আসা উচিত ছিল"

def test_duplicate_tag_not_added_twice():
    repository = InMemoryTaskRepository()
    service = TaskService(repository)
    task = service.add_task("C")
    service.add_tag(task, "urgent")
    service.add_tag(task, "urgent")
    assert len(task["tags"]) == 1, "AC3 ব্যর্থ: ডুপ্লিকেট ট্যাগ তৈরি হয়েছে"

TEST_SUITE = [
    ("test_add_tag_adds_tag (AC1)", test_add_tag_adds_tag),
    ("test_get_tasks_by_tag_filters_correctly (AC2)", test_get_tasks_by_tag_filters_correctly),
    ("test_duplicate_tag_not_added_twice (AC3)", test_duplicate_tag_not_added_twice),
]

def run_tests(suite):
    results = []
    for name, fn in suite:
        try:
            fn()
            results.append((name, "PASS"))
        except AssertionError as e:
            results.append((name, f"FAIL: {e}"))
    return results

test_results = run_tests(TEST_SUITE)
for name, status in test_results:
    print(f"  {status:6} -- {name}")
all_passed = all(status == "PASS" for _, status in test_results)
print(f"  সব টেস্ট পাস: {all_passed}")


print()
print("=" * 70)
print("ধাপ ৫ -- পুল রিকোয়েস্ট ও রিভিউ (M9/L40)")
print("=" * 70)

class PullRequest:
    def __init__(self, source_branch, target_branch):
        self.source_branch = source_branch
        self.target_branch = target_branch
        self.status = "open"
        self.reviews = {}  # reviewer -> সর্বশেষ রিভিউ (নতুন রিভিউ পুরনোটি সুপারসিড করে)

    def add_review(self, reviewer, decision, comment):
        self.reviews[reviewer] = {"decision": decision, "comment": comment}

    def can_merge(self):
        decisions = [r["decision"] for r in self.reviews.values()]
        return "approved" in decisions and "changes_requested" not in decisions

pr = PullRequest("feature/task-tags", "main")
pr.add_review("রহিম", "changes_requested", "duplicate-tag কেসের জন্য একটি টেস্ট মিসিং ছিল")
print(f"  রিভিউ ১ -- রহিম: changes_requested  |  can_merge() = {pr.can_merge()}")

# লেখক ফিডব্যাক অনুযায়ী duplicate-tag টেস্ট যোগ করেছেন (ধাপ ৪-এই আছে) -- পুনরায় রিভিউ চাওয়া হলো
pr.add_review("রহিম", "approved", "এখন সব ঠিক আছে, মার্জ করা যায়")
print(f"  রিভিউ ২ -- রহিম: approved            |  can_merge() = {pr.can_merge()}")


print()
print("=" * 70)
print("ধাপ ৬ -- CI পাইপলাইন রান (M12/L53)")
print("=" * 70)

class CIPipeline:
    def run_build(self, commit_label, results):
        passed = all(status == "PASS" for _, status in results)
        status = "PASSED" if passed else "FAILED"
        ok = sum(1 for _, s in results if s == "PASS")
        print(f"  CI [{commit_label}]: বিল্ড {status}  ({ok}/{len(results)} টেস্ট পাস)")
        return status

ci = CIPipeline()
ci_status = ci.run_build(f"feature/task-tags @ {c3}", test_results)


print()
print("=" * 70)
print("ধাপ ৭ -- চূড়ান্ত মার্জ (M8/L35) -- ৩-ওয়ে মার্জ, ২-প্যারেন্ট মার্জ কমিট")
print("=" * 70)

merge_type = None
if pr.can_merge() and ci_status == "PASSED":
    git_switch(repo, "main")
    merge_hash, merge_type = git_merge(repo, "feature/task-tags", "Merge PR: টাস্কে 'urgent' ট্যাগিং ফিচার যোগ")
    pr.status = "merged"
    print(f"  মার্জ সফল: {merge_hash}  ({merge_type})")
    print(f"  মার্জ কমিটের parents: {repo['commits'][merge_hash]['parents']}  (main={c4}, feature={c3} -- উভয়ই)")
else:
    print("  মার্জ করা যায়নি -- PR অ্যাপ্রুভড নয় অথবা CI ফেইল করেছে।")

print()
print("=" * 70)
print("সারসংক্ষেপ প্রজেক্ট লগ -- সাতটি ধাপ, চূড়ান্ত অবস্থা")
print("=" * 70)
print(f"  ১. ইউজার স্টোরি     : {story.format_story()}")
print(f"  ২. SOLID/DI ডিজাইন  : TaskService <-(injected)- InMemoryTaskRepository")
print(f"  ৩. Git ওয়ার্কফ্লো   : main {c1}->{c4}  |  feature/task-tags {c1}->{c2}->{c3}")
print(f"  ৪. টেস্ট স্যুট       : {sum(1 for _, s in test_results if s == 'PASS')}/{len(test_results)} পাস (all_passed={all_passed})")
print(f"  ৫. পুল রিকোয়েস্ট    : status={pr.status}, can_merge()={pr.can_merge()}")
print(f"  ৬. CI পাইপলাইন      : {ci_status}")
print(f"  ৭. চূড়ান্ত মার্জ     : main এখন {repo['branches']['main']}  ({merge_type})")

    
হাতে-যাচাই সারসংক্ষেপ: main কমিট করে c1→c4 (parent c1); feature ব্রাঞ্চ c1→c2→c3। মার্জের সময় is_ancestor(repo, c4, c3) মিথ্যা (c4, c3-এর ইতিহাসে নেই) — তাই fast-forward সম্ভব নয়, find_merge_base(c4, c3) সঠিকভাবে c1 রিটার্ন করে (উভয়ের সবচেয়ে সাম্প্রতিক কমন পূর্বপুরুষ), এবং নতুন মার্জ কমিট c5-এর parents = [c4, c3] — একটি প্রকৃত দুই-প্যারেন্ট মার্জ কমিট। তিনটি টেস্টই (AC1, AC2, AC3) পাস করে (আগের পাঠগুলোতে প্রতিটি হাতে-যাচাই করা হয়েছে), PR-এর can_merge() প্রথমে False (changes_requested) তারপর True (approved) হয়, এবং CI "PASSED" রিপোর্ট করে — ফলে সাতটি শর্তই পূরণ হয়ে চূড়ান্ত মার্জ সম্পন্ন হয়।
মূল কথা · Key takeaway

এই কোর্সের প্রতিটি মডিউল বিচ্ছিন্ন নয় — একটি বাস্তব ফিচার-ডেভেলপমেন্ট চক্র একসাথে রিকোয়ারমেন্ট এনালাইসিস, ভালো ক্লাস ডিজাইন, Git ব্রাঞ্চিং/মার্জিং, টেস্টিং, কোড রিভিউ, ও CI/CD ব্যবহার করে — এবং এই টুকরোগুলো একে অপরের উপর নির্ভরশীল (যেমন DIP-কমপ্লায়েন্ট ডিজাইন সরাসরি সহজ ইউনিট টেস্টিং সম্ভব করে তোলে)। এটিই "সফটওয়্যার ইঞ্জিনিয়ারিং" শব্দটির প্রকৃত অর্থ — শুধু কোড লেখা নয়, বরং এই সম্পূর্ণ, আন্তঃসংযুক্ত অনুশীলন।

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

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

প্র ০১ ফিচার ব্রাঞ্চকে main-এ মার্জ করতে কেন একটি সত্যিকারের ৩-ওয়ে মার্জ (দুই-প্যারেন্ট মার্জ কমিট) লাগল, একটি সাধারণ fast-forward নয় — এটি main-এ কী ঘটছিল তা সম্পর্কে কী বোঝায়?

fast-forward তখনই সম্ভব যখন current (target) ব্রাঞ্চের টিপ source ব্রাঞ্চের টিপের একটি পূর্বপুরুষ — অর্থাৎ main ফিচার ব্রাঞ্চ তৈরি হওয়ার পর থেকে একদমই নড়েনি। যেহেতু PR রিভিউ প্রক্রিয়া (M9/L40) কিছুটা সময় নিয়েছে, বাস্তবসম্মতভাবে এই সময়ে অন্য কাজও main-এ মার্জ হয়েছে (উদাহরণে c4) — অর্থাৎ main ও feature/task-tags উভয় ব্রাঞ্চই সত্যিকারের ডাইভার্জ করেছে c1 থেকে। এই ডাইভারজেন্স মানেই git-কে উভয় দিকের পরিবর্তন একটি নতুন, দুই-প্যারেন্ট মার্জ কমিটে একত্র করতে হয় — যা ঠিক এই দৃশ্যপটে বাস্তবসম্মতভাবে ঘটার কথা।

প্র ০২ TaskService-এর constructor-এ TaskRepository ইনজেক্ট করার সিদ্ধান্ত (DIP, M4/L18) কীভাবে ধাপ ৪-এর ইউনিট টেস্ট লেখা সহজ করে তুলল?

কারণ TaskService কখনো নিজে কংক্রিট InMemoryTaskRepository তৈরি করে না — এটি বাইরে থেকে পাস করা হয় (constructor-এর মাধ্যমে dependency injection)। ফলে প্রতিটি টেস্টে একটি তাজা, বিচ্ছিন্ন InMemoryTaskRepository() সহজেই তৈরি করে TaskService-এ পাস করা যায় — কোনো real ডেটাবেস, নেটওয়ার্ক কল, বা শেয়ার্ড স্টেট ছাড়াই। এটি সরাসরি M10/L44-এর ইউনিট-টেস্ট আইসোলেশন প্রয়োজনীয়তা এবং M10/L47-এর টেস্ট-ডাবল প্র্যাক্টিস সম্ভব করে তোলে — DIP ছাড়া এই ধরনের সহজ, বিচ্ছিন্ন টেস্টিং অনেক কঠিন হতো।

প্র ০৩ PR-এর can_merge() ইতিমধ্যেই True হওয়া সত্ত্বেও কেন চূড়ান্ত মার্জের আগে CI স্ট্যাটাসও আলাদাভাবে চেক করা হলো — একটি না থাকলে কী ঝুঁকি থাকত?

PR অ্যাপ্রুভাল (মানুষ রিভিউয়ারের মতামত) এবং CI পাস (স্বয়ংক্রিয় বিল্ড/টেস্ট যাচাই) — এই দুটো ভিন্ন, পরিপূরক কোয়ালিটি গেট, একে অপরের বিকল্প নয়। একজন মানব রিভিউয়ার ডিজাইন/লজিক সমস্যা দেখতে পারেন কিন্তু ভুলবশত একটি সূক্ষ্ম রানটাইম বাগ মিস করতে পারেন — CI সেই বাগ ধরবে প্রকৃতপক্ষে টেস্ট চালিয়ে। উল্টোদিকে, CI শুধু যা explicitly টেস্ট করা হয়েছে তা যাচাই করে, ডিজাইন কোয়ালিটি বা ব্যবসায়িক যুক্তিসঙ্গততা নয় — সেটা মানুষ রিভিউয়ারের কাজ। শুধু একটি গেট থাকলে, অন্যটি যা ধরত এমন সমস্যা কখনো কখনো অলক্ষিত থেকে যেত।

অনুশীলন

  1. চিন্তা করুন: যদি রহিমের প্রথম রিভিউ (changes_requested) দেওয়ার আগেই CI চালানো হতো এবং CI "PASSED" রিপোর্ট করত, তাহলে কি ধাপ ৭-এ মার্জ হয়ে যেত? কেন বা কেন নয়?

    না, মার্জ হতো না। ধাপ ৭-এর শর্ত হলো pr.can_merge() and ci_status == "PASSED" — দুটোই সত্যি হতে হবে। CI পাস করলেও, যতক্ষণ PR-এর সর্বশেষ রিভিউ decision "changes_requested" থাকে, can_merge() False থাকবে (যেহেতু "approved" এখনো decisions তালিকায় নেই)। এটি দেখায় দুটো গেটই স্বাধীনভাবে সত্য হতে হয় — একটি পাস করলেই অন্যটির প্রয়োজনীয়তা এড়ানো যায় না।

  2. পরীক্ষা করুন: উপরের কোড সেলে ধাপ ৩-এ main-এর দ্বিতীয় কমিট c4 করার আগে বদলে দেখুন — যদি main-এ কোনো নতুন কমিটই না হতো (শুধু feature/task-tags-এ কমিট হতো), ধাপ ৭-এর merge_type কী প্রিন্ট হতো? কোড থেকে বদলে Run করে যাচাই করুন।

    যদি main c1-এই থেকে যেত (c4 তৈরি না হতো), তাহলে মার্জের সময় is_ancestor(repo, c1, c3) সত্যি হতো (c1, c3-এর পূর্বপুরুষ, যেহেতু c3 এর chain c3→c2→c1)। ফলে git_merge fast-forward পথ নিত — main-এর পয়েন্টার সরাসরি c3-তে সরে যেত, কোনো নতুন মার্জ কমিট তৈরি না হয়েই। merge_type তখন "fast-forward" প্রিন্ট হতো, "three-way merge" নয় — ঠিক M8/L35-এর বর্ণনা অনুযায়ী।

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

আগের পাঠ
রিস্ক ম্যানেজমেন্ট ও টিম কোলাবোরেশন