# 📂 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.

```mermaid
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

```steps
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.

<details><summary>Xem đáp án</summary>

`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.
</details>

**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.

<details><summary>Xem đáp án</summary>

```
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.
</details>

**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?

<details><summary>Xem đáp án</summary>

```
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.
</details>

## 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.

```cloze
`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}}.
```

```flashcard
[
  { "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`." }
]
```
