Git: Merging so với Rebasing

Các lệnh git rebasel có tiếng là huyền diệu Git voodoo rằng người mới bắt đầu nên tránh xa, nhưng nó thực sự có thể làm cho cuộc sống dễ dàng hơn nhiều cho một nhóm phát triển khi được sử dụng một cách cẩn thận. Trong bài viết này, chúng tôi sẽ so sánh lệnh git rebase với lệnh git merge liên quan và xác định tất cả các cơ hội tiềm năng để kết hợp rebasing vào quy trình công việc Git điển hình.

Tổng quan về khái niệm

Điều đầu tiên cần hiểu git rebaselà nó giải quyết vấn đề tương tự như git merge. Cả hai lệnh này đều được thiết kế để tích hợp các thay đổi từ nhánh này sang nhánh khác. Họ chỉ thực hiện theo những cách rất khác nhau.

Xem xét những gì xảy ra khi bạn bắt đầu làm việc trên một tính năng mới trong một nhánh chuyên dụng, sau đó một thành viên khác trong nhóm cập nhật masterchi nhánh với các cam kết mới. Điều này dẫn đến một lịch sử rẽ nhánh, vốn quen thuộc với bất kỳ ai đã sử dụng Git làm công cụ cộng tác.

Một lịch sử cam kết ngã ba

Bây giờ, hãy nói rằng các cam kết mới mastercó liên quan đến tính năng mà bạn đang làm việc. Để kết hợp các cam kết mới vào featurechi nhánh của bạn , bạn có hai tùy chọn: hợp nhất hoặc nổi loạn.

Tùy chọn hợp nhất

Tùy chọn đơn giản nhất là hợp nhất masternhánh vào nhánh tính năng bằng cách sử dụng một cái gì đó như sau:

git

checkout feature

git

merge master

Hoặc, bạn có thể ngưng tụ điều này thành một lớp lót:

git 

merge

feature

master

Điều này tạo ra một hợp nhất mới của Cam kết cam kết trong featurenhánh gắn kết lịch sử của cả hai nhánh, tạo cho bạn một cấu trúc nhánh trông như thế này:

Sáp nhập tổng thể vào nhánh tính năng

Sáp nhập là tốt bởi vì nó là một hoạt động không phá hủy . Các chi nhánh hiện tại không được thay đổi trong bất kỳ cách nào. Điều này tránh tất cả những cạm bẫy tiềm tàng của việc nổi loạn (thảo luận dưới đây).

Mặt khác, điều này cũng có nghĩa là featurechi nhánh sẽ có một cam kết hợp nhất bên ngoài mỗi khi bạn cần kết hợp các thay đổi ngược dòng. Nếu masterrất tích cực, điều này có thể gây ô nhiễm lịch sử của nhánh tính năng của bạn khá nhiều. Mặc dù có thể giảm thiểu vấn đề này bằng các git logtùy chọn nâng cao , nhưng nó có thể khiến các nhà phát triển khác khó hiểu được lịch sử của dự án.

Tùy chọn Rebase

Thay thế cho việc hợp nhất, bạn có thể khởi động lại featurenhánh lên masternhánh bằng các lệnh sau:

git

checkout feature

git

rebase master

Điều này di chuyển toàn bộ featurechi nhánh để bắt đầu trên đỉnh của masterchi nhánh, kết hợp hiệu quả tất cả các cam kết mới trong master. Nhưng, thay vì sử dụng một cam kết hợp nhất, hãy viết lại ghi lại lịch sử dự án bằng cách tạo các cam kết hoàn toàn mới cho mỗi cam kết trong nhánh ban đầu.

Khởi động lại nhánh tính năng lên master

Lợi ích chính của việc nổi loạn là bạn có được lịch sử dự án sạch hơn nhiều. Đầu tiên, nó loại bỏ các cam kết hợp nhất không cần thiết theo yêu cầu git merge. Thứ hai, như bạn có thể thấy trong sơ đồ trên, việc khởi động lại cũng dẫn đến một lịch sử dự án tuyến tính hoàn hảo, bạn có thể theo dõi featuretất cả các cách để bắt đầu dự án mà không cần bất kỳ nhánh nào. Điều này làm cho nó dễ dàng hơn để di chuyển dự án của bạn với lệnh như git loggit bisect, và gitk.

Nhưng, có hai sự đánh đổi cho lịch sử cam kết nguyên sơ này: an toàn và truy xuất nguồn gốc. Nếu bạn không tuân theo Nguyên tắc Vàng của Nổi loạn , viết lại lịch sử dự án có thể là thảm họa cho quy trình làm việc cộng tác của bạn. Và, ít quan trọng hơn, việc đánh trả lại làm mất bối cảnh được cung cấp bởi một cam kết hợp nhất mà bạn không thể thấy khi các thay đổi ngược dòng được tích hợp vào tính năng.

Rebasing tương tác

Rebasing tương tác cung cấp cho bạn cơ hội để thay đổi các cam kết khi chúng được chuyển sang chi nhánh mới. Điều này thậm chí còn mạnh hơn cả một cuộc nổi loạn tự động, vì nó cung cấp sự kiểm soát hoàn toàn đối với lịch sử cam kết của chi nhánh. Thông thường, điều này được sử dụng để dọn dẹp một lịch sử lộn xộn trước khi hợp nhất một nhánh tính năng vào master.

Để bắt đầu một phiên khởi động lại tương tác, hãy chuyển itùy chọn cho git rebaselệnh:

git

checkout feature

git

rebase -i master

Điều này sẽ mở một trình soạn thảo văn bản liệt kê tất cả các cam kết sắp được di chuyển:

pick 

33

d5b7a Message

for

commit #

1

pick

9480

b3d Message

for

commit #

2

pick

5

c67e61 Message

for

commit #

3

Danh sách này xác định chính xác chi nhánh sẽ trông như thế nào sau khi rebase được thực hiện. Bằng cách thay đổi picklệnh và / hoặc sắp xếp lại các mục, bạn có thể làm cho lịch sử của chi nhánh trông giống như bất cứ điều gì bạn muốn. Ví dụ: nếu lần xác nhận thứ 2 khắc phục một vấn đề nhỏ trong lần xác nhận thứ nhất, bạn có thể cô đặc chúng thành một lần xác nhận bằng fixuplệnh:

pick 

33

d5b7a Message

for

commit #

1

fixup

9480

b3d Message

for

commit #

2

pick

5

c67e61 Message

for

commit #

3

Khi bạn lưu và đóng tệp, Git sẽ thực hiện rebase theo hướng dẫn của bạn, dẫn đến lịch sử dự án giống như sau:

Bóp một cam kết với một rebase tương tác

Loại bỏ các cam kết không đáng kể như thế này làm cho lịch sử tính năng của bạn dễ hiểu hơn nhiều. Đây là điều mà git mergeđơn giản là không thể làm được.

Nguyên tắc vàng của sự nổi loạn

Một khi bạn hiểu nổi loạn là gì, điều quan trọng nhất cần học là khi không làm điều đó. Nguyên tắc vàng git rebaselà không bao giờ sử dụng nó trên các chi nhánh công cộng .

Ví dụ, suy nghĩ về những gì sẽ xảy ra nếu bạn nổi loạn masterlên featurechi nhánh của mình :

Nổi loạn chi nhánh

Các rebase di chuyển tất cả các cam kết mastervào đầu feature. Vấn đề là điều này chỉ xảy ra trong kho lưu trữ của bạn . Tất cả các nhà phát triển khác vẫn đang làm việc với bản gốc master. Kể từ khi đánh bại kết quả trong các cam kết hoàn toàn mới, Git sẽ nghĩ rằng masterlịch sử của chi nhánh của bạn đã chuyển hướng khỏi mọi người khác.

Cách duy nhất để đồng bộ hóa hai masternhánh là hợp nhất chúng lại với nhau, dẫn đến một cam kết hợp nhất bổ sung và hai bộ cam kết có cùng các thay đổi (các nhánh ban đầu và các nhánh từ nhánh bị từ chối của bạn). Không cần phải nói, đây là một tình huống rất khó hiểu.

Vì vậy, trước khi chạy git rebase, hãy luôn tự hỏi, Có ai khác đang nhìn vào chi nhánh này không? Nếu câu trả lời là có, hãy bỏ tay ra và bắt đầu suy nghĩ về một cách không phá hủy để thực hiện các thay đổi của bạn (ví dụ: git revertlệnh ). Mặt khác, bạn an toàn để viết lại lịch sử bao nhiêu tùy thích.

Lực đẩy

Nếu bạn cố gắng đẩy masternhánh bị từ chối trở lại một kho lưu trữ từ xa, Git sẽ ngăn bạn làm như vậy vì nó xung đột với masternhánh từ xa . Nhưng, bạn có thể buộc lực đẩy đi qua bằng cách chuyền --forcecờ, như vậy:

# Be very careful 

with

this

command! git push --

force

Điều này ghi đè lên masterchi nhánh từ xa để khớp với chi nhánh bị từ chối từ kho lưu trữ của bạn và khiến mọi thứ trở nên rất khó hiểu cho phần còn lại của nhóm bạn. Vì vậy, hãy thật cẩn thận khi chỉ sử dụng lệnh này khi bạn biết chính xác những gì bạn đang làm.

Một trong những lần duy nhất bạn nên ép buộc là khi bạn đã thực hiện dọn dẹp cục bộ sau khi bạn đã đẩy một nhánh tính năng riêng tư vào một kho lưu trữ từ xa (ví dụ: cho mục đích sao lưu). Điều này giống như nói rằng, Oops, tôi không thực sự muốn đẩy phiên bản gốc của nhánh tính năng đó. Lấy cái hiện tại thay thế. Một lần nữa, điều quan trọng là không ai làm việc với các cam kết từ phiên bản gốc của nhánh tính năng.

Quy trình làm việc

Rebasing có thể được tích hợp vào quy trình công việc Git hiện tại của bạn nhiều hay ít tùy theo nhóm của bạn. Trong phần này, chúng ta sẽ xem xét những lợi ích mà việc nổi loạn có thể mang lại ở các giai đoạn phát triển khác nhau của một tính năng.

Bước đầu tiên trong bất kỳ quy trình công việc nào tận dụng git rebaselà tạo một nhánh dành riêng cho từng tính năng. Điều này cung cấp cho bạn cấu trúc chi nhánh cần thiết để sử dụng việc khởi động lại một cách an toàn:

Phát triển một tính năng trong một nhánh chuyên dụng

Dọn dẹp địa phương

Một trong những cách tốt nhất để kết hợp việc nổi loạn vào quy trình làm việc của bạn là dọn sạch các tính năng đang thực hiện, cục bộ. Bằng cách định kỳ thực hiện một rebase tương tác, bạn có thể đảm bảo mỗi cam kết trong tính năng của bạn được tập trung và có ý nghĩa. Điều này cho phép bạn viết mã của mình mà không phải lo lắng về việc chia nó thành các cam kết bị cô lập, bạn có thể sửa nó sau khi thực tế.

Khi gọi git rebase, bạn có hai tùy chọn cho cơ sở mới: Chi nhánh mẹ của tính năng (ví dụ master🙂 hoặc một cam kết trước đó trong tính năng của bạn. Chúng tôi đã thấy một ví dụ về tùy chọn đầu tiên trong phần Rebasing tương tác . Tùy chọn thứ hai là tốt khi bạn chỉ cần sửa chữa một vài cam kết cuối cùng. Ví dụ, lệnh sau bắt đầu một rebase tương tác chỉ với 3 lần xác nhận cuối cùng.

git

checkout feature

git

rebase -i HEAD~3

Bằng cách chỉ định HEAD~3làm cơ sở mới, bạn không thực sự di chuyển chi nhánh mà bạn chỉ cần viết lại tương tác 3 cam kết tuân theo nó. Lưu ý rằng điều này sẽ không kết hợp các thay đổi ngược dòng vào featurechi nhánh.

Nổi loạn lên Head ~ 3

Nếu bạn muốn viết lại toàn bộ tính năng bằng phương thức này, git merge-baselệnh có thể hữu ích để tìm cơ sở ban đầu của featurenhánh. Sau đây trả về ID cam kết của cơ sở ban đầu, sau đó bạn có thể chuyển đến git rebase:

git 

merge

-base feature

master

Việc sử dụng rebasing tương tác này là một cách tuyệt vời để giới thiệu git rebasevào quy trình công việc của bạn, vì nó chỉ ảnh hưởng đến các chi nhánh địa phương. Điều duy nhất các nhà phát triển khác sẽ thấy là sản phẩm hoàn chỉnh của bạn, đây phải là một lịch sử chi nhánh tính năng sạch sẽ, dễ theo dõi.

Nhưng một lần nữa, điều này chỉ hoạt động cho các nhánh tính năng riêng tư . Nếu bạn cộng tác với các nhà phát triển khác thông qua cùng một nhánh tính năng, nhánh đó là công khai và bạn không được phép viết lại lịch sử của nó.

Không có git mergecách nào khác để làm sạch các cam kết cục bộ với một rebase tương tác.

Kết hợp các thay đổi ngược dòng vào một tính năng

Trong phần Tổng quan về khái niệm , chúng tôi đã thấy cách một nhánh tính năng có thể kết hợp các thay đổi ngược dòng từ masterviệc sử dụng một trong hai git mergehoặc git rebase. Hợp nhất là một tùy chọn an toàn bảo tồn toàn bộ lịch sử của kho lưu trữ của bạn, trong khi việc tạo lại sẽ tạo ra một lịch sử tuyến tính bằng cách di chuyển nhánh tính năng của bạn lên trên đỉnh master.

Việc sử dụng git rebasenày tương tự như dọn dẹp cục bộ (và có thể được thực hiện đồng thời), nhưng trong quá trình nó kết hợp những cam kết ngược dòng từ đó master.

Hãy nhớ rằng việc tấn công vào một chi nhánh từ xa thay vì hoàn toàn hợp pháp master. Điều này có thể xảy ra khi cộng tác trên cùng một tính năng với nhà phát triển khác và bạn cần kết hợp các thay đổi của họ vào kho lưu trữ của mình.

Ví dụ: nếu bạn và một nhà phát triển khác có tên John đã thêm các cam kết vào featurechi nhánh, kho lưu trữ của bạn có thể trông giống như sau sau khi tìm nạp featurechi nhánh từ xa từ kho lưu trữ của John:

Cộng tác trên cùng một nhánh tính năng

Bạn có thể giải quyết ngã ba này theo cách chính xác giống như khi bạn tích hợp các thay đổi ngược dòng từ master: hoặc hợp nhất cục bộ của bạn featurevới john/featurehoặc khởi động lại cục bộ của bạn featurevào đầu john/feature.

Sáp nhập so với nổi loạn vào một chi nhánh từ xa

Lưu ý rằng cuộc nổi loạn này không vi phạm Nguyên tắc Nổi loạn Vàng vì chỉ có các featurecam kết tại địa phương của bạn đang được di chuyển, mọi thứ trước khi bị ảnh hưởng. Điều này cũng giống như nói, hãy thêm những thay đổi của tôi vào những gì John đã làm. Trong hầu hết các trường hợp, điều này trực quan hơn là đồng bộ hóa với chi nhánh từ xa thông qua một cam kết hợp nhất.

Theo mặc định, git pulllệnh thực hiện hợp nhất, nhưng bạn có thể buộc nó tích hợp nhánh từ xa với rebase bằng cách chuyển --rebasetùy chọn đó.

Xem lại một tính năng với một yêu cầu kéo

Nếu bạn sử dụng yêu cầu kéo như một phần của quy trình xem xét mã của mình, bạn cần tránh sử dụng git rebasesau khi tạo yêu cầu kéo. Ngay khi bạn thực hiện yêu cầu kéo, các nhà phát triển khác sẽ xem xét các cam kết của bạn, điều đó có nghĩa là đó là một chi nhánh công cộng . Viết lại lịch sử của nó sẽ khiến Git và đồng đội của bạn không thể theo dõi bất kỳ cam kết tiếp theo nào được thêm vào tính năng.

Mọi thay đổi từ các nhà phát triển khác cần được kết hợp với git mergethay vì git rebase.

Vì lý do này, thông thường nên làm sạch mã của bạn bằng một rebase tương tác trước khi gửi yêu cầu kéo của bạn.

Tích hợp một tính năng được phê duyệt

Sau khi một tính năng đã được nhóm của bạn chấp thuận, bạn có tùy chọn khởi động lại tính năng này vào đầu masterchi nhánh trước khi sử dụng git mergeđể tích hợp tính năng này vào cơ sở mã chính.

Đây là một tình huống tương tự với việc kết hợp các thay đổi ngược dòng vào một nhánh tính năng, nhưng vì bạn không được phép viết lại các cam kết trong masternhánh, nên cuối cùng bạn phải sử dụng git mergeđể tích hợp tính năng này. Tuy nhiên, bằng cách thực hiện rebase trước khi hợp nhất, bạn chắc chắn rằng việc hợp nhất sẽ được chuyển tiếp nhanh, dẫn đến một lịch sử tuyến tính hoàn hảo. Điều này cũng cung cấp cho bạn cơ hội để xóa bất kỳ cam kết tiếp theo nào được thêm vào trong yêu cầu kéo.

Tích hợp một tính năng thành chủ có và không có rebase

Nếu bạn không hoàn toàn thoải mái git rebase, bạn luôn có thể thực hiện rebase trong một nhánh tạm thời. Bằng cách đó, nếu bạn vô tình làm xáo trộn lịch sử tính năng của mình, bạn có thể kiểm tra chi nhánh ban đầu và thử lại. Ví dụ:

git

checkout feature

git

checkout -b temporary-branch

git

rebase -i master

# [Clean up the history]

git

checkout master

git

merge temporary-branch

Tóm lược

Và đó là tất cả những gì bạn thực sự cần biết để bắt đầu đánh bại các chi nhánh của mình. Nếu bạn muốn một lịch sử tuyến tính rõ ràng, không có các cam kết hợp nhất không cần thiết, bạn nên tiếp cận git rebasethay vì git mergekhi tích hợp các thay đổi từ một chi nhánh khác.

Mặt khác, nếu bạn muốn lưu giữ toàn bộ lịch sử dự án của mình và tránh nguy cơ viết lại các cam kết công khai, bạn có thể gắn bó git merge. Hoặc là tùy chọn là hoàn toàn hợp lệ, nhưng ít nhất bây giờ bạn có tùy chọn tận dụng lợi ích của git rebase.