บทความ › การเขียนโปรแกรม

Git สำหรับคนทำโปรเจกต์คนเดียว: นิสัยเล็ก ๆ ที่ช่วยชีวิตตอนระบบพัง

commit เล็กพร้อมข้อความชัด trunk กับ branch สั้น tag ทุก release ใช้ git bisect หา commit ที่ทำพังใน 7 รอบจาก 100 commit และ git reflog กู้งานที่เผลอลบ

เริ่มต้น อ่าน 10 นาที 22 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • commit เล็ก หนึ่งเรื่องต่อหนึ่ง commit ทำให้ย้อน หา และอ่านประวัติได้ง่าย
  • ทำงานบน main และแตก branch สั้น ๆ ที่ merge กลับภายในวันสองวัน
  • git bisect หา commit ที่ทำพังจาก 100 commit ได้ใน 7 รอบ เพราะผ่าครึ่งทุกครั้ง
  • git reflog จำทุกที่ที่ HEAD เคยอยู่ reset --hard ผิดก็กู้คืนได้ ส่วน secret ที่ commit ไปแล้วต้องเปลี่ยน key ก่อน

ทำโปรเจกต์คนเดียว ไม่มีใครรีวิว ไม่มีใครบ่นว่า commit ชื่อ “fix” ซ้ำสิบครั้ง ทุกอย่างดูไม่สำคัญ จนวันที่ production พังแล้วต้องหาว่าพังตั้งแต่ commit ไหน หรือเผลอ reset --hard งานทั้งบ่าย บทความนี้รวมนิสัย Git ไม่กี่ข้อที่ทำวันนี้แล้วช่วยตัวเองในอีกหกเดือน

ทดสอบบน Git 2.45.2 (Windows, Git Bash) คำสั่งทั้งหมดเป็น shell แบบ POSIX ใช้บน Linux และ macOS ได้เหมือนกัน

commit เล็ก ข้อความบอกว่าทำไม

หนึ่ง commit = หนึ่งเรื่อง แก้บั๊กหนึ่งตัว เพิ่มฟีเจอร์หนึ่งชิ้น หรือ refactor หนึ่งจุด ไม่ปนกัน เหตุผลไม่ใช่ความสวย แต่เพราะ ทุกเครื่องมือกู้ชีวิตทำงานเป็นหน่วย commit revert ย้อนทีละ commit, bisect ชี้ทีละ commit ถ้า commit เดียวมีห้าเรื่อง เครื่องมือชี้ได้แค่ว่า “อยู่ในห้าเรื่องนี้”

ข้อความแบบนี้ดีกว่าแบบนี้
fixfix: ปัดเศษภาษีก่อนรวมยอด ไม่ใช่หลังรวม
updatefeat: ส่งอีเมลยืนยันเมื่อสั่งซื้อสำเร็จ
wiprefactor: แยกการคำนวณราคาออกจาก controller

บรรทัดแรกสั้น บอกว่าเปลี่ยนอะไร ถ้ามีเหตุผลที่ไม่ชัดจากโค้ด เขียนต่อในบรรทัดถัดไป ตัวเองในอีกหกเดือนคือคนอ่าน

ก่อน commit ดูสิ่งที่จะเข้าไปทุกครั้ง

git status
git diff --staged
git commit -m "fix: ปัดเศษภาษีก่อนรวมยอด"

trunk กับ branch สั้น ๆ

คนเดียวไม่ต้องมี develop, release, hotfix แบบทีมใหญ่ ใช้ main เป็น trunk ที่พร้อม deploy เสมอ งานที่ใช้เวลาหลายวันหรือยังไม่แน่ใจ แตก branch แล้ว merge กลับเร็ว ๆ

git switch -c feat/export-csv     # แตก branch ใหม่แล้วย้ายไปเลย
# ... ทำงาน commit ทีละเรื่อง ...
git switch main
git merge feat/export-csv
git branch -d feat/export-csv     # merge แล้วลบทิ้ง

branch ที่อยู่นานเป็นสัปดาห์จะห่างจาก main เรื่อย ๆ วันที่ merge จะเจอ conflict ก้อนใหญ่ ถ้าทดลองแล้วไม่เวิร์ก ก็ทิ้ง branch ไป main ไม่เปื้อน

tag ทุกครั้งที่ปล่อยของ

tag คือป้ายติดที่ commit บอกว่า “นี่คือเวอร์ชันที่ขึ้น production” ใช้ annotated tag (-a) เพราะเก็บวันที่ ผู้สร้าง และข้อความ เอกสาร Git บอกตรง ๆ ว่า annotated tag มีไว้สำหรับ release ส่วน lightweight tag สำหรับป้ายชั่วคราว และ git describe จะมองข้าม lightweight tag โดยค่าเริ่มต้น

git tag -a v1.4.0 -m "release 1.4.0: export CSV"
git describe            # เช่น v1.4.0-12-g6219891 = เลย v1.4.0 มา 12 commit
git push --follow-tags  # push commit พร้อม annotated tag ที่เกี่ยวข้อง

ผลคือเมื่อมีคนแจ้งว่า “เวอร์ชันเมื่อวานยังดีอยู่” เรารู้ทันทีว่าเมื่อวานคือ commit ไหน และใช้เป็นจุดเริ่มของ bisect ได้ ถ้า deploy ด้วยการสลับโฟลเดอร์แบบในบทความ deploy แบบย้อนกลับได้ tag ช่วยให้รู้ว่าแต่ละโฟลเดอร์คือเวอร์ชันไหน

git bisect: ผ่าครึ่งหา commit ที่ทำพัง

รู้ว่า v1.0 ดี ตอนนี้พัง ระหว่างนั้นมี 99 commit ไล่ทีละอันคือ 99 รอบ git bisect ผ่าครึ่งให้ ทดสอบตรงกลาง ถ้าดีแปลว่าบั๊กอยู่ครึ่งหลัง ถ้าพังแปลว่าอยู่ครึ่งแรก ทำซ้ำจนเหลือ commit เดียว

ตัวอย่างคำนวณ: 99 commit ใช้กี่รอบ

แต่ละรอบตัดผู้ต้องสงสัยเหลือครึ่งหนึ่ง จำนวนรอบจึงประมาณ log₂(จำนวน commit)

  1. ผู้ต้องสงสัย 99 commit
  2. 2⁶ = 64 น้อยกว่า 99 ผ่าครึ่ง 6 รอบยังไม่พอ
  3. 2⁷ = 128 มากกว่า 99 ผ่าครึ่ง 7 รอบพอแน่นอน
  4. log₂(99) ≈ 6.6 ปัดขึ้น = 7 รอบ แทน 99 รอบ
commit ที่ต้องค้นไล่ทีละอัน (แย่สุด)bisect (ประมาณ)
1001007
1,0001,00010
10,00010,00014

เราลองจริงกับ repo ทดสอบ 100 commit ที่ใส่บั๊กไว้ที่ commit ที่ 73 Git บอกตั้งแต่เริ่มว่า “49 revisions left to test after this (roughly 6 steps)” แล้วรันสคริปต์ทดสอบทั้งหมด 7 ครั้ง ชี้ถูกที่ commit ที่ 73

ให้สคริปต์ตัดสินแทนเรา

ถ้าเขียนการทดสอบเป็นสคริปต์ได้ git bisect run ทำทั้งหมดเอง สคริปต์ต้อง exit 0 เมื่อดี, 1-127 (ยกเว้น 125) เมื่อพัง และ 125 เมื่อ commit นั้นทดสอบไม่ได้ (เช่น build ไม่ผ่าน) ให้ข้ามไป

git bisect start HEAD v1.0      # bad = HEAD, good = v1.0
git bisect run ./test.sh        # Git รันสคริปต์ทุก commit ที่ผ่าครึ่งให้
git bisect reset                # จบ กลับไป branch เดิม
#!/bin/sh
# test.sh ตัวอย่าง: build ไม่ผ่าน = ข้าม, เทสต์ไม่ผ่าน = commit นี้พัง
npm run build || exit 125
npm test

ถ้าทดสอบด้วยมือ ใช้ git bisect good / git bisect bad ตอบทีละรอบแทน ข้อสำคัญคือ bisect ได้ผลเมื่อ commit เล็กและทุก commit build ได้ ถ้า commit ครึ่งหนึ่ง build ไม่ผ่าน ต้องข้ามเยอะ และ commit ที่ชี้ได้ก็ใหญ่จนหาจุดผิดไม่เจอ

bisect เร็วแค่ไหน ขึ้นกับว่า commit เล็กแค่ไหน นิสัยข้อแรกคือสิ่งที่ทำให้ข้อนี้ใช้ได้

เริ่ม99หลังครั้งที่ 149หลังครั้งที่ 224หลังครั้งที่ 312หลังครั้งที่ 46หลังครั้งที่ 53หลังครั้งที่ 61commit ที่ยังต้องสงสัย เหลือครึ่งทุกครั้ง
bisect ผ่าครึ่งทุกรอบ 99 commit เหลือตัวเดียวภายใน 7 ครั้ง

git reflog: กู้งานที่คิดว่าหายไปแล้ว

reset --hard ผิด commit, ลบ branch ที่ยังไม่ merge, rebase แล้วพัง ดูเหมือนงานหาย แต่ Git จำทุกที่ที่ HEAD เคยชี้ไว้ใน reflog

git reset --hard HEAD~3      # เผลอทิ้ง 3 commit ล่าสุด
git reflog -3
# 4a7f3a8 HEAD@{0}: reset: moving to HEAD~3
# 6219891 HEAD@{1}: checkout: moving from ... to main
# ...
git reset --hard 'HEAD@{1}'  # กลับไปจุดก่อน reset

ถ้าไม่อยากย้าย branch ปัจจุบัน ให้สร้าง branch ใหม่ชี้ไปที่จุดนั้นก่อน แล้วค่อยดู

git branch rescue 'HEAD@{1}'

ข้อจำกัดที่ต้องรู้

  • reflog อยู่ในเครื่องเราเท่านั้น ไม่ถูก push ลบโฟลเดอร์โปรเจกต์ทิ้งก็หายด้วย
  • ค่าเริ่มต้นเก็บ 90 วัน และ entry ที่ไม่มี branch ไหนชี้ถึงแล้ว 30 วัน หลังจากนั้น git gc เก็บกวาดได้
  • reflog กู้ได้เฉพาะสิ่งที่ เคย commit ไฟล์ที่แก้แล้วยังไม่ commit แล้วโดน reset --hard กู้ด้วย reflog ไม่ได้ นี่คืออีกเหตุผลที่ควร commit บ่อย

ใส่ 'HEAD@{1}' ในเครื่องหมายคำพูดเสมอ บาง shell (เช่น PowerShell) ตีความ {} เอง

.gitignore กัน secret ตั้งแต่ commit แรก

ใส่ .gitignore ก่อน git add ครั้งแรก ไม่ใช่หลังจากเผลอไปแล้ว

# secret และค่าเฉพาะเครื่อง
.env
.env.*
!.env.example
*.pem
*.key

# ของที่ build ใหม่ได้
node_modules/
dist/
__pycache__/

ตรวจว่าไฟล์ถูก ignore จริง

git check-ignore -v .env
# .gitignore:2:.env	.env

กับดักที่เจอบ่อย: .gitignore ไม่มีผลกับไฟล์ที่ track ไปแล้ว เอกสาร Git บอกไว้ตรง ๆ ต้องเอาออกจาก index ก่อน

git rm --cached .env
git commit -m "chore: เลิก track .env"

ไฟล์ยังอยู่ในเครื่อง แต่ ยังอยู่ใน commit เก่าทุกอัน ถ้าเคย push ไปแล้ว ถือว่า key รั่ว GitHub แนะนำให้ revoke หรือ rotate key เป็นอย่างแรก การลบออกจากประวัติด้วย git filter-repo เป็นขั้นตอนรอง และเปลี่ยน hash ของ commit ทั้งหมด ถ้าเปลี่ยน key แล้ว อาจไม่จำเป็นด้วยซ้ำ แนวคิดแยก secret ออกจากโค้ดอยู่ในบทความแยก credentials

secret ที่เคย push แล้ว แก้ด้วยการเปลี่ยน key ไม่ใช่ลบ commit

ข้อผิดพลาดที่เจอบ่อย

  • commit ก้อนใหญ่ทุกสิ้นวัน ทำให้ revert และ bisect ไร้ประโยชน์
  • git add . ก่อนมี .gitignore แล้ว .env หรือ node_modules ติดเข้าไป
  • branch อายุยาว merge ทีเดียวตอนจบ เจอ conflict ทั้งก้อน
  • ใช้ lightweight tag กับ release git describe จะมองไม่เห็น ใช้ -a
  • ตกใจแล้วลบโฟลเดอร์ clone ใหม่ ทั้งที่ git reflog กู้ได้ในบรรทัดเดียว
  • push --force ทับ main แม้ทำคนเดียว ถ้ามีหลายเครื่อง เครื่องอื่นจะสับสน ใช้ --force-with-lease ถ้าจำเป็นจริง

แหล่งอ้างอิง

คำถามที่พบบ่อย

ทำคนเดียว ต้องใช้ branch ไหม

ไม่จำเป็นทุกครั้ง งานเล็กที่เสร็จในรอบเดียว commit ลง main ได้เลย แต่งานที่ใช้หลายวันหรือเป็นการทดลอง แตก branch ไว้ main จะพร้อม deploy ตลอด และทิ้ง branch ได้ถ้าไม่เวิร์ก

reflog เก็บไว้นานแค่ไหน

ค่าเริ่มต้น entry ทั่วไปเก็บ 90 วัน (gc.reflogExpire) entry ที่ไม่มี branch ไหนชี้ถึงแล้วเก็บ 30 วัน (gc.reflogExpireUnreachable) และ reflog อยู่ในเครื่องเราเท่านั้น ไม่ถูก push ไปด้วย

เผลอ commit ไฟล์ .env ขึ้น GitHub แล้ว ลบ commit ทิ้งพอไหม

ไม่พอ ถือว่า key รั่วแล้ว GitHub แนะนำให้ revoke หรือ rotate key ก่อนเป็นอย่างแรก จากนั้นค่อยพิจารณาลบออกจากประวัติด้วย git filter-repo

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ