Làm thế nào để hoàn nguyên nhiều cam kết git?


983

Tôi có một kho git trông như thế này:

A -> B -> C -> D -> HEAD

Tôi muốn người đứng đầu chi nhánh chỉ vào A, tức là tôi muốn B, C, D và HEAD biến mất và tôi muốn đầu đồng nghĩa với A.

Có vẻ như tôi có thể cố gắng bắt đầu lại (không áp dụng, vì tôi đã đẩy các thay đổi ở giữa) hoặc hoàn nguyên. Nhưng làm thế nào để tôi hoàn nguyên nhiều cam kết? Tôi có hoàn nguyên từng cái một không? Là thứ tự quan trọng?


3
Nếu bạn chỉ muốn đặt lại điều khiển từ xa, bạn có thể ghi đè nó bằng bất cứ thứ gì! Nhưng chúng ta hãy sử dụng cam kết thứ tư trước đây: git push -f HEAD~4:master(giả sử nhánh từ xa là chủ). Vâng, bạn có thể đẩy bất kỳ cam kết như thế.
u0b34a0f6ae

21
Nếu mọi người đã kéo bạn phải thực hiện một cam kết hoàn nguyên các thay đổi bằng cách sử dụng git revert.
Jakub Narębski

1
Sử dụng git show HEAD ~ 4 để đảm bảo bạn đang đẩy sang phải để điều khiển từ xa
Mâtt Frëëman


5
"Thứ tự có quan trọng không?" Có, nếu các xác nhận ảnh hưởng đến cùng một dòng trong cùng một tệp. Sau đó, bạn nên bắt đầu hoàn nguyên các cam kết gần đây nhất và tìm đường quay trở lại.
avandeursen

Câu trả lời:


1336

Mở rộng những gì tôi đã viết trong một bình luận

Nguyên tắc chung là bạn không nên viết lại (thay đổi) lịch sử mà bạn đã xuất bản, bởi vì ai đó có thể đã dựa trên công việc của họ trên đó. Nếu bạn viết lại (thay đổi) lịch sử, bạn sẽ gặp vấn đề với việc hợp nhất các thay đổi của chúng và với việc cập nhật cho chúng.

Vì vậy, giải pháp là tạo ra một mới cam kết đó trở lại trạng thay đổi mà bạn muốn để có được thoát khỏi. Bạn có thể làm điều này bằng cách sử dụng lệnh git Revert .

Bạn có tình huống sau:

A <- B <- C <- D <- master <- ĐẦU

(mũi tên ở đây đề cập đến hướng của con trỏ: tham chiếu "cha mẹ" trong trường hợp xác nhận, cam kết hàng đầu trong trường hợp đầu chi nhánh (chi nhánh ref) và tên của chi nhánh trong trường hợp tham chiếu CHÍNH).

Những gì bạn cần tạo là như sau:

A <- B <- C <- D <- [(BCD) ^ - 1] <- master <- CHÍNH

trong đó "[(BCD) ^ - 1]" có nghĩa là cam kết hoàn nguyên các thay đổi trong các cam kết B, C, D. Toán học cho chúng ta biết rằng (BCD) ^ - 1 = D ^ -1 C ^ -1 B ^ -1, vì vậy bạn có thể nhận được tình huống cần thiết bằng các lệnh sau:

$ git revert --no-commit D
$ git revert --no-commit C
$ git revert --no-commit B
$ git commit -m "the commit message"

Giải pháp thay thế sẽ là kiểm tra nội dung của cam kết A và cam kết trạng thái này:

$ git checkout -f A -- .
$ git commit -a

Sau đó, bạn sẽ có tình huống sau đây:

A <- B <- C <- D <- A '<- master <- CHÍNH

Cam kết A 'có cùng nội dung với cam kết A, nhưng là một cam kết khác (thông điệp cam kết, cha mẹ, ngày cam kết).

Các giải pháp của Jeff Ferland, sửa đổi bởi Charles Bailey xây dựng dựa trên ý tưởng tương tự, nhưng sử dụng thiết lập lại git :

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit

40
Nếu bạn đã thêm các tệp trong B, C hoặc D. git checkout -f A -- .Sẽ không xóa các tệp này, bạn sẽ phải thực hiện thủ công. Tôi đã áp dụng chiến lược này ngay bây giờ, cảm ơn Jakub
oma

18
Những giải pháp đó không tương đương. Cái đầu tiên không xóa các tập tin mới được tạo.
m33lky

10
@Jerry: git checkout foocó thể có nghĩa là chi nhánh thanh toán foo(chuyển sang chi nhánh) hoặc tệp thanh toán foo (từ chỉ mục). --được sử dụng để định hướng, ví dụ như git checkout -- fooluôn luôn là về tập tin.
Jakub Narębski

87
Ngoài câu trả lời tuyệt vời. git revert --no-commit D C B
Tốc

9
@ welldan97: Cảm ơn bạn đã bình luận. Khi viết câu trả lời này git revertđã không chấp nhận nhiều cam kết; nó là một bổ sung khá mới.
Jakub Narębski

248

Cách sạch mà tôi thấy hữu ích

git revert --no-commit HEAD~3..

Lệnh này hoàn nguyên 3 lần xác nhận cuối cùng chỉ với một lần xác nhận.

Cũng không viết lại lịch sử.


16
Đây là câu trả lời đơn giản và tốt nhất
Julien Deniau

3
@JohnLittle nó giai đoạn thay đổi. git committừ đó sẽ thực sự cam kết.
x1a4

14
Điều này sẽ không hoạt động nếu một số cam kết là hợp nhất cam kết.
MegaManX

5
Hai dấu chấm ở cuối làm gì?
thảo quả

5
@cardamom Những người chỉ định một phạm vi. HEAD~3..giống nhưHEAD~3..HEAD
Toine H

238

Để làm như vậy, bạn chỉ cần sử dụng lệnh hoàn nguyên , chỉ định phạm vi xác nhận bạn muốn được hoàn nguyên.

Xem xét ví dụ của bạn, bạn sẽ phải làm điều này (giả sử bạn đang ở chi nhánh 'chính chủ'):

git revert master~3..master

Điều này sẽ tạo ra một cam kết mới trong địa phương của bạn với cam kết nghịch đảo của B, C và D (có nghĩa là nó sẽ hoàn tác các thay đổi được giới thiệu bởi các cam kết này):

A <- B <- C <- D <- BCD' <- HEAD

129
git revert --no-commit HEAD~2..là một cách thành ngữ hơn một chút để làm điều đó. Nếu bạn ở nhánh chính, không cần chỉ định lại chủ. Các --no-committùy chọn cho phép git cố gắng để phục hồi tất cả các cam kết cùng một lúc, thay vì xả rác lịch sử với nhiều revert commit ...thông điệp (giả định rằng là những gì bạn muốn).
kubi

6
@Victor Tôi đã sửa phạm vi cam kết của bạn. Phần đầu của phạm vi là độc quyền, có nghĩa là nó không được bao gồm. Vì vậy, nếu bạn muốn hoàn nguyên 3 lần xác nhận cuối cùng, bạn cần bắt đầu phạm vi từ cha mẹ của lần xác nhận thứ 3, tức là master~3.

2
@kubi không có cách nào để đưa SHA vào thông điệp cam kết, sử dụng một cam kết duy nhất (phương thức của bạn nhưng không phải nhập thủ công các cam kết đã được hoàn nguyên)?
Chris S

@ChrisS Suy nghĩ đầu tiên của tôi là không sử dụng --no-commit(để bạn có được một cam kết riêng cho mỗi lần hoàn nguyên), và sau đó kết hợp tất cả chúng lại với nhau trong một cuộc nổi loạn tương tác. Thông báo cam kết kết hợp sẽ chứa tất cả các SHA và bạn có thể sắp xếp chúng theo cách bạn muốn bằng trình chỉnh sửa thông điệp cam kết yêu thích của bạn.
Radon Rosborough

71

Tương tự như câu trả lời của Jakub, điều này cho phép bạn dễ dàng chọn các cam kết liên tiếp để hoàn nguyên.

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'

9
Giải pháp của bạn đã làm việc tốt cho tôi, nhưng với một sửa đổi nhỏ. Nếu chúng ta gặp trường hợp này Z -> A -> B -> C -> D -> TRƯỚC và nếu tôi muốn quay lại trạng thái A, thì thật kỳ lạ tôi sẽ phải thực hiện git Revert --no-commit Z .. TRƯỚC
Bogdan

3
Đồng ý với @Bogdan, phạm vi hoàn nguyên là như thế: SHA_TO_REVERT_TO..HEAD
Vadym Tyemirov

11
Phạm vi là sai. Nó nên được B^..HEAD, nếu không B được loại trừ.
tessus

3
Đồng ý với @tessus, vì vậy điều đúng đắn sẽ là: git revert --no-commit B^..HEADhoặcgit revert --no-commit A..HEAD
Yoho

64
git reset --hard a
git reset --mixed d
git commit

Điều đó sẽ hoạt động như một sự hoàn nguyên cho tất cả chúng cùng một lúc. Đưa ra một thông điệp cam kết tốt.


4
Nếu anh ta muốn HEADtrông như thế Athì có lẽ anh ta muốn chỉ số khớp với nhau nên git reset --soft Dcó lẽ phù hợp hơn.
CB Bailey

2
--soft đặt lại không di chuyển chỉ mục, vì vậy khi anh ta cam kết, có vẻ như cam kết đến trực tiếp từ thay vì từ D. Điều đó sẽ khiến chi nhánh bị chia tách. --mixed để lại các thay đổi, nhưng di chuyển con trỏ chỉ mục, do đó D sẽ trở thành cam kết chính.
Jeff Ferland

5
Vâng, tôi nghĩ git reset --keep chính xác là những gì tôi có ở trên. Nó được phát hành trong phiên bản 1.7.1, được phát hành vào tháng 4 năm 2010, vì vậy câu trả lời không có ở thời điểm đó.
Jeff Ferland

git checkout Asau đó git commitở trên không làm việc cho tôi, nhưng câu trả lời này đã làm.
SimplGy

Tại sao git reset --mixed Dcần thiết? Cụ thể tại sao reset? Có phải bởi vì, nếu không thiết lập lại D, điều đó, CHÍNH sẽ chỉ vào A, khiến B, C và D bị "treo lủng lẳng" và thu gom rác - đó không phải là điều anh ta muốn? Nhưng tại sao --mixed? Bạn đã trả lời " --softđặt lại không di chuyển chỉ mục ..." Vì vậy, bằng cách di chuyển chỉ mục, điều này có nghĩa là chỉ mục sẽ chứa các thay đổi của D, trong khi Thư mục làm việc sẽ chứa các thay đổi của A - theo cách này git status, hoặc git diffso sánh Index [D] với Thư mục làm việc [A]) sẽ hiển thị chất; người dùng đó đang đi từ D trở lại A?
Hạt đậu đỏ

39

Trước tiên hãy chắc chắn rằng bản sao làm việc của bạn không được sửa đổi. Sau đó:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

và sau đó chỉ cần cam kết. Đừng quên ghi lại lý do hoàn nguyên.


1
Đã làm cho tôi! Có vấn đề với các thay đổi lỗi thời của nhánh tính năng được thực hiện trong nhánh phát triển (một số sửa lỗi), do đó nhánh tính năng phải ghi đè lên tất cả các thay đổi được thực hiện trong quá trình phát triển (bao gồm xóa một số tệp).
im lặng

1
Không hoạt động với các tệp nhị phân:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux

2
Đây là một giải pháp linh hoạt hơn nhiều so với câu trả lời được chấp nhận. Cảm ơn!
Brian Kung

2
re: sử dụng tệp nhị phângit diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
weinerk

2
Điều này sẽ hoạt động ngay cả khi bạn muốn hoàn nguyên một loạt các xác nhận có chứa các xác nhận hợp nhất. Khi sử dụng, git revert A..Zbạn sẽ nhận đượcerror: commit X is a merge but no -m option was given.
Juliusz Gonera

35

Tôi rất thất vọng vì câu hỏi này không thể trả lời được. Mọi câu hỏi khác liên quan đến cách hoàn nguyên chính xác và bảo tồn lịch sử. Câu hỏi này cho biết "Tôi muốn người đứng đầu chi nhánh chỉ vào A, tức là tôi muốn B, C, D và HEAD biến mất và tôi muốn đầu đồng nghĩa với A."

git checkout <branch_name>
git reset --hard <commit Hash for A>
git push -f

Tôi đã học được rất nhiều khi đọc bài đăng của Jakub, nhưng một số người trong công ty (có quyền truy cập để đẩy đến chi nhánh "thử nghiệm" của chúng tôi mà không cần Pull-Request) đã đẩy như 5 cam kết xấu cố gắng sửa chữa và sửa lỗi và sửa một lỗi mà anh ta đã thực hiện 5 lần trước. Không chỉ vậy, nhưng một hoặc hai Yêu cầu Kéo đã được chấp nhận, hiện đã tệ. Vì vậy, quên nó đi, tôi đã tìm thấy cam kết tốt cuối cùng (abc1234) và chỉ chạy tập lệnh cơ bản:

git checkout testing
git reset --hard abc1234
git push -f

Tôi đã nói với 5 người khác làm việc trong repo này rằng họ nên lưu ý những thay đổi của họ trong vài giờ qua và Wipe / Re-Branch từ thử nghiệm mới nhất. Kết thúc câu chuyện.


2
Tôi đã không công bố các cam kết, vì vậy đây là câu trả lời tôi cần. Cảm ơn, @Suamere.
Tom Barron

Cách tốt hơn để làm điều này là git push --force-with-lease, nó sẽ viết lại lịch sử chỉ khi không có ai khác cam kết với chi nhánh sau hoặc trong phạm vi của các cam kết để vapourize. Nếu những người khác đã sử dụng chi nhánh, thì lịch sử của nó sẽ không bao giờ được viết lại và cam kết đơn giản nên được hoàn nguyên rõ ràng.
frandroid

1
@frandroid "lịch sử không bao giờ nên được viết lại", chỉ có thỏa thuận sith trong tuyệt đối. Câu hỏi của chủ đề này, và quan điểm của câu trả lời của tôi, chính xác là đối với một kịch bản cụ thể, tất cả lịch sử nên được xóa sạch.
Suamere

@Suamere Chắc chắn, đó là câu hỏi. Nhưng như câu trả lời của bạn đề cập đến những gì bạn đã nói với những người khác, có khả năng gây rắc rối. Từ kinh nghiệm cá nhân, Push -f có thể làm rối tung cơ sở mã của bạn nếu người khác đã cam kết sau những gì bạn đang cố xóa. - Force-with-cho thuê đạt được kết quả tương tự, ngoại trừ việc nó tiết kiệm được mông của bạn nếu bạn chuẩn bị làm hỏng repo của mình. Tại sao phải nắm lấy cơ hội? Nếu - Force-with-cho thuê thất bại, bạn có thể xem cam kết nào được thực hiện, đánh giá đúng, điều chỉnh và thử lại.
frandroid

1
@Suamere Cảm ơn bạn! Tôi đồng ý câu hỏi nêu rõ nó muốn viết lại lịch sử. Tôi cũng ở trong hoàn cảnh giống như bạn và tôi đoán OP ở chỗ ai đó đã tạo ra hàng tá những điều xấu xí và những cam kết kỳ lạ và hoàn nguyên về sự hoàn nguyên một cách tình cờ (trong khi tôi đang đi nghỉ) và nhà nước cần phải được hoàn nguyên. Trong mọi trường hợp cùng với một cảnh báo tốt cho sức khỏe, đây sẽ là câu trả lời được chấp nhận.
Lee Richardson

9

Đây là một bản mở rộng của một trong những giải pháp được cung cấp trong câu trả lời của Jakub

Tôi đã phải đối mặt với một tình huống mà các cam kết tôi cần quay lại có phần phức tạp, với một số cam kết là các cam kết hợp nhất, và tôi cần tránh viết lại lịch sử. Tôi đã không thể sử dụng một loạt các git revertlệnh vì cuối cùng tôi đã gặp phải xung đột giữa các thay đổi đảo ngược được thêm vào. Tôi đã kết thúc bằng cách sử dụng các bước sau.

Đầu tiên, hãy kiểm tra nội dung của cam kết đích trong khi để lại CHÍNH ở đầu nhánh:

$ git checkout -f <target-commit> -- .

(- đảm bảo <target-commit>được hiểu là một cam kết chứ không phải là một tệp;. Tham chiếu đến thư mục hiện tại.)

Sau đó, xác định những tập tin nào đã được thêm vào trong các cam kết được khôi phục và do đó cần phải xóa:

$ git diff --name-status --cached <target-commit>

Các tệp đã được thêm sẽ hiển thị bằng chữ "A" ở đầu dòng và không có sự khác biệt nào khác. Bây giờ, nếu có bất kỳ tệp nào cần được xóa, giai đoạn các tệp này để xóa:

$ git rm <filespec>[ <filespec> ...]

Cuối cùng, cam kết đảo ngược:

$ git commit -m 'revert to <target-commit>'

Nếu muốn, hãy đảm bảo rằng chúng tôi trở lại trạng thái mong muốn:

$git diff <target-commit> <current-commit>

Không nên có sự khác biệt.


Bạn có chắc chắn rằng bạn có thể gitĐẦU chỉ với đầu cành?
Suamere

2
Đây là một giải pháp tốt hơn cho tôi, vì trong tôi đã có những cam kết hợp nhất.
sovemp

3

Cách dễ dàng để hoàn nguyên một nhóm các cam kết trên kho lưu trữ được chia sẻ (mà mọi người sử dụng và bạn muốn lưu giữ lịch sử) là sử dụng git revertcùng với git rev-list. Cái sau sẽ cung cấp cho bạn một danh sách các xác nhận, cái trước sẽ tự hoàn nguyên.

Có hai cách để làm điều đó. Nếu bạn muốn hoàn nguyên nhiều lần xác nhận trong một lần sử dụng cam kết:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert -n $i; done

điều này sẽ hoàn nguyên một nhóm các cam kết bạn cần, nhưng để lại tất cả các thay đổi trên cây làm việc của bạn, bạn nên cam kết tất cả chúng như bình thường.

Một tùy chọn khác là có một cam kết duy nhất cho mỗi thay đổi được hoàn nguyên:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-edit -s $i; done

Ví dụ, nếu bạn có một cây cam kết như

 o---o---o---o---o---o--->    
fff eee ddd ccc bbb aaa

để hoàn nguyên các thay đổi từ eee sang bbb , hãy chạy

for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done

chỉ sử dụng cái này Cảm ơn!
Ran Biron

2

Không ai trong số đó làm việc cho tôi, vì vậy tôi có ba lần hoàn nguyên (ba lần cam kết cuối cùng), vì vậy tôi đã làm:

git revert HEAD
git revert HEAD~2
git revert HEAD~4
git rebase -i HEAD~3 # pick, squash, squash

Làm việc như người ở :)


2
Đây chỉ là một lựa chọn khả thi nếu những thay đổi của bạn chưa được thúc đẩy.
kboom 18/03/18

0

Theo tôi một cách rất dễ dàng và sạch sẽ có thể là:

quay lại A

git checkout -f A

điểm đầu của chủ đến trạng thái hiện tại

git symbolic-ref HEAD refs/heads/master

tiết kiệm

git commit

1
Bạn có thể vui lòng giải thích lý do của downvote?
nulll

Điều này hoạt động tốt. Điểm đưa ra downvote cho câu trả lời hữu ích này là gì hoặc bất cứ ai có thể giải thích thực hành tốt nhất là gì?
Levent Divilioglu

Có giống như git checkout master; git reset --hard Avậy không? Hoặc nếu không bạn có thể giải thích thêm một chút về những gì nó làm?
MM

chỉ hoạt động, nhưng ĐẦU TIÊN tượng trưng dường như không phải là một lệnh "an toàn"
Sérgio

Tôi không muốn sửa chủ, tôi muốn sửa một nhánh
Sérgio

-6

Nếu bạn muốn tạm thời hoàn nguyên các cam kết của một tính năng, thì bạn có thể sử dụng chuỗi các lệnh sau.

Đây là cách nó làm việc

nhật ký git --pretty = oneline | grep 'Feature_name' | cắt -d '' -f1 | xargs -n1 git hoàn nguyên - không chỉnh sửa

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.