Trennen Sie viele Unterverzeichnisse in einem neuen, separaten Git-Repository

135

Diese Frage basiert auf dem Unterverzeichnis Detach in ein separates Git-Repository

Anstatt ein einzelnes Unterverzeichnis zu trennen, möchte ich ein Paar trennen. Zum Beispiel sieht mein aktueller Verzeichnisbaum folgendermaßen aus:

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

Und ich möchte stattdessen Folgendes:

/apps
  /AAA
/libs
  /XXX

Das --subdirectory-filterArgument, git filter-branchnicht zu funktionieren, weil es beim ersten Ausführen alles außer dem angegebenen Verzeichnis entfernt. Ich dachte, die Verwendung des --index-filterArguments für alle unerwünschten Dateien würde funktionieren (wenn auch mühsam), aber wenn ich versuche, es mehrmals auszuführen, wird die folgende Meldung angezeigt:

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

Irgendwelche Ideen? TIA

Gefangener John
quelle

Antworten:

155

Versuchen Sie diesen viel einfacheren Ansatz, anstatt sich mit einer Subshell befassen und ext glob verwenden zu müssen (wie von kynan vorgeschlagen):

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

Wie von void.pointer in seinem Kommentar erwähnt , wird dadurch alles außer apps/AAAund libs/XXXaus dem aktuellen Repository entfernt.

Leere Zusammenführungs-Commits beschneiden

Dies hinterlässt viele leere Zusammenführungen. Diese können durch einen anderen Durchgang entfernt werden, wie von Raphinesse in seiner Antwort beschrieben :

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

⚠️ Achtung : Die oben Muss Verwendung GNU - Version sedund xargssonst wäre es alle Commits entfernen , wie xargsfehlschlägt. brew install gnu-sed findutilsund dann benutze gsedund gxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 
David Smiley
quelle
4
Außerdem sollte das Flag --ignore-unmatch an git rm übergeben werden. Andernfalls ist es beim allerersten Commit für mich fehlgeschlagen (das Repository wurde in meinem Fall mit git svn clone erstellt)
Pontomedon
8
Angenommen, Sie haben Tags in der Mischung, sollten Sie wahrscheinlich --tag-name-filter catzu Ihren Parametern hinzufügen
Yonatan
16
Könnten Sie weitere Informationen hinzufügen, die erklären, was dieser lange Befehl tut?
Burhan Ali
4
Ich bin angenehm überrascht, dass dies unter Windows mit Git Bash perfekt funktioniert, Puh!
Dai
3
@BurhanAli Bei jedem Commit im Verlauf werden alle Dateien außer den Dateien gelöscht, die Sie behalten möchten. Wenn alles erledigt ist, bleibt nur der Teil des Baums übrig, den Sie angegeben haben, zusammen mit nur diesem Verlauf.
void.pointer
39

Manuelle Schritte mit einfachen Git-Befehlen

Es ist geplant, einzelne Verzeichnisse in eigene Repos aufzuteilen und diese dann zusammenzuführen. In den folgenden manuellen Schritten wurden keine Geek-to-Use-Skripte verwendet, sondern leicht verständliche Befehle. Sie können dazu beitragen, zusätzliche N Unterordner in einem anderen Repository zusammenzuführen.

Teilen

Nehmen wir an, Ihr ursprüngliches Repo lautet: original_repo

1 - Geteilte Apps:

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

2 - Geteilte Bibliotheken

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

Fahren Sie fort, wenn Sie mehr als 2 Ordner haben. Jetzt haben Sie zwei neue und temporäre Git-Repository.

Erobern Sie, indem Sie Apps und Bibliotheken zusammenführen

3 - Bereiten Sie das brandneue Repo vor:

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

Und Sie müssen mindestens ein Commit durchführen. Wenn die folgenden drei Zeilen übersprungen werden sollen, wird Ihr erstes Repo unmittelbar unter dem Stammverzeichnis Ihres Repos angezeigt:

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"

Wenn die temporäre Datei festgeschrieben ist, wird der mergeBefehl im späteren Abschnitt wie erwartet gestoppt.

Unter von Benutzer-Feedback, stattdessen eine zufällige Datei hinzuzufügen , wie a_file_and_make_a_commitkönnen Sie wählen , eine hinzuzufügen .gitignore, oder README.mdusw.

4 - Apps zuerst zusammenführen 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"

Jetzt sollte das Apps- Verzeichnis in Ihrem neuen Repository angezeigt werden. git logsollte alle relevanten historischen Commit-Nachrichten anzeigen.

Hinweis: wie Chris unten in den Kommentaren erwähnt, für neuere Version (> = 2.9) von git, müssen Sie angeben , --allow-unrelated-historiesmitgit merge

5 - Merge libs repo als nächstes auf die gleiche Weise zusammenführen:

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"

Fahren Sie fort, wenn Sie mehr als 2 Repos zusammenführen möchten.

Referenz: Führen Sie ein Unterverzeichnis eines anderen Repositorys mit git zusammen

chfw
quelle
4
Seit Git 2.9 müssen Sie für die Zusammenführungsbefehle --allow-non-related-histories verwenden. Ansonsten scheint das für mich gut zu funktionieren.
Chris
1
Genius! Vielen Dank dafür. Bei den ersten Antworten, die ich mir mit einem Baumfilter in einem sehr großen Repository angesehen hatte, hatte Git vorausgesagt, dass es mehr als 26 Stunden dauern würde, bis die Git-Umschreibungen abgeschlossen waren. Viel glücklicher mit diesem einfachen, aber wiederholbaren Ansatz und haben erfolgreich 4 Unterordner in ein neues Repo mit dem gesamten erwarteten Commit-Verlauf verschoben.
Shuttsy
1
Sie können das erste Commit für ein "Initial Commit" verwenden, das Adds .gitignoreund README.mdDateien hinzufügt .
Jack Miller
2
Leider scheint dieser Ansatz den Tracking-Verlauf für die im git merge .. git read-treeSchritt hinzugefügten Dateien zu unterbrechen, da er sie als neu hinzugefügte Dateien aufzeichnet und alle meine Git-Guis keine Verbindung zu ihren früheren Commits herstellen.
Dai
1
@ksadjad, keine Ahnung, um ehrlich zu sein. Der zentrale Punkt der manuellen Zusammenführung besteht darin, die Verzeichnisse auszuwählen, um das neue Repo zu bilden, und ihre Commit-Historien beizubehalten. Ich bin nicht sicher, wie ich mit einer solchen Situation umgehen soll, in der ein Commit Dateien in dirA, dirB, dirDrop und nur dirA und dirB für das neue Repo auswählt. Wie sollte sich der Commit-Verlauf auf den ursprünglichen beziehen?
chfw
27

Warum sollten Sie filter-branchmehr als einmal laufen wollen ? Sie können alles in einem Durchgang ausführen, sodass Sie es nicht erzwingen müssen (beachten Sie, dass Sie extglobes in Ihrer Shell aktivieren müssen, damit dies funktioniert):

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

Dies sollte alle Änderungen in den unerwünschten Unterverzeichnissen beseitigen und alle Ihre Zweige und Commits beibehalten (es sei denn, sie wirken sich nur auf Dateien in den beschnittenen Unterverzeichnissen aus --prune-empty) - kein Problem mit doppelten Commits usw.

Nach diesem Vorgang werden die unerwünschten Verzeichnisse als nicht verfolgt von aufgelistet git status.

Das $(ls ...)ist notwendig extglob, wenn es von Ihrer Shell anstelle des shIndexfilters ausgewertet wird, der das eingebaute verwendet eval(wo extglobnicht verfügbar). Siehe Wie aktiviere ich Shell-Optionen in Git? für weitere Details dazu.

kynan
quelle
1
Interessante Idee. Ich habe ein ähnliches Problem, konnte es aber nicht zum Laufen
bringen
Dies ist so ziemlich das, was ich brauchte, obwohl ich sowohl Dateien als auch Ordner über mein Repo
gestreut hatte
1
Hm. Selbst wenn extglob aktiviert ist, wird in der Nähe meiner Klammer ein Fehler angezeigt: Syntaxfehler in der Nähe eines unerwarteten Tokens `('Mein Befehl sieht aus wie folgt: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - --all an ls mit src / css / theme /! (some_theme *) gibt alle anderen Themen zurück, so dass extglob erscheint arbeiten ...
Robdodson
2
@ MikeGraf Ich glaube nicht, dass dies das gewünschte Ergebnis liefert: Flucht würde einem wörtlichen "!" usw. auf deinem Weg.
Kynan
1
Die (neuere) Antwort von @ david-smiley verwendet einen sehr ähnlichen Ansatz, hat jedoch den Vorteil, dass sie sich ausschließlich auf gitBefehle stützt , und ist daher nicht so anfällig für Unterschiede in der lsInterpretation der Betriebssysteme, wie @Bae herausgefunden hat.
Jeremy Caney
20

Beantwortung meiner eigenen Frage hier ... nach viel Versuch und Irrtum.

Ich habe es geschafft, dies mit einer Kombination aus git subtreeund zu tun git-stitch-repo. Diese Anweisungen basieren auf:

Zuerst zog ich die Verzeichnisse heraus, die ich in ihrem eigenen separaten Repository aufbewahren wollte:

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

Ich habe dann ein neues leeres Repository erstellt und die letzten beiden importiert / zusammengefügt:

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

Dies schafft zwei Zweige, master-Aund master-B, die jeweils den Inhalt eines des genähten repos halten. Um sie zu kombinieren und aufzuräumen:

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

Jetzt bin ich mir nicht ganz sicher, wie / wann dies geschieht, aber nach dem ersten checkoutund dem pullgeht der Code auf magische Weise in den Hauptzweig über (jeder Einblick in das, was hier vor sich geht, ist willkommen!).

Alles scheint wie erwartet gearbeitet zu haben, mit der Ausnahme , dass , wenn ich den Blick durch die newRepoGeschichte begehen, es Duplikate sind , wenn die changeset beide betroffen apps/AAAund libs/XXX. Wenn es eine Möglichkeit gibt, Duplikate zu entfernen, ist dies perfekt.

Gefangener John
quelle
Ordentliche Werkzeuge, die Sie hier gefunden haben. Einblick in "checkout": "git pull" ist dasselbe wie "git fetch && git merge". Der Teil "Abrufen" ist harmlos, da Sie "lokal abrufen". Ich denke, dieser Checkout-Befehl ist der gleiche wie "git merge master-B", was etwas offensichtlicher ist. Siehe kernel.org/pub/software/scm/git/docs/git-pull.html
Phord
1
Leider ist das Git-Stitch-Repo-Tool heutzutage aufgrund schlechter Abhängigkeiten defekt.
Henrik
@ Henrik Welches Problem hatten Sie genau? Es funktioniert für mich, obwohl ich export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"meine Bash-Konfiguration hinzufügen musste , damit es Git.pm finden konnte. Dann habe ich es mit cpan installiert.
Es ist möglich git subtree add, diese Aufgabe auszuführen. Siehe stackoverflow.com/a/58253979/1894803
Laconbass
7

Ich habe einen Git-Filter geschrieben, um genau dieses Problem zu lösen. Es hat den fantastischen Namen git_filter und befindet sich hier bei github:

https://github.com/slobobaby/git_filter

Es basiert auf dem exzellenten libgit2.

Ich musste ein großes Repository mit vielen Commits (~ 100000) aufteilen, und die Ausführung der auf git filter-branch basierenden Lösungen dauerte mehrere Tage. git_filter benötigt eine Minute, um dasselbe zu tun.

Slobobaby
quelle
7

Verwenden Sie die Git-Erweiterung "Git Splits"

git splitsist ein Bash-Skript, das ein Wrapper ist git branch-filter, den ich als Git-Erweiterung erstellt habe, basierend auf der Lösung von jkeating .

Es wurde genau für diese Situation gemacht. Verwenden Sie für Ihren Fehler die git splits -fOption, um das Entfernen der Sicherung zu erzwingen. Da git splitsin einem neuen Zweig gearbeitet wird, wird Ihr aktueller Zweig nicht neu geschrieben, sodass die Sicherung irrelevant ist. Weitere Informationen finden Sie in der Readme-Datei. Verwenden Sie sie auf einer Kopie / einem Klon Ihres Repos (nur für den Fall!) .

  1. installieren git splits.
  2. Teilen Sie die Verzeichnisse in einen lokalen Zweig auf #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. Erstellen Sie irgendwo ein leeres Repo. Wir gehen davon aus, dass wir xyzauf GitHub ein leeres Repo mit dem folgenden Pfad erstellt haben:[email protected]:simpliwp/xyz.git

  4. Zum neuen Repo drücken. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz [email protected]:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Klonen Sie das neu erstellte Remote-Repo in ein neues lokales Verzeichnis
    #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 [email protected]:simpliwp/xyz.git

AndrewD
quelle
Es scheint nicht möglich zu sein, Dateien zum Split hinzuzufügen und später zu aktualisieren, oder?
Alex
Dies scheint auf meinem Repo mit Tonnen von Commits zu verlangsamen
Shinta Smith
git-split scheint git --index filter zu verwenden, was im Vergleich zu --subdirectory-filter extrem langsam ist. Für einige Repos ist dies möglicherweise immer noch eine praktikable Option, aber für große Repos (mehrere Gigabyte, 6-stellige Commits) dauert die Ausführung des Indexfilters selbst auf dedizierter Cloud-Hardware Wochen.
Jostein Kjønigsen
6
git clone [email protected]: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 [email protected]:newthing.git
git push --all
Richard Barraclough
quelle
Das Lesen aller anderen Kommentare brachte mich auf den richtigen Weg. Ihre Lösung funktioniert jedoch einfach. Es importiert alle Zweige und arbeitet mit mehreren Verzeichnissen! Toll!
24.
1
Die forSchleife ist eine Bestätigung wert, da andere ähnliche Antworten sie nicht enthalten. Wenn Sie nicht über eine lokale Kopie jedes Zweigs in Ihrem Klon verfügen, filter-branchwerden diese beim Umschreiben nicht berücksichtigt. Dies kann möglicherweise Dateien ausschließen, die in anderen Zweigen eingeführt, aber noch nicht mit Ihrem aktuellen Zweig zusammengeführt wurden. (Obwohl es sich auch lohnt, eine git fetchFiliale zu machen, die Sie zuvor ausgecheckt haben, um sicherzustellen, dass sie aktuell bleibt.)
Jeremy Caney
5

Eine einfache Lösung: Git-Filter-Repo

Ich hatte ein ähnliches Problem und nachdem ich die verschiedenen hier aufgeführten Ansätze überprüft hatte, entdeckte ich Git-Filter-Repo . Es wird als Alternative zum Git-Filter-Zweig in der offiziellen Git-Dokumentation hier empfohlen .

Verwenden Sie den folgenden Befehl, um ein neues Repository aus einer Teilmenge von Verzeichnissen in einem vorhandenen Repository zu erstellen:

git filter-repo --path <file_to_remove>

Filtern Sie mehrere Dateien / Ordner, indem Sie sie verketten:

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

Um die ursprüngliche Frage zu beantworten , benötigen Sie mit git-filter-repo nur den folgenden Befehl:

git filter-repo --path apps/AAA/ --path libs/XXX/
Elmo
quelle
Dies ist definitiv eine gute Antwort. Das Problem bei allen anderen Lösungen ist, dass ich den Inhalt ALLER Zweige eines Verzeichnisses nicht extrahieren konnte. Git filter-repo hat jedoch den Ordner aus allen Zweigen abgerufen und den Verlauf perfekt umgeschrieben, so als würde der gesamte Baum von allem gereinigt, was ich nicht brauchte.
Teodoro
3

Ja. Erzwingen Sie das Überschreiben der Sicherung, indem Sie bei -fnachfolgenden Aufrufen das Flag verwenden filter-branch, um diese Warnung zu überschreiben. :) Ansonsten denke ich, dass Sie die Lösung haben (dh ein unerwünschtes Verzeichnis gleichzeitig mit löschen filter-branch).

Jakob Borg
quelle
-4

Löschen Sie die Sicherung im Verzeichnis .git in refs / original, wie in der Nachricht vorgeschlagen. Das Verzeichnis ist ausgeblendet.

user5200576
quelle