Công cụ · Bài 1/1

Bài 1: Git thực hành từ cơ bản đến nâng cao

10 lượt đọc25 phút đọc

Git là công cụ quản lý phiên bản: nó nhớ mọi phiên bản của mã nguồn mà bạn từng lưu, ai thay đổi, thay đổi lúc nào và vì sao. Nhờ đó khi chương trình đang chạy bỗng lỗi, bạn tìm được ngay thay đổi nào gây ra, người nào viết, và quay về bản tốt trong vài giây. Bài này đi theo những câu hỏi thực tế mà nhóm phát triển phải trả lời hằng ngày, mỗi câu hỏi là một tình huống làm trên dự án C nhỏ tên thermo, in nhiệt độ cảm biến kèm giá trị trung bình trượt.

1.Mục tiêu bài học

Sau bài học này, bạn sẽ có thể:

  • Ghi lại một phiên bản bằng commit và tìm lại: ai sửa, sửa lúc nào, sửa để làm gì, sửa những gì.
  • Khoanh vùng commit gây lỗi và quay về bản cũ an toàn bằng bisect, revert, reset, reflog.
  • Phát triển song song bằng nhánh, merge, giải xung đột và cộng tác với đồng nghiệp qua remote.
  • Dọn lịch sử trước khi chia sẻ và đánh dấu bản phát hành bằng rebase, squash, cherry-pick, stash, tag, hook.

2.Quản lý phiên bản để làm gì

Những câu hỏi mà mọi dự án đều gặp

Khi chưa có công cụ, người ta chép file thành main_v2.c, main_final.c, main_final_that.c. Cách này không trả lời được những câu hỏi sau, còn Git trả lời được cả sáu.

  • Tuần trước chạy tốt, nay lỗi: code đã đổi gì? — Chép file thủ công: So sánh hai thư mục bằng mắt; Git: git diff, git log
  • Dòng này ai viết, để làm gì? — Chép file thủ công: Không biết; Git: git blame, git show
  • Commit nào làm hỏng chương trình? — Chép file thủ công: Thử từng bản một; Git: git bisect
  • Quay về bản chạy tốt thế nào? — Chép file thủ công: Chép đè, dễ mất bản mới; Git: git restore, git revert
  • Thử tính năng mới mà không phá bản đang chạy? — Chép file thủ công: Chép cả thư mục; Git: Nhánh (git switch -c)
  • Hai người cùng sửa một file thì sao? — Chép file thủ công: Ghép tay, hay ghi đè nhau; Git: git merge, git pull

Mỗi commit là một phiên bản có đủ ai, khi nào, vì sao

Đơn vị lịch sử của Git là commit: một bản chụp toàn bộ dự án tại một thời điểm, kèm thông tin để trả lời các câu hỏi ở trên. Hình dưới là commit sẽ xuất hiện nhiều lần trong bài.

Minh họa Git, hình 1

Phần "Why" là thông điệp commit do người viết tự gõ, nên chất lượng thông điệp quyết định lịch sử có đọc được hay không. Thông điệp tốt có dòng đầu ngắn (dưới 50 ký tự, viết ở dạng mệnh lệnh như "Add -f option") và nếu cần thì thêm một đoạn giải thích lý do, ví dụ số ticket hoặc nguyên nhân kỹ thuật.

Bốn vùng của Git

Một phiên bản được tạo ra qua bốn vùng. Bạn sửa file ở working directory, chọn những thay đổi muốn lưu đưa vào staging area, git commit chụp staging area thành commit trong local repository, và git push chia sẻ lên remote repository để cả nhóm thấy.

Minh họa Git, hình 2

Staging area cho phép bạn chọn thay đổi nào vào commit này, thay vì phải commit mọi thứ đang sửa dở.

3.Ghi lại phiên bản đầu tiên

Các ví dụ trong bài chạy trên Git 2.54. Lệnh git switch và git restore có từ Git 2.23, Ubuntu 22.04 trở lên đã có sẵn. Mã băm của commit (như c8006da) phụ thuộc nội dung, tác giả và thời gian nên sẽ khác trên máy bạn.

Ví dụ 1: Khai báo danh tính và tạo repository

Mỗi commit ghi tên và email tác giả, nên việc đầu tiên là khai báo một lần cho cả máy. Sau đó git init biến thư mục dự án thành repository bằng cách tạo thư mục ẩn .git chứa toàn bộ lịch sử.

bash
$ git config --global user.name "Nguyen Van A"
$ git config --global user.email "[email protected]"
$ git init -b main
Initialized empty Git repository in /work/l0/thermo/.git/
$ git status -s
?? sensor.c
?? sensor.h
  • Hai lệnh git config ghi vào ~/.gitconfig; đây chính là "ai" sẽ hiện trong mọi commit của bạn.
  • -b main đặt tên nhánh đầu tiên là main.
  • Dấu ?? nghĩa là Git thấy file nhưng chưa theo dõi.

Ví dụ 2: Commit đầu tiên

bash
$ git add sensor.c sensor.h && git status -s
A  sensor.c
A  sensor.h
$ git commit -m "Add sensor.c: read_temp() returns a raw sample"
[main (root-commit) c8006da] Add sensor.c: read_temp() returns a raw sample
 2 files changed, 14 insertions(+)
 create mode 100644 sensor.c
 create mode 100644 sensor.h
$ git log --oneline
c8006da Add sensor.c: read_temp() returns a raw sample

git add đưa file vào staging area (chữ A), git commit chụp thành phiên bản đầu tiên. Từ giờ mọi thay đổi đều được so sánh với phiên bản này.

Ví dụ 3: Xem trước khi lưu và bỏ thay đổi

Sửa một giá trị mẫu trong sensor.c, git diff cho thấy chính xác dòng nào đổi trước khi bạn commit.

bash
$ git status -s
 M sensor.c
$ git diff
diff --git a/sensor.c b/sensor.c
index 4083fda..fbbadad 100644
--- a/sensor.c
+++ b/sensor.c
@@ -1,6 +1,6 @@
 #include "sensor.h"
 
-static const int samples[] = {200, 210, 220, 230, 240, 250};
+static const int samples[] = {200, 215, 220, 230, 240, 250};
 
 int read_temp(int i)
 {
$ git restore sensor.c && git status -s
  • Dòng bắt đầu bằng - là bản cũ, dòng + là bản mới.
  • git restore sensor.c bỏ thay đổi chưa commit, file về đúng phiên bản gần nhất. Lệnh này không hoàn tác được vì thay đổi chưa từng nằm trong lịch sử.
  • Thói quen tốt: chạy git diff trước mỗi lần commit để chắc chỉ lưu đúng thứ định lưu. Muốn xem phần đã git add thì dùng git diff --staged.

Từ đây, bài dùng dự án thermo đã có đủ 7 commit của hai người, như bảng dưới. Bạn có thể tự tạo lịch sử tương tự để làm theo, hoặc chỉ cần đọc kết quả.

  • 02/11 — Tác giả: Nguyen Van A; Thông điệp: Add sensor.c: read_temp() returns a raw sample
  • 03/11 — Tác giả: Nguyen Van A; Thông điệp: Add main.c: print each sample in Celsius
  • 04/11 — Tác giả: Tran Thi B; Thông điệp: Add moving average over the last 4 samples
  • 05/11 — Tác giả: Tran Thi B; Thông điệp: Add -f option to print Fahrenheit (gắn tag v0.1)
  • 09/11 — Tác giả: Nguyen Van A; Thông điệp: Speed up the average with a running sum
  • 10/11 — Tác giả: Tran Thi B; Thông điệp: Add README
  • 11/11 — Tác giả: Nguyen Van A; Thông điệp: Ignore the thermo binary

4.Tình huống 1: Chương trình chạy sai, code đã đổi gì?

Bản v0.1 in trung bình đúng. Hôm nay chạy bản mới nhất thì dòng đầu tiên in avg=5.0 thay vì 20.0. Việc đầu tiên là xem lịch sử, rồi so hai bản.

Ví dụ 4: Đọc lịch sử

bash
$ git log
commit 01f99f876c102d157d00bdec468f87840f63bc67
Author: Nguyen Van A <[email protected]>
Date:   Wed Nov 11 10:00:00 2026 +0700

    Ignore the thermo binary

commit 6fbbab339648be28aaf5596cdca9c29d476a96bc
Author: Tran Thi B <[email protected]>
Date:   Tue Nov 10 10:00:00 2026 +0700

    Add README

commit 209276dd99e6f95625bd933dd5ae6f1e97043bd9
Author: Nguyen Van A <[email protected]>
Date:   Mon Nov 9 10:00:00 2026 +0700

    Speed up the average with a running sum
    
    Do not re-add 4 samples on every read, keep a running sum instead.

commit 20743078de5d782d9e7ef945ede5a6e47e40c7c6
Author: Tran Thi B <[email protected]>
Date:   Thu Nov 5 10:00:00 2026 +0700

    Add -f option to print Fahrenheit
    
    Requested by the customer in ticket #31.
...

Mỗi khối là một commit với đủ tác giả, ngày giờ và thông điệp; kết quả được rút gọn từ khối thứ tư. Khi chỉ cần nhìn tổng thể, thêm tùy chọn để lọc và gọn hóa:

bash
$ git log --oneline
01f99f8 Ignore the thermo binary
6fbbab3 Add README
209276d Speed up the average with a running sum
2074307 Add -f option to print Fahrenheit
89a716c Add moving average over the last 4 samples
30dffa6 Add main.c: print each sample in Celsius
d3e3dd1 Add sensor.c: read_temp() returns a raw sample
$ git log --author="Tran Thi B --oneline
6fbbab3 Add README
2074307 Add -f option to print Fahrenheit
89a716c Add moving average over the last 4 samples
$ git log --since="2026-11-09" --format="%h %an %ad %s --date=short
01f99f8 Nguyen Van A 2026-11-11 Ignore the thermo binary
6fbbab3 Tran Thi B 2026-11-10 Add README
$ git log --stat -2
commit 01f99f876c102d157d00bdec468f87840f63bc67
Author: Nguyen Van A <[email protected]>
Date:   Wed Nov 11 10:00:00 2026 +0700

    Ignore the thermo binary

 .gitignore | 1 +
 1 file changed, 1 insertion(+)
...
  • `--oneline` — Cho biết: Mỗi commit một dòng
  • `--author="tên"` — Cho biết: Chỉ commit của một người
  • `--since`, `--until` — Cho biết: Chỉ commit trong khoảng thời gian
  • `--stat` — Cho biết: File nào đổi, bao nhiêu dòng
  • `--graph --all` — Cho biết: Vẽ cả nhánh (mục 6)

Ví dụ 5: So sánh hai phiên bản

Tag v0.1 là tên dễ nhớ cho bản chạy tốt. git diff v0.1 HEAD so bản đó với bản hiện tại.

bash
$ gcc -o thermo main.c sensor.c && ./thermo | head -3
t=20.0 avg=5.0
t=21.0 avg=10.2
t=22.0 avg=15.8
$ git switch -q --detach v0.1 && gcc -o thermo main.c sensor.c && ./thermo | head -3; git switch -q main
t=20.0 avg=20.0
t=21.0 avg=20.5
t=22.0 avg=21.0
$ git diff --stat v0.1 HEAD
 .gitignore |  1 +
 README.md  |  5 +++++
 main.c     | 10 +++++-----
 3 files changed, 11 insertions(+), 5 deletions(-)
$ git diff v0.1 HEAD -- main.c
diff --git a/main.c b/main.c
index 2ce0589..e4ed58c 100644
--- a/main.c
+++ b/main.c
@@ -11,15 +11,15 @@ static double conv(double c, int fahrenheit)
 
 int main(int argc, char *argv[])
 {
-	int i, n, sum;
+	int i, sum = 0;
 	int f = argc > 1 && !strcmp(argv[1], "-f");
 
 	for (i = 0; i < 6; i++) {
-		sum = 0;
-		for (n = 0; n < WIN && n <= i; n++)
-			sum += read_temp(i - n);
+		sum += read_temp(i);
+		if (i >= WIN)
+			sum -= read_temp(i - WIN);
 		printf("t=%.1f avg=%.1f\n", conv(read_temp(i) / 10.0, f),
-		       conv(sum / (double)n / 10.0, f));
+		       conv(sum / (double)WIN / 10.0, f));
 	}
 	return 0;
 }
  • Bản v0.1 in avg=20.0, bản mới nhất in avg=5.0, đúng như báo lỗi.
  • git switch --detach v0.1 đưa working directory về đúng phiên bản v0.1 để chạy thử (ở trạng thái "detached HEAD", chỉ để xem), git switch main quay lại.
  • Trong số 3 file khác nhau, chỉ main.c liên quan. Phần diff cho thấy tính trung bình đã đổi sang tổng trượt sum và luôn chia cho WIN (4), kể cả khi mới có 1 mẫu, nên giá trị đầu tiên là 20.0 chia 4.

5.Tình huống 2: Dòng này ai viết, để làm gì?

Biết lỗi nằm ở đoạn tính trung bình, bạn cần hỏi người viết lý do. git blame ghi bên cạnh mỗi dòng commit, tác giả và ngày sửa cuối cùng của dòng đó; git show đọc lý do trong commit.

Ví dụ 6: Dòng này ai viết, để làm gì?

bash
$ git blame -L 17,24 main.c
89a716ca (Tran Thi B   2026-11-04 10:00:00 +0700 17) 	for (i = 0; i < 6; i++) {
209276dd (Nguyen Van A 2026-11-09 10:00:00 +0700 18) 		sum += read_temp(i);
209276dd (Nguyen Van A 2026-11-09 10:00:00 +0700 19) 		if (i >= WIN)
209276dd (Nguyen Van A 2026-11-09 10:00:00 +0700 20) 			sum -= read_temp(i - WIN);
20743078 (Tran Thi B   2026-11-05 10:00:00 +0700 21) 		printf("t=%.1f avg=%.1f\n", conv(read_temp(i) / 10.0, f),
209276dd (Nguyen Van A 2026-11-09 10:00:00 +0700 22) 		       conv(sum / (double)WIN / 10.0, f));
89a716ca (Tran Thi B   2026-11-04 10:00:00 +0700 23) 	}
30dffa65 (Nguyen Van A 2026-11-03 10:00:00 +0700 24) 	return 0;
$ git show 209276d
commit 209276dd99e6f95625bd933dd5ae6f1e97043bd9
Author: Nguyen Van A <[email protected]>
Date:   Mon Nov 9 10:00:00 2026 +0700

    Speed up the average with a running sum
    
    Do not re-add 4 samples on every read, keep a running sum instead.

diff --git a/main.c b/main.c
...
$ git log -S"sum -=" --format="%h %an %ad %s --date=short
209276d Nguyen Van A 2026-11-09 Speed up the average with a running sum
  • Các dòng 18, 19, 20 và 22 đều do commit 209276dd của Nguyen Van A ngày 09/11, còn dòng 17, 21, 23 là của người khác ở những ngày khác. Mỗi dòng có thể thuộc một commit khác nhau.
  • git show 209276d cho thấy lý do ("Do not re-add 4 samples...") và đúng những dòng đã đổi. Bạn biết ngay ý định là tăng tốc, và hỏi người viết được đúng chỗ.
  • git log -S"sum -=" tìm commit đã thêm hoặc xóa đoạn mã đó, hữu ích khi bạn chỉ nhớ một đoạn mã mà không biết nó nằm ở đâu hay do ai viết.

6.Tình huống 3: Commit nào làm hỏng chương trình?

Ở ví dụ trước, ta đoán được commit nhờ đọc diff. Khi có hàng chục commit giữa bản tốt và bản xấu, đọc từng commit tốn thời gian. git bisect tìm kiếm nhị phân: bạn chỉ bản tốt và bản xấu, Git tự chuyển tới commit ở giữa, bạn (hoặc một script) cho biết tốt hay xấu, lặp lại cho tới khi tìm ra commit đầu tiên xấu. Với 1000 commit chỉ cần khoảng 10 lần thử.

Script kiểm tra check.sh đặt ngoài repository (vì các commit cũ chưa có nó): biên dịch, chạy và kiểm dòng đầu tiên phải là avg=20.0. Mã thoát 0 là tốt, 1 là xấu, 125 là bỏ qua commit không biên dịch được.

Ví dụ 7: Commit nào làm hỏng chương trình?

bash
$ cat ~/check.sh
#!/bin/sh
gcc -o thermo main.c sensor.c 2>/dev/null || exit 125
./thermo | head -1 | grep -q 'avg=20.0'
$ git bisect start HEAD v0.1
Bisecting: 0 revisions left to test after this (roughly 1 step)
[6fbbab339648be28aaf5596cdca9c29d476a96bc] Add README
$ git bisect run ~/check.sh
running '/work/home/check.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[209276dd99e6f95625bd933dd5ae6f1e97043bd9] Speed up the average with a running sum
running '/work/home/check.sh'
209276dd99e6f95625bd933dd5ae6f1e97043bd9 is the first bad commit
commit 209276dd99e6f95625bd933dd5ae6f1e97043bd9
Author: Nguyen Van A <[email protected]>
Date:   Mon Nov 9 10:00:00 2026 +0700

    Speed up the average with a running sum

 main.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
bisect found first bad commit
$ git bisect reset
Previous HEAD position was 209276d Speed up the average with a running sum
Switched to branch 'main'
  • git bisect start HEAD v0.1 khai báo bản xấu (HEAD) và bản tốt (v0.1).
  • git bisect run chạy script tại từng commit Git chọn; chỉ cần hai lần thử là tìm ra 209276d.
  • git bisect reset đưa bạn về nhánh ban đầu. Trong dự án lớn, script thường là make && ./run_tests.

7.Tình huống 4: Quay về bản cũ

Biết commit gây lỗi, có ba cách xử lý, tùy việc commit đã được chia sẻ hay chưa.

  • Chỉ cần xem hoặc lấy lại một file cũ — Lệnh: git show <bản>:<file>, git restore --source=<bản> <file>; Lịch sử: Không đổi
  • Hoàn tác commit đã push hoặc đã chia sẻ — Lệnh: git revert <commit>; Lịch sử: Thêm commit mới, không mất gì
  • Bỏ commit chưa chia sẻ — Lệnh: git reset; Lịch sử: Commit bị bỏ khỏi nhánh

Ví dụ 8: Xem file cũ và revert

bash
$ git show v0.1:main.c | sed -n 16,23p

	for (i = 0; i < 6; i++) {
		sum = 0;
		for (n = 0; n < WIN && n <= i; n++)
			sum += read_temp(i - n);
		printf("t=%.1f avg=%.1f\n", conv(read_temp(i) / 10.0, f),
		       conv(sum / (double)n / 10.0, f));
	}
$ git revert --no-edit 209276d
[main 693878d] Revert "Speed up the average with a running sum"
 1 file changed, 5 insertions(+), 5 deletions(-)
$ ~/check.sh && echo PASS
PASS
$ git log --oneline -3
693878d Revert "Speed up the average with a running sum"
01f99f8 Ignore the thermo binary
6fbbab3 Add README
  • git show v0.1:main.c in nội dung file ở bản v0.1 mà không đổi gì ở working directory.
  • git revert 209276d tạo commit mới làm đúng ngược lại commit lỗi. Commit lỗi vẫn nằm trong lịch sử, đồng nghiệp đã lấy về vẫn pull bình thường. Đây là cách an toàn cho mọi commit đã push.
  • Sau revert, check.sh cho PASS.

Ví dụ 9: Reset và reflog

Với commit mới chỉ nằm trên máy bạn, git reset bỏ nó khỏi nhánh. Ba chế độ khác nhau ở chỗ có kéo staging area và working directory theo hay không.

Minh họa Git, hình 3

Chế độ --hard làm mất thay đổi chưa commit, nhưng nếu nhỡ tay bỏ nhầm commit đã tạo thì git reflog cứu được. Mỗi lần HEAD di chuyển, Git ghi lại trong reflog (giữ khoảng 90 ngày).

bash
$ echo "int x;" >> main.c && git commit -qam "Oops, wrong file" && git reset --hard HEAD~1
HEAD is now at 693878d Revert "Speed up the average with a running sum"
$ git reflog -3
693878d HEAD@{0}: reset: moving to HEAD~1
d88fd47 HEAD@{1}: commit: Oops, wrong file
693878d HEAD@{2}: revert: Revert "Speed up the average with a running sum"
$ git reset --hard HEAD@{1} && git log --oneline -1
HEAD is now at d88fd47 Oops, wrong file
d88fd47 Oops, wrong file
  • Sau git reset --hard HEAD~1, git log không còn thấy commit Oops, wrong file.
  • git reflog vẫn ghi dòng commit: Oops, wrong file với mã d88fd47.
  • git reset --hard HEAD@{1} đưa nhánh về đúng commit đó, như chưa từng reset. (Bạn xóa commit "Oops" thật sự bằng git reset --hard HEAD~1 lần nữa.)

8.Tình huống 5: Thử tính năng mới mà không phá bản đang chạy

Bạn muốn thêm tùy chọn Kelvin nhưng main phải luôn chạy được. Hãy làm trên một nhánh. Nhánh trong Git chỉ là con trỏ nhỏ tới một commit, tạo nhánh gần như không tốn gì nên nên tạo nhánh cho mọi việc. HEAD là con trỏ tới nhánh bạn đang làm việc.

Minh họa Git, hình 4

Ví dụ 10: Merge và xung đột

Khi hai nhánh sửa cùng một dòng, Git không biết chọn bên nào và dừng lại để bạn quyết định. Ví dụ: nhánh feature/kelvin sửa dòng Usage trong README thành [-f|-k], còn main cùng lúc sửa dòng đó thành [-f] [-v].

bash
$ git switch -c feature/kelvin
Switched to a new branch 'feature/kelvin'
$ git commit -qam "Document the -k option" && git log --oneline --graph --all -3
* 633189c Document the -k option
* 693878d Revert "Speed up the average with a running sum"
* 01f99f8 Ignore the thermo binary
$ git switch main && sed -i "s/Usage: thermo \[-f\]/Usage: thermo [-f] [-v]/" README.md && git commit -qam "Document the -v option"
Switched to branch 'main'
$ git merge feature/kelvin
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
$ git diff
diff --cc README.md
index d160159,bc7d3bb..0000000
--- a/README.md
+++ b/README.md
@@@ -2,4 -2,4 +2,8 @@@
  
  Print the temperature and its moving average.
  
++<<<<<<< HEAD
 +Usage: thermo [-f] [-v]
++=======
+ Usage: thermo [-f|-k]
++>>>>>>> feature/kelvin

Phần từ dòng <<<<<<< HEAD tới dòng ngăn cách là bản của nhánh hiện tại, phần từ dòng ngăn cách tới dòng >>>>>>> feature/kelvin là bản của nhánh đang merge. Bạn sửa file thành kết quả mong muốn (xóa các dấu, giữ ý của cả hai bên), git add rồi git commit.

bash
$ cat README.md
# thermo

Print the temperature and its moving average.

Usage: thermo [-f] [-k] [-v]
$ git add README.md && git commit -qm "Merge feature/kelvin" && git log --oneline --graph -4
*   f6ebe50 Merge feature/kelvin
|\  
| * 633189c Document the -k option
* | d5dbc28 Document the -v option
|/  
* 693878d Revert "Speed up the average with a running sum"
  • git log --graph vẽ lịch sử: hai đường rẽ ra từ 693878d và gặp lại ở f6ebe50, commit merge có hai parent.
  • Merge có thể hủy giữa chừng bằng git merge --abort.
  • Chỉ commit khi file không còn dấu xung đột, và nên biên dịch thử trước.

9.Tình huống 6: Làm việc nhóm qua remote

Repository thật thường có một bản trung tâm trên máy chủ (GitHub, GitLab, máy chủ nội bộ), mỗi người giữ một bản sao. Để thực hành không cần tài khoản, bài dùng một bare repository trên cùng máy làm máy chủ giả; đó là kho chỉ chứa lịch sử, đúng như kho trên máy chủ.

Minh họa Git, hình 5

Ví dụ 11: Làm việc nhóm qua remote

bash
$ git init -q --bare remote.git
$ git remote add origin /work/remote.git && git push -u origin main --tags
$ git clone -q remote.git teammate
  • git remote add origin đặt tên cho kho từ xa (origin chỉ là quy ước); git push -u đẩy main lên và đặt origin/main làm nhánh theo dõi, từ đó git push, git pull không cần tham số.
  • Đồng nghiệp Tran Thi B lấy bản sao bằng git clone, commit và push trước bạn. Khi bạn push, Git từ chối vì remote có commit mà bạn chưa có:
bash
$ git push
To /work/remote.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '/work/remote.git'
$ git pull --rebase
From /work/remote
   f6ebe50..f5c69d5  main       -> origin/main
Rebasing (1/1)Successfully rebased and updated refs/heads/main.
$ git push
To /work/remote.git
   f5c69d5..a435b94  main -> main
$ git log --format="%h %an %s -3
a435b94 Nguyen Van A Comment main.c
f5c69d5 Tran Thi B Comment sensor.c
f6ebe50 Nguyen Van A Merge feature/kelvin
  • rejected ... (fetch first) nghĩa là bạn phải lấy commit mới về trước; Git không bao giờ cho ghi đè công việc của người khác.
  • git pull --rebase đặt commit của bạn lên trên commit mới của đồng nghiệp, lịch sử vẫn là đường thẳng. Dùng git pull (không --rebase) thì Git tạo thêm một merge commit.
  • git fetch chỉ tải commit về và cập nhật origin/main mà không đụng vào file của bạn, an toàn để xem người khác đã làm gì; git pull là fetch rồi ghép vào nhánh hiện tại.

Hình dưới so sánh hai cách ghép công việc của hai nhánh: merge giữ nguyên chỗ rẽ nhánh, rebase sao chép commit lên đầu nhánh kia để thành một đường thẳng.

Minh họa Git, hình 6

10.Tình huống 7: Dọn dẹp và phát hành

Ví dụ 12: Đánh dấu bản phát hành bằng tag

Bản v0.1 ở trên là một tag: tên cố định cho một commit, không di chuyển khi có commit mới. Dùng tag cho các bản phát hành và để hỏi "từ bản trước tới nay có gì mới".

bash
$ git tag -a v0.2 -m "Second release" && git tag
v0.1
v0.2
$ git log v0.1..v0.2 --oneline
a435b94 Comment main.c
f5c69d5 Comment sensor.c
f6ebe50 Merge feature/kelvin
d5dbc28 Document the -v option
633189c Document the -k option
693878d Revert "Speed up the average with a running sum"
01f99f8 Ignore the thermo binary
6fbbab3 Add README
209276d Speed up the average with a running sum

git log v0.1..v0.2 liệt kê mọi commit có trong v0.2 mà không có trong v0.1, chính là nội dung ghi chú phát hành. Tag không tự lên remote: dùng git push origin v0.2 hoặc git push --tags.

Ví dụ 13: Gộp commit nháp trước khi chia sẻ

Trong lúc làm việc bạn sinh ra nhiều commit nháp. Trước khi đưa lên, gộp chúng thành một commit có ý nghĩa bằng rebase tương tác.

bash
$ git switch -c feature/led
Switched to a new branch 'feature/led'
$ git log --oneline -4
fb11759 Fix LED comment again
5cab960 Fix LED comment
7f3d56a Show LED state in sensor.c
a435b94 Comment main.c
$ GIT_SEQUENCE_EDITOR="sed -i -e 2,3s/^pick/squash/" GIT_EDITOR=true git rebase -i HEAD~3
...
Successfully rebased and updated refs/heads/feature/led.
$ git log --oneline -2
407b571 Show LED state in sensor.c
a435b94 Comment main.c
  • git rebase -i HEAD~3 mở editor liệt kê ba commit gần nhất, mỗi dòng bắt đầu bằng pick. Bạn đổi pick của hai commit sau thành squash rồi lưu, Git gộp chúng vào commit đầu.
  • Ở đây hai biến GIT_SEQUENCE_EDITOR và GIT_EDITOR thay editor bằng lệnh làm sẵn việc đó, tiện cho script. Dùng thường chỉ cần gõ git rebase -i HEAD~3.
  • Ba commit nháp thành một commit 407b571. Các lệnh khác trong rebase tương tác: reword (sửa thông điệp), edit (dừng để sửa commit), drop (bỏ commit), fixup (như squash nhưng bỏ thông điệp).

Ví dụ 14: Cherry-pick: lấy đúng một commit

Có một bản sửa nhỏ làm trên nhánh khác, giờ cần đưa riêng nó vào main. git cherry-pick sao chép một commit sang nhánh hiện tại.

bash
$ git switch -q -c hotfix v0.1 && echo "/* raw unit: 0.1 C */" >> sensor.h && git commit -qam "Document the raw unit in sensor.h" && git log --oneline
d120ca4 Document the raw unit in sensor.h
2074307 Add -f option to print Fahrenheit
$ git switch main && git cherry-pick hotfix
Switched to branch 'main'
[main fc5747d] Document the raw unit in sensor.h
 1 file changed, 1 insertion(+)
$ git log --oneline -3
fc5747d Document the raw unit in sensor.h
a435b94 Comment main.c
f5c69d5 Comment sensor.c

Commit trên main là fc5747d, một bản sao của d120ca4 với mã băm khác vì parent khác. Cherry-pick hay dùng để đưa bản sửa lỗi vào nhánh phát hành cũ.

Ví dụ 15: Stash: cất việc dở dang

Đang sửa dở thì phải chuyển nhánh gấp. git stash cất thay đổi chưa commit và làm sạch working directory.

bash
$ git stash
Saved working directory and index state WIP on main: fc5747d Document the raw unit in sensor.h
$ git status -s
$ git stash pop
...
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (b6287622cb3fb99a56f7d03a69a45d9f129ade67)

Sau git stash, git status -s không in gì, bạn chuyển nhánh thoải mái. git stash pop lấy thay đổi ra và xóa khỏi danh sách cất; git stash apply lấy ra nhưng giữ lại.

Ví dụ 16: Hook: chặn commit xấu

Hook là script trong .git/hooks mà Git tự chạy ở các sự kiện như trước khi commit (pre-commit). Hook thoát với mã khác 0 thì thao tác bị hủy. Hook dưới đây từ chối commit nếu code không biên dịch được.

bash
$ cat .git/hooks/pre-commit
#!/bin/sh
if ! gcc -fsyntax-only main.c sensor.c 2>/dev/null; then
    echo "pre-commit: main.c does not compile, commit rejected"
    exit 1
fi
$ echo "int broken(" >> main.c
$ git commit -am "Broken change"
pre-commit: main.c does not compile, commit rejected
  • Hook phải có quyền thực thi (chmod +x).
  • git commit --no-verify bỏ qua hook khi bạn chắc chắn cần commit.
  • Thư mục .git không lên remote nên hook không tự tới máy đồng nghiệp; nhóm thường lưu hook trong repository và cài bằng git config core.hooksPath.

11.Bài thực hành tổng hợp

Tạo repository calc mới và làm lần lượt; mỗi bước kiểm tra bằng git log --oneline --graph --all.

  1. Tạo calc.c và .gitignore (bỏ qua file chạy được), commit hai lần với thông điệp ngắn, rõ nghĩa.
  2. Thêm hàm cộng, hàm trừ, hàm nhân thành ba commit riêng. Dùng git log --stat và git show kể lại điều từng commit đã làm.
  3. Cố ý làm hỏng hàm trừ ở một commit giữa chừng. Dùng git bisect run với một script kiểm tra để tìm commit đó, rồi git revert nó.
  4. Tạo nhánh feature/div thêm hàm chia, trong khi main sửa cùng dòng kết thúc của file. Merge và giải quyết xung đột.
  5. Tạo bare repository, thêm làm origin, push, clone sang thư mục thứ hai đóng vai đồng nghiệp. Cho hai bên cùng commit, thấy push bị từ chối và xử lý bằng git pull --rebase.
  6. Tạo ba commit nháp, gộp thành một bằng git rebase -i. Cố ý git reset --hard bỏ một commit rồi tìm lại bằng git reflog. Cuối cùng đặt tag v1.0.

12.Kết luận

  • Git là công cụ quản lý phiên bản: mỗi commit là một phiên bản ghi lại ai, khi nào, vì sao và thay đổi gì, nên thông điệp commit viết tốt là phần quan trọng nhất.
  • Khi có lỗi, đọc lịch sử bằng git log, so sánh bằng git diff, hỏi người viết bằng git blame và git show, khoanh vùng bằng git bisect.
  • Quay về bản cũ bằng git revert nếu commit đã chia sẻ, bằng git reset nếu chưa, và git reflog cứu hầu hết nhầm lẫn.
  • Nhánh cho phép thử việc mới mà không phá bản đang chạy; làm việc nhóm qua remote bằng push, fetch, pull, và chỉ viết lại lịch sử chưa chia sẻ.
  • Tag đánh dấu bản phát hành, stash cất việc dở, cherry-pick lấy một commit, hook chặn commit xấu.