Git, শাখা আর Pull Request — টিমে কাজের নিয়ম
git init থেকে push-pull, .gitignore দিয়ে .venv আর .env বাইরে রাখা, ছোট ফিচার-শাখা, পরিষ্কার কমিট মেসেজ আর Pull Request — সাথে git log পড়ে কমিট যাচাইয়ের একটি পাইথন টুল।
কীভাবে শিখতে চান?
যে সমস্যাটা আমরা সমাধান করছি
একজন এআই ইঞ্জিনিয়ার এক সপ্তাহ ধরে একটি টিকিট-বাছাই প্রজেক্টে কাজ করছেন: প্রম্পট বদলাচ্ছেন, কিওয়ার্ডের তালিকা বাড়াচ্ছেন, একটি ফাংশন নতুন করে লিখছেন। শুক্রবার দেখা গেল ফলাফল সোমবারের চেয়ে খারাপ। কোন বদলটি দোষী? ফোল্ডারে আছে triage.py, triage_v2.py, triage_final.py আর triage_final_REAL.py — কোনটিতে কী বদলেছিল, কেউ জানে না।
একই সময়ে একজন সহকর্মী শেয়ার করা ফোল্ডারে একই ফাইল সম্পাদনা করে তাঁর কাজ মুছে দিয়েছেন। আর মাসখানেক আগে কেউ পরীক্ষার জন্য একটি API কী সরাসরি কোডে বসিয়েছিলেন, যা সেই ফোল্ডার হয়ে আরও দশজনের কাছে পৌঁছেছে।
তিনটি সমস্যার একটাই উত্তর, আর পৃথিবীর প্রায় প্রতিটি সফটওয়্যার টিম সেটাই ব্যবহার করে: Git দিয়ে ইতিহাস রাখা, প্রতিটি কাজ আলাদা শাখায় করা, আর মূল কোডে কিছু ঢোকার আগে একটি Pull Request-এ অন্য কাউকে দিয়ে রিভিউ করানো। এই অধ্যায় সেই পুরো চক্রটি শেখায় — আর শেষে একটি পাইথন টুল লেখে, যা রিভিউয়ারের একটি কাজ স্বয়ংক্রিয় করে।
এই অধ্যায় শেষে আপনি পারবেন
git init,add,commit,push,pullদিয়ে একটি প্রজেক্টের ইতিহাস রাখতে ও শেয়ার করতে.gitignoreদিয়ে.venvআর.envরিপোজিটরির বাইরে রাখতে, আর ভুল করে কমিট হয়ে গেলে কী করতে হয় তা জানতে- ছোট, স্বল্পস্থায়ী ফিচার-শাখায় কাজ করে একটি পরিষ্কার Pull Request খুলতে
- রিভিউয়ার যেভাবে পড়েন সেভাবে কমিট মেসেজ লিখতে
subprocessদিয়েgit logপড়ে কমিট মেসেজ যাচাই করার একটি পাইথন টুল লিখতে
আগে যা জানা লাগবে: এআই ইঞ্জিনিয়ার ট্র্যাক আর uv দিয়ে ডেভ এনভায়রনমেন্ট — বিশেষ করে uv init আর uv run।
প্রথমবারের সেটআপ
Git প্রতিটি কমিটে লিখে রাখে কে কমিটটি করেছেন। তাই প্রথমবার একবার নিজের পরিচয় দিতে হয়, আর নতুন রিপোজিটরির প্রধান শাখার নাম main রাখতে বলে দেওয়া ভালো, কারণ বেশিরভাগ টিম এখন সেটাই ব্যবহার করে:
git config --global user.name "Nadia Karim"
git config --global user.email "[email protected]"
git config --global init.defaultBranch mainনিজের নাম আর যে ইমেইল দিয়ে আপনার কোড-হোস্টিং অ্যাকাউন্ট খোলা, সেটা দিন। শেষের লাইনটি না দিলে পুরনো Git সংস্করণ প্রধান শাখার নাম master রাখে — চলবে, কিন্তু টিমের বাকিদের সাথে মিলবে না।
তিনটি জায়গা: ফোল্ডার, স্টেজিং আর ইতিহাস
আগের অধ্যায়ে uv init নিজেই একটি Git রিপোজিটরি বানিয়েছিল। এই অধ্যায়ের উদাহরণ প্রজেক্টটি একই ভাবে শুরু:
uv init ticket-triage --python 3.12
cd ticket-triageGit-এর মাথায় তিনটি জায়গা আছে, আর প্রতিটি কমান্ড কোনো একটি থেকে অন্যটিতে কিছু সরায়:
- ওয়ার্কিং ট্রি — আপনার ফোল্ডার, যেখানে আপনি ফাইল সম্পাদনা করেন
- স্টেজিং এরিয়া — পরের কমিটে কী কী যাবে, তার তালিকা;
git addএখানে তোলে - ইতিহাস — কমিটগুলোর সারি;
git commitস্টেজিং এরিয়াকে একটি স্থায়ী কমিটে পরিণত করে
স্টেজিং আলাদা কেন? কারণ একটি কাজের মাঝে আপনি হয়তো পাঁচটি ফাইল বদলেছেন, কিন্তু তার দুটি একটি বাগ ঠিক করে আর তিনটি একটি নতুন ফিচার। স্টেজিং আপনাকে সেগুলো দুটি আলাদা, অর্থবহ কমিটে ভাগ করতে দেয় — রিভিউয়ারের কাছে এর মূল্য অনেক।
কী অবস্থায় আছে, তা দেখার কমান্ড git status, আর সংক্ষেপে git status --short।
.gitignore: যা কখনো রিপোজিটরিতে যাবে না
এআই প্রজেক্টে প্রায় সবসময় একটি .env ফাইল থাকে, যেখানে API কী রাখা হয়। ধরা যাক প্রজেক্টে এমন একটি ফাইল বানানো হলো (এখানে sk-... কেবল একটি প্লেসহোল্ডার, আসল কী নয়):
LLM_API_KEY=sk-...uv init যে .gitignore বানায়, তাতে .venv আছে, কিন্তু .env নেই। তাই প্রথম কাজ, শেষে এটি যোগ করা:
# Secrets
.envএবার uv run main.py একবার চালিয়ে (যাতে .venv আর uv.lock তৈরি হয়) অবস্থা দেখা যাক:
git status --short
?? .gitignore
?? .python-version
?? README.md
?? main.py
?? pyproject.toml
?? uv.lock?? মানে Git ফাইলটি দেখছে কিন্তু এখনো ট্র্যাক করছে না। তালিকায় যা নেই, সেটাই আসল খবর: .venv নেই, .env নেই। দুটিই ফোল্ডারে আছে, কিন্তু Git সেগুলো দেখেও না দেখার ভান করছে। (.venv-এর পাহারা আসলে দুই স্তরের: uv পরিবেশ বানানোর সময় তার ভেতরেও শুধু * লেখা একটি ছোট .gitignore রেখে দেয়।) আর uv.lock আছে — সেটা কমিট হওয়াই উচিত, কারণ সহকর্মীরা এটা দিয়েই হুবহু একই পরিবেশ পাবেন।
সহকর্মীরা তাহলে জানবেন কীভাবে কোন কোন কী লাগবে? প্রচলিত উপায় হলো একটি .env.example ফাইল কমিট করা, যেখানে নামগুলো থাকে কিন্তু মান থাকে না। নতুন কেউ সেটি কপি করে .env বানিয়ে নিজের কী বসান।
একবার কমিট হওয়া গোপন তথ্য পরের কমিটে মুছলে মোছে না। Git-এর কাজই পুরনো সব অবস্থা মনে রাখা, তাই আগের কমিটে কী-টি চিরকাল থেকে যায় — যে কেউgit showদিয়ে সেটা বের করতে পারেন। কী একবার কোনো রিমোটে পুশ হয়ে গেলে একমাত্র নিরাপদ পদক্ষেপ হলো যে সেবার কী, সেখানে গিয়ে কী-টি বাতিল করে নতুন একটি বানানো। এজন্যই.envফাইলটি বানানোর আগেই.gitignore-এ থাকা দরকার।
প্রথম কমিট, আর ভালো কমিট মেসেজ
git add .
git commit -m "chore: scaffold project with uv"
[main (root-commit) 0f78e59] chore: scaffold project with uv
6 files changed, 35 insertions(+)
create mode 100644 .gitignore
create mode 100644 .python-version
create mode 100644 README.md
create mode 100644 main.py
create mode 100644 pyproject.toml
create mode 100644 uv.lock0f78e59 হলো কমিটের পরিচয় (হ্যাশের প্রথম অংশ) — আপনার মেশিনে অন্যরকম হবে, কারণ এতে সময় আর লেখকের নামও মিশে থাকে।
কমিট মেসেজ ভবিষ্যতের পাঠকের জন্য লেখা — ছয় মাস পরের আপনি, আর আজকের রিভিউয়ার। অনেক টিম একটি প্রচলিত ধাঁচ মানে, যাকে বলে Conventional Commits:
type(scope): short summary in the imperative- type বলে কী ধরনের বদল:
feat(নতুন ক্ষমতা),fix(বাগ সারানো),docs,refactor,test,chore(রক্ষণাবেক্ষণ) - scope ঐচ্ছিক, বন্ধনীর ভেতরে প্রজেক্টের কোন অংশ:
feat(triage): ... - summary আদেশের সুরে («add», «fix», «remove» — «added» নয়), ৭২ অক্ষরের মধ্যে, শেষে দাঁড়ি ছাড়া
পাশাপাশি রাখলে পার্থক্য স্পষ্ট:
wip
fixed stuff
Fixed the bug where priority returned normal for tickets that say OUTAGE in capitals
fix(triage): lowercase ticket text before keyword matchingপ্রথম তিনটি কোনো ত্রুটি দেয় না — Git যেকোনো মেসেজই মেনে নেয়। কিন্তু git log-এ প্রথম দুটি কিছুই বলে না, আর তৃতীয়টি এত লম্বা যে এক লাইনের তালিকায় কেটে যায়। শেষেরটি পড়েই রিভিউয়ার জানেন কী বদলেছে আর কোথায়। «কেন» বদলেছে, সেটা লম্বা হলে মেসেজের বডিতে লেখা যায় — একটি ফাঁকা লাইনের পরে।
শাখা: প্রতিটি কাজ আলাদা ঘরে
main শাখাটি টিমের সবচেয়ে মূল্যবান জিনিস: যা সেখানে আছে, তা চলে, আর যেকোনো সময় ডিপ্লয় করা যায়। তাই কেউ সরাসরি main-এ কাজ করেন না। প্রতিটি কাজের জন্য একটি নতুন শাখা:
git switch -c feat/priority-score
Switched to a new branch 'feat/priority-score'এখানে triage.py লেখা হলো:
def priority(text: str) -> str:
urgent_words = ("outage", "down", "breach")
if any(word in text for word in urgent_words):
return "high"
return "normal"git add triage.py
git commit -m "feat(triage): add keyword-based priority"
[feat/priority-score ded4456] feat(triage): add keyword-based priority
1 file changed, 5 insertions(+)
create mode 100644 triage.pyশাখার নামেও একই ধাঁচ: feat/..., fix/..., docs/... — তালিকা দেখেই বোঝা যায় কোনটি কী।
শাখার কৌশল নিয়ে একটি কথা। এক সময় অনেক টিম দীর্ঘস্থায়ী কয়েকটি শাখা রাখত — develop, release, hotfix — আর সেগুলোর মধ্যে জটিল নিয়মে কোড সরাত। এখন বেশিরভাগ টিম সরল পথ নেয়: একটি main, আর তা থেকে বেরোনো ছোট, স্বল্পস্থায়ী ফিচার-শাখা, যা এক-দুই দিনের মধ্যে রিভিউ হয়ে main-এ ফিরে যায়। শাখা যত বেশি দিন আলাদা থাকে, main তত এগিয়ে যায়, আর ফেরার সময় সংঘাত তত বড় হয়। এই কোর্সে আমরা সরল পথটিই মানব।
রিমোট: push আর pull
এতক্ষণ সবকিছু আপনার মেশিনে। টিমের সাথে শেয়ার করতে একটি রিমোট রিপোজিটরি লাগে — সাধারণত GitHub বা এরকম কোনো সেবায়। সেখানে রিপোজিটরি খুললে সেবাটি একটি ঠিকানা দেয়, আর আপনি লেখেন:
git remote add origin https://github.com/<your-account>/ticket-triage.gitএই অধ্যায়ের ট্রান্সক্রিপ্টগুলো কোনো অনলাইন সেবা ছাড়াই যাচাই করা হয়েছে: পাশের ফোল্ডারে একটি «bare» রিপোজিটরি বানিয়ে সেটাকেই রিমোট হিসেবে ব্যবহার করা হয়েছে। Git-এর কাছে দুটি একই জিনিস, আর আপনিও নিজের মেশিনে এভাবে অনুশীলন করতে পারেন:
git init --bare ../remote.git
git remote add origin ../remote.gitপ্রথমবার পুশ করার সময় -u দিয়ে বলে দিতে হয় যে এই শাখা রিমোটের কোন শাখার সাথে যুক্ত:
git push -u origin main
branch 'main' set up to track 'origin/main'.
To ../remote.git
* [new branch] main -> mainফিচার-শাখাটিও একইভাবে:
git push -u origin feat/priority-score
branch 'feat/priority-score' set up to track 'origin/feat/priority-score'.
To ../remote.git
* [new branch] feat/priority-score -> feat/priority-scoreএবার একজন সহকর্মী রিপোজিটরিটি নামালেন:
git clone remote.git teammate
Cloning into 'teammate'...
done.তাঁর ফোল্ডারে আছে README.md, main.py, pyproject.toml, uv.lock (আর লুকানো ফাইলগুলো) — কিন্তু .venv নেই, .env-ও নেই। ঠিক যেমন হওয়ার কথা: তিনি uv sync চালিয়ে নিজের পরিবেশ বানাবেন, আর .env.example থেকে নিজের .env।
ফিচারটি main-এ মেশানোর পর (বাস্তবে এটা Pull Request মার্জ করার সময় ঘটে, নিচে দেখুন), সহকর্মী নতুন অবস্থা নামান:
git pull
Updating 0f78e59..5f802b4
Fast-forward
triage.py | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 triage.py(আসল আউটপুটের শুরুতে একটি From ... লাইনে রিমোটের পুরো পথ থাকে, এখানে বাদ দেওয়া হয়েছে।) Fast-forward মানে সহকর্মীর নিজের কোনো নতুন কমিট ছিল না, তাই Git শুধু তাঁর main-কে সামনে এগিয়ে দিয়েছে।
Issue আর Pull Request
কোম্পানিতে কাজ সাধারণত একটি issue দিয়ে শুরু হয় — কোড-হোস্টিং সেবায় লেখা একটি সমস্যার বিবরণ, যার একটি নম্বর থাকে। যেমন: #12 — Uppercase keywords are not detected («OUTAGE» লেখা টিকিট normal হিসেবে আসছে)।
তারপর চক্রটি এরকম:
mainথেকে একটি শাখা:git switch -c fix/uppercase-keywords- ছোট ছোট অর্থবহ কমিট
- শাখাটি পুশ
- সেবার ওয়েবসাইটে, বা কমান্ড লাইনে, একটি Pull Request (PR) খোলা — «আমার শাখার বদলগুলো
main-এ নিন» এই অনুরোধ - রিভিউয়ার মন্তব্য করেন, আপনি একই শাখায় নতুন কমিট দিয়ে উত্তর দেন
- অনুমোদন আর স্বয়ংক্রিয় পরীক্ষা (CI) পাস হলে PR মার্জ হয়, আর শাখাটি মুছে ফেলা হয়
GitHub-এর কমান্ড-লাইন টুল gh ব্যবহার করলে চতুর্থ ধাপটি এরকম (এর আউটপুট আপনার অ্যাকাউন্ট আর রিপোজিটরি অনুযায়ী আলাদা, তাই এখানে দেখানো হলো না):
gh pr create --base main --title "fix(triage): detect uppercase keywords" --body-file PR.mdএকটি পরিষ্কার PR-এর বিবরণ (PR.md) রিভিউয়ারের তিনটি প্রশ্নের উত্তর দেয় — কী, কেন, আর কীভাবে পরীক্ষা করা হলো:
## What
Lowercase ticket text before matching urgent keywords.
## Why
Tickets written in capitals ("OUTAGE") were scored as normal.
Closes #12
## How I tested
priority("Service DOWN") returned "normal" on main, "high" on this branch
uv run check_commits.py -> 0 of 2 commit messages need workCloses #12 লাইনটি একটি ছোট সুবিধা: PR প্রধান শাখায় মার্জ হলে সেবাটি issue #12 নিজে থেকেই বন্ধ করে দেয়।
মেন্টর কীভাবে রিভিউ করেন
এই কোর্সের প্রতিটি হাতে-কলমে কাজ একজন রিভিউয়ারের চোখ দিয়ে দেখা হবে, আর কোম্পানিতেও এই প্রশ্নগুলোই আসে:
- PR কি ছোট আর এক উদ্দেশ্যের? একটি ফিচার, একটি বাগ, বা একটি রিফ্যাক্টর — তিনটি একসাথে নয়। কয়েকশো লাইনের বেশি হলে রিভিউয়ের মান পড়ে যায়, কারণ মানুষ দীর্ঘ ডিফে মনোযোগ ধরে রাখতে পারেন না।
- কমিট মেসেজ কি ইতিহাস হিসেবে পড়া যায়?
wipআরfixদিয়ে ভরা ইতিহাস কোনো কাজে আসে না। - কী ঢোকেনি?
.venv,.env,__pycache__, বড় ডেটা ফাইল — এগুলো ডিফে দেখা গেলেই PR ফেরত যায়। - কী থাকা উচিত ছিল? নতুন প্যাকেজ যোগ হলে
pyproject.tomlআরuv.lock-এর বদল ডিফে থাকতে হবে। - বিবরণে কি বলা আছে কীভাবে পরীক্ষা করা হলো?
শেষ কমিটের মেসেজ ভুল হলে, পুশ করার আগে সেটা বদলানো যায়:
git commit --amend -m "docs: describe the project in README"
git log --oneline -1
a18cc1a docs: describe the project in READMEপুশ করার পর কমিট বদলানো মানে ইতিহাস বদলানো, যা সহকর্মীদের জন্য ঝামেলা তৈরি করে; সেক্ষেত্রে নতুন একটি কমিট দেওয়াই ভালো। অনেক টিম PR মার্জ করে «squash» করে — পুরো শাখার সব কমিট একটি কমিটে মিশে যায়, আর PR-এর শিরোনামটি তার মেসেজ হয়। তখন PR-এর শিরোনামও একই ধাঁচে লেখা জরুরি।
একটা সম্পূর্ণ উদাহরণ
রিভিউয়ারের একটি কাজ যন্ত্রকে দেওয়া যায়: শাখার প্রতিটি কমিট মেসেজ নিয়ম মানছে কি না দেখা। টিমগুলো এমন পরীক্ষা CI-তে চালায়, যাতে PR খোলার সাথে সাথেই ফল আসে। নিচের check_commits.py ঠিক সেটা করে — git log চালিয়ে শুধু বর্তমান শাখার কমিটগুলো পড়ে, যেগুলো main-এ নেই:
import re
import subprocess
import sys
ALLOWED_TYPES = {"feat", "fix", "docs", "refactor", "test", "chore"}
SUBJECT = re.compile(r"^(?P<type>[a-z]+)(\([a-z0-9-]+\))?: (?P<summary>.+)$")
MAX_LENGTH = 72
VAGUE = {"wip", "fix", "update", "changes", "stuff", "misc"}
def problems_in(subject: str) -> list[str]:
found = []
if subject.strip().lower() in VAGUE:
found.append("too vague for a reviewer")
if len(subject) > MAX_LENGTH:
found.append(f"longer than {MAX_LENGTH} characters ({len(subject)})")
match = SUBJECT.match(subject)
if match is None:
found.append("missing 'type: summary' prefix")
elif match["type"] not in ALLOWED_TYPES:
found.append(f"unknown type '{match['type']}'")
if subject.endswith("."):
found.append("ends with a period")
return found
def branch_subjects(base: str) -> list[str]:
result = subprocess.run(
["git", "log", "--reverse", "--format=%s", f"{base}..HEAD"],
capture_output=True,
encoding="utf-8",
)
if result.returncode != 0:
raise SystemExit(f"git log failed: {result.stderr.strip()}")
return result.stdout.splitlines()
def main() -> int:
subjects = branch_subjects("main")
failures = 0
for subject in subjects:
problems = problems_in(subject)
print(("ok " if not problems else "FAIL") + " " + subject)
for problem in problems:
print(f" - {problem}")
failures += 1 if problems else 0
print(f"{failures} of {len(subjects)} commit messages need work")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())পরীক্ষার জন্য main থেকে একটি শাখা বানিয়ে পাঁচটি কমিট দেওয়া হলো — ইচ্ছে করে কয়েকটি খারাপ মেসেজসহ। (--allow-empty কোনো ফাইল না বদলেই কমিট বানায়; মেসেজ যাচাইয়ের জন্য সেটাই যথেষ্ট। আসল কাজে প্রতিটি কমিটে কোডের বদল থাকবে।)
git switch -c fix/uppercase-keywords
git commit --allow-empty -m "wip"
git commit --allow-empty -m "Fixed the bug where priority returned normal for tickets that say OUTAGE in capitals"
git commit --allow-empty -m "fix(triage): lowercase text before matching."
git commit --allow-empty -m "feature: add tests"
git commit --allow-empty -m "test(triage): cover uppercase keywords"তারপর রিপোজিটরির ভেতর থেকে:
uv run check_commits.pyFAIL wip
- too vague for a reviewer
- missing 'type: summary' prefix
FAIL Fixed the bug where priority returned normal for tickets that say OUTAGE in capitals
- longer than 72 characters (84)
- missing 'type: summary' prefix
FAIL fix(triage): lowercase text before matching.
- ends with a period
FAIL feature: add tests
- unknown type 'feature'
ok test(triage): cover uppercase keywords
4 of 5 commit messages need workপ্রোগ্রামটির এক্সিট কোড 1, কারণ অন্তত একটি মেসেজ ব্যর্থ। সব ঠিক থাকলে 0।
এই স্ক্রিপ্টের সিদ্ধান্তগুলো:
main..HEAD — দুই বিন্দুর এই রেঞ্জের মানে «HEAD-এ আছে কিন্তু main-এ নেই এমন কমিট»। অর্থাৎ কেবল এই শাখার নিজের কাজ। পুরো ইতিহাস পরীক্ষা করলে বছর পুরনো, অন্যদের লেখা কমিটের জন্যও আপনার PR ব্যর্থ হতো। --reverse পুরনো থেকে নতুন ক্রমে সাজায়, যে ক্রমে কাজটি হয়েছিল।
--format=%s — %s মানে শুধু কমিটের প্রথম লাইন (subject)। হ্যাশ, তারিখ, লেখক কিছুই নয় — তাই আউটপুট পার্স করার দরকারই পড়ে না, প্রতিটি লাইন একটি মেসেজ।
encoding="utf-8" — text=True দিলে পাইথন অপারেটিং সিস্টেমের ডিফল্ট এনকোডিং ব্যবহার করে, যা উইন্ডোজে প্রায়ই UTF-8 নয়। কেউ বাংলায় বা ইমোজি দিয়ে কমিট মেসেজ লিখলে তখন আউটপুট ভেঙে যেতে পারে। এনকোডিং স্পষ্ট বলে দেওয়া সেই অনিশ্চয়তা দূর করে।
check=True নয়, নিজের বার্তা — git ব্যর্থ হলে (রিপোজিটরির বাইরে চালালে, বা main নামে শাখা না থাকলে) পাইথনের দীর্ঘ ট্রেসব্যাক না দেখিয়ে git-এর নিজের ত্রুটির বার্তাটিই দেখানো হয়। SystemExit-এ একটি স্ট্রিং দিলে পাইথন সেটি stderr-এ ছাপে আর এক্সিট কোড 1 দিয়ে থামে।
এক্সিট কোড — CI কোনো আউটপুট পড়ে না; সে শুধু দেখে প্রোগ্রাম 0 দিয়ে শেষ হলো কি না। তাই main() একটি সংখ্যা ফেরত দেয়, আর sys.exit সেটাই অপারেটিং সিস্টেমকে জানায়।
কিছু ভাঙা অবস্থা ও তার সমাধান
Author identity unknown তারপর fatal: unable to auto-detect email address Git জানে না কমিটটি কার নামে লিখবে। অধ্যায়ের শুরুর দুটি git config --global user.name ... আর user.email ... কমান্ড একবার চালান।
error: src refspec main does not match any এখনো কোনো কমিট হয়নি, তাই main শাখাটি আসলে তৈরিই হয়নি — পুশ করার কিছু নেই। আগে git add আর git commit, তারপর পুশ। শাখার নাম master হলে একই বার্তা আসে; git branch দিয়ে দেখুন আপনি কোন শাখায়।
fatal: The current branch feat/x has no upstream branch. নতুন শাখা প্রথমবার পুশ হচ্ছে, আর Git জানে না রিমোটের কোন শাখায় পাঠাবে। বার্তাটি নিজেই সমাধান বলে দেয়: git push --set-upstream origin feat/x (সংক্ষেপে git push -u origin feat/x)। পরের বার থেকে শুধু git push।
! [rejected] main -> main (fetch first) আপনি শেষবার নামানোর পর অন্য কেউ একই শাখায় পুশ করেছেন। আগে git pull দিয়ে তাঁর কাজ নিজের সাথে মেলান, তারপর পুশ। git push --force দিয়ে «সমাধান» করবেন না — সেটা সহকর্মীর কাজ মুছে দেয়।
warning: in the working copy of 'main.py', LF will be replaced by CRLF the next time Git touches it উইন্ডোজে এটি একটি সতর্কবার্তা, ত্রুটি নয়। উইন্ডোজ আর macOS/লিনাক্স লাইনের শেষ আলাদা চিহ্ন দিয়ে লেখে, আর Git সেটা মিলিয়ে নিচ্ছে বলে জানাচ্ছে। কমিট ঠিকঠাক হয়েছে।
.env ভুল করে কমিট হয়ে গেছে git rm --cached .env চালান — এটি ফাইলটি ডিস্কে রেখে শুধু Git-এর ট্র্যাকিং থেকে সরায় (আউটপুট rm '.env') — তারপর .gitignore-এ .env যোগ করে কমিট দিন। কিন্তু মনে রাখবেন, আগের কমিটে কী-টি থেকেই গেছে। পুশ হয়ে থাকলে কী-টি বাতিল করে নতুন বানানো ছাড়া নিরাপদ উপায় নেই।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
অধ্যায়ের SUBJECT প্যাটার্ন দিয়ে তিনটি মেসেজ যাচাই করা হলো। কী ছাপা হবে?
import re
SUBJECT = re.compile(r"^(?P<type>[a-z]+)(\([a-z0-9-]+\))?: (?P<summary>.+)$")
for subject in ["fix: handle empty input", "Fix: handle empty input", "fix:handle empty input"]:
print(SUBJECT.match(subject) is not None)- A`True`, `False`, `True`
- B`True`, `True`, `True`
- C`True`, `True`, `False`
- D`True`, `False`, `False`
একজন সহকর্মী check_commits.py-এর এই ফাংশনটি «ঠিক» করেছেন। এখন wip আর feature: add tests কমিটওয়ালা একটি শাখাতেও CI সবুজ, আর আউটপুট 0 of 0 commit messages need work। ভুলটি কোথায়?
import subprocess
def branch_subjects(base: str) -> list[str]:
result = subprocess.run(
["git", "log", "--reverse", "--format=%s", f"HEAD..{base}"],
capture_output=True,
encoding="utf-8",
)
if result.returncode != 0:
raise SystemExit(f"git log failed: {result.stderr.strip()}")
return result.stdout.splitlines()- Aরেঞ্জ উল্টে গেছে: `HEAD..main` মানে «`main`-এ আছে কিন্তু এই শাখায় নেই», তাই শাখার নিজের কমিট একটিও আসে না; হওয়া উচিত `main..HEAD`
- B`--reverse` কমিটগুলো বাদ দিয়ে দেয়; সেটি মুছে দিলেই কমিট দেখা যাবে
- C`returncode` পরীক্ষাটি সব কমিট ফেলে দিচ্ছে; `check=True` লাগবে
- D`encoding="utf-8"` দিলে `git` আউটপুট দেয় না; `text=True` লাগবে
একটি PR-এর বিবরণে Closes #12 লেখা আছে। এতে কী হয়?
- APR প্রধান শাখায় মার্জ হলে কোড-হোস্টিং সেবা issue #12 নিজে থেকে বন্ধ করে, আর দুটির মধ্যে সংযোগ রেখে দেয়
- BPR খোলার সাথে সাথেই issue #12 বন্ধ হয়ে যায়
- Cকিছুই না — এটি শুধু রিভিউয়ারের জন্য একটি মন্তব্য, কোনো যন্ত্র এটি পড়ে না
- DGit কমিট ইতিহাস থেকে ১২ নম্বর কমিটটি মুছে দেয়
প্লে প্রো-এর অংশ
+30 XP অপেক্ষায়
প্রো দিয়ে এই অধ্যায়টি খেলুন
পড়া ফ্রি, পুরো অধ্যায়টিই খোলা। প্রতিটি ফ্রি কোর্সের প্রথম 5টি অধ্যায় ফ্রিতে খেলা যায়। অধ্যায় 6 থেকে প্রশ্নের উত্তর দেওয়া, XP পাওয়া আর স্ট্রিক বাড়ানো প্রো-এর অংশ।
প্রো দিয়ে প্লে চালু করুননিজে করুন
নিজের মেশিনে একটি প্রজেক্ট বানিয়ে পুরো চক্রটি একবার চালান:
uv init my-triage --python 3.12দিয়ে প্রজেক্ট বানান,.gitignore-এ.envযোগ করুন, একটি.env.exampleফাইল রাখুন (শুধুLLM_API_KEY=লেখা), আরgit status --short-এ.envআর.venvযে দেখা যাচ্ছে না তা নিশ্চিত করে প্রথম কমিট দিন- একটি bare রিপোজিটরিকে রিমোট হিসেবে যোগ করে (
git init --bare ../my-triage-remote.git)mainপুশ করুন feat/priority-scoreনামে শাখা বানিয়ে এই অধ্যায়েরtriage.pyআরcheck_commits.pyযোগ করুন, ঠিক তিনটি কমিটে, প্রতিটি নিয়ম মেনে; শাখাটি পুশ করুন- একটি
PR.mdলিখুন — What, Why, How I tested — আর আপনার কোড-হোস্টিং অ্যাকাউন্ট থাকলে সেখানে একটি আসল PR খুলে এই বিবরণটি দিন
শাখায় দাঁড়িয়ে uv run check_commits.py চালালে শেষ লাইনটি ঠিক এরকম হওয়া উচিত, আর তার আগে তিনটি ok লাইন:
0 of 3 commit messages need workতারপর ইচ্ছে করে একটি খারাপ কমিট দিন (git commit --allow-empty -m "wip"), আবার চালিয়ে দেখুন কোন দুটি সমস্যা ধরা পড়ে, আর git commit --amend -m "..." দিয়ে সেটি ঠিক করুন। অথবা চেষ্টা করে দেখুন: স্ক্রিপ্টে main..HEAD-এর বদলে HEAD..main লিখলে কী ছাপা হয়, আর কেন সেটা একটি CI পরীক্ষার জন্য সবচেয়ে বিপজ্জনক ধরনের ভুল।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
প্রো দিয়ে খেলুন