Repo, staging, commit, log và undo: nhìn đúng trạng thái
Mục tiêu bài học
- Đọc được vòng đời của một file trong Git: untracked, modified, staged, committed.
- Hiểu ba vùng làm việc và biết lệnh nào đưa file từ vùng này sang vùng khác.
- Viết commit message rõ nghĩa, đủ để đồng nghiệp đọc log là hiểu.
- Hoàn tác an toàn: bỏ thay đổi chưa stage, bỏ khỏi staging, sửa commit gần nhất, lùi commit mà giữ code.
Lý thuyết: ba vùng và bốn trạng thái
Git không "lưu file". Git lưu snapshot của toàn bộ project tại thời điểm bạn commit. Trước khi tới snapshot đó, thay đổi của bạn đi qua ba vùng:
| Vùng | Nằm ở đâu | Vai trò |
|---|---|---|
| Working tree | thư mục bạn đang mở | Nơi bạn sửa file |
| Staging area (index) | trong .git |
"Giỏ hàng" của commit tiếp theo |
| Repository | trong .git |
Lịch sử commit đã chốt |
Lệnh git add đưa thay đổi từ working tree vào {{c1::staging area}}. Đây là điểm khiến người mới hay nhầm: commit chỉ lấy những gì đang staged, không lấy mọi thứ bạn vừa sửa.
flowchart LR
U["❓<br/>Untracked"] -->|git add| S["📦<br/>Staged"]
M["✏️<br/>Modified"] -->|git add| S
S -->|git commit| R["🗄️<br/>Committed"]
R -->|sửa file| M
Đọc git status như đọc bảng trạng thái
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: style.css
Untracked files:
notes.md
Dùng git status --short cho nhanh. Ký tự ?? nghĩa là file {{c4::untracked}}. Điểm quan trọng nhất: hai cột. Cột trái là staging, cột phải là working tree.
| Mã | Nghĩa |
|---|---|
?? |
untracked, Git chưa theo dõi |
M |
đã sửa, chưa staged |
M |
đã staged, sẵn sàng commit |
MM |
phần đã staged, và bạn vừa sửa thêm lần nữa |
MM là ca hay gặp nhất khi bạn git add . quá sớm rồi sửa tiếp. Commit lúc này chỉ chứa phần đã staged.
Commit message: viết cho người đọc log
Quy tắc của team mình: dòng tiêu đề dưới 72 ký tự, thể mệnh lệnh, nói cái gì và vì sao, không viết "update", "fix", "sửa lỗi".
feat: thêm trang danh sách bài viết
fix: sửa lỗi ảnh bìa không hiển thị trên mobile
Xem lịch sử
git log --oneline hiển thị lịch sử commit {{c6::rút gọn mỗi commit một dòng}}. Bản đầy đủ mình hay dùng: git log --oneline --graph --decorate -5. Cần xem nội dung: git show <hash> hoặc git log -p -- index.html.
Bốn lệnh undo cần thuộc
| Tình huống | Lệnh | Kết quả |
|---|---|---|
| Sửa file, chưa add, muốn bỏ | git restore <file> |
Về bản trong commit gần nhất |
| Lỡ add, muốn rút ra | git restore --staged <file> |
Ra khỏi staging, giữ nguyên thay đổi |
| Sai message hoặc quên file | git commit --amend |
Sửa commit gần nhất, không tạo commit mới |
| Muốn lùi một commit, giữ code | git reset --soft HEAD~1 |
Commit biến mất, thay đổi nằm trong staging |
git restore --staged <file> đưa file ra khỏi staging nhưng {{c2::giữ nguyên thay đổi trong working tree}}. Còn git commit --amend cho phép sửa lại {{c5::commit gần nhất}} mà không tạo commit mới.
[!WARNING]
git reset --hardxoá cả commit lẫn {{c3::thay đổi chưa commit}}. Lệnh này rất nguy hiểm, không có bước xác nhận. Chỉ dùng khi bạn chắc chắn muốn mất thay đổi đó.
[!NOTE]
--amendviết lại lịch sử. Chỉ dùng khi commit đó chưa được push. Với commit đã đẩy lên remote, cách đúng làgit revert, mình sẽ nói kỹ ở bài release.
Ví dụ từng bước: vòng đời một file trong project blog
1. Khởi tạo repo cho blog: `mkdir blog-ca-nhan && cd blog-ca-nhan && git init`
2. Tạo file trang chủ: `echo "<h1>Blog</h1>" > index.html`, chạy `git status --short` thấy `?? index.html`
3. Đưa vào staging: `git add index.html`, status chuyển thành `A index.html`
4. Chốt commit đầu tiên: `git commit -m "Thêm trang chủ blog"`, status sạch
5. Sửa file, status thành ` M index.html`, xem chi tiết bằng `git diff`
6. Lỡ tay: `git add .`, rồi sửa thêm một dòng nữa, status thành `MM index.html`
7. Rút khỏi staging để chọn lại: `git restore --staged index.html`
8. Chỉ add phần đã ưng rồi commit: `git add index.html && git commit -m "Thêm tiêu đề cho trang chủ"`
9. Quên một file: `git add style.css && git commit --amend --no-edit`
10. Kiểm tra lịch sử: `git log --oneline --graph --decorate -5`, thấy đúng hai commit
Điểm cần nhớ: bước 6 cho bạn cơ hội nhìn thấy MM. Nếu không nhìn status, bạn sẽ commit thiếu dòng vừa sửa và tự hỏi vì sao code trên repo không chạy.
Luyện tập có hướng dẫn
Bài 1. git status --short của project blog in ra:
?? notes.md
M index.html
M style.css
MM post.html
Cho biết từng file đang ở trạng thái nào và lệnh tiếp theo cần làm nếu muốn commit style.css và post.html cho sạch.
Xem đáp án
notes.md chưa được theo dõi (untracked). index.html đã sửa nhưng chưa staged. style.css đã staged, sẵn sàng commit. post.html có phần staged và phần mới sửa thêm chưa staged.
Muốn commit sạch: git add post.html để gom phần còn lại, sau đó git add index.html nếu muốn gồm cả nó, rồi git commit -m "Thêm trang bài viết và cập nhật style". Nhớ thêm notes.md vào .gitignore và git restore --staged notes.md nếu đã lỡ add.
Bài 2. Bạn vừa commit "update" nhưng quên file style.css và message quá tệ. Sửa cả hai mà không tạo commit mới.
Xem đáp án
git add style.css
git commit --amend -m "fix: sửa ảnh bìa không hiển thị trên mobile"
--amend gộp file mới vào chính commit vừa rồi và thay message. Chỉ làm được khi commit này chưa push.
Bài 3. Bạn commit hai thay đổi vào cùng một commit, nhưng muốn tách thành hai commit riêng. Làm sao?
Xem đáp án
git reset --soft HEAD~1
git restore --staged <file can-tach>
git commit -m "feat: thêm trang danh sách bài viết"
git add <file can-tach>
git commit -m "style: đổi màu chủ đề"
reset --soft xoá commit nhưng giữ toàn bộ thay đổi trong staging, nên code không mất. Từ đó bạn chia lại giỏ hàng theo ý mình.
Góc chuyên gia: lỗi thường gặp
git add .theo phản xạ. Đây là lỗi số một. Hãygit statustrước,git addsau, và add theo file khi bạn chưa chắc.- Không có
git difftrước khi commit.git diffxem phần chưa staged,git diff --stagedxem đúng thứ sẽ vào commit. Một lần--stagedtiết kiệm cho bạn một buổi chiều debug. - Commit message kiểu "update", "fix", "done". Sáu tháng sau bạn sẽ là người nguyền rủa chính mình. Viết thể mệnh lệnh, nói rõ ý định.
git reset --hardđể "cho sạch". Nó xoá thật. Nếu chỉ muốn bỏ staging, dùnggit restore --staged.--amendtrên commit đã push. Cần revert thay vì viết lại lịch sử. Nội dung này sẽ quay lại ở bài release.- Quy tắc ngón tay cái: nhìn
git statustrước và sau mỗi lệnh Git. Nếu status không đổi như bạn mong, dừng lại và đọc.
Tóm tắt và bước tiếp theo
Bạn vừa nắm mô hình ba vùng: working tree, staging, repository. File đi qua bốn trạng thái, và mỗi lệnh undo chỉ tác động lên một vùng cụ thể. Nhớ ba câu: git add đưa vào staging, git commit chốt snapshot, git restore để lùi lại an toàn.
Bài tiếp theo: Remote, push, pull: đưa project blog lên GitHub. Bạn sẽ tạo remote, đẩy hai commit vừa làm lên GitHub và biết cách xử lý khi push bị từ chối.
`git add` đưa thay đổi từ working tree vào {{c1::staging area}}.
`git restore --staged <file>` rút file khỏi staging nhưng {{c2::giữ nguyên thay đổi trong working tree}}.
`git reset --hard` xoá cả commit lẫn {{c3::thay đổi chưa commit}} nên rất nguy hiểm.
Trong `git status --short`, ký hiệu `??` nghĩa là file {{c4::untracked}}.
`git commit --amend` cho phép sửa lại {{c5::commit gần nhất}} mà không tạo commit mới.
`git log --oneline` hiển thị lịch sử commit {{c6::rút gọn mỗi commit một dòng}}.
[
{ "front": "Ba vùng làm việc của Git là gì?", "back": "Working tree, staging area và repository (.git)." },
{ "front": "`git add` làm gì?", "back": "Đưa thay đổi từ working tree vào staging area, chuẩn bị cho commit." },
{ "front": "`git commit` lấy nội dung từ đâu?", "back": "Chỉ lấy những gì đang staged, không lấy mọi thay đổi trong working tree." },
{ "front": "File untracked hiển thị thế nào trong `git status --short`?", "back": "Bằng ký hiệu `??`." },
{ "front": "Cột trái và cột phải trong `git status --short` nghĩa là gì?", "back": "Cột trái là trạng thái ở staging area, cột phải là trạng thái ở working tree." },
{ "front": "`MM` trong `git status --short` nghĩa là gì?", "back": "File có phần đã staged và có thêm thay đổi mới chưa staged." },
{ "front": "`git restore <file>` làm gì?", "back": "Bỏ thay đổi chưa stage ở working tree, đưa file về bản trong commit gần nhất." },
{ "front": "`git restore --staged <file>` làm gì?", "back": "Rút file khỏi staging nhưng giữ nguyên nội dung đã sửa trong working tree." },
{ "front": "`git reset --soft HEAD~1` làm gì?", "back": "Bỏ commit gần nhất nhưng giữ toàn bộ thay đổi trong staging." },
{ "front": "Vì sao `git reset --hard` nguy hiểm?", "back": "Nó xoá cả commit lẫn thay đổi chưa commit, không khôi phục được." },
{ "front": "`git commit --amend` dùng khi nào?", "back": "Khi cần sửa commit gần nhất (message hoặc thêm file), và commit đó chưa được push." },
{ "front": "Một commit message tốt có đặc điểm gì?", "back": "Dòng tiêu đề dưới 72 ký tự, thể mệnh lệnh, nói rõ thay đổi gì và vì sao." },
{ "front": "Lệnh xem lịch sử commit gọn và có nhánh?", "back": "`git log --oneline --graph --decorate -5`." }
]