Làm cách nào để khôi phục / đồng bộ lại sau khi ai đó đẩy rebase hoặc đặt lại vào nhánh đã xuất bản?


88

Tất cả chúng ta đều đã nghe nói rằng không bao giờ nên rebase tác phẩm đã xuất bản, rằng nó nguy hiểm, v.v. Tuy nhiên, tôi chưa thấy bất kỳ công thức nào được đăng về cách xử lý tình huống trong trường hợp rebase được xuất bản.

Bây giờ, xin lưu ý rằng điều này chỉ thực sự khả thi nếu kho lưu trữ chỉ được sao chép bởi một nhóm người đã biết (và tốt nhất là nhỏ), để bất kỳ ai đẩy rebase hoặc đặt lại có thể thông báo cho mọi người khác rằng họ sẽ cần chú ý vào lần sau. lấy (!).

Một giải pháp rõ ràng mà tôi đã thấy sẽ hoạt động nếu bạn không có cam kết địa phương foovà nó được phục hồi:

git fetch
git checkout foo
git reset --hard origin/foo

Điều này sẽ chỉ đơn giản là loại bỏ trạng thái cục bộ có foolợi cho lịch sử của nó theo kho lưu trữ từ xa.

Nhưng làm thế nào để đối phó với tình huống nếu một người đã thực hiện những thay đổi cục bộ đáng kể trên nhánh đó?


+1 cho công thức trường hợp đơn giản. Nó lý tưởng để đồng bộ hóa cá nhân giữa các máy, đặc biệt nếu chúng có hệ điều hành khác nhau. Đó là điều cần được đề cập trong sách hướng dẫn.
Philip Oakley

Công thức lý tưởng để đồng bộ hóa cá nhân là git pull --rebase && git push. Nếu bạn chỉ làm việc dựa trên master, thì điều này sẽ rất gần với việc làm đúng với bạn, ngay cả khi bạn đã từ chối và thúc đẩy ở đầu bên kia.
Aristotle Pagaltzis

Vì tôi đang đồng bộ hóa và phát triển giữa máy PC và máy Linux nên tôi thấy rằng việc sử dụng nhánh mới cho mọi rebase / update hoạt động tốt. Bây giờ tôi cũng sử dụng biến thể git reset --hard @{upstream}mà tôi biết rằng câu thần chú refspec kỳ diệu cho "quên những gì tôi đã / có, sử dụng những gì tôi đã tìm nạp từ điều khiển từ xa" Xem nhận xét cuối cùng của tôi tại stackoverflow.com/a/15284176/717355
Philip Oakley

Bạn sẽ có thể, với Git2.0, để tìm nguồn gốc cũ của chi nhánh của bạn (trước khi chi nhánh thượng nguồn được viết lại với một push -f): xem câu trả lời của tôi dưới đây
VonC

Câu trả lời:


75

Lấy lại đồng bộ sau khi rebase được đẩy thực sự không quá phức tạp trong hầu hết các trường hợp.

git checkout foo
git branch old-foo origin/foo # BEFORE fetching!!
git fetch
git rebase --onto origin/foo old-foo foo
git branch -D old-foo

I E. trước tiên, bạn thiết lập một đánh dấu cho vị trí ban đầu của chi nhánh từ xa, sau đó bạn sử dụng đánh dấu đó để phát lại các cam kết cục bộ của mình từ thời điểm đó trở đi vào chi nhánh từ xa được khôi phục.

Rebasing giống như bạo lực: nếu nó không giải quyết được vấn đề của bạn, bạn chỉ cần thêm nó. ☺

Tất nhiên, bạn có thể thực hiện việc này mà không cần dấu trang, nếu bạn tra cứu origin/fooID cam kết pre-rebase và sử dụng nó.

Đây cũng là cách bạn giải quyết tình huống quên đánh dấu trang trước khi tìm nạp. Không có gì bị mất - bạn chỉ cần kiểm tra nhật ký lại cho nhánh từ xa:

git reflog show origin/foo | awk '
    PRINT_NEXT==1 { print $1; exit }
    /fetch: forced-update/ { PRINT_NEXT=1 }'

Thao tác này sẽ in ID cam kết origin/foođã trỏ đến trước lần tìm nạp gần đây nhất đã thay đổi lịch sử của nó.

Sau đó bạn có thể đơn giản

git rebase --onto origin/foo $commit foo

11
Lưu ý nhanh: Tôi nghĩ nó khá trực quan, nhưng nếu bạn không biết rõ về awk ... thì một lớp lót đó chỉ đang xem qua kết quả đầu ra của git reflog show origin/foodòng đầu tiên nói rằng "tìm nạp: buộc cập nhật"; đó là những gì git ghi lại khi một lần tìm nạp khiến nhánh từ xa thực hiện bất cứ điều gì ngoại trừ tua đi nhanh. (Bạn cũng có thể làm điều đó bằng tay - bản cập nhật bắt buộc có lẽ là bản cập nhật gần đây nhất.)
Cascabel

2
Nó không giống như bạo lực. Bạo lực đôi khi là niềm vui
Iolo

5
@iolo Đúng, việc phục hồi luôn thú vị.
Dan Bechard

1
Giống như bạo lực, hầu như luôn luôn tránh nổi loạn. Nhưng có một manh mối làm thế nào.
Bob Stein

2
Tốt, tránh đẩy một rebase nơi những người khác sẽ bị ảnh hưởng.
Aristotle Pagaltzis

11

Tôi muốn nói rằng việc khôi phục từ phần rebase ngược dòng của trang git-rebase man bao gồm khá nhiều điều này.

Nó thực sự không khác gì khôi phục từ rebase của riêng bạn - bạn di chuyển một nhánh và rebase tất cả các nhánh đã có trong lịch sử của họ lên vị trí mới.


4
À, ra là vậy. Nhưng mặc dù bây giờ tôi hiểu nó nói gì, nhưng tôi sẽ không có trước đây, trước khi tự mình tìm ra điều này. Và không có công thức sách nấu ăn (có lẽ đúng như vậy trong tài liệu như vậy). Tôi cũng sẽ trình bày rằng gọi "trường hợp khó" là FUD Tôi đệ trình rằng lịch sử được viết lại có thể quản lý nhẹ nhàng ở quy mô của hầu hết các phát triển nội bộ. Cách thức mê tín mà đối tượng này luôn bị đối xử khiến tôi khó chịu.
Aristotle Pagaltzis

4
@Aristotle: Bạn nói đúng rằng nó rất dễ quản lý, vì tất cả các nhà phát triển đều biết cách sử dụng git và bạn có thể giao tiếp hiệu quả với tất cả các nhà phát triển. Trong một thế giới hoàn hảo, đó sẽ là kết thúc của câu chuyện. Nhưng rất nhiều dự án ngoài kia đủ lớn đến mức một rebase ngược dòng thực sự là một điều đáng sợ. (Và sau đó có những nơi như nơi làm việc của tôi, nơi hầu hết các nhà phát triển thậm chí chưa bao giờ nghe nói về rebase.) Tôi nghĩ "mê tín" chỉ là một cách cung cấp lời khuyên an toàn nhất, chung chung nhất có thể. Không ai muốn trở thành người gây ra thảm họa trong repo của người khác.
Cascabel

2
Vâng, tôi hiểu động cơ. Và tôi hoàn toàn đồng ý với điều đó. Nhưng có một thế giới khác biệt giữa “đừng thử điều này nếu bạn không hiểu hậu quả” và “bạn không bao giờ nên làm điều đó vì nó xấu xa”, và điều này chỉ có một mình tôi lo lắng. Hướng dẫn luôn tốt hơn là gieo rắc nỗi sợ hãi.
Aristotle Pagaltzis

@Aristotle: Đồng ý. Tôi cố gắng hướng đến kết thúc "hãy chắc chắn rằng bạn biết mình đang làm gì", nhưng đặc biệt là trực tuyến, tôi cố gắng cung cấp cho nó đủ trọng lượng để một khách truy cập bình thường từ google sẽ lưu ý. Bạn nói đúng, rất nhiều thứ có lẽ nên được giảm bớt.
Cascabel

11

Bắt đầu với git 1.9 / 2.0 Q1 2014, bạn sẽ không phải đánh dấu nguồn gốc nhánh trước của mình trước khi khôi phục nó trên nhánh ngược dòng được viết lại, như được mô tả trong câu trả lời của Aristotle Pagaltzis : Xem commit 07d406bcommit d96855f :

Sau khi làm việc với topicnhánh được tạo bằng git checkout -b topic origin/master, lịch sử của nhánh theo dõi từ xa origin/mastercó thể đã được viết lại và xây dựng lại, dẫn đến lịch sử của hình dạng này:

                   o---B1
                  /
  ---o---o---B2--o---o---o---B (origin/master)
          \
           B3
            \
             Derived (topic)

nơi origin/mastersử dụng đến thời điểm tại các cam kết B3, B2, B1và bây giờ nó chỉ vào B, và bạn topicchi nhánh đã bắt đầu trên đầu trang của nó sao khi origin/masterđang ở B3.

Chế độ này sử dụng bản ghi lại của origin/masterđể tìm B3làm điểm rẽ nhánh, để topiccó thể khôi phục lại trên đầu bản cập nhậtorigin/master bằng cách:

$ fork_point=$(git merge-base --fork-point origin/master topic)
$ git rebase --onto origin/master $fork_point topic

Đó là lý do tại sao git merge-baselệnh có một tùy chọn mới:

--fork-point::

Tìm điểm mà tại đó một nhánh (hoặc bất kỳ lịch sử nào dẫn đến <commit>) được tách từ một nhánh khác (hoặc bất kỳ tham chiếu nào) <ref>.
Điều này không chỉ tìm kiếm tổ tiên chung của hai cam kết, mà còn tính đến bản tóm tắt của <ref>để xem liệu lịch sử dẫn đến phân <commit>nhánh từ một hậu thân trước đó của nhánh hay không<ref> .


Lệnh " git pull --rebase" tính toán điểm rẽ nhánh của nhánh đang được phục hồi bằng cách sử dụng các mục nhập reflog của basenhánh "" (thường là nhánh theo dõi từ xa) mà công việc của nhánh dựa vào, để đối phó với trường hợp "cơ sở" chi nhánh đã được quấn lại và xây dựng lại.

Ví dụ: nếu lịch sử trông giống như nơi:

  • đầu hiện tại của basenhánh "" đang ở B, nhưng lần tìm nạp trước đó đã quan sát thấy rằng đầu của nó đã từng như vậy B3và sau B2đó B1 trước khi đến cam kết hiện tại và
  • nhánh được phục hồi trên đầu "cơ sở" mới nhất dựa trên cam kết B3,

nó cố gắng tìm B3bằng cách đi qua đầu ra của " git rev-list --reflog base" (có nghĩa là B, B1, B2, B3) cho đến khi nó tìm thấy một cam kết rằng là tổ tiên của mũi hiện tại " Derived (topic)".

Trong nội bộ, chúng tôi có get_merge_bases_many()thể tính toán điều này bằng một lần thực hiện.
Chúng tôi muốn có một cơ sở hợp nhất giữa Derivedvà một cam kết hợp nhất hư cấu sẽ dẫn đến việc hợp nhất tất cả các mẹo lịch sử của " base (origin/master)".
Khi một cam kết như vậy tồn tại, chúng ta sẽ nhận được một kết quả duy nhất, kết quả này khớp chính xác với một trong các mục nhập reflog của " base".


Git 2.1 (Quý 3 năm 2014) sẽ thêm vào làm cho tính năng này trở nên mạnh mẽ hơn: xem cam kết 1e0dacd của John Giữ ( johnkeeping)

xử lý chính xác tình huống mà chúng tôi có cấu trúc liên kết sau:

    C --- D --- E  <- dev
   /
  B  <- master@{1}
 /
o --- B' --- C* --- D*  <- master

Ở đâu:

  • B'là một phiên bản cố định của Bnó không giống với B;
  • C*D*là bản vá giống hệt nhau CDtương ứng và xung đột về văn bản nếu áp dụng sai thứ tự;
  • Ephụ thuộc vào văn bản D.

Đúng kết quả của git rebase master devBđược xác định là ngã ba-điểm devmaster, do đó C, D, Elà những cam kết mà cần phải được thực hiện lại vào master; nhưng CDlà bản vá giống hệt với C*D*và vì vậy có thể bị loại bỏ, để kết quả cuối cùng là:

o --- B' --- C* --- D* --- E  <- dev

Nếu điểm rẽ nhánh không được xác định, thì việc chọn Bmột nhánh có chứa B'dẫn đến xung đột và nếu các cam kết giống hệt bản vá không được xác định chính xác thì việc chọn Cvào một nhánh chứa D(hoặc tương đương D*) dẫn đến xung đột.


Các " --fork-point" phương thức " git rebase" thụt lùi khi lệnh được viết lại bằng C trở lại trong 2.20 thời đại, mà đã được sửa chữa với Git 2,27 (Q2 2020).

Xem cam kết f08132f ( 09/12/2019 ) của Junio ​​C Hamano ( gitster) .
(Hợp nhất bởi Junio ​​C Hamano - gitster- trong cam kết fb4175b , ngày 27 tháng 3 năm 2020)

rebase: --fork-pointsửa chữa hồi quy

Người ký hợp đồng: Alex Torok
[jc: sửa đổi bản sửa lỗi và sử dụng các bài kiểm tra của Alex]
Người ký hợp đồng: Junio ​​C Hamano

" git rebase --fork-point master" được sử dụng để hoạt động OK, vì nó được gọi trong nội bộ " git merge-base --fork-point" biết cách xử lý tên đổi tên ngắn và chuyển nó thành tên đổi tên đầy đủ trước khi gọi get_fork_point()hàm cơ bản .

Điều này không còn đúng nữa sau khi lệnh được viết lại trong C, vì lệnh internall của nó được thực hiện trực tiếp để get_fork_point()không tạo ra một tham chiếu ngắn.

Di chuyển logic "dwim the refname refname to the full refname" được sử dụng trong "git merge-base" vào get_fork_point()hàm cơ bản để người gọi hàm khác trong quá trình triển khai "git rebase" hoạt động theo cách tương tự để khắc phục hồi quy này.


1
Lưu ý rằng một push --force git có thể bây giờ (git 1.8.5) được thực hiện một cách thận trọng hơn: stackoverflow.com/a/18505634/6309
VonC
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.