Hợp nhất hai kho Git mà không phá vỡ lịch sử tập tin


226

Tôi cần hợp nhất hai kho lưu trữ Git vào một kho lưu trữ thứ ba hoàn toàn mới. Tôi đã tìm thấy nhiều mô tả về cách thực hiện việc này bằng cách sử dụng hợp nhất cây con (ví dụ: câu trả lời của Jakub Narębski về cách bạn hợp nhất hai kho lưu trữ Git? ) Và làm theo các hướng dẫn đó chủ yếu hoạt động, ngoại trừ khi tôi cam kết cây con hợp nhất tất cả các tệp từ kho lưu trữ cũ được ghi lại dưới dạng tệp mới được thêm vào. Tôi có thể thấy lịch sử cam kết từ các kho lưu trữ cũ khi tôi thực hiện git log, nhưng nếu tôi thực hiện git log <file>nó chỉ hiển thị một cam kết cho tệp đó - hợp nhất cây con. Đánh giá từ các ý kiến ​​về câu trả lời trên, tôi không đơn độc khi thấy vấn đề này nhưng tôi không tìm thấy giải pháp nào được công bố cho nó.

Có cách nào để hợp nhất các kho lưu trữ và giữ nguyên lịch sử tệp không?


Tôi không sử dụng Git, nhưng trong Mercurial trước tiên tôi sẽ thực hiện chuyển đổi nếu cần để sửa đường dẫn tệp của các repos được hợp nhất, sau đó buộc kéo một repo vào mục tiêu để lấy các thay đổi, sau đó thực hiện hợp nhất của các ngành khác nhau. Điều này đã được thử nghiệm và hoạt động;) Có lẽ điều này cũng giúp tìm ra giải pháp cho Git ... so với cách tiếp cận hợp nhất cây con Tôi đoán bước chuyển đổi là khác nhau khi lịch sử được viết lại thay vì chỉ ánh xạ một đường dẫn (nếu tôi hiểu chính xác). Điều này sau đó đảm bảo hợp nhất trơn tru mà không có bất kỳ xử lý đặc biệt nào của đường dẫn tệp.
Lucero

Tôi cũng thấy câu hỏi này rất hữu ích stackoverflow.com/questions/1683531/ từ
nacross 17/214

Tôi đã tạo ra một câu hỏi tiếp theo. Có thể thú vị: Hợp nhất hai kho Git và giữ lịch sử chính: stackoverflow.com/questions/42161910/
Kẻ

Giải pháp tự động phù hợp với tôi là stackoverflow.com/a/30781527/239408
xverges

Câu trả lời:


269

Nó chỉ ra rằng câu trả lời đơn giản hơn nhiều nếu bạn chỉ đơn giản là cố gắng dán hai kho lưu trữ lại với nhau và làm cho nó trông giống như vậy theo cách đó thay vì quản lý một phụ thuộc bên ngoài. Bạn chỉ cần thêm điều khiển từ xa vào repos cũ của mình, hợp nhất chúng với chủ mới của bạn, di chuyển các tệp và thư mục sang thư mục con, cam kết di chuyển và lặp lại cho tất cả các repos bổ sung. Các mô hình con, sáp nhập cây con và các cuộc nổi loạn ưa thích nhằm giải quyết một vấn đề hơi khác và không phù hợp với những gì tôi đang cố gắng thực hiện.

Dưới đây là một ví dụ về kịch bản Powershell để dán hai kho lưu trữ lại với nhau:

# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init

# Before we do a merge, we have to have an initial commit, so we'll make a dummy commit
git commit --allow-empty -m "Initial dummy commit"

# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>

# Merge the files from old_a/master into new/master
git merge old_a/master --allow-unrelated-histories

# Move the old_a repo files and folders into a subdirectory so they don't collide with the other repo coming later
mkdir old_a
dir -exclude old_a | %{git mv $_.Name old_a}

# Commit the move
git commit -m "Move old_a files into subdir"

# Do the same thing for old_b
git remote add -f old_b <OldB repo URL>
git merge old_b/master --allow-unrelated-histories
mkdir old_b
dir exclude old_a,old_b | %{git mv $_.Name old_b}
git commit -m "Move old_b files into subdir"

Rõ ràng thay vào đó, bạn có thể hợp nhất old_b vào old_a (trở thành repo kết hợp mới) nếu bạn muốn làm điều đó - sửa đổi tập lệnh cho phù hợp.

Nếu bạn cũng muốn mang lại các nhánh tính năng đang thực hiện, hãy sử dụng:

# Bring over a feature branch from one of the old repos
git checkout -b feature-in-progress
git merge -s recursive -Xsubtree=old_a old_a/feature-in-progress

Đó là phần không rõ ràng duy nhất của quy trình - đó không phải là hợp nhất cây con, mà là một đối số cho phép hợp nhất đệ quy thông thường cho Git biết rằng chúng tôi đã đổi tên mục tiêu và giúp Git sắp xếp chính xác mọi thứ.

Tôi đã viết lên một lời giải thích chi tiết hơn một chút ở đây .


16
giải pháp này sử dụng git mvkhông hoạt động tốt. khi bạn sử dụng một git logtrong các tệp đã di chuyển, bạn chỉ nhận được cam kết từ di chuyển. tất cả lịch sử trước đó bị mất. Điều này là bởi vì git mvthực sự git rm; git addnhưng trong một bước .
mholm815

15
Nó giống như mọi thao tác di chuyển / đổi tên khác trong Git: từ dòng lệnh bạn có thể lấy tất cả lịch sử bằng cách thực hiện git log --followhoặc tất cả các công cụ GUI sẽ tự động làm điều đó cho bạn. Với sự hợp nhất của cây con, bạn không thể có được lịch sử cho các tệp riêng lẻ, theo như tôi biết, vì vậy phương pháp này tốt hơn.
Eric Lee

3
@EricLee Khi repo old_b được hợp nhất tôi nhận được rất nhiều xung đột hợp nhất. Điều đó có được mong đợi không? Tôi nhận CONFLICT (đổi tên / xóa)
Jon

9
Khi tôi thử "dir -exclude old_a |% {git mv $ _. Name old_a}", tôi nhận được sh.exe ": dir: lệnh không tìm thấy và sh.exe": git: lệnh không tìm thấy. Sử dụng công việc này: ls -I old_a | xargs -I '{}' git mv '{}' old_a /
George

5
Đây là 1(số một) cho lsvà vốn 'mắt' cho xargs. Cảm ơn bạn cho mẹo này!
Dominique Vial

149

Đây là một cách không viết lại bất kỳ lịch sử nào, vì vậy tất cả các ID cam kết sẽ vẫn còn hiệu lực. Kết quả cuối cùng là các tệp của repo thứ hai sẽ kết thúc trong thư mục con.

  1. Thêm repo thứ hai như một điều khiển từ xa:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    
  2. Đảm bảo rằng bạn đã tải xuống tất cả các cam kết của secondrepo:

    git fetch secondrepo
    
  3. Tạo một chi nhánh địa phương từ chi nhánh của repo thứ hai:

    git branch branchfromsecondrepo secondrepo/master
    
  4. Di chuyển tất cả các tệp của nó vào thư mục con:

    git checkout branchfromsecondrepo
    mkdir subdir/
    git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} subdir/
    git commit -m "Moved files to subdir/"
    
  5. Hợp nhất nhánh thứ hai thành nhánh chính của repo thứ nhất:

    git checkout master
    git merge --allow-unrelated-histories branchfromsecondrepo
    

Kho lưu trữ của bạn sẽ có nhiều hơn một cam kết gốc, nhưng điều đó không gây ra vấn đề gì.


1
Bước 2 không hoạt động đối với tôi: fatal: Không phải là tên đối tượng hợp lệ: 'secondrepo / master'.
Keith

@Keith: Hãy chắc chắn rằng bạn đã thêm repo thứ hai như là một "secondrepo" tên từ xa, và đó repo mà có một chi nhánh có tên là "bậc thầy" (bạn có thể xem chi nhánh trên repo từ xa với lệnh git remote show secondrepo)
Flimm

Tôi đã phải làm một tìm nạp để đưa nó xuống là tốt. Trong khoảng từ 1 đến 2 tôi đã git fetch
secondrepo

@monkjack: Tôi đã chỉnh sửa câu trả lời của mình để bao gồm bước tìm nạp git. Hãy tự chỉnh sửa câu trả lời trong tương lai.
Flimm 17/03 '

4
@MartijnHeemels Đối với phiên bản cũ hơn của Git, chỉ cần bỏ qua --allow-unrelated-histories. Xem lịch sử của bài trả lời này.
Flimm

8

Một vài năm đã trôi qua và có những giải pháp được bình chọn dựa trên cơ sở tốt nhưng tôi muốn chia sẻ giải pháp của mình vì nó hơi khác một chút vì tôi muốn hợp nhất 2 kho lưu trữ từ xa vào một kho mới mà không xóa lịch sử khỏi kho lưu trữ trước đó.

  1. Tạo một kho lưu trữ mới trong Github.

    nhập mô tả hình ảnh ở đây

  2. Tải về repo mới được tạo và thêm kho lưu trữ từ xa cũ.

    git clone https://github.com/alexbr9007/Test.git
    cd Test
    git remote add OldRepo https://github.com/alexbr9007/Django-React.git
    git remote -v
    
  3. Tìm nạp tất cả các tệp từ repo cũ để một nhánh mới được tạo.

    git fetch OldRepo
    git branch -a
    

    nhập mô tả hình ảnh ở đây

  4. Trong nhánh chính, thực hiện hợp nhất để kết hợp repo cũ với cái mới được tạo.

    git merge remotes/OldRepo/master --allow-unrelated-histories
    

    nhập mô tả hình ảnh ở đây

  5. Tạo một thư mục mới để lưu trữ tất cả nội dung mới được tạo từ OldRepo và di chuyển các tệp của nó vào thư mục mới này.

  6. Cuối cùng, bạn có thể tải lên các tệp từ các repos kết hợp và xóa OldRepo khỏi GitHub một cách an toàn.

Hy vọng điều này có thể hữu ích cho bất cứ ai đối phó với việc hợp nhất các kho lưu trữ từ xa.


1
Đây là giải pháp duy nhất giúp tôi bảo tồn lịch sử git. Đừng quên xóa liên kết từ xa đến repo cũ với git remote rm OldRepo.
Harubiyori

7

xin vui lòng sử dụng

git rebase --root --preserve-merges --onto

để liên kết hai lịch sử sớm trong cuộc sống của họ.

Nếu bạn có các đường dẫn trùng nhau, hãy sửa chúng bằng

git filter-branch --index-filter

khi bạn sử dụng nhật ký, đảm bảo bạn "tìm bản sao khó hơn" với

git log -CC

bằng cách đó bạn sẽ tìm thấy bất kỳ chuyển động của tập tin trong đường dẫn.


Tài liệu Git khuyên bạn không nên đánh lại ... git-scm.com/book/en/v2/Git-Branching-Rebasing#_Vbase_peril
Stephen Turner

7

Tôi đã biến giải pháp từ @Flimm này thành git aliasnhư thế này (thêm vào của tôi ~/.gitconfig):

[alias]
 mergeRepo = "!mergeRepo() { \
  [ $# -ne 3 ] && echo \"Three parameters required, <remote URI> <new branch> <new dir>\" && exit 1; \
  git remote add newRepo $1; \
  git fetch newRepo; \
  git branch \"$2\" newRepo/master; \
  git checkout \"$2\"; \
  mkdir -vp \"${GIT_PREFIX}$3\"; \
  git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} \"${GIT_PREFIX}$3\"/; \
  git commit -m \"Moved files to '${GIT_PREFIX}$3'\"; \
  git checkout master; git merge --allow-unrelated-histories --no-edit -s recursive -X no-renames \"$2\"; \
  git branch -D \"$2\"; git remote remove newRepo; \
}; \
mergeRepo"

12
Chỉ tò mò: bạn có thực sự làm điều này thường xuyên đủ để cần một bí danh?
Parker Coates

1
Không tôi không nhưng không bao giờ nhớ làm thế nào để làm bí danh chỉ là một cách để tôi nhớ nó.
Fredrik Erlandsson

1
Vâng .. nhưng hãy thử thay đổi máy tính và quên di chuyển bí danh của bạn;)
quetzalcoatl

1
Giá trị của $GIT_PREFIXcái gì?
neowulf33

github.com/git/git/blob/ '' GIT_PREFIX 'được đặt là trả về bằng cách chạy' git rev-parse --show-prefix 'từ thư mục hiện tại ban đầu. Xem linkgit: git-rev-parse [1].
Fredrik Erlandsson

3

Hàm này sẽ sao chép repo từ xa vào thư mục repo cục bộ:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

Cách sử dụng:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

Để ý. Kịch bản lệnh này có thể viết lại các xác nhận nhưng sẽ lưu tất cả các tác giả và ngày, điều đó có nghĩa là các cam kết mới sẽ có một giá trị băm khác và nếu bạn cố gắng thay đổi các máy chủ từ xa, nó chỉ có thể bằng phím buộc, nó cũng sẽ viết lại các xác nhận trên máy chủ. Vì vậy, hãy tạo bản sao lưu trước khi khởi chạy.

Lợi nhuận!


Tôi đang sử dụng zsh chứ không phải bash và v2.13.0 của git. Bất kể tôi đã cố gắng gì, tôi vẫn chưa thể git filter-branch --index-filterđi làm. Thông thường tôi nhận được một thông báo lỗi rằng tệp chỉ mục .new không tồn tại. Cái chuông đó có chuông không?
Patrick Beard

@PatrickBeard Tôi không biết zsh, bạn có thể tạo tệp riêng biệt git-add-repo.shvới chức năng ở trên, ở cuối tệp đặt dòng này git-add-repo "$@". Sau đó, bạn có thể sử dụng nó từ zsh like cd current/git/packagebash path/to/git-add-repo.sh https://github.com/example/example dir/to/save
Andrey Izman

Vấn đề đã được thảo luận ở đây: stackoverflow.com/questions/7798142/ mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" , đôi khi không thành công, vì vậy bạn phải thêm một if test.
Patrick Beard

1
Tôi sẽ không sử dụng phương pháp này! Tôi đã thử kịch bản, ngây thơ và nguyên văn (tôi chỉ có thể tự trách mình về phần đó), và nó đã bịt kín repo git địa phương của tôi. Lịch sử có vẻ đúng, nhưng thực hiện một cú đẩy git trở lại Github dẫn đến lỗi "RPC không thành công; curl 55 SSL_write () đã trả về lỗi SYSCALL, errno = 32". Tôi đã cố gắng sửa chữa nó, nhưng nó đã bị hỏng không thể sửa chữa. Tôi cuối cùng đã phải xây dựng lại mọi thứ trong một repo địa phương mới.
Mason giải phóng

@MasonFreed tập lệnh này tạo ra một lịch sử git mới với sự pha trộn của cả hai repos, vì vậy nó không thể được đẩy sang repo cũ, nó yêu cầu tạo một cái mới hoặc đẩy bằng phím lực, có nghĩa là nó viết lại repo của bạn trên máy chủ
Andrey Izman

2

Thực hiện theo các bước để nhúng một repo vào một repo khác, có một lịch sử git duy nhất bằng cách hợp nhất cả hai lịch sử git.

  1. Nhân bản cả hai repos bạn muốn hợp nhất.

git clone git@github.com: người dùng / phụ huynh-repo.git

git clone git@github.com: người dùng / trẻ em repo.git

  1. Đi repo con

cd con-repo /

  1. chạy lệnh dưới đây, thay thế đường dẫn my/new/subdir(3 lần xuất hiện) bằng cấu trúc thư mục mà bạn muốn có repo con.

git bộ lọc-nhánh --prune-trống --tree-filter 'if [! -e my / new / subir]; sau đó mkdir -p my / new / subir git ls-tree - chỉ tên $ GIT_COMMIT | xargs -I tập tin mv tập tin của tôi / new / subir fi '

  1. Đi đến phụ huynh

cd ../parent-repo/

  1. Thêm một điều khiển từ xa vào repo cha, chỉ đường dẫn đến repo con

git từ xa thêm trẻ em từ xa ../child-repo/

  1. Lấy repo con

git lấy con từ xa

  1. Hợp nhất lịch sử

git merge --allow-không liên quan đến lịch sử con-remote / master

Nếu bạn kiểm tra nhật ký git trong repo cha bây giờ, nó sẽ có repo con cam kết hợp nhất. Bạn cũng có thể thấy thẻ chỉ ra từ nguồn cam kết.

Bài viết dưới đây đã giúp tôi trong việc nhúng một repo vào một repo khác, có một lịch sử git duy nhất bằng cách hợp nhất cả hai lịch sử git.

http://ericlathrop.com/2014/01/combining-git-repose khu /

Hi vọng điêu nay co ich. Chúc mừng mã hóa!


Bước 3 thất bại với tôi với lỗi cú pháp. Bán đại tràng bị thiếu. Khắc phụcgit filter-branch --prune-empty --tree-filter ' if [ ! -e my/new/subdir ]; then mkdir -p my/new/subdir; git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files my/new/subdir; fi'
Yuri L

1

Giả sử bạn muốn hợp nhất kho lưu trữ avào b(Tôi giả sử chúng nằm cạnh nhau):

cd b
git remote add a ../a
git fetch a
git merge --allow-unrelated-histories a/master
git remote remove a

Trong trường hợp bạn muốn đưa avào thư mục con, hãy làm như sau trước các lệnh trên:

cd a
git filter-repo --to-subdirectory-filter a
cd ..

Đối với điều này bạn không cần git-filter-repocài đặt ( filter-branchnản ).

Một ví dụ về việc hợp nhất 2 kho lưu trữ lớn, đưa một trong số chúng vào thư mục con: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Thêm về nó ở đây .

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.