1983 Stimmen

Subverzeichnis in eigenes Git-Repository auslagern

Ich habe ein Git-Repository, das eine Reihe von Unterverzeichnissen enthält. Jetzt habe ich festgestellt, dass eines der Unterverzeichnisse nicht mit den anderen zusammenhängt und in ein separates Repository abgetrennt werden sollte.

Wie kann ich das tun, während ich die Historie der Dateien innerhalb des Unterverzeichnisses beibehalte?

Ich könnte vermutlich einen Klon erstellen und die unerwünschten Teile jedes Klon entfernen, aber ich nehme an, dass dies mir den vollständigen Baum liefern würde, wenn ich eine ältere Revision herausnehme usw. Dies mag akzeptabel sein, aber ich möchte lieber so tun, als ob die beiden Repositories keine gemeinsame Historie haben.

Nur zur Klarstellung, ich habe folgende Struktur:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

Aber ich möchte stattdessen das hier:

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

10 Stimmen

Das ist jetzt trivial mit git filter-branch siehe meine Antwort unten.

16 Stimmen

@jeremyjjbrown hat recht. Es ist zwar nicht mehr schwierig dies zu tun, aber es ist schwierig, die richtige Antwort auf Google zu finden, da die alten Antworten die Ergebnisse dominieren.

4 Stimmen

Die Verwendung von git filter-branch wird abgeraten. Siehe Warnung in der Dokumentation.

1603voto

coolaj86 Punkte 69032

Der einfache Weg™

Es stellt sich heraus, dass dies eine so häufige und nützliche Praxis ist, dass es die Oberherren von Git wirklich einfach gemacht haben (hinzugefügt in Version 1.7.11 - Mai 2012). Außerdem gibt es ein praktisches Beispiel in der Anleitung unten.

  1. Bereiten Sie das alte Repository vor

     cd 
     git subtree split -P  -b 

    Hinweis: darf keine führenden oder abschließenden Zeichen enthalten. Beispielsweise muss der Ordner mit dem Namen subproject als subproject übergeben werden, NICHT als ./subproject/

    Hinweis: ist ein Branch, den Sie im bestehenden/alten Repo erstellen, NICHT den neuen [der später kommt].

    Hinweis für Windows-Benutzer: Wenn Ihre Ordnerstruktur größer als 1 ist, muss den *nix-Stil-Ordnerseparator (/) enthalten. Beispielsweise muss der Ordner mit dem Namen path1\path2\subproject als path1/path2/subproject übergeben werden.

  2. Erstellen Sie das neue Repository

     mkdir ~/ && cd ~/
     git init
     git pull  
  3. Verknüpfen Sie das neue Repository mit GitHub oder wo auch immer

     git remote add origin 
     git push -u origin master
  4. Bereinigen Sie im falls gewünscht

     git rm -rf 

    Hinweis: Dies hinterlässt alle historischen Verweise im Repository. Siehe den Anhang unten, wenn Sie tatsächlich besorgt sind, dass ein Passwort committed wurde oder wenn Sie die Dateigröße Ihres .git-Ordners verringern müssen.


Beispiel

Das sind die gleichen Schritte wie oben, aber nach meinen genauen Schritten für mein Repository anstatt zu verwenden.

Hier ist ein Projekt, das ich für die Implementierung von JavaScript-Browsermodulen in Node habe:

tree ~/node-browser-compat

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

Ich möchte einen einzelnen Ordner, btoa, in ein separates Git-Repository auslagern

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

Jetzt habe ich einen neuen Branch, btoa-only, der nur Commits für btoa hat, und ich möchte ein neues Repository erstellen.

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

Dann erstelle ich ein neues Repo auf GitHub oder Bitbucket oder wo auch immer und füge es als origin hinzu

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

Schöner Tag!

Hinweis: Wenn Sie ein Repo mit einem README.md, .gitignore und LICENSE erstellt haben, müssen Sie zuerst pullen:

git pull origin master
git push origin master

Zuletzt möchte ich den Ordner aus dem größeren Repo entfernen

git rm -rf btoa

Bereinigung Ihres Verlaufs

Standardmäßig werden Dateien aus Git entfernen, entfernt sie tatsächlich nicht; es bestätigt nur, dass sie nicht mehr da sind. Wenn Sie die historischen Verweise tatsächlich entfernen möchten (d. h. Sie haben ein Passwort committed), müssen Sie dies tun:

git filter-branch --prune-empty --tree-filter 'rm -rf ' HEAD

Danach können Sie überprüfen, dass Ihre Datei oder Ihr Ordner gar nicht mehr im Git-Verlauf auftaucht:

git log --  # sollte nichts anzeigen

Allerdings können Sie Löschungen nicht "pushen" zu GitHub und ähnlichen. Wenn Sie es versuchen, erhalten Sie einen Fehler und müssen git pull ausführen, bevor Sie git push können - und dann haben Sie wieder alles in Ihrem Verlauf.

Wenn Sie also den Verlauf vom "Ursprung" löschen wollen - also um ihn von GitHub, Bitbucket usw. zu löschen - müssen Sie das Repo löschen und eine bereinigte Kopie des Repos erneut pushen. Aber warten Sie - es gibt mehr! - wenn Sie wirklich besorgt sind, ein Passwort oder ähnliches loszuwerden, müssen Sie das Backup bereinigen (siehe unten).

Die Größe von .git reduzieren

Der zuvor genannte Befehl zum Löschen des Verlaufs hinterlässt immer eine Menge Backup-Dateien - weil Git allzu gerne dabei hilft, Ihr Repo nicht aus Versehen zu ruinieren. Es wird im Laufe der Tage und Monate verwaiste Dateien löschen, aber es lässt sie eine Weile dort, falls Sie merken, dass Sie etwas versehentlich gelöscht haben, was Sie nicht wollten.

Wenn Sie also wirklich den Papierkorb leeren wollen, um die Klonegröße eines Repos sofort zu reduzieren, müssen Sie all diese wirklich seltsamen Dinge tun:

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

Trotzdem würde ich empfehlen, diese Schritte nur dann auszuführen, wenn Sie wissen, dass Sie es müssen - nur für den Fall, dass Sie das falsche Unterverzeichnis bereinigt haben, wissen Sie? Die Backup-Dateien sollten nicht geklont werden, wenn Sie das Repo pushen; sie werden einfach in Ihrer lokalen Kopie sein.

Quelle

17 Stimmen

git subtree ist immer noch Teil des 'contrib'-Ordners und wird nicht standardmäßig auf allen Distributionen installiert. github.com/git/git/blob/master/contrib/subtree

1 Stimmen

...insbesondere nicht auf Ubuntu, auch nicht bei Verwendung von ppa:git-core/ppa :-(

11 Stimmen

@krlmlr 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 Um in Ubuntu 13.04 zu aktivieren

1273voto

Paul Punkte 16115

Aktualisierung: Dieser Prozess ist so häufig, dass das Git-Team es mit einem neuen Tool, git subtree, viel einfacher gemacht hat. Siehe hier: Teilverzeichnis (verschieben) in separates Git-Repository abtrennen


Sie möchten Ihr Repository klonen und dann git filter-branch verwenden, um alles außer dem Teilverzeichnis, das Sie in Ihrem neuen Repo behalten möchten, als Müll zu markieren.

  1. Um Ihr lokales Repository zu klonen:

    git clone /XYZ /ABC

    (Hinweis: Das Repository wird unter Verwendung von Hardlinks geklont, aber das ist kein Problem, da die hardverlinkten Dateien nicht in sich selbst geändert werden - es werden neue erstellt.)

  2. Lassen Sie uns nun die interessanten Branches beibehalten, die wir auch neu schreiben möchten, und entfernen dann den Ursprung, um das Pushen dorthin zu vermeiden und sicherzustellen, dass alte Commits nicht vom Ursprung referenziert werden:

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

    oder für alle Remote-Branches:

    cd /ABC
    for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done
    git remote rm origin
  3. Es könnte sein, dass Sie auch Tags entfernen möchten, die keine Beziehung zum Teilprojekt haben; Sie können dies auch später tun, aber möglicherweise müssen Sie Ihr Repo erneut beschneiden. Ich habe das nicht getan und für alle Tags eine WARNUNG: Ref 'refs/tags/v0.1' ist unverändert erhalten (da sie alle nichts mit dem Teilprojekt zu tun hatten); darüber hinaus wird nach dem Entfernen solcher Tags mehr Speicherplatz zurückgewonnen. Offensichtlich sollte git filter-branch in der Lage sein, andere Tags neu zu schreiben, aber ich konnte dies nicht verifizieren. Wenn Sie alle Tags entfernen möchten, verwenden Sie git tag -l | xargs git tag -d.

  4. Verwenden Sie dann filter-branch und reset, um die anderen Dateien auszuschließen, damit sie beschnitten werden können. Fügen Sie außerdem --tag-name-filter cat --prune-empty hinzu, um leere Commits zu entfernen und Tags neu zu schreiben (beachten Sie, dass dies ihre Signatur ablegen wird):

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

    oder alternativ, um nur den HEAD-Branch neu zu schreiben und Tags und andere Branches zu ignorieren:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
  5. Löschen Sie dann die Backup-Reflogs, damit der Speicherplatz tatsächlich zurückgewonnen werden kann (obwohl die Operation jetzt zerstörerisch ist)

    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

    und jetzt haben Sie ein lokales Git-Repository des ABC-Teilverzeichnisses, das seine gesamte Historie bewahrt hat.

Hinweis: Für die meisten Anwendungen sollte git filter-branch tatsächlich den zusätzlichen Parameter -- --all haben. Ja, das ist wirklich --Leerzeichen-- all. Dies müssen die letzten Parameter für den Befehl sein. Wie Matli entdeckt hat, behält dies die Projekt-Branches und Tags im neuen Repo bei.

Bearbeitung: Verschiedene Vorschläge aus den Kommentaren unten wurden aufgenommen, um sicherzustellen, dass das Repository tatsächlich verkleinert wird (was früher nicht immer der Fall war).

1 Stimmen

Guter Punkt! Das würde für die meisten Personen gelten, die git filter-branch auf diese Weise verwenden. In meinem Fall habe ich eine Bibliothek erstellt, die ihre eigenen Reihe von Versionsnummern in Tags hatte, sodass ich die Projekt-Tags nicht in meinem neuen Repository haben wollte. Das werde ich in die Antwort aufnehmen.

1 Stimmen

Bis heute wird filter-branch nicht auf Windows unterstützt. Es sieht jedoch so aus, als ob es bald unterstützt wird. Überprüfen Sie die msysgit-Diskussionsgruppe (bei Google Groups) für Details.

13 Stimmen

Warum benötigen Sie --no-hardlinks? Das Entfernen eines Hardlinks beeinflusst die andere Datei nicht. Git-Objekte sind auch unveränderlich. Nur wenn Sie Besitzer-/Dateiberechtigungen ändern würden, benötigen Sie --no-hardlinks.

140voto

pgs Punkte 12237

Pauls Antwort erstellt ein neues Repository, das /ABC enthält, entfernt jedoch nicht /ABC aus /XYZ. Der folgende Befehl entfernt /ABC aus /XYZ:

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

Natürlich sollten Sie es zuerst in einem 'clone --no-hardlinks' Repository testen und den Befehl mit dem Reset, gc und prune Befehlen ausführen, die Paul auflistet.

54 Stimmen

Mach das git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEAD und es wird viel schneller sein. Der index-filter arbeitet am Index, während der tree-filter für jeden Commit alles auschecken und stagen muss.

52 Stimmen

In einigen Fällen ist es übertrieben, die Historie des Repositorys XYZ durcheinander zu bringen ... nur ein einfaches "rm -rf ABC; git rm -r ABC; git commit -m'ABC in eigenes Repo extrahiert'" würde für die meisten Menschen besser funktionieren.

2 Stimmen

Sie möchten wahrscheinlich -f (force) verwenden, um diesen Befehl auszuführen, wenn Sie dies mehr als einmal tun, z.B. um zwei Verzeichnisse zu entfernen, nachdem sie getrennt wurden. Andernfalls erhalten Sie die Meldung "Kann kein neues Backup erstellen."

98voto

Josh Lee Punkte 159535

Ich habe festgestellt, dass man, um die alte History richtig aus dem neuen Repository zu löschen, nach dem Schritt filter-branch noch ein wenig mehr Arbeit leisten muss.

  1. Führen Sie den Clone und den Filter durch:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
  2. Entfernen Sie jede Referenz auf die alte History. "origin" behielt den Überblick über Ihren Clone, und "original" ist der Ort, an dem filter-branch die alten Sachen speichert:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
  3. Selbst jetzt könnte Ihre History in einer Packdatei feststecken, die fsck nicht berührt. Zerreißen Sie sie in Stücke, erstellen Sie eine neue Packdatei und löschen Sie die nicht verwendeten Objekte:

    git repack -ad

Es gibt eine Erklärung dazu im Handbuch für filter-branch.

3 Stimmen

Ich glaube, etwas wie git gc --aggressive --prune=now fehlt immer noch, oder?

1 Stimmen

@Albert Der Befehl "repack" kümmert sich darum und es gibt keine losen Objekte.

0 Stimmen

Ja, git gc --aggressive --prune=now hat einen Großteil des neuen Repositories reduziert.

56voto

lpearson Punkte 411

Wenn git filter-branch mit einer neueren Version von git (2.22+ vielleicht?) ausgeführt wird, wird empfohlen dieses neue Tool git-filter-repo zu verwenden. Dieses Tool hat mir die Dinge sicherlich vereinfacht.

Filtern mit filter-repo

Befehle zum Erstellen des XYZ Repos aus der Originalfrage:

# Erstelle lokal einen Klon des Original-Repo im Verzeichnis XYZ
tmp $ git clone git@github.com:user/original.git XYZ

# Wechseln zur Arbeit in XYZ
tmp $ cd XYZ

# Behalte Unterverzeichnisse XY1 und XY2 (lasse ABC fallen)
XYZ $ git filter-repo --path XY1 --path XY2

# Hinweis: Das Original-Remote "origin" wurde entfernt
# (Schutz vor versehentlichen Pushs, die Daten des Original-Repo überschreiben)

# XYZ $ ls -1
# XY1
# XY2

# XYZ $ git log --oneline
# letzter Commit, der ./XY1 oder ./XY2 modifiziert hat
# erster Commit, der ./XY1 oder ./XY2 modifiziert hat

# Auf neues gehostetes, dediziertes Repo zeigen
XYZ $ git remote add origin git@github.com:user/XYZ.git

# Remote master pushen (und verfolgen)
XYZ $ git push -u origin master

Annahmen: * Remote XYZ Repo war neu und leer vor dem Push

Filtern und Verschieben

In meinem Fall wollte ich auch ein paar Verzeichnisse für eine konsistentere Struktur verschieben. Zuerst habe ich diesen einfachen filter-repo Befehl ausgeführt, gefolgt von git mv dir-to-rename, aber ich fand heraus, dass ich mit der --path-rename Option eine etwas "bessere" Historie bekommen konnte. Anstelle von "zuletzt geändert vor 5 Stunden" auf verschobenen Dateien im neuen Repo sehe ich jetzt "letztes Jahr" (in der GitHub-Benutzeroberfläche), was den Änderungszeiten im Original-Repo entspricht.

Anstelle von...

git filter-repo --path XY1 --path XY2 --path inconsistent
git mv inconsistent XY3  # aktualisiert die letzte Änderungszeit

Habe ich letztendlich ausgeführt...

git filter-repo --path XY1 --path XY2 --path inconsistent --path-rename inconsistent:XY3

Notizen:

  • Ich fand den Git Rev News Blogbeitrag gut erklärend, warum ein weiteres Repo-Filter-Tool erstellt wurde.

  • Ursprünglich habe ich versucht, einen Unterordner mit dem Namen des Ziel-Repos im Original-Repo zu erstellen und dann zu filtern (mithilfe von git filter-repo --subdirectory-filter dir-matching-new-repo-name). Dieser Befehl hat diesen Unterordner korrekt in das Stammverzeichnis des kopierten lokalen Repositoriums konvertiert, führte aber auch zu einer Historie von nur den drei Commits, die für die Erstellung des Unterordners erforderlich waren. (Ich hatte nicht realisiert, dass --path mehrmals angegeben werden kann, wodurch die Notwendigkeit entfällt, einen Unterordner im Quell-Repo zu erstellen.) Da jemand bis zu dem Zeitpunkt, an dem ich bemerkte, dass ich die Historie nicht mitgenommen hatte, in das Quell-Repo committet hatte, habe ich einfach nach dem clone-Befehl git reset commit-before-subdir-move --hard verwendet und dem filter-repo-Befehl --force hinzugefügt, um ihn auf den leicht modifizierten lokalen Klon anzuwenden.

    git clone ... git reset HEAD~7 --hard # vor dem Fehler zurückrollen git filter-repo ... --force # filter-repo mit erwarteten Änderungen ausführen

  • Ich war bei der Installation ratlos, da ich das Erweiterungsmuster mit git nicht kannte, aber letztendlich habe ich git-filter-repo geklont und mit $(git --exec-path) verlinkt:

    ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)

4 Stimmen

Hochgewählt für die Empfehlung des neuen filter-repo Tools (das ich letzten Monat auf stackoverflow.com/a/58251653/6309 vorgestellt habe)

6 Stimmen

Verwenden von git-filter-repo sollte definitiv der bevorzugte Ansatz sein. Es ist viel, viel schneller und sicherer als git-filter-branch und schützt vor vielen der Fallstricke, in die man geraten kann, wenn man die Git-Geschichte neu schreibt. Hoffentlich bekommt diese Antwort mehr Aufmerksamkeit, da sie git-filter-repo behandelt.

0 Stimmen

Eigentlich versuche ich derzeit, Dinge mit git filter-repo zum Laufen zu bringen, aber leider fehlen mir nach dem Ausführen Dateien, die in einem Commit hinzugefügt wurden, und die einen Pfad enthalten, der von filter-repo entfernt wurde. Zum Beispiel: Foo/ Foo.cs Bar/ Bar.cs Alle wurden im selben Commit hinzugefügt. Ich möchte Foo und Bar in separate Repos verschieben. Also habe ich mein Repo in einem Ordner geklont, der dem Namen des neuen Repos entspricht, und habe git filter-repo -path Foo ausgeführt. Auch Foo wird entfernt. Ich spreche von einem viel größeren Repo und es funktioniert für jede andere Datei, aber nicht, wenn es eine Konstellation wie diese ist.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X