เคยประสบกับปัญหาการ Commit ไฟล์ที่ไม่ควร Commit เข้าไปใน Git Repository หรือไม่ ไม่ว่าจะเป็นไฟล์ .env ที่มี API Keys ที่เป็นความลับ Database Passwords Configuration ที่มีข้อมูลส่วนตัว Private Keys SSH Keys หรือแม้แต่ไฟล์ที่มีขนาดใหญ่เกินไป ปัญหาจะยังคงอยู่ในทั้ง Repository ถึงแม้คุณจะลบไฟล์นั้นออกไปใน Commit ถัดไป บทความนี้จะสอนให้คุณใช้เครื่องมือ git filter-branch และ git-filter-repo เพื่อลบไฟล์ที่ไม่ต้องการออกจากทั้ง Git History อย่างสมบูรณ์ เพื่อให้ Code ของคุณปลอดภัยและ Repository มีขนาดเล็กลงได้อย่างมีประสิทธิภาพ
ทำไมถึงต้องลบไฟล์ออกจาก Git History?
การ Commit ไฟล์ที่ไม่ควร Commit นั้นเป็นปัญหาทั่วไปที่พบได้บ่อยในการพัฒนา Software โดยไม่คำนึงว่าคุณจะ Commit API Keys Database Passwords Database Connection Strings Private Keys SSH Keys หรือแม้แต่ไฟล์ที่มีขนาดใหญ่เกิน ปัญหาจะยังคงอยู่ในทั้ง Repository ข้อปัญหาของการเก็บไฟล์ลับเหล่านี้ใน Git History มีดังนี้
- ความเสี่ยงด้านความปลอดภัย: ถ้า Repository นั้นเป็นแบบ Public หรือ Private ที่มี Access มากมาย ใครก็สามารถดู API Keys และ Credentials ของคุณได้จากการดู Git History ผ่านคำสั่ง git log ซึ่งเป็นความเสี่ยงที่รุนแรงมาก
- ขนาด Repository เพิ่มขึ้น: ไฟล์ที่ Commit ไปแล้ว Git ยังคงเก็บไว้ในทุก Clone ของ Repository ทำให้ขนาด Repository เพิ่มขึ้นอย่างมาก ถ้า Clone หลายครั้งจะเหลือเกิน
- ปัญหา Git Garbage Collection: เมื่อจำนวน Git Objects มากขึ้น Git Garbage Collector จะใช้เวลานานขึ้นในการทำความสะอาด ส่งผลให้ Performance ลดลง
- ปัญหากฎหมายและการปฏิบัติตาม: Credentials ลับที่เผยแพร่อาจเป็นการละเมิดนโยบายบริษัท หรือกฎหมายการป้องกันข้อมูล
- ความไว้วางใจทีมและสัญญา: ถ้า Credentials ถูกเผยแพร่มันอาจทำให้เสียความไว้วางใจจากทีมและภายนอกองค์กร
วิธีที่ 1: ใช้ git filter-branch (วิธีเก่า แต่ยังใช้ได้)
git filter-branch เป็นวิธี Standard ที่ Git มีมาตั้งแต่เดิม มันช่วยให้คุณสามารถลบไฟล์ออกจากทุก Commit ได้อย่างมีประสิทธิภาพ อย่างไรก็ตาม วิธีนี้ค่อนข้างช้า และมีความปลอดภัยน้อยกว่า git-filter-repo ซึ่งเป็นวิธีที่ Git Official Team recommend อยู่ในปัจจุบัน แต่หาก Repository ของคุณเก่าหรือสภาพแวดล้อมไม่รองรับ git-filter-repo คุณสามารถใช้ filter-branch ได้
ตัวอย่างการลบไฟล์ .env ออกจากทั้ง Repository:
# ลบไฟล์ออกจากทุก Commit โดยใช้ filter-branch
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch path/to/secret-file.env" \
--prune-empty --tag-name-filter cat -- --all
# หรือลบไฟล์ที่มีชื่อใดๆ ที่ตรงกับ pattern
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch *.env" \
--prune-empty --tag-name-filter cat -- --all
# ต่อ filter-branch จบแล้ว
rm -rf .git/refs/original/
ข้อควรระวัง: หลังจากใช้ filter-branch Repository Reference จะเปลี่ยนแปลงไปอย่างสิ้นเชิง คุณจำเป็นต้อง Force Push ไปยัง Remote ซึ่งอาจ Conflict กับ History ของ Collaborators คน อื่นๆ ดังนั้นต้องแจ้งให้ทีมของคุณทราบก่อนทำการ Force Push และให้ทีมทำการ Clone Repository ใหม่
วิธีที่ 2: ใช้ git-filter-repo (วิธีแนะนำ)
git-filter-repo เป็นเครื่องมือที่ Git Official Team recommend ใช้แทน filter-branch เพราะว่ามันเร็วกว่า 10-100 เท่า เสถียรกว่า และมี Features ครบครันมากกว่า ข้อดีของ git-filter-repo คือช่วยให้คุณลบไฟล์ได้อย่างมีประสิทธิภาพ และสามารถทำการตัวอักษรที่ซับซ้อนได้อย่างสมบูรณ์ นอกจากนี้ git-filter-repo ยังให้ Dry-run Mode เพื่อให้คุณสามารถตรวจสอบว่าจะเกิดอะไรขึ้นก่อนที่จะทำการ Filter จริงๆ
ขั้นตอนที่ 1: ติดตั้ง git-filter-repo
ขั้นแรก คุณต้องติดตั้ง git-filter-repo ซึ่งต้องใช้ Python 3 และ pip ก่อน ติดตั้ง Python ให้ถูกต้องเสียก่อน
# macOS (ใช้ Homebrew)
brew install git-filter-repo
# Linux Ubuntu / Debian (ใช้ apt)
sudo apt-get install git-filter-repo
# Linux ทั่วไป (ใช้ pip)
pip3 install git-filter-repo
# Windows (ใช้ pip)
pip install git-filter-repo
# หรือติดตั้งแบบ Manual จาก GitHub
wget https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo
chmod +x git-filter-repo
sudo mv git-filter-repo /usr/local/bin/
# ตรวจสอบการติดตั้ง
git filter-repo --version
ขั้นตอนที่ 2: สำรอง Repository ก่อนทำการลบไฟล์
ก่อนทำการ Filter Repository ต้องสำรองข้อมูลก่อน เพราะว่าการ Filter จะแก้ไข Git History อย่างถาวร
# Clone Repository เป็นสำรอง
git clone --mirror /path/to/original/repo /path/to/backup/repo.git
# หรือใช้ zip archive
zip -r repo-backup.zip /path/to/repo
# ย้ายไปยัง Repository ที่จะ Filter
cd /path/to/repo
ขั้นตอนที่ 3: ลบไฟล์ออก
หลังจากติดตั้งแล้ว คุณสามารถลบไฟล์ได้โดยใช้คำสั่งต่อไปนี้:
# ลบไฟล์จำเพาะ เช่น secret.env
git filter-repo --path secret.env --invert-paths
# ลบไฟล์ที่มีขยาย .env ทั้งหมด
git filter-repo --path-glob '*.env' --invert-paths
# ลบ node_modules directory จากทั้ง Repository
git filter-repo --path node_modules --invert-paths
# ลบไฟล์หลายไฟล์พร้อมกัน
git filter-repo --path secret.env --path .env.local --path config/database.yml --invert-paths
# ลบไฟล์และโฟลเดอร์ที่มีชื่อเฉพาะ
git filter-repo --path .env --path .ssh --path credentials/ --invert-paths
อธิบาย: Flag –invert-paths หมายความว่าลบไฟล์ที่ specified แต่ให้เก็บไฟล์อื่นๆ ไว้ หากไม่ใส่ –invert-paths มันจะเก็บแค่ไฟล์ที่ specified เท่านั้นซึ่งไม่ใช่สิ่งที่เราต้องการ
ขั้นตอนที่ 4: ตรวจสอบผลลัพธ์ (Dry Run)
ก่อนที่จะ Filter Repository จริง ลองใช้ –analyze เพื่อดูว่าจะเกิดอะไรขึ้น:
# ดูข้อมูลไฟล์ที่ใหญ่ที่สุด
git filter-repo --analyze
# ผลลัพธ์จะแสดงในไฟล์ report.txt
cat .git/filter-repo/analysis/
# ดูไฟล์ที่จะถูกลบ
git filter-repo --path secret.env --invert-paths --dry-run
ขั้นตอนที่ 5: Force Push ไปยัง Remote
หลังจากลบไฟล์ออก History จะเปลี่ยนไป คุณต้อง Force Push ไปยัง Remote Repository ของคุณ
# Force push ทุก branch
git push origin --force --all
# Force push ทุก tag
git push origin --force --tags
# หรือใช้ --force-with-lease เพื่อปลอดภัยกว่า
git push origin --force-with-lease --all
git push origin --force-with-lease --tags
# ตรวจสอบว่า push สำเร็จ
git log --oneline | head -n 10
หมายเหตุ: ใช้ –force-with-lease แทน –force เมื่อสามารถ เพราะว่ามันปลอดภัยกว่า และจะป้องกันไม่ให้คุณลบ Commits ของคนอื่น โดยไม่ได้รับ Reject หากมี Commits ใหม่บน Remote
ขั้นตอนที่ 6: เพิ่ม .gitignore เพื่อป้องกันการเกิดซ้ำ
สิ่งสำคัญที่สุดคือการป้องกันไม่ให้เกิดปัญหาเดียวกันอีก ให้เพิ่มไฟล์ที่ไม่ควร Commit ลงใน .gitignore
# สร้าง .gitignore ใหม่ หรือแก้ไขที่มีอยู่
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.*.local" >> .gitignore
echo ".env.development.local" >> .gitignore
echo ".env.test.local" >> .gitignore
echo ".env.production.local" >> .gitignore
echo "*.pem" >> .gitignore
echo "*.key" >> .gitignore
echo "node_modules/" >> .gitignore
echo ".DS_Store" >> .gitignore
echo "Thumbs.db" >> .gitignore
# Commit .gitignore
git add .gitignore
git commit -m "chore: เพิ่ม .gitignore เพื่อป้องกันไม่ให้ Commit ไฟล์ที่มี Secrets"
git push origin main
Template .gitignore ที่สมบูรณ์
นี่คือ .gitignore ที่สมบูรณ์ที่ควรมีไฟล์ต่างๆ ที่ไม่ควร Commit ซึ่งคุณสามารถใช้เป็นแม่แบบได้:
# Environment Variables และ Secrets
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
.env.staging.local
# OS Files
.DS_Store
Thumbs.db
.windows-build-tools
.AppleDouble
.LSOverride
*.swp
*~
# IDE & Editor
.vscode/
.idea/
.sublime-project
.sublime-workspace
.vim
*.swp
*.swo
*~
.project
.pydevproject
.settings/
*.iml
# Node.js
node_modules/
npm-debug.log
yarn-error.log
package-lock.json (ถ้าไม่ share กับทีม)
yarn.lock (ถ้าไม่ share กับทีม)
# Python
venv/
env/
env.bak/
venv.bak/
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Credentials & Secrets
id_rsa
id_rsa.pub
*.pem
*.key
config/secrets.yml
.credentials
.aws/credentials
.aws/config
.ssh/
private_key
access_token
refresh_token
# Database
*.db
*.sqlite
*.sqlite3
.mysql_history
# Build Files
/dist
/build
*.egg-info/
# Logs
*.log
logs/
# Temporary files
*.tmp
*.bak
*.swp
# OS specific
.env.vault
.envrc
กรณีฉุกเฉิน: Credentials ถูกเผยแพร่ไปแล้ว
หากเกิดว่า Credentials ของคุณเผยแพร่ไปแล้วใน Public Repository โดยไม่ระวัง คุณต้องทำการดังต่อไปนี้ทันที เพื่อลดความเสี่ยง:
- ตรวจสอบการใช้งาน: ตรวจสอบบัญชี API และ Database นั้นทันที ว่ามีกิจกรรมที่ผิดปกติหรือไม่ โดยตรวจสอบ Activity Logs
- Revoke Credentials ทันที: ยกเลิก API Keys Access Tokens Database Passwords ทั้งหมด ก่อนลบออกจาก History
- สร้าง Credentials ใหม่: หลังจาก Revoke ให้สร้าง Credentials ใหม่และ Update ลงใน .env ใหม่
- ลบออกจาก History: ใช้วิธี git-filter-repo เพื่อลบ Credentials เดิมออก
- Force Push ทันที: Force Push ลงไปยัง Remote ตามที่อธิบายไว้ข้างต้น
- Inform the Team: แจ้งให้ทีมของคุณทราบเพื่อให้ Clone Repository ใหม่และลบ Repository เก่าออก
- Monitor: Monitor การใช้งาน API และ Database เป็นเวลา 24-48 ชั่วโมง เพื่อตรวจสอบว่าไม่มีการใช้งานที่ผิดปกติ
การตรวจสอบขนาด Repository หลังลบไฟล์
หลังจากลบไฟล์ออกจาก History คุณสามารถตรวจสอบขนาดของ Repository ว่าลดลงหรือไม่ โดยใช้คำสั่งดังนี้:
# ดูขนาดของ .git directory
du -sh .git
# ดูขนาดของไฟล์ใหญ่ที่สุด
git rev-list --all --objects | sed -n '/^[^[:space:]]*$/p' | cut -d' ' -f2- | \
git cat-file --batch-check | grep blob | sort -k3 -n | tail -10
# ทำให้ Repository เล็กลงต่อไป (garbage collection)
git gc --aggressive --prune=now
# ตรวจสอบขนาดอีกครั้ง
du -sh .git
ความเสี่ยงและข้อพึงระวัง
- Force Push จำเป็น: ต้องใช้ Force Push ซึ่งอาจ Conflict กับ History ของคนอื่น ให้แจ้งทีมก่อน
- Backup สำคัญมาก: สร้าง Backup ของ Repository ก่อนทำการลบไฟล์ เพราะหากพลาดจะกู้คืนอีกวิธีแล้ว
- ไม่สามารถเรียกคืนได้ (หลังจาก Push): ถ้าลบไฟล์ผิด และ Push ไปแล้ว ให้ใช้ Reflog เพื่อกู้คืน
- ทรีของเครื่อง: Collaborators ต้องลบ Cached Objects บนเครื่องของพวกเขา และ Clone Repository ใหม่
- Tags และ Branches: ตรวจสอบว่า Tags และ Branches ถูก Update อย่างถูกต้อง
ความแตกต่าง: git-filter-repo vs git filter-branch
ทั้งสองเครื่องมือต่างก็ทำหน้าที่เดียวกัน แต่มีข้อแตกต่างสำคัญที่ต้องทำความเข้าใจ:
- ความเร็ว: git-filter-repo เร็วกว่า 10-100 เท่า เพราะมันเขียนใหม่ใน Python ที่ดีกว่า
- Reliability: git-filter-repo เสถียรกว่า และ recommended โดย Git Official Team
- ฟีเจอร์: git-filter-repo มีฟีเจอร์มากกว่า เช่น dry run analyze mailmap เป็นต้น
- Legacy Support: git filter-branch ยังคงถูกสนับสนุน แต่ deprecated แล้ว
- Documentation: git-filter-repo มี Documentation ที่ดีและ Examples ชัดเจน
ตัวอย่างการลบไฟล์หลายไฟล์พร้อมกัน
หากต้องการลบไฟล์หลายไฟล์พร้อมกัน คุณสามารถสร้าง Text File เพื่อทำงานนี้ได้อย่างมีระเบียบ:
# สร้างไฟล์ exclude-files.txt ที่มีรายการไฟล์ที่ต้องลบ
cat > exclude-files.txt << 'EOF'
.env
.env.local
.env.production
secrets/
config/database.yml
config/aws.yml
.aws/
.ssh/
private_keys/
EOF
# จากนั้นใช้ git-filter-repo กับไฟล์นี้
git filter-repo --paths-from-file exclude-files.txt --invert-paths
# ทำความสะอาด
rm exclude-files.txt
สรุป
การลบไฟล์ออกจาก Git History เป็นวิธีที่จำเป็นเมื่อเกิดการ Commit ไฟล์ที่ไม่ควร Commit เข้าไป แนะนำให้ใช้ git-filter-repo มากกว่า filter-branch เพราะว่าเร็วกว่า เสถียรกว่า และ recommended ของ Git Official Team สิ่งที่สำคัญที่สุดคือการป้องกันปัญหาในอนาคตด้วยการตั้งค่า .gitignore ที่เหมาะสม และการใช้ Pre-commit Hooks เพื่อให้แน่ใจว่าไฟล์ที่มี Secrets จะไม่ถูก Commit เข้าไปอีก ด้วยการเข้าใจ Git History Rewriting คุณจะสามารถจัดการ Repository ได้อย่างปลอดภัยและมีประสิทธิภาพ
- Git Reflog: กู้คืน Commit ที่หายไปอย่างไร?
- Git Stash: บันทึกงานชั่วคราวก่อน Switch Branch
- git reset ความต่าง Soft Mixed Hard Mode
- .gitignore: Best Practices ป้องกันไม่ให้ Commit Secrets
