Detach (di chuyển) thư mục con vào kho Git riêng


1758

Tôi có một kho Git chứa một số thư mục con. Bây giờ tôi đã thấy rằng một trong những thư mục con không liên quan đến cái kia và nên được tách ra thành một kho lưu trữ riêng.

Làm thế nào tôi có thể làm điều này trong khi giữ lịch sử của các tệp trong thư mục con?

Tôi đoán rằng tôi có thể tạo một bản sao và loại bỏ các phần không mong muốn của mỗi bản sao, nhưng tôi cho rằng điều này sẽ cho tôi cây hoàn chỉnh khi kiểm tra bản sửa đổi cũ hơn, v.v. Điều này có thể được chấp nhận, nhưng tôi thích giả vờ rằng hai kho lưu trữ không có lịch sử chia sẻ.

Để làm cho nó rõ ràng, tôi có cấu trúc sau:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

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

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/

7
Điều này là tầm thường bây giờ với git filter-branchxem câu trả lời của tôi dưới đây.
jeremyjjbrown

8
@jeremyjjbrown là đúng. Điều này không còn khó thực hiện nhưng rất khó để tìm câu trả lời đúng trên Google vì tất cả các câu trả lời cũ chi phối kết quả.
Agnel Kurian

Câu trả lời:


1228

Cập nhật : Quá trình này rất phổ biến, nhóm git đã làm cho nó đơn giản hơn nhiều với một công cụ mới git subtree,. Xem tại đây: Detach (di chuyển) thư mục con vào kho Git riêng


Bạn muốn sao chép kho lưu trữ của mình và sau đó sử dụng git filter-branchđể đánh dấu mọi thứ trừ thư mục con bạn muốn trong repo mới của mình sẽ được thu gom rác.

  1. Để sao chép kho lưu trữ cục bộ của bạn:

    git clone /XYZ /ABC
    

    (Lưu ý: kho lưu trữ sẽ được sao chép bằng các liên kết cứng, nhưng đó không phải là vấn đề vì các tệp được liên kết cứng sẽ không được sửa đổi - các tệp mới sẽ được tạo.)

  2. Bây giờ, chúng ta hãy bảo tồn các nhánh thú vị mà chúng ta muốn viết lại, và sau đó xóa nguồn gốc để tránh đẩy vào đó và để đảm bảo rằng các cam kết cũ sẽ không được tham chiếu bởi nguồn gốc:

    cd /ABC
    for i in branch1 br2 br3; do git branch -t $i origin/$i; done
    git remote rm origin
    

    hoặc cho tất cả các chi nhánh từ xa:

    cd /ABC
    for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done
    git remote rm origin
    
  3. Bây giờ bạn có thể muốn xóa các thẻ không liên quan đến dự án con; bạn cũng có thể làm điều đó sau, nhưng bạn có thể cần phải tỉa lại repo của mình. Tôi đã không làm như vậy và có một WARNING: Ref 'refs/tags/v0.1' is unchangedthẻ cho tất cả các thẻ (vì tất cả chúng đều không liên quan đến tiểu dự án); Ngoài ra, sau khi loại bỏ các thẻ như vậy, nhiều không gian hơn sẽ được thu hồi. Rõ ràng git filter-branchcó thể viết lại các thẻ khác, nhưng tôi không thể xác minh điều này. Nếu bạn muốn xóa tất cả các thẻ, sử dụng git tag -l | xargs git tag -d.

  4. Sau đó sử dụng nhánh lọc và đặt lại để loại trừ các tệp khác, để chúng có thể được cắt bớt. Chúng ta cũng thêm --tag-name-filter cat --prune-emptyđể xóa các cam kết trống và viết lại các thẻ (lưu ý rằng điều này sẽ phải tước chữ ký của chúng):

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC -- --all
    

    hoặc cách khác, để chỉ viết lại nhánh CHÍNH và bỏ qua các thẻ và các nhánh khác:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
    
  5. Sau đó xóa các bản sao lưu dự phòng để không gian có thể được thu hồi thực sự (mặc dù bây giờ hoạt động đã bị phá hủy)

    git reset --hard
    git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
    git reflog expire --expire=now --all
    git gc --aggressive --prune=now
    

    và bây giờ bạn có một kho git cục bộ của thư mục con ABC với tất cả lịch sử của nó được bảo tồn.

Lưu ý: Đối với hầu hết các sử dụng, git filter-branchthực sự nên có tham số được thêm vào -- --all. Vâng, đó thực sự là --space-- all. Điều này cần phải là các tham số cuối cùng cho lệnh. Như Matli đã phát hiện ra, điều này giữ cho các nhánh dự án và các thẻ được bao gồm trong repo mới.

Chỉnh sửa: các đề xuất khác nhau từ các bình luận bên dưới đã được kết hợp để đảm bảo, ví dụ, kho lưu trữ thực sự bị thu hẹp (điều này không phải luôn luôn như vậy trước đây).


29
Câu trả lời rất hay. Cảm ơn! Và để thực sự có được chính xác những gì tôi muốn, tôi đã thêm "- --all" vào lệnh bộ lọc nhánh.
matli

12
Tại sao bạn cần --no-hardlinks? Xóa một liên kết cứng sẽ không ảnh hưởng đến tệp khác. Đối tượng Git là bất biến quá. Chỉ khi bạn thay đổi quyền sở hữu / quyền bạn cần --no-hardlinks.
vdboor

67
Một bước bổ sung tôi muốn giới thiệu sẽ là "git remote rm origin". Điều này sẽ ngăn không cho quay trở lại kho lưu trữ ban đầu, nếu tôi không nhầm.
Tom

13
Một lệnh khác để nối vào filter-branch--prune-empty, để loại bỏ các xác nhận hiện đang trống.
Seth Johnson

8
Giống như Paul, tôi không muốn thẻ dự án trong repo mới của mình, vì vậy tôi đã không sử dụng -- --all. Tôi cũng chạy git remote rm origin, và git tag -l | xargs git tag -dtrước git filter-branchlệnh. Điều này đã thu hẹp .gitthư mục của tôi từ 60M xuống ~ 300K. Lưu ý rằng tôi cần chạy cả hai lệnh này để giảm kích thước.
saltycrane

1321

Cách dễ dàng ™

Hóa ra đây là một cách thực hành phổ biến và hữu ích đến nỗi các lớp phủ của Git làm cho nó thực sự dễ dàng, nhưng bạn phải có một phiên bản Git mới hơn (> = 1.7.11 tháng 5 năm 2012). Xem phần phụ lục để biết cách cài đặt Git mới nhất. Ngoài ra, có một ví dụ thực tế trong phần hướng dẫn bên dưới.

  1. Chuẩn bị repo cũ

    cd <big-repo>
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    

    Lưu ý: <name-of-folder> KHÔNG được chứa các ký tự đầu hoặc cuối. Chẳng hạn, thư mục có tên subprojectPHẢI được chuyển qua subproject, KHÔNG./subproject/

    Lưu ý cho người dùng Windows: Khi độ sâu thư mục của bạn> 1, <name-of-folder>phải có dấu tách thư mục kiểu * nix (/). Chẳng hạn, thư mục có tên path1\path2\subprojectPHẢI được thông qua làpath1/path2/subproject

  2. Tạo repo mới

    mkdir ~/<new-repo> && cd ~/<new-repo>
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Liên kết repo mới với GitHub hoặc bất cứ nơi nào

    git remote add origin <git@github.com:user/new-repo.git>
    git push -u origin master
    
  4. Dọn dẹp bên trong <big-repo>, nếu muốn

    git rm -rf <name-of-folder>
    

    Lưu ý : Điều này để lại tất cả các tham chiếu lịch sử trong kho lưu trữ. Xem Phụ lục bên dưới nếu bạn thực sự lo lắng về việc đã cam kết mật khẩu hoặc bạn cần giảm kích thước tệp của bạn.git thư mục .

...

Hướng dẫn

Đây là các bước tương tự như trên , nhưng làm theo các bước chính xác của tôi cho kho lưu trữ của tôi thay vì sử dụng<meta-named-things> .

Đây là một dự án tôi có để triển khai các mô-đun trình duyệt JavaScript trong nút:

tree ~/node-browser-compat

node-browser-compat
├── ArrayBuffer
├── Audio
├── Blob
├── FormData
├── atob
├── btoa
├── location
└── navigator

Tôi muốn tách ra một thư mục btoa, vào một kho Git riêng

cd ~/node-browser-compat/
git subtree split -P btoa -b btoa-only

Bây giờ tôi có một chi nhánh mới, btoa-onlychỉ có cam kết btoavà tôi muốn tạo một kho lưu trữ mới.

mkdir ~/btoa/ && cd ~/btoa/
git init
git pull ~/node-browser-compat btoa-only

Tiếp theo tôi tạo một repo mới trên GitHub hoặc Bitbucket, hoặc bất cứ điều gì và thêm nó dưới dạng origin

git remote add origin git@github.com:node-browser-compat/btoa.git
git push -u origin master

Ngày hạnh phúc!

Lưu ý: Nếu bạn đã tạo một repo bằng a README.md, .gitignoreLICENSE, bạn sẽ cần phải kéo trước:

git pull origin master
git push origin master

Cuối cùng, tôi sẽ muốn xóa thư mục khỏi repo lớn hơn

git rm -rf btoa

...

ruột thừa

Git mới nhất trên macOS

Để có phiên bản Git mới nhất bằng Homebrew :

brew install git

Git mới nhất trên Ubuntu

sudo apt-get update
sudo apt-get install git
git --version

Nếu điều đó không hoạt động (bạn có phiên bản Ubuntu rất cũ), hãy thử

sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git

Nếu vẫn không được, hãy thử

sudo chmod +x /usr/share/doc/git/contrib/subtree/git-subtree.sh
sudo ln -s \
/usr/share/doc/git/contrib/subtree/git-subtree.sh \
/usr/lib/git-core/git-subtree

Cảm ơn rui.araujo từ các ý kiến.

Xóa lịch sử của bạn

Theo mặc định, việc xóa các tệp khỏi Git không thực sự xóa chúng, nó chỉ cam kết rằng chúng không còn ở đó nữa. Nếu bạn thực sự muốn xóa các tham chiếu lịch sử (nghĩa là bạn đã nhập mật khẩu), bạn cần thực hiện việc này:

git filter-branch --prune-empty --tree-filter 'rm -rf <name-of-folder>' HEAD

Sau đó, bạn có thể kiểm tra xem tệp hoặc thư mục của mình không còn hiển thị trong lịch sử Git hay không

git log -- <name-of-folder> # should show nothing

Tuy nhiên, bạn không thể "đẩy" xóa GitHub và tương tự. Nếu bạn thử, bạn sẽ gặp lỗi và bạn sẽ phải chịu git pulltrước khi bạn có thể git push- và sau đó bạn quay lại để có mọi thứ trong lịch sử của mình.

Vì vậy, nếu bạn muốn xóa lịch sử khỏi "nguồn gốc" - nghĩa là xóa nó khỏi GitHub, Bitbucket, v.v. - bạn sẽ cần xóa repo và đẩy lại một bản sao được cắt tỉa của repo. Nhưng chờ đã - còn nhiều nữa ! - Nếu bạn thực sự lo lắng về việc thoát khỏi mật khẩu hoặc thứ gì đó tương tự, bạn sẽ cần phải cắt bớt bản sao lưu (xem bên dưới).

Làm .gitnhỏ hơn

Lệnh lịch sử xóa đã nói ở trên vẫn để lại một loạt các tệp sao lưu - bởi vì Git rất tốt trong việc giúp bạn không làm hỏng repo của bạn một cách tình cờ. Cuối cùng nó sẽ xóa các tập tin mồ côi trong nhiều ngày và tháng, nhưng nó sẽ để chúng ở đó trong một thời gian trong trường hợp bạn nhận ra rằng bạn đã vô tình xóa một cái gì đó bạn không muốn.

Vì vậy, nếu bạn thực sự muốn dọn sạch thùng rác để giảm kích thước bản sao của một repo ngay lập tức, bạn phải làm tất cả những thứ thực sự kỳ lạ này:

rm -rf .git/refs/original/ && \
git reflog expire --all && \
git gc --aggressive --prune=now

git reflog expire --all --expire-unreachable=0
git repack -A -d
git prune

Điều đó nói rằng, tôi khuyên bạn không nên thực hiện các bước này trừ khi bạn biết rằng bạn cần phải - chỉ trong trường hợp bạn đã cắt tỉa thư mục con sai, bạn biết không? Các tập tin sao lưu không nên được sao chép khi bạn đẩy repo, chúng sẽ ở trong bản sao cục bộ của bạn.

tín dụng


16
git subtreevẫn là một phần của thư mục 'đóng góp' và không được cài đặt theo mặc định trên tất cả các bản phát hành. github.com/git/git/blob/master/contrib/subtree
củ hành tây

11
@krlmlr sudo chmod + x /usr/share/doc/git/contrib/subtree/git-subtree.sh sudo ln -s /usr/share/doc/git/contrib/subtree/git-sub / git-core / git-subtree Để kích hoạt trên Ubuntu 13.04
rui.araujo

41
Nếu bạn đã đẩy mật khẩu vào kho lưu trữ công cộng, bạn nên thay đổi mật khẩu, không cố xóa mật khẩu khỏi repo công khai và hy vọng không ai nhìn thấy nó.
Miles Rout

8
Giải pháp này không bảo tồn lịch sử.
Cœur

18
Lệnh popdpushdlàm cho điều này khá ngầm và khó khăn hơn để tìm hiểu những gì nó dự định làm ...
jones77

133

Câu trả lời của Paul tạo ra một kho lưu trữ mới chứa / ABC, nhưng không xóa / ABC từ bên trong / XYZ. Lệnh sau sẽ xóa / ABC từ bên trong / XYZ:

git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD

Tất nhiên, trước tiên hãy kiểm tra nó trong kho lưu trữ 'clone --no-hardlinks' và theo dõi nó với các lệnh reset, gc và prune Paul liệt kê.


53
làm điều đó git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEADvà nó sẽ nhanh hơn nhiều bộ lọc chỉ mục hoạt động trên chỉ mục trong khi bộ lọc cây phải kiểm tra và xử lý mọi thứ cho mỗi lần xác nhận .
fmarc

51
trong một số trường hợp làm xáo trộn lịch sử của kho XYZ là quá mức cần thiết ... chỉ cần một "rm -rf ABC; git rm -r ABC; git commit -m'extracted ABC vào repo của chính nó" sẽ hoạt động tốt hơn với hầu hết mọi người.
Evgeny

2
Bạn có thể muốn sử dụng -f (lực lượng) cho lệnh này nếu bạn thực hiện nhiều lần, ví dụ: để xóa hai thư mục sau khi chúng được tách ra. Nếu không, bạn sẽ nhận được "Không thể tạo bản sao lưu mới."
Brian Carlton

4
Nếu bạn đang thực hiện --index-filterphương thức, bạn cũng có thể muốn thực hiện điều đó git rm -q -r -f, để mỗi lệnh gọi sẽ không in một dòng cho mỗi tệp mà nó xóa.
Eric Naeseth

1
Tôi muốn đề nghị chỉnh sửa câu trả lời của Paul, chỉ vì Paul rất kỹ lưỡng.
Erik Aronesty

96

Tôi đã thấy rằng để xóa đúng lịch sử cũ khỏi kho lưu trữ mới, bạn phải thực hiện thêm một chút công việc sau filter-branchbước này.

  1. Làm bản sao và bộ lọc:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. Xóa mọi tham chiếu đến lịch sử cũ. Nguồn gốc của người Bỉ đã theo dõi bản sao của bạn, và bản gốc là nơi bộ lọc lưu các nội dung cũ:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. Ngay cả bây giờ, lịch sử của bạn có thể bị kẹt trong một packfile mà fsck sẽ không chạm vào. Xé nó thành mảnh vụn, tạo ra một packfile mới và xóa các đối tượng không sử dụng:

    git repack -ad
    

một lời giải thích về điều này trong hướng dẫn cho nhánh lọc .


3
Tôi nghĩ đôi khi git gc --aggressive --prune=nowvẫn còn thiếu, phải không?
Albert

1
@Albert Lệnh repack đảm nhận việc đó và sẽ không có bất kỳ đối tượng lỏng lẻo nào.
Josh Lee

vâng, git gc --aggressive --prune=nowgiảm nhiều repo mới
Tomek Wyderka

Đơn giản và thanh lịch. Cảm ơn!
Marco Pelegrini

40

Chỉnh sửa: Đã thêm đoạn script Bash.

Các câu trả lời ở đây chỉ làm việc một phần cho tôi; Rất nhiều tập tin lớn vẫn còn trong bộ nhớ cache. Điều cuối cùng đã hoạt động (sau nhiều giờ trong #git trên freenode):

git clone --no-hardlinks file:///SOURCE /tmp/blubb
cd blubb
git filter-branch --subdirectory-filter ./PATH_TO_EXTRACT  --prune-empty --tag-name-filter cat -- --all
git clone file:///tmp/blubb/ /tmp/blooh
cd /tmp/blooh
git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

Với các giải pháp trước đó, kích thước kho lưu trữ là khoảng 100 MB. Cái này đưa nó xuống còn 1,7 MB. Có lẽ nó giúp được ai đó :)


Kịch bản bash sau tự động hóa tác vụ:

!/bin/bash

if (( $# < 3 ))
then
    echo "Usage:   $0 </path/to/repo/> <directory/to/extract/> <newName>"
    echo
    echo "Example: $0 /Projects/42.git first/answer/ firstAnswer"
    exit 1
fi


clone=/tmp/${3}Clone
newN=/tmp/${3}

git clone --no-hardlinks file://$1 ${clone}
cd ${clone}

git filter-branch --subdirectory-filter $2  --prune-empty --tag-name-filter cat -- --all

git clone file://${clone} ${newN}
cd ${newN}

git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

26

Điều này không còn quá phức tạp, bạn chỉ có thể sử dụng lệnh git bộ lọc nhánh trên bản sao của bạn repo để loại bỏ các thư mục con bạn không muốn và sau đó đẩy sang điều khiển từ xa mới.

git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .

3
Điều này làm việc như một nét duyên dáng. Your_SUBDIR trong ví dụ trên là thư mục con mà bạn muốn KEEP, mọi thứ khác sẽ bị xóa
JT Taylor

1
Cập nhật dựa trên bình luận của bạn.
jeremyjjbrown

2
Điều này không trả lời câu hỏi. Từ các tài liệu nó nói The result will contain that directory (and only that) as its project root.và thực sự đây là những gì bạn sẽ nhận được, tức là cấu trúc dự án ban đầu không được bảo tồn.
NicBright

2
@NicBright Bạn có thể minh họa vấn đề của mình với XYZ và ABC như trong câu hỏi, để chỉ ra điều gì sai không?
Adam

@jeremyjjbrown có thể sử dụng lại repo nhân bản và không sử dụng repo mới, tức là câu hỏi của tôi ở đây stackoverflow.com/questions/49269602/
Lỗi

19

Cập nhật : Mô-đun git-Subree rất hữu ích đến nỗi nhóm git đã kéo nó vào lõi và tạo ra nó git subtree. Xem tại đây: Detach (di chuyển) thư mục con vào kho Git riêng

git-Subree có thể hữu ích cho việc này

http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt (không dùng nữa)

http://psionides.jogger.pl/2010/02/04/shaming-code-b between-project-with-git-subtree /


1
git-Subree hiện là một phần của Git, mặc dù nó nằm trong cây contrib, do đó không phải lúc nào cũng được cài đặt theo mặc định. Tôi biết nó được cài đặt theo công thức Homebrew git, nhưng không có trang man của nó. apenwarr do đó gọi phiên bản của mình đã lỗi thời.
echristopherson

19

Đây là một thay đổi nhỏ để CoolAJ86 's 'The Easy Way ™' câu trả lời để chia nhiều thư mục phụ (giả sử sub1sub2 ) vào một kho git mới.

Easy Way ™ (nhiều thư mục phụ)

  1. Chuẩn bị repo cũ

    pushd <big-repo>
    git filter-branch --tree-filter "mkdir <name-of-folder>; mv <sub1> <sub2> <name-of-folder>/" HEAD
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    popd
    

    Lưu ý: <name-of-folder> KHÔNG được chứa các ký tự đầu hoặc cuối. Chẳng hạn, thư mục có tên subprojectPHẢI được chuyển qua subproject, KHÔNG./subproject/

    Lưu ý cho người dùng windows: khi độ sâu thư mục của bạn> 1, <name-of-folder>phải có dấu tách thư mục kiểu * nix (/). Chẳng hạn, thư mục có tên path1\path2\subprojectPHẢI được chuyển qua path1/path2/subproject. Hơn nữa không sử dụng mvlệnh nhưng move.

    Lưu ý cuối cùng: sự khác biệt lớn và duy nhất với câu trả lời cơ bản là dòng thứ hai của tập lệnh " git filter-branch..."

  2. Tạo repo mới

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Liên kết repo mới với Github hoặc bất cứ nơi nào

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. Dọn dẹp, nếu muốn

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    Lưu ý : Điều này để lại tất cả các tham chiếu lịch sử trong kho lưu trữ. Xem Phụ lục trong câu trả lời ban đầu nếu bạn thực sự lo lắng về việc đã nhập mật khẩu hoặc bạn cần giảm kích thước tệp của .gitthư mục.


1
Điều này làm việc cho tôi với sửa đổi nhỏ. Vì phiên bản sub1sub2thư mục của tôi không tồn tại với phiên bản ban đầu, tôi đã phải sửa đổi --tree-filtertập lệnh của mình như sau : "mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi". Đối với filter-branchlệnh thứ hai, tôi đã thay thế <sub1> bằng <sub2>, bỏ qua việc tạo <name-of-thư mục> và được bao gồm -fsau filter-branchđể ghi đè cảnh báo của bản sao lưu hiện có.
pglezen

Điều này không hoạt động nếu bất kỳ thư mục con nào đã thay đổi trong lịch sử trong git. Làm thế nào điều này có thể được giải quyết?
nietras

@nietras xem câu trả lời của rogerdpack. Mất một lúc tôi mới tìm thấy nó sau khi đọc và tiếp thu tất cả thông tin trong những câu trả lời khác này.
Adam

12

Câu hỏi ban đầu muốn các tệp XYZ / ABC / (*) trở thành các tệp ABC / ABC / (*). Sau khi thực hiện câu trả lời được chấp nhận cho mã của riêng tôi, tôi nhận thấy rằng nó thực sự thay đổi các tệp XYZ / ABC / (*) thành các tệp ABC / (*). Trang người đàn ông của bộ lọc thậm chí còn nói,

Kết quả sẽ chứa thư mục đó (và chỉ có điều đó) làm gốc dự án của nó . "

Nói cách khác, nó thúc đẩy thư mục cấp cao nhất "lên" một cấp. Đó là một sự khác biệt quan trọng bởi vì, ví dụ, trong lịch sử của tôi, tôi đã đổi tên một thư mục cấp cao nhất. Bằng cách thúc đẩy các thư mục "lên" một cấp, git sẽ mất tính liên tục tại cam kết nơi tôi đã đổi tên.

Tôi đã mất thông tin sau khi lọc chi nhánh

Câu trả lời của tôi cho câu hỏi sau đó là tạo 2 bản sao của kho lưu trữ và xóa thủ công (các) thư mục bạn muốn giữ trong mỗi thư mục. Trang người đàn ông ủng hộ tôi với điều này:

[...] tránh sử dụng [lệnh này] nếu một cam kết đơn giản đủ để khắc phục sự cố của bạn


1
Tôi thích phong cách của đồ thị đó. Tôi có thể hỏi bạn đang sử dụng công cụ nào không?
Slipp D. Thompson

3
Tháp cho Mac. Tôi thực sự thích nó. Bản thân nó gần như đáng để chuyển sang Mac.
MM.

2
Đúng, mặc dù trong trường hợp của tôi, thư mục con của tôi targetdirđã được đổi tên tại một số điểm và git filter-branchchỉ đơn giản gọi nó là một ngày, xóa tất cả các cam kết được thực hiện trước khi đổi tên! Thật sốc, xem xét cách Git lão luyện trong việc theo dõi những thứ như vậy và thậm chí di chuyển các khối nội dung riêng lẻ!
Jay Allen

1
Ồ, ngoài ra, nếu bất cứ ai thấy mình trong cùng một chiếc thuyền, đây là lệnh tôi đã sử dụng. Đừng quên git rmmất nhiều đối số, vì vậy không có lý do gì để chạy nó cho từng tệp / thư mục: BYEBYE="dir/subdir2 dir2 file1 dir/file2"; git filter-branch -f --index-filter "git rm -q -r -f --cached --ignore-unmatch $BYEBYE" --prune-empty -- --all
Jay Allen

7

Để thêm vào câu trả lời của Paul , tôi thấy rằng để cuối cùng phục hồi không gian, tôi phải đẩy HEAD vào một kho lưu trữ sạch và cắt giảm kích thước của thư mục .git / object / pack.

I E

$ mkdir ... ABC.git
$ cd ... ABC.git
$ git init --bare

Sau gc prune, cũng làm:

$ git đẩy ... ABC.git TRỤ

Sau đó bạn có thể làm

$ git nhân bản ... ABC.git

và kích thước của ABC / .git bị giảm

Trên thực tế, một số bước tốn thời gian (ví dụ: git gc) không cần thiết với việc đẩy sang kho lưu trữ sạch, tức là:

$ git clone - không liên kết cứng / XYZ / ABC
$ git bộ lọc-chi nhánh - bộ lọc ABC
thiết lập lại $ git -
$ git đẩy ... ABC.git TRỤ

6

Cách thích hợp bây giờ là như sau:

git filter-branch --prune-empty --subdirectory-filter FOLDER_NAME [first_branch] [another_branch]

GitHub bây giờ thậm chí có bài viết nhỏ về những trường hợp như vậy.

Nhưng hãy chắc chắn sao chép repo gốc của bạn vào thư mục riêng trước (vì nó sẽ xóa tất cả các tệp và các thư mục khác và bạn có thể cần phải làm việc với chúng).

Vì vậy, thuật toán của bạn nên là:

  1. sao chép repo từ xa của bạn vào một thư mục khác
  2. sử dụng git filter-branchcác tệp chỉ còn lại trong một số thư mục con, đẩy sang điều khiển từ xa mới
  3. tạo cam kết để loại bỏ thư mục con này khỏi repo từ xa ban đầu của bạn

6

Dường như hầu hết (tất cả?) Các câu trả lời ở đây đều dựa vào một số dạng git filter-branch --subdirectory-filtervà ilk của nó. Điều này có thể hoạt động "hầu hết thời gian" tuy nhiên đối với một số trường hợp, ví dụ như trường hợp khi bạn đổi tên thư mục, ví dụ:

 ABC/
    /move_this_dir # did some work here, then renamed it to

ABC/
    /move_this_dir_renamed

Nếu bạn thực hiện kiểu bộ lọc git bình thường để trích xuất "move_me_renamed", bạn sẽ mất lịch sử thay đổi tệp xảy ra từ phía sau khi ban đầu move_this_dir ( ref ).

Do đó, có vẻ như cách duy nhất để thực sự giữ tất cả lịch sử thay đổi (nếu bạn là trường hợp như thế này), về bản chất, là sao chép kho lưu trữ (tạo repo mới, đặt đó là gốc), sau đó nuke mọi thứ khác và đổi tên thư mục con thành cha mẹ như thế này:

  1. Nhân bản dự án đa mô-đun tại địa phương
  2. Chi nhánh - kiểm tra những gì có: git branch -a
  3. Thực hiện kiểm tra cho mỗi chi nhánh để được bao gồm trong phần tách để có được một bản sao cục bộ trên máy trạm của bạn: git checkout --track origin/branchABC
  4. Tạo một bản sao trong một thư mục mới: cp -r oldmultimod simple
  5. Đi vào bản sao dự án mới: cd simple
  6. Loại bỏ các mô-đun khác không cần thiết trong dự án này:
  7. git rm otherModule1 other2 other3
  8. Bây giờ chỉ còn phần con của mô đun đích
  9. Loại bỏ tiểu thư mô-đun để gốc mô-đun trở thành gốc dự án mới
  10. git mv moduleSubdir1/* .
  11. Xóa subir di tích: rmdir moduleSubdir1
  12. Kiểm tra các thay đổi tại bất kỳ điểm nào: git status
  13. Tạo repo git mới và sao chép URL của nó để trỏ dự án này vào đó:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. Xác nhận điều này là tốt: git remote -v
  16. Đẩy các thay đổi lên đến repo từ xa: git push
  17. Đi đến repo từ xa và kiểm tra tất cả ở đó
  18. Lặp lại nó cho bất kỳ chi nhánh khác cần thiết: git checkout branch2

Điều này tuân theo tài liệu github "Tách một thư mục con ra một kho lưu trữ mới" các bước 6-11 để đẩy mô-đun sang một repo mới.

Điều này sẽ không tiết kiệm cho bạn bất kỳ không gian nào trong thư mục .git của bạn, nhưng nó sẽ lưu giữ tất cả lịch sử thay đổi của bạn cho các tệp đó ngay cả khi đổi tên. Và điều này có thể không có giá trị nếu không có "rất nhiều" lịch sử bị mất, v.v. Nhưng ít nhất bạn được đảm bảo không để mất các cam kết cũ hơn!


1
Tìm thấy kim trong git haystack! Bây giờ tôi có thể giữ TẤT CẢ lịch sử cam kết của mình.
Adam

5

Tôi khuyên bạn nên hướng dẫn của GitHub để tách các thư mục con thành một kho lưu trữ mới . Các bước tương tự như câu trả lời của Paul , nhưng tôi thấy hướng dẫn của họ dễ hiểu hơn.

Tôi đã sửa đổi các hướng dẫn để chúng áp dụng cho một kho lưu trữ cục bộ, thay vì một lưu trữ trên GitHub.


Tách một thư mục con ra một kho lưu trữ mới

  1. Mở Git Bash.

  2. Thay đổi thư mục làm việc hiện tại thành vị trí bạn muốn tạo kho lưu trữ mới.

  3. Nhân bản kho lưu trữ chứa thư mục con.

git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
  1. Thay đổi thư mục làm việc hiện tại vào kho lưu trữ nhân bản của bạn.

cd REPOSITORY-NAME
  1. Để lọc thư mục con khỏi phần còn lại của tệp trong kho lưu trữ, hãy chạy git filter-branch, cung cấp thông tin này:
    • FOLDER-NAME: Thư mục trong dự án của bạn mà bạn muốn tạo một kho lưu trữ riêng biệt.
      • Mẹo: Người dùng Windows nên sử dụng /để phân định các thư mục.
    • BRANCH-NAME: Chi nhánh mặc định cho dự án hiện tại của bạn, ví dụ, masterhoặc gh-pages.

git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME  BRANCH-NAME 
# Filter the specified branch in your directory and remove empty commits
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/BRANCH-NAME' was rewritten

Bài đăng hay, nhưng tôi nhận thấy đoạn đầu tiên của tài liệu mà bạn liên kết nói If you create a new clone of the repository, you won't lose any of your Git history or changes when you split a folder into a separate repository.Tuy nhiên theo nhận xét về tất cả các câu trả lời ở đây filter-branchvà cả subtreekịch bản dẫn đến việc mất lịch sử ở bất cứ nơi nào một thư mục con đã được đổi tên. Có bất cứ điều gì có thể được thực hiện để giải quyết điều này?
Adam

Tìm thấy giải pháp để bảo toàn tất cả các cam kết, bao gồm cả những lần đổi tên / di chuyển thư mục trước đó - đó là câu trả lời của rogerdpack cho chính câu hỏi này.
Adam

Vấn đề duy nhất là tôi không thể sử dụng repo nhân bản nữa
Qiulang

5

Khi chạy git filter-branchbằng phiên bản mới hơn git( 2.22+có thể?), Nó nói sẽ sử dụng công cụ mới này git-filter-repo . Công cụ này chắc chắn đơn giản hóa mọi thứ cho tôi.

Lọc với bộ lọc-repo

Các lệnh để tạo XYZrepo từ câu hỏi ban đầu:

# create local clone of original repo in directory XYZ
tmp $ git clone git@github.com:user/original.git XYZ

# switch to working in XYZ
tmp $ cd XYZ

# keep subdirectories XY1 and XY2 (dropping ABC)
XYZ $ git filter-repo --path XY1 --path XY2

# note: original remote origin was dropped
# (protecting against accidental pushes overwriting original repo data)

# XYZ $ ls -1
# XY1
# XY2

# XYZ $ git log --oneline
# last commit modifying ./XY1 or ./XY2
# first commit modifying ./XY1 or ./XY2

# point at new hosted, dedicated repo
XYZ $ git remote add origin git@github.com:user/XYZ.git

# push (and track) remote master
XYZ $ git push -u origin master

các giả định: * repo XYZ từ xa là mới và trống trước khi đẩy

Lọc và di chuyển

Trong trường hợp của tôi, tôi cũng muốn di chuyển một vài thư mục để có cấu trúc phù hợp hơn. Ban đầu, tôi đã chạy filter-repolệnh đơn giản đó theo sau git mv dir-to-rename, nhưng tôi thấy tôi có thể có được một lịch sử "tốt hơn" một chút bằng cách sử dụng --path-renametùy chọn. Thay vì nhìn thấy lần sửa đổi cuối cùng 5 hours agotrên các tệp đã di chuyển trong repo mới, bây giờ tôi thấy last year(trong Giao diện người dùng GitHub), khớp với thời gian đã sửa đổi trong repo gốc.

Thay vì...

git filter-repo --path XY1 --path XY2 --path inconsistent
git mv inconsistent XY3  # which updates last modification time

Cuối cùng tôi đã chạy ...

git filter-repo --path XY1 --path XY2 --path inconsistent --path-rename inconsistent:XY3
Ghi chú:
  • Tôi nghĩ rằng bài đăng trên blog Git Rev News đã giải thích tốt lý do đằng sau việc tạo ra một công cụ lọc repo khác.
  • Ban đầu tôi đã thử đường dẫn tạo thư mục con khớp với tên repo đích trong kho lưu trữ ban đầu và sau đó lọc (sử dụng git filter-repo --subdirectory-filter dir-matching-new-repo-name). Lệnh đó đã chuyển đổi chính xác thư mục con đó thành thư mục gốc của repo cục bộ được sao chép, nhưng nó cũng dẫn đến một lịch sử chỉ có ba cam kết cần có để tạo thư mục con. (Tôi đã không nhận ra rằng --pathcó thể được chỉ định nhiều lần; do đó, không cần phải tạo thư mục con trong repo nguồn.) Vì ai đó đã cam kết với repo nguồn vào thời điểm tôi nhận thấy rằng tôi đã không thực hiện được lịch sử, tôi chỉ sử dụng git reset commit-before-subdir-move --hardsau khi clonelệnh, và thêm --forcevào filter-repolệnh để làm cho nó hoạt động trên clone địa phương chút thay đổi.
git clone ...
git reset HEAD~7 --hard      # roll back before mistake
git filter-repo ... --force  # tell filter-repo the alterations are expected
  • Tôi đã bị vấp ngã khi cài đặt vì tôi không biết mẫu mở rộng với git, nhưng cuối cùng tôi đã nhân bản git-filter-repo và liên kết nó với $(git --exec-path):
ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)

1
Được khuyến khích giới thiệu filter-repocông cụ mới (mà tôi đã trình bày vào tháng trước trong stackoverflow.com/a/58251653/6309 )
VonC

Sử dụng git-filter-repochắc chắn nên là phương pháp ưa thích tại thời điểm này. Nó nhanh hơn rất nhiều và an toàn hơn git-filter-branch, và bảo vệ chống lại rất nhiều vấn đề mà người ta có thể gặp phải khi viết lại lịch sử git của một người. Hy vọng rằng câu trả lời này sẽ được chú ý nhiều hơn, vì đó là câu trả lời git-filter-repo.
Jeremy Caney

4

Tôi đã có chính xác vấn đề này nhưng tất cả các giải pháp tiêu chuẩn dựa trên nhánh bộ lọc git đều rất chậm. Nếu bạn có một kho lưu trữ nhỏ thì đây có thể không phải là vấn đề, nó là dành cho tôi. Tôi đã viết một chương trình lọc git khác dựa trên libgit2, bước đầu tiên tạo ra các nhánh cho mỗi lần lọc của kho lưu trữ chính và sau đó đẩy chúng vào các kho lưu trữ như bước tiếp theo. Trên kho lưu trữ của tôi (500Mb 100000 cam kết), các phương thức nhánh bộ lọc git tiêu chuẩn mất nhiều ngày. Chương trình của tôi mất vài phút để thực hiện lọc tương tự.

Nó có tên tuyệt vời của git_filter và sống ở đây:

https://github.com/slobobaby / git_filter

trên GitHub.

Tôi hy vọng nó hữu ích cho ai đó.


4

Sử dụng lệnh bộ lọc này để xóa thư mục con, đồng thời bảo toàn các thẻ và nhánh của bạn:

git filter-branch --index-filter \
"git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \
--tag-name-filter cat -- --all

mèo ở đây là gì
rogerdpack

4

Để biết giá trị của nó, đây là cách sử dụng GitHub trên máy Windows. Giả sử bạn có một repo nhân bản trong cư trú C:\dir1. Cấu trúc thư mục trông như thế này : C:\dir1\dir2\dir3. Thư mục dir3này là thư mục tôi muốn trở thành một repo riêng biệt mới.

Github:

  1. Tạo kho lưu trữ mới của bạn: MyTeam/mynewrepo

Bash nhắc nhở:

  1. $ cd c:/Dir1
  2. $ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
    Trả về: Ref 'refs/heads/master' was rewritten(fyi: dir2 / dir3 phân biệt chữ hoa chữ thường.)

  3. $ git remote add some_name git@github.com:MyTeam/mynewrepo.git
    git remote add origin etc. không hoạt động, trả lại " remote origin already exists"

  4. $ git push --progress some_name master


3

Như tôi đã đề cập ở trên , tôi đã phải sử dụng giải pháp đảo ngược (xóa tất cả các cam kết không chạm vào tôi dir/subdir/targetdir) dường như hoạt động khá tốt khi loại bỏ khoảng 95% các cam kết (như mong muốn). Tuy nhiên, có hai vấn đề nhỏ còn lại.

FIRST , filter-branchđã thực hiện một công việc rất khó khăn trong việc loại bỏ các cam kết giới thiệu hoặc sửa đổi mã nhưng rõ ràng, các cam kết hợp nhất nằm bên dưới trạm của nó trong Gitiverse.

Đây là một vấn đề thẩm mỹ mà tôi có lẽ có thể sống với (anh ấy nói ... lùi dần dần với đôi mắt đảo ngược) .

THỨ HAI một số cam kết còn lại là khá nhiều TẤT CẢ trùng lặp! Tôi dường như đã có được một dòng thời gian dư thừa thứ hai kéo dài gần như toàn bộ lịch sử của dự án. Điều thú vị (mà bạn có thể nhìn thấy từ hình ảnh bên dưới), là ba chi nhánh địa phương của tôi không phải là tất cả trên cùng một dòng thời gian (đó là, chắc chắn là tại sao nó tồn tại và không chỉ là rác được thu thập).

Điều duy nhất tôi có thể tưởng tượng là một trong những cam kết đã bị xóa là, có lẽ, một cam kết hợp nhất duy nhất filter-branch thực sự đã xóa và điều đó đã tạo ra dòng thời gian song song khi mỗi chuỗi không được trộn lẫn đã lấy bản sao của các cam kết. ( nhún vai TARDiS của tôi ở đâu?) Tôi khá chắc chắn rằng tôi có thể khắc phục vấn đề này, mặc dù tôi thực sự muốn hiểu nó đã xảy ra như thế nào.

Trong trường hợp hợp nhất điên rồ-O-RAMA, tôi có thể sẽ để nó một mình vì nó đã cố thủ rất nhiều trong lịch sử cam kết của tôi, mối đe dọa với tôi bất cứ khi nào tôi đến gần, điều đó dường như không thực sự gây ra bất kỳ vấn đề phi mỹ phẩm nào và vì nó khá đẹp trong Tower.app.


3

Cách dễ dàng hơn

  1. cài đặt git splits. Tôi đã tạo nó dưới dạng tiện ích mở rộng git, dựa trên giải pháp của jkeat .
  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 XY1 XY2

  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


Một lợi thế của phương pháp này so với "Cách dễ dàng" là điều khiển từ xa đã được thiết lập cho repo mới, vì vậy bạn có thể ngay lập tức thực hiện thêm một cây con. Trên thực tế, cách này có vẻ dễ hơn đối với tôi (ngay cả khi không có git splits)
MM

Đạo cụ cho AndrewD để đăng giải pháp này. Tôi đã rẽ nhánh repo của anh ấy để làm cho nó hoạt động trên OSX ( github.com/ricardoespsanto/git-splits ) nếu điều đó hữu ích cho bất kỳ ai khác
ricardoespsanto

2

Bạn có thể cần một cái gì đó như "git reflog expire --Exire = now --all" trước khi bộ sưu tập rác thực sự dọn sạch các tệp. nhánh bộ lọc git chỉ xóa các tham chiếu trong lịch sử, nhưng không xóa các mục nhập lưu trữ dữ liệu. Tất nhiên, kiểm tra điều này đầu tiên.

Việc sử dụng đĩa của tôi giảm đáng kể khi làm điều này, mặc dù điều kiện ban đầu của tôi có hơi khác. Có lẽ --subdirectory-filter phủ nhận nhu cầu này, nhưng tôi nghi ngờ nó.


2

Kiểm tra dự án git_split tại https://github.com/vangorra/git_split

Biến thư mục git vào kho lưu trữ của riêng họ ở vị trí riêng của họ. Không có kinh doanh buồn cười. Kịch bản lệnh này sẽ lấy một thư mục hiện có trong kho git của bạn và biến thư mục đó thành một kho lưu trữ độc lập của riêng nó. Trên đường đi, nó sẽ sao chép toàn bộ lịch sử thay đổi cho thư mục bạn cung cấp.

./git_split.sh <src_repo> <src_branch> <relative_dir_path> <dest_repo>
        src_repo  - The source repo to pull from.
        src_branch - The branch of the source repo to pull from. (usually master)
        relative_dir_path   - Relative path of the directory in the source repo to split.
        dest_repo - The repo to push to.

1

Đặt cái này vào gitconfig của bạn:

reduce-to-subfolder = !sh -c 'git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter cookbooks/unicorn HEAD && git reset --hard && git for-each-ref refs/original/ | cut -f 2 | xargs -n 1 git update-ref -d && git reflog expire --expire=now --all && git gc --aggressive --prune=now && git remote rm origin'

1

Tôi chắc chắn rằng cây con git đều ổn và tuyệt vời, nhưng các thư mục con của mã git được quản lý mà tôi muốn di chuyển là tất cả trong nhật thực. Vì vậy, nếu bạn đang sử dụng egit, thật dễ dàng. Thực hiện dự án bạn muốn di chuyển và nhóm-> ngắt kết nối dự án, sau đó nhóm-> chia sẻ nó đến vị trí mới. Nó sẽ mặc định cố gắng sử dụng vị trí repo cũ, nhưng bạn có thể bỏ chọn lựa chọn hiện có và chọn địa điểm mới để di chuyển vị trí đó. Tất cả mưa đá egit.


3
Phần "tốt và tuyệt vời" của cây con là lịch sử của thư mục con của bạn đi cùng. Nếu bạn không cần lịch sử, thì phương pháp dễ dàng của bạn là cách để đi.
pglezen

0

Bạn có thể dễ dàng thử https://help.github.com/enterprise/2.15/user/articles/splmit-a-subfolder-out-into-a-new-reposeective/

Điều này làm việc cho tôi. Các vấn đề tôi gặp phải trong các bước được đưa ra ở trên là

  1. trong lệnh này git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME Các BRANCH-NAMEbậc thầy

  2. nếu bước cuối cùng không thành công khi cam kết do vấn đề bảo vệ, hãy làm theo - https://docs.gitlab.com/ee/user/project/protected_branches.html


0

Tôi đã tìm thấy giải pháp khá đơn giản, Ý tưởng là sao chép kho lưu trữ và sau đó chỉ cần loại bỏ phần không cần thiết. Đây là cách nó hoạt động:

1) Sao chép một kho lưu trữ mà bạn muốn chia

git clone git@git.thehost.io:testrepo/test.git

2) Di chuyển đến thư mục git

cd test/

2) Xóa các thư mục không cần thiết và cam kết nó

rm -r ABC/
git add .
enter code here
git commit -m 'Remove ABC'

3) Xóa lịch sử biểu mẫu thư mục không cần thiết với BFG

cd ..
java -jar bfg.jar --delete-folders "{ABC}" test
cd test/
git reflog expire --expire=now --all && git gc --prune=now --aggressive

để nhân nhiều thư mục bạn có thể sử dụng dấu phẩy

java -jar bfg.jar --delete-folders "{ABC1,ABC2}" metric.git

4) Kiểm tra xem lịch sử không chứa các tệp / thư mục bạn vừa xóa

git log --diff-filter=D --summary | grep delete

5) Bây giờ bạn có kho lưu trữ sạch mà không có ABC, vì vậy chỉ cần đẩy nó vào nguồn gốc mới

remote add origin git@github.com:username/new_repo
git push -u origin master

Đó là nó. Bạn có thể lặp lại các bước để có được một kho lưu trữ khác,

chỉ cần xóa XY1, XY2 và đổi tên XYZ -> ABC ở bước 3


Gần như hoàn hảo ... nhưng bạn đã quên "git filter-Branch --prune-blank" để xóa tất cả các xác nhận cũ hiện đang trống. Để làm trước khi đẩy để nguồn gốc!
ZettaCircl

Nếu bạn đã phạm sai lầm và vẫn muốn "sửa lại" sau khi đã xóa cam kết trống cũ, hãy thực hiện: "git push -u master master --force-with-cho thuê"
ZettaCircl
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.