/static/codemoomoo2.png

8. Control Version (Git)

บทความนี้นำเสนอแนวคิดและการใช้งานระบบควบคุมเวอร์ชัน (Version Control System – VCS) โดยเน้น Git ตั้งแต่พื้นฐานการติดตั้ง คำสั่งทั่วไป การแตกสาขา (Branching) การทำงานกับ Remote Repository ไปจนถึง Workflow ระดับองค์กรและเทคนิคขั้นสูง พร้อมตัวอย่างปฏิบัติที่นำไปทดลองใช้ได้จริง


8.1 แนวคิดของ Version Control System

ระบบควบคุมเวอร์ชัน (Version Control System – VCS) คือเครื่องมือที่ใช้บันทึกการเปลี่ยนแปลงของไฟล์ (โดยเฉพาะซอร์สโค้ด) ตลอดเวลา ทำให้สามารถย้อนดูประวัติ เปรียบเทียบความต่าง กลับไปยังเวอร์ชันก่อนหน้า และทำงานร่วมกับผู้อื่นได้อย่างเป็นระบบ

8.1.1 Local VCS vs Centralized (CVS, SVN) vs Distributed (Git, Mercurial, Fossil)

VCS ถูกพัฒนามาในสามรูปแบบหลัก ซึ่งสะท้อนวิวัฒนาการของวิธีจัดเก็บประวัติการเปลี่ยนแปลง

8.1.1.1 Local Version Control System

Local VCS เป็นรูปแบบดั้งเดิมที่สุด เก็บประวัติไว้ในเครื่องของผู้ใช้เพียงเครื่องเดียว โดยใช้ฐานข้อมูลในเครื่อง (Local Database) เพื่อจดจำการเปลี่ยนแปลงของไฟล์ในแต่ละ Revision

8.1.1.2 Centralized Version Control System (CVCS)

CVCS เก็บประวัติทั้งหมดไว้บน Server กลาง เพียงตัวเดียว และผู้พัฒนาแต่ละคนต้อง Checkout ไฟล์มาที่เครื่องตนเอง

8.1.1.3 Distributed Version Control System (DVCS)

DVCS ทุก Client มีสำเนา (Clone) ของ Repository เต็มรูปแบบ รวมประวัติทั้งหมด ทำให้สามารถทำงานแบบ Offline ได้ และไม่มี Single Point of Failure

flowchart LR
    subgraph LOCAL["Local VCS (เครื่องเดียว)"]
        DEV1[Developer
ผู้พัฒนา] --- DB1[(Local DB
ฐานข้อมูลในเครื่อง)] end subgraph CENTRAL["Centralized VCS (SVN, CVS)"] DEV2[Developer A] --> SRV[Central Server
เซิร์ฟเวอร์กลาง] DEV3[Developer B] --> SRV DEV4[Developer C] --> SRV SRV --- DB2[(Repository
คลังเก็บ)] end subgraph DIST["Distributed VCS (Git, Mercurial)"] DEV5[Developer A] --- DB3[(Full Clone)] DEV6[Developer B] --- DB4[(Full Clone)] DEV7[Developer C] --- DB5[(Full Clone)] DEV5 -.sync.- DEV6 DEV6 -.sync.- DEV7 DEV5 -.sync.- DEV7 end style LOCAL fill:#3c3836,stroke:#d79921,color:#ebdbb2 style CENTRAL fill:#3c3836,stroke:#458588,color:#ebdbb2 style DIST fill:#3c3836,stroke:#98971a,color:#ebdbb2

8.1.1.4 ตารางเปรียบเทียบ VCS แต่ละประเภท

คุณสมบัติ (Feature) Local VCS Centralized (SVN) Distributed (Git)
ที่เก็บประวัติ (History Storage) เครื่องเดียว Server กลาง ทุกเครื่อง (Clone เต็ม)
ทำงาน Offline
Single Point of Failure ✓ (เครื่องตัวเอง) ✓ (Server)
ความเร็วในการ Branch ช้า/ไม่มี ช้า (สร้าง folder) เร็วมาก (pointer)
Merge ซับซ้อน ยาก ปานกลาง รองรับดี
ความยากในการเรียนรู้ ง่าย ปานกลาง ปานกลาง-ยาก
ตัวอย่างเครื่องมือ RCS, SCCS CVS, SVN, Perforce Git, Mercurial, Fossil

8.1.2 ประโยชน์: History, Collaboration, Branching, Backup

ประโยชน์หลักของการใช้ VCS โดยเฉพาะ Git สามารถสรุปได้ดังนี้

  1. ประวัติการเปลี่ยนแปลง (History Tracking) – บันทึก commit ทุกครั้งพร้อมผู้แก้ไข เวลา และข้อความอธิบาย ทำให้สามารถสืบสวน (Forensic) ได้ว่าใครแก้ไข อะไร เมื่อไร และทำไม
  2. การทำงานร่วมกัน (Collaboration) – ผู้พัฒนาหลายคนสามารถแก้ไขโค้ดส่วนเดียวกันได้ผ่าน Branch แล้ว Merge กลับ ลดการชนกันของไฟล์
  3. การแตกสาขา (Branching) และทดลอง – แตก Branch สำหรับฟีเจอร์ใหม่หรือแก้บั๊ก โดยไม่กระทบโค้ดหลัก ทดลองได้เต็มที่
  4. การสำรองข้อมูล (Backup) และกู้คืน – ทุก clone คือ full backup กู้คืนเวอร์ชันก่อนหน้าได้ทุกเมื่อ
  5. Code Review และ Quality Control – บูรณาการกับ Pull Request / Merge Request เพื่อตรวจคุณภาพโค้ดก่อนรวมเข้า main
  6. Continuous Integration / Deployment – CI/CD Pipeline อ่าน commit history เพื่อ build, test, deploy อัตโนมัติ

สูตรประเมินคุณค่าของ VCS สามารถมองในเชิง Mean Time To Recovery (MTTR) เมื่อเกิดปัญหา:

MTTR = Time(Detect) +Time(Revert) +Time(Deploy) N(Incidents)

โดยที่ Time(Detect) คือเวลาที่ใช้ตรวจพบปัญหา, Time(Revert) คือเวลาในการย้อน commit ผ่าน git revert หรือ git reset (โดยทั่วไป Git ใช้เวลาน้อยกว่า 1 นาที), Time(Deploy) คือเวลาที่ใช้ deploy เวอร์ชันที่กลับไปแล้ว, และ N(Incidents) คือจำนวนเหตุการณ์ทั้งหมด ยิ่ง MTTR ต่ำ ยิ่งแสดงว่าระบบฟื้นตัวได้เร็ว

8.1.3 ประวัติของ Git (Linus Torvalds, 2005)

Git ถือกำเนิดในปี ค.ศ. 2005 จากความจำเป็นของโครงการ Linux Kernel หลังจากที่ผู้พัฒนา Linux เคยใช้ระบบเชิงพาณิชย์ชื่อ BitKeeper ฟรีอยู่ระยะหนึ่ง แต่ผู้พัฒนา BitKeeper ตัดสินใจถอนสิทธิ์การใช้งานฟรี Linus Torvalds จึงตัดสินใจสร้าง VCS ใหม่ที่มีคุณสมบัติตรงตามที่ Linux Kernel ต้องการ

8.1.3.1 เป้าหมายการออกแบบของ Git

8.1.3.2 Timeline วิวัฒนาการของ VCS

flowchart TB
    subgraph ERA1["ยุค 1970s-1980s Local VCS"]
        SCCS[SCCS
1972] RCS[RCS
1982] end subgraph ERA2["ยุค 1990s-2000s Centralized VCS"] CVS[CVS
1990] SVN[Subversion
2000] PERFORCE[Perforce
1995] end subgraph ERA3["ยุค 2005+ Distributed VCS"] BK[BitKeeper
2000] GIT[Git
2005] HG[Mercurial
2005] FOSSIL[Fossil
2007] end subgraph ERA4["ยุค Cloud Hosting 2008+"] GH[GitHub
2008] BB[Bitbucket
2008] GL[GitLab
2011] FORGE[Forgejo/Gitea
2016+] end SCCS --> RCS RCS --> CVS CVS --> SVN SVN --> BK BK --> GIT BK --> HG GIT --> GH GIT --> BB GIT --> GL GIT --> FORGE style ERA1 fill:#3c3836,stroke:#cc241d,color:#ebdbb2 style ERA2 fill:#3c3836,stroke:#d79921,color:#ebdbb2 style ERA3 fill:#3c3836,stroke:#98971a,color:#ebdbb2 style ERA4 fill:#3c3836,stroke:#458588,color:#ebdbb2

ชื่อ "Git" Linus Torvalds เคยให้สัมภาษณ์ขำ ๆ ว่ามาจากภาษา British Slang ที่หมายถึง "คนน่ารำคาญ" และเล่นมุกว่า "ผมตั้งชื่อโปรแกรมตามตัวเองทุกครั้ง คราวก่อนคือ Linux คราวนี้คือ Git"


8.2 การติดตั้งและตั้งค่า Git

8.2.1 การติดตั้งบน Linux, macOS, Windows

การติดตั้ง Git แตกต่างกันตามระบบปฏิบัติการ

8.2.1.1 Linux

# Debian/Ubuntu - ใช้ APT
sudo apt update
sudo apt install -y git

# Fedora/RHEL/Rocky - ใช้ DNF
sudo dnf install -y git

# Arch Linux/Manjaro - ใช้ Pacman
sudo pacman -S git

# openSUSE - ใช้ Zypper
sudo zypper install git

# Alpine Linux - ใช้ APK
sudo apk add git

# ตรวจสอบเวอร์ชันหลังติดตั้ง
git --version
# ตัวอย่าง output: git version 2.43.0

8.2.1.2 macOS

# ผ่าน Homebrew (แนะนำ)
brew install git

# ผ่าน Xcode Command Line Tools
xcode-select --install

# ตรวจสอบ
git --version

8.2.1.3 Windows

# ผ่าน winget (ตัวจัดการแพ็กเกจของ Windows 10/11)
winget install --id Git.Git -e

# ผ่าน Chocolatey
choco install git -y

# ผ่าน Scoop
scoop install git

# หรือดาวน์โหลด Git for Windows จาก https://git-scm.com/
# ซึ่งมาพร้อม Git Bash, MinTTY, Git GUI

8.2.2 git config ระดับ --system, --global, --local

Git มี config 3 ระดับ ที่ override ซึ่งกันและกันตามลำดับ (ระดับ local มีลำดับสูงสุด)

ระดับ (Level) Flag ตำแหน่งไฟล์ ขอบเขต
System --system /etc/gitconfig ผู้ใช้ทุกคนในเครื่อง
Global (User) --global ~/.gitconfig หรือ ~/.config/git/config ผู้ใช้ปัจจุบัน ทุก repo
Local (Repo) --local .git/config repo ปัจจุบันเท่านั้น
# ดูค่า config ทั้งหมด พร้อมแหล่งที่มา
git config --list --show-origin

# ตั้งค่าระดับ Global (ใช้บ่อยที่สุด)
git config --global user.name "Moo Lecturer"
git config --global user.email "moo@example.ac.th"

# ตั้งค่าเฉพาะ repo (เช่น ใช้ email งานในโปรเจกต์งาน)
cd ~/projects/work-project
git config --local user.email "moo.work@company.com"

# ลบค่าที่ตั้งไว้
git config --global --unset user.email

# แก้ไข config ผ่าน editor
git config --global --edit

8.2.3 ค่าพื้นฐาน: user.name, user.email, core.editor, init.defaultBranch, pull.rebase

ค่า config สำคัญที่ควรตั้งหลังติดตั้ง Git

# ตัวตนของผู้ใช้ (จะติดไปกับทุก commit)
git config --global user.name "Moo Lecturer"
git config --global user.email "moo@rmutsv.ac.th"

# Editor ที่ใช้ในการเขียน commit message
git config --global core.editor "vim"        # Vim
git config --global core.editor "nano"       # Nano
git config --global core.editor "code --wait" # VS Code
git config --global core.editor "zed --wait"  # Zed

# ชื่อ Branch เริ่มต้นเมื่อใช้ git init (ปัจจุบันนิยม main แทน master)
git config --global init.defaultBranch main

# พฤติกรรมของ git pull - เลือก rebase แทน merge เพื่อ history สะอาด
git config --global pull.rebase true

# จัดการ line ending ระหว่างระบบ (สำคัญสำหรับทีมข้ามแพลตฟอร์ม)
git config --global core.autocrlf input    # บน Linux/macOS
git config --global core.autocrlf true     # บน Windows

# เปิดสีในเอาต์พุต
git config --global color.ui auto

# Alias ช่วยพิมพ์สั้นลง
git config --global alias.st "status -sb"
git config --global alias.co "checkout"
git config --global alias.br "branch"
git config --global alias.lg "log --oneline --graph --all --decorate"

# Push เฉพาะ branch ปัจจุบัน
git config --global push.default current

# ใช้ rerere (reuse recorded resolution) ลดการแก้ conflict ซ้ำ
git config --global rerere.enabled true

8.2.4 Credential Helper

Credential Helper ช่วยจดจำ username/password หรือ token เพื่อไม่ต้องพิมพ์ทุกครั้งที่ push/pull จาก Remote

# Cache ใน RAM (default 15 นาที, ปรับเวลาเป็นวินาที)
git config --global credential.helper "cache --timeout=3600"

# เก็บใน plain text (ไม่แนะนำสำหรับเครื่อง shared)
git config --global credential.helper store

# บน macOS - ใช้ Keychain (เข้ารหัสในระบบ)
git config --global credential.helper osxkeychain

# บน Windows - ใช้ Git Credential Manager
git config --global credential.helper manager-core

# บน Linux - ใช้ libsecret (GNOME Keyring/KWallet)
sudo apt install libsecret-1-0 libsecret-1-dev   # Debian/Ubuntu
git config --global credential.helper /usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret

# ใช้ SSH key แทน HTTPS (วิธีที่แนะนำที่สุด)
# สร้าง key
ssh-keygen -t ed25519 -C "moo@rmutsv.ac.th"
# เพิ่ม public key ใน GitHub/GitLab
cat ~/.ssh/id_ed25519.pub

8.3 คำสั่งพื้นฐานของ Git

8.3.1 git init, git clone

# สร้าง repository ใหม่ในโฟลเดอร์ปัจจุบัน
mkdir my-project && cd my-project
git init
# ผลลัพธ์: สร้างโฟลเดอร์ .git/ ที่เก็บข้อมูลทั้งหมด

# สร้าง repository พร้อมระบุ branch เริ่มต้น
git init --initial-branch=main

# สร้าง bare repository (สำหรับใช้เป็น remote บน server)
git init --bare /srv/git/my-project.git

# Clone จาก remote (HTTPS)
git clone https://github.com/torvalds/linux.git

# Clone ผ่าน SSH (แนะนำเมื่อมี SSH key)
git clone git@github.com:torvalds/linux.git

# Clone ลง folder ชื่ออื่น
git clone https://github.com/user/repo.git my-folder

# Clone เฉพาะ branch
git clone -b develop --single-branch https://github.com/user/repo.git

# Shallow clone (เอาเฉพาะ commit ล่าสุด ลดเวลา/ขนาด)
git clone --depth 1 https://github.com/user/repo.git

8.3.2 git status, git add, git commit (-m, -a, --amend)

# ดูสถานะของ Working Directory และ Staging Area
git status
git status -s     # short format (สั้น เห็นง่าย)
git status -sb    # short + branch info

# เพิ่มไฟล์เข้า Staging Area
git add file1.txt           # ไฟล์เดียว
git add file1.txt file2.py  # หลายไฟล์
git add .                   # ทุกไฟล์ในโฟลเดอร์ปัจจุบัน
git add -A                  # ทุกไฟล์ที่เปลี่ยน (รวม deleted)
git add *.py                # ใช้ wildcard
git add -p                  # interactive: เลือก hunk ที่จะ stage
git add -u                  # เฉพาะไฟล์ที่ tracked อยู่แล้ว

# Commit
git commit -m "feat: add user login feature"

# Commit ทุกไฟล์ที่ tracked โดยไม่ต้อง add (ข้าม staging)
git commit -am "fix: typo in README"

# แก้ commit ล่าสุด (เพิ่มไฟล์ที่ลืม หรือแก้ message)
git add forgotten-file.txt
git commit --amend --no-edit       # ไม่แก้ message
git commit --amend -m "new message" # แก้ message

# Commit แบบเขียน message ยาว (เปิด editor)
git commit

8.3.2.1 Conventional Commits

มาตรฐานการเขียน commit message ที่นิยมในโปรเจกต์โอเพนซอร์ส มีรูปแบบ:

<type>(<scope>): <subject>

<body>

<footer>
Type ความหมาย
feat ฟีเจอร์ใหม่
fix แก้ไขบั๊ก
docs เปลี่ยนเอกสาร
style จัดรูปแบบ ไม่กระทบ logic
refactor ปรับโครงสร้างโค้ดโดยไม่เปลี่ยนพฤติกรรม
test เพิ่ม/แก้ test
chore งานบำรุงรักษา (build, dep update)
perf ปรับปรุงประสิทธิภาพ
ci เปลี่ยน CI/CD config

ตัวอย่าง:

feat(auth): add OAuth2 login with Google

- เพิ่ม endpoint /auth/google
- ใช้ passport-google-oauth20
- เพิ่ม environment variable GOOGLE_CLIENT_ID

Closes #123

8.3.3 Working Directory → Staging Area (Index) → Repository

Git มี 3 พื้นที่หลัก (Three Trees) ที่ไฟล์เดินทางผ่าน

flowchart LR
    WD["Working Directory
โฟลเดอร์ทำงาน
(ไฟล์จริงในเครื่อง)"] SA["Staging Area / Index
พื้นที่เตรียม commit
(.git/index)"] REPO["Repository
คลังเก็บ commit
(.git/objects)"] REMOTE["Remote Repository
(GitHub/GitLab/...)"] WD -->|"git add"| SA SA -->|"git commit"| REPO REPO -->|"git push"| REMOTE REMOTE -->|"git fetch / pull"| REPO REPO -->|"git checkout / restore"| WD SA -->|"git restore --staged"| WD style WD fill:#3c3836,stroke:#d79921,color:#ebdbb2 style SA fill:#3c3836,stroke:#d65d0e,color:#ebdbb2 style REPO fill:#3c3836,stroke:#98971a,color:#ebdbb2 style REMOTE fill:#3c3836,stroke:#458588,color:#ebdbb2

8.3.4 git log (--oneline, --graph, --all, --author)

# ดูประวัติทั้งหมด (ละเอียด)
git log

# แบบบรรทัดเดียว สั้นกระชับ
git log --oneline

# แสดงกราฟของ branch
git log --oneline --graph --all --decorate

# กรองตาม author
git log --author="Moo"

# กรองตามช่วงเวลา
git log --since="2 weeks ago" --until="yesterday"
git log --after="2024-01-01" --before="2024-12-31"

# ค้นหาใน commit message
git log --grep="bug"

# ค้นหาใน source code (pickaxe)
git log -S"function_name"

# ดูว่า commit ใดแก้ไขไฟล์ใด (พร้อม diff)
git log -p file.txt

# สถิติ insertion/deletion
git log --stat
git log --shortstat

# Format แบบกำหนดเอง (ใช้ใน script)
git log --pretty=format:"%h - %an, %ar : %s"
# %h = short hash, %an = author name, %ar = relative time, %s = subject

# alias ที่นิยม
git config --global alias.lg "log --color --graph \
  --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' \
  --abbrev-commit"
git lg

8.3.5 git diff (Working vs Staged vs Commit)

# ความต่างระหว่าง Working Directory กับ Staging Area
git diff

# ความต่างระหว่าง Staging Area กับ commit ล่าสุด (HEAD)
git diff --staged
git diff --cached  # alias เดียวกัน

# ความต่างระหว่าง Working Directory กับ HEAD (รวม unstaged + staged)
git diff HEAD

# เปรียบเทียบสอง commit
git diff abc123 def456

# เปรียบเทียบ branch
git diff main..feature
git diff main...feature   # 3 dots = หา common ancestor

# เปรียบเทียบเฉพาะไฟล์
git diff main feature -- src/app.py

# แสดงเฉพาะชื่อไฟล์ที่เปลี่ยน
git diff --name-only
git diff --name-status   # พร้อมสถานะ A/M/D

# นับ insertion/deletion
git diff --shortstat

# ใช้ word-level diff (เห็นการเปลี่ยนคำ ไม่ใช่บรรทัด)
git diff --word-diff

8.3.6 .gitignore และ global gitignore

ไฟล์ .gitignore บอก Git ว่า "ไฟล์ใดไม่ต้อง track" – มักเก็บไฟล์ build, secret, IDE config, dependency

# ตัวอย่าง .gitignore สำหรับโปรเจกต์ Python + Node + IDE
cat > .gitignore <<'EOF'
# === Python ===
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
.venv/
env/
*.egg-info/
dist/
build/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# === Node.js ===
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# === Environment & Secrets ===
.env
.env.local
.env.*.local
*.pem
*.key
secrets.yml

# === IDE / Editor ===
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db

# === Build artifacts ===
*.log
*.tmp
coverage/
.nyc_output/

# === ยกเว้นบางไฟล์จากการ ignore (negation) ===
!important.log
EOF

# Global gitignore (ใช้กับทุก repo ของผู้ใช้)
git config --global core.excludesfile ~/.gitignore_global
cat > ~/.gitignore_global <<'EOF'
# OS-specific files
.DS_Store
Thumbs.db
desktop.ini
# IDE-specific (ส่วนตัว ไม่ควรใส่ใน repo)
.idea/
.vscode/settings.json
EOF

# ถ้าไฟล์ถูก track ไปแล้ว ต้องลบออกก่อน .gitignore จะมีผล
git rm --cached secret.env
git commit -m "chore: stop tracking secret.env"

# ตรวจว่าไฟล์ใดถูก ignore เพราะ rule ไหน
git check-ignore -v node_modules/foo.js

ดู template .gitignore สำเร็จรูปสำหรับภาษาต่าง ๆ ได้ที่ https://github.com/github/gitignore


8.4 Branching และ Merging

8.4.1 แนวคิด Branch และ HEAD

ใน Git Branch คือ pointer (ตัวชี้) ไปยัง commit หนึ่ง ไม่ใช่สำเนาของไฟล์ ทำให้แตก branch เร็วและประหยัดพื้นที่มาก

gitGraph
    commit id: "C1: init"
    commit id: "C2: add README"
    branch feature/login
    checkout feature/login
    commit id: "C3: form HTML"
    commit id: "C4: validate"
    checkout main
    commit id: "C5: fix typo"
    checkout feature/login
    commit id: "C6: connect API"
    checkout main
    merge feature/login id: "C7: merge login"
    commit id: "C8: release v1.0"

8.4.2 git branch, git checkout, git switch

# ดู branch ทั้งหมด
git branch              # local branch
git branch -r           # remote branch
git branch -a           # ทั้ง local + remote
git branch -v           # พร้อม commit hash ล่าสุด
git branch --merged     # branch ที่ merge เข้า branch ปัจจุบันแล้ว
git branch --no-merged  # branch ที่ยังไม่ merge

# สร้าง branch ใหม่ (ยังไม่ switch ไป)
git branch feature/user-auth

# สร้างและ switch ไปทีเดียว (วิธีเก่า)
git checkout -b feature/user-auth

# สร้างและ switch (วิธีใหม่ แนะนำ - ตั้งแต่ Git 2.23)
git switch -c feature/user-auth

# Switch กลับไป branch อื่น
git switch main
git checkout main       # วิธีเก่า

# เปลี่ยนชื่อ branch
git branch -m old-name new-name
git branch -m new-name        # เปลี่ยนชื่อ branch ปัจจุบัน

# ลบ branch (ปลอดภัย - ห้ามลบถ้ายังไม่ merge)
git branch -d feature/user-auth

# ลบ branch แบบบังคับ (ใช้ระวัง)
git branch -D feature/user-auth

# สร้าง branch จาก commit ที่ระบุ
git switch -c hotfix/v1.2 abc1234

# Detached HEAD - ชี้ commit โดยตรง (เพื่อดูประวัติ ไม่ควร commit)
git checkout abc1234
git switch -d abc1234   # วิธีใหม่

8.4.3 git merge: Fast-forward vs Three-way merge

มี 2 รูปแบบหลัก ของการ merge

8.4.3.1 Fast-forward Merge

เกิดเมื่อ target branch ไม่มี commit ใหม่ที่ source branch ไม่มี – Git แค่เลื่อน pointer

# scenario: main ไม่มี commit ใหม่หลังจาก feature แตกออก
git switch main
git merge feature/login
# Git จะ "fast-forward" main pointer ไปที่ commit ของ feature

8.4.3.2 Three-way Merge

เกิดเมื่อ ทั้งสอง branch มี commit ใหม่ตั้งแต่จุดแยก – Git สร้าง merge commit ใหม่ที่มี 2 parents

git switch main
git merge feature/login
# Git สร้าง merge commit ใหม่
# จะเปิด editor ให้ใส่ message

# บังคับ merge commit แม้จะ fast-forward ได้
git merge --no-ff feature/login

# บังคับ fast-forward เท่านั้น (ถ้าไม่ได้จะ error)
git merge --ff-only feature/login

# Squash merge - รวมทุก commit ของ feature เป็น 1 commit
git merge --squash feature/login
git commit -m "feat: complete login feature"
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C"
    commit id: "D"
    checkout main
    commit id: "E"
    merge feature id: "F (merge)"
    commit id: "G"

8.4.4 Merge Conflict และการแก้ไข

Merge Conflict เกิดเมื่อสอง branch แก้ไขบรรทัดเดียวกันในไฟล์เดียวกันด้วยวิธีต่างกัน Git จะหยุดและให้คนตัดสินใจ

# จำลองสถานการณ์
git switch main
echo "main version" > config.txt
git add . && git commit -m "main: edit config"

git switch -c feature
git switch main && echo "back" > /dev/null  # ทำต่อบน main

# main ไม่ได้แก้ไขใหม่ ลองเล่นแบบจริง:
git switch -c feature
echo "feature version" > config.txt
git add . && git commit -m "feature: edit config"

git switch main
echo "main updated" > config.txt
git add . && git commit -m "main: update config"

git merge feature
# CONFLICT (content): Merge conflict in config.txt
# Automatic merge failed; fix conflicts and then commit the result.

ไฟล์ที่ conflict จะมี conflict marker:

<<<<<<< HEAD
main updated
=======
feature version
>>>>>>> feature

ขั้นตอนการแก้:

# 1. ดูสถานะและไฟล์ที่ conflict
git status

# 2. แก้ไขไฟล์ - เลือกเวอร์ชัน หรือผสมทั้งสอง
vim config.txt
# ลบเครื่องหมาย <<<<<<<, =======, >>>>>>> ออก เก็บเฉพาะที่ต้องการ

# 3. add ไฟล์ที่แก้แล้ว
git add config.txt

# 4. commit เพื่อจบ merge
git commit  # editor จะเปิดให้แก้ message

# หรือถ้าอยากยกเลิก merge ทั้งหมด
git merge --abort

# ใช้ tool ช่วยแก้ conflict
git mergetool
git config --global merge.tool vimdiff
git config --global merge.tool meld   # หรือ kdiff3, vscode

8.4.5 git rebase vs git merge

Rebase ย้าย commit ของ branch ปัจจุบันไป "วาง" บนยอดของ branch อื่น ทำให้ history เป็นเส้นตรง สวย แต่ rewrite history (เปลี่ยน hash)

# Merge - คง history แท้จริง สร้าง merge commit
git switch feature
git merge main

# Rebase - ทำให้ history เป็นเส้นตรง
git switch feature
git rebase main
# ถ้า conflict: แก้ -> git add -> git rebase --continue
# หรือยกเลิก: git rebase --abort
# ข้าม commit ที่ conflict: git rebase --skip

# Interactive Rebase (จะอธิบายในหัวข้อ 8.8)
git rebase -i HEAD~3

กฎทอง: อย่า rebase commit ที่ push ไป shared branch แล้ว เพราะจะทำให้คนอื่นปวดหัว

ลักษณะ Merge Rebase
รักษา history ✓ ตรงตามจริง ✗ เขียนใหม่
Merge commit สร้าง ไม่มี
ความสะอาดของกราฟ ซับซ้อน เป็นเส้นตรง
ความปลอดภัย สูง (ไม่เปลี่ยน hash) ต่ำ (เปลี่ยน hash)
เหมาะกับ shared branch private/feature branch

8.4.6 Cherry-pick

นำเฉพาะ commit ที่เลือก จาก branch หนึ่งไปอีก branch หนึ่งโดยไม่ต้อง merge ทั้ง branch

# ดู commit ของ branch ที่ต้องการ
git log feature --oneline

# Cherry-pick commit เดียว
git cherry-pick abc1234

# Cherry-pick หลาย commit
git cherry-pick abc1234 def5678

# Cherry-pick ช่วง commit
git cherry-pick abc1234..def5678   # ไม่รวม abc1234
git cherry-pick abc1234^..def5678  # รวม abc1234

# Cherry-pick โดยไม่ commit (ให้ทำต่อใน staging)
git cherry-pick -n abc1234

# จัดการ conflict
git cherry-pick --continue
git cherry-pick --abort
git cherry-pick --skip

Use case ทั่วไป: นำ hotfix จาก main ไปใส่ branch ของรุ่น release เก่าที่ยังต้อง support


8.5 Remote Repository

8.5.1 git remote (add, remove, rename, -v)

Remote Repository คือ repository ที่อยู่บน server อื่น (เช่น GitHub) ที่เราเชื่อมต่อกับ local repo

# ดู remote ทั้งหมด
git remote
git remote -v   # พร้อม URL

# เพิ่ม remote ชื่อ "origin"
git remote add origin git@github.com:moo/my-project.git

# เพิ่ม remote ที่สอง (กรณี fork)
git remote add upstream git@github.com:original-author/my-project.git

# เปลี่ยน URL ของ remote
git remote set-url origin git@gitlab.com:moo/my-project.git

# เปลี่ยนชื่อ remote
git remote rename origin github

# ลบ remote
git remote remove old-remote

# ดูข้อมูลละเอียด
git remote show origin

8.5.2 git fetch vs git pull vs git push

# Fetch - ดาวน์โหลด commit จาก remote มาที่ local แต่ไม่ merge
git fetch origin
git fetch --all       # ทุก remote
git fetch --prune     # ลบ remote-tracking ที่ไม่มีบน remote แล้ว

# Pull = Fetch + Merge (โดย default) หรือ Fetch + Rebase
git pull origin main
git pull --rebase origin main
git pull --ff-only origin main   # ยอมเฉพาะ fast-forward (ปลอดภัย)

# Push - ส่ง commit ของ local ไปที่ remote
git push origin main
git push -u origin feature/new   # -u = ตั้ง upstream ครั้งแรก

# Push ทุก branch
git push --all origin

# Push tags
git push --tags origin

# Force push (อันตราย!) - ใช้เมื่อ rebase แล้วและรู้ว่าทำอะไร
git push --force origin feature       # อันตรายมาก
git push --force-with-lease origin feature  # ปลอดภัยกว่า

# ลบ branch บน remote
git push origin --delete feature/old

8.5.3 Tracking Branch และ Upstream (-u)

Tracking Branch คือ local branch ที่ผูกกับ remote branch เฉพาะตัว ทำให้ git pull/git push ไม่ต้องระบุชื่ออีก

# ตั้ง upstream ครั้งแรก
git push -u origin feature/login
# หลังจากนี้ใช้ git push / git pull ได้เลย

# เปลี่ยน upstream ของ branch ปัจจุบัน
git branch --set-upstream-to=origin/main

# ดูสถานะ ahead/behind เทียบกับ upstream
git status -sb
# ## main...origin/main [ahead 2, behind 1]

# ดู tracking ทั้งหมด
git branch -vv

8.5.4 Origin, Upstream, Fork

ในวัฒนธรรม OSS (Open Source Software) เมื่อ contribute โปรเจกต์ของผู้อื่น:

  1. Fork repo ผ่าน GitHub UI → ได้ copy ใน account ตัวเอง
  2. Clone fork ของตัวเอง → ตั้งเป็น origin
  3. เพิ่ม upstream ชี้ไปที่ repo ต้นฉบับ
  4. ดึง update จาก upstream เป็นระยะเพื่อให้ fork ไม่ตกรุ่น
# Workflow Fork & Pull Request
git clone git@github.com:moo/awesome-lib.git   # fork ของเรา
cd awesome-lib
git remote add upstream git@github.com:original/awesome-lib.git

# อัปเดตจาก upstream
git fetch upstream
git switch main
git merge upstream/main
git push origin main

# สร้าง feature branch แล้ว push ขึ้น fork
git switch -c feature/cool-feature
# ... แก้โค้ด commit ...
git push -u origin feature/cool-feature

# เปิด Pull Request จาก fork ไปยัง upstream ผ่าน GitHub UI

8.5.5 Refspec

Refspec บอก Git ว่า "อ้างอิง local จะตรงกับอ้างอิง remote อย่างไร" รูปแบบ: <src>:<dst> หรือ +<src>:<dst> (force)

# Push branch local ชื่อ feature ไปเป็น branch ชื่อ wip บน remote
git push origin feature:wip

# Pull เฉพาะ branch
git fetch origin main:main

# ดู refspec ใน config
git config --get-all remote.origin.fetch
# +refs/heads/*:refs/remotes/origin/*

# เพิ่ม refspec - fetch tag ทั้งหมด
git config --add remote.origin.fetch "+refs/tags/*:refs/tags/*"

8.6 Git Workflow

ทีมพัฒนาแต่ละแบบเลือก Workflow ต่างกัน ขึ้นกับขนาดทีม ความถี่ในการ release และความซับซ้อนของผลิตภัณฑ์

8.6.1 Centralized Workflow

ทีมเล็ก ทุกคนทำงานบน main โดยตรง ใช้ git pull --rebase ก่อน push

# ทุกคนใน main
git switch main
git pull --rebase origin main
# แก้โค้ด
git add . && git commit -m "feat: ..."
git pull --rebase origin main
git push origin main

ข้อเสีย: เสี่ยงโค้ดพังบน main, ไม่มี code review

8.6.2 Feature Branch Workflow

ทุก feature ทำใน branch แยก แล้ว merge กลับ main ผ่าน Pull Request

git switch main && git pull
git switch -c feature/payment-integration
# ... ทำงาน + commit ...
git push -u origin feature/payment-integration
# เปิด PR ใน GitHub/GitLab
# หลัง review ผ่าน → merge

8.6.3 Git Flow (develop, feature, release, hotfix)

โครงสร้างซับซ้อน เหมาะกับโปรเจกต์ที่มี release schedule ชัดเจน

gitGraph
    commit id: "init"
    branch develop
    checkout develop
    commit id: "dev-1"
    branch feature/A
    commit id: "feat-A1"
    commit id: "feat-A2"
    checkout develop
    merge feature/A
    branch release/1.0
    commit id: "rel-prep"
    checkout main
    merge release/1.0 tag: "v1.0"
    checkout develop
    merge release/1.0
    branch hotfix/1.0.1
    commit id: "hotfix"
    checkout main
    merge hotfix/1.0.1 tag: "v1.0.1"
    checkout develop
    merge hotfix/1.0.1

8.6.4 GitHub Flow

เรียบง่าย เหมาะกับ Continuous Deployment

  1. แตก branch จาก main
  2. commit + push
  3. เปิด Pull Request
  4. Code Review
  5. Deploy + Merge

8.6.5 GitLab Flow

เพิ่ม environment branch (เช่น pre-production, production) ระหว่าง main กับ deployment

main → pre-production → production

ทุก deploy คือ merge จาก branch หนึ่งไปอีก branch หนึ่ง ทำให้ track ได้ว่ามี code ไหนอยู่ environment ไหน

8.6.6 Trunk-based Development

ทุกคน commit เข้า main (trunk) ผ่าน short-lived branch (ภายใน 1-2 วัน) ใช้ Feature Flag ซ่อนของที่ยังไม่พร้อม

ข้อดี: integration ถี่ ลด merge hell, เหมาะกับ DevOps/CI ระดับสูง
ข้อเสีย: ต้องการ test coverage และวินัยสูงมาก

Workflow ขนาดทีม ความถี่ Release ความซับซ้อน
Centralized 1-3 ต่ำ ต่ำมาก
Feature Branch 3-20 ปานกลาง ต่ำ
Git Flow 20+ ตามรอบ สูง
GitHub Flow 5-50 สูง (CD) ต่ำ-ปานกลาง
GitLab Flow 10-50 สูง ปานกลาง
Trunk-based 5-1000+ สูงมาก ต่ำ (แต่ต้องวินัยสูง)

8.7 การทำงานร่วมกัน (Collaboration)

8.7.1 Pull Request / Merge Request

Pull Request (GitHub, Gitea) หรือ Merge Request (GitLab) คือ feature ของ git host ที่ให้ผู้พัฒนาเสนอให้ merge branch ของตนเข้าสู่อีก branch หนึ่ง พร้อมระบบ Review, Discussion, CI Integration

8.7.1.1 ขั้นตอนการเปิด PR

  1. Push branch ขึ้น remote: git push -u origin feature/x
  2. ไปที่ web UI แล้วกด "New Pull Request"
  3. เลือก base branch (เช่น main) และ compare branch (feature/x)
  4. เขียน title + description ตาม template (อธิบาย what/why/how to test)
  5. กำหนด reviewer, label, milestone
  6. รอ CI ผ่าน + รอ approval
  7. Merge (Merge commit / Squash / Rebase)

8.7.1.2 PR Description Template

## What
อธิบายสิ่งที่เปลี่ยนแปลงในระดับสูง

## Why
ที่มาของการเปลี่ยนแปลง อ้างอิง Issue (Closes #123)

## How
รายละเอียดทางเทคนิคที่ผู้รีวิวควรรู้

## Test
- [ ] Unit test ผ่าน
- [ ] ทดสอบบน staging แล้ว
- [ ] อัปเดต documentation

## Screenshot
แนบรูปก่อน/หลัง ถ้าเป็น UI

8.7.2 Code Review

แนวปฏิบัติของการรีวิวที่ดี

8.7.3 Fork และ Upstream Sync

ดู 8.5.4 + เพิ่มเติม

# Sync fork กับ upstream เป็นประจำ (ผ่าน rebase)
git fetch upstream
git switch main
git rebase upstream/main
git push origin main

# หรือ merge
git merge upstream/main

8.7.4 git am และ patch (git format-patch)

สำหรับโครงการแบบ Linux Kernel ที่ใช้ email-based workflow

# สร้าง patch จาก commit
git format-patch -1 HEAD              # patch เดียวจาก HEAD
git format-patch main..feature        # patch ทุก commit ใน feature ที่ไม่มีใน main
# ผลลัพธ์: 0001-feat-add-x.patch, 0002-fix-y.patch, ...

# ส่ง patch ทาง email
git send-email --to=mailing-list@kernel.org *.patch

# Apply patch ที่ได้รับ
git am < patch.eml
git am 0001-feat-add-x.patch
git am --abort  # ยกเลิก
git am --continue  # หลังแก้ conflict

8.7.5 CODEOWNERS, CONTRIBUTING.md

ไฟล์มาตรฐานในรากของ repo:

# .github/CODEOWNERS
# Default owners
*       @moo @team-lead

# Backend code
/backend/   @backend-team

# Specific file
/Dockerfile  @devops-team

# Frontend
*.tsx   @frontend-team
*.css   @design-team

8.8 การจัดการประวัติ (History Management)

8.8.1 git reset: --soft, --mixed, --hard

git reset ย้าย HEAD ไปยัง commit ที่ระบุ มี 3 mode ตามผลกระทบต่อ Staging Area และ Working Directory

Mode HEAD Staging Area Working Directory
--soft ย้าย คงเดิม คงเดิม
--mixed (default) ย้าย reset ตาม HEAD ใหม่ คงเดิม
--hard ย้าย reset reset (อันตราย!)
# ยกเลิก commit ล่าสุด เก็บการเปลี่ยนแปลงไว้ใน staging
git reset --soft HEAD~1

# ยกเลิก commit ล่าสุด เก็บการเปลี่ยนแปลงไว้ใน working dir แต่ unstage
git reset HEAD~1   # หรือ git reset --mixed HEAD~1

# ยกเลิก commit + ทิ้งการเปลี่ยนแปลง (ใช้ระวังสุดขีด)
git reset --hard HEAD~1

# Reset ไป commit ที่ระบุ
git reset --hard abc1234

# Unstage ไฟล์ (ใหม่ ใช้ git restore)
git reset HEAD file.txt
git restore --staged file.txt   # คำสั่งใหม่ ตั้งแต่ Git 2.23

8.8.2 git revert vs git reset

# สร้าง commit ใหม่ที่ลบล้าง commit เก่า (history ปลอดภัย)
git revert abc1234

# Revert หลาย commit
git revert abc1234..def5678

# Revert merge commit (ต้องระบุ parent)
git revert -m 1 <merge-commit>

# Revert โดยไม่ commit (เก็บใน staging)
git revert -n abc1234

8.8.3 git reflog สำหรับกู้ commit

reflog คือบันทึกของทุกครั้งที่ HEAD หรือ branch reference เปลี่ยน เก็บเฉพาะใน local แต่ช่วยกู้ commit ที่ "หาย"

# ดู reflog ทั้งหมด
git reflog

# ตัวอย่าง output:
# abc1234 HEAD@{0}: reset: moving to HEAD~3
# def5678 HEAD@{1}: commit: feat: add feature X
# ...

# กู้ commit ที่ลบทิ้งไปด้วย reset --hard
git reset --hard HEAD@{1}
# หรือ
git reset --hard def5678

# reflog ของ branch
git reflog show feature/login

# reflog เก็บนานเท่าใด (default 90 วัน, expired 30 วัน)
git config gc.reflogExpire
git config gc.reflogExpireUnreachable

8.8.4 Interactive Rebase (git rebase -i): pick, squash, fixup, reword, edit, drop

เครื่องมือทรงพลังที่สุดในการจัดระเบียบ history

# rebase 5 commit ล่าสุด แบบ interactive
git rebase -i HEAD~5

Editor จะเปิดให้แก้:

pick abc1234 feat: add login
pick def5678 fix: typo
pick ghi9012 feat: add logout
pick jkl3456 wip: testing
pick mno7890 fix: bug in login

# Commands:
# p, pick   = ใช้ commit นี้
# r, reword = ใช้แต่แก้ message
# e, edit   = หยุดเพื่อแก้ commit
# s, squash = รวมเข้า commit ก่อนหน้า (รวม message)
# f, fixup  = รวมเข้า commit ก่อนหน้า (ทิ้ง message)
# x, exec   = รัน shell command
# d, drop   = ลบ commit นี้

แก้เป็น:

pick abc1234 feat: add login
fixup def5678 fix: typo
pick ghi9012 feat: add logout
drop jkl3456 wip: testing
fixup mno7890 fix: bug in login

ผลลัพธ์: รวมจาก 5 commit เหลือ 2 commit สะอาด

# Auto-squash workflow
git commit --fixup=abc1234   # สร้าง commit "fixup! ..."
git rebase -i --autosquash HEAD~10  # จัดเรียงและรวมอัตโนมัติ

8.8.5 git stash (push, pop, apply, list)

Stash = พักงานที่ยังไม่พร้อม commit ไว้ที่ "ชั้นวางชั่วคราว"

# พักงานปัจจุบัน
git stash
git stash push -m "WIP: refactoring auth"   # มี message
git stash -u                                # รวม untracked file
git stash -a                                # รวม ignored file ด้วย
git stash push -p                           # interactive: เลือก hunk

# ดู stash ทั้งหมด
git stash list
# stash@{0}: WIP on main: abc1234 last commit
# stash@{1}: On feature: ...

# ดูเนื้อหา stash
git stash show
git stash show -p stash@{1}   # full diff

# กลับมาทำงานต่อ
git stash pop          # apply + ลบ stash ล่าสุด
git stash apply        # apply แต่เก็บ stash ไว้
git stash apply stash@{2}

# ลบ stash
git stash drop stash@{1}
git stash clear        # ลบทั้งหมด

# สร้าง branch จาก stash
git stash branch new-feature stash@{0}

8.9 Tag และ Release

8.9.1 Lightweight Tag vs Annotated Tag

Tag คือ pointer ไปยัง commit เฉพาะ ที่ไม่เคลื่อนที่ ใช้ทำเครื่องหมาย version release

คุณสมบัติ Lightweight Tag Annotated Tag
เก็บผู้สร้าง/วันที่
เก็บ message
GPG sign ได้
ใช้สำหรับ bookmark ส่วนตัว release ทางการ
# Lightweight Tag - แค่ pointer
git tag v1.0-light

# Annotated Tag - object เต็มรูปแบบ (แนะนำสำหรับ release)
git tag -a v1.0 -m "Release version 1.0 - initial public release"

# Tag commit เก่า (ไม่ใช่ HEAD)
git tag -a v0.9 abc1234 -m "Beta release"

# GPG signed tag
git tag -s v1.0 -m "Signed release"

8.9.2 git tag, git push --tags

# ดู tag ทั้งหมด
git tag
git tag -l "v1.*"          # filter pattern
git tag -n                 # พร้อม annotation

# ดูรายละเอียด tag
git show v1.0

# ลบ tag ใน local
git tag -d v1.0

# ลบ tag บน remote
git push origin --delete v1.0

# Push tag ไป remote
git push origin v1.0          # tag เดียว
git push origin --tags        # ทุก tag (เฉพาะที่ไม่ลึก)
git push origin --follow-tags # ทุก annotated tag ที่เกี่ยวข้องกับ commit ที่ push

# Checkout ที่ tag (เข้า detached HEAD)
git checkout v1.0
git switch --detach v1.0

8.9.3 Semantic Versioning (SemVer)

มาตรฐาน MAJOR.MINOR.PATCH ตามเอกสาร https://semver.org

v= MAJOR. MINOR. PATCH [-PRERELEASE] [+BUILD]

โดยที่ MAJOR เพิ่มเมื่อมี breaking change (API ไม่เข้ากันย้อนหลัง), MINOR เพิ่มเมื่อมี feature ใหม่ที่ backward compatible, PATCH เพิ่มเมื่อแก้บั๊กเล็กน้อย, PRERELEASE เช่น -alpha.1, -rc.2 และ BUILD เช่น +20240125

ตัวอย่าง ความหมาย
v1.0.0 Stable release แรก
v1.0.1 Patch แก้บั๊ก
v1.1.0 Minor – feature ใหม่
v2.0.0 Major – breaking change
v2.0.0-rc.1 Release candidate
v2.0.0-beta.3+a1b2c3d Beta พร้อม build metadata

8.9.4 Release บน GitHub / GitLab

Release บน Git host = Tag + เอกสาร changelog + binary artifact

# วิธี CLI ผ่าน GitHub CLI (gh)
gh release create v1.0.0 \
  --title "v1.0.0 - Initial Release" \
  --notes "Initial public release with login feature" \
  ./dist/myapp-linux-amd64.tar.gz \
  ./dist/myapp-windows-amd64.zip

# Generate release notes อัตโนมัติจาก commit/PR
gh release create v1.1.0 --generate-notes

# วิธี CLI ผ่าน GitLab (glab)
glab release create v1.0.0 \
  --name "Version 1.0.0" \
  --notes "Initial release" \
  --assets-link '{"name":"binary","url":"https://..."}'

Conventional Changelog แนะนำให้ใช้ tool เช่น git-cliff, standard-version, release-please สร้าง CHANGELOG.md อัตโนมัติจาก commit

# git-cliff (เขียนด้วย Rust)
cargo install git-cliff
git cliff --output CHANGELOG.md
git cliff --tag v1.1.0 --output CHANGELOG.md

8.10 Git Hosting และ Tools

8.10.1 GitHub, GitLab, Bitbucket

Hosting จุดเด่น ฟรี Private Repo CI/CD ในตัว
GitHub community ใหญ่ที่สุด, GitHub Actions, Copilot ✓ ไม่จำกัด ✓ Actions
GitLab DevOps ครบวงจรที่สุด, Self-host ได้ ✓ ไม่จำกัด ✓ GitLab CI
Bitbucket บูรณาการกับ Jira/Confluence ✓ (≤5 user) ✓ Pipelines

8.10.2 Self-hosted: Gitea, Forgejo, Gogs, GitLab CE

สำหรับองค์กรที่ต้องการ host เอง

# ตัวอย่าง docker-compose.yml สำหรับ Gitea
services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=sqlite3
    restart: always
    volumes:
      - ./gitea:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "2222:22"

8.10.3 Decentralized: Radicle

Radicle เป็น P2P git hosting ที่ไม่ต้องพึ่ง server กลาง ใช้ public-key cryptography ในการระบุตัวตนและ replicate ระหว่าง peer

8.10.4 GUI Client: GitKraken, Sourcetree, GitHub Desktop, Fork, Lazygit, Tig

# ติดตั้ง Lazygit
sudo pacman -S lazygit            # Arch
brew install lazygit              # macOS
sudo dnf copr enable atim/lazygit && sudo dnf install lazygit  # Fedora

# ใช้งาน
cd repo && lazygit

8.10.5 Git Hook (pre-commit, commit-msg, pre-push)

Hook = script ที่ Git รันอัตโนมัติเมื่อเกิด event ใน repo (เก็บใน .git/hooks/)

# Hook ที่ใช้บ่อย
ls .git/hooks/
# applypatch-msg.sample, commit-msg.sample, post-update.sample,
# pre-applypatch.sample, pre-commit.sample, pre-push.sample,
# pre-rebase.sample, prepare-commit-msg.sample, update.sample

ตัวอย่าง pre-commit hook ที่บล็อกการ commit ถ้า lint ไม่ผ่าน:

#!/usr/bin/env bash
# .git/hooks/pre-commit
# ตรวจ Python lint ด้วย ruff ก่อน commit

set -e

# หาไฟล์ Python ที่จะ commit
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.py$' || true)

if [ -z "$files" ]; then
    exit 0
fi

echo "Running ruff on staged Python files..."
echo "$files" | xargs ruff check

if [ $? -ne 0 ]; then
    echo "❌ Lint failed. Commit aborted."
    echo "💡 Fix errors or run: git commit --no-verify (ไม่แนะนำ)"
    exit 1
fi

echo "✅ Lint passed"

# จากนั้น
chmod +x .git/hooks/pre-commit

8.10.6 pre-commit framework

pre-commit (https://pre-commit.com) คือ framework ของ Python ที่บริหาร hook ผ่าน YAML config – แชร์ config กับทีมง่ายกว่า hook ใน .git/hooks/

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ['--maxkb=500']

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.10.0
    hooks:
      - id: shellcheck
# ติดตั้งและใช้งาน
pip install pre-commit
pre-commit install               # ติดตั้ง hook
pre-commit run --all-files       # รันทันทีทุกไฟล์
pre-commit autoupdate            # อัพเดต rev ของ repo

8.11 Advanced Git

8.11.1 Submodule และ Subtree

ทั้งสองใช้ฝัง repo อีกตัวภายใน repo หลัก แต่กลไกต่างกัน

8.11.1.1 Submodule

# เพิ่ม submodule
git submodule add https://github.com/lib/awesome.git external/awesome
git commit -m "chore: add awesome lib as submodule"

# Clone repo ที่มี submodule
git clone --recurse-submodules https://github.com/me/myapp.git

# หรือถ้า clone แล้วเพิ่งรู้
git submodule update --init --recursive

# อัปเดต submodule ทั้งหมด
git submodule update --remote --merge

# ลบ submodule
git submodule deinit -f external/awesome
git rm -f external/awesome
rm -rf .git/modules/external/awesome

ข้อเสีย: complex, ต้องจำคำสั่ง --recurse-submodules ตลอด

8.11.1.2 Subtree

# เพิ่ม subtree (เก็บ history ภายในเดียวกัน)
git subtree add --prefix=external/awesome \
    https://github.com/lib/awesome.git main --squash

# Pull update
git subtree pull --prefix=external/awesome \
    https://github.com/lib/awesome.git main --squash

# Push back
git subtree push --prefix=external/awesome \
    https://github.com/lib/awesome.git main

ข้อดี: ผู้ใช้ที่ clone repo ไม่ต้องรู้ว่ามี subtree, history รวมในไฟล์เดียวกัน

คุณสมบัติ Submodule Subtree
ขนาด repo เล็ก (pointer) ใหญ่ (รวมไฟล์)
Clone ต้อง --recurse ไม่ต้องคิด
Update ใช้ submodule update ใช้ subtree pull
Push กลับ upstream ตรงไปตรงมา ต้องระบุ prefix
Learning curve สูง ปานกลาง

8.11.2 Git LFS (Large File Storage)

สำหรับไฟล์ binary ขนาดใหญ่ (รูป, video, model AI) ที่ Git ปกติจัดการไม่เก่ง

# ติดตั้ง git-lfs
sudo apt install git-lfs        # Debian/Ubuntu
brew install git-lfs            # macOS
sudo pacman -S git-lfs          # Arch

# Initialize ใน repo
git lfs install

# Track ไฟล์ที่ใหญ่
git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "models/*.pt"

# .gitattributes ถูกอัปเดตอัตโนมัติ
git add .gitattributes
git add big-file.psd
git commit -m "feat: add design source"

# ดูไฟล์ LFS
git lfs ls-files
git lfs status

8.11.3 git worktree

ทำงานหลาย branch พร้อมกันโดยไม่ต้อง stash/switch ไปมา – แต่ละ branch มี directory แยกแต่ใช้ .git/ ร่วมกัน

# สร้าง worktree ของ branch hotfix ที่โฟลเดอร์ ../hotfix
git worktree add ../hotfix hotfix/v1.2

# สร้างพร้อม branch ใหม่
git worktree add -b feature/x ../feature-x main

# ดู worktree ทั้งหมด
git worktree list

# ลบ worktree
git worktree remove ../hotfix

# ทำความสะอาด worktree ที่ไม่อยู่แล้ว
git worktree prune

Use case: กำลังทำ feature ใหญ่บน branch หนึ่ง ขณะ production มีบั๊กด่วน → สร้าง worktree ของ hotfix branch ในอีกโฟลเดอร์ เปิด terminal/editor 2 หน้าต่าง

8.11.4 git bisect

หา commit ที่ทำให้บั๊กเกิดด้วย binary search (เร็วระดับ log₂)

O(log2(n))

โดยที่ n = จำนวน commit ระหว่าง good และ bad ตัวอย่าง: 1024 commit ใช้แค่ 10 ครั้งทดสอบ

# เริ่ม bisect
git bisect start

# บอกว่าตอนนี้ (HEAD) บั๊ก
git bisect bad

# บอก commit ที่รู้ว่ายังดี
git bisect good v1.0

# Git จะ checkout ที่กลางทาง ให้เราทดสอบ
# ทดสอบแล้วถ้าดี
git bisect good
# ถ้ายังบั๊ก
git bisect bad
# ถ้า build ไม่ผ่าน ข้าม commit นี้
git bisect skip

# จบแล้ว Git จะบอก commit ที่ต้นเหตุ
# 7c3a9b8 is the first bad commit

# ออกจาก bisect
git bisect reset

# Automate ด้วย script (return 0=good, 1=bad, 125=skip)
git bisect start HEAD v1.0
git bisect run ./test.sh

8.11.5 Signed Commit (GPG / SSH signing)

ลงนาม commit ด้วย key เพื่อยืนยันตัวตน ป้องกันการปลอมแปลง

# === GPG Signing ===
# สร้าง GPG key
gpg --full-generate-key
gpg --list-secret-keys --keyid-format=long

# ตั้งค่า Git
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
git config --global tag.gpgsign true

# Sign commit
git commit -S -m "signed commit"
git tag -s v1.0 -m "signed tag"

# === SSH Signing (Git 2.34+) - ง่ายกว่า ===
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

# ตรวจสอบ signature
git log --show-signature
git verify-commit HEAD

อัพโหลด public key ไป GitHub/GitLab เพื่อให้ขึ้น "Verified" badge

8.11.6 git blame, git annotate

หาว่าใครแก้บรรทัดไหน เมื่อไร

# blame ทั้งไฟล์
git blame src/app.py

# blame เฉพาะบรรทัด 10-20
git blame -L 10,20 src/app.py

# blame หา function
git blame -L :function_name src/app.py

# ละเว้น whitespace change
git blame -w src/app.py

# ติดตามผ่าน rename
git blame -C -C src/app.py

# เห็น original commit แม้ผ่าน reformat
git config --global blame.ignoreRevsFile .git-blame-ignore-revs
echo "abc1234567890" > .git-blame-ignore-revs   # commit reformat

8.11.7 git clean, git gc

# === git clean - ลบไฟล์ untracked ===
# Dry run (แค่แสดง ไม่ลบ)
git clean -n

# ลบไฟล์ untracked
git clean -f

# ลบทั้งไฟล์ + folder untracked
git clean -fd

# ลบรวมที่อยู่ใน .gitignore
git clean -fdx

# Interactive
git clean -i

# === git gc - garbage collection ===
# รวม loose objects เป็น pack file ลดพื้นที่
git gc

# Aggressive - ใช้เวลานาน แต่บีบอัดดีกว่า
git gc --aggressive --prune=now

# ดูขนาด repo
git count-objects -vH

# Verify integrity
git fsck --full

8.11.7.1 ตัวอย่างการใช้ Git ครบวงจรในงานจริง

#!/usr/bin/env bash
# ตัวอย่าง workflow จริง: feature → PR → merge → release

# 1. เริ่ม feature ใหม่
git switch main
git pull --rebase origin main
git switch -c feature/user-profile

# 2. ทำงาน + commit เล็ก ๆ บ่อย ๆ
echo "user code" > user.py
git add user.py
git commit -m "feat(user): add user model"

echo "more code" >> user.py
git add user.py
git commit -m "feat(user): add validation"

# 3. ก่อน push ให้จัดระเบียบ commit (squash)
git rebase -i HEAD~2
# เลือก fixup รวม commit ที่สอง

# 4. Sync กับ main แล้ว push
git fetch origin
git rebase origin/main
git push -u origin feature/user-profile

# 5. เปิด PR (ผ่าน gh CLI)
gh pr create \
  --title "feat: add user profile" \
  --body "Closes #42" \
  --reviewer @teammate

# 6. หลัง review + CI ผ่าน → merge แบบ squash
gh pr merge --squash --delete-branch

# 7. กลับมา main + tag release
git switch main
git pull --rebase origin main
git tag -a v1.2.0 -m "Release: User Profile feature"
git push origin v1.2.0

# 8. สร้าง release บน GitHub
gh release create v1.2.0 --generate-notes

8.12 สรุปท้ายบท (Chapter Summary)

บทนี้ได้ครอบคลุม Git อย่างเป็นระบบ ตั้งแต่ปรัชญาของ Version Control System ไปจนถึงการใช้งานขั้นสูงในระดับองค์กร โดยสามารถสรุปสาระสำคัญได้ดังนี้

8.12.1 ประเด็นหลักที่ควรจดจำ

  1. Git เป็น Distributed VCS ที่ทุก clone คือ full backup ทำให้ปลอดภัยและทำงาน offline ได้ — ต่างจาก Centralized VCS อย่าง SVN ที่พึ่ง server เดียว
  2. Three Trees Model (Working Directory → Staging Area → Repository) เป็นหัวใจของการเข้าใจ Git ที่ทำให้ควบคุมการ commit ได้แม่นยำผ่าน git add -p
  3. Branch ใน Git คือ pointer ไม่ใช่สำเนา ทำให้แตก/รวม branch รวดเร็วและประหยัดพื้นที่ ส่งเสริมวัฒนธรรม Feature Branch Workflow
  4. Merge vs Rebase มีบทบาทต่างกัน — merge รักษา history จริง, rebase ทำให้ history เป็นเส้นตรงสะอาด แต่ห้าม rebase commit ที่ shared แล้ว
  5. Workflow ขึ้นกับบริบทของทีม — ทีมเล็กใช้ GitHub Flow, ทีมที่มี release schedule ใช้ Git Flow, ทีม DevOps ระดับสูงใช้ Trunk-based Development
  6. Pull Request เป็นมากกว่าเครื่องมือ merge — เป็นพื้นที่ของ Code Review, CI Integration และ knowledge sharing ของทีม
  7. History Management (reset, revert, reflog, interactive rebase, stash) คือเครื่องมือกู้สถานการณ์ที่ผู้พัฒนาควรฝึกใช้ก่อนเกิดเหตุ
  8. เครื่องมือเสริม เช่น pre-commit hook, signed commit, Git LFS และ git worktree ช่วยยกระดับคุณภาพและประสิทธิภาพการทำงาน

8.12.2 Mental Model ของ Git

mindmap
  root((Git))
    Local Operations
      Working Directory
      Staging Area
      Repository
      Stash
    History
      Commit Graph DAG
      Branches as Pointers
      Tags & Releases
      Reflog Safety Net
    Collaboration
      Remote/Origin/Upstream
      Pull Request
      Code Review
      Fork Workflow
    Workflow
      GitHub Flow
      Git Flow
      Trunk-based
    Advanced
      Submodule/Subtree
      Worktree
      Bisect
      Signed Commit
      LFS

8.12.3 Best Practices ที่ควรนำไปใช้

8.12.4 ตารางสรุปคำสั่ง Git ที่ใช้บ่อย

หมวดหมู่ (Category) คำสั่งที่ใช้บ่อย
Setup git init, git clone, git config
Snapshot git status, git add, git commit, git restore
History git log, git diff, git blame, git reflog
Branch git branch, git switch, git checkout
Merge git merge, git rebase, git cherry-pick
Remote git remote, git fetch, git pull, git push
Undo git reset, git revert, git stash
Tag git tag, git describe
Advanced git worktree, git bisect, git submodule, git lfs

8.12.5 เส้นทางการเรียนรู้ต่อ

ผู้เรียนที่เข้าใจเนื้อหาในบทนี้แล้ว ควรเรียนรู้เพิ่มเติมในด้านต่อไปนี้

  1. CI/CD Pipeline — GitHub Actions, GitLab CI/CD, Jenkins (เชื่อมโยงกับบทที่ 4 หัวข้อ 4.8)
  2. Container & Deployment — Docker, Kubernetes (บทที่ 11)
  3. Code Review Culture — เทคนิคการรีวิวที่สร้างสรรค์, conventional comments
  4. Monorepo Tools — Nx, Turborepo, Bazel สำหรับโครงการขนาดใหญ่
  5. Git Internals — โครงสร้าง object (blob, tree, commit, tag), pack file, plumbing commands เช่น git cat-file, git hash-object, git update-ref

คำสุดท้าย: Git เป็นเครื่องมือที่เรียนรู้ได้ตลอดชีวิต — เริ่มจาก 10 คำสั่งพื้นฐาน (init, clone, status, add, commit, push, pull, branch, switch, log) แล้วค่อย ๆ ขยับไปสู่ workflow ที่ซับซ้อนขึ้นเมื่อทีมเติบโต ผู้พัฒนาที่ใช้ Git อย่างเชี่ยวชาญจะมี ความมั่นใจในการทดลอง เพราะรู้ว่าทุกการเปลี่ยนแปลงสามารถย้อนกลับได้เสมอ