Các cam kết của Git được sao chép trong cùng một nhánh sau khi thực hiện rebase


131

Tôi hiểu kịch bản được trình bày trong Pro Git về Nguy cơ Rebasing . Về cơ bản, tác giả cho bạn biết cách tránh các cam kết trùng lặp:

Không rebase cam kết rằng bạn đã đẩy lên kho lưu trữ công khai.

Tôi sẽ cho bạn biết tình huống cụ thể của tôi bởi vì tôi nghĩ rằng nó không hoàn toàn phù hợp với kịch bản Pro Git và tôi vẫn kết thúc với các cam kết trùng lặp.

Giả sử tôi có hai chi nhánh từ xa với các đối tác địa phương của họ:

origin/master    origin/dev
|                |
master           dev

Tất cả bốn nhánh đều chứa các cam kết giống nhau và tôi sẽ bắt đầu phát triển trong dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Sau một vài lần cam kết, tôi đẩy các thay đổi thành origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Tôi phải quay lại để mastersửa chữa nhanh chóng:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

Và quay lại, devtôi căn cứ lại các thay đổi để đưa vào bản sửa lỗi nhanh chóng trong quá trình phát triển thực tế của mình:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Nếu tôi hiển thị lịch sử các cam kết với GitX / gitk, tôi nhận thấy rằng origin/devbây giờ có hai cam kết giống hệt nhau C5'C6'khác với Git. Bây giờ nếu tôi đẩy các thay đổi vào origin/devđây là kết quả:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Có thể tôi không hiểu hết lời giải thích trong Pro Git, vì vậy tôi muốn biết hai điều:

  1. Tại sao Git sao chép các cam kết này trong khi giảm giá? Có lý do cụ thể nào để làm điều đó thay vì chỉ áp dụng C5C6sau đó C7không?
  2. Làm thế nào tôi có thể tránh điều đó? Nó sẽ là khôn ngoan để làm điều đó?

Câu trả lời:


87

Bạn không nên sử dụng rebase ở đây, một hợp nhất đơn giản là đủ. Cuốn sách Pro Git mà bạn đã liên kết về cơ bản giải thích tình huống chính xác này. Các hoạt động bên trong có thể hơi khác một chút, nhưng đây là cách tôi hình dung nó:

  • C5C6tạm thời bị rút khỏidev
  • C7 Được áp dụng cho dev
  • C5C6được phát lại trên đầu trang C7, tạo ra các điểm khác biệt mới và do đó các cam kết mới

Vì vậy, trong devchi nhánh của bạn , C5C6thực sự không còn tồn tại nữa: họ hiện tại C5'C6'. Khi bạn push to origin/dev, git thấy C5'C6'như cam kết mới và đinh họ vào phần cuối của lịch sử. Thật vậy, nếu bạn nhìn vào sự khác biệt giữa C5C5'trong origin/dev, bạn sẽ nhận thấy rằng mặc dù nội dung giống nhau, nhưng số dòng có thể khác nhau - điều này làm cho hàm băm của cam kết khác nhau.

Tôi sẽ trình bày lại quy tắc Pro Git: không bao giờ rebase các cam kết đã từng tồn tại ở bất kỳ đâu ngoại trừ kho lưu trữ cục bộ của bạn . Sử dụng hợp nhất để thay thế.


Tôi gặp vấn đề tương tự, làm cách nào để sửa lịch sử chi nhánh từ xa của mình bây giờ, có tùy chọn nào khác ngoài việc xóa chi nhánh và tạo lại nó bằng cách hái anh đào không ??
Wazery

1
@xdsy: Hãy xem cái nàycái này .
Justin ᚅᚔᚈᚄᚒᚔ

2
Bạn nói "C5 và C6 tạm thời bị rút khỏi nhà phát triển ... C7 được áp dụng cho nhà phát triển". Nếu đúng như vậy, thì tại sao C5 và C6 lại hiển thị trước C7 theo thứ tự của các cam kết trên origin / dev?
KJ50

@ KJ50: Vì C5 và C6 đã được đẩy lên origin/dev. Khi devđược phục hồi, lịch sử của nó được sửa đổi (C5 / C6 tạm thời bị xóa và áp dụng lại sau C7). Việc sửa đổi lịch sử của các repo đã đẩy thường là một Ý tưởng Thực sự Xấu trừ khi bạn biết mình đang làm gì. Trong trường hợp đơn giản này, vấn đề có thể được giải quyết bằng cách thực hiện một sự thúc đẩy có hiệu lực từ devđể origin/devsau rebase và thông báo cho bất cứ ai khác làm việc tắt của origin/devrằng họ đang có lẽ sắp có một ngày tồi tệ. Câu trả lời tốt hơn, một lần nữa, là "đừng làm điều đó ... thay vào đó hãy sử dụng hợp nhất"
Justin ᚅᚔᚈᚄᚒᚔ

3
Một điều cần lưu ý: Hàm băm của C5 và C5 'chắc chắn là khác nhau, nhưng không phải vì số dòng khác nhau, mà đối với hai dữ kiện sau đây mà bất kỳ dữ kiện nào cũng đủ cho sự khác biệt: 1) băm mà chúng ta đang nói đến là băm của toàn bộ cây nguồn sau khi cam kết, không phải băm của sự khác biệt delta và do đó C5 'chứa bất kỳ thứ gì đến từ C7, trong khi C5 thì không, và 2) Cha của C5' khác với C5 và thông tin này cũng được bao gồm trong nút gốc của cây cam kết ảnh hưởng đến kết quả băm.
Ozgur Murat

113

Câu trả lời ngắn

Bạn đã bỏ qua thực tế rằng bạn đã chạy git push, gặp lỗi sau và sau đó tiếp tục chạy git pull:

To git@bitbucket.org:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Mặc dù Git cố gắng tỏ ra hữu ích, nhưng lời khuyên 'git pull' của nó rất có thể không phải là điều bạn muốn làm .

Nếu bạn là:

  • Làm việc trên một "tính năng chi nhánh" hoặc "chi nhánh phát triển" một mình , sau đó bạn có thể chạy git push --forceđể cập nhật từ xa với cam kết sau rebase bạn ( theo câu trả lời của user4405677 ).
  • Làm việc trên một nhánh với nhiều nhà phát triển cùng một lúc, thì có lẽ bạn không nên sử dụnggit rebase ngay từ đầu. Để cập nhật devcác thay đổi từ master, bạn nên, thay vì chạy git rebase master dev, hãy chạy git merge mastertrong khi bật dev( theo câu trả lời của Justin ).

Giải thích dài hơn một chút

Mỗi băm cam kết trong Git dựa trên một số yếu tố, một trong số đó là băm của cam kết đứng trước nó.

Nếu bạn sắp xếp lại các cam kết, bạn sẽ thay đổi các băm cam kết; phục hồi (khi nó làm điều gì đó) sẽ thay đổi các băm cam kết. Cùng với đó, kết quả của quá trình chạy git rebase master dev, nơi devkhông đồng bộ với master, sẽ tạo ra các cam kết mới (và do đó băm) có cùng nội dung với nội dung trên devnhưng với các cam kết masterđược chèn trước chúng.

Bạn có thể rơi vào tình huống như thế này theo nhiều cách. Tôi có thể nghĩ ra hai cách:

  • Bạn có thể có những cam kết mastermà bạn muốn làm cơ sở cho devcông việc của mình
  • Bạn có thể có các cam kết devđã được đẩy đến một điều khiển từ xa, sau đó bạn sẽ tiến hành thay đổi (đổi từ khóa thông báo cam kết, sắp xếp lại các cam kết, cam kết bí, v.v.)

Hãy hiểu rõ hơn điều gì đã xảy ra — đây là một ví dụ:

Bạn có một kho lưu trữ:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Tập hợp ban đầu của các cam kết tuyến tính trong một kho lưu trữ

Sau đó, bạn tiến hành thay đổi cam kết.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(Đây là nơi bạn sẽ phải nghe lời tôi: có một số cách để thay đổi các cam kết trong Git. Trong ví dụ này, tôi đã thay đổi thời gian C3, nhưng bạn sẽ chèn các cam kết mới, thay đổi thông báo cam kết, sắp xếp lại các cam kết, cam kết chặt chẽ với nhau, v.v.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

Cam kết tương tự với các hàm băm mới

Đây là nơi điều quan trọng cần lưu ý rằng các băm cam kết là khác nhau. Đây là hành vi được mong đợi vì bạn đã thay đổi điều gì đó (bất cứ điều gì) về chúng. Điều này không sao, NHƯNG:

Nhật ký biểu đồ cho thấy trang cái đó không đồng bộ với điều khiển từ xa

Cố gắng đẩy sẽ hiển thị cho bạn một lỗi (và gợi ý rằng bạn nên chạy git pull).

$ git push origin master
To git@bitbucket.org:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@bitbucket.org:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Nếu chúng tôi chạy git pull, chúng tôi thấy nhật ký này:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Hoặc, được hiển thị theo cách khác:

Nhật ký biểu đồ hiển thị cam kết hợp nhất

Và bây giờ chúng tôi có các cam kết trùng lặp tại địa phương. Nếu chúng tôi chạy, git pushchúng tôi sẽ gửi chúng đến máy chủ.

Để tránh đến giai đoạn này, chúng tôi có thể đã chạy git push --force(thay vào đó chúng tôi đã chạy git pull). Điều này sẽ gửi cam kết của chúng tôi với các băm mới đến máy chủ mà không có vấn đề gì. Để khắc phục sự cố ở giai đoạn này, chúng tôi có thể đặt lại về trước khi chạy git pull:

Nhìn vào reflog ( git reflog) để biết giá trị băm cam kết trước khi chúng ta chạy git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Ở trên, chúng ta thấy đó ba7688alà cam kết mà chúng ta đã thực hiện trước khi chạy git pull. Với hàm băm cam kết đó trong tay, chúng ta có thể đặt lại về đó ( git reset --hard ba7688a) và sau đó chạy git push --force.

Và chúng tôi đã hoàn thành.

Nhưng chờ đã, tôi tiếp tục làm việc dựa trên các cam kết trùng lặp

Nếu bằng cách nào đó, bạn không nhận thấy rằng các cam kết đã bị trùng lặp và tiếp tục tiếp tục làm việc ở đầu các cam kết trùng lặp, bạn thực sự đã tự làm rối mình. Kích thước của mớ hỗn độn tỷ lệ thuận với số lượng cam kết bạn có trên các bản sao.

Cái này trông như thế nào:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Nhật ký git hiển thị các cam kết tuyến tính ở đầu các cam kết trùng lặp

Hoặc, được hiển thị theo cách khác:

Biểu đồ nhật ký hiển thị các cam kết tuyến tính ở đầu các cam kết trùng lặp

Trong trường hợp này, chúng tôi muốn xóa các cam kết trùng lặp, nhưng vẫn giữ các cam kết mà chúng tôi đã dựa trên chúng — chúng tôi muốn giữ lại từ C6 đến C10. Như với hầu hết mọi thứ, có một số cách để thực hiện điều này:

Hoặc:

  • Tạo một nhánh mới tại lần cam kết 1 được sao chép cuối cùng , cherry-pickmỗi lần cam kết (bao gồm từ C6 đến C10) vào nhánh mới đó và coi nhánh mới đó là chính tắc.
  • Chạy git rebase --interactive $commit, $commitcam kết ở đâu trước cả hai cam kết được sao chép 2 . Tại đây chúng tôi có thể xóa hoàn toàn các dòng cho các bản sao.

1 Không quan trọng bạn chọn cái nào trong hai cái ba7688ahoặc 2a2e220hoạt động tốt.

2 Trong ví dụ, nó sẽ là 85f59ab.

TL; DR

Đặt advice.pushNonFastForwardthành false:

git config --global advice.pushNonFastForward false

1
Bạn có thể làm theo lời khuyên "git pull ..." miễn là người ta nhận ra dấu chấm lửng ẩn tùy chọn "--rebase" (còn gọi là "-r"). ;-)
G. Sylvie Davies

4
Tôi sẽ khuyên bạn sử dụng git push--force-with-leasengày nay như đó là một mặc định tốt hơn
Whymarrh

4
Đó là câu trả lời này hoặc là cỗ máy thời gian. Cảm ơn!
ZeMoon

Lời giải thích rất gọn gàng ... Tôi đã tình cờ gặp một vấn đề tương tự đã sao chép mã của tôi 5-6 lần sau khi tôi cố gắng rebase liên tục ... chỉ để đảm bảo mã được cập nhật với chính ... nhưng lần nào nó cũng được đẩy cam kết mới đối với chi nhánh của tôi, đồng thời sao chép mã của tôi. Bạn có thể vui lòng cho tôi biết liệu lực đẩy (với tùy chọn thuê) có an toàn để thực hiện ở đây không nếu tôi là nhà phát triển duy nhất làm việc trên chi nhánh của mình? Hoặc sáp nhập tổng thể vào của tôi thay vì phục hồi là cách tốt hơn?
Dhruv Singhal

12

Tôi nghĩ rằng bạn đã bỏ qua một chi tiết quan trọng khi mô tả các bước của mình. Cụ thể hơn, bước cuối cùng của bạn, git pushtrên nhà phát triển, sẽ thực sự gây ra lỗi cho bạn, vì thông thường bạn không thể đẩy các thay đổi không nhanh.

Vì vậy, bạn đã làm git pulltrước lần đẩy cuối cùng, dẫn đến một cam kết hợp nhất với C6 và C6 'là cha mẹ, đó là lý do tại sao cả hai sẽ vẫn được liệt kê trong nhật ký. Một định dạng nhật ký đẹp hơn có thể làm rõ ràng hơn rằng chúng là các nhánh hợp nhất của các cam kết trùng lặp.

Hoặc bạn đã thực hiện git pull --rebase(hoặc không rõ ràng --rebasenếu nó được cấu hình của bạn ngụ ý), điều này đã kéo C5 và C6 ban đầu trở lại nhà phát triển cục bộ của bạn (và tiếp tục khôi phục những cái sau thành hàm băm mới, C7 'C5' 'C6' ').

Một cách để giải quyết vấn đề này có thể là git push -fbuộc đẩy khi nó đưa ra lỗi và xóa C5 C6 khỏi nguồn gốc, nhưng nếu bất kỳ ai khác cũng đã kéo họ trước khi bạn xóa chúng, bạn sẽ gặp nhiều rắc rối hơn .. . Về cơ bản, tất cả mọi người có C5 C6 sẽ cần thực hiện các bước đặc biệt để loại bỏ chúng. Đó chính là lý do tại sao họ nói rằng bạn không bao giờ nên căn cứ lại bất kỳ thứ gì đã được xuất bản. Tuy nhiên, vẫn có thể thực hiện được nếu nói "xuất bản" là trong một nhóm nhỏ.


1
Việc bỏ sót git pulllà rất quan trọng. Đề xuất của bạn git push -f, mặc dù nguy hiểm, có lẽ là những gì độc giả đang tìm kiếm.
Whymarrh

Thật. Quay lại khi tôi viết câu hỏi mà tôi thực sự đã làm git push --force, chỉ để xem Git sẽ làm gì. Tôi đã học rất nhiều về Git kể từ đó và ngày nay rebaselà một phần trong quy trình làm việc bình thường của tôi. Tuy nhiên, tôi làm git push --force-with-leaseđể tránh ghi đè lên tác phẩm của người khác.
elitalon

Sử dụng --force-with-leaseđược một mặc định tốt, tôi sẽ để lại nhận xét dưới câu trả lời của tôi cũng
Whymarrh

2

Tôi phát hiện ra rằng trong trường hợp của mình, vấn đề này là hậu quả của sự cố cấu hình Git. (Liên quan đến kéo và hợp nhất)

Mô tả vấn đề:

Sympthoms: Các cam kết được sao chép trên nhánh con sau rebase, ngụ ý nhiều hợp nhất trong và sau rebase.

Dòng công việc: Dưới đây là các bước của dòng công việc tôi đã thực hiện:

  • Làm việc trên "Features-branch" (con của "Develop-branch")
  • Cam kết và Đẩy các thay đổi trên "Chi nhánh tính năng"
  • Kiểm tra "Develop-branch" (Nhánh mẹ của các Tính năng) và làm việc với nó.
  • Cam kết và thúc đẩy các thay đổi trên "Develop-branch"
  • Kiểm tra "Tính năng-chi nhánh" và lấy các thay đổi từ kho lưu trữ (Trong trường hợp người khác đã thực hiện công việc)
  • Rebase "Features-branch" thành "Develop-branch"
  • Lực đẩy của các thay đổi trên "Chi nhánh tính năng"

Là các chuỗi của quy trình làm việc này, sao chép tất cả các cam kết của "Feature-branch" kể từ rebase trước đó ... :-(

Vấn đề là do kéo các thay đổi của nhánh con trước rebase. Cấu hình kéo mặc định của Git là "hợp nhất". Đây là thay đổi chỉ mục của các cam kết được thực hiện trên nhánh con.

Giải pháp: trong tệp cấu hình Git, định cấu hình pull để hoạt động ở chế độ rebase:

...
[pull]
    rebase = preserve
...

Hy vọng nó có thể giúp JN Grx


1

Bạn có thể đã kéo từ một nhánh từ xa khác với nhánh hiện tại của bạn. Ví dụ, bạn có thể đã rút khỏi Master khi chi nhánh của bạn đang phát triển theo dõi phát triển. Git sẽ thực hiện nghiêm túc các cam kết trùng lặp nếu được kéo từ một nhánh không được theo dõi.

Nếu điều này xảy ra, bạn có thể làm như sau:

git reset --hard HEAD~n

Ở đâu n == <number of duplicate commits that shouldn't be there.>

Sau đó, đảm bảo rằng bạn đang kéo từ đúng nhánh và sau đó chạy:

git pull upstream <correct remote branch> --rebase

Kéo theo --rebasesẽ đảm bảo bạn không thêm các cam kết không liên quan có thể làm mờ lịch sử cam kết.

Đây là một chút nắm bắt đối với git rebase.

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.