Làm web (11) - Git: tạo nhánh

Bài học trước: Làm web (10) - Git: đánh dấu
-----

3.8        Phân nhánh

3.8.1     Nhánh là gì?


Ở các phần trước, bạn đã biết cách lấy một kho chứa từ trên mạng về, thực hiện thêm thông tin vào kho chứa (ví dụ lập trình thêm một chức năng cho dự án), sau đó đẩy ngược lên mạng để chia sẻ cho các thành viên còn lại.

Để cập nhật kết quả mới nhất của các thành viên khác, bạn sẽ sử dụng lệnh $ git pull để kéo các thông tin mới từ remote repo về, trộn vào local repo. Cách làm này sẽ gặp trục trặc khi mã nguồn của thành viên khác làm cho chương trình của bạn bị lỗi. Hoặc đoạn mã của bạn có lỗi mà bạn lại đẩy (push) lên remote repo. Nói chung là có khá nhiều hạn chế nếu tất cả các thành viên cùng lập trình trên nhánh master.

Phần này sẽ giới thiệu một giải pháp, giúp bạn khắc phục được những nhược điểm trên, để quá trình làm dự án được hiệu quả hơn. Đó là phân nhánh trong Git.

Đọc tài liệu tiếng Anh:


Định nghĩa phân nhánh: branching means you diverge from the main line of development and continue to do work without messing with that main line.

Phân nhánh (branching) là việc tạo ra một nhánh mới từ luồng công việc chính, nhờ vậy các công việc được thực hiện độc lập trên nhánh mới mà không làm ảnh hưởng đến luồng chính.

Trong Git việc tạo nhánh không mất nhiều thời gian và có rất nhiều ưu điểm trong quá trình sử dụng. Vì vậy, Git khuyến khích các lập trình viên thường xuyên tạo và trộn nhánh trong quá trình phát triển dự án.

Để hiểu về phân nhánh, cần tìm hiểu kĩ hơn về việc tổ chức lưu trữ dữ liệu của Git. Như đã đề cập ở các phần trước, Git không lưu trữ chuỗi các sự thay đổi mà nó lưu trữ chuỗi các ảnh của dự án (các snapshot). Mỗi khi commit (lưu lại trạng thái hiện tại của dự án trong Git), Git sẽ “chụp một ảnh” ghi lại nội dung của tất cả tập tin tại thời điểm đó và tạo một tham chiếu tới “ảnh” vừa chụp. Những tập tin không có thay đổi sẽ không cần “chụp ảnh”, mà chỉ cần tạo tham chiếu tới tập tin gốc (để tiết kiệm không gian lưu trữ). Xem hình minh họa,



Khi thực hiện commit, Git sẽ tạo ra một “đối tượng commit”, lưu trong kho chứa Git, gồm các thông tin sau:

– Một con trỏ, trỏ tới snapshot chứa các nội dung (tập tin, thư mục)  mà bạn đã stage

– Tên của tác giả (author), email, và thông điệp đi kèm (message)

– Có thể có: không, một hoặc nhiều các con trỏ, trỏ tới commit cha của commit đó. Commit lần đầu không có cha, commit bình thường có một commit cha, các commit được tạo ra từ việc tích hợp (merge) hai hay nhiều nhánh sẽ có nhiều commit cha.

Để dễ hiểu, giả sử bạn có một kho chứa gồm ba tập tin (README, test.rb và LICENSE), khi bạn thực hiện stage ba tập tin,

$ git add README test.rb LICENSE

Git sẽ thực hiện việc “băm” từng tập tin để tạo ra mã SHA-1, đây là tên mới của tập tin, được sử dụng để thao tác trong Git, Git không quan tâm tới tên thật của mỗi tập tin. Phiên bản (nội dung) hiện tại của tập tin được lưu vào kho chứa của Git, mã SHA-1 được thêm vào khu vực stage. Git gọi mỗi phiên bản của một tập tin trong kho chứa là một blob.

[Blob is an abbreviation for “binary large object”. When we git add a file such as example_file.txt , git creates a blob object containing the contents of the file. Blobs are therefore the git object type for storing files.]

Khi chạy lệnh commit,

$ git commit -m 'initial commit of my project'

Git sẽ “băm” tất cả các thư mục của dự án, mỗi thư mục tạo thành một đối tượng, các đối tượng này được đặt tên theo mã SHA-1, các đối tượng được tổ chức theo dạng cây, thư mục gốc tương đương với nút gốc của cây. Cây này được lưu trong kho chứa của Git. Trong “đối tượng commit” có một con trỏ, trỏ tới nút gốc của cây, nhờ vậy Git có thể dễ dàng khôi phục được các “ảnh” của dự án khi cần.

Lúc này kho chứa Git sẽ chứa năm đối tượng sau (xem hình minh họa),



Năm đối tượng đó là: ba blob (5b1d3, 911eb, cba0a) chứa nội dung của ba tập tin; một đối tượng chứa thông tin về cây thư mục (92ec2); một “đối tượng commit” (98ca9) chứa con trỏ, trỏ tới nút gốc của cây thư mục, và các thông tin về tác giả (author), người commit (committer), thông điệp (message).

Có thể hiểu nôm na, một ảnh (snapshot) của dự án sẽ gồm cây thư mục và các blob tương ứng.
Sau khi thực hiện thay đổi trên dự án, rồi commit, thì commit sau sẽ có một con trỏ, trỏ tới commit trước đó (commit cha). Sau hai lần commit, lịch sử của dự án sẽ có dạng như sau, (commit đầu tiên có SHA-1 là 98ca9, lần thứ hai là 34ac2, lần thứ ba là f30ab)



Lab 27. Khảo sát một số đối tượng (commit, tree và blob) trong git.

– Tạo một kho chứa trên máy cục bộ (ví dụ tên là example_objects)

– Tạo một tập tin (example_file.txt) trong kho chứa, thực hiện add và commit

– Mở thư mục .git/objects sẽ thấy các đối tượng của git được tạo ra, để ý ba đối tượng đầu tiên (ví dụ: 3d, b2, e6).

– Sử dụng lệnh $ git cat-file –t <mã SHA-1> để kiểm tra xem loại của ba đối tượng đầu tiên? (kiểm tra xem là: commit, tree, hay blob). Ví dụ:

$ git cat-file -t 3d67701726ea1086341586101f272b9a029c9510
commit

(nhớ nối thêm 2 kí tự đầu tiên của thư mục vào mã SHA-1, ở ví dụ trên đã thực hiện nối “3d” vào “67701726ea1086341586101f272b9a029c9510”).

– Sử dụng lệnh $ git cat-file –p <mã SHA-1> để xem nội dung của các tập tin đi kèm với mã SHA-1. Mã SHA-1 của tập tin example_file.txt trên máy của bạn là gì?
Ví dụ:
$ git cat-file -p b25d4c8f7e67444504c50e428d23d981bae9f087
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    example_file.txt

– Thêm nội dung cho tập tin example_file.txt, thực hiện commit, sử dụng $git cat-file –p để xem lại nội dung của tập tin example_file.txt.

Tham khảo: https://matthew-brett.github.io/curious-git/git_object_types.html

Trong Git, nhánh là một con trỏ có thể di chuyển được, nó trỏ tới một trong các commit ở trên. Khi tạo một kho chứa, và thực hiện commit lần đầu là bạn đã tạo ra nhánh mặc định có tên là master, đây là nhánh bình thường như các nhánh sẽ tạo sau này, nhánh master sẽ trỏ tới commit bạn vừa thực hiện. Mỗi khi bạn thực hiện một commit, con trỏ của nhánh master sẽ tự động di chuyển về phía trước.

Hình dưới đây là nhánh master và lịch sử commit của nó, (v1.0 là con trỏ của tag, master là con trỏ nhánh, HEAD là con trỏ cho biết nhánh hiện thời mà bạn đang làm việc)



3.8.2     Tạo nhánh mới


Tạo nhánh mới chính là việc tạo ra một con trỏ mới, trỏ tới commit hiện tại và đặt cho con trỏ mới một cái tên. Để tạo nhánh mới sử dụng lệnh $ git branch <tên nhánh>.

Ví dụ, tạo nhánh mới có tên là testing,

$ git branch testing

Khi đó, kho chứa sẽ có hai nhánh (master và testing), cùng trỏ tới commit mới nhất (f30ab), và có cùng lịch sử commit. Xem hình minh họa,



Tại một thời điểm, kho chứa có thể có rất nhiều nhánh. Để biết được hiện thời bạn đang ở nhánh nào? Git sử dụng thêm một con trỏ khác có tên là HEAD. HEAD là một con trỏ, nó trỏ đến nhánh hiện hành trên kho chứa cục bộ (nhánh bạn đang làm việc). Khi tạo nhánh mới con trỏ HEAD không tự trỏ tới nhánh vừa tạo, mà nó vẫn nằm trên nhánh hiện thời, người dùng cần phải thực hiện lệnh chuyển nhánh một cách tường minh.

Xem hình minh họa về con trỏ HEAD,



Để biết được con trỏ HEAD đang nằm trên nhánh nào? sử dụng lệnh $ git log với tham số --decorate

Ví dụ, con trỏ HEAD đang trỏ tới nhánh master,

$ git log --oneline --decorate
a89362e (HEAD -> master, tag: v1.4-lw, tag: v1.3, tag: v1.2, tag: v1.1, origin/master, origin/HEAD, testing, phienban1) Update testdesktop.txt
c2aa729 Add files via upload
a31e6f1 conglg them mot tap tin
abaf964 commit lan 1 amend
6b49bd0 add noi dung 83_git commit amend thêm m?i
9a99d67 them mot tap tin 8thang3
0cf8354 (tag: v1.4) doi ten tap tin
a6c7b42 doi ten

3.8.3     Chuyển nhánh


Để chuyển qua một nhánh khác (nhánh có tồn tại), cũng có nghĩa là cho con trỏ HEAD trỏ vào nhánh khác, sử dụng lệnh $ git checkout <tên nhánh>

Ví dụ, chuyển sang nhánh testing và kiểm tra bằng lệnh $ git log --decorate

$ git checkout testing
Switched to branch 'testing'

$ git log --oneline --decorate
a89362e (HEAD -> testing, tag: v1.4-lw, tag: v1.3, tag: v1.2, tag: v1.1, origin/master, origin/HEAD, phienban1, master) Update testdesktop.txt
c2aa729 Add files via upload
a31e6f1 conglg them mot tap tin
abaf964 commit lan 1 amend
6b49bd0 add noi dung 83_git commit amend thêm m?i
9a99d67 them mot tap tin 8thang3
0cf8354 (tag: v1.4) doi ten tap tin

Xem hình minh họa,



Việc chuyển nhánh này có ý nghĩ gì? hãy thay đổi nội dung của một tập tin, rồi thực hiện commit.

$ git add *

$ git commit -m "test chuyen nhanh"
[testing cd69866] test chuyen nhanh
 1 file changed, 1 insertion(+)

$ git log --oneline --decorate
cd69866 (HEAD -> testing) test chuyen nhanh
a89362e (tag: v1.4-lw, tag: v1.3, tag: v1.2, tag: v1.1, origin/master, origin/HEAD, phienban1, master) Update testdesktop.txt
c2aa729 Add files via upload

Sau lệnh commit, nhánh hiện thời (con trỏ HEAD đang trỏ tới testing) sẽ tiến về phía trước một bước.
Xem hình minh họa,



Để ý sẽ thấy, sau khi commit, nhánh testing tiến về phía một bước, trong khi nhánh master vẫn trỏ tới commit tại thời điểm bạn thực hiện lệnh $ git checkout để chuyển nhánh.

Giờ sẽ chuyển sang nhánh master,

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

Sau lệnh $ git checkout master, Git sẽ thực hiện hai việc: một là chuyển con trỏ HEAD quay trở lại nhánh master, hai là phục hồi (revert) các tập tin trong thư mục làm việc trở về thời điểm mà con trỏ master đang trỏ tới (phục hồi snapshot). Điều này cũng có nghĩa là các thay đổi bạn thực hiện từ thời điểm này sẽ được tách ra theo một luồng khác (so với nhánh testing), các thay đổi khi bạn thực hiện trên nhánh testing đã bị “tua lại” để bạn có thể phát triển dự án theo một hướng khác.

Xem hình minh họa,



Chú ý: thao tác chuyển nhánh sẽ làm thay đổi các tập tin tại thư mục làm việc trong kho chứa. Nếu bạn thực hiện chuyển qua các nhánh cũ hơn, thư mục làm việc cũng được phục hồi lại trạng thái tại thời điểm mà bạn thực hiện commit cuối cùng trên nhánh. Nếu Git không thực hiện được việc phục hồi lại trạng thái các tập tin này, cũng có nghĩa là việc chuyển nhánh sẽ không được thực hiện.

Giờ sẽ tạo một số thay đổi trong các tập tin và commit thêm một lần nữa,

$ git add *
Maxsys@DESKTOP-7LPDOL6 MINGW64 /e/langbiang (master)

$ git commit -m "thay doi tren master branch"
[master 47ed23e] thay doi tren master branch
 1 file changed, 1 insertion(+)
$ git log --oneline --decorate
47ed23e (HEAD -> master) thay doi tren master branch
a89362e (tag: v1.4-lw, tag: v1.3, tag: v1.2, tag: v1.1, origin/master, origin/HEAD, phienban1) Update testdesktop.txt
c2aa729 Add files via upload

Bây giờ lịch sử của dự án đã được tách ra thành hai nhánh riêng biệt là master và testing.

Xem hình minh họa,



Tới thời điểm này, bạn đã thực hiện việc tạo và chuyển sang một nhánh mới (testing), thực hiện thay đổi trên dự án, commit, rồi sau đó chuyển ngược lại nhánh chính (master), cũng thực hiện thay đổi và commit trên nhánh master. Sự thay đổi trên tập tin và commit đều được thực hiện một cách biệt lập trên hai nhánh riêng biệt. Trong quá trình làm việc, bạn có thể di chuyển qua lại giữa các nhánh để làm việc và tích hợp các nhánh lại khi cần thiết.

Có thể sử dụng lệnh $ git log --oneline --decorate --graph --all để xem lịch sử các commit, các nhánh và lịch sử chia nhánh.

Ví dụ,

* 47ed23e (HEAD -> master) thay doi tren master branch
| * cd69866 (testing) test chuyen nhanh
|/
* a89362e (tag: v1.4-lw, tag: v1.3, tag: v1.2, tag: v1.1, origin/master, origin/HEAD, phienban1) Update testdesktop.txt
* c2aa729 Add files via upload
* a31e6f1 conglg them mot tap tin
* abaf964 commit lan 1 amend
* 6b49bd0 add noi dung 83_git commit amend thêm m?i
* 9a99d67 them mot tap tin 8thang3
* 0cf8354 (tag: v1.4) doi ten tap tin

Một nhánh trong Git là một tập tin chứa 40 kí tự, đó là giá trị SHA-1 của commit mà nhánh đó trỏ tới. Do vậy, việc tạo và hủy nhánh được thực hiện rất nhanh chóng, chỉ là thao tác ghi/xóa vào tập tin 41 kí tự (40 kí tự của SHA-1 và một dòng mới).

Lab 28. Thực hiện các yêu cầu sau về tạo nhánh.

– Tạo một local repo với ba tập tin bất kì

– Thực hiện thay đổi tên các tập tin và commit ba lần với các message khác nhau

– Tạo nhánh mới với tên bất kì (ví dụ testing)

– Chuyển sang nhánh testing, thực hiện thay đổi tập tin trong local repo, thực hiện commit một lần

– Chuyển lại về nhánh master, thực hiện thay đổi tập tin trong local repo, thực hiện commit một lần

– Điền giá trị SHA-1 của mỗi commit vào bảng sau:


SHA-1 của commit lần 4 trên nhánh master
SHA-1 của commit lần 1
SHA-1 của commit lần 2
SHA-1 của commit lần 3


SHA-1 của commit lần 1 trên nhánh testing

 </////11
-----------
Cập nhật [27/05/2020]
-----------
Xem thêm: Tổng hợp các bài viết về Làm web
Xem thêm: Làm web (12) - Git: phân nhánh căn bản