Đặt con trỏ cha git thành cha mẹ khác


220

Nếu trước đây tôi có một cam kết chỉ ra một phụ huynh, nhưng tôi muốn thay đổi phụ huynh mà nó chỉ ra, tôi sẽ làm thế nào để làm điều đó?

Câu trả lời:


404

Sử dụng git rebase. Đó là lệnh "Take commit (s) và đặt nó trên một lệnh cha (cơ sở)" khác trong Git.

Tuy nhiên, một số điều cần biết:

  1. Vì các SHA cam kết liên quan đến cha mẹ của họ, nên khi bạn thay đổi cha mẹ của một cam kết nhất định, SHA của nó sẽ thay đổi - cũng như các SHA của tất cả các cam kết đi sau nó (gần đây hơn) trong quá trình phát triển.

  2. Nếu bạn đang làm việc với những người khác và bạn đã đẩy cam kết trong câu hỏi công khai đến nơi họ đã kéo nó, sửa đổi cam kết có lẽ là một ý tưởng tồi ™. Điều này là do # 1, và do đó, sự nhầm lẫn dẫn đến các kho lưu trữ của người dùng khác sẽ gặp phải khi cố gắng tìm hiểu điều gì đã xảy ra do SHA của bạn không còn phù hợp với cam kết "tương tự" của họ. (Xem phần "THU HỒI TỪ UPSTREAM REBASE" của trang man được liên kết để biết chi tiết.)

Điều đó nói rằng, nếu bạn hiện đang ở trong một chi nhánh với một số cam kết rằng bạn muốn chuyển đến một phụ huynh mới, thì nó sẽ trông giống như thế này:

git rebase --onto <new-parent> <old-parent>

Điều đó sẽ di chuyển mọi thứ sau khi <old-parent> trong chi nhánh hiện tại để ngồi trên đầu <new-parent>thay thế.


Tôi đang sử dụng git rebase --onto để thay đổi cha mẹ nhưng có vẻ như tôi chỉ kết thúc với một cam kết duy nhất. Tôi đặt ra một câu hỏi về nó: stackoverflow.com/questions/19181665/ Kẻ
Eduardo Mello

7
Aha, vậy đó là những gì --ontođược sử dụng cho! Các tài liệu luôn luôn hoàn toàn tối nghĩa với tôi, ngay cả sau khi đọc câu trả lời này!
Daniel C. Sobral

6
Dòng này: Đã cho git rebase --onto <new-parent> <old-parent>tôi biết mọi thứ về cách sử dụng rebase --ontomà mọi câu hỏi và tài liệu khác mà tôi đã đọc cho đến nay đều thất bại.
jpmc26

3
Đối với tôi lệnh này nên đọc : rebase this commit on that one, hoặc git rebase --on <new-parent> <commit>. Sử dụng cha mẹ cũ ở đây không có ý nghĩa với tôi.
Samir Aguiar

1
@kopranb Cha mẹ già rất quan trọng khi bạn muốn loại bỏ một số đường dẫn từ đầu đến đầu từ xa. Các trường hợp bạn đang mô tả được xử lý bởi git rebase <new-parent>.
clacke

35

Nếu nó chỉ ra rằng bạn cần tránh việc đánh bại các cam kết tiếp theo (ví dụ vì việc viết lại lịch sử sẽ không thể thực hiện được), thì bạn có thể sử dụng thay thế git (có sẵn trong Git 1.6.5 trở lên).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

Với sự thay thế ở trên được thiết lập, mọi yêu cầu cho đối tượng B sẽ thực sự trả về đối tượng C. Nội dung của C hoàn toàn giống với nội dung của B ngoại trừ cha mẹ đầu tiên (cùng cha mẹ (ngoại trừ đầu tiên), cùng một cây, cùng một thông điệp cam kết).

Thay thế được kích hoạt theo mặc định, nhưng có thể được bật bằng cách sử dụng --no-replace-objectstùy chọn để git (trước tên lệnh) hoặc bằng cách đặt GIT_NO_REPLACE_OBJECTSbiến môi trường. Thay thế có thể được chia sẻ bằng cách đẩy refs/replace/*(ngoài bình thườngrefs/heads/* ).

Nếu bạn không thích cam kết (được thực hiện với sed ở trên), thì bạn có thể tạo cam kết thay thế bằng các lệnh cấp cao hơn:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

Sự khác biệt lớn là trình tự này không truyền bá cha mẹ bổ sung nếu B là một cam kết hợp nhất.


Và sau đó, làm thế nào bạn sẽ đẩy các thay đổi đến một kho lưu trữ hiện có? Nó dường như hoạt động tại địa phương, nhưng git đẩy không thúc đẩy bất cứ điều gì sau đó
Baptiste Wicht

2
@BaptisteWicht: Các thay thế được lưu trữ trong một bộ refs riêng biệt. Bạn sẽ cần sắp xếp để đẩy (và tìm nạp) refs/replace/hệ thống phân cấp ref. Nếu bạn chỉ cần làm một lần, thì bạn có thể thực hiện git push yourremote 'refs/replace/*'trong kho lưu trữ nguồn và git fetch yourremote 'refs/replace/*:refs/replace/*'trong kho đích. Nếu bạn cần thực hiện nhiều lần, thì thay vào đó bạn có thể thêm các refspec đó vào một remote.yourremote.pushbiến cấu hình (trong kho lưu trữ nguồn) và một remote.yourremote.fetchbiến cấu hình (trong kho đích).
Chris Johnsen

3
Một cách đơn giản hơn để làm điều này là sử dụng git replace --grafts. Không chắc chắn khi điều này được thêm vào, nhưng toàn bộ mục đích của nó là tạo ra một sự thay thế giống như cam kết B nhưng với cha mẹ được chỉ định.
Paul Wagland

32

Lưu ý rằng việc thay đổi một cam kết trong Git yêu cầu tất cả các cam kết tuân theo nó phải được thay đổi. Điều này không được khuyến khích nếu bạn đã xuất bản phần này của lịch sử và ai đó có thể đã xây dựng công trình của họ trên lịch sử rằng đó là trước khi thay đổi.

Giải pháp thay thế để git rebaseđề cập trong câu trả lời của Amber là sử dụng mô ghép cơ chế (xem định nghĩa của Git ghép trong Git Thuật ngữ và tài liệu hướng dẫn của .git/info/graftstập tin trong Git Repository Layout tài liệu) để mẹ thay đổi của một cam kết, kiểm tra rằng nó đã làm điều đúng với một số người xem lịch sử ( gitk, git log --graph, v.v.) và sau đó sử dụng git filter-branch(như được mô tả trong phần "Ví dụ" trên trang chủ của nó) để làm cho nó vĩnh viễn (và sau đó loại bỏ mảnh ghép và tùy ý xóa các giới thiệu ban đầu được sao lưu bởigit filter-branch , hoặc kho lưu trữ lại):

echo "$ commit-id $ graft-id" >> .git / thông tin / ghép
chi nhánh bộ lọc git $ graft-id..IAD

GHI CHÚ !!! Giải pháp này khác với giải pháp rebase ở chỗ git rebasesẽ thay đổi / ghép thay đổi , trong khi giải pháp dựa trên ghép sẽ chỉ đơn giản là cam kết như vậy , không tính đến sự khác biệt giữa cha mẹ cũ và cha mẹ mới!


3
Từ những gì tôi hiểu (từ git.wiki.kernel.org/index.php/GraftPoint ), git replaceđã ghép các git ghép (giả sử bạn có git 1.6.5 trở lên).
Alexander Bird

4
@ Thr4wn: Phần lớn là đúng, nhưng nếu bạn muốn viết lại lịch sử thì ghép + git-lọc-nhánh (hoặc rebase tương tác, hoặc xuất nhanh + ví dụ: reposurgeon) là cách thực hiện. Nếu bạn muốn / cần lưu giữ lịch sử , thì git-thay thế vượt trội hơn nhiều so với ghép.
Jakub Narębski

@ JakubNarębski, bạn có thể giải thích ý của bạn bằng cách viết lạibảo tồn lịch sử không? Tại sao tôi không thể sử dụng git-replace+ git-filter-branch? Trong thử nghiệm của tôi, filter-branchdường như tôn trọng sự thay thế, đánh bại toàn bộ cây.
cdunn2001

1
@ cdunn2001: git filter-branchviết lại lịch sử, tôn trọng các mảnh ghép và thay thế (nhưng trong các thay thế lịch sử viết lại sẽ được đưa ra); đẩy sẽ dẫn đến thay đổi không fasf-chuyển tiếp. git replacetạo ra sự thay thế có thể chuyển nhượng tại chỗ; đẩy sẽ được chuyển tiếp nhanh, nhưng bạn sẽ cần phải đẩy refs/replaceđể chuyển thay thế để có lịch sử sửa chữa. HTH
Jakub Narębski

23

Để làm rõ các câu trả lời trên và không biết xấu hổ cắm kịch bản của riêng tôi:

Nó phụ thuộc vào việc bạn muốn "rebase" hay "reparent". Một cuộc nổi loạn , theo đề xuất của Amber , di chuyển xung quanh khác . Một người ăn năn , theo gợi ý của JakubChris , di chuyển xung quanh những bức ảnh chụp toàn bộ cây. Nếu bạn muốn hối lỗi, tôi khuyên bạn nên sử dụng git reparentthay vì thực hiện công việc một cách thủ công.

So sánh

Giả sử bạn có hình ảnh bên trái và bạn muốn nó trông giống như hình ảnh bên phải:

                   C'
                  /
A---B---C        A---B---C

Cả việc nổi loạn và hối lỗi sẽ tạo ra cùng một bức tranh, nhưng định nghĩa về sự C'khác biệt. Với git rebase --onto A B, C'sẽ không chứa bất kỳ thay đổi được giới thiệu bởi B. Với git reparent -p A, C'sẽ giống hệt với C(ngoại trừ điều đó Bsẽ không có trong lịch sử).


1
+1 Tôi đã thử nghiệm với reparentkịch bản; nó hoạt động rất đẹp; rất khuyến khích . Tôi cũng khuyên bạn nên tạo branchnhãn mới và đến checkoutchi nhánh đó trước khi gọi (vì nhãn chi nhánh sẽ di chuyển).
Đại hoàng

Cũng đáng lưu ý rằng: (1) nút gốc vẫn còn nguyên và (2) nút mới không kế thừa các con của nút gốc.
Đại hoàng

0

Chắc chắn câu trả lời của @ Jakub đã giúp tôi vài giờ trước khi tôi đang thử chính xác điều tương tự như OP.
Tuy nhiên, git replace --graftbây giờ là giải pháp dễ dàng hơn về ghép. Ngoài ra, một vấn đề lớn với giải pháp đó là nhánh lọc khiến tôi mất mọi nhánh không được sáp nhập vào nhánh của HEAD. Sau đó, git filter-repođã làm công việc hoàn hảo và hoàn hảo.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

Để biết thêm thông tin: kiểm tra phần "Lịch sử ghép lại" trong tài liệu

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.