Chuyện thường ngày: bạn đang code dở feature/cart-refactor, webpack dev server chạy ngon lành, vài file đang sửa nửa chừng. Rồi Slack hiện lên: “Prod lỗi 500 ở /api/login, fix gấp nhé.” Bạn thở dài, git stash, git checkout main, chờ npm install chạy lại, node_modules rebuild… mất 5 phút trước khi bắt đầu nhìn bug. Fix xong, quay lại feature branch, git stash pop, conflict. Một buổi sáng bay theo gió.
Nhiều người làm điều đó vài lần một tuần cho đến khi phát hiện git worktree, một tính năng có sẵn trong Git mà không hiểu sao ít người dùng. Nó cho phép bạn mở nhiều nhánh cùng lúc, mỗi nhánh một thư mục riêng, mà không cần clone thêm repo. Nghe đơn giản, nhưng ảnh hưởng đến workflow hằng ngày nhiều hơn bạn nghĩ.
Worktree là gì và tại sao nó khác clone
Khi bạn git clone, Git tạo cho bạn một working tree, thư mục chứa file mã nguồn tương ứng với nhánh đang checkout. Phía sau là thư mục .git/ chứa toàn bộ object database (blob, tree, commit), refs, config. Mối quan hệ là 1:1, một .git/, một thư mục code.
git worktree phá vỡ mối quan hệ 1:1 đó. Nó cho phép bạn tạo nhiều working tree cùng chia sẻ một object database. Nghĩa là bạn có thể có feature/cart ở thư mục ~/work/proj, đồng thời main ở ~/work/proj-hotfix, và staging ở ~/work/proj-staging, tất cả dùng chung lịch sử commit, remote, stash. Không phải ba bản clone tách biệt. Fetch một lần ở bất kỳ worktree nào thì cả ba đều thấy commit mới.
Worktree đầu tiên (thư mục chứa .git/) gọi là primary, các worktree thêm vào gọi là linked worktree. Mỗi linked worktree có metadata riêng trong .git/worktrees/<name>/, nhưng object database vẫn chung.
Có một luật quan trọng: một nhánh local chỉ được checkout ở đúng một worktree tại một thời điểm. Git chặn để tránh hai nơi cùng sửa working copy của một nhánh. Và khi xoá thư mục worktree bằng rm -rf thay vì git worktree remove, metadata “ma” vẫn còn, bạn cần chạy git worktree prune để dọn.
So sánh nhanh ba cách tiếp cận:
| Cách | Ưu | Nhược |
|---|---|---|
git stash + git checkout | Không cần thư mục khác | Mất context, build cache reset, dễ conflict khi pop |
| Clone nhiều lần | Độc lập hoàn toàn | Tốn gấp đôi disk, fetch nhiều lần, refs không chia sẻ |
git worktree | Share object DB, nhiều nhánh sống cùng lúc | Cần kỷ luật đặt thư mục, vài cạm bẫy nhỏ |
Các lệnh thường dùng
Liệt kê worktree hiện có:
git worktree list
# /home/me/proj abc1234 [feature/api]
# /home/me/proj-hotfix def5678 [main]
# /home/me/proj-review 9876abc (detached HEAD)
Thêm worktree cho nhánh có sẵn, Git tạo thư mục mới và checkout nhánh đó vào:
git worktree add ../proj-hotfix main
Tạo nhánh mới đồng thời với worktree, rất tiện khi cần hotfix branch từ main:
git worktree add -b fix/login-500 ../proj-fix main
Khi muốn xem lại một tag cũ mà không ảnh hưởng gì, dùng detached mode:
git worktree add --detach ../proj-v1.8.0 v1.8.0
Xoá worktree đúng cách (không phải rm -rf):
git worktree remove ../proj-hotfix
# Nếu còn uncommitted changes, Git báo lỗi, thêm --force nếu bạn chắc chắn
Và nếu lỡ xoá thư mục bằng tay, chạy prune để dọn metadata mồ côi:
git worktree prune
prune an toàn, chỉ xoá entry trỏ tới thư mục không còn tồn tại, có thể chạy định kỳ hoặc kèm theo git gc.
Đặt thư mục thế nào cho gọn
Sau vài tháng dùng worktree, cách đặt thư mục ảnh hưởng nhiều đến trải nghiệm. Pattern thoải mái nhất là sibling, các worktree nằm cạnh nhau cùng cấp:
~/work/
├── proj/ ← worktree chính (feature đang làm)
├── proj-hotfix/ ← checkout main để fix nhanh
├── proj-review/ ← dùng review PR
└── proj-benchmark/ ← chạy benchmark bản cũ
Mỗi thư mục là một cửa sổ IDE riêng. cd ../proj-hotfix là đủ. Nếu bạn làm nhiều repo cùng lúc, có thể gom thêm một cấp: ~/work/proj/worktrees/hotfix/, nhưng pattern này ít cần thiết trừ khi bạn có hơn 5 repo active.
Một pattern khác là đặt worktree bên trong repo chính (ví dụ proj/worktrees/hotfix/). Nghe gọn nhưng trên thực tế hay gặp rắc rối: linter, formatter, hay Docker build context sẽ quét đệ quy vào thư mục con và xử lý file hai lần. Không khuyến nghị cách này.
Về tên thư mục, nên đặt theo mục đích thay vì tên nhánh. proj-hotfix hay hơn proj-fix-login-500-abc, vì bạn có thể checkout nhánh khác trong worktree đó sau này, metadata worktree không gắn chặt với branch name. Worktree proj-review có thể dùng đi dùng lại cho nhiều PR khác nhau, chỉ cần git checkout sang nhánh PR mới.
Kịch bản thực tế thường gặp
Hotfix trong khi đang dở feature
Đây là tình huống mở đầu bài viết. Thay vì stash rồi checkout, tạo worktree mới từ main:
cd ~/work/proj
git worktree add -b hotfix/login-500 ../proj-hotfix origin/main
cd ../proj-hotfix
# sửa bug, chạy test, tạo PR, merge
cd ~/work/proj # quay lại, build cache, node_modules, dev server vẫn nguyên
Điều giá trị nhất ở đây không phải tiết kiệm vài lệnh git, mà là build cache và state của dev server không bị reset. Với mono-repo lớn dùng Gradle hay webpack, mỗi lần checkout branch khác có thể phải rebuild 2-3 phút. Worktree giúp bạn bỏ qua toàn bộ chi phí đó.
Review PR mà không phá môi trường hiện tại
Khi cần review kỹ một PR (chạy test, debug thử), fetch branch rồi tạo worktree riêng:
git fetch origin pull/1234/head:pr-1234
git worktree add ../proj-pr-1234 pr-1234
cd ../proj-pr-1234
npm install && npm test
Review xong thì dọn sạch:
cd ~/work/proj
git worktree remove ../proj-pr-1234
git branch -D pr-1234
Cách này đặc biệt hữu ích khi PR đổi dependencies hoặc config, bạn không muốn npm install của PR đè lên node_modules đang chạy ổn của feature branch.
So sánh hiệu năng giữa hai branch
Đôi khi cần benchmark trước/sau khi optimize. Với worktree, bạn có thể chạy cả hai phiên bản song song trên hai port khác nhau, giảm biến thiên do thời gian giữa hai lần đo. Hoặc ít nhất, cả hai thư mục đều có warm cache sẵn.
Bisect mà không mất context
git bisect thay đổi HEAD liên tục khi nhị phân tìm commit gây lỗi. Nếu đang dở thay đổi lớn, tạo worktree phụ để bisect:
git worktree add ../proj-bisect main
cd ../proj-bisect
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
# lặp cho đến khi tìm ra commit xấu
git bisect reset
Worktree chính hoàn toàn không bị đụng vào.
Worktree sống chung với IDE, CI và submodule
IDE và editor
VSCode, JetBrains đều coi mỗi worktree như một repo riêng khi bạn mở thư mục đó, Git panel, commit, branch list hoạt động bình thường. Có điều, một số thông tin chia sẻ giữa các worktree (như refs mới sau khi fetch) cần IDE refresh mới thấy. Và tuyệt đối không mở cùng một thư mục worktree trong hai cửa sổ IDE, lock file sẽ conflict.
Một tip nhỏ: trong VSCode, nên lưu mỗi worktree thành một workspace riêng (.code-workspace), đặt ở thư mục sibling. Mở workspace nào là nhảy sang ngữ cảnh đó.
CI cục bộ
Nếu bạn chạy CI local bằng act hay container runner, mỗi worktree nên có thư mục cache riêng (.ci/ hay .cache/) để tránh đè nhau. Cũng đừng symlink node_modules giữa hai worktree, race condition khi cài package là chuyện có thật, và debug mất thời gian hơn cài lại từ đầu.
Submodule
Worktree phụ có working copy submodule riêng, nhưng object của submodule vẫn nằm trong primary (.git/modules/ chung). Khi tạo worktree mới mà repo dùng submodule, bạn cần:
cd ../proj-hotfix
git submodule update --init --recursive
Cẩn thận với git submodule deinit, chạy ở một worktree có thể ảnh hưởng các worktree khác nếu config share. Nếu dự án phụ thuộc nhiều submodule, nên đọc kỹ tài liệu về mối quan hệ giữa submodule và worktree trước khi dùng.
Hooks
.git/hooks/ nằm ở primary worktree. Tất cả linked worktree dùng chung hooks (trừ khi bạn đặt core.hooksPath khác cho từng worktree). Thường thì đây là điều tốt, pre-commit, pre-push chạy đồng nhất ở mọi worktree.
Rebase, merge, pull
Các thao tác này chạy trên worktree hiện tại và không ảnh hưởng nhánh đang checkout ở worktree khác, miễn bạn giữ luật “một nhánh một worktree”. Tuy nhiên, git push thì luôn push ref chứ không quan tâm worktree nào, có thể push nhánh đang checkout ở worktree khác. Cẩn thận với push --force.
Những rắc rối hay gặp
“fatal: … is already checked out at …”
Lỗi này xuất hiện khi bạn thử tạo worktree cho nhánh đang được checkout ở nơi khác. Giải pháp đơn giản nhất là tạo nhánh tạm:
git worktree add -b feature/foo-review ../proj-x feature/foo
Hoặc giải phóng worktree kia bằng cách commit/stash rồi checkout sang nhánh khác, hoặc remove nó.
Worktree “ma” sau khi xoá thư mục bằng tay
Nếu bạn lỡ rm -rf ../proj-hotfix thay vì dùng git worktree remove, Git vẫn nghĩ worktree đó tồn tại:
git worktree list
# /home/me/proj-hotfix (prunable)
git worktree prune # dọn sạch
Lỗi này hay gặp trong thời gian đầu dùng worktree. Nên tập thói quen: luôn dùng git worktree remove, coi như rm -rf worktree là cấm.
Branch xoá rồi mà ref vẫn còn
git worktree remove xoá worktree nhưng không tự xoá branch. Nếu muốn dọn sạch:
git worktree remove ../proj-fix
git branch -d fix/login-500 # -D nếu branch chưa merge
Bare repo + worktree, pattern nâng cao
Một cách dùng khá hay: clone bare repo làm “gốc refs”, rồi mỗi nhánh là một worktree:
git clone --bare git@github.com:org/proj.git ~/work/proj.git
cd ~/work/proj.git
git worktree add ../proj-main main
git worktree add ../proj-develop develop
Pattern này gọn khi bạn cần nhiều nhánh luôn sẵn sàng, ví dụ maintain nhiều phiên bản song song. Nhưng ban đầu dễ nhầm lẫn vì thư mục primary chỉ chứa bare git objects, không có file mã nguồn nào.
Worktree trên filesystem khác
Git vẫn hoạt động nếu worktree nằm trên disk khác (ổ ngoài, mount point), nhưng một số thao tác dựa vào hardlink cho pack files sẽ fall back sang copy, performance thấp hơn. Với SSD nội bộ thì không phải lo, nhưng nếu đặt trên NFS hay network drive, bạn sẽ thấy fetch/checkout chậm đáng kể.
Khi nào không nên dùng worktree
Worktree không phải lúc nào cũng là lựa chọn tốt. Nếu dự án nhỏ và bạn hiếm khi phải nhảy nhánh, stash + checkout hoàn toàn đủ, thêm worktree chỉ tạo loạn thư mục. Nếu bạn chưa quen Git lắm, worktree thêm một tầng mental model mà chưa cần thiết, nắm vững checkout, branch, stash trước đã.
Trong môi trường CI hoặc remote dev container, mỗi task thường chạy trong container riêng rồi, worktree không mang lại giá trị gì thêm. Và nếu repo có tooling cứng đường dẫn tuyệt đối trong script build, worktree phụ ở thư mục khác có thể phá vỡ assumption đó.
Tóm tắt
Worktree giải quyết một vấn đề cụ thể: giảm chi phí context switch khi bạn cần làm việc trên nhiều nhánh song song. Không phải clone thêm repo, không phải stash/checkout liên tục, không mất build cache. Bạn có nhiều thư mục, mỗi thư mục một nhánh, tất cả chia sẻ chung lịch sử Git.
Vài điều cần nhớ: mỗi nhánh chỉ checkout ở một worktree tại một thời điểm; xoá worktree bằng git worktree remove chứ không rm -rf; đặt thư mục sibling với tên mô tả mục đích thay vì tên nhánh; và chạy git worktree prune khi nghi có metadata mồ côi.
Trong một tuần làm việc bình thường, worktree tiết kiệm vài chục phút context switch, tính ra cả tháng cũng kha khá. Quan trọng hơn, nó giữ được flow: không phải dừng dev server, không phải rebuild, không phải nhớ stash ở đâu. Một khi đã quen, rất khó quay lại cách cũ.