git worktree คืออะไร และวิธีเปิดหลาย branch พร้อมกันในโปรเจกต์ Laravel

git worktree เพิ่มโฟลเดอร์ทำงานให้ repository เดิม แต่ละโฟลเดอร์อยู่คนละ branch โดยใช้ประวัติชุดเดียวกัน ไม่ต้อง stash หรือ clone ใหม่ แต่ในโปรเจกต์ Laravel ต้องตั้ง vendor, .env และฐานข้อมูลให้แต่ละโฟลเดอร์เอง

เผยแพร่
เวลาอ่าน
7 นาที
แท็ก
#Git#Laravel#Tools

สลับ branch ทีไรต้อง stash ทุกครั้ง

ระหว่างทำ feat/export-pdf ไปครึ่งทาง ลูกค้าแจ้งบั๊กด่วน หรือเพื่อนขอให้ช่วยรีวิว PR ทางที่ใช้กันมาตลอดคือ git stash หรือ commit งานค้างไว้ก่อน สลับ branch ไปแก้ แล้วค่อยสลับกลับมา stash pop

ในโปรเจกต์ Laravel สลับ branch แต่ละครั้งยังมีเรื่องตามมาอีก ถ้า composer.lock ของสอง branch ไม่เหมือนกันต้อง composer install ใหม่ ถ้ามี migration ต่างกันฐานข้อมูลก็ไม่ตรงกับโค้ด หน้าจอที่ build ไว้ด้วย Vite ก็เป็นของอีก branch พอสลับกลับมาก็ต้องทำซ้ำอีกรอบ

git worktree แก้เรื่องนี้ด้วยการเปิด branch ที่สองไว้ในโฟลเดอร์ใหม่ไปเลย งานเดิมอยู่ที่เดิมไม่ต้องแตะ ในคำสั่ง git ที่ใช้ทุกวัน มีคำสั่งพื้นฐานไว้แล้ว โพสต์นี้ลงรายละเอียดว่าอะไรใช้ร่วมกัน อะไรแยกกัน และต้องตั้งอะไรบ้างให้โปรเจกต์ Laravel รันได้ในโฟลเดอร์ใหม่ ตัวอย่างทั้งหมดใช้ git 2.54

worktree ใช้ .git ชุดเดียวกัน แต่ไฟล์แยกกันคนละโฟลเดอร์

ทุก worktree ใช้ .git ชุดเดียวกัน แต่ละโฟลเดอร์มีไฟล์และ HEAD ของตัวเอง

commit ที่ทำใน app-login โผล่ใน app ทันที เพราะเป็น .git ชุดเดียวกัน

ปกติ repository หนึ่งมีโฟลเดอร์ทำงาน (working tree) โฟลเดอร์เดียว worktree เพิ่มโฟลเดอร์ทำงานได้อีก โดยทุกโฟลเดอร์อ่านเขียน .git ที่โฟลเดอร์แรก ไม่ได้ clone ใหม่ ไม่ต้อง fetch ข้ามกัน

  • ใช้ร่วมกัน commit ทั้งหมด, branch, tag, stash, remote และ config ของ repository
  • แยกต่อโฟลเดอร์ ไฟล์ในโฟลเดอร์, HEAD (อยู่ branch ไหน), staging area และไฟล์ที่ git ไม่ได้เก็บ อย่าง vendor, node_modules และ .env

ในโฟลเดอร์ worktree ใหม่ .git ไม่ใช่โฟลเดอร์ แต่เป็นไฟล์บรรทัดเดียวที่ชี้กลับไปหา repository หลัก

cat ../app-login/.git
gitdir: /Users/me/www/app/.git/worktrees/app-login

สร้าง worktree ด้วย git worktree add

# สร้าง branch ใหม่จาก develop ในโฟลเดอร์ใหม่
git worktree add -b fix/login-redirect ../app-login develop
Preparing worktree (new branch 'fix/login-redirect')
HEAD is now at 45e379a init

ถ้า branch มีอยู่แล้ว ไม่ต้องใส่ -b บอกแค่โฟลเดอร์กับชื่อ branch

git worktree add ../app-export-pdf feat/export-pdf

ถ้าอยากเริ่มจากงานล่าสุดบน remote ให้ git fetch ก่อน แล้วใช้ origin/develop แทน develop ใส่ --no-track ด้วย ไม่อย่างนั้น branch ใหม่จะตั้ง upstream เป็น origin/develop แล้ว git push ครั้งแรกจะไม่ยอม เพราะชื่อ branch ไม่ตรงกับ upstream

git fetch
git worktree add --no-track -b fix/login-redirect ../app-login origin/develop

ดูว่ามี worktree ไหนบ้าง อยู่ branch อะไร

git worktree list
/Users/me/www/app            45e379a [develop]
/Users/me/www/app-export-pdf 45e379a [feat/export-pdf]
/Users/me/www/app-login      45e379a [fix/login-redirect]

git branch ก็บอกเหมือนกัน branch ที่เปิดอยู่ใน worktree อื่นมี + นำหน้า

* develop
+ feat/export-pdf
+ fix/login-redirect

จะวาง worktree ไว้ตรงไหนก็ได้ ผมชอบวางข้าง ๆ โฟลเดอร์หลักแล้วตั้งชื่อตาม branch อย่าง app-login จะได้เห็นใน Finder หรือ IDE ว่าเป็นโปรเจกต์เดียวกัน ถ้าอยากเก็บไว้ในโฟลเดอร์โปรเจกต์ก็ได้ เช่น .worktrees/login แต่ต้องใส่ .worktrees/ ใน .gitignore ด้วย ไม่อย่างนั้น git status ของโฟลเดอร์หลักจะเห็นทั้งโฟลเดอร์เป็นไฟล์ใหม่

ตั้งโปรเจกต์ Laravel ในโฟลเดอร์ใหม่ให้รันได้

worktree ใหม่มีเฉพาะไฟล์ที่อยู่ใน git ไฟล์ที่ .gitignore ไว้ไม่ตามมาเลย โปรเจกต์ Laravel จึงยังรันไม่ได้จนกว่าจะตั้งสามอย่างนี้

ทางที่ถูกคือ composer install ในโฟลเดอร์ใหม่ หรือถ้า composer.lock ของสอง branch เหมือนกันก็คัดลอก vendor มาทั้งโฟลเดอร์ได้ ซึ่งเร็วกว่า

cd ../app-login
composer install
# หรือ ถ้า composer.lock เหมือนกัน
cp -R ../app/vendor vendor

บน Mac ใส่ -c เป็น cp -cR ได้ macOS จะใช้ copy-on-write ของ APFS คัดลอกเสร็จแทบทันทีและแทบไม่กินพื้นที่ดิสก์เพิ่ม จนกว่าจะมีไฟล์ไหนถูกแก้

ที่ห้ามคือ ln -s ../app/vendor vendor แม้จะดูเหมือนใช้ได้ php artisan รันได้ test ก็ผ่าน แต่ไฟล์ autoload ของ Composer หาโฟลเดอร์โปรเจกต์จาก __DIR__ ซึ่ง PHP แปลงผ่าน symlink เป็น path จริงเสมอ class ใน app/ จึงโหลดจากโฟลเดอร์หลัก ไม่ใช่โฟลเดอร์ที่เรากำลังแก้

ผมลองกับโปรเจกต์เล็ก ๆ ที่มี class App\Greeting แก้ค่าใน worktree เป็น feat-x แล้วเรียกดู

# vendor เป็น symlink ไปที่ app
develop from /Users/me/www/app/app/Greeting.php

# vendor คัดลอกมาจริง
feat-x from /Users/me/www/app-x/app/Greeting.php

แบบ symlink ไม่มี error อะไรเตือนเลย เรากำลังรันโค้ดของอีก branch โดยไม่รู้ตัว อาการที่เห็นคือแก้ไฟล์แล้วผลไม่เปลี่ยน หรือ class ที่เพิ่งสร้างหาไม่เจอ

.env และฐานข้อมูล

.env คัดลอกจากโฟลเดอร์หลักได้เลย แต่ต้องคิดเรื่องฐานข้อมูล worktree ทุกโฟลเดอร์ต่อ MySQL หรือ PostgreSQL ตัวเดียวกัน ถ้า branch หนึ่งมี migration เพิ่ม column หรือลบตาราง อีก branch ที่ใช้ฐานข้อมูลเดียวกันก็โดนไปด้วย

ถ้า branch นั้นแตะ migration ให้แยกฐานข้อมูลไปเลย จะสร้างใหม่แล้ว seed หรือคัดลอกข้อมูลจากฐานข้อมูลเดิมมาก็ได้

cp ../app/.env .env
# แก้ DB_DATABASE=app_login ใน .env

# MySQL: สร้างใหม่แล้ว seed
mysql -u root -e "CREATE DATABASE app_login"
php artisan migrate --seed

# MySQL: หรือคัดลอกข้อมูลจาก app มาทั้งหมด
mysqldump -u root --single-transaction app | mysql -u root app_login

# PostgreSQL: คัดลอกจาก app เป็น template
createdb -T app app_login

createdb -T ของ PostgreSQL ใช้ได้เฉพาะตอนที่ไม่มีใครต่อฐานข้อมูล app อยู่ ถ้ามี queue worker หรือ php artisan serve เปิดค้างไว้ให้ปิดก่อน หรือใช้ pg_dump app | psql app_login แทน

ถ้า branch ไม่ได้แก้ migration ใช้ฐานข้อมูลเดียวกันได้ แค่อย่าสั่ง migrate:fresh ในโฟลเดอร์ไหนก็ตามระหว่างที่อีกโฟลเดอร์ยังทำงานอยู่ Redis, queue และ cache ก็ใช้ร่วมกันแบบเดียวกัน ถ้าสองโฟลเดอร์รัน queue:work พร้อมกันบน queue เดียว job อาจไปตกที่ worker ของอีก branch

node_modules และโดเมนสำหรับเปิดในเบราว์เซอร์

ถ้าจะเปิดหน้าเว็บด้วย ต้อง npm install แล้ว npm run build หรือ npm run dev ในโฟลเดอร์ใหม่ด้วย จากนั้นให้แต่ละโฟลเดอร์มีโดเมนของตัวเอง

  • Laravel Herd สั่ง herd link ในโฟลเดอร์ worktree จะได้โดเมน app-login.test ตามชื่อโฟลเดอร์
  • Laragon ถ้าวาง worktree ไว้ในโฟลเดอร์ www ของ Laragon แล้วกด Reload จะได้โดเมน .test ตามชื่อโฟลเดอร์เหมือนกัน
  • XAMPP หรือ AppServ ต้องเพิ่ม virtual host ให้โฟลเดอร์ใหม่เอง หรือใช้ php artisan serve --port=8001 แยกพอร์ตไปเลยก็ได้

อย่าลืมแก้ APP_URL ใน .env ให้ตรงกับโดเมนใหม่ด้วย ไม่อย่างนั้นลิงก์ที่สร้างจาก queue หรือ artisan และ URL ของไฟล์ใน storage จะชี้กลับไปที่โดเมนของโฟลเดอร์หลัก

ก่อนรัน test และก่อน commit

test ที่ render หน้าเว็บผ่าน @vite จะพังใน worktree ใหม่ ถ้ายังไม่มี public/build

Vite manifest not found at: /Users/me/www/app-login/public/build/manifest.json

public/build อยู่ใน .gitignore จึงไม่ตามมา ถ้าไม่ได้แก้ CSS หรือ JavaScript ใน branch นี้ ก็คัดลอกจากโฟลเดอร์หลักมาได้เลย แต่ถ้าเพิ่ม class ของ Tailwind ใหม่ ต้อง npm run build ในโฟลเดอร์นี้เอง ไม่อย่างนั้น class ใหม่จะไม่มีใน CSS ที่คัดลอกมา

cp -R ../app/public/build public/build

composer install ในโฟลเดอร์ใหม่อาจ publish asset ของแพ็กเกจลงใน public/ ซ้ำ อย่างโปรเจกต์ที่ใช้ Filament ซึ่งตั้ง php artisan filament:upgrade ไว้ใน script post-autoload-dump ของ composer.json ก่อน commit ให้ดู git status ก่อน ถ้ามีไฟล์ใน public/ ที่เราไม่ได้แก้ ให้คืนค่าเดิมไป

git status --short
git checkout -- public/

branch เดียวเปิดได้ทีละ worktree

ถ้าสั่ง git switch fix/login-redirect ในโฟลเดอร์หลัก ขณะที่ branch นี้เปิดอยู่ใน app-login git จะไม่ยอม

fatal: 'fix/login-redirect' is already used by worktree at '/Users/me/www/app-login'

ถ้าสองโฟลเดอร์อยู่ branch เดียวกัน พอ commit ในโฟลเดอร์หนึ่ง branch ก็เลื่อนไป แต่อีกโฟลเดอร์ยังมีไฟล์ชุดเก่า ไฟล์จะดูเหมือนถูกแก้ย้อนกลับทั้งหมด git เลยกันไว้ ทางแก้คือไปทำงานในโฟลเดอร์ที่เปิด branch นั้นอยู่ หรือถ้าแค่อยากเปิดดูโค้ด ใช้ --detach ซึ่งเปิดที่ commit นั้นโดยไม่ผูกกับ branch

git worktree add --detach ../app-peek fix/login-redirect

stash เห็นร่วมกันทุก worktree

stash เก็บอยู่ใน .git ชุดเดียวกัน stash ที่สร้างใน app-login จึงโผล่ใน git stash list ของโฟลเดอร์หลักด้วย

stash@{0}: On fix/login-redirect: งานค้างใน app-login

ถ้าเผลอ git stash pop ในอีกโฟลเดอร์ งานจาก branch หนึ่งจะไปลงอีก branch ใช้ worktree แล้วแทบไม่ต้อง stash อีก แต่ถ้ายังใช้ ให้ตั้งชื่อด้วย git stash push -m "…" และ git stash apply ตัวที่ต้องการแทน pop เฉย ๆ

เลิกใช้แล้วลบให้ครบทั้ง worktree และ branch

git worktree remove ../app-login

ถ้าในโฟลเดอร์ยังมีไฟล์ที่แก้ค้างหรือไฟล์ใหม่ git จะไม่ยอมลบ

fatal: '../app-export-pdf' contains modified or untracked files, use --force to delete it

--force ลบได้ แต่ไฟล์ที่ยังไม่ commit หายไปด้วย เช็ก git -C ../app-export-pdf status ก่อนทุกครั้ง ส่วน vendor, node_modules และ .env ไม่ทำให้ git ปฏิเสธ เพราะอยู่ใน .gitignore แล้ว ลบไปพร้อมโฟลเดอร์ได้เลย

remove ลบแค่โฟลเดอร์ ส่วน branch ยังอยู่ ถ้า merge แล้วก็ลบตาม

git branch -d fix/login-redirect

ถ้าเผลอลบโฟลเดอร์ทิ้งเองใน Finder หรือ rm -rf รายชื่อจะยังค้างอยู่ใน git worktree list พร้อมคำว่า prunable สั่ง prune ล้างได้

git worktree prune -v
Removing worktrees/app-export-pdf: gitdir file points to non-existent location

มีเครื่องมือที่ทำขั้นตอนพวกนี้ให้ในคำสั่งเดียว

ถ้าสร้าง worktree บ่อย ขั้นตอนข้างบนทำมือทุกครั้งก็เหนื่อย ตอนนี้มีเครื่องมือที่รวบให้แล้วหลายตัว

  • mozex/laravel-worktree เป็น artisan command ติดตั้งด้วย composer require --dev ในแต่ละโปรเจกต์ php artisan worktree:setup สร้าง worktree พร้อมฐานข้อมูลแยกและแก้ .env ให้ ส่วน worktree:teardown เปิด PR หรือ merge แล้วลบทั้ง worktree และฐานข้อมูล ต้องใช้คู่กับ Laravel Herd
  • wilburpowery/wt เป็น bash script คำสั่งเดียวได้ branch, ฐานข้อมูลที่คัดลอกจากของเดิม, site บน Herd และคัดลอก vendor กับ node_modules ด้วย cp -c ใช้ได้เฉพาะบน Mac ที่มี Herd

ทั้งสองตัวผูกกับ Herd ถ้าไม่ได้ใช้ Herd ผมทำ worktree-prompt ไว้เป็นอีกทาง ติดตั้งครั้งเดียวด้วย composer global require ใช้ได้ทุกโปรเจกต์ สั่ง lwt add fix/login-redirect แล้วได้ worktree ที่มี vendor ของจริง .env โฟลเดอร์ storage และไฟล์ build ครบ ใส่ --db=copy ถ้าอยากได้ฐานข้อมูลแยก และคืนค่าไฟล์ใน git ที่ขั้นตอน setup ไปแก้ให้ด้วย

ผมใช้ worktree ให้ AI agent หลายตัวทำงานพร้อมกัน

ช่วงหลังผมให้ Claude Code หลาย session ทำงานใน repository เดียวกันพร้อมกัน ตัวหนึ่งเขียนโพสต์ อีกตัวแก้โค้ดของเว็บ แต่ละ session ที่แก้โค้ดได้ worktree ของตัวเอง ไฟล์ไม่ทับกัน และไม่ต้องรอกันสลับ branch

Claude Code สร้าง worktree ให้เองด้วย claude --worktree <ชื่อ> โดยวางไว้ที่ .claude/worktrees/ และถ้าใส่ชื่อไฟล์ที่อยู่ใน .gitignore ไว้ในไฟล์ .worktreeinclude ที่ root ของโปรเจกต์ เช่น .env ก็จะคัดลอกไฟล์พวกนั้นเข้า worktree ใหม่ให้ทุกครั้ง (เอกสาร Claude Code) แต่ vendor, ฐานข้อมูล และ build ยังต้องตั้งเองเหมือนเดิม

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

  • โฟลเดอร์หลักอยู่บน main เสมอ ถ้า session ไหนสลับ branch ในโฟลเดอร์หลัก session อื่นที่ทำงานอยู่ที่นั่นจะ commit ลงไปผิด branch ทันที งานที่เปิด branch ใหม่ต้องไปทำใน worktree
  • ฐานข้อมูลสำหรับ test ใช้ร่วมกัน จึงห้ามคำสั่งที่ล้างหรือสร้างฐานข้อมูลใหม่ อย่าง migrate:fresh หรือรัน test แบบ parallel ที่สร้างฐานข้อมูลแยกตามลำดับ worker
  • stash ใช้ร่วมกัน session หนึ่ง stash pop อาจหยิบงานของอีก session ไป จึงใช้ commit ชั่วคราวแทน stash

ถ้าอยากสลับ branch และดูว่าแต่ละ worktree แก้ไฟล์อะไรค้างอยู่จากหน้าต่างเดียวใน terminal ลองดู git-prompt ได้ครับ

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →