สลับ 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 ชุดเดียวกัน แต่ไฟล์แยกกันคนละโฟลเดอร์

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 จึงยังรันไม่ได้จนกว่าจะตั้งสามอย่างนี้
vendor ต้องเป็นของจริง ห้าม symlink
ทางที่ถูกคือ 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 ได้ครับ






