পাঠ ৩১ · ৫৮-এর মধ্যে · মডিউল ৭
Home / Courses / Software Engineering Principles & Git / Git ফাউন্ডেশন

git status, diff ও log

git status, diff & log
৭ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • git status ঠিক কীভাবে তিনটি ট্রি তুলনা করে প্রতিটি ফাইলকে ক্যাটাগরাইজ করে
  • git diff-এর দুটি ভিন্ন মোড — unstaged বনাম --staged — এবং তাদের নির্দিষ্ট পার্থক্য
  • git log কীভাবে parent চেইন অনুসরণ করে ইতিহাস দেখায়
  • L30-এর Repo মডেল সম্প্রসারিত করে বাস্তব git_status ও git_diff ফাংশন লেখা

১ · git status — এই মুহূর্তে আমার অবস্থা কী?

L30-এর থ্রি-ট্রি মডেল এখন আমরা দৈনন্দিন ব্যবহারে কাজে লাগাব। git statusgit statusতিনটি ট্রি — Working Directory, Staging Area, ও সর্বশেষ কমিট — তুলনা করে প্রতিটি ফাইলের বর্তমান অবস্থা রিপোর্ট করে। হলো তিনটি ট্রির মধ্যে বর্তমান সম্পর্ক রিপোর্ট করার কমান্ড — কোন ফাইল Working Directory-তে বদলেছে কিন্তু এখনও stage হয়নি, কোনটি staged ও কমিট করার জন্য প্রস্তুত, আর কোনটি "untracked" (একদম নতুন ফাইল, যা git এখনও চেনেই না)। এটি genuinely সবচেয়ে বেশি চালানো git কমান্ড — কারণ এটি "আমার এই মুহূর্তের পরিস্থিতি কী?" প্রশ্নের সরাসরি উত্তর দেয়।

২ · git diff — ঠিক কী বদলেছে

git diffgit diffলাইন-বাই-লাইন সঠিক পার্থক্য দেখায় — ডিফল্টে Working Directory বনাম Staging Area, --staged দিলে Staging Area বনাম সর্বশেষ কমিট। হলো সঠিক, লাইন-বাই-লাইন পার্থক্য দেখানোর কমান্ড — এখানে dual-mode আচরণটি স্পষ্টভাবে জানা জরুরি, এটি একটি সাধারণ বিভ্রান্তির জায়গা:

git diff (ডিফল্ট)
Working Directory বনাম Staging Area — অর্থাৎ unstaged পরিবর্তন দেখায়।
git diff --staged
Staging Area বনাম সর্বশেষ কমিট — অর্থাৎ staged, কমিট হওয়ার অপেক্ষায় থাকা পরিবর্তন দেখায়।

৩ · git log — কমিট-ইতিহাস

git loggit logparent পয়েন্টার ধরে পেছনের দিকে হেঁটে কমিট-ইতিহাস দেখায় — সাধারণত প্রতিটি কমিটের hash, author, date, ও message দেখিয়ে। হলো কমিট-ইতিহাস দেখানোর কমান্ড — প্রতিটি কমিটের parent পয়েন্টার ধরে পেছনের দিকে হেঁটে (L01-এর সেই সরলীকৃত প্রিভিউ মনে আছে? এটি এখন সেই একই ধারণার real, নির্ভুল ফরমালাইজেশন), সাধারণত প্রতিটি কমিটের hash, লেখক, তারিখ, ও মেসেজ দেখিয়ে।

নিচের কোড সেলে L30-এর ঠিক একই Repo মডেল (একই make_repo, git_add, git_commit) সম্প্রসারিত করে বাস্তব git_status, git_diff, ও git_log ফাংশন লেখা হয়েছে।

Python
# L30-এর ঠিক একই থ্রি-ট্রি Repo মডেল সম্প্রসারিত করা হচ্ছে -- বাস্তব git বাইনারি নেই,
# তাই status/diff/log নিজেরাই তিনটি dict তুলনা করে বাস্তবায়ন করা হলো।

def make_repo():
    return {"working_directory": {}, "staging_area": {}, "commits": {}, "head": None}

def git_add(repo, filename):
    repo["staging_area"][filename] = repo["working_directory"][filename]

def git_commit(repo, message):
    commit_hash = f"c{len(repo['commits']) + 1}"
    snapshot = dict(repo["staging_area"])
    repo["commits"][commit_hash] = {"message": message, "snapshot": snapshot, "parent": repo["head"]}
    repo["head"] = commit_hash
    repo["staging_area"] = {}
    return commit_hash


def git_status(repo):
    committed = repo["commits"][repo["head"]]["snapshot"] if repo["head"] else {}
    all_files = set(repo["working_directory"]) | set(repo["staging_area"]) | set(committed)
    status = {}
    for f in sorted(all_files):
        wd = repo["working_directory"].get(f)
        staged = repo["staging_area"].get(f)
        head = committed.get(f)
        if f not in repo["staging_area"] and f not in committed:
            status[f] = "untracked"
        elif wd != staged:
            status[f] = "modified (not staged)"
        elif staged != head:
            status[f] = "staged"
        else:
            status[f] = "no changes"
    return status

def git_diff(repo, filename, staged=False):
    committed = repo["commits"][repo["head"]]["snapshot"] if repo["head"] else {}
    if staged:
        old = committed.get(filename, "")
        new = repo["staging_area"].get(filename, "")
    else:
        old = repo["staging_area"].get(filename, committed.get(filename, ""))
        new = repo["working_directory"].get(filename, "")
    old_lines, new_lines = old.split("\n"), new.split("\n")
    added = [l for l in new_lines if l not in old_lines]
    removed = [l for l in old_lines if l not in new_lines]
    return {"added": added, "removed": removed}

def git_log(repo):
    entries = []
    current = repo["head"]
    while current:
        c = repo["commits"][current]
        entries.append((current, c["message"]))
        current = c["parent"]
    return entries


# --- একটি বাস্তব multi-file পরিস্থিতি তৈরি করা হচ্ছে ---
repo = make_repo()
repo["working_directory"]["staged.txt"] = "v1"
repo["working_directory"]["modified.txt"] = "লাইন এক\nলাইন দুই\nলাইন তিন"
git_add(repo, "staged.txt")
git_add(repo, "modified.txt")
git_commit(repo, "প্রাথমিক কমিট")

repo["working_directory"]["staged.txt"] = "v2 (পরের কমিটের জন্য স্টেজড)"
git_add(repo, "staged.txt")                      # staged.txt: staged

repo["working_directory"]["modified.txt"] = "লাইন এক\nলাইন দুই - সম্পাদিত\nলাইন তিন"  # modified, unstaged

repo["working_directory"]["untracked.txt"] = "একদম নতুন ফাইল"                       # untracked

print("== git status ==")
for f, s in git_status(repo).items():
    print(f"  {f}: {s}")

print()
print("== git diff (unstaged, modified.txt) ==")
d = git_diff(repo, "modified.txt")
print("  removed:", d["removed"])
print("  added:  ", d["added"])

print()
print("== git diff --staged (staged.txt) ==")
d2 = git_diff(repo, "staged.txt", staged=True)
print("  removed:", d2["removed"])
print("  added:  ", d2["added"])

print()
print("== git log ==")
for h, msg in git_log(repo):
    print(f"  {h}  {msg}")

    
তিনটি ফাইলের তিনটি আলাদা অবস্থা লক্ষ্য করুন: staged.txt stage করা হয়েছে এবং তারপর আর Working Directory-তে বদলানো হয়নি, তাই এটি ঠিক staged। modified.txt commit হওয়ার পর আবার এডিট হয়েছে কিন্তু git_add করা হয়নি, তাই এটি modified (not staged)। untracked.txt কখনো git_add-ই করা হয়নি এবং আগের কোনো কমিটেও ছিল না, তাই এটি untracked — git_status ফাংশনটি এই তিনটিকে সঠিকভাবে আলাদা করেছে, শুধু তিনটি dict তুলনা করেই।
মূল কথা · Key takeaway

git status, git diff, ও git log — তিনটিই L30-এর থ্রি-ট্রি মডেলের ওপর সরাসরি প্রশ্ন — কোনোটিই নতুন কোনো ডেটা তৈরি করে না, শুধু বিদ্যমান তিনটি ট্রি (বা তাদের parent চেইন) তুলনা/ট্রাভার্স করে। এই তিনটি কমান্ড ভালোভাবে বোঝা মানে থ্রি-ট্রি মডেলটিই সত্যিকার অর্থে আয়ত্ত করা — এখান থেকে M7-এর বাকি পাঠ (L32-L33) ও M8-এর ব্রাঞ্চিং একই ভিত্তির ওপর দাঁড়াবে।

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

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

প্র ০১ কোড সেলে modified.txt-কে "modified (not staged)" হিসেবে চেনার শর্তটি ছিল wd != staged। এখানে staged ভ্যারিয়েবলটির মান আসলে কোথা থেকে এসেছে, যেহেতু ফাইলটি এবার কখনো git_add করা হয়নি?

যেহেতু কমিটের পর staging area খালি হয়ে গিয়েছিল এবং modified.txt আর কখনো git_add করা হয়নি, তাই repo["staging_area"].get("modified.txt") রিটার্ন করে None। ফাংশনটির staged ভ্যারিয়েবলটির মান তাই None, যা Working Directory-র বর্তমান কনটেন্টের সাথে মেলে না (wd != staged সত্য) — ফলে সঠিকভাবে "modified (not staged)" ধরা পড়ে।

প্র ০২ git diff (ডিফল্ট, unstaged) আর git diff --staged — একই ফাইলে দুটো কমান্ড ভিন্ন ফলাফল দিতে পারে কেন, এবং এই পার্থক্যটা প্র্যাকটিক্যালি কখন গুরুত্বপূর্ণ?

কারণ তারা ভিন্ন জোড়া ট্রি তুলনা করে — ডিফল্ট git diff Working Directory বনাম Staging Area তুলনা করে (আপনি এখনো stage করেননি এমন পরিবর্তন), আর --staged Staging Area বনাম সর্বশেষ কমিট তুলনা করে (আপনি ইতিমধ্যে stage করেছেন এমন পরিবর্তন)। এটি গুরুত্বপূর্ণ যখন একই ফাইলে আপনি প্রথমে কিছু বদল stage করেছেন, তারপর আরও বদল করেছেন কিন্তু সেগুলো stage করেননি — তখন দুটো কমান্ড সত্যিই সম্পূর্ণ ভিন্ন পরিবর্তনের সেট দেখাবে।

প্র ০৩ কোড সেলে git_log মাত্র একটি কমিট (c1) দেখাচ্ছে, যদিও repo-তে অনেক পরিবর্তন হয়েছে। কেন?

কারণ পুরো স্ক্রিপ্টে git_commit মাত্র একবারই কল করা হয়েছে (প্রাথমিক কমিটে) — পরের সব পরিবর্তন (staged.txt-এর নতুন এডিট ও তার stage, modified.txt-এর এডিট, untracked.txt যোগ) কোনোটিই git_commit কল করে "চূড়ান্ত" করা হয়নি। L30-এর মডেল অনুযায়ী git_log শুধু Repository ট্রি-র ওপর কাজ করে, Working Directory বা Staging Area-র কোনো এখনো-committed-না-হওয়া অবস্থার ওপর নয়।

অনুশীলন

  1. চিন্তা করুন: একজন নতুন ডেভেলপার একটি ফাইল এডিট করে সরাসরি git commit চালিয়েছে, কিন্তু আগে কখনো git add করেনি — কমিট কি সফল হবে, এবং এর ফলাফল কী হতে পারে? (ইঙ্গিত: L30-এর git_commit ফাংশনটি কোথা থেকে স্ন্যাপশট নেয় তা মনে করুন।)

    যদি ফাইলটির জন্য এর আগে কখনো কিছু staged না থাকে, তাহলে সেই ফাইলটির পরিবর্তন কমিটে যাবেই না — git commit সবসময় Staging Area থেকে স্ন্যাপশট নেয়, Working Directory থেকে সরাসরি নয় (বাস্তব git-এ, যদি স্টেজিং এরিয়াতে কোনো পরিবর্তনই না থাকে, git আসলে "nothing to commit" বলে সম্পূর্ণ প্রত্যাখ্যান করে)। এটিই git status এত ঘন ঘন চালানোর genuine, প্র্যাকটিক্যাল কারণ — কমিট করার আগে নিশ্চিত হওয়া ঠিক কী staged আছে।

  2. পরীক্ষা করুন: উপরের কোড সেলে untracked.txt-এর ওপর git_diff(repo, "untracked.txt") কল করে Run চাপুন — added/removed লিস্টে কী দেখা যায় এবং কেন, তা ব্যাখ্যা করুন।

    old হবে খালি স্ট্রিং "" (কারণ untracked.txt না staging area-তে না কমিটে আছে), আর new হবে Working Directory-র কনটেন্ট — ফলে removed খালি থাকবে, আর added-এ পুরো ফাইলের কনটেন্ট (লাইন হিসেবে ভাগ করা) দেখা যাবে, যেন পুরো ফাইলটিই "নতুন যোগ হওয়া" — যা যৌক্তিক, কারণ untracked ফাইল আসলে git-এর দৃষ্টিতে সম্পূর্ণ নতুন।

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

আগের পাঠ
git init, add, commit — থ্রি-ট্রি আর্কিটেকচার