رفتن به محتوا
LoopX

روش‌های کار تیمی

توی این درس یاد می‌گیری قرارداد‌هایی که تیم‌های حرفه‌ای استفاده می‌کنند: GitHub Flow (یک شاخه‌ی اصلی همیشه سالم و شاخه‌های کوتاه‌عمر با PR) در مقایسه با Git Flow و trunk-based، Conventional Commits (git commit -m "feat: ..."، fix:، BREAKING CHANGE) و اینکه چطور شماره‌ی نسخه را خودکار می‌دهد، اسم‌گذاری شاخه‌ها، اعمال این قاعده‌ها با hook ها، و محافظت از main (branch protection) تا هیچ‌کس مستقیم به آن push نکند. تمرین: برای مخزنت قانون محافظت از main تنظیم کن.

مسئله: قاعده‌ی نانوشته، قاعده‌ی شکسته‌شده

Section titled “مسئله: قاعده‌ی نانوشته، قاعده‌ی شکسته‌شده”

یک تیم پنج‌نفره بدون قرارداد: یکی شاخه‌اش را new-stuff می‌نامد و یکی Sara_final2؛ یکی commit می‌زند «fix» و دیگری «تغییرات». چند نفر مستقیم به main push می‌کنند و یک روز یک commit خراب سایت را می‌خواباند. هیچ‌کس نمی‌داند «نسخه‌ی بعدی» چه چیزی دارد و git log یک فهرست بی‌معنی است. قرارداد چیزی نیست جز چند قاعده‌ی ساده که همه رعایت می‌کنند؛ و بهترین قاعده آن است که ابزار آن را اعمال کند، نه حافظه‌ی آدم‌ها.

همه می‌توانند رانندگی کنند، ولی اگر هر کس از سمتی برود تصادف می‌شود. قوانین رانندگی آزادی را کم نمی‌کنند؛ پیش‌بینی‌پذیری می‌آورند: همه می‌دانند چراغ قرمز یعنی ایست. قرارداد تیمی هم همین است: معنی نام یک شاخه، شکل یک پیام commit و راه ادغام، برای همه یکی است.

سه الگوی کار با شاخه‌ها

Section titled “سه الگوی کار با شاخه‌ها”
GitHub Flow ساده است: یک main همیشه سالم و شاخه‌های کوتاه‌عمر که با PR ادغام می‌شوند. Git Flow شاخه‌های بیشتری دارد (develop، release، hotfix) و برای نرم‌افزارهایی با نسخه‌های منتشرشده‌ی چندگانه مناسب است.
الگو چطور مناسب برای
GitHub Flow main همیشه قابل‌انتشار؛ هر کار یک شاخه‌ی کوتاه؛ PR؛ بعد از ادغام منتشر می‌کنی وب‌سایت‌ها و سرویس‌هایی که مدام منتشر می‌شوند؛ پیش‌فرض خوب
Git Flow main (نسخه‌های منتشرشده) + develop + شاخه‌های feature/، release/، hotfix/ نرم‌افزار با چند نسخه‌ی هم‌زمان (اپ‌های دسکتاپ، کتابخانه‌ها)
Trunk-based همه روی main (trunk)؛ شاخه‌ها خیلی کوتاه (کمتر از یک روز) یا هیچ؛ ویژگی‌های ناتمام با feature flag پنهان تیم‌های بزرگ با CI قوی و انتشار روزانه

قاعده‌ی کلی: ساده‌ترین الگویی را انتخاب کن که به نیازت جواب می‌دهد. برای بیشتر پروژه‌ها GitHub Flow کافی است و در ادامه‌ی درس همین را با ابزارها اعمال می‌کنیم. دو اصل مشترک همه‌ی الگوها: شاخه‌ها کوتاه‌عمر باشند (شاخه‌ی چندهفته‌ای = conflict بزرگ)، و main هر لحظه سالم.

GitHub Flow: از main یک شاخه‌ی کوتاه برای هر کار می‌گیری، با PR ادغام می‌کنی و main همیشه سالم می‌ماند.

مثال ۱: Conventional Commits، قالب پیام

Section titled “مثال ۱: Conventional Commits، قالب پیام”

Conventional Commits یک قرارداد ساده برای پیام commit است که هم آدم‌ها و هم ابزارها بتوانند بخوانند:

قالب پیام Conventional Commit
<نوع>[(حوزه)][!]: <توضیح کوتاه>
[بدنه‌ی اختیاری: چرا و جزئیات]
[پاورقی اختیاری: BREAKING CHANGE: ... یا Closes #12]
نوع معنی اثر روی نسخه (SemVer)
feat ویژگی جدید MINOR
fix رفع باگ PATCH
docs فقط مستندات —
style فرمت‌دهی بدون تغییر منطق (فاصله، ;) —
refactor بازسازی کد بدون تغییر رفتار —
perf بهبود سرعت —
test افزودن یا اصلاح تست —
build / ci ساخت و CI —
chore کار فنی متفرقه —
revert برگرداندن یک commit —
! بعد از نوع یا BREAKING CHANGE: در پاورقی تغییر ناسازگار MAJOR

نمونه‌ها: feat(cart): add coupon codes، fix: handle empty cart، docs: update install steps، feat(api)!: remove v1 endpoints. دلیل خوبی دارد: تاریخچه خوانا می‌شود و ابزارها می‌توانند از روی آن شماره‌ی نسخه و فهرست تغییرها را خودکار بسازند. (این قرارداد در conventionalcommits.org تعریف شده و بخشی از آن، نقشه‌ی مستقیم به Semantic Versioning است که در درس tag دیدی.)

مثال ۲: از commit ها به نسخه و changelog

Section titled “مثال ۲: از commit ها به نسخه و changelog”

وقتی commit ها قالب دارند، می‌شود با یک اسکریپت کوچک نسخه‌ی بعدی و فهرست تغییرها را ساخت. این‌جا یک تاریخچه‌ی نمونه می‌سازم و دو خروجی را از روی همان استخراج می‌کنم:

Terminal window
mkdir -p ~/gitlab/conv && cd ~/gitlab/conv && git init -q
n=0; for m in "feat(cart): add coupon codes" "fix: handle empty cart" "docs: update install steps" "fix(login): trim spaces in email" "feat(api)!: remove v1 endpoints" "chore: bump dev dependencies"; do n=$((n+1)); echo $n > f; git add f; git commit -qm "$m"; done
git tag -a v1.4.2 -m "Release 1.4.2" HEAD~5
echo "--- commit ها از آخرین tag به بعد:"
git log v1.4.2..HEAD --format='%s'
echo "--- changelog خودکار (گروه‌بندی بر اساس نوع):"
for t in feat fix docs; do
git log v1.4.2..HEAD --format='%s' | grep -E "^$t(\(.*\))?!?:" | sed -E "s/^$t(\(.*\))?!?: / [$t] /"
done
echo "--- نسخه‌ی بعدی چند می‌شود؟"
log=$(git log v1.4.2..HEAD --format='%s%n%b')
if echo "$log" | grep -qE '^[a-z]+(\(.*\))?!:|BREAKING CHANGE'; then bump=major
elif echo "$log" | grep -qE '^feat(\(.*\))?:'; then bump=minor
elif echo "$log" | grep -qE '^fix(\(.*\))?:'; then bump=patch; else bump=none; fi
echo "نوع افزایش: $bump (نسخه‌ی فعلی: 1.4.2)"
IFS=. read M m p <<< "1.4.2"
case $bump in major) echo "نسخه‌ی بعدی: $((M+1)).0.0";; minor) echo "نسخه‌ی بعدی: $M.$((m+1)).0";; patch) echo "نسخه‌ی بعدی: $M.$m.$((p+1))";; esac
خروجی
--- commit ها از آخرین tag به بعد:
chore: bump dev dependencies
feat(api)!: remove v1 endpoints
fix(login): trim spaces in email
docs: update install steps
fix: handle empty cart
--- changelog خودکار (گروه‌بندی بر اساس نوع):
[feat] remove v1 endpoints
[fix] trim spaces in email
[fix] handle empty cart
[docs] update install steps
--- نسخه‌ی بعدی چند می‌شود؟
نوع افزایش: major (نسخه‌ی فعلی: 1.4.2)
نسخه‌ی بعدی: 2.0.0

وجود یک ! (feat(api)!:) معنی‌اش «ناسازگار» است، پس MAJOR: 2.0.0. اگر آن نبود، feat یعنی MINOR (1.5.0) و فقط fix یعنی PATCH (1.4.3). ابزارهایی مثل semantic-release و release-please دقیقاً همین منطق را خودکار می‌کنند (این ابزارها را من اجرا نکرده‌ام؛ اسکریپت بالا همان ایده در چند خط است).

مثال ۳: اعمال خودکار قاعده‌ی پیام، commit-msg hook

Section titled “مثال ۳: اعمال خودکار قاعده‌ی پیام، commit-msg hook”

قاعده‌ای که فقط در سند نوشته شده، فراموش می‌شود. Git hook هایی دارد: اسکریپت‌هایی که در لحظه‌های مشخص خودکار اجرا می‌شوند. commit-msg بعد از نوشتن پیام و قبل از ثبت اجرا می‌شود و اگر با کد خروج ناصفر برگردد، commit را رد می‌کند:

cd ~/gitlab/conv
cat > .git/hooks/commit-msg <<'EOF'
#!/bin/sh
# پیام باید Conventional Commit باشد: type(scope)!: description
pattern='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?!?: .{3,}'
if ! head -1 "$1" | grep -qE "$pattern"; then
echo "✗ پیام commit با قالب Conventional Commits نمی‌خواند." >&2
echo " نمونه: feat(cart): add coupon codes" >&2
exit 1
fi
EOF
chmod +x .git/hooks/commit-msg
echo "x" >> f
echo "=== پیام بد:"
git commit -am "fixed some stuff" 2>&1; echo "کد خروج: $?"
echo "=== پیام خوب:"
git commit -am "fix(cart): recalculate total after coupon" 2>&1 | head -2
خروجی
=== پیام بد:
✗ پیام commit با قالب Conventional Commits نمی‌خواند.
نمونه: feat(cart): add coupon codes
کد خروج: 1
=== پیام خوب:
[main 2c5240b] fix(cart): recalculate total after coupon
1 file changed, 1 insertion(+)

hook با chmod +x اجرایی شد و در .git/hooks/ ذخیره شد. پیام بد ثبت نشد و راهنما چاپ شد؛ پیام خوب پذیرفته شد. دو مشکل: ۱) محتوای .git/hooks/ commit نمی‌شود، پس هر هم‌تیمی باید خودش آن را نصب کند؛ ۲) هر کس می‌تواند با git commit --no-verify آن را دور بزند. راه‌حل مشکل اول: hook ها را در یک پوشه‌ی داخل مخزن (مثلاً .githooks/) بگذار و core.hooksPath را به آن بده:

Terminal window
cd ~/gitlab/conv
mkdir -p .githooks && cp .git/hooks/commit-msg .githooks/ && rm .git/hooks/commit-msg
git config core.hooksPath .githooks
git add .githooks && git commit -qm "chore: add commit-msg hook for conventional commits"
echo "--- از این به بعد، hook ها از پوشه‌ی داخل مخزن خوانده می‌شوند (commit می‌شوند):"
git config core.hooksPath
git ls-files .githooks
echo "x2" >> f
git commit -qam "bad message" 2>&1 | head -1
echo "--- دور زدن عمدی (فقط برای اضطرار):"
git commit -qam "bad message" --no-verify && echo "با --no-verify ثبت شد"
خروجی
--- از این به بعد، hook ها از پوشه‌ی داخل مخزن خوانده می‌شوند (commit می‌شوند):
.githooks
.githooks/commit-msg
✗ پیام commit با قالب Conventional Commits نمی‌خواند.
--- دور زدن عمدی (فقط برای اضطرار):
با --no-verify ثبت شد

هر هم‌تیمی بعد از clone یک بار git config core.hooksPath .githooks می‌زند (می‌شود در README یا یک اسکریپت setup گذاشت). و چون --no-verify همیشه ممکن است، قاعده‌ی نهایی را روی سرور اعمال کن (CI یا محافظت از شاخه، مثال ۵)؛ hook های محلی فقط راحتی و بازخورد سریع‌اند.

مثال ۴: اسم‌گذاری شاخه و pre-push

Section titled “مثال ۴: اسم‌گذاری شاخه و pre-push”

اسم شاخه هم قاعده می‌خواهد: نوع/توضیح-کوتاه، با حروف کوچک و خط تیره:

الگو مثال
feature/نام feature/coupon-codes
fix/نام fix/login-crash
hotfix/نام hotfix/payment-timeout
docs/نام، chore/نام، refactor/نام docs/readme-install
feature/123-نام با شماره‌ی Issue: feature/123-coupon-codes

یک hook pre-push می‌تواند قبل از ارسال، اسم شاخه را بررسی کند:

cd ~/gitlab/conv
cat > .githooks/pre-push <<'EOF'
#!/bin/sh
branch=$(git rev-parse --abbrev-ref HEAD)
case "$branch" in
main) exit 0 ;;
feature/*|fix/*|hotfix/*|docs/*|chore/*|refactor/*) exit 0 ;;
esac
echo "✗ اسم شاخه «$branch» مجاز نیست. از feature/، fix/، hotfix/، docs/، chore/ یا refactor/ استفاده کن." >&2
exit 1
EOF
chmod +x .githooks/pre-push
git init -q --bare ~/gitlab/server.git && git remote add origin ~/gitlab/server.git
git switch -qc new-stuff
echo "=== شاخه‌ی با اسم بد:"
git push -u origin new-stuff 2>&1 | head -2
git switch -qc feature/coupon-codes
echo "=== شاخه‌ی با اسم خوب:"
git push -q -u origin feature/coupon-codes 2>&1 | tail -1; echo "(ارسال شد)"
git switch -q main
خروجی
=== شاخه‌ی با اسم بد:
✗ اسم شاخه «new-stuff» مجاز نیست. از feature/، fix/، hotfix/، docs/، chore/ یا refactor/ استفاده کن.
error: failed to push some refs to '/home/ali/gitlab/server.git'
=== شاخه‌ی با اسم خوب:
(ارسال شد)

مهم‌ترین قاعده‌ی تیمی: هیچ‌کس مستقیم روی main push نکند؛ فقط از راه PR. روی GitHub این کار Branch protection rule (یا Ruleset) است: Settings ← Branches ← Add rule (یا Rules ← Rulesets). گزینه‌های مهم:

گزینه معنی
Require a pull request before merging ممنوعیت push مستقیم؛ فقط PR
Require approvals (تعداد) حداقل چند تأیید از بازبین‌ها
Require status checks to pass تست‌های CI باید سبز باشند
Require branches to be up to date شاخه باید با آخرین main همگام باشد
Require linear history فقط squash یا rebase مجاز (بدون commit ادغام)
Do not allow force pushes و delete ممنوعیت بازنویسی و حذف main
Include administrators قاعده برای ادمین‌ها هم اعمال شود

(تنظیم این قاعده‌ها در رابط GitHub است و به حساب و مخزن واقعی نیاز دارد؛ من اجرایش نکرده‌ام: «نمونه»). ولی خود مکانیزم یک ویژگی Git سمت سرور است: سرور می‌تواند push های ورودی را با hook ها (مثل pre-receive) و تنظیم‌هایش رد کند. این را روی مخزن bare محلی (که نقش GitHub را دارد) واقعاً نشان می‌دهم: push مستقیم به main رد شود، push شاخه‌ی فرعی مجاز، و force push ممنوع:

cd ~/gitlab
rm -rf protected.git && git init -q --bare protected.git
cat > protected.git/hooks/pre-receive <<'EOF'
#!/bin/sh
# شبیه‌سازی «Require a pull request»: push مستقیم به main ممنوع
while read old new ref; do
if [ "$ref" = "refs/heads/main" ] && [ "$old" != "0000000000000000000000000000000000000000" ]; then
echo "error: push مستقیم به main ممنوع است؛ یک Pull Request باز کن." >&2
exit 1
fi
done
EOF
chmod +x protected.git/hooks/pre-receive
git -C protected.git config receive.denyNonFastForwards true
git -C protected.git config receive.denyDeletes true
rm -rf dev && git clone -q protected.git dev 2>/dev/null && cd dev
git config user.name Ali && git config user.email ali@example.com
echo "base" > f && git add f && git commit -qm "chore: initial" && git push -q origin main 2>&1 | tail -1
echo "(اولین push که main را می‌سازد مجاز است)"
echo "=== ۱) push مستقیم به main:"
echo "change" >> f && git commit -qam "fix: direct change"
git push origin main 2>&1 | tail -4
echo "=== ۲) همان تغییر روی شاخه‌ی فرعی (مجاز):"
git switch -qc fix/direct-change && git push -q -u origin fix/direct-change 2>&1 | tail -1; echo "(ارسال شد)"
echo "=== ۳) force push روی شاخه (بازنویسی تاریخچه‌ی قبلاً push‌شده):"
git commit -q --amend -m "fix: rewritten message"
git push --force origin fix/direct-change 2>&1 | tail -3
echo "=== ۴) حذف شاخه:"
git push origin --delete fix/direct-change 2>&1 | tail -2
خروجی
(اولین push که main را می‌سازد مجاز است)
=== ۱) push مستقیم به main:
remote: error: push مستقیم به main ممنوع است؛ یک Pull Request باز کن.
To /home/ali/gitlab/protected.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '/home/ali/gitlab/protected.git'
=== ۲) همان تغییر روی شاخه‌ی فرعی (مجاز):
(ارسال شد)
=== ۳) force push روی شاخه (بازنویسی تاریخچه‌ی قبلاً push‌شده):
To /home/ali/gitlab/protected.git
! [remote rejected] fix/direct-change -> fix/direct-change (non-fast-forward)
error: failed to push some refs to '/home/ali/gitlab/protected.git'
=== ۴) حذف شاخه:
! [remote rejected] fix/direct-change (deletion prohibited)
error: failed to push some refs to '/home/ali/gitlab/protected.git'

اولین push شاخه را می‌سازد؛ push های بعدی به main رد می‌شوند (pre-receive hook declined)، شاخه‌ی فرعی آزاد است، و force push و حذف با receive.denyNonFastForwards و receive.denyDeletes ممنوع شده‌اند (معادل «Do not allow force pushes» و «Do not allow deletions»). روی GitHub همه‌ی این‌ها گزینه‌ی تیک‌خوردنی‌اند؛ بقیه‌ی گزینه‌ها (تأیید بازبین، تست‌های سبز) به ساختار PR وصل‌اند و فقط روی GitHub معنی دارند.

فایل .github/CODEOWNERS مشخص می‌کند هر بخش از کد «صاحب» دارد؛ وقتی PR فایل‌های آن بخش را عوض کند، GitHub خودکار صاحب را برای بازبینی دعوت می‌کند (و می‌شود «تأیید صاحب کد» را اجباری کرد). آن را همراه یک قالب PR می‌سازم (فایل‌های ساده‌اند؛ تفسیرشان با GitHub است):

Terminal window
cd ~/gitlab/conv
mkdir -p .github
cat > .github/CODEOWNERS <<'EOF'
# هر خط: الگوی مسیر و صاحب‌ها
* @company/dev-team
/docs/ @sara
/src/payments/ @reza @company/security
*.sql @company/dba
EOF
cat > .github/PULL_REQUEST_TEMPLATE.md <<'EOF'
## چه چیزی عوض شد؟
## چرا؟ (لینک Issue: Closes #)
## چطور آزمایش کردی؟
- [ ] تست‌ها سبز است
- [ ] مستندات به‌روز شد
EOF
git add .github && git commit -qm "chore: add CODEOWNERS and PR template"
git ls-files .github
خروجی
.github/CODEOWNERS
.github/PULL_REQUEST_TEMPLATE.md

ترتیب در CODEOWNERS مهم است: آخرین الگوی منطبق برنده است (مثل .gitignore). همین فایل‌ها با commit وارد مخزن می‌شوند و برای همه‌ی تیم یکسان‌اند.

GitHub کنار commit هایی که امضای معتبر دارند برچسب Verified می‌گذارد؛ یعنی واقعاً از تو آمده‌اند (نه کسی که ایمیلت را در user.email گذاشته). Git می‌تواند commit ها را با کلید SSH (و GPG) امضا کند. این‌جا روی کلید یک‌بارمصرف خودم امضای SSH را کامل آزمایش می‌کنم (بدون حساب GitHub):

Terminal window
cd ~/gitlab/conv
ssh-keygen -q -t ed25519 -N "" -C "signing" -f ~/.ssh/id_sign
echo "ali@example.com $(cut -d' ' -f1,2 ~/.ssh/id_sign.pub)" > ~/.ssh/allowed_signers
git config gpg.format ssh
git config user.signingkey ~/.ssh/id_sign.pub
git config gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
echo "signed" >> f
git commit -qS -am "feat: signed change"
echo "--- امضا چک می‌شود:"
git log -1 --show-signature --format='%h %s' 2>&1 | head -4
echo "--- git verify-commit:"
git verify-commit HEAD 2>&1 | head -2
خروجی
--- امضا چک می‌شود:
Good "git" signature for ali@example.com with ED25519 key SHA256:EikuIWypARHdUjUCmLtCSen7hLzJJCy54AY/M3v9H2w
4f7c39a feat: signed change
--- git verify-commit:
Good "git" signature for ali@example.com with ED25519 key SHA256:EikuIWypARHdUjUCmLtCSen7hLzJJCy54AY/M3v9H2w

برای اینکه همیشه امضا کند: git config --global commit.gpgsign true. فایل allowed_signers فهرستی از «کدام ایمیل و کدام کلید مجاز است» است که Git با آن امضا را تأیید می‌کند. روی GitHub باید کلید عمومی امضا را هم اضافه کنی (Settings ← SSH and GPG keys ← New SSH key، با نوع Signing Key) تا برچسب Verified بگیری؛ این مرحله را اجرا نکرده‌ام (نمونه). (کلید خصوصی id_sign هرگز نمایش داده نشد؛ فقط کلید عمومی و نتیجه‌ی امضا.)

مثال ۸: یک جریان کامل، از Issue تا انتشار

Section titled “مثال ۸: یک جریان کامل، از Issue تا انتشار”

همه‌ی قطعه‌ها کنار هم، به‌شکل چک‌لیستی که در تیم‌ها رایج است (GitHub Flow + Conventional Commits):

جریان یک ویژگی
۱. Issue باز می‌شود: «#123 افزودن کد تخفیف»
۲. git switch -c feature/123-coupon-codes (از main به‌روز)
۳. commit های کوچک: feat(cart): add coupon model
test(cart): cover invalid codes
docs: describe coupon usage
۴. git push -u origin feature/123-coupon-codes
۵. Pull Request ← توضیح + «Closes #123»؛ CI اجرا می‌شود
۶. بازبینی ← اصلاح (commit های تازه) ← تأیید + CI سبز
۷. «Squash and merge» ← یک commit: feat(cart): add coupon codes (#124)
۸. شاخه حذف می‌شود؛ release-please (یا دستی) نسخه‌ی 1.5.0 و tag v1.5.0 را می‌سازد

مراحل ۱ تا ۴ مهارت‌های همین دوره‌اند و ۵ تا ۸ بخش GitHub که در درس قبل و درس بعد (Actions) پوشش می‌دهیم.

پشت پرده: hook ها فقط فایل‌های اجرایی‌اند

Section titled “پشت پرده: hook ها فقط فایل‌های اجرایی‌اند”

هر hook یک فایل اجرایی با اسم ثابت داخل .git/hooks/ (یا core.hooksPath) است. Git در لحظه‌ی مشخص آن را اجرا می‌کند و اگر کد خروج صفر نباشد، عملیات را متوقف می‌کند (برای hook هایی که می‌توانند). فهرست hook های مهم:

hook کی اجرا می‌شود کاربرد
pre-commit قبل از ساختن commit lint، فرمت‌دهی، جلوگیری از commit رمز
commit-msg بعد از نوشتن پیام اعمال Conventional Commits
pre-push قبل از push اجرای تست‌ها، بررسی نام شاخه
pre-receive / update روی سرور هنگام دریافت push ممنوعیت push مستقیم به main
post-merge، post-checkout بعد از آن عملیات نصب خودکار وابستگی‌ها

ببین چه hook هایی نمونه می‌آید:

Terminal window
cd ~/gitlab
ls protected.git/hooks | head -5
echo "..."
ls -l protected.git/hooks/pre-receive | awk '{print $1, $NF}'
خروجی
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
...
-rwxr-xr-x protected.git/hooks/pre-receive

وقتی مخزن می‌سازی، Git چند فایل *.sample می‌گذارد؛ برای فعال‌کردنشان .sample را بردار و اجرایی کن. روی GitHub hook ها وجود ندارند (به‌جای آن‌ها branch protection، Actions و webhooks دارند)؛ ولی روی سرور خودت (GitLab، Gitea یا سرور SSH) hook ها همان pre-receive اند که دیدی.

قرارداد قالب نمونه
پیام commit type(scope)!: description feat(cart): add coupon codes
شاخه type/short-name fix/login-crash
tag vMAJOR.MINOR.PATCH v1.5.0
PR عنوان مثل commit؛ توضیح با «Closes #N» feat(cart): add coupon codes (#124)
دستور کار
git config core.hooksPath .githooks hook ها از یک پوشه‌ی داخل مخزن
git commit --no-verify دور زدن hook های pre-commit و commit-msg
git push --no-verify دور زدن pre-push
git log --grep='^feat' فقط commit های ویژگی
git commit -S / git config commit.gpgsign true امضای commit
git config gpg.format ssh امضا با کلید SSH
git log --show-signature دیدن و تأیید امضا
git config receive.denyNonFastForwards true (روی سرور) ممنوعیت force push

۱) دور زدن قاعده با --no-verify یا push مستقیم

Section titled “۱) دور زدن قاعده با --no-verify یا push مستقیم”

hook های محلی را همه می‌توانند دور بزنند. راه‌حل: قاعده‌ی نهایی را روی سرور اعمال کن: branch protection و CI.

شاخه‌ای که هفته‌ها زنده است، هر روز بیشتر از main فاصله می‌گیرد و ادغامش conflict های بزرگ می‌سازد. راه‌حل: شاخه‌ها کوچک و کوتاه (چند روز)؛ ویژگی بزرگ را به قطعه‌های کوچک بشکن (و با feature flag پنهان نگه دار)، و main را مرتب در شاخه‌ات بگیر.

۳) هر commit یک خانه‌تکانی نامرتبط

Section titled “۳) هر commit یک خانه‌تکانی نامرتبط”

یک commit با عنوان feat: add coupons که در آن فرمت‌دهی کل پروژه هم هست، بازبینی را غیرممکن می‌کند. راه‌حل: commit های اتمی: هر commit یک تغییر منطقی. فرمت‌دهی و بازسازی جدا (style:، refactor:).

.git/hooks/ commit نمی‌شود: hook ای که فقط روی کامپیوتر تو هست، برای بقیه نیست. راه‌حل: core.hooksPath به یک پوشه‌ی داخل مخزن (مثال ۳) و یادآوری در README.

۵) قرارداد پیچیده برای تیم کوچک

Section titled “۵) قرارداد پیچیده برای تیم کوچک”

Git Flow با پنج نوع شاخه برای دو نفر، فقط کار را کند می‌کند. راه‌حل: با ساده‌ترین الگو (GitHub Flow) شروع کن و فقط وقتی لازم شد پیچیده‌اش کن.

✎ تمرینآسان

پنج commit با قالب Conventional Commits بزن (feat، fix، docs، fix، chore) و فقط commit های fix را فهرست کن.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -q
i=0; for m in "feat: add search" "fix: crash on empty query" "docs: add usage section" "fix(ui): align button" "chore: update deps"; do i=$((i+1)); echo $i > f; git add f; git commit -qm "$m"; done
git log --oneline --grep='^fix'
خروجی
465c22c fix(ui): align button
ff45b47 fix: crash on empty query
✎ تمرینمتوسط

تمرین اصلی: برای مخزنت قانون محافظت از main تنظیم کن. روی GitHub واقعی: Settings ← Branches ← Add branch ruleset (یا Add rule) برای main با «Require a pull request before merging»، «Require approvals: 1»، «Block force pushes» و «Restrict deletions». این را اجرا نکرده‌ام (نیاز به مخزن و دسترسی ادمین دارد: نمونه). معادل مکانیزمش را روی یک مخزن bare محلی تست کردم و نتیجه‌اش را ببین.

دیدن جواب
cd ~/gitlab
rm -rf ex2.git && git init -q --bare ex2.git
cat > ex2.git/hooks/pre-receive <<'EOF'
#!/bin/sh
while read old new ref; do
if [ "$ref" = "refs/heads/main" ] && [ "$old" != "0000000000000000000000000000000000000000" ]; then
echo "error: main protected: open a pull request" >&2; exit 1
fi
done
EOF
chmod +x ex2.git/hooks/pre-receive
git -C ex2.git config receive.denyNonFastForwards true
git -C ex2.git config receive.denyDeletes true
rm -rf ex2 && git clone -q ex2.git ex2 2>/dev/null && cd ex2
git config user.name Ali && git config user.email ali@example.com
echo a > a && git add a && git commit -qm "chore: init" && git push -q origin main 2>&1 | tail -1
echo b >> a && git commit -qam "fix: change"
git push origin main 2>&1 | tail -2
git switch -qc fix/change && git push -q -u origin fix/change 2>&1 | tail -1 && echo "(شاخه‌ی فرعی مجاز)"
خروجی
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '/home/ali/gitlab/ex2.git'
(شاخه‌ی فرعی مجاز)
✎ تمرینسخت

یک hook commit-msg بنویس که علاوه بر قالب Conventional Commit، پیام‌هایی را که عنوانشان بیشتر از ۷۲ نویسه است رد کند؛ سپس یک اسکریپت next-version.sh که از روی commit های بعد از آخرین tag نسخه‌ی بعدی را حساب کند. هر دو را با یک مثال خوب و بد ثابت کن.

دیدن جواب
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -q
cat > .git/hooks/commit-msg <<'EOF'
#!/bin/sh
title=$(head -1 "$1")
echo "$title" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?!?: .+' || { echo "✗ قالب نادرست" >&2; exit 1; }
[ ${#title} -le 72 ] || { echo "✗ عنوان بیشتر از 72 نویسه است (${#title})" >&2; exit 1; }
EOF
chmod +x .git/hooks/commit-msg
echo 1 > f; git add f
git commit -m "fix: ok" -q && echo "commit خوب پذیرفته شد"
echo 2 > f
git commit -qam "feat: $(printf 'x%.0s' $(seq 1 80))" 2>&1 | head -1
git tag v0.1.0
cat > next-version.sh <<'EOF'
#!/bin/bash
last=$(git describe --tags --abbrev=0); log=$(git log "$last"..HEAD --format='%s%n%b')
IFS=. read M m p <<< "${last#v}"
if echo "$log" | grep -qE '^[a-z]+(\(.*\))?!:|BREAKING CHANGE'; then echo "v$((M+1)).0.0"
elif echo "$log" | grep -qE '^feat(\(.*\))?:'; then echo "v$M.$((m+1)).0"
elif echo "$log" | grep -qE '^fix(\(.*\))?:'; then echo "v$M.$m.$((p+1))"; else echo "$last"; fi
EOF
chmod +x next-version.sh
echo 3 > f && git commit -qam "feat(search): add fuzzy search"
echo 4 > f && git commit -qam "fix: typo"
echo "نسخه‌ی بعدی: $(./next-version.sh)"
خروجی
commit خوب پذیرفته شد
✗ عنوان بیشتر از 72 نویسه است (86)
نسخه‌ی بعدی: v0.2.0
⚡ بررسی سریع

بر اساس Conventional Commits، «feat(api)!: remove v1 endpoints» کدام بخش نسخه را بالا می‌برد؟

؟ آزمونک
  1. در GitHub Flow کدام درست است؟

  2. نوع commit ای که یک باگ را درست می‌کند و نسخه را PATCH بالا می‌برد؟

  3. چرا hook های `.git/hooks` برای اعمال قاعده‌ی تیمی کافی نیستند؟

  4. گزینه‌ی «Require a pull request before merging» در branch protection چه می‌کند؟

  5. فایل CODEOWNERS چه می‌کند؟

  • GitHub Flow: main همیشه سالم، شاخه‌ی کوتاه برای هر کار، PR، ادغام، انتشار. Git Flow (develop/release/hotfix) و trunk-based (feature flag) برای نیازهای خاص. شاخه‌ها کوتاه‌عمر.
  • Conventional Commits: type(scope)!: description (feat، fix، docs، refactor، test، chore…)؛ feat←MINOR، fix←PATCH، ! یا BREAKING CHANGE←MAJOR. از روی آن‌ها نسخه و changelog خودکار می‌شود.
  • شاخه: feature/…، fix/…، hotfix/…، docs/…. اعمال قاعده: hook ها (commit-msg، pre-push) با core.hooksPath .githooks (commit می‌شوند)، ولی --no-verify دورشان می‌زند؛ قاعده‌ی نهایی روی سرور.
  • محافظت از main (Branch protection / Rulesets): PR اجباری، تأیید، تست‌های سبز، بدون force push و حذف، شامل ادمین‌ها. CODEOWNERS و قالب PR در .github/.
  • commit اتمی، PR کوچک، امضا (git commit -S، gpg.format ssh) برای Verified.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
git commit -m "feat(cart): add coupon codes"پیام Conventional Commit
feat! یا BREAKING CHANGE:تغییر ناسازگار (MAJOR)
git log --grep="^feat"فقط commit های ویژگی
git config core.hooksPath .githookshook های داخل مخزن (قابل‌اشتراک)
git commit --no-verifyدور زدن hook ها (اضطرار)
git switch -c feature/123-نامشاخه با قرارداد
git commit -S / git log --show-signatureامضا و تأیید
git config receive.denyNonFastForwards true(سرور) ممنوعیت force push
.github/CODEOWNERSصاحب کد هر مسیر