📂 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 --hard xoá 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] --amend viế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ãy git status trước, git add sau, và add theo file khi bạn chưa chắc.
  • Không có git diff trước khi commit. git diff xem phần chưa staged, git diff --staged xem đúng thứ sẽ vào commit. Một lần --staged tiế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ùng git restore --staged.
  • --amend trê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 status trướ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`." }
]