---
id: git-fundamentals
title: "Git cơ bản: commit và history"
shortTitle: "Commit & history"
track: git
trackTitle: "Git & VCS Hosting"
order: 1
level: core
status: published
estimatedMinutes: 40
prerequisites: []
next: git-branching
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Pro Git — Git Basics"
    url: https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository
  - title: "Pro Git — Git Objects"
    url: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
  - title: "gitignore"
    url: https://git-scm.com/docs/gitignore
  - title: "git-commit"
    url: https://git-scm.com/docs/git-commit
---

# Git cơ bản: commit và history

Lệnh chạy trên Git for Windows (Git Bash), macOS, Linux hoặc WSL2. Kiểm tra bằng `git --version`. Chưa có thì cài trước:

| Máy | Cách cài |
| --- | --- |
| Windows | Git for Windows từ [git-scm.com/install/windows](https://git-scm.com/install/windows), kèm Git Bash |
| macOS | `xcode-select --install`, hoặc `brew install git` |
| Ubuntu, Debian, WSL2 | `sudo apt install git` |

Repo trên máy bạn là local repository. Bản trên GitHub, GitLab hay Azure Repos là remote repository. Bài này tạo repo local và clone. Push, fetch và remote tên `origin` ở bài 3.

## Để làm gì

Với DevOps, Git không chỉ để lưu code. Pipeline chạy theo commit, Terraform và Kubernetes manifest nằm trong repo, rollback nghĩa là quay về một commit cũ. Muốn làm mấy việc đó thì trước hết phải hiểu một commit chứa gì, và đọc được history để biết ai đổi gì, lúc nào, vì sao.

## Gồm những gì

- Ba vùng: working tree, staging area (index), repository.
- Commit là snapshot, không phải diff. Mỗi commit có hash, parent, author, message.
- `git config` cho identity và default branch.
- `git init`, `git init -b`, `git init --bare`.
- `git clone <url> [directory]`, `git clone -b`, `git clone --bare`.
- `git status`, `git add`, `git commit`, `git diff`, `git log`, `git show`, `git restore`.
- `.gitignore` và cách bỏ track một file đã lỡ commit.
- Viết commit message đọc được khi review và khi debug.

Branch, merge và rebase nằm ở bài sau. Sửa history (revert, reset, reflog) nằm ở bài recovery.

## Dùng khi nào

Bạn dùng Git hằng ngày, mỗi lần đổi code hay config. Vài tình huống DevOps cụ thể:

- Pipeline fail sau một lần merge: `git log` và `git show` cho biết commit nào đổi gì.
- Cần biết ai sửa một dòng config production: `git log -p -- path/to/file` hoặc `git blame`.
- Trước khi push: `git diff --staged` để chắc không lẫn file `.env` hay file build.

## Kiến thức cốt lõi

### Ba vùng của một repo

| Vùng | Là gì | Lệnh đưa file vào |
| --- | --- | --- |
| Working tree | File bạn đang sửa trên đĩa | Sửa file |
| Staging area (index) | Bản nháp của commit kế tiếp | `git add` |
| Repository (`.git/`) | Các commit đã lưu | `git commit` |

Nhờ có staging area, bạn commit được một phần thay đổi. Ví dụ bạn sửa ba file nhưng chỉ hai file thuộc cùng một ý: `git add` hai file đó, commit, rồi tính file còn lại sau. `git add -p` còn cho chọn từng đoạn trong một file.

### Commit là gì

Theo Pro Git, Git lưu dữ liệu thành các object:

- **blob**: nội dung một file.
- **tree**: danh sách tên file, trỏ tới blob hoặc tree con. Tương ứng một thư mục.
- **commit**: trỏ tới một tree gốc (snapshot cả project), cùng parent commit, author, committer, message.

Hash của commit được tính từ toàn bộ nội dung đó. Đổi một byte trong file, hay đổi message, là ra hash khác. Vì vậy nếu bạn sửa một commit đã push, Git tạo ra commit mới, và history của người khác sẽ lệch với của bạn. Bài 5 nói kỹ chỗ này.

Git mặc định dùng SHA-1 cho object ID. Hầu hết lệnh chấp nhận hash rút gọn, ví dụ `a1b2c3d`, miễn không trùng.

### HEAD

`HEAD` trỏ tới commit bạn đang đứng, thường qua một branch. `HEAD~1` là commit cha, `HEAD~3` là lùi ba commit. Mấy ký hiệu này dùng nhiều trong `git show`, `git diff`, `git reset`.

`git init` tạo `.git` trong thư mục hiện tại, hoặc trong thư mục ghi ở argument cuối. `-b` / `--initial-branch` đặt tên nhánh đầu. Không có `-b`, Git lấy `init.defaultBranch`. Chưa đặt biến đó thì bản Git hiện tại vẫn dùng `master`, tới Git 3.0 mới đổi mặc định thành `main`.

`--bare` tạo repo không có working tree. Thư mục đó chính là `$GIT_DIR`. Máy dùng làm remote trung tâm, ví dụ storage server, thì `init --bare`. Clone từ bare repo mới có file để sửa. `git init` lần hai trên repo đã có thì an toàn, không ghi đè object.

`git clone <repository> [<directory>]` tạo thư mục mới, copy object, đặt remote `origin`, rồi checkout nhánh đang active của nguồn. Bỏ `directory` thì tên thư mục lấy từ repo. `-b` / `--branch` checkout đúng nhánh đó sau khi clone. `--bare` clone thành bare repo, không checkout, không tạo `origin/*`.

### Commit message

Dòng đầu ngắn (khoảng 50 ký tự), nói commit làm gì. Dòng trống. Rồi phần thân giải thích vì sao, nếu cần. `git log --oneline` chỉ hiện dòng đầu, nên dòng đầu phải tự đứng được.

```text
Fix healthcheck path for API container

Kubernetes readiness probe gọi /health, nhưng app đã đổi sang /healthz
từ commit a1b2c3d. Pod không bao giờ Ready nên rollout bị treo.
```

Nhiều team dùng Conventional Commits (`feat:`, `fix:`, `chore:`). Bài 4 nói về format này vì nó liên quan tới release tự động.

### .gitignore

`.gitignore` liệt kê pattern file Git bỏ qua khi `git status` và `git add`. Nó chỉ có tác dụng với file **chưa được track**. File đã commit rồi thì thêm vào `.gitignore` cũng không làm Git quên nó, phải `git rm --cached`.

| Pattern | Khớp |
| --- | --- |
| `node_modules/` | Thư mục `node_modules` ở mọi cấp |
| `/dist` | Chỉ `dist` ở gốc repo |
| `*.log` | Mọi file đuôi `.log` |
| `.env` | File `.env` ở mọi cấp |
| `!.env.example` | Ngoại lệ: vẫn track `.env.example` |

## Lệnh và code mẫu

Cấu hình một lần trên máy. Email nên trùng email trên GitHub để commit gắn đúng account:

```bash
git config --global user.name "Nguyen Van A"
git config --global user.email "a@example.com"
git config --global init.defaultBranch main
git config --global --list
```

Tạo repo và commit đầu tiên. `-b main` đặt tên nhánh, khỏi phụ thuộc mặc định của bản Git:

```bash
mkdir -p "$HOME/devops-lab/git-basics"
cd "$HOME/devops-lab/git-basics"
git init -b main

printf 'PORT=3000\n' > .env.example
printf 'console.log("hello");\n' > server.js
printf 'node_modules/\n.env\n*.log\n' > .gitignore

git status
git add .gitignore server.js .env.example
git commit -m "Initial app skeleton"
```

Xem thay đổi trước khi commit:

```bash
git diff            # working tree so với staging
git diff --staged   # staging so với commit cuối, chính là thứ sắp commit
git add -p          # chọn từng đoạn thay đổi
```

Đọc history:

```bash
git log --oneline --graph --decorate
git log -p -- server.js         # mọi thay đổi của một file
git log --since="2 days ago" --author="Nguyen"
git show HEAD                   # nội dung commit cuối
git show HEAD~1:server.js       # file ở commit trước đó
git blame server.js             # dòng nào do commit nào
```

Bỏ thay đổi chưa commit:

```bash
git restore server.js            # bỏ sửa trong working tree
git restore --staged server.js   # bỏ khỏi staging, giữ sửa trên đĩa
```

Lỡ commit file cần ignore:

```bash
git rm --cached app.log
echo "*.log" >> .gitignore
git commit -m "Stop tracking log files"
```

Bare repo trên máy, rồi clone ra working tree. Đây là kiểu đặt repo trên storage server rồi clone sang chỗ làm việc:

```bash
mkdir -p "$HOME/devops-lab/git-storage"
cd "$HOME/devops-lab/git-storage"
git init --bare -b main app.git
git clone "$HOME/devops-lab/git-storage/app.git" work
cd work
printf 'ok\n' > README
git add README
git commit -m "Add README"
git push -u origin main
```

`git push` lần này chỉ để bare repo có commit, rồi mới clone được nhánh `main`. Push, fetch và `-u` nằm ở bài 3.

Clone một nhánh cụ thể, sau khi bare repo đã có commit:

```bash
git clone -b main "$HOME/devops-lab/git-storage/app.git" work-main
```

## Thực hành

1. Chạy khối cấu hình và tạo repo ở trên.
2. Tạo file `.env` có `DB_PASSWORD=secret`. Chạy `git status`, xác nhận `.env` không xuất hiện.
3. Sửa `server.js`, chạy `git diff`. `git add server.js`, chạy lại `git diff` (trống) và `git diff --staged` (có nội dung).
4. Commit với message có dòng đầu và phần thân.
5. Tạo `app.log`, `git add -f app.log` rồi commit (giả lập lỡ commit). Sau đó bỏ track bằng `git rm --cached`.
6. Chạy `git log --oneline` và `git show HEAD~1`.
7. Chạy `git cat-file -p HEAD` để xem commit object: dòng `tree`, `parent`, `author`.
8. Chạy khối bare repo. Trong `app.git` không có `README` trên đĩa. Trong `work` mới có file.

## Kết quả mong đợi

- `.env` không bao giờ hiện trong `git status`, còn `.env.example` có.
- `git diff --staged` cho thấy đúng phần sắp commit.
- `git log --oneline` có 3 đến 4 commit, dòng đầu đọc hiểu được.
- `git cat-file -p HEAD` in ra `tree <hash>`, `parent <hash>`, author và message.
- Sau `git rm --cached app.log`, file vẫn còn trên đĩa nhưng không còn trong commit mới nhất.
- `ls "$HOME/devops-lab/git-storage/app.git"` thấy `HEAD`, `objects`, `refs`. Không thấy `README`. `ls` trong `work` thấy `README`.

## Checklist

- [ ] Phân biệt working tree, staging area và repository.
- [ ] Giải thích vì sao sửa một commit đã push lại tạo ra hash mới.
- [ ] Dùng `git diff --staged` trước mỗi commit.
- [ ] Viết `.gitignore` có ngoại lệ bằng `!`.
- [ ] Bỏ track một file đã commit mà không xoá nó trên đĩa.
- [ ] Tìm được commit đã đổi một dòng bằng `git log -p` hoặc `git blame`.
- [ ] Tạo repo làm việc bằng `git init -b main`. Tạo remote trung tâm bằng `git init --bare`.
- [ ] Clone bằng `git clone <url> [directory]`. Checkout nhánh lúc clone thì thêm `-b`.

## Thẻ ôn

### git-fundamentals-areas
kind: distinguish
cue: Working tree là file đang sửa trên đĩa. Staging là phần đã `git add`. `git diff --staged` mới là thứ sắp vào commit.

Q: Bạn sửa ba file, `git add` hai file, rồi muốn xem mình sắp commit cái gì. Working tree, staging area và repository mỗi chỗ đang giữ gì, và `git diff` khác `git diff --staged` chỗ nào?

A: Working tree là các file trên đĩa, kể cả phần vừa sửa chưa add. Staging area, còn gọi là index, là phần bạn đã `git add` và Git sẽ ghi vào commit kế tiếp. Repository là các commit đã ghi xong. `git diff` so working tree với staging, nên nó cho thấy phần chưa add. `git diff --staged` so staging với commit cuối, đúng là nội dung sắp commit. Nhìn nhầm hai lệnh thì dễ commit thiếu hoặc commit nhầm.

### git-fundamentals-hash
kind: trap
cue: Sửa message của commit đã push không sửa commit cũ. Git tạo commit mới với hash mới, history của người khác lệch.

Q: Một commit đã push lên remote. Bạn sửa message cho hay hơn bằng amend. Vì sao máy đồng nghiệp, và cả pipeline đã checkout commit cũ, bị lệch history?

A: Hash của commit được tính từ toàn bộ nội dung: cây file, message, tác giả, và commit cha. Đổi message, hoặc đổi một byte trong file, ra một hash khác. Git không sửa commit cũ tại chỗ. Nó tạo một commit mới. Người đã kéo commit cũ vẫn đang đứng trên hash cũ, còn bạn đang đứng trên hash mới. Hai history không còn là một đường thẳng.

### git-fundamentals-add
kind: trap
cue: `git add .` rồi commit ngay dễ kéo `.env`, file build và log vào repo. Xem `git status` và `git diff --staged` trước.

Q: Sắp commit, bạn gõ `git add .` cho nhanh rồi commit luôn. Với một repo có pipeline và có file `.env` nằm trong thư mục làm việc, rủi ro cụ thể là gì?

A: `git add .` lấy mọi thay đổi Git đang thấy trong thư mục, không hỏi file nào thuộc commit này. `.env`, file build và log dễ đi theo nếu chúng chưa bị ignore. Một khi đã commit và push, secret hoặc artifact nằm trong history, pipeline và người clone sau đều lấy được. Trước khi commit, chạy `git status` và `git diff --staged`, hoặc add theo đúng tên file.

### git-fundamentals-ignore
kind: trap
cue: Thêm `.env` vào `.gitignore` sau khi đã commit không xóa file khỏi history. Secret đã lộ thì phải rotate.

Q: `.env` đã lỡ commit. Bạn thêm dòng `.env` vào `.gitignore` và commit tiếp, rồi nghĩ clone mới sẽ không còn secret. Clone mới còn đọc được secret không, và `git rm --cached` làm được tới đâu?

A: Còn. `.gitignore` chỉ ngăn Git theo dõi file từ commit sau trở đi. Các commit cũ vẫn chứa nguyên file, `git show` vẫn đọc được. `git rm --cached .env` bỏ file khỏi lần commit kế tiếp nhưng giữ file trên đĩa của bạn. Nó không xóa các bản đã nằm trong history. Secret đã vào commit thì coi như đã lộ: rotate secret trước, rồi mới tính chuyện dọn history.

### git-fundamentals-message
kind: decide
cue: Một commit gom config, API và log thì không revert riêng phần hỏng được. Mỗi commit một việc, tách bằng `git add -p`.

Q: Một commit vừa sửa config deploy, vừa đổi API, vừa thêm log, message ghi `update`. Production lỗi ở phần config. Bạn muốn revert đúng phần đó. Làm được không, và lần sau nên tách thế nào?

A: Không tách được. Revert hoàn một commit, nên cả ba việc cùng quay lại hoặc cùng ở lại. Mỗi commit nên là một việc có thể deploy hoặc gỡ riêng. `git add -p` chọn từng đoạn trong file khi các việc nằm cạnh nhau. Dòng đầu message nói hành động và đối tượng, ví dụ sửa timeout của health check, không ghi `update`, `fix` hay `wip`. Đọc message là biết commit này gỡ được những gì.

### git-fundamentals-init
kind: distinguish
cue: Làm việc trên file thì `git init -b main`. Remote trung tâm trên storage server thì `git init --bare`. Clone mới có working tree.

Q: Cần một repo trên storage server để máy khác clone vào. `git init` trong thư mục đó rồi copy file vào có đủ không, và `git clone url work` khác `git init` chỗ nào?

A: `git init` tạo `.git` cạnh file đang sửa, có working tree. `git init --bare` không có working tree: thư mục đó là `$GIT_DIR`, dùng làm remote. Copy file vào bare repo không phải cách đưa nội dung vào Git. Clone từ bare repo mới tạo thư mục làm việc, remote `origin`, và checkout. `-b` lúc `init` đặt tên nhánh đầu. `-b` lúc `clone` checkout đúng nhánh đó.

## Lỗi thường gặp

**`git add .` rồi commit luôn.** Lệnh này dễ kéo theo `.env`, file build, file log. Chạy `git status` và `git diff --staged` trước, hoặc add theo tên file.

**Thêm `.env` vào `.gitignore` sau khi đã commit nó.** File vẫn nằm trong history, ai clone cũng đọc được. Bỏ track bằng `git rm --cached .env` cho commit sau. Secret đã lộ thì phải rotate, xem bài 5.

**Commit message kiểu `update`, `fix`, `wip`.** Ba tháng sau đọc `git log`, bạn không biết commit nào đổi config gì. Dòng đầu nên nói rõ hành động và đối tượng: `Raise API memory limit to 512Mi`.

**Một commit gộp nhiều việc không liên quan.** Khi một phần bị lỗi, bạn không revert riêng phần đó được. Dùng `git add -p` để tách thành nhiều commit nhỏ.

**`git init` trên storage server rồi sửa file ngay trong thư mục đó.** Repo thường cần `--bare`. File làm việc nằm ở bản clone.

**Commit bằng email công ty trên máy cá nhân, hoặc ngược lại.** Commit sẽ không gắn đúng account trên GitHub. Kiểm tra `git config user.email` trong repo; bạn có thể đặt email riêng cho từng repo bằng `git config user.email` không có `--global`.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày dựng repo trên storage server và ngày clone. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [KodeKloud — GIT for Beginners](https://learn.kodekloud.com/learn/courses/git-for-beginners) — phạm vi và thứ tự module GIT Introduction: local và remote, cài Git, `git init`, `git log`, và phần Understanding GIT.
3. [Pro Git — Git Basics](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) — kiểm lệnh, chương 2.1 đến 2.3.
4. [Pro Git — Git Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) — blob, tree, commit object.
5. [git-commit](https://git-scm.com/docs/git-commit), [git-diff](https://git-scm.com/docs/git-diff), [git-restore](https://git-scm.com/docs/git-restore), [git-log](https://git-scm.com/docs/git-log) — reference từng lệnh.
6. [gitignore](https://git-scm.com/docs/gitignore) — cú pháp pattern, gồm ngoại lệ `!` và `/`.
7. [git-config](https://git-scm.com/docs/git-config) — `user.name`, `user.email`, `init.defaultBranch`.
8. [git-init](https://git-scm.com/docs/git-init), [git-clone](https://git-scm.com/docs/git-clone) — `-b`, `--bare`, thư mục đích.

## Tiếp theo

[Branch, merge và rebase](./02-branching.md)
