Table of Contents
- บทนำ (Introduction)
- Git Hooks คืออะไร? มีกี่แบบ?
- ทำความรู้จักกับ Git
pre-commit
Hook - Use Cases การนำ Git
pre-commit
Hook มาใช้ - วิธีการ Configure และใช้งาน Git
pre-commit
Hook - ตัวอย่าง 1: การใช้งาน Git
pre-commit
Hook - ข้อจำกัดของ
pre-commit
Hook แบบปกติ - แนะนำ Frameworks ที่ช่วยจัดการ Git Hooks ให้ง่ายขึ้น
- ตัวอย่าง 2: ใช้ pre-commit (Framework) ช่วยจัดการ
pre-commit
Hook - บทสรุป (Conclusion)
บทนำ (Introduction)
กระบวนการพัฒนา software ทุกวันนี้มี tasks ซ้ำ ๆ ที่มีความซับซ้อนและกินเวลามากอยู่แทบทุกส่วน เหตุผลก็เพื่อให้ได้มาซึ่ง application ที่มีคุณภาพและปลอดภัย เช่น การตรวจสอบต่าง ๆ ภายใน code ซึ่งการที่เราจะต้องทำสิ่งเหล่านั้นด้วยตัวเองทั้งหมดก็เป็นอะไรที่ค่อนข้างท้าทาย
แต่เราสามารถใช้ประโยชน์จากความสามารถของ Git อย่าง “Git Hooks” เพื่อช่วยทำงานบางอย่างได้ โดยเฉพาะอย่างยิ่ง pre-commit
hook ซึ่งเป็นการทำ automation ตั้งแต่ก่อน commit ซึ่งเป็นจุดที่เหมาะสมสำหรับการใช้ script เพื่อตรวจสอบหรือ transform code ก่อนจะมีการ commit code เข้าไปใน Git repository (กันไว้ย่อมดีกว่าแก้)
Git Hooks คืออะไร? มีกี่แบบ?
Git hooks
คือ script ที่จะรันโดยอัตโนมัติเมื่อมี event บางอย่างเกิดขึ้นใน Git เช่น การ commit, merge หรือ push โดยหลัก ๆ จะแบ่งออกเป็น 2 แบบคือ
1. Client-side Hooks (ทำงานฝั่ง Git Local)
pre-commit
: รันก่อน commitprepare-commit-msg
: รันหลังสร้าง commit message แต่ก่อนจะเปิด editorcommit-msg
: รันหลัง commit message ถูกสร้างเพื่อตรวจสอบ messagepost-commit
: รันหลัง commit เสร็จสมบูรณ์pre-push
: รันก่อน push ไปยัง remotepost-checkout
: รันหลังจากมีการ checkout branch หรือ restore ไฟล์ใด ๆpre-rebase
: รันก่อนมีการ rebase โดยสามารถใช้ป้องกันไม่ให้ทำ rebase กับ branch ที่ต้องการได้
2. Server-side Hooks (ทำงานฝั่ง Git Server)
pre-receive
: รันเมื่อ server รับ push เพื่อตรวจสอบก่อนupdate
: คล้าย pre-receive แต่รันต่อ branchpost-receive
: รันหลังจาก push เสร็จสมบูรณ์ เพื่อ notification หรือ update service อื่น ๆ
ซึ่งไฟล์ hooks พวกนี้จะอยู่ที่ directory .git/hooks/
และมีตัวอย่างให้ดูอยู่ใน directory นั่นแหละครับ ลอง ls -l .git/hooks/
ดูได้เลย หากจะใช้ก็แค่ลบ .sample
ออก
ทำความรู้จักกับ Git pre-commit
Hook
pre-commit
hook คือ script ที่จะถูกรันโดยอัตโนมัติก่อนการ commit (มาดักหลังจากที่เราใช้คำสั่ง git commit
ไป) โดยเราสามารถเขียน script เพื่อตรวจสอบหรือปรับแต่ง code ก่อนที่จะถูก commit เข้าไปใน repository ได้ ซึ่งเป็นการป้องกันปัญหาตั้งแต่ต้นทาง
Use Cases การนำ Git pre-commit
Hook มาใช้
วิเคราะห์ Static Code:
- ตรวจสอบ/ปรับ indentation, spacing, line breaks
- ตรวจหา bugs, potential errors, code smells
- ใช้ Prettier, ESLint สำหรับ Javascript หรือ Black, PyLint สำหรับ Python
- ฝั่ง infrastructure เช่น Ansible Lint หรือของ Terraform อย่างคำสั่ง
terraform fmt
และterraform validate
Automated Tests:
- รัน unit tests หรือ integration tests
- ตรวจสอบว่า changes ใหม่ไม่ทำให้เกิดปัญหากับฟังก์ชันที่มีอยู่
- แจ้งเตือน developer กรณีเจอ tests ไม่ผ่าน
Security และ Audit:
- รัน tools เพื่อตรวจสอบความปลอดภัยของ application หรือ infrastructure code
- ตรวจสอบ vulnerabilities หรือ insecure configurations
- ใช้ tools เช่น Checkov, Talisman หรือ Snyk
Sensitive Information:
- ตรวจหา passwords, keys, tokens หรือ API keys
- ป้องกันไม่ให้ commit ข้อมูล sensitive ขึ้น Git repository
- ใช้ tools เช่น gitleaks หรือ TruffleHog
Dependencies:
- ตรวจสอบการอัพเดท dependencies ต่าง ๆ
- เตือนกรณีที่มี dependencies version เก่าหรือมีความเสี่ยงด้านความปลอดภัย
- ใช้ tools เช่น SafetyCLI ใน Python หรือ
npm audit
,yarn audit
ของ Node.js
Version Pinning:
- ตรวจสอบว่า version ของ tools และ dependencies ให้เป็นแบบ specific version
- ป้องกันปัญหาเรื่อง compatibility จากการอัพเดทของ dependencies ใน version ใหม่ ๆ
- เช่น การใช้ package-lock.json ใน Node.js หรือ Gemfile.lock ใน Ruby
Document และ Alert/Notification:
- สร้างหรืออัพเดท document อัตโนมัติ เช่น API documentation, README.md หรือ changelogs
- ส่ง notification ไปยัง chat หรือ email หากมีการ commit ที่สำคัญ
- ใช้ tools เช่น mkdocs หรือ Sphinx
วิธีการ Configure และใช้งาน Git pre-commit
Hook
- สร้างไฟล์ script ชื่อว่า
pre-commit
ใน.git/hooks/
directory - เขียน script ตามที่ต้องการ เช่น Bash หรือ Python
- เซ็ต permission ให้ script สามารถรันได้ด้วย
chmod +x .git/hooks/pre-commit
- ทุกครั้งที่เรารัน
git commit
script จะทำงานอัตโนมัติก่อน commit จริง
ซึ่งถ้า…
- script รันสำเร็จ (exit code เท่ากับ 0) ก็จะ commit
- script รันไม่สำเร็จ (exit code ไม่เท่ากับ 0) ก็จะยกเลิก commit นั้นไป
ตัวอย่าง 1: การใช้งาน Git pre-commit
Hook
ผมใช้ pre-commit
hook นี้เพื่อช่วยในการเขียนบทความครับ โดยปกติแล้วผมจะเขียนบทความอยู่ในไฟล์ Markdown (*.md
) และผมต้องการให้มันช่วยใส่ pubDatetime
และ modDatetime
ใน frontmatter ให้อัตโนมัติ โดยมีเงื่อนไขคือ…
- ถ้าผมสร้างไฟล์ใหม่เพื่อเขียนบทความ มันจะใส่
pubDatetime
ให้ตามวันเวลาปัจจุบัน - ถ้าผมแก้ไขบทความและ commit ไป มันจะใส่
modDatetime
ให้ตามวันเวลาปัจจุบัน - และทั้งสองกรณีจะต้องเป็น
draft: false
เท่านั้น เพราะถ้าเป็น draft ผมจะไม่บันทึกวันเวลา
รูปด้านล่างนี้เปรียบเทียบ flow ระหว่างการ “ใช้ และ ไม่ใช้” pre-commit
hook
น่าจะพอเห็นภาพบ้างแล้ว งั้นไปดูวิธีการทำกันเลย (ลองทำตามได้นะครับ)
- หลังจากที่เรา
git init
แล้ว ให้สร้างไฟล์pre-commit
ใน directory.git/hooks/
และเขียน Bash script ตามนี้
#!/bin/bash
# Check if the file has the frontmatter
has_frontmatter() {
local file_path="$1"
local first_line=$(head -n 1 "$file_path")
local second_line=$(head -n 2 "$file_path" | tail -n 1)
if [[ $first_line != "---" || ! $second_line =~ author: ]]; then
return 1
fi
return 0
}
# Get the current UTC and Bangkok time
get_current_time() {
local utc_time=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
local bkk_time=$(TZ="Asia/Bangkok" date "+%d %b %Y %H:%M:%S")
echo "$utc_time # $bkk_time (Bangkok)"
}
# Function to update the datetime field in frontmatter
update_datetime() {
local file="$1"
local file_status="$2"
local field="$3"
local current_time="$4"
# Split the file into frontmatter and content
csplit "$file" '/^---$/2' > /dev/null
# Update the frontmatter using appropriate sed command based on OS
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/^$field:.*/$field: $current_time/" xx00
else
sed -i "s/^$field:.*/$field: $current_time/" xx00
fi
# Merge updated frontmatter and content
cat xx00 xx01 > temp_updated_file
# Update the original file
mv temp_updated_file "$file"
rm xx00 xx01
echo "- Updated $field for $file ($file_status)"
}
main() {
local changed_files="$1"
# Check if there are any changed files to process
if [ -n "$changed_files" ]; then
while read -r file_status file1 file2; do
# Determine the file path to process based on its status
# For renames (R status), use the new file name
local file_path=""
if [[ "$file_status" == "R"* ]]; then
file_path="$file2"
else
file_path="$file1"
fi
# Skip processing for deleted files, renames without changes,
# files without frontmatter, or draft files
if [[ "$file_status" == "D" || "$file_status" == "R100" ]] || \
! has_frontmatter "$file_path" || grep -q "^draft: true" "$file_path"; then
echo "- Skipping file: $file_path"
continue
# Set the field name to be updated in the frontmatter
# 'pubDatetime' for added files, 'modDatetime' for others
local field_name=""
elif [[ "$file_status" == "A" ]]; then
field_name="pubDatetime"
else
field_name="modDatetime"
fi
# Get the current time in UTC and Bangkok timezone
local current_time=$(get_current_time)
# Update the datetime field in the file's frontmatter
update_datetime "$file_path" "${file_status:0:1}" "$field_name" "$current_time"
# Stage the updated file for commit
git add "$file_path"
done <<< "$changed_files"
echo ""
echo "--------------------------------------------------------------------"
fi
}
# Get the status and paths of all changed markdown files
changed_files=$(git diff --cached --name-status \
| grep -iE "(^[AMD]|^R[0-9]{3}).*\.md$" \
| awk '{print $1, $2, $3}')
main "$changed_files"
- เซ็ต permission ของ script ให้เป็น executable (สามารถรันได้)
chmod +x .git/hooks/pre-commit
- กลับมาที่ working directory สร้างไฟล์บทความใหม่ชื่อ
myarticle1.md
และเขียนเนื้อหาลงไป
---
title: "My Article 1"
draft: false
pubDatetime:
modDatetime:
---
## Topic 1
Welcome to my new article
...
...
...
- เมื่อเขียนบทความเสร็จก็ save ไฟล์และรัน
git add
จากนั้นก็git commit
git add myarticle1.md
git commit -m "add new article 1"
ปกติแล้วจะเป็นการ commit ไฟล์เข้า Git ทันที แต่เมื่อมี pre-commit
script จังหวะที่เรา commit มันจะไป trigger ให้ Bash script ทำงาน โดยจะเห็น output ว่ามันมีการเพิ่ม pubDatetime
ใน frontmatter ของไฟล์ myarticle1.md
ก่อน จากนั้นจึงรัน git add
อีกครั้งก่อนจะ commit
- Updated pubDatetime for myarticle1.md (A)
--------------------------------------------------------------------
[main a1b2c3d] add new article 1
1 file changed, 24 insertions(+)
create mode 100644 myarticle1.md
และเมื่อผมลองเปิดไฟล์ myarticle1.md
ขึ้นมาดูก็จะเห็นว่า pubDatetime
ถูกอัพเดทตามวันเวลาปัจจุบันแล้วเรียบร้อย
---
title: "My Article 1"
draft: false
pubDatetime: 2024-03-25T09:43:51Z # 25 Mar 2024 16:43:51 (Bangkok)
modDatetime:
---
## Topic 1
Welcome to my new article
...
...
...
ต่อมาเมื่อผมมีเหตุให้ต้องแก้ไขบทความ ผมก็ไม่ต้องใส่ modDatetime
เอง แค่แก้ไขไฟล์แล้ว git add
และ git commit
ไปตามปกติ เจ้า pre-commit
script ก็จะอัพเดท modDatetime
ให้อัตโนมัติแบบนี้
---
title: "My Article 1"
draft: false
pubDatetime: 2024-03-25T09:43:51Z # 25 Mar 2024 16:43:51 (Bangkok)
modDatetime: 2024-03-26T11:48:36Z # 26 Mar 2024 18:48:36 (Bangkok)
---
## Topic 1
Welcome to my updated article
...
...
...
ข้อจำกัดของ pre-commit
Hook แบบปกติ
แม้ว่าเราจะสามารถเขียน pre-commit
hook ขึ้นมาใช้เองได้ไม่ยากนัก แต่ก็มีข้อจำกัดบางประการ เช่น
- ต้องสร้างไฟล์
pre-commit
ใน.git/hooks/
เอง - อัพเดทหรือเพิ่ม hook ใหม่ลำบาก ต้องแก้ในไฟล์ script เอง
- ติดตั้งและจัดการ tools หรือ dependencies ที่ใช้ใน hook เองทั้งหมด เช่น linter หรือ formatter
- แชร์ hook configuration ให้คนอื่นในทีมใช้ได้ยาก เพราะต้องนำ script ไปวางเอง
- ไม่สามารถใช้ hooks/scripts ที่มีคนเขียนไว้แล้วจาก community ได้
แนะนำ Frameworks ที่ช่วยจัดการ Git Hooks ให้ง่ายขึ้น
เพื่อแก้ปัญหาข้อจำกัดต่าง ๆ ของการใช้ Git hooks แบบตรง ๆ ที่เราทำกันไป จึงมี framework (ที่จริงมันเหมือน wrapper แหละครับ) ที่ถูกพัฒนาขึ้นมาเพื่อช่วยให้แก้ปัญหาข้างต้น เช่น
- pre-commit: ช่วยจัดการ configuration ของ Git hooks ได้ง่ายขึ้น (ไม่ใช่แค่
pre-commit
ตามชื่อ) - Husky: ช่วยจัดการ hooks สำหรับ Node.js project (JavaScript หรือ TypeScript) ผ่าน
package.json
ตัวอย่าง 2: ใช้ pre-commit (Framework) ช่วยจัดการ pre-commit
Hook
ด้วยความยุ่งยากของการใช้ Git hooks โดยตรงแบบที่ผมได้อธิบายไป เดี๋ยว section นี้เราจะมาปรับ pre-commit
hook ให้จัดการได้ง่ายขึ้นผ่าน framework ที่ชื่อว่า pre-commit กันครับ
- ติดตั้ง pre-commit (framework) ก่อน โดยเครื่องเราต้องมี Python ด้วยนะครับ
pip install pre-commit
- ย้ายไฟล์
.git/hooks/pre-commit
ไปไว้ที่ root ของ project แล้วเปลี่ยนชื่อเป็นupdate_frontmatter.sh
mv .git/hooks/pre-commit update_frontmatter.sh
- สร้างไฟล์
.pre-commit-config.yaml
ที่ root ของ project ตามนี้
repos:
- repo: local
hooks:
- id: update-frontmatter-datetime
name: Update frontmatter datetime
entry: bash update_frontmatter.sh
language: system
types: [markdown]
stages: [commit]
verbose: true
- repo: local
เราจะใช้ hook ที่เขียนเองบนเครื่อง (ไม่ได้ดึงมาจาก remote repository)hooks:
กำหนด hooks ต่าง ๆ ที่เราต้องการใช้งาน (มีได้หลายตัว)- id: update-frontmatter-datetime
กำหนด ID ของ hook นี้ ใช้อ้างอิงถึงได้name: Update frontmatter datetime
กำหนดชื่อของ hookentry: bash update_frontmatter.sh
กำหนด script ที่ต้องการรันด้วย hooklanguage: system
กำหนดภาษาที่ใช้ใน hook (ดูเพิ่มที่ hooks language)types: [markdown]
ระบุประเภทไฟล์ที่ hook นี้จะถูกเรียกใช้ด้วย (ในที่นี้คือไฟล์.md
เท่านั้น)stages: [commit]
ระบุ stage ของ Git ที่ต้องการให้ hook นี้ทำงาน (ในที่นี้คือจังหวะ commit)verbose: true
: ให้แสดง output จาก script ด้วย
สรุปคือ configuration นี้กำหนดให้ pre-commit framework รัน script update_frontmatter.sh
ทุกครั้งก่อนการ commit โดยจะรันกับไฟล์ Markdown (.md
) ใน stage area เท่านั้น
ที่จริงผมสามารถปรับ Bash script ให้ใช้ชื่อไฟล์จาก framework ได้โดยใช้
pass_filenames: true
ใน configuration แต่ผมขี้เกียจปรับ logic ใน code ใหม่บวกกับข้อจำกัดบางอย่างของเคสนี้ที่การ loop through จากgit
command นั้นง่ายกว่า
- ใช้ pre-commit (framework) ช่วย generate
pre-commit
script ขึ้นมา
pre-commit install
สิ่งที่เรา configure ไว้มันจะถูกนำไปสร้างเป็น pre-commit
hook ใน .git/hooks/
ให้
- ทดสอบการทำงานของ
pre-commit
hook ใหม่ด้วยการแก้ไขไฟล์myarticle1.md
แล้วลอง commit ดูครับ
git add myarticle1.md
git commit -m "test new pre-commit hook"
เมื่อเรารัน git commit
มันจะเรียก pre-commit
hook ที่เรากำหนดจากไฟล์ .pre-commit-config.yaml
ซึ่งจะไปรัน Bash script ของเรา และถ้าทุกอย่างเรียบร้อยก็จะเห็นมันอัพเดท pubDatetime
หรือ modDatetime
ในไฟล์ที่เรา commit ไปให้ครับ
ในตัวอย่างที่ 2 นี้เราสามารถนำไฟล์ configuration และ script นี้ commit ขึ้น Git repository เพื่อให้คนอื่น ๆ ในทีมสามารถนำไปใช้ได้ทันทีโดยไม่ต้องมา copy script เองให้เสียเวลา
บทสรุป (Conclusion)
จะเห็นว่า Git hooks (โดยเฉพาะ pre-commit
hook) นั้นมีประโยชน์มากในการช่วยเพิ่มประสิทธิภาพการทำงานและคุณภาพของ code ด้วยการทำ automation ตั้งแต่ก่อนการ commit เข้าไปใน Git repository
เราสามารถเขียน pre-commit
script ขึ้นมาใช้เองได้ไม่ยาก ด้วยการสร้างไฟล์ pre-commit
ใน .git/hooks/
และเขียน code ให้ทำงานตามที่เราต้องการ เช่น ตรวจสอบและปรับ code formatting, รัน automated tests, scan หาช่องโหว่ด้านความปลอดภัย และอื่น ๆ อีกมากมาย
อย่างไรก็ตามการใช้ pre-commit
hook แบบธรรมดาก็มีข้อจำกัดในเรื่องของความยุ่งยากในการแชร์ให้กับคนในทีม, การอัพเดท/เพิ่ม hook ใหม่หรือการใช้ script จาก community ดังนั้นจึงมี frameworks ที่ถูกพัฒนาขึ้นมาเพื่อช่วยจัดการเรื่องพวกนี้ให้ง่ายขึ้นอย่าง pre-commit หรือ Husky ยังไงไปลองใช้กันดูครับ