แยกไดเรกทอรีย่อยจำนวนมากออกเป็นที่เก็บ Git ใหม่แยกต่างหาก


137

คำถามนี้อ้างอิงจากไดเร็กทอรีย่อยแยกออกเป็นที่เก็บ Git แยกต่างหาก

แทนที่จะแยกไดเรกทอรีย่อยเดียวฉันต้องการแยกสองรายการออก ตัวอย่างเช่นโครงสร้างไดเรกทอรีปัจจุบันของฉันมีลักษณะดังนี้:

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

และฉันต้องการสิ่งนี้แทน:

/apps
  /AAA
/libs
  /XXX

--subdirectory-filterอาร์กิวเมนต์git filter-branchจะไม่ทำงานเพราะจะได้รับการกำจัดของทุกอย่างยกเว้นไดเรกทอรีที่กำหนดครั้งแรกที่มันวิ่ง ฉันคิดว่าการใช้--index-filterอาร์กิวเมนต์สำหรับไฟล์ที่ไม่ต้องการทั้งหมดจะใช้งานได้ (แม้ว่าจะน่าเบื่อ) แต่ถ้าฉันลองเรียกใช้มากกว่าหนึ่งครั้งฉันจะได้รับข้อความต่อไปนี้:

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

ความคิดใด ๆ ? TIA

คำตอบ:


161

แทนที่จะต้องจัดการกับ subshell และใช้ ext glob (ตามที่kynan แนะนำ ) ลองใช้วิธีที่ง่ายกว่านี้:

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

ตามที่กล่าวไว้ในความคิดเห็นของ void.pointerสิ่งนี้จะลบทุกอย่างยกเว้นapps/AAAและlibs/XXXจากที่เก็บปัจจุบัน

พรุนเปล่าผสานกระทำ

สิ่งนี้ทิ้งการผสานที่ว่างเปล่าจำนวนมาก สิ่งเหล่านี้สามารถลบออกได้ด้วยบัตรอื่นตามที่อธิบายโดยraphinesseในคำตอบของเขา:

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

⚠️คำเตือน : ผู้ต้องดังกล่าวข้างต้นรุ่นที่ใช้ GNU ของsedและxargsมิฉะนั้นก็จะเอากระทำทั้งหมดเป็นxargsล้มเหลว brew install gnu-sed findutilsจากนั้นใช้gsedและgxargs:

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

4
นอกจากนี้แฟล็ก --ignore-unmatch ควรถูกส่งผ่านไปยัง git rm มันล้มเหลวสำหรับการคอมมิตครั้งแรกสำหรับฉันเป็นอย่างอื่น (ที่เก็บถูกสร้างขึ้นด้วย git svn clone ในกรณีของฉัน)
Pontomedon

8
สมมติว่าคุณมีแท็กในส่วนผสมคุณควรเพิ่ม--tag-name-filter catพารามิเตอร์ของคุณ
Yonatan

16
คุณสามารถเพิ่มข้อมูลเพิ่มเติมเพื่ออธิบายว่าคำสั่งที่มีความยาวนี้ใช้ทำอะไร
Burhan Ali

4
ฉันประหลาดใจมากที่มันทำงานได้อย่างสมบูรณ์บน Windows โดยใช้ git bash ว้าว!
Dai

4
@BurhanAli สำหรับทุกการกระทำในประวัติศาสตร์จะเป็นการลบไฟล์ทั้งหมดยกเว้นไฟล์ที่คุณต้องการเก็บไว้ เมื่อทุกอย่างเสร็จสิ้นคุณจะเหลือเพียงส่วนของต้นไม้ที่คุณระบุพร้อมกับประวัติเท่านั้น
void.pointer

39

ขั้นตอนแบบแมนนวลพร้อมคำสั่งคอมไพล์ง่ายๆ

แผนคือการแยกแต่ละไดเรกทอรีออกเป็น repos ของตัวเองจากนั้นรวมเข้าด้วยกัน ขั้นตอนแบบแมนนวลต่อไปนี้ไม่ได้ใช้สคริปต์ที่ใช้งานเกินขนาด แต่เป็นคำสั่งที่เข้าใจง่ายและสามารถช่วยรวมโฟลเดอร์ย่อย N พิเศษลงในที่เก็บเดียวได้

หาร

สมมติว่า repo เดิมของคุณคือoriginal_repo

1 - แยกแอป:

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

2 - แยก libs

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

ดำเนินการต่อหากคุณมีมากกว่า 2 โฟลเดอร์ ตอนนี้คุณจะมีที่เก็บ git ใหม่และชั่วคราวสองที่

พิชิตโดยการรวมแอพและ libs

3 - เตรียม repo ใหม่ล่าสุด:

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

และคุณจะต้องทำการคอมมิตอย่างน้อยหนึ่งครั้ง หากควรข้ามสามบรรทัดต่อไปนี้ repo แรกของคุณจะปรากฏทันทีภายใต้รูท repo ของคุณ:

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"

เมื่อคอมมิตไฟล์ชั่วคราวmergeคำสั่งในส่วนต่อมาจะหยุดตามที่คาดไว้

จากความคิดเห็นของผู้ใช้แทนที่จะเพิ่มไฟล์แบบสุ่มเช่นa_file_and_make_a_commitคุณสามารถเลือกที่จะเพิ่ม.gitignoreหรือREADME.mdอื่น ๆ

4 - รวมแอป repo ก่อน:

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"

ตอนนี้คุณควรเห็นไดเรกทอรีแอพในที่เก็บใหม่ของคุณ git logควรแสดงข้อความยืนยันในอดีตที่เกี่ยวข้องทั้งหมด

หมายเหตุ: ดังที่ Chris ระบุไว้ด้านล่างในความคิดเห็นสำหรับ git เวอร์ชันใหม่กว่า (> = 2.9) คุณต้องระบุ--allow-unrelated-historiesด้วยgit merge

5 - รวม libs repo ถัดไปในลักษณะเดียวกัน:

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"

ดำเนินการต่อหากคุณมีมากกว่า 2 repos ที่จะรวม

การอ้างอิง: ผสานไดเร็กทอรีย่อยของที่เก็บอื่นด้วย git


4
เนื่องจาก git 2.9 คุณต้องใช้ --allow-unrelated-histories กับคำสั่ง merge มิฉะนั้นดูเหมือนว่าจะทำงานได้ดีสำหรับฉัน
คริส

1
อัจฉริยะ! ขอบคุณมากสำหรับเรื่องนี้. คำตอบเริ่มต้นที่ฉันดูโดยใช้ตัวกรองต้นไม้บนพื้นที่เก็บข้อมูลขนาดใหญ่มากมีการคาดการณ์ว่า git จะใช้เวลามากกว่า 26 ชั่วโมงในการเขียน git ใหม่ให้เสร็จสมบูรณ์ มีความสุขมากขึ้นกับวิธีการที่เรียบง่าย แต่สามารถทำซ้ำได้นี้และได้ย้าย 4 โฟลเดอร์ย่อยไปยัง repo ใหม่พร้อมประวัติการกระทำที่คาดไว้ทั้งหมด
ปิด

1
คุณสามารถใช้การกระทำแรกสำหรับ "การกระทำเริ่มต้น" ซึ่งเพิ่ม.gitignoreและREADME.mdไฟล์
Jack Miller

2
น่าเสียดายที่วิธีนี้ดูเหมือนจะทำลายประวัติการติดตามสำหรับไฟล์ที่เพิ่มในgit merge .. git read-treeขั้นตอนเนื่องจากบันทึกเป็นไฟล์ที่เพิ่มใหม่และ git guis ทั้งหมดของฉันไม่ได้ทำการเชื่อมต่อกับการกระทำก่อนหน้านี้
Dai

1
@ksadjad คิดไม่ออกจริงๆ จุดศูนย์กลางของการผสานด้วยตนเองคือการเลือกไดเร็กทอรีเพื่อสร้าง repo ใหม่และเก็บประวัติการคอมมิตไว้ ฉันไม่แน่ใจว่าจะจัดการกับสถานการณ์ดังกล่าวได้อย่างไรเมื่อการคอมมิตใส่ไฟล์ลงใน dirA, dirB, dirDrop และมีเพียง dirA และ dirB เท่านั้นที่ถูกเลือกสำหรับ repo ใหม่ประวัติการกระทำควรเกี่ยวข้องกับไฟล์ต้นฉบับอย่างไร
chfw

28

ทำไมคุณถึงต้องการวิ่งfilter-branchมากกว่าหนึ่งครั้ง? คุณสามารถทำได้ทั้งหมดในการกวาดครั้งเดียวดังนั้นไม่จำเป็นต้องบังคับ (โปรดทราบว่าคุณต้องextglobเปิดใช้งานในเชลล์ของคุณเพื่อให้สิ่งนี้ทำงานได้):

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

สิ่งนี้ควรกำจัดการเปลี่ยนแปลงทั้งหมดในไดเร็กทอรีย่อยที่ไม่ต้องการและเก็บสาขาและคอมมิตทั้งหมดของคุณไว้ (เว้นแต่จะมีผลกับไฟล์ในไดเร็กทอรีย่อยที่ถูกตัดออกเท่านั้น--prune-empty) - ไม่มีปัญหากับการคอมมิตซ้ำเป็นต้น

git statusหลังจากการดำเนินการนี้ไดเรกทอรีที่ไม่พึงประสงค์จะได้รับการระบุว่าเป็นที่ไม่ได้ติดตามโดย

$(ls ...)เป็นสิ่งที่จำเป็นเซนต์extglobได้รับการประเมินจากเปลือกของคุณแทนการกรองดัชนีที่ใช้shในตัวeval(ที่extglobไม่สามารถใช้ได้) ดูฉันจะเปิดใช้งานตัวเลือกเชลล์ใน git ได้อย่างไร สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนั้น


1
ความคิดที่น่าสนใจ ฉันมีปัญหาคล้ายกัน แต่ไม่สามารถใช้งานได้โปรดดูstackoverflow.com/questions/8050687/…
manol

นี่เป็นสิ่งที่ฉันต้องการมากแม้ว่าฉันจะโรยทั้งไฟล์และโฟลเดอร์ใน repo ของฉัน ... ขอบคุณ :)
notlesh

1
หืม แม้จะเปิด extglob ฉันก็ได้รับข้อผิดพลาดใกล้วงเล็บ: ข้อผิดพลาดทางไวยากรณ์ใกล้โทเค็นที่ไม่คาดคิด `('คำสั่งของฉันดูเหมือน: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - - all an ls with src / css / themes /! (some_theme *) ส่งคืนธีมอื่น ๆ ทั้งหมดเพื่อให้ extglob ปรากฏ กำลังทำงาน ...
robdodson

2
@MikeGraf ฉันไม่คิดว่าจะให้ผลลัพธ์ที่ต้องการ: การหลบหนีจะตรงกับตัวอักษร "! ฯลฯ ในเส้นทางของคุณ
kynan

1
คำตอบของ @ david-smiley (ล่าสุด) ใช้วิธีการที่คล้ายกันมาก แต่มีข้อได้เปรียบในการพึ่งพาgitคำสั่งเท่านั้นดังนั้นจึงไม่ไวต่อความแตกต่างในการlsตีความข้ามระบบปฏิบัติการตามที่ @Bae ค้นพบ
Jeremy Caney

20

ตอบคำถามของตัวเองที่นี่ ... หลังจากลองผิดลองถูกมาหลายครั้ง

ฉันจัดการได้โดยใช้การรวมกันของgit subtreeและgit-stitch-repo. คำแนะนำเหล่านี้อ้างอิงจาก:

ก่อนอื่นฉันดึงไดเร็กทอรีที่ต้องการเก็บไว้ในที่เก็บแยกต่างหาก:

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

จากนั้นฉันก็สร้างที่เก็บว่างใหม่และนำเข้า / เย็บสองอันสุดท้ายเข้าไป:

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

สิ่งนี้จะสร้างสองสาขาmaster-Aและmaster-Bแต่ละอันถือเนื้อหาของหนึ่งใน repos ที่เย็บไว้ ในการรวมเข้าด้วยกันและทำความสะอาด:

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

ตอนนี้ฉันไม่ค่อยแน่ใจว่าสิ่งนี้เกิดขึ้นได้อย่างไร / เมื่อใด แต่หลังจากครั้งแรกcheckoutและครั้งแรกpullรหัสจะรวมเข้ากับสาขาหลักอย่างน่าอัศจรรย์ (ขอขอบคุณข้อมูลเชิงลึกเกี่ยวกับสิ่งที่เกิดขึ้นที่นี่!)

ทุกอย่างดูเหมือนจะได้ทำงานตามที่คาดไว้ยกเว้นว่าถ้าผมมองผ่านnewRepoประวัติศาสตร์กระทำมีซ้ำกันเมื่อแก้ไขได้รับผลกระทบทั้งในและapps/AAA libs/XXXหากมีวิธีลบรายการที่ซ้ำกันก็คงจะสมบูรณ์แบบ


เครื่องมือที่คุณพบที่นี่ ข้อมูลเชิงลึกเกี่ยวกับ "checkout": "git pull" เหมือนกับ "git fetch && git merge" ส่วน "ดึงข้อมูล" ไม่เป็นอันตรายเนื่องจากคุณกำลัง "ดึงข้อมูลในเครื่อง" ดังนั้นฉันคิดว่าคำสั่ง checkout นี้เหมือนกับ "git merge master-B" ซึ่งค่อนข้างชัดเจนในตัวเองมากขึ้น ดูkernel.org/pub/software/scm/git/docs/git-pull.html
phord

1
น่าเสียดายที่เครื่องมือ git-stitch-repo เสียเนื่องจากการอ้างอิงที่ไม่ดีในปัจจุบัน
Henrik

@Henrik คุณประสบปัญหาอะไรกันแน่? มันใช้ได้สำหรับฉันแม้ว่าฉันจะต้องเพิ่มexport PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"ใน bash config ของฉันเพื่อให้มันสามารถค้นหา Git.pm. จากนั้นฉันติดตั้งด้วย cpan

สามารถใช้git subtree addเพื่อทำงานนี้ได้ ดูstackoverflow.com/a/58253979/1894803
laconbass

10

วิธีง่ายๆ: git-filter-repo

ผมมีปัญหาที่คล้ายกันและหลังจากตรวจสอบวิธีการต่างๆที่ระบุไว้ที่นี่ที่ผมค้นพบGit-กรอง repo มันถูกแนะนำให้เป็นทางเลือกในการคอมไพล์กรองสาขาในเอกสารประกอบคอมไพล์อย่างเป็นทางการที่นี่

ในการสร้างที่เก็บใหม่จากชุดย่อยของไดเร็กทอรีในที่เก็บที่มีอยู่คุณสามารถใช้คำสั่ง:

git filter-repo --path <file_to_remove>

กรองไฟล์ / โฟลเดอร์หลาย ๆ ไฟล์โดยผูกมัด:

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

ดังนั้นในการตอบคำถามเดิมด้วย git-filter-repo คุณจะต้องใช้คำสั่งต่อไปนี้:

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

นี่คือคำตอบที่ดีอย่างแน่นอน ปัญหาเกี่ยวกับวิธีแก้ปัญหาอื่น ๆ ทั้งหมดคือฉันไม่สามารถแยกเนื้อหาของไดเร็กทอรีทุกสาขา อย่างไรก็ตาม git filter-repo ดึงโฟลเดอร์จากทุกสาขาและเขียนประวัติใหม่อย่างสมบูรณ์แบบเช่นทำความสะอาดต้นไม้ทั้งหมดของทุกสิ่งที่ฉันไม่ต้องการ
Teodoro

ฉันใช้คำตอบของคุณ นับเป็นทางออกที่ยอดเยี่ยม จากนั้นก็หายไปภายในแท็บที่เปิดอยู่มากมาย ฉันต้องค้นหาจากประวัติเบราว์เซอร์ของฉันอย่างแท้จริงเพื่อหาคุณและกล่าวขอบคุณ
blueray

7

ฉันเขียนตัวกรองคอมไพล์เพื่อแก้ปัญหานี้อย่างตรงจุด มีชื่อที่ยอดเยี่ยมของ git_filter และตั้งอยู่ที่ github ที่นี่:

https://github.com/slobobaby/git_filter

มันขึ้นอยู่กับ libgit2 ที่ยอดเยี่ยม

ฉันต้องการแยกที่เก็บขนาดใหญ่ที่มีการคอมมิตจำนวนมาก (~ 100000) และการแก้ปัญหาตาม git filter-branch ใช้เวลาหลายวันในการรัน git_filter ใช้เวลาหนึ่งนาทีในการทำสิ่งเดียวกัน


7

ใช้ส่วนขยายคอมไพล์ 'git splits'

git splitsเป็นสคริปต์ทุบตีที่เป็นเสื้อคลุมรอบgit branch-filterที่ฉันสร้างเป็นส่วนขยายของคอมไพล์ขึ้นอยู่กับวิธีการแก้ปัญหาของ jkeating

มันถูกสร้างขึ้นมาเพื่อสถานการณ์นี้ สำหรับข้อผิดพลาดของคุณให้ลองใช้git splits -fตัวเลือกเพื่อบังคับให้ลบข้อมูลสำรอง เนื่องจากgit splitsดำเนินการในสาขาใหม่จึงไม่เขียนสาขาปัจจุบันของคุณใหม่ดังนั้นการสำรองข้อมูลจึงไม่เกี่ยวข้อง ดูไฟล์ README สำหรับรายละเอียดเพิ่มเติมและให้แน่ใจว่าจะใช้ในการคัดลอก / โคลนของ repo ของคุณ (ในกรณี!)

  1. git splitsติดตั้ง
  2. แยกไดเรกทอรีออกเป็นสาขาท้องถิ่น #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. สร้าง repo ว่างที่ใดที่หนึ่ง เราจะถือว่าเราได้สร้าง repo ว่างที่เรียกว่าxyzGitHub ที่มีเส้นทาง:git@github.com:simpliwp/xyz.git

  4. กดไปที่ repo ใหม่ #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. โคลน repo ระยะไกลที่สร้างขึ้นใหม่ลงในไดเร็กทอรีโลคัลใหม่
    #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


ดูเหมือนจะไม่สามารถเพิ่มไฟล์ลงในการแยกและอัปเดตได้ในภายหลังใช่ไหม
Alex

สิ่งนี้ดูเหมือนจะช้าในการทำงานใน repo ของฉันด้วยจำนวนการคอมมิต
Shinta Smith

git-split ดูเหมือนจะใช้git --index filterซึ่งช้ามากเมื่อเทียบกับ --subdirectory-filter สำหรับ repos บางตัวอาจยังคงเป็นตัวเลือกที่ใช้งานได้ แต่สำหรับ repos ขนาดใหญ่ (หลายกิกะไบต์การคอมมิต 6 หลัก) ตัวกรองดัชนีจะใช้เวลาหลายสัปดาห์ในการทำงานอย่างมีประสิทธิภาพแม้ในฮาร์ดแวร์ระบบคลาวด์โดยเฉพาะ
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

การอ่านความคิดเห็นอื่น ๆ ทั้งหมดทำให้ฉันมาถูกทาง อย่างไรก็ตามโซลูชันของคุณก็ใช้งานได้ นำเข้าทุกสาขาและทำงานร่วมกับหลายไดเรกทอรี! เยี่ยมมาก!
jschober

1
การforวนซ้ำเป็นสิ่งที่ควรค่าแก่การยอมรับเนื่องจากคำตอบอื่น ๆ ที่คล้ายกันจะไม่รวมไว้ หากคุณไม่มีสำเนาท้องถิ่นของแต่ละสาขาในโคลนของคุณfilter-branchจะไม่นำมาพิจารณาเป็นส่วนหนึ่งของการเขียนซ้ำซึ่งอาจยกเว้นไฟล์ที่นำมาใช้ในสาขาอื่น แต่ยังไม่รวมกับสาขาปัจจุบันของคุณ (แม้ว่าจะคุ้มค่ากับการทำgit fetchในสาขาใด ๆ ที่คุณเคยตรวจสอบก่อนหน้านี้เพื่อให้แน่ใจว่ายังคงเป็นปัจจุบัน)
Jeremy Caney

3

ใช่. บังคับให้เขียนทับข้อมูลสำรองโดยใช้-fแฟล็กในการเรียกครั้งต่อ ๆ ไปfilter-branchเพื่อแทนที่คำเตือนนั้น :) มิฉะนั้นฉันคิดว่าคุณมีวิธีแก้ปัญหา (นั่นคือลบไดเร็กทอรีที่ไม่ต้องการในแต่ละครั้งด้วยfilter-branch)


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.