Sách “Pro Git 2” – Chương 3

Scott Chacon – Ben Straub – Dịch bởi http://elinux.vn

Làm việc với Nhánh trên Git

Gần như mọi VCS đều hỗ trợ chức năng Nhánh. Nhánh nghĩa là tạo một nhánh mới từ nhánh phát triển chính và tiếp tục làm việc không ảnh hưởng tới nhánh chính. Trong nhiều công cụ VCS, quá trình tạo nhánh mới khá phức tạp, thường yêu cầu tạo một bản sao mới của thư mục mã nguồn, điều đó rất mất thời gian đối với các Dự án lớn.

Vài người nói tới mô hình nhánh của Git là Đặc trưng đặc biệt, và chính nó làm cho Git trở thành một phần của cộng đồng VCS. Vậy tại sao nó lại đặc biệt? Cách mà Git tạo một nhánh mới rất dễ dàng, nó gần như đồng thời, quá trình chuyển từ nhánh nọ sang nhánh kia rất nhanh. Không giống như nhiều VCS khác, Git khuyến kích áp dụng Quy trình làm việcmà sử dụng phân nhánh và trộn nhánh nhiều, thậm chí nhiều lần trong ngày. Hiểu và làm chủ đặc trưng này bạn sẽ có trong tay công cụ mạnh và chỉ có ở Git và có thể hoàn toàn thay đổi cách bạn làm việc.

Hiểu về Nhánh

Để thực sự hiểu cách Git làm việc với nhánh, ta cần nhớ lại cách Git lưu dữ liệu của nó.

Như trong ch01-getting-started.html đã trình bày, Git không lưu dữ liệu là một chuỗi sự khác nhau mà thay vào đó nó lưu snapshots.

Khi bạn commit, Git lưu đối tượng, đối tượng này gồm: con trỏ tới snapshot của nội dung mà đã được staged, tên tác giả, địa chỉ email, thông điệp bạn đã nhập, con trỏ tới commit ngay trước.

Để minh họa điều này, ví dụ bạn có một thư mục chứa 3 tệp, và bạn stage tất cả chúng và commit. Đầu tiên Git sẽ tính toán checksum cho mỗi tệp (chính là giá trị băm SHA-1 như đã trình bày ở ch01-getting-started.html). Sau đó, Git lưu nội dung của tệp vào trong Kho chứa Git (nội dung của tệp trong Git được gọi là blobs). Khi này, giá trị SHA-1 cho mỗi tệp chính là con trỏ tới blobs tương ứng của tệp đó.

$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'

Khi bạn tạo một commit bằng lệnh git commit, Git sẽ tính SHA-1 cho mọi thư mục con (trong ví dụ này, không có thư mục con), giá trị SHA-1 này bây giờ sẽ là con trỏ để tham chiếu tới thư mục đó (thay cho tên thư mục). Cuối cùng, Git tạo một đối tượng chứa metadata và một con trỏ tới thư mục gốc để nó có thể tạo lại snapshot đó khi cần.

Bây giờ, trong Kho chứa có 5 đối tượng: 3 cái chính là 3 blob, một cái liệt kê nội dung của thư mục và chỉ rõ là tệp nào tương ứng với blob nào, cái cuối cùng là con trỏ tới thư mục gốc và tất cả metadata.

Một commit và cây dữ liệu của nó.

Figure 1. Một commit và cây dữ liệu của nó

Nếu bạn tiếp tục làm việc (thực hiện những thay đổi) và rồi commit, thì commit tiếp theo sẽ lưu con trỏ tới commit ngay trước nó.

Commit và commit trước nó.

Figure 2. Commit và commit trước nó

Một nhánh của Git chính là một chuỗi cái commit liên tiếp nhau. Tên nhánh mặc định trong Git là master. Khi bạn thực hiện commit đầu tiên, Git tự động đặt tên đó là nhánh master. Mỗi lần bạn thực hiện commit thì con trỏ của nhánh sẽ tự động di chuyển tới commit mới nhất.

Nhánh “master” trong Git không phải là một nhánh đặc biệt. Chính xác là nó cũng giống hệt như bất kì một nhánh nào khác. Lí do duy nhất là Git mặc định đặt tên nhánh là master khi bắt đầu khởi tạo Kho chứa bằng lệnh git init, và hầu hết mọi người không có nhu cầu đổi tên nó.
Nhánh và lịch sử commit của nó.

Figure 3. Nhánh và lịch sử commit của nó

Tạo một nhánh mới

Điều gì xảy ra nếu bạn tạo một nhánh mới? Giả sử, bạn muốn tạo một nhánh tên là testing. Tạo nhánh mới sử dụng lệnh git branch:

$ git branch testing

Lệnh này sẽ tạo một con trỏ mới ở vị trí commit mà bạn đang ở đó.

Hai nhánh cùng trỏ tới commit giống nhau.

Figure 4. Hai nhánh cùng trỏ tới commit giống nhau

Bây giờ có 02 nhánh: master và testing. Vậy, làm thế nào để Git biết bạn đang làm việc trên nhánh nào? Git có một con trỏ đặc biệt tên là HEAD (HEAD ở Git khác nhiều so với các VCS khác). Trong Git, đây là con trỏ tới nhánh địa phương mà bạn đang làm việc trên đó. Trong ví dụ này, bạn vẫn đang ở trên nhánh master. Lệnh git branch chỉ tạo một nhánh mới chứ nó không chuyển bạn tới nhánh đó.

HEAD trỏ tới một nhánh.

Figure 5. HEAD trỏ tới một nhánh

Bạn có thể dễ dàng thấy điều này bằng sử dụng lệnh git log, lệnh này chỉ cho bạn rằng HEAD đang trỏ tới đâu. Cờ lệnh này tên là --decorate.

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) add feature #32 - ability to add new formats to the central interface
34ac2 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project

Bạn có thể thấy rằng nhánh “master” and “testing” đang trỏ tới f30ab commit.

Chuyển nhánh

Để chuyển sang một nhánh đã tồn tại, bạn dùng lệnh git checkout. Ví dụ, chuyển sang nhánh testing:

$ git checkout testing

Lệnh này sẽ di chuyển HEAD tới nhánh testing.

HEAD trỏ tới nhánh hiện tại.

Figure 6. HEAD trỏ tới nhánh hiện tại

Ý nghĩa của nó là gì? Nào, tiếp tục commit tiếp theo:

$ vim test.rb
$ git commit -a -m 'made a change'
HEAD di chuyển theo commit mới.

Figure 7. HEAD di chuyển theo commit mới

Điều này thật thú vị, bởi vì bây giờ ta thấy rằng nhánh testing đã di chuyển tới commit tiếp theo, nhưng nhánh mastervẫn trỏ tới commit ở thời điểm bạn chạy lệnh git checkout. Bây giờ, chuyển lại nhánh `master:

$ git checkout master
HEAD di chuyển khi bạn checkout.

Figure 8. HEAD di chuyển khi bạn checkout

Lệnh này làm 2 việc. Nó di chuyển con trỏ HEAD quay lại trỏ tới nhánh master và nó đưa toàn bộ tệp trong Thư mục làm việc về trạng thái snapshot mà master đang trỏ. Điều này có nghĩa là những thay đổi bạn vừa làm sẽ không thấy nữa. Nghĩa là, Git đã bọc lại những công việc bạn đã làm ở nhánh testing, từ đó bạn có thể thực hiện các công việc khác bắt đầu từ thời điểm mà nhánh master đang trỏ.

Chuyển nhánh sẽ thay đổi nội dung của Thư mục làm việcĐiều quan trọng ta nhớ rằng chuyển nhánh làm việc trong Git thì Thư mục làm việc sẽ thay đổi. Nếu bạn chuyển tới nhánh cũ hơn, thì Thư mục làm việc sẽ quay trở lại trạng thái ở vị trí commit cuối cùng nhánh đó. Nếu vì lí do nào đó Git không thể làm như vậy thì nó sẽ không cho phép chuyển nhánh.

Nào, giờ thay đổi một chút và tiếp tục commit:

$ vim test.rb
$ git commit -a -m 'made other changes'

Bây giờ lịch sử dự án của bạn đã phân tách (xem Lịch sử bị phân kỳ). Bạn đã tạo và chuyển tới nhánh khác, làm việc trên nhánh đó, và sau đó quay trở lại nhánh chính để làm công việc khác. Công việc làm trên các nhánh khác nhau được lưu riêng rẽ: bạn có thể di chuyển giữa các nhánh khác nhau để làm việc hoặc có thể trộn 2 nhánh với nhau khi bạn thấy đã sẵn sàng. Bạn làm tất cả những công việc đó với các lệnh branchcheckout, và commit.

Lịch sử bị phân kỳ.

Figure 9. Lịch sử bị phân kỳ

Bạn có thể dễ dàng thấy điều này bằng lệnh git log. Nếu bạn dùng lệnh git log --oneline --decorate --graph --all, lệnh này sẽ in ra lịch sử của các commit, cho ta biết con trỏ nhánh đang ở đâu và lịch sử dự án rẽ nhánh như thế nào.

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

Vì một nhánh trong Git thực tế là một tệp chứa 40 kí tự, đây chính là giá trị SHA-1 của commit mà nó trỏ tới. Nên việc tạo và xóa bỏ nhánh rất dễ dàng. Quá trình tạo một nhánh mới nhánh và đơn giản giống như việc ghi 41bytes tới một tệp (40 kí tự SHA-1 và 1 kí tự xuống dòng).

Điều này ngược hẳn với hầu hết VCS khác, nơi mà khi tạo nhánh mới nó sao chép tất cả dự án vào trong một thư mục thứ hai. Quá trình như vậy có thể mất vài giây, thậm chí hàng phút phụ thuộc vào độ lớn của dự án. Không như Git, quá trình này gần như là ngay lập tức. Ngoài ra, vì Git ghi lại con trỏ tới commit ngay trước nó, điều này giúp cho việc tìm commit chung của 2 nhánh dễ dàng và tự động được thực hiện. Đặc trưng này làm cho người dùng không ngần ngại gì trong việc tạo và sử dụng nhánh thường xuyên.

Nào cùng xem tại sao bạn nên làm vậy.

Cơ bản về Phân nhánh (Branching) và Trộn nhánh (merging)

Xem xét một ví dụ đơn giản về Phân nhánh và Trộn nhánh bằng một Quy trình làm việc mà bạn có thể sử dụng trong thực tế. Quy trình làm việc như sau:

  1. Làm một vài việc trên một trang web.
  2. Tạo một nhánh cho công việc mới mà bạn bắt đầu làm việc.
  3. Làm một vài việc trên nhánh đó.

Đến giai đoạn này, bạn sẽ nhận được một cuộc gọi nói rằng có một vấn đề khác nghiêm trọng và bạn cần khắc phục. Bạn sẽ làm như sau:

  1. Quay lại nhánh gốc.
  2. Tạo một nhánh (gọi là hotfix) để thực hiện khắc phục vấn đề nghiêm trọng kia.
  3. Sau khi đã kiểm tra cách khắc phục thành công, bạn trộn nhánh hotfix tới nhánh gốc.
  4. Sau đó, quay trở lại nhánh mà bạn đang làm việc dở lúc nãy.

Cơ bản về Nhánh

Đầu tiên, cho rằng bạn đang làm việc trên Dự án của mình và dự án đó có hai commit đã ở trên nhánh master.

Một lịch sử commit đơn giản.

Figure 10. Một lịch sử commit đơn giản

Bạn quyết định rằng mình sẽ làm việc về issue #53 (một vấn đề bất kì trong rất nhiều vấn đề mà công ty bạn đang cần giải quyết). Để tạo một nhánh mới và chuyển ngay tới nhánh đó để làm việc, bạn dùng lệnh git checkout cùng với cờ -b:

$ git checkout -b iss53
Switched to a new branch "iss53"

Lệnh trên thực hiện công việc tương đương 2 lệnh sau:

$ git branch iss53
$ git checkout iss53
Tạo một con trỏ nhánh mới.

Figure 11. Tạo một con trỏ nhánh mới

Bạn làm việc trên nhánh iss53 và đã làm một vài commits. Quá trình commit, con trỏ nhánh iss53 sẽ di chuyển theo, và con trỏ HEAD cũng vậy (vì bạn đã checkout):

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
Con trỏ `iss53` di chuyển theo các commit.

Figure 12. Con trỏ iss53 di chuyển theo các commit

Bây giờ bạn nhận được cuộc gọi báo rằng có một vấn đề với trang web và bạn cần khắc phục nó ngay lập tức, vấn đề nghiêm trọng này gọi là hotfix. Với Git, bạn không phải phát hành giải pháp cho hotfix có kèm những công việc bạn đã làm ở nhánh iss53, và bạn cũng không mất nhiều thời gian để quay trở về trạng thái giống như bắt đầu chưa làm công việc gì về iss53. Bạn chỉ cần chuyển quay lại nhánh master là đủ.

Mặc dù vậy, nhớ rằng nếu Thư mục làm việc hoặc vùng staging area vẫn chưa sạch sẽ (tức là, còn tệp nào ở trạng thái modified hoặc staged) thì Git sẽ không cho phép bạn chuyển nhánh. Tốt nhất trước khi chuyển nhánh thì nhánh đó đang phải ở trạng thái sạch sẽ. Có vài cách để giải quyết vấn đề này (đó là, stashing và commit amending), kiến thức sẽ được trình bày phía sau tại ch07-git-tools.html. Bây giờ, ta cho rằng nhánh đang làm việc ở trạng thái sạch sẽ, vì vậy có thể quay lại nhánh master:

$ git checkout master
Switched to branch 'master'

Bây giờ, Thư mục làm việc quay về trạng thái đúng như trước khi bạn bắt đầu công việc về issue #53, và giờ bạn có thể tập trung vào hotfix. Như vậy, đến đây có một điểm quan trọng cần nhớ là: khi bạn chuyển nhánh, Git sẽ đưa Thư mục làm việc về trạng thái ở thời điểm commit lần cuối trên nhánh đó. Có nghĩa là bây giờ thư mục Thư mục làm việc đang ở commit cuối cùng trên nhánh master.

Bây giờ, tập chung giải quyết vấn đề hotfix, tạo một nhánh tên là hotfix để làm việc cho tới khi công việc này hòan thành:

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'

[hotfix 1fb7853]

fixed the broken email address 1 file changed, 2 insertions(+)

Nhánh Hotfix rẽ nhánh từ nhánh `master`.

Figure 13. Nhánh Hotfix rẽ nhánh từ nhánh master

Khi đã chắc chắn hotfix được giải quyết. Bạn trộn nhánh hotfix vào nhánh master để phát hành sản phẩm. Bạn làm điều này bằng lệnh git merge:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

Nhìn thông tin xuất ra khi trộn, ta thấy rằng, có một giai đoạn gọi là “fast-forward” diễn ra trong khi trộn. Điều này bởi vì, C4 được trỏ bởi nhánh hotfix — nhánh đang muốn trộn, lại ở phía trước C2 — nhánh đích đang muốn trộn vào, cho nên Git chỉ đơn giản là di chuyển con trỏ lên phía trước (từ C2 lên C4). Nói chung, khi bạn cố gắng trộn một commit2 vào commit1, mà commit2 nằm trên nhánh nối commit1 và commit đầu tiên, thì Git chỉ đơn giản là di chuyển con trỏ tới phía trước — kỹ thuật này được gọi là “fast-forward.”

Bây giờ, nhánh master đã trỏ tới commit chứa công việc hotfix, giờ bạn có thể phát hành nó.

`master` được fast-forward tới `hotfix`.

Figure 14. master được fast-forward tới hotfix

Sau khi giải quyết xong hotfix, bạn sẵn sàng quay trở lại công việc (issue #53) mà lúc nãy bị ngắt giữa chừng. Nhưng, trước tiên, nên xóa bỏ nhánh hotfix, vì nó không cần thiết nữa, nó và nhánh master cùng trỏ vị trí giống nhau. Bạn có thể xóa nhánh bằng lệnh git branch với cờ -d:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

Bây giờ, bạn có thể quay trở lại với issue #53 để tiếp tục làm việc:

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'

[iss53 ad82d7a]

finished the new footer [issue 53] 1 file changed, 1 insertion(+)

Tiếp tục làm việc về `iss53`.

Figure 15. Tiếp tục làm việc về iss53

Ta thấy rằng, công việc bạn đã làm trong nhánh hotfix không được chứa trong các tệp trong nhánh iss53. Nếu bạn cần công việc đã làm ở hotfix cũng có ở iss53, thì bạn trộn nhánh master vào nhánh iss53. Thực hiện trộn bằng lệnh git merge master, hoặc bạn có thể chờ cho tới thời điểm bạn cho là thích hợp thì trộn nhánh iss53 vào nhánh master (giống với trộn nhánh iss53 ở trên).

Cơ bản về trộn

Giả sử rằng, công việc về issue #53 của bạn đã hoàn thành và sẵn sàng trộn với nhánh master. Quá trình trộn giống hệt như làm với nhánh iss53 ở trên: đầu tiên, bạn chuyển tới nhánh master, sau đó dùng lệnh git merge:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

Nhìn thông tin hiển thị, ta thấy rằng quá trình trộn có khác so với trộn nhánh hotfix. Trong trường hợp này, lịch sử phát triển của bạn đã bị phân nhánh ở vị trí trước thời điểm commit cuối cùng của nhánh master. Vì thế Git phải sử dụng 3 điểm: 1 điểm là snapshot cuối cùng của nhánh master, một điểm là snapshot cuối cùng của nhánh iss53, và một điểm là snapshot chung giữa hai nhánh (chính là thời điểm bắt đầu tạo nhánh iss53)

3 snapshots được sử dụng trong khi trộn.

Figure 16. 3 snapshots được sử dụng trong khi trộn

Thay cho việc chỉ phải di chuyển con trỏ về phía trước, Git tạo một snapshot mới từ 3 snapshot đã nói trên và tự động tạo một commit mới trỏ tới snapshot mới đó. commit này giờ gọi là commit trộn, và nó đặc biệt hơn các commit thông thường vì nó có hơn một commit cha.

Commit trộn.

Figure 17. Commit trộn

Bây giờ, không có việc gì để làm với nhánh iss53 nữa. xóa bỏ nó:

$ git branch -d iss53

Cơ bản về xung đột khi trộn

Thỉnh thoảng, quá trình trộn không xảy ra mượt mà mà có xung đột. Nếu bạn thay đổi phần giống nhau ở cùng một tệp ở 2 nhánh khác nhau và sự thay đổi ở 2 nhánh không giống nhau. Sau đó tiến hành trộn 2 nhánh đó, Git sẽ không thể tự động trộn chúng mà bạn phải tự trộn. Ví dụ, bạn sửa cùng một chỗ trong một tệp nào đó ở nhánh iss53 và nhánh hotfix, khi bạn trộn sẽ có lỗi như sau:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Git sẽ không tự động tạo một commit mới. Nó sẽ dừng quá trình này để bạn tự xử lí xung đột. Nếu bạn muốn xem là tệp nào chưa trộn được vì xung đột, sử dụng lệnh git status:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

Bất kì tệp nào có xung đột và chưa được xử lí được liệt kê trong phần unmerged. Ngoài ra, Git còn chỉ thêm cả nguyên nhân gây ra xung đột dẫn tới không tự động trộn được. Hơn nữa, trong nội dung của tệp có xung đột, Git còn đưa vào đó cách xử lí xung đột giúp dễ dàng trong quá trình xử lí. Bạn có thể mở bằng tay tệp xung đột và xử lí xung đột. Giả sử, tệp có xung đột của bạn bây giờ như sau:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

Nội dung này có nghĩa là: ở phần trên ======= là công việc đã thực hiện ở nhánh hotfix (bây giờ chính là nhánh master vì bạn đã trộn nhánh hotfix vào nhánh master). Phần ở dưới ======= là công việc đã thực hiện ở nhánh iss53. Vậy để giải quyết xung đột bạn phải chọn một trong hai. Ví dụ, bạn chọn công việc ở nhánh iss53, thì xóa bỏ công việc ở nhánh hotfix đi, còn lại như sau:

<div id="footer">
please contact us at email.support@github.com
</div>

Cách khắc phục này cũng phải xóa bỏ đi các dòng chứa <<<<<<<=======, và >>>>>>>. Sau khi làm xong thì chạy lệnh git add <file> để đánh dấu là vấn đề xung đột của tệp đó đã được giải quyết.

Nếu bạn muốn sử dụng công cụ đồ họa để khắc phục xung đột, bạn chạy lệnh git mergetool, lệnh này sẽ khởi động công cụ trộn tương ứng và hướng dẫn bạn từng bước để khắc phục xung đột:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

Nếu bạn muốn sử dụng công cụ trộn khác chứ không phải công cụ mặc định (như opendiff), bạn có thể nhìn thấy tất cả các công cụ được hỗ trợ được liệt kê ở trên cùng sau hàng chữ “one of the following tools.” Chỉ cần gõ tên công cụ bạn muốn dùng vào rồi ấn Enter.

Nếu cần công cụ mạnh hơn để giải quyết xung đột, xem trong phần ch07-git-tools.html.

Sau khi bạn thoát công cụ trộn, Git hỏi bạn quá trình trộn đã hoàn thành chưa. Nếu bạn nói rằng đã hoàn thành thì tự động nó sẽ stage tệp để đánh dấu rằng xung đột đã được giải quyết. Bạn có thể kiểm tra lại bằng lệnh git status để xem các xung đột đã được giải quyết hết chưa:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   index.html

Ta thấy rằng, các xung đột đã được giải quyết. Nếu bạn thấy ổn, làm một commit, đó chính là commit trộn. Và mặc định thông điệp cho commit này như sau:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#	.git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#	modified:   index.html
#

Bạn có thể sửa thông điệp mặc định này nếu thấy cần thiết để giải thích hoặc chú giải để sau này xem lại bất kì ai cũng có thể hiểu được ở đây xảy ra việc gì.

Quản lí nhánh

Bây giờ bạn đã tạo, trộn và xóa một vài nhánh. Bây giờ, xem qua một vài công cụ quản lí nhánh

Lệnh git branch không chỉ có mỗi chức năng là tạo và xóa nhánh. Nếu bạn chạy nó với không có cờ gì, thì sẽ có được một danh sách các nhánh hiện tại đang có:

$ git branch
  iss53
* master
  testing

Ta thấy, dấu * ở trước nhánh master, điều đó có nghĩa là con trỏ HEAD đang trỏ vào đó. Như vậy, nếu bây giờ làm một commit thì nhánh master sẽ thêm một commit. Để xem, commit cuối cùng của mỗi nhánh, dùng lệnh git branch -v:

$ git branch -v
  iss53   93b412c fix javascript issue
* master  7a98805 Merge branch 'iss53'
  testing 782fd34 add scott to the author list in the readmes

Ta có cờ --merged để lọc danh sách trên chỉ hiển thị các nhánh mà được trộn vào nhánh mà bạn đang làm việc. Ngược lại, cờ --no-merged chỉ hiển thị các nhánh mà không trộn vào nhánh bạn đang làm việc. Ví dụ, lệnh git branch --merged:

$ git branch --merged
  iss53
* master

Vì trước bạn đã trộn nhánh iss53 vào nhánh master nên nó có trong danh sách hiển thị. Các nhánh trong danh sách này, mà không có dấu * thì thường có thể xóa bỏ đi mà không có vấn đề gì.

Để xem các nhánh mà công việc vẫn chưa được trộn, dùng lệnh git branch --no-merged:

$ git branch --no-merged
  testing

Ta thấy có nhánh testing, vì công việc thực hiện ở nhánh này chưa được trộn vào master. Và bởi vậy, nếu ta có muốn xóa nhánh này bằng lệnh git branch -d thì Git cũng không cho phép:

$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

Git làm vậy để tránh làm mất mát công việc đã được làm, nhưng nếu bạn chắc chắn muốn xóa bỏ nó thì có thể sử dụng cờ -D.

Hai cờ --merged và --no-merged không có tham số phía sau thì mặc định là hiển thị các nhánh được trộn hoặc không được trộn với nhánh hiện tại mà bạn đang làm việc.Hoàn toàn có thể sử dụng hai cờ này để biết về trạng thái trộn của một nhánh khác mà không phải checkout nhánh đó. Ví dụ: giờ đang ở nhánh testing, để hỏi danh sách về nhánh nào mà chưa được trộn vào nhánh master:$ git checkout testing $ git branch --no-merged master topicA featureB

Quy trình làm việc dùng Nhánh

Bây giờ, bạn có kiến thức cơ bản về nhánh và trộn nhánh, điều gì bạn nên hoặc có thể làm với chúng? Trong phần này, sẽ trình bày một vài Quy trình làm việc phổ biến khi dùng Nhánh, từ đó bạn có thể tự quyết định có áp dụng chúng vào quy trình làm việc riêng của bạn hay không.

Nhánh sương sống

Vì Git sử dụng trộn 3 điểm, nên việc trộn một nhánh vào một nhánh khác nhiều lần nhìn chung là dễ dàng. Điều đó có nghĩa rằng bạn có thể có vài nhánh luôn luôn được mở và bạn sử dụng cho các giai đoạn phát triển khác nhau trong chu trình phát triển của mình; bạn có thể thường xuyên trộn một trong số chúng vào nhánh khác.

Nhiều nhà phát triển Git sử dụng cách tiếp cận này, như vậy chỉ có mã nguồn ở nhánh master là ổn định – chỉ mã nguồn ở đây mới được phát hành ra cho cộng đồng sử dụng. Họ có các nhánh song song cùng tồn tại tên là develop và next. Ở các nhánh này tiến hành công việc và kiểm tra công việc, khi công việc đi tới trạng thái ổn định thì nó được trộn vào nhánh master. Nó được sử dụng để kéo công việc ở các nhánh chủ đề (loại nhánh có thời gian tồn tại ngắn, giống như nhánh iss53 ở trên) khi công việc ở nhánh chủ đề đã hoàn thành, điều đó đảm bảo rằng nhánh chính đã qua tất cả các bài kiểm tra và không mang lỗi.

Trong thực tế, chúng ta đang nói về việc các con trỏ di chuyển trên một dãy các commit. Các nhánh ổn định thì ở vị trí phía sau, còn các nhánh đề tài thì con trỏ luôn ở phía trước.

A linear view of progressive-stability branching.

Figure 18. A linear view of progressive-stability branching

Nhìn chung, để dễ hiểu có thể nghĩ chúng như là các Hầm chứa (silo) — các commit được chuyển tới Hầm chứa khác ổn định hơn khi chúng được kiểm tra cẩn thận.

A ``silo'' view of progressive-stability branching.

Figure 19. A “silo” view of progressive-stability branching

Bạn có thể tiếp tục áp dụng cách này cho nhiều mức ổn định khác nhau. Nhiều dự án lớn có nhánh “proposed” hoặc “pu” (chứa các cập nhật được đề xuất) được sử dụng cho các nhánh chưa đủ điều kiện để tích hợp vào nhánh next hoặc master. Ý tưởng ở đây là, các nhánh ở các tầng khác nhau, các tầng này được phân theo mức ổn định; khi chúng đạt tới một mức ổn định hơn nào đó, chúng sẽ được tích hợp vào tầng trên nó. Tóm lại, có nhiều nhánh sương sống không bắt buộc, nhưng nó thường rất hữu ích, đặc biệt là khi bạn làm việc với các dự án lớn và phức tạp.

Nhánh chủ đề

Nhánh chủ đề hữu ích trong mọi loại dự án. Một nhánh chủ đề là nhánh có vòng đời ngắn mà bạn tạo để phát triển một tính năng nào đó. Nó là thứ gì đó mà bạn chưa từng làm với một VCS trước đây, bởi vì với các VCS khác nhìn chung đòi hỏi rất nhiều nỗ lực để tạo một nhánh mới cũng như trộn các nhánh lại với nhau. Nhưng với Git, việc tạo, xóa vài nhánh trong một ngày là bình thường.

Như bạn đã thấy trong phần trước với các nhánh iss53 và hotfix bạn đã tạo ra. Bạn thực hiện một số commit trên đó và xóa chúng đi ngay sau khi trộn chúng lại với nhánh chính master. Kỹ thuật này cho phép bạn chuyển ngữ cảnh một cách nhanh chóng và toàn diện — vì công việc của bạn tách biệt hoàn toàn ở các Hầm chứa, nơi mà tất cả các thay đổi ở nhánh đó chỉ liên quan đến chủ đề đó, điều này khiến cho việc xem xét lại (review) mã nguồn hoặc làm việc gì đó tương tự trở nên dễ dàng hơn rất nhiều. Bạn có thể giữ các thay đổi ở nhánh đó trong bất kỳ khoảng thời gian nào bạn muốn, có thể tính bằng phút, ngày, hoặc tháng, và sau đó trộn khi chúng đã sẵn sàng, không quan trọng thứ tự chúng được tạo ra hay thứ tự công việc nó đảm nhiệm.

Hãy cùng xét một ví dụ về thực hiện một số công việc (trên nhánh master), tạo nhánh cho một vấn đề cần giải quyết (iss91), làm việc trên đó một chút, tạo một nhánh thứ hai cùng giải quyết vấn đề đó nhưng theo một cách khác (iss91v2), quay trở lại nhánh master và làm việc trong một khoảng thời gian nhất định, sau đó tạo một nhánh khác từ đó cho một ý tưởng mà bạn không chắc chắn là nó có phải là ý hay hay không (nhánh dumbidea). Lúc này lịch sử commit của bạn sẽ giống như sau:

Nhiều nhánh chủ đề.

Figure 20. Nhiều nhánh chủ đề

Bây giờ, giả sử bạn quyết định lựa chọn cách giải quyết thứ hai (iss91v2) là tối ưu hơn cả; và bạn trình bày ý tưởng dumbidea cho các đồng nghiệp, điều mà bạn không ngờ tới rằng mọi người lại cho đó là một ý tưởng tuyệt vời. Bạn đã có thể bỏ đi nhánh ban đầu iss91 (mất commit C5 và C6) và tích hợp hai commit còn lại. Lịch sử của bạn lúc này sẽ giống sau:

Lịch sử sau khi trộn `dumbidea` và `iss91v2`.

Figure 21. Lịch sử sau khi trộn dumbidea và iss91v2

Chi tiết hơn về nhiều Quy trình làm việc có thể sử dụng với Git để quản lí dự án được trình bày chi tiết tại ch05-distributed-git.html, do vậy trước khi bạn quyết định chọn Quy trình nào thì nên đọc chương này thêm.

Quan trọng nên nhớ là bạn làm tất cả những việc trên đều là ở nội bộ (hoàn toàn trong kho chứa của bạn). Khi bạn phân nhánh và trộn nhánh, mọi thứ được thực hiện bên trong kho chứa của bạn — hoàn toàn không có gì liên quan tới máy chủ.

Nhánh ở xa

Các tham chiếu ở xa là các tham chiếu (các con trỏ) trong kho chứa ở xa, chúng gồm tham chiếu tới nhánh, tham chiếu tới các thẻ,…​ Bạn muốn có một danh sách rõ ràng các tham chiếu ở xa tương ứng với các nhánh ở xa dùng lệnh git ls-remote [remote], hoặc lệnh git remote show [remote], ngoài ra lệnh này còn cung cấp thêm nhiều thông tin khác. Tuy nhiên, một cách phổ biến hơn là tận dụng điểm mạnh của các con trỏ trỏ nhánh ở xa (remote-tracking branches).

Các con trỏ trỏ nhánh ở xa được chứa ở kho chứa nội bộ, nhưng lại trỏ tới tới trạng thái của nhánh ở xa, nghĩa là nó biểu thị trạng thái của nhánh ở xa tương ứng. Chúng ta không thể làm cái commit trên nó; Git tự động lấy các commit từ máy chủ xuống cho bạn bất cứ khi nào bạn thực hiện giao tiếp với máy chủ, điều đó đảm bảo rằng chúng biểu diễn chính xác trạng thái của kho chứa ở xa. Hãy nghĩ về chúng như là các đánh dấu (bookmarks), nó nhắc nhở bạn nơi các nhánh trong kho chứa ở xa ở và lần cuối cùng bạn kết nối tới chúng.

Các con trỏ trỏ nhánh ở xa có dạng <remote>/<branch>. Ví dụ, nếu bạn muốn xem nhánh master trên remote origin trông ra sao kể từ lần cuối cùng bạn giao tiếp với nó, bạn sẽ kiểm tra nhánh origin/master. Nếu bạn đang làm việc về một vấn đề với đối tác và họ đẩy dữ liệu lên nhánh iss53, bạn có thể cũng có một nhánh iss53 ở trong kho chứa địa phương, nhưng nhánh trên máy chủ được truy cập bởi con trỏ trỏ nhánh ở xa tên là origin/iss53.

Điều này có thể gây một chút nhầm lẫn, để rõ ràng, cùng xét một ví dụ. Giả sử, có một máy chủ Git tại địa chỉ git.ourcompany.com. Nếu bạn clone kho chứa trên máy chủ thì nó sẽ tự động đặt tên remote cho bạn là origin, rồi kéo tất cả dữ liệu xuống, tạo một con trỏ tên là origin/master, con trỏ này trỏ tới nhánh master trên máy chủ. Git cũng cho bạn một nhánh master địa phương bắt đầu ở thời điểm giống như nhánh master trên máy chủ. Và công việc của bạn bắt đầu từ đây.

“origin” không có gì đặc biệtNó cũng giống như tên nhánh master không có ý nghĩa gì đặc biệt cả. Tương tự “master” là tên nhánh mặc định khi bắt đầu chạy lệnh khởi tạo kho chứa. “origin” là tên mặc định khi bạn chạy lệnh git clone. Nếu bạn dùng lệnh git clone -o booyah, thì bạn sẽ có tên tham chiếu tới nhánh ở xa là booyah/master.
Kho chứa trên server và địa phương sau khi clone.

Figure 22. Kho chứa trên máy chủ và địa phương sau khi clone

Nếu bạn làm một vài công việc trên nhánh master địa phương (nội bộ), trong thời gian bạn chưa đẩy dữ liệu lên máy chủ, một người khác đẩy dữ liệu công việc của họ lên, thì lịch sử nhánh master nội bộ của bạn sẽ khác với lịch sử của nhánh master trên máy chủ. Ngoài ra, khi bạn không liên hệ với origin thì con trỏ origin/master sẽ không cập nhật trạng thái mới nhất của nhánh master trên máy chủ.

Công việc ở địa phương và ở máy chủ có thể phân kỳ.

Figure 23. Công việc ở địa phương và ở máy chủ có thể phân kỳ

Để đồng bộ công việc, bạn dùng lệnh git fetch origin. Lệnh này nhìn máy chủ “origin” (trong trường hợp này đang là git.ourcompany.com) và lấy tất cả dữ liệu mà bạn chưa có về, và cập nhật cơ sở dữ liệu của bạn, di chuyển con trỏ origin/master tới vị trí mới nhất giống trên máy chủ.

`git fetch` cập nhật các nhánh bám từ xa.

Figure 24. git fetch cập nhật các nhánh bám từ xa

Để minh họa cho việc có nhiều máy chủ từ xa và các nhánh từ xa của các dự án thuộc các máy chủ đó, giả sử bạn có một máy chủ Git nội bộ khác sử dụng riêng cho các nhóm “thần tốc”. Máy chủ này có địa chỉ là git.team1.ourcompany.com. Bạn có thể thêm nó như là một tham chiếu từ xa tới dự án bạn đang làm việc bằng cách chạy lệnh git remote add như đã giới thiệu ở ch02-git-basics-chapter.html. Đặt tên cho remote đó là teamone, đó sẽ là tên rút gọn thay thế cho địa chỉ đầy đủ của nó.

Thêm máy chủ khác tới kho chứa như là remote.

Figure 25. Thêm máy chủ khác tới kho chứa như là remote

Bây giờ bạn có thể chạy lệnh git fetch teamone để lấy toàn bộ nội dung mà bạn chưa có từ máy chủ teamone. Bởi vì máy chủ đó có chứa một tập con dữ liệu từ máy chủ origin đang có, Git không truy xuất dữ liệu nào cả mà thiết lập một nhánh từ xa mới là teamone/master để trỏ tới commit mà teamone đang có.

`teamone/master` bám nhánh `master` trên `teamone`.

Figure 26. teamone/master bám nhánh master trên teamone

Đẩy lên

Khi bạn muốn chia sẻ một nhánh với người khác, bạn cần đẩy nó lên remote mà bạn có quyền ghi trên nó. Nhánh nội bộ của bạn sẽ không tự động thực hiện quá trình đồng bộ hóa – mà bạn phải tự đẩy lên các nhánh mà bạn muốn chia sẻ. Theo cách này, bạn có thể có các nhánh riêng tư cho những công việc mà bạn không muốn chia sẻ, và chỉ đẩy lên các nhánh chủ đề mà bạn muốn mọi người cùng tham gia đóng góp.

Nếu bạn có một nhánh là serverfix mà bạn muốn mọi người cùng cộng tác, bạn có thể đẩy nó lên theo cách mà chúng ta đã làm đối với nhánh đầu tiên. Chạy git push <remote> <branch>:

$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
 * [new branch]      serverfix -> serverfix

Đây là một cách làm tắt. Git tự động mở rộng nhánh serverfix thành refs/heads/serverfix:refs/heads/serverfix, có nghĩa là, “Hãy sử dụng nhánh nội bộ serverfix của tôi và đẩy nó lên để cập nhật nhánh serverfix trên máy chủ từ xa. Chúng ta sẽ đi sâu vào phần refs/heads/` ở ch10-git-internals.html, nhưng bạn thường có thể bỏ qua nó. Bạn cũng có thể chạy lệnh sau git push origin serverfix:serverfix, cách này cũng cho kết quả tương tự – nó có nghĩa là “Hãy sử dụng serverfix của tôi để tạo một serverfix trên máy chủ”. Bạn có thể sử dụng định dạng này để đẩy một nhánh nội bộ lên một nhánh từ xa với một tên khác. Nếu bạn không muốn gọi nó là serverfix trên máy chủ, bạn có thể chạy lệnh sau git push origin serverfix:awesomebranch để đẩy nhánh nội bộ serverfix vào nhánh awesomebranch trên máy chủ trung tâm.

Đừng nhập password mỗi lần đẩy dữ liệuNếu bạn đang sử dụng HTTPS URL để đẩy dữ liệu lên, máy chủ Git sẽ hỏi bạn username và password để xác thực. Mặc định, nó sẽ nhắc bạn nhập thông tin này trên terminal, bởi vậy máy chủ có thể cho phép đẩy dữ liệu hay không.Nếu bạn không muốn nhập thông tin này mỗi lần bạn đẩy dữ liệu, bạn có thể thiết lập một “credential cache”. Bạn có thể dễ dàng thiết lập bằng lệnh git config --global credential.helper cache.Để học thêm về credential cache, xem ch07-git-tools.html.

Lần tới một trong các đồng nghiệp của bạn truy xuất nó từ trên máy chủ, họ sẽ có một tham chiếu tới phiên bản trên máy chủ của serverfix dưới tên origin/serverfix:

$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
 * [new branch]      serverfix    -> origin/serverfix

Điều quan trọng cần chú ý ở đây là khi bạn truy xuất dữ liệu từ máy chủ mà có kèm theo nhánh mới, Git sẽ không tự động tạo phiên bản nội bộ của nhánh đó. Nói cách khác, trong trường hợp này, bạn sẽ không có nhánh serverfix nội bộ mới – mà bạn chỉ có một con trỏ tên origin/serverfix trỏ tới nhánh serverfix trên máy chủ origin, và con trỏ này bạn không thể chỉnh sửa, nó chỉ cập nhật trạng thái theo nhánh tương ứng trên máy chủ khi bạn kết nối tới máy chủ.

Để tích hợp công việc hiện tại vào nhánh bạn đang làm việc, bạn có thể chạy git merge origin/serverfix. Nếu bạn muốn nhánh serverfix riêng để có thể làm việc trên đó, bạn có thể tách nó ra khỏi nhánh trung tâm bằng cách:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

Cách này sẽ tạo cho bạn một nhánh nội bộ mà bạn có thể làm việc, bắt đầu cùng một vị trí với origin/serverfix.

Nhánh Bám

Checkout một nhánh nội bộ từ con trỏ trỏ nhánh ở xa sẽ tự động tạo ra một nhánh gọi là Nhánh BámNhánh Bám là nhánh nội bộ có liên quan trực tiếp với một nhánh trên máy chủ. Nếu bạn đang ở trên một Nhánh Bám và chạy lệnh git push, Git tự động biết nó sẽ phải đẩy lên nhánh nào, ở máy chủ nào. Ngoài ra, chạy git pull khi đang ở trên một Nhánh Bám, Git sẽ tự động biết máy chủ nào để kéo dữ liệu xuống và nhánh nào trên máy chủ được trộn với nhánh nội bộ.

Khi bạn clone một kho chứa, thông thường Git tự động tạo một nhánh master để theo dõi origin/master. Tuy nhiên, bạn có thể cài đặt Nhánh Bám khác nếu muốn – các nhánh này không theo dõi nhánh trên origin cũng như master. Trường hợp đơn giản là ví dụ bạn vừa thấy, chạy git checkout -b <branch> <remote>/<branch>. Hoặc có thể thay thế bằng cờ --track như sau:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

Thậm chí bạn không cần dùng tên remote. Nếu tên nhánh bạn đang cố gắng checkout không tồn tại ở nội bộ và nó trùng với tên nhánh trên một remote nào đó mà bạn đã thêm vào kho chứa của mình, thì Git sẽ tự động tạo Nhánh Bám cho bạn như sau:

$ git checkout serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

Để cài đặt một nhánh nội bộ sử dụng tên khác với tên mặc định trên nhánh trung tâm, bạn có thể dễ dàng sử dụng phiên bản đầu tiên của lệnh với một tên nội bộ khác như sau:

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

Bây giờ, nhánh nội bộ sf sẽ tự động “kéo và đẩy” từ origin/serverfix.

Nếu bạn đã có một nhánh nội bộ và muốn thiết lập nó tới một nhánh trung tâm bạn vừa mới kéo xuống, hoặc muốn thay đổi nhánh trung tâm bạn đang bám, bạn có thể sử dụng cờ -u hoặc --set-upstream-to với lệnh git branch để thiết lập bất kì lúc nào.

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Upstream shorthandKhi bạn đã có một nhánh bám nhánh ở xa, bạn có thể tham chiếu tới nhánh ở xa bằng @{upstream} hoặc @{u}. Nếu bạn đang ở trên nhánh master, mà nhánh này lại đang bám nhánh origin/master, thì thay cho việc dùng lệnh git merge origin/master, bạn có thể dùng lệnh git merge @{u}.

Nếu bạn muốn xem nhánh bám tương ứng với nhánh nào trên máy chủ, và trạng thái giữa chúng như thế nào, bạn có thể sử dụng cờ -vv cho lệnh git branch. Lệnh này sẽ cho chúng ta thông tin về các nhánh nội bộ của bạn, các nhánh đó đang bám nhánh nào trên máy chủ, và nhánh nội bộ của bạn đang mới hơn hay cũ hơn so với nhánh nó bám trên máy chủ.

$ git branch -vv
  iss53     7e424c3 [origin/iss53: ahead 2] forgot the brackets
  master    1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
  testing   5ea463a trying something new

Như ví dụ này ta có thể thấy rằng nhánh iss53 nội bộ đang bám nhánh origin/iss53 và nó vượt trước nhánh trên máy chủ 2 commit, có nghĩa là có 2 commit được thực hiện ở nội bộ của bạn mà chưa đẩy lên máy chủ. Còn nhánh master đang bám nhánh origin/master, và 2 nhánh này trạng thái giống nhau. Tiếp theo, nhánh serverfix đang bám nhánh server-fix-good trên máy chủ tên là teamone, và nhánh địa phương của bạn có 3 commit chưa đẩy lên máy chủ, và một commit mới trên máy chủ được thực hiện bởi người khác mà bạn chưa cập nhật. Cuối cùng, nhánh testing không bám nhánh nào trên máy chủ.

Quan trọng chú ý rằng các giá trị này chỉ được tính từ lần cuối cùng bạn lấy dữ liệu từ máy chủ. Lệnh này không giao tiếp với máy chủ, thông tin này là thông tin được lưu nội bộ từ lần cuối cùng giao tiếp với máy chủ. Nếu bạn muốn thông tin là cập nhật nhất thì trước khi chạy lệnh này nên git fetch dữ liệu từ máy chủ xuống. Làm như sau:

$ git fetch --all; git branch -vv

Kéo xuống (pull)

Trong khi lệnh git fetch sẽ kéo xuống tất cả các thay đổi trên máy chủ mà bạn chưa có ở nội bộ, nó hoàn toàn không sửa Thư mục làm việc của bạn. Nó đơn giản là lấy dữ liệu xuống và cho phép bạn trộn nó với dữ liệu nội bộ của bạn. Nhưng, có một lệnh là git pull, ngoài thực hiện công việc của git fetch, nó còn ngay lập tức chạy lệnh git merge. Nếu bạn có một nhánh bám như minh họa trên (do tự thiết lập bằng tay hoặc tự động được tạo khi dùng lệnh clone hoặc checkout), lệnh git pull sẽ xem xem nhánh hiện tại bạn đang làm việc đang bám nhánh nào ở máy chủ nào, sau đó nó lấy dữ liệu từ máy chủ xuống, nó sẽ cố gắng trộn công việc nhánh đó trên máy chủ với nhánh nội bộ.

Nhìn chung, sử dụng lệnh git pull dễ gây rối so với sử dụng kết hợp lệnh fetch và merge.

Xóa các nhánh trên máy chủ

Cho rằng bạn đã thực hiện xong công việc với một nhánh trên máy chủ, nghĩa là bạn và những cộng tác viên của bạn đã hoàn thành một công việc và đã trộn nó tới nhánh master của máy chủ. Bây giờ hoàn toàn có thể xóa bỏ nhánh đó sử dụng cờ --delete với lệnh git push. Nếu bạn muốn xóa nhánh serverfix trên máy chủ, làm như sau:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
 - [deleted]         serverfix

Về mặt cơ bản, lệnh này chỉ xóa con trỏ từ máy chủ. Máy chủ Git sẽ giữ lại dữ liệu ở đó một thời gian cho tới khi chức năng thu rác chạy, bởi vậy nếu bạn xóa nhầm thì cũng dễ dàng để khôi phục lại.

Rebasing

Trong Git, có 2 cách để tích hợp những thay đổi ở một nhánh vào nhánh khác: là trộn và rebaseTrộn đã trình bày ở trên. Trong phần này sẽ trình bày rebase là cái gì, làm thế nào để dùng nó, tại sao nó lại là một công cụ khá kinh ngạc, và trong trường hợp nào bạn sẽ không muốn sử dụng nó.

Cơ bản về Rebasing

Nếu bạn quay trở lại ví dụ trước ở Cơ bản về trộn, bạn có thể thấy rằng bạn đã chia công việc ra 2 nhánh khác nhau.

Lịch sử bị phân kỳ ở mức đơn giản.

Figure 27. Lịch sử bị phân kỳ ở mức đơn giản

Cách dễ dàng nhất để tích hợp các nhánh, như đã trình bày ở trên là trộn. Quá trình trộn sẽ thực hiện bằng phương pháp 3 điểm: 2 snapshot mới nhất của 2 nhánh là C3 và C4 với một commit chung của cả 2 nhánh C2, cuối cùng một snapshot mới được tạo ra.

Trộn để tích hợp công việc đã bị phân kỳ.

Figure 28. Trộn để tích hợp công việc đã bị phân kỳ

Nhưng, có một cách khác để thực hiện công việc này: bạn dùng bản vá (patch) của C4 và áp dụng vào C3. Trong Git, kỹ thuật này gọi là rebasing. Với lệnh rebase, bạn có thể dùng tất cả những thay đổi đã được commit trên một nhánh và áp chúng tới nhánh khác.

Trong ví dụ này, bạn chạy lệnh như sau:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Đầu tiên, nó di chuyển tới commit chung của 2 nhánh (nhánh bạn đang làm việc và nhánh bạn đang rebase trên nó), thực hiện tạo bản vá cho mỗi commit mà bạn đã thực hiện trên nhánh bạn đang làm việc, lưu các bản vá này vào một thư mục tạm thời, reset nhánh hiện tại tới commit cũng ở trên nhánh bạn đang rebase (chính là commit chung giữa hai nhánh), và cuối cùng áp dụng lần lượt các bản vá đó vào.

Rebase những thay đổi thực hiện ở `C4` vào `C3`.

Figure 29. Rebase những thay đổi thực hiện ở C4 vào C3

Đến lúc này, bạn có thể quay lại nhánh master và thực hiện trộn fast-forward.

$ git checkout master
$ git merge experiment
Di chuyển nhánh master lên phía trước.

Figure 30. Di chuyển nhánh master lên phía trước.

Bây giờ, snapshot trỏ bởi C4' chính xác là với cái được trỏ bởi C5 khi dùng trộn. Không có sự khác biệt về sản phẩm cuối cùng, nhưng rebase làm cho lịch sử sáng sủa hơn. Nếu bạn xem lại lịch sử của nhánh đã được rebase thì trông nó giống như một lịch sử tuyến tính, nghĩa là công việc được thực hiện ở nhánh này là liên tục không có phân nhánh.

Thường, bạn sẽ dùng phương pháp này để đảm bảo các commit của bạn được áp dụng sáng sủa trên một nhánh trên máy chủ — có lẽ trong một dự án bạn đang cố gắng cống hiến nhưng bạn không phải là người duy trì. Trong trường hợp đó, bạn thực hiện công việc ở nhánh riêng của mình và sau đó rebase công việc vào origin/master khi bạn thấy đã sẵn sàng trình các bản vá của mình lên dự án chính. Làm như vậy, người duy trì dự án sẽ không phải làm thao tác tích hợp công việc nữa — họ chỉ cần chuyển tiến lên phía trước (fast-forward) hoặc đơn giản là áp dụng chúng vào.

Lưu ý rằng snapshot được trỏ tới bởi commit cuối cùng, cho dù nó là kết quả của việc rebase hay merge, thì nó vẫn giống nhau – chỉ khác nhau về các bước thực hiện mà thôi. Quá trình rebase được thực hiện bằng cách thực hiện lại các thay đổi từ nhánh này qua nhánh khác theo thứ tự chúng đã được thực hiện, trong khi đó Trộn lại lấy hai điểm kết thúc và gộp chúng lại với nhau.

Rebase Nâng Cao

Bạn cũng có thể thực hiện rebase trên một đối tượng khác mà không phải là nhánh đích. Xem ví dụ Hình Lịch sử của nhánh chủ đề sinh ra từ một nhánh chủ đề khác. Bạn tạo một nhánh chủ đề (server) để thêm một số tính năng vào dự án, và thực hiện một số commit. Sau đó bạn tạo một nhánh khác client, và cũng commit vài lần. Cuối cùng, bạn quay trở lại nhánh server và thực hiện thêm một số commit nữa.

Lịch sử của nhánh chủ đề sinh ra từ một nhánh chủ đề khác.

Figure 31. Lịch sử của nhánh chủ đề sinh ra từ một nhánh chủ đề khác

Giả sử bây giờ bạn quyết định muốn trộn những thay đổi đã làm ở nhánh “client” vào trong nhánh chính để phát hành, nhưng bạn muốn giữ lại những thay đổi ở nhánh “server” cho tới khi nó được kiểm tra kỹ hơn. Bạn có thể dùng những thay đổi trên “client” mà chưa ở trên máy chủ (C8 and C9) và sau đó chạy lại (replay) chúng trên nhánh master bằng cách sử dụng cờ --onto cho lệnh git rebase:

$ git rebase --onto master server client

Lệnh này cơ bản nói rằng, “Hãy check out nhánh client, tìm ra các bản vá từ commit chung của nhánh client và server, sau đó thực thi lại vào nhánh master.” Nó hơi phức tạp một chút nhưng kết quả như Hình 3-32 thì lại rất tuyệt.

Quá trình rebase nhánh chủ đề khỏi một nhánh chủ đề khác.

Figure 32. Quá trình rebase nhánh chủ đề khỏi một nhánh chủ đề khác

Bây giờ bạn có thể di chuyển con trỏ của nhánh master tiến lên phía trước (xem Di chuyển nhánh master lên phía trước để chứa các thay đổi của nhánh client.):

$ git checkout master
$ git merge client
Di chuyển nhánh master lên phía trước để chứa các thay đổi của nhánh client.

Figure 33. Di chuyển nhánh master lên phía trước để chứa các thay đổi của nhánh client.

Giả sử rằng bạn quyết định kéo về cả nhánh server. Bạn có thể rebase nhánh server vào nhánh master mà không phải checkout trước bằng lệnh git rebase <basebranch> <topicbranch> — lệnh này sẽ checkout nhánh chủ đề (trong trường hợp này là server) cho bạn và áp dụng lại các thay đổi vào nhánh cơ sở master:

$ git rebase master server

Lệnh này sẽ thực hiện lại các thay đổi trên nhánh server đưa vào nhánh master như trong Rebase nhánh server lên trước nhánh master.

Rebase nhánh server lên trước nhánh master.

Figure 34. Rebase nhánh server lên trước nhánh master

Sau đó bạn có thể di chuyển con trỏ nhánh chính (master) lên phía trước:

$ git checkout master
$ git merge server

Bây giờ bạn có thể xóa bỏ nhánh client và server bởi vì tất cả công việc của chúng giờ đã ở trên nhánh master, và lịch sử cho toàn quá trình trông như Lịch sử commit cuối cùng:

$ git branch -d client
$ git branch -d server
Lịch sử commit cuối cùng.

Figure 35. Lịch sử commit cuối cùng

Rủi Ro của Rebase

Mặc dù rebase rất hữu ích nhưng nó cũng có không ít những mặt hạn chế, điều này có thể tổng kết bằng câu sau đây:

Không được rebase các commit mà đã tồn tại ở ngoài kho chứa của bạn.

Miễn là bạn làm theo hướng dẫn này, sẽ không có chuyện gì xảy ra. Nếu không, mọi người sẽ ghét bạn, và bạn sẽ bị bạn bè và gia đình coi thường.

Khi bạn thực hiện rebase, bạn đang bỏ đi các commit đã tồn tại và tái tạo lại các commit mới tương tự nhưng thực ra khác biệt. Nếu bạn đẩy commit ở một nơi nào đó và mọi người kéo xuống máy của họ, sau đó bạn sửa lại các commit đó bằng lệnh git rebase và đẩy lên một lần nữa, đồng nghiệp của bạn sẽ phải trộn lại công việc của họ và mọi thứ sẽ rối tung lên khi bạn cố gắng kéo các thay đổi của họ ngược lại máy bạn.

Hãy cùng xem một ví dụ làm sao việc rebase các commit đã được chia sẻ có thể gây sự cố. Giả sử bạn tạo bản sao từ một máy chủ trung tâm và thực hiện một số thay đổi từ đó. Lịch sử commit của bạn sẽ giống như sau:

Clone một kho chứa, và làm một số việc từ nó.

Figure 36. Clone một kho chứa, và làm một số việc từ nó.

Bây giờ, một người khác thực hiện một số thay đổi khác có kèm theo một lần trộn, và đẩy lên máy chủ trung tâm. Bạn fetch chúng và tích hợp nhánh trung tâm mới đó vào của bạn, lúc này lịch sử của bạn sẽ giống như sau:

Fetch các commits, và trộn chúng vào trong công việc của bạn.

Figure 37. Fetch các commits, và trộn chúng vào trong công việc của bạn.

Tiếp theo, người đã đẩy tích hợp đó quyết định lại và rebase lại những thay đổi của họ; họ thực hiện git push --forceđể ghi đè lịch sử trên máy chủ. Sau đó bạn truy xuất lại dữ liệu từ máy chủ, đưa về các commit mới.

Ai đó đẩy rebased commits lên, bỏ đi các commit mà công việc của bạn bắt đầu từ nó.

Figure 38. Ai đó đẩy rebased commits lên, bỏ đi các commit mà công việc của bạn bắt đầu từ nó

Bây giờ cả hai đều ở trong tình cảnh rắc rối. Nếu bạn thực hiện một git pull, thì bạn sẽ tạo một commit trộn chứa cả 2 đường lịch sử , và kho chứa của bạn trong như sau:

Bạn trộn lại công việc đã trộn vào `commit trộn` mới.

Figure 39. Bạn trộn lại công việc đã trộn vào commit trộn mới

Khi bạn chạy lệnh git log sẽ thấy lịch sử giống như trên, bạn sẽ thấy 2 commit có cùng tác giả, ngày tháng, và thông điệp, điều này có thể gây khó hiểu. Hơn nữa, nếu bạn đẩy chúng ngược lên máy chủ, bạn sẽ đưa vào một lần nữa tất cả các commit đã rebase đó và sẽ gây khó hiểu cho nhiều người khác nữa. Có thể không sai khi cho rằng các nhà phát triển khác không muốn C4 và C6 ở trong lịch sử; đó là lí do tại sao họ rebase.

Rebase khi bạn Rebase

Nếu bạn ở trong tình huống này, Git có một vài cách giúp bạn thoát khỏi. Nếu một ai đó trong đội của bạn cố gắng đẩy những thay đổi mà nó lại ghi đè cái mà bạn bắt đầu làm việc từ đó, thách thức của bạn là nhận ra cái nào của bạn và cái nào bị ghi đè.

Hóa ra ngoài SHA-1, Git còn tính toán checksum cho các bản vá mà được áp dụng cho các commit. Chúng được gọi là “patch-id”.

Nếu bạn kéo công việc xuống mà đã bị rebase bởi đối tác, Git có thể nhận ra cái nào là của riêng bạn và áp dụng chúng quay trở lại đầu của nhánh mới (nhánh mà đối tác của bạn rebase tạo thành).

Ví dụ, trong trường hợp trước, thay cho việc trộn như Ai đó đẩy rebased commits lên, bỏ đi các commit mà công việc của bạn bắt đầu từ nó, chúng ta dùng git rebase teamone/master, Git sẽ:

  • Xác định công việc nào là riêng cho nhánh của bạn (C2, C3, C4, C6, C7)
  • Xác định công việc nào không phải là commit trộn (C2, C3, C4)
  • Xác định cái nào không được thêm lại vào trong nhánh đích (chỉ C2 và C3, vì C4 là bản vá giống C4′)
  • Áp dụng những commit này tới trên cùng của nhánh teamone/master.

Vậy thay cho chúng ta được kết quả như trong Bạn trộn lại công việc đã trộn vào commit trộn mới, chúng ta sẽ được kết quả như Rebase trên đỉnh của force-pushed rebase work.

Rebase trên đỉnh của force-pushed rebase work.

Figure 40. Rebase trên đỉnh của force-pushed rebase work

Cách này chỉ làm việc khi C4′ chính xác được tạo ra từ bản vá của C4. Ngược lại, rebase sẽ cho rằng C4′ không là bản sao của C4 và khi đó nó sẽ áp dụng thêm bản vá khác từ C4 (bản vá này có lẽ sẽ không áp dụng được suôn sẻ, vì ít nhất có thay đổi ở một nơi nào đó).

Bạn có thể đơn giản hóa cách này bằng chạy lệnh git pull --rebase thay cho việc thông thường dùng lệnh git pull. Hoặc bạn có thể làm nó bằng tay với lệnh git fetch, sau đó là lệnh git rebase teamone/master.

Nếu bạn đang sử dụng git pull mà muốn thực hiện --rebase mặc định, thì bạn có thể thiết lập biến cấu hình pull.rebase như sau git config --global pull.rebase true.

Nếu bạn xem rebase như là một cách để làm sạch và làm việc với các commit trước khi bạn đẩy chúng ra ngoài, và nếu bạn chỉ rebase các commit mà chưa từng được công bố ra khỏi kho chứa nội bộ của bạn thì điều đó hoàn toàn ổn. Còn nếu bạn rebase các commit mà đã được đẩy ra ngoài kho chứa của bạn, và mọi người có thể làm việc của họ dựa trên commit đó, thì bạn có thể sẽ lâm vào trạng thái bực dọc, và đối tác của bạn sẽ khinh bỉ bạn.

Nếu bạn và một đối tác của bạn thấy cần phải làm như trên ở một thời điểm nào đó để loại bỏ sự rắc rối mà ai đó đã đẩy các commit được rebase lên cho cộng đồng , thì cần thông báo ngay cho mọi người chạy lệnh git pull --rebase, để giảm rắc rối kéo dài.

Rebase hay Trộn

Bây giờ, ta đã biết cả về trộn và rebase, giờ bạn đang tự hỏi rằng cái nào tốt hơn. Trước khi trả lời câu hỏi này, cùng quay lại một chút và nói về lịch sử nghĩa là gì.

Có một quan điểm nói rằng lịch sử kho chứa của bạn là một bản ghi lại những điều đã xảy ra. Nó là một tài liệu lịch sử, có giá trị trong đánh giá đóng góp của bạn và không nên phá bỏ nó. Từ góc nhìn này, thay đổi lịch sử commit là báng bổ; bạn đang dối trá về cái thực sự được tiết lộ. Nếu có một chuỗi hỗn độn các commit trộn? Nó xảy ra như thế nào, và kho chứa nên giữ nó cho thế hệ sau.

Quan điểm ngược lại cho rằng lịch sử commit là câu chuyện kể về dự án được thực hiện như thế nào. Bạn sẽ không công bố bản nháp đầu tiên của một cuốn sách, và sổ tay làm thế nào duy trì phần mềm của bạn. Ở đây thì các công cụ giống như rebase và filter-branch tạo kết quả tốt đối với những người đọc sau này.

Bây giờ, trở lại với câu hỏi về trộn hay rebase tốt hơn: hi vọng bạn sẽ thấy rằng đó không phải là điều đơn giản. Git là một công cụ mạnh, và cho phép bạn làm nhiều thứ với lịch sử, nhưng mỗi đội và mỗi dự án thì khác nhau. Và khi bạn đã biết cả hai chúng vận hành như thế nào, tùy bạn lựa chọn cái nào tốt trong từng trường hợp cụ thể.

Nói chung, cách thực hành tốt nhất là rebase các nhánh nội bộ bạn đã làm nhưng chưa chia sẻ trước khi đẩy nó lên máy chủ để công việc bạn đang hợp tác sáng sủa, nhưng không bao giờ rebase bất kì cái gì mà bạn đã đẩy tới một nơi nào khác ngoài kho chứa nội bộ.

Tóm tắt

Chương này đã trình bày những điều cơ bản nhất về phân nhánh và trộn nhánh trong Git. Bạn cảm thấy thoải mái khi tạo và chuyển tới nhánh mới, di chuyển giữa các nhánh và trộn các nhánh nội bộ với nhau. Bạn cũng có thể chia sẻ nhánh của bạn bằng cách đẩy chúng tới máy chủ, làm việc với người khác trên các nhánh chia sẻ đó và rebase nhánh của bạn trước khi bạn chia sẻ nó. Tiếp theo, bạn sẽ được học cách làm thế nào để làm một máy chủ Git của riêng mình.

Tải xuống

Đọc online tại: http://elinux.vn/Books/kythuat/progit_chap3.html

Hãy bình luận đầu tiên

Để lại một phản hồi

Thư điện tử của bạn sẽ không được hiện thị công khai.


*