worktree-prompt ตั้งโปรเจกต์ Laravel ใน git worktree ให้รันได้ด้วยคำสั่งเดียว

พิมพ์ lwt add ตามด้วยชื่อ branch แล้วได้โฟลเดอร์ worktree ที่มี vendor ของจริง .env โฟลเดอร์ storage ไฟล์ build และฐานข้อมูลของตัวเองถ้าต้องการ เลิกใช้แล้ว lwt remove ก็ลบให้ครบทั้งโฟลเดอร์ ฐานข้อมูล และ branch

เผยแพร่
เวลาอ่าน
9 นาที

ทุก worktree ใหม่ของโปรเจกต์ Laravel ต้องตั้งชุดเดิมซ้ำทุกครั้ง

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

  • vendor ต้องคัดลอกมาจริงหรือ composer install ใหม่ ห้าม symlink
  • .env และไฟล์ .env.* อื่นที่ git ไม่ได้เก็บ
  • โฟลเดอร์ใน storage/ ที่ git ไม่ได้เก็บ ถ้าไม่มี storage/framework/views ทุกหน้าที่ render Blade จะ error
  • public/build สำหรับ Vite manifest
  • ฐานข้อมูลแยก ถ้า branch นั้นแก้ migration
  • APP_URL ให้ตรงกับโดเมนของโฟลเดอร์ใหม่
  • node_modules ถ้าจะ build หน้าเว็บ

ทำครั้งเดียวก็ไม่เท่าไร แต่ช่วงหลังผมเปิด Claude Code หลาย session ให้ทำงานพร้อมกัน แต่ละ session ได้ worktree ของตัวเอง ทุกตัวต้องตั้งชุดเดิมซ้ำ และพลาดได้ทุกรอบ อย่างที่เคยเจอคือ agent symlink vendor มาจากโฟลเดอร์หลักเพราะเร็วกว่า test ผ่านหมดจนดูเหมือนใช้ได้ ทั้งที่กำลังรันโค้ดของโฟลเดอร์หลักอยู่ ผมเลยรวมทุกขั้นไว้ในคำสั่งเดียวชื่อ lwt

lwt add สร้าง worktree แล้วตั้งทุกอย่างให้

บรรทัด → คือคำสั่งที่ lwt รันจริง ส่วนบรรทัด ✓ บอกว่าแต่ละขั้นได้อะไร

ทั้งชุดใช้เวลาราว 15 วินาทีกับโปรเจกต์ของเว็บนี้ ซึ่งรวมคัดลอกฐานข้อมูลและ build หน้าเว็บแล้ว

ติดตั้งครั้งเดียว ใช้ได้ทุกโปรเจกต์

composer global require phattarachai/worktree-prompt

แล้วเพิ่ม alias ไว้ใน ~/.zshrc หรือใส่ ~/.composer/vendor/bin ไว้ใน PATH

alias lwt="$HOME/.composer/vendor/bin/lwt"

ต้องมี PHP 8.3 ขึ้นไป และถ้าจะคัดลอกฐานข้อมูล ต้องมี pg_dump กับ psql หรือ mysqldump กับ mysql ในเครื่องด้วย หน้าจอถามตอบสร้างด้วย Laravel Prompts แบบเดียวกับ git-prompt

lwt ไม่ได้เป็นคำสั่ง artisan เพราะ worktree ที่เพิ่งสร้างยังไม่มี vendor จึงรัน php artisan ไม่ได้จนกว่าจะตั้งเสร็จ และแบบนี้ก็ไม่ต้อง composer require ลงทุกโปรเจกต์ด้วย

lwt add ถามแค่ที่ยังไม่ได้บอก

สั่ง lwt add เฉย ๆ จะถามทีละข้อ ตอบไปแล้ว Enter ได้เลยเพราะค่าที่เติมไว้ให้ส่วนใหญ่คือค่าที่เราจะเลือกอยู่แล้ว

lwt add ถามชื่อ branch แล้วไล่ถามที่เหลือ

หลังจาก Database จะถาม APP_URL และ node_modules ต่อ

  • Branch พิมพ์ชื่อ branch ที่มีอยู่แล้ว ทั้งในเครื่องหรือบน remote ก็ได้ หรือพิมพ์ชื่อใหม่ lwt สั่ง git fetch ให้ก่อนเสมอ ถ้าเป็น branch ที่มีแค่บน remote จะได้ branch ในเครื่องที่ track ตัวบน remote ให้
  • Branch from ถามเฉพาะ branch ใหม่ ค่าเริ่มต้นคือ branch หลักของ remote (origin/HEAD ซึ่งมักเป็น origin/main) และใส่ --no-track ให้ ไม่อย่างนั้น git push ครั้งแรกจะไม่ยอม เพราะ upstream เป็น origin/main คนละชื่อกับ branch ของเรา
  • Folder ค่าเริ่มต้นคือโฟลเดอร์ข้าง ๆ โฟลเดอร์หลัก ชื่อ <โปรเจกต์>-<ท้ายชื่อ branch> ถ้าเครื่องเราตั้งโฟลเดอร์ ~/www ให้ทุกโฟลเดอร์ย่อยเปิดเป็น <ชื่อโฟลเดอร์>.test ได้ (parked folder) worktree ใหม่ก็เปิดในเบราว์เซอร์ได้ทันที
  • Database ถ้า branch ที่เลือกมี migration ที่ branch หลักยังไม่มี lwt จะเลือกแบบคัดลอกไว้ให้แทน
  • APP_URL ค่าเริ่มต้นคือ http://<ชื่อโฟลเดอร์>.test พอเปลี่ยน SESSION_DOMAIN, SANCTUM_STATEFUL_DOMAINS และค่า *_URL ที่ใช้โดเมนเดิมก็เปลี่ยนตามให้ ปล่อยว่างถ้าจะใช้ค่าเดิม
  • node_modules ค่าเริ่มต้นคือข้าม เพราะงานฝั่ง backend และการรัน test ไม่ต้องใช้

ทุกข้อใส่เป็น option ได้ อย่าง lwt add fix/login-redirect --db=copy --npm ข้อที่ใส่มาแล้วจะไม่ถามซ้ำ

vendor เป็นของจริงเสมอ และตรวจว่า App\ โหลดจากโฟลเดอร์ใหม่

ในโพสต์ก่อนผมลองให้ดูแล้วว่า symlink vendor ทำให้ class ใน app/ โหลดจากโฟลเดอร์หลัก เพราะ autoload ของ Composer หาโฟลเดอร์โปรเจกต์จาก __DIR__ ซึ่ง PHP แปลงผ่าน symlink เป็น path จริงเสมอ lwt จึงทำ vendor ให้ worktree เองทุกครั้ง

  • ถ้า composer.lock เหมือนกับโฟลเดอร์หลัก lwt คัดลอก vendor มาด้วย cp -c ซึ่งบน macOS (APFS) เป็นการ clone แบบ copy-on-write ไฟล์ยังใช้พื้นที่ดิสก์ชุดเดิมจนกว่าจะมีฝั่งไหนแก้ vendor 226 MB ของเว็บนี้ใช้เวลาราว 2 วินาที ส่วน cp -R ปกติใช้ราว 6 วินาที บน Linux ใช้ cp --reflink=auto ซึ่ง clone ได้บน btrfs หรือ XFS และคัดลอกปกติบนดิสก์อื่น
  • ถ้า composer.lock ต่างกัน คือ branch นี้เปลี่ยน dependency lwt จะ composer install ใหม่ในโฟลเดอร์นั้น
  • ถ้าเจอ vendor ที่เป็น symlink อยู่แล้ว lwt จะลบ symlink นั้นออกแล้วทำใหม่ พร้อมบอกเหตุผล

ตั้งเสร็จแล้ว lwt จะถาม Composer ว่า namespace App\ โหลดจากโฟลเดอร์ไหน ถ้าไม่ใช่โฟลเดอร์ worktree จะแจ้งและจบด้วย exit code ที่ไม่ใช่ 0 แล้วค่อยสั่ง php artisan --version ดูว่าแอปบูตได้

ไฟล์อื่นที่ git ไม่ได้เก็บมาให้ lwt ก็ตั้งให้ครบ

  • .env คัดลอกจากโฟลเดอร์หลัก หรือใส่ --env=symlink ถ้าอยากให้ใช้ไฟล์เดียวกัน ส่วนไฟล์ .env.* อื่นที่ gitignore ไว้อย่าง .env.local ก็คัดลอกให้ด้วย แต่ไฟล์ที่ git เก็บอยู่อย่าง .env.testing จะไม่แตะเลย เคยมี agent ln -sfn ทับ .env.testing ที่ commit ไว้ ไฟล์กลายเป็น symlink แล้วโผล่ใน git status เกือบติดไปใน commit
  • storage/ และ bootstrap/cache สร้างโฟลเดอร์ที่ขาดให้ ถ้าไม่มี storage/framework/views ทุกหน้าที่ render Blade จะ error ว่า Please provide a valid cache path ซึ่งตอนรัน feature test จะเห็นเป็น HTTP 500 เต็มจอ จนดูเหมือน branch ทำแอปพัง
  • public/build และ bootstrap/ssr คัดลอกจากโฟลเดอร์หลัก หน้าที่อ่าน Vite manifest และ test ที่ render view จึงทำงานได้ ถ้า branch เพิ่ม class ของ Tailwind ใหม่ ต้อง build ใหม่ในโฟลเดอร์นั้น
  • public/storage ลิงก์ไปที่ storage/app/public ของ worktree เอง ไฟล์ที่ผู้ใช้อัปโหลดไว้ยังอยู่ที่โฟลเดอร์หลัก
  • node_modules เมื่อใส่ --npm ถ้า lockfile เหมือนกันจะคัดลอกแบบ clone ถ้าต่างกันจะ npm ci ซึ่งไม่เขียน package-lock.json ใหม่ ถ้ารัน npm install ใน worktree เอง npm จะเปลี่ยน "name" ใน lockfile เป็นชื่อโฟลเดอร์ ใช้ pnpm, yarn หรือ bun ก็ได้ lwt ดูจาก lockfile ที่มี ถ้าโปรเจกต์เก็บ .cache/puppeteer ไว้ในโฟลเดอร์ ก็คัดลอกให้ด้วย ใส่ --build แทนถ้าจะ npm run build ต่อเลย

ถ้าโปรเจกต์มีไฟล์อื่นที่ gitignore ไว้แต่ทุก worktree ต้องมี เช่น certificate สำหรับ dev ให้เขียนชื่อไว้ในไฟล์ .worktreeinclude ที่ root บรรทัดละหนึ่ง path ซึ่งเป็นไฟล์เดียวกับที่ Claude Code ใช้ตอนสร้าง worktree lwt คัดลอกเฉพาะ path ที่ gitignore ไว้ และไม่ทับไฟล์ที่มีอยู่แล้ว

.cache/certs
storage/app/fixtures

composer script บางตัวเขียนไฟล์ใน public/ ใหม่ระหว่าง composer install อย่างที่ Filament publish asset ของตัวเอง lwt จึงดู git status ก่อนและหลังตั้ง ไฟล์ที่ git เก็บอยู่แล้วโดนเปลี่ยนไประหว่างนั้นจะคืนกลับด้วย git restore ส่วนไฟล์ใหม่ที่ไม่ได้อยู่ใน .gitignore จะขึ้นรายชื่อให้ดูก่อน commit worktree ที่ได้จึง clean เหมือนตอนเพิ่งสร้าง

branch ที่มี migration ได้ฐานข้อมูลของตัวเอง

ทุก worktree ต่อฐานข้อมูลตัวเดียวกันกับโฟลเดอร์หลัก ถ้า branch หนึ่ง migrate เพิ่ม column หรือลบตาราง โฟลเดอร์อื่นก็โดนไปด้วย สำหรับ branch แบบนี้ให้ใส่ --db

  • --db=copy สร้างฐานข้อมูลชื่อ <ฐานข้อมูลเดิม>_<ท้ายชื่อ branch> คัดลอกข้อมูลเดิมเข้าไปด้วย pg_dump | psql บน PostgreSQL หรือ mysqldump | mysql บน MySQL แล้วสั่ง php artisan migrate บนฐานข้อมูลใหม่นั้น migration ของ branch จึงไม่แตะฐานข้อมูลของโฟลเดอร์หลัก
  • --db=empty สร้างฐานข้อมูลเปล่าแล้ว migrate ใส่ --seed ถ้าอยากได้ข้อมูลตั้งต้น
  • SQLite ได้ไฟล์ของตัวเองในโฟลเดอร์ worktree
  • ตอนสั่ง migrate lwt ใส่ DB_DATABASE ของฐานข้อมูลใหม่ไปกับคำสั่งด้วย เผื่อมีค่าใน environment ของ shell ที่ทับ .env อยู่

lwt สร้างฐานข้อมูลเฉพาะบนเครื่องเรา ถ้า DB_HOST เป็นเซิร์ฟเวอร์อื่น lwt จะไม่สร้างให้และใช้ฐานข้อมูลเดิมต่อ เพราะ .env ที่ตั้งผิดเพียงบรรทัดเดียวอาจพาไปสร้างฐานข้อมูลบนเซิร์ฟเวอร์ที่คนอื่นใช้อยู่

บน MySQL โปรเจกต์ส่วนใหญ่ใช้ user ของแอปที่มีสิทธิ์แค่ฐานข้อมูลของตัวเอง สร้างฐานข้อมูลใหม่ไม่ได้ จะเห็น error 1044 Access denied ให้ใส่ --db-admin=root lwt จะสร้างฐานข้อมูลด้วย root แล้วให้สิทธิ์ user ของแอปเฉพาะฐานข้อมูลนั้น (บน PostgreSQL จะตั้ง user ของแอปเป็นเจ้าของ) รหัสผ่านของ admin อ่านจาก LWT_DB_ADMIN_PASSWORD ถ้ามี

lwt add feat/export-pdf --db=copy --db-admin=root

test ยังใช้ฐานข้อมูล testing ชุดเดียวกันทุกโฟลเดอร์

lwt แยกเฉพาะฐานข้อมูลของแอป ไม่แยกฐานข้อมูลของ test โปรเจกต์ของผมส่วนใหญ่ตั้งไว้แบบนี้ คือ test รันด้วย Pest บนฐานข้อมูล testing ที่โหลดจาก snapshot ด้วย php artisan snapshot:load testing --env=testing --force ของ spatie/laravel-db-snapshots และทุก test อยู่ใน DatabaseTransactions จึงย้อนกลับเองทุกครั้ง หลายโฟลเดอร์รัน test พร้อมกันบนฐานข้อมูลนี้ได้ บางโปรเจกต์ยังมี test ที่เช็กว่ารันอยู่บนฐานข้อมูลชื่อนี้จริง กันไม่ให้ test ไปเขียนฐานข้อมูลอื่น ถ้าแยกชื่อตาม worktree test ตัวนี้ก็จะไม่ผ่าน

ตอนตั้ง lwt จะเปิดดูฐานข้อมูล testing ที่ .env.testing กับ phpunit.xml ชี้ไว้ แล้วบอกจำนวนตารางในบรรทัด tests ตอนท้าย ถ้ายังไม่มีหรือยังว่าง lwt จะบอกคำสั่งที่ต้องรัน

  ! The tests' database shop_testing doesn't exist — load it once, while no other checkout is testing: php artisan snapshot:load testing --env=testing --force

พอใช้ร่วมกันก็มีคำสั่งที่ต้องระวัง

  • อย่าสั่ง migrate:fresh หรือ php artisan test --parallel ระหว่างที่โฟลเดอร์อื่นรัน test อยู่ ทั้งสองอย่างล้างหรือสร้างฐานข้อมูลใหม่ทับ
  • snapshot:load ก็โหลดทับทั้งฐานข้อมูลเหมือนกัน รันตอนที่ไม่มีใครรัน test อยู่
  • branch ที่เพิ่ม column ใหม่ และมี test ที่อ่านหรือเขียน column นั้น ให้ migrate เฉพาะไฟล์นั้นเข้าไป php artisan migrate --path=database/migrations/<ไฟล์> --env=testing column ที่เพิ่มเข้ามาไม่กระทบโฟลเดอร์อื่นที่ยังไม่ใช้

lwt setup ตั้ง worktree ที่สร้างจากที่อื่น

worktree ไม่ได้มาจาก lwt add เสมอไป บางทีสร้างด้วย git worktree add เอง จากแท็บ Worktrees ของ git-prompt หรือ Claude Code สร้างให้ใน .claude/worktrees/ เข้าไปในโฟลเดอร์นั้นแล้วสั่ง

lwt setup

ได้ทุกขั้นเหมือน lwt add ยกเว้นการสร้าง branch สั่งซ้ำได้ ของที่ตั้งไว้แล้วจะเก็บไว้เหมือนเดิม

ถ้าใช้ AI agent ให้ใส่ -n ซึ่งไม่ถามอะไรเลย ใช้ค่าเริ่มต้นกับ option ที่ใส่มาเท่านั้น และ exit code จะไม่ใช่ 0 ถ้ามีขั้นไหนที่ขอแล้วไม่สำเร็จ เช่น สร้างฐานข้อมูลไม่ได้ หรือ migrate ไม่ผ่าน agent จะได้รู้ว่าต้องหยุดก่อน ไม่ไป migrate ต่อบนฐานข้อมูลที่ใช้ร่วมกัน

lwt setup -n                         # worktree ที่ agent สร้าง
lwt add feat/export-pdf -n --db=copy # branch ที่มี migration
lwt remove feat/export-pdf -n --drop-db --delete-branch

ผมเขียนวิธีใช้แบบนี้ไว้ใน skill ของ Claude Code ทุก session ที่ทำงานใน worktree จึงสั่ง lwt setup -n ก่อน ไม่ต้องตั้งเองทีละขั้น

lwt remove ลบให้ครบทั้งโฟลเดอร์ ฐานข้อมูล และ branch

git worktree remove ลบแค่โฟลเดอร์ branch ยังอยู่ ฐานข้อมูลที่สร้างไว้ก็ยังอยู่ และถ้าเป็น worktree ใน .claude/worktrees/ ก็เหลือโฟลเดอร์ว่างค้างไว้ lwt remove เก็บให้ครบในคำสั่งเดียว

lwt remove ลบโฟลเดอร์ ฐานข้อมูลที่สร้างไว้ และ branch ที่ merge แล้ว

ฐานข้อมูลกับ branch ถามก่อนลบทุกครั้ง ภาพนี้ตอบ Enter ทั้งสองข้อ

  • ถ้าในโฟลเดอร์ยังมีไฟล์ที่แก้ค้างหรือไฟล์ใหม่ที่ยังไม่ commit lwt จะไม่ลบเลย ไฟล์ที่ gitignore ไว้อย่าง vendor, node_modules และ .env ไม่นับ
  • ลบเฉพาะฐานข้อมูลที่ lwt สร้างเอง lwt จดชื่อไว้ในโฟลเดอร์ .git/worktrees/<ชื่อ>/ ของ worktree นั้น ซึ่งอยู่นอกไฟล์งาน ไม่มีทางติดไปใน commit และ git ลบให้เองพร้อม worktree และไม่ลบฐานข้อมูลที่ชื่อตรงกับของโฟลเดอร์หลักแน่นอน
  • branch ที่ merge เข้า branch หลักแล้ว ค่าที่เลือกไว้คือลบ ถ้ายังมี commit ที่ branch หลักไม่มี จะบอกจำนวน commit แล้วถามยืนยันอีกรอบ
  • ถ้า parent folder อย่าง .claude/worktrees/ ว่างหลังลบ ก็ลบโฟลเดอร์นั้นด้วย แต่ไม่แตะโฟลเดอร์ที่โฟลเดอร์หลักอยู่
  • ถ้าเผลอลบโฟลเดอร์ทิ้งเองไปแล้ว lwt remove จะ git worktree prune ให้ แล้วจัดการ branch ต่อ

คำสั่งทั้งหมด

คำสั่ง ทำอะไร
lwt add [branch] fetch สร้างหรือเลือก branch เลือกโฟลเดอร์ git worktree add แล้วตั้งให้
lwt setup [path] ตั้ง worktree ที่มีอยู่แล้ว ค่าเริ่มต้นคือโฟลเดอร์ที่อยู่
lwt remove [path|branch] ลบ worktree ที่ clean ฐานข้อมูลที่ lwt สร้าง และ branch ถ้าเลือก
lwt list ทุก worktree ตั้งแล้วหรือยัง และฐานข้อมูลที่ lwt สร้างให้
Option
--db=shared|empty|copy ฐานข้อมูลเดิม (ค่าเริ่มต้น) ฐานข้อมูลใหม่ที่ migrate แล้ว หรือฐานข้อมูลที่คัดลอกจากเดิม
--db-admin=<user> สร้างฐานข้อมูลด้วย root หรือ postgres เมื่อ user ของแอปไม่มีสิทธิ์
--url=<url> APP_URL ของ worktree
--npm, --build node_modules และ npm run build
--vendor=install บังคับ composer install แม้ lockfile เหมือนกัน
--base=<ref>, --path=<โฟลเดอร์> ของ add แตก branch จากไหน และวางโฟลเดอร์ไว้ที่ไหน
--delete-branch, --drop-db ของ remove ตอนใช้กับ -n
-n ไม่ถามอะไรเลย สำหรับ script และ AI agent

GitHub: phattarachai/worktree-prompt

อ่านต่อ

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

ล่าสุด

ดูทั้งหมด →