توی این درس یاد میگیری قراردادهایی که تیمهای حرفهای استفاده میکنند: 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 یک فهرست بیمعنی است. قرارداد چیزی نیست جز چند قاعدهی ساده که همه رعایت میکنند؛ و بهترین قاعده آن است که ابزار آن را اعمال کند، نه حافظهی آدمها.
تشبیه: قوانین رانندگی
Section titled “تشبیه: قوانین رانندگی”همه میتوانند رانندگی کنند، ولی اگر هر کس از سمتی برود تصادف میشود. قوانین رانندگی آزادی را کم نمیکنند؛ پیشبینیپذیری میآورند: همه میدانند چراغ قرمز یعنی ایست. قرارداد تیمی هم همین است: معنی نام یک شاخه، شکل یک پیام commit و راه ادغام، برای همه یکی است.
سه الگوی کار با شاخهها
Section titled “سه الگوی کار با شاخهها”| الگو | چطور | مناسب برای |
|---|---|---|
| GitHub Flow | main همیشه قابلانتشار؛ هر کار یک شاخهی کوتاه؛ PR؛ بعد از ادغام منتشر میکنی |
وبسایتها و سرویسهایی که مدام منتشر میشوند؛ پیشفرض خوب |
| Git Flow | main (نسخههای منتشرشده) + develop + شاخههای feature/، release/، hotfix/ |
نرمافزار با چند نسخهی همزمان (اپهای دسکتاپ، کتابخانهها) |
| Trunk-based | همه روی main (trunk)؛ شاخهها خیلی کوتاه (کمتر از یک روز) یا هیچ؛ ویژگیهای ناتمام با feature flag پنهان |
تیمهای بزرگ با CI قوی و انتشار روزانه |
قاعدهی کلی: سادهترین الگویی را انتخاب کن که به نیازت جواب میدهد. برای بیشتر پروژهها GitHub Flow کافی است و در ادامهی درس همین را با ابزارها اعمال میکنیم. دو اصل مشترک همهی الگوها: شاخهها کوتاهعمر باشند (شاخهی چندهفتهای = conflict بزرگ)، و main هر لحظه سالم.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: Conventional Commits، قالب پیام
Section titled “مثال ۱: Conventional Commits، قالب پیام”Conventional Commits یک قرارداد ساده برای پیام 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 ها قالب دارند، میشود با یک اسکریپت کوچک نسخهی بعدی و فهرست تغییرها را ساخت. اینجا یک تاریخچهی نمونه میسازم و دو خروجی را از روی همان استخراج میکنم:
mkdir -p ~/gitlab/conv && cd ~/gitlab/conv && git init -qn=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"; donegit tag -a v1.4.2 -m "Release 1.4.2" HEAD~5echo "--- 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] /"doneecho "--- نسخهی بعدی چند میشود؟"log=$(git log v1.4.2..HEAD --format='%s%n%b')if echo "$log" | grep -qE '^[a-z]+(\(.*\))?!:|BREAKING CHANGE'; then bump=majorelif echo "$log" | grep -qE '^feat(\(.*\))?:'; then bump=minorelif echo "$log" | grep -qE '^fix(\(.*\))?:'; then bump=patch; else bump=none; fiecho "نوع افزایش: $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 dependenciesfeat(api)!: remove v1 endpointsfix(login): trim spaces in emaildocs: update install stepsfix: 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/convcat > .git/hooks/commit-msg <<'EOF'#!/bin/sh# پیام باید Conventional Commit باشد: type(scope)!: descriptionpattern='^(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 1fiEOFchmod +x .git/hooks/commit-msgecho "x" >> fecho "=== پیام بد:"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 را به آن بده:
cd ~/gitlab/convmkdir -p .githooks && cp .git/hooks/commit-msg .githooks/ && rm .git/hooks/commit-msggit config core.hooksPath .githooksgit add .githooks && git commit -qm "chore: add commit-msg hook for conventional commits"echo "--- از این به بعد، hook ها از پوشهی داخل مخزن خوانده میشوند (commit میشوند):"git config core.hooksPathgit ls-files .githooksecho "x2" >> fgit commit -qam "bad message" 2>&1 | head -1echo "--- دور زدن عمدی (فقط برای اضطرار):"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/convcat > .githooks/pre-push <<'EOF'#!/bin/shbranch=$(git rev-parse --abbrev-ref HEAD)case "$branch" in main) exit 0 ;; feature/*|fix/*|hotfix/*|docs/*|chore/*|refactor/*) exit 0 ;;esacecho "✗ اسم شاخه «$branch» مجاز نیست. از feature/، fix/، hotfix/، docs/، chore/ یا refactor/ استفاده کن." >&2exit 1EOFchmod +x .githooks/pre-pushgit init -q --bare ~/gitlab/server.git && git remote add origin ~/gitlab/server.gitgit switch -qc new-stuffecho "=== شاخهی با اسم بد:"git push -u origin new-stuff 2>&1 | head -2git switch -qc feature/coupon-codesecho "=== شاخهی با اسم خوب:"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
Section titled “مثال ۵: محافظت از main”مهمترین قاعدهی تیمی: هیچکس مستقیم روی 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 ~/gitlabrm -rf protected.git && git init -q --bare protected.gitcat > 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 fidoneEOFchmod +x protected.git/hooks/pre-receivegit -C protected.git config receive.denyNonFastForwards truegit -C protected.git config receive.denyDeletes truerm -rf dev && git clone -q protected.git dev 2>/dev/null && cd devgit config user.name Ali && git config user.email ali@example.comecho "base" > f && git add f && git commit -qm "chore: initial" && git push -q origin main 2>&1 | tail -1echo "(اولین push که main را میسازد مجاز است)"echo "=== ۱) push مستقیم به main:"echo "change" >> f && git commit -qam "fix: direct change"git push origin main 2>&1 | tail -4echo "=== ۲) همان تغییر روی شاخهی فرعی (مجاز):"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 -3echo "=== ۴) حذف شاخه:"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 معنی دارند.
مثال ۶: CODEOWNERS و قالب PR
Section titled “مثال ۶: CODEOWNERS و قالب PR”فایل .github/CODEOWNERS مشخص میکند هر بخش از کد «صاحب» دارد؛ وقتی PR فایلهای آن بخش را عوض کند، GitHub خودکار صاحب را برای بازبینی دعوت میکند (و میشود «تأیید صاحب کد» را اجباری کرد). آن را همراه یک قالب PR میسازم (فایلهای سادهاند؛ تفسیرشان با GitHub است):
cd ~/gitlab/convmkdir -p .githubcat > .github/CODEOWNERS <<'EOF'# هر خط: الگوی مسیر و صاحبها* @company/dev-team/docs/ @sara/src/payments/ @reza @company/security*.sql @company/dbaEOFcat > .github/PULL_REQUEST_TEMPLATE.md <<'EOF'## چه چیزی عوض شد؟
## چرا؟ (لینک Issue: Closes #)
## چطور آزمایش کردی؟
- [ ] تستها سبز است- [ ] مستندات بهروز شدEOFgit 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 وارد مخزن میشوند و برای همهی تیم یکساناند.
مثال ۷: commit امضاشده
Section titled “مثال ۷: commit امضاشده”GitHub کنار commit هایی که امضای معتبر دارند برچسب Verified میگذارد؛ یعنی واقعاً از تو آمدهاند (نه کسی که ایمیلت را در user.email گذاشته). Git میتواند commit ها را با کلید SSH (و GPG) امضا کند. اینجا روی کلید یکبارمصرف خودم امضای SSH را کامل آزمایش میکنم (بدون حساب GitHub):
cd ~/gitlab/convssh-keygen -q -t ed25519 -N "" -C "signing" -f ~/.ssh/id_signecho "ali@example.com $(cut -d' ' -f1,2 ~/.ssh/id_sign.pub)" > ~/.ssh/allowed_signersgit config gpg.format sshgit config user.signingkey ~/.ssh/id_sign.pubgit config gpg.ssh.allowedSignersFile ~/.ssh/allowed_signersecho "signed" >> fgit commit -qS -am "feat: signed change"echo "--- امضا چک میشود:"git log -1 --show-signature --format='%h %s' 2>&1 | head -4echo "--- git verify-commit:"git verify-commit HEAD 2>&1 | head -2--- امضا چک میشود:Good "git" signature for ali@example.com with ED25519 key SHA256:EikuIWypARHdUjUCmLtCSen7hLzJJCy54AY/M3v9H2w4f7c39a 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 هایی نمونه میآید:
cd ~/gitlabls protected.git/hooks | head -5echo "..."ls -l protected.git/hooks/pre-receive | awk '{print $1, $NF}'applypatch-msg.samplecommit-msg.samplefsmonitor-watchman.samplepost-update.samplepre-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 اند که دیدی.
جدولهای مرجع
Section titled “جدولهای مرجع”| قرارداد | قالب | نمونه |
|---|---|---|
| پیام 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 |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) دور زدن قاعده با --no-verify یا push مستقیم
Section titled “۱) دور زدن قاعده با --no-verify یا push مستقیم”hook های محلی را همه میتوانند دور بزنند. راهحل: قاعدهی نهایی را روی سرور اعمال کن: branch protection و CI.
۲) شاخهی بلندمدت
Section titled “۲) شاخهی بلندمدت”شاخهای که هفتهها زنده است، هر روز بیشتر از main فاصله میگیرد و ادغامش conflict های بزرگ میسازد. راهحل: شاخهها کوچک و کوتاه (چند روز)؛ ویژگی بزرگ را به قطعههای کوچک بشکن (و با feature flag پنهان نگه دار)، و main را مرتب در شاخهات بگیر.
۳) هر commit یک خانهتکانی نامرتبط
Section titled “۳) هر commit یک خانهتکانی نامرتبط”یک commit با عنوان feat: add coupons که در آن فرمتدهی کل پروژه هم هست، بازبینی را غیرممکن میکند. راهحل: commit های اتمی: هر commit یک تغییر منطقی. فرمتدهی و بازسازی جدا (style:، refactor:).
۴) hook ای که هرگز نصب نشد
Section titled “۴) hook ای که هرگز نصب نشد”.git/hooks/ commit نمیشود: hook ای که فقط روی کامپیوتر تو هست، برای بقیه نیست. راهحل: core.hooksPath به یک پوشهی داخل مخزن (مثال ۳) و یادآوری در README.
۵) قرارداد پیچیده برای تیم کوچک
Section titled “۵) قرارداد پیچیده برای تیم کوچک”Git Flow با پنج نوع شاخه برای دو نفر، فقط کار را کند میکند. راهحل: با سادهترین الگو (GitHub Flow) شروع کن و فقط وقتی لازم شد پیچیدهاش کن.
پنج commit با قالب Conventional Commits بزن (feat، fix، docs، fix، chore) و فقط commit های fix را فهرست کن.
دیدن جواب
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -qi=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"; donegit log --oneline --grep='^fix'465c22c fix(ui): align buttonff45b47 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 ~/gitlabrm -rf ex2.git && git init -q --bare ex2.gitcat > ex2.git/hooks/pre-receive <<'EOF'#!/bin/shwhile read old new ref; do if [ "$ref" = "refs/heads/main" ] && [ "$old" != "0000000000000000000000000000000000000000" ]; then echo "error: main protected: open a pull request" >&2; exit 1 fidoneEOFchmod +x ex2.git/hooks/pre-receivegit -C ex2.git config receive.denyNonFastForwards truegit -C ex2.git config receive.denyDeletes truerm -rf ex2 && git clone -q ex2.git ex2 2>/dev/null && cd ex2git config user.name Ali && git config user.email ali@example.comecho a > a && git add a && git commit -qm "chore: init" && git push -q origin main 2>&1 | tail -1echo b >> a && git commit -qam "fix: change"git push origin main 2>&1 | tail -2git 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 -qcat > .git/hooks/commit-msg <<'EOF'#!/bin/shtitle=$(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; }EOFchmod +x .git/hooks/commit-msgecho 1 > f; git add fgit commit -m "fix: ok" -q && echo "commit خوب پذیرفته شد"echo 2 > fgit commit -qam "feat: $(printf 'x%.0s' $(seq 1 80))" 2>&1 | head -1git tag v0.1.0cat > next-version.sh <<'EOF'#!/bin/bashlast=$(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"; fiEOFchmod +x next-version.shecho 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آزمونک
Section titled “آزمونک”بر اساس Conventional Commits، «feat(api)!: remove v1 endpoints» کدام بخش نسخه را بالا میبرد؟
! بعد از نوع (یا BREAKING CHANGE در پاورقی) یعنی ناسازگار، و در SemVer ناسازگار یعنی MAJOR. feat معمولی MINOR و fix PATCH است.
در GitHub Flow کدام درست است؟
Git Flow است که develop و release دارد.
نوع commit ای که یک باگ را درست میکند و نسخه را PATCH بالا میبرد؟
feat ← MINOR، fix ← PATCH، ! یا BREAKING CHANGE ← MAJOR.
چرا hook های `.git/hooks` برای اعمال قاعدهی تیمی کافی نیستند؟
core.hooksPath مشکل نصب را کم میکند، ولی دور زدن را نه.
گزینهی «Require a pull request before merging» در branch protection چه میکند؟
همراه تأییدها و تستهای اجباری.
فایل CODEOWNERS چه میکند؟
آخرین الگوی منطبق برنده است.
جمعبندی
Section titled “جمعبندی”- 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 .githooks | hook های داخل مخزن (قابلاشتراک) |
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 | صاحب کد هر مسیر |