Tách nhiều thư mục con vào kho lưu trữ Git mới, riêng biệt


135

Câu hỏi này dựa trên thư mục con Detach vào kho Git riêng

Thay vì tách ra một thư mục con duy nhất, tôi muốn tách ra một cặp. Ví dụ, cây thư mục hiện tại của tôi trông như thế này:

/apps
  /AAA
  /BBB
  /CCC
/libs
  /XXX
  /YYY
  /ZZZ

Và tôi muốn điều này thay vào đó:

/apps
  /AAA
/libs
  /XXX

Đối --subdirectory-filtersố git filter-branchsẽ không hoạt động vì nó loại bỏ mọi thứ trừ thư mục đã cho lần đầu tiên chạy. Tôi nghĩ rằng sử dụng --index-filterđối số cho tất cả các tệp không mong muốn sẽ hoạt động (mặc dù tẻ nhạt), nhưng nếu tôi thử chạy nó nhiều lần, tôi nhận được thông báo sau:

Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

Có ý kiến ​​gì không? TIA

Câu trả lời:


155

Thay vì phải xử lý một subshell và sử dụng ext global (như kynan đề xuất), hãy thử phương pháp đơn giản hơn nhiều này:

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- apps/AAA libs/XXX' --prune-empty -- --all

Như được đề cập bởi void.pulum trong bình luận của anh ấy / cô ấy , điều này sẽ loại bỏ mọi thứ ngoại trừ apps/AAAlibs/XXXkhỏi kho lưu trữ hiện tại.

Prune trống hợp nhất cam kết

Điều này để lại rất nhiều sự hợp nhất trống rỗng. Chúng có thể được loại bỏ bởi một đường chuyền khác như được mô tả bởi raphinesse trong câu trả lời của ông :

git filter-branch --prune-empty --parent-filter \
'sed "s/-p //g" | xargs -r git show-branch --independent | sed "s/\</-p /g"'

Cảnh báo : Ở trên phải sử dụng phiên bản GNU sedxargsnếu không, nó sẽ xóa tất cả các xác nhận là xargskhông thành công. brew install gnu-sed findutilsvà sau đó sử dụng gsedgxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 

4
ngoài ra, cờ --ignore-unmatch nên được chuyển cho git rm, nó đã thất bại cho lần cam kết đầu tiên đối với tôi (kho lưu trữ được tạo bằng bản sao git svn trong trường hợp của tôi)
Pontomedon

8
Giả sử bạn có các thẻ trong hỗn hợp, có lẽ bạn nên thêm --tag-name-filter catvào các tham số của mình
Yonatan

16
Bạn có thể thêm một số thông tin giải thích những gì lệnh dài này đang làm?
Burhan Ali

4
Tôi rất ngạc nhiên khi điều này hoạt động hoàn hảo trên Windows bằng cách sử dụng git bash, phew!
Đại

3
@BurhanAli Đối với mọi cam kết trong lịch sử, nó sẽ xóa tất cả các tệp ngoại trừ các tệp bạn muốn giữ. Khi mọi thứ đã xong, bạn chỉ còn lại một phần của cây bạn đã chỉ định, cùng với lịch sử đó.
void.pulum

39

Các bước thủ công với các lệnh git đơn giản

Kế hoạch là chia các thư mục riêng lẻ thành các kho riêng, sau đó hợp nhất chúng lại với nhau. Các bước thủ công sau đây không sử dụng các tập lệnh chuyên nghiệp nhưng sử dụng các lệnh dễ hiểu và có thể giúp hợp nhất các thư mục con N bổ sung vào một kho lưu trữ duy nhất.

Chia

Giả sử repo ban đầu của bạn là: original_Vpo

1 - Chia ứng dụng:

git clone original_repo apps-repo
cd apps-repo
git filter-branch --prune-empty --subdirectory-filter apps master

2 - Chia lib

git clone original_repo libs-repo
cd libs-repo
git filter-branch --prune-empty --subdirectory-filter libs master

Tiếp tục nếu bạn có nhiều hơn 2 thư mục. Bây giờ bạn sẽ có hai kho git mới và tạm thời.

Chinh phục bằng cách hợp nhất các ứng dụng và libs

3 - Chuẩn bị repo hoàn toàn mới:

mkdir my-desired-repo
cd my-desired-repo
git init

Và bạn sẽ cần phải thực hiện ít nhất một cam kết. Nếu bỏ qua ba dòng sau, repo đầu tiên của bạn sẽ xuất hiện ngay dưới gốc của repo của bạn:

touch a_file_and_make_a_commit # see user's feedback
git add a_file_and_make_a_commit
git commit -am "at least one commit is needed for it to work"

Với tệp tạm thời được cam kết, mergelệnh trong phần sau sẽ dừng như mong đợi.

Lấy từ phản hồi của người dùng, thay vì thêm một tệp ngẫu nhiên như a_file_and_make_a_commit, bạn có thể chọn thêm một .gitignore, hoặc README.mdv.v.

4 - Hợp nhất repo ứng dụng trước:

git remote add apps-repo ../apps-repo
git fetch apps-repo
git merge -s ours --no-commit apps-repo/master # see below note.
git read-tree --prefix=apps -u apps-repo/master
git commit -m "import apps"

Bây giờ bạn sẽ thấy thư mục ứng dụng bên trong kho lưu trữ mới của bạn. git logsẽ hiển thị tất cả các thông điệp cam kết lịch sử có liên quan.

Lưu ý: như Chris đã lưu ý bên dưới trong các nhận xét, đối với phiên bản mới hơn (> = 2.9) của git, bạn cần chỉ định --allow-unrelated-historiesvớigit merge

5 - Hợp nhất libs repo tiếp theo theo cách tương tự:

git remote add libs-repo ../libs-repo
git fetch libs-repo
git merge -s ours --no-commit libs-repo/master # see above note.
git read-tree --prefix=libs -u libs-repo/master
git commit -m "import libs"

Tiếp tục nếu bạn có nhiều hơn 2 repos để hợp nhất.

Tham khảo: Hợp nhất thư mục con của kho lưu trữ khác với git


4
Vì git 2.9, bạn cần sử dụng - lịch sử không liên quan đến các lệnh hợp nhất. Nếu không, điều này dường như đã làm việc tốt cho tôi.
Chris

1
Thiên tài! Cám ơn bạn rất nhiều về điều này. Các câu trả lời ban đầu tôi đã xem xét, sử dụng bộ lọc cây trên một kho lưu trữ rất lớn, đã dự đoán git mất hơn 26 giờ để hoàn thành việc viết lại git. Hạnh phúc hơn nhiều với cách tiếp cận đơn giản nhưng có thể lặp lại này và đã chuyển thành công 4 thư mục con sang một repo mới với tất cả lịch sử cam kết dự kiến.
đóng cửa

1
Bạn có thể sử dụng cam kết đầu tiên cho "Cam kết ban đầu" có thêm .gitignoreREADME.mdtệp.
Jack Miller

2
Thật không may, cách tiếp cận này dường như phá vỡ lịch sử theo dõi đối với các tệp được thêm vào trong git merge .. git read-treebước này, vì nó ghi lại chúng dưới dạng các tệp mới được thêm vào và tất cả các git guis của tôi không thực hiện kết nối với các cam kết trước đó của chúng.
Đại

1
@ksadjad, Không có ý kiến, phải trung thực. Điểm trung tâm của việc hợp nhất thủ công là chọn các thư mục để tạo thành repo mới và giữ lịch sử cam kết của chúng. Tôi không chắc chắn làm thế nào để xử lý tình huống như vậy khi một cam kết đặt các tệp vào dirA, dirB, dirDrop và chỉ dirA và dirB được chọn cho repo mới, lịch sử cam kết liên quan đến bản gốc như thế nào.
chfw

27

Tại sao bạn muốn chạy filter-branchnhiều hơn một lần? Bạn có thể thực hiện tất cả trong một lần quét, do đó không cần phải ép buộc (lưu ý rằng bạn cần extglobkích hoạt trong vỏ để hoạt động này):

git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch $(ls -xd apps/!(AAA) libs/!(XXX))" --prune-empty -- --all

Điều này sẽ loại bỏ tất cả các thay đổi trong các thư mục con không mong muốn và giữ tất cả các nhánh và cam kết của bạn (trừ khi chúng chỉ ảnh hưởng đến các tệp trong các thư mục con được cắt tỉa, theo nguyên tắc --prune-empty) - không có vấn đề gì với các cam kết trùng lặp, v.v.

Sau thao tác này, các thư mục không mong muốn sẽ được liệt kê là không bị theo dõi bởi git status.

Điều $(ls ...)cần thiết là st extglobđược đánh giá bởi trình bao của bạn thay vì bộ lọc chỉ mục, sử dụng shnội dung dựng sẵn eval(nơi extglobkhông có sẵn). Xem Làm thế nào để tôi kích hoạt tùy chọn shell trong git? để biết thêm chi tiết về điều đó.


1
Ý tưởng thú vị. Tôi có một vấn đề tương tự nhưng không thể giải quyết được, hãy xem stackoverflow.com/questions/8050687/
mẹo

Đây là khá nhiều thứ tôi cần, mặc dù tôi đã rắc cả tệp và thư mục trên repo của mình ... Cảm ơn :)
phải

1
hm ngay cả khi extglob được bật, tôi vẫn gặp lỗi gần dấu ngoặc đơn: lỗi cú pháp gần mã thông báo không mong muốn `('lệnh của tôi trông giống như: git filter-Branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / Themes /! (some_theme *) "--prune-blank - --tất cả một ls với src / css / Themes /! được làm việc ...
robdodson

2
@MikeGraf Tôi không nghĩ rằng sẽ mang lại kết quả mong muốn: thoát sẽ phù hợp với nghĩa đen "!" vv trong con đường của bạn.
kynan

1
Câu trả lời của @ david-smiley (gần đây hơn) sử dụng một cách tiếp cận rất giống nhau, nhưng có lợi thế là chỉ dựa vào gitcác lệnh và do đó không dễ bị khác biệt trong cách lsdiễn giải trên các hệ điều hành, như @Bae đã phát hiện ra.
Jeremy Caney

20

Trả lời câu hỏi của riêng tôi ở đây ... sau rất nhiều thử nghiệm và sai sót.

Tôi quản lý để làm điều này bằng cách sử dụng kết hợp git subtreegit-stitch-repo. Các hướng dẫn này dựa trên:

Đầu tiên, tôi lấy ra các thư mục tôi muốn giữ trong kho riêng của họ:

cd origRepo
git subtree split -P apps/AAA -b aaa
git subtree split -P libs/XXX -b xxx

cd ..
mkdir aaaRepo
cd aaaRepo
git init
git fetch ../origRepo aaa
git checkout -b master FETCH_HEAD

cd ..
mkdir xxxRepo
cd xxxRepo
git init
git fetch ../origRepo xxx
git checkout -b master FETCH_HEAD

Sau đó tôi đã tạo một kho lưu trữ trống mới và nhập / khâu hai phần cuối vào đó:

cd ..
mkdir newRepo
cd newRepo
git init
git-stitch-repo ../aaaRepo:apps/AAA ../xxxRepo:libs/XXX | git fast-import

Điều này tạo ra hai nhánh master-Amaster-Bmỗi nhánh giữ nội dung của một trong các repos được khâu. Để kết hợp chúng và dọn dẹp:

git checkout master-A
git pull . master-B
git checkout master
git branch -d master-A 
git branch -d master-B

Bây giờ tôi không chắc chắn làm thế nào / khi điều này xảy ra, nhưng sau lần đầu tiên checkoutvà sau đó pull, mã hợp nhất một cách kỳ diệu vào nhánh chính (mọi hiểu biết về những gì đang diễn ra ở đây đều được đánh giá cao!)

Mọi thứ dường như đã hoạt động như mong đợi, ngoại trừ việc nếu tôi xem qua newRepolịch sử cam kết, sẽ có các bản sao khi thay đổi ảnh hưởng đến cả apps/AAAlibs/XXX. Nếu có một cách để loại bỏ trùng lặp, thì nó sẽ hoàn hảo.


Công cụ gọn gàng bạn tìm thấy ở đây. Thông tin chi tiết về "thanh toán": "git pull" giống như "git fetch && git merge". Phần "tìm nạp" là vô hại vì bạn đang "tìm nạp cục bộ". Vì vậy, tôi nghĩ rằng lệnh thanh toán này giống như "git merge master-B", một cách rõ ràng hơn một chút. Xem kernel.org/pub/software/scm/git/docs/git-pull.html
phord

1
Thật không may, công cụ git-Stitch-repo bị hỏng do phụ thuộc xấu hiện nay.
Henrik

@Henrik Bạn đã gặp vấn đề gì chính xác? Nó hoạt động với tôi, mặc dù tôi phải thêm export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"vào cấu hình bash của mình để nó có thể tìm thấy Git.pm. Sau đó, tôi đã cài đặt nó với cpan.

Có thể sử dụng git subtree addđể thực hiện nhiệm vụ này. Xem stackoverflow.com/a/58253979/1894804
laconbass

7

Tôi đã viết một bộ lọc git để giải quyết chính xác vấn đề này. Nó có tên tuyệt vời của git_filter và được đặt tại github tại đây:

https://github.com/slobobaby / git_filter

Nó được dựa trên libgit2 tuyệt vời.

Tôi cần phải phân chia một kho lưu trữ lớn với nhiều cam kết (~ 100000) và các giải pháp dựa trên nhánh bộ lọc git mất vài ngày để chạy. git_filter mất một phút để làm điều tương tự.


7

Sử dụng phần mở rộng git 'git split'

git splitslà một tập lệnh bash là một trình bao bọc xung quanh git branch-filtermà tôi đã tạo như một phần mở rộng git, dựa trên giải pháp của jkeat .

Nó đã được thực hiện chính xác cho tình huống này. Đối với lỗi của bạn, hãy thử sử dụng git splits -ftùy chọn để buộc loại bỏ bản sao lưu. Bởi vì git splitshoạt động trên một nhánh mới, nó sẽ không viết lại nhánh hiện tại của bạn, vì vậy bản sao lưu là không liên quan. Xem readme để biết thêm chi tiết và chắc chắn sử dụng nó trên một bản sao / bản sao của repo của bạn (chỉ trong trường hợp!) .

  1. cài đặt git splits.
  2. Tách các thư mục thành một chi nhánh địa phương #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
    #split multiple directories into new branch XYZ git splits -b XYZ apps/AAA libs/ZZZ

  3. Tạo một repo trống ở đâu đó. Chúng tôi sẽ cho rằng chúng tôi đã tạo một repo trống được gọi xyztrên GitHub có đường dẫn:git@github.com:simpliwp/xyz.git

  4. Đẩy sang repo mới. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz git@github.com:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Sao chép repo từ xa mới được tạo vào một thư mục cục bộ mới
    #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone git@github.com:simpliwp/xyz.git


Dường như không thể thêm tệp vào phần tách và cập nhật chúng sau này, phải không?
Alex

Điều này dường như chậm để chạy trên repo của tôi với hàng tấn cam kết
Shinta Smith

git-split dường như sử dụng bộ lọc git --index cực kỳ chậm so với --subdirectory-filter. Đối với một số repos, nó vẫn có thể là một lựa chọn khả thi, nhưng đối với các repos lớn (nhiều gigabyte, cam kết 6 chữ số) - bộ lọcindex thực sự mất nhiều tuần để chạy, ngay cả trên phần cứng đám mây chuyên dụng.
Jostein Kjønigsen

6
git clone git@example.com:thing.git
cd thing
git fetch
for originBranch in `git branch -r | grep -v master`; do
    branch=${originBranch:7:${#originBranch}}
    git checkout $branch
done
git checkout master

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- dir1 dir2 .gitignore' --prune-empty -- --all

git remote set-url origin git@example.com:newthing.git
git push --all

Đọc qua tất cả các ý kiến ​​khác đã đưa tôi đi đúng hướng. Tuy nhiên, giải pháp của bạn chỉ hoạt động. Nó nhập tất cả các chi nhánh, và làm việc với nhiều thư mục! Tuyệt quá!
jschober

1
Các forvòng lặp là đáng ghi nhận, vì câu trả lời tương tự khác không bao gồm nó. Nếu bạn không có một bản sao cục bộ của từng chi nhánh trong bản sao của mình, thì filter-branchbạn sẽ không xem chúng là một phần của việc viết lại, có khả năng loại trừ các tệp được giới thiệu trong các chi nhánh khác, nhưng chưa được hợp nhất với chi nhánh hiện tại của bạn. (Mặc dù cũng đáng để thực hiện git fetchtrên bất kỳ chi nhánh nào bạn đã kiểm tra trước đây để đảm bảo rằng chúng vẫn còn hiện hành.)
Jeremy Caney

5

Một giải pháp dễ dàng: git-filter-repo

Tôi đã có một vấn đề tương tự và sau khi xem xét các cách tiếp cận khác nhau được liệt kê ở đây, tôi đã phát hiện ra git-filter-repo . Nó được khuyến nghị thay thế cho nhánh git-filter trong tài liệu git chính thức ở đây .

Để tạo một kho lưu trữ mới từ một tập hợp con các thư mục trong một kho lưu trữ hiện có, bạn có thể sử dụng lệnh:

git filter-repo --path <file_to_remove>

Lọc nhiều tệp / thư mục bằng cách xâu chuỗi chúng:

git filter-repo --path keepthisfile --path keepthisfolder/

Vì vậy, để trả lời câu hỏi ban đầu , với git-filter-repo, bạn sẽ chỉ cần lệnh sau:

git filter-repo --path apps/AAA/ --path libs/XXX/

Đây chắc chắn là một câu trả lời tuyệt vời. Vấn đề với tất cả các giải pháp khác là tôi không thể quản lý để trích xuất nội dung của TẤT CẢ các nhánh của một thư mục. Tuy nhiên, git filter-repo đã lấy thư mục từ tất cả các nhánh và viết lại lịch sử một cách hoàn hảo, như làm sạch toàn bộ cây mọi thứ tôi không cần.
Teodoro

3

Vâng. Buộc ghi đè bản sao lưu bằng cách sử dụng -fcờ trong các cuộc gọi tiếp theo filter-branchđể ghi đè cảnh báo đó. :) Nếu không, tôi nghĩ rằng bạn có giải pháp (nghĩa là xóa một thư mục không mong muốn tại một thời điểm với filter-branch).


-4

Xóa bản sao lưu hiện có trong thư mục .git trong refs / gốc như thông báo gợi ý. Thư mục bị ẩn.

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.