Unisci due repository Git senza interrompere la cronologia dei file


226

Devo unire due repository Git in un nuovo repository nuovo di zecca. Ho trovato molte descrizioni di come eseguire questa operazione utilizzando un'unione di sottostruttura (ad esempio la risposta di Jakub Narębski su Come unire due repository Git? ) E seguire queste istruzioni funziona principalmente, tranne che quando commetto la sottostruttura unisci tutti i file dai vecchi repository vengono registrati come nuovi file aggiunti. Riesco a vedere la cronologia dei commit dai vecchi repository quando lo faccio git log, ma se lo faccio git log <file>mostra solo un commit per quel file - l'unione sottostruttura. A giudicare dai commenti sulla risposta sopra, non sono il solo a vedere questo problema, ma non ho trovato soluzioni pubblicate per questo.

Esiste un modo per unire i repository e lasciare intatta la cronologia dei singoli file?


Non sto usando Git, ma in Mercurial farei prima una conversione, se necessario, per correggere i percorsi dei file dei repository da unire, quindi forzare il pull di un repository nella destinazione per ottenere i changeset, e quindi fare un unione dei diversi rami. Questo è testato e funziona;) Forse questo aiuta a trovare una soluzione anche per Git ... rispetto all'approccio sub -ree-merge Immagino che il passaggio di conversione sia diverso in cui la storia viene riscritta invece di mappare semplicemente un percorso (se capisco correttamente). Ciò garantisce quindi un'unione uniforme senza alcuna gestione speciale dei percorsi dei file.
Lucero,

Ho trovato anche questa domanda utile stackoverflow.com/questions/1683531/...
nacross

Ho creato una domanda di follow-up. Potrebbe essere interessante: unisci due repository Git e mantieni la storia principale: stackoverflow.com/questions/42161910/…
Dimitri Dewaele

La soluzione automatizzata che ha funzionato per me è stato stackoverflow.com/a/30781527/239408
xverges

Risposte:


269

Si scopre che la risposta è molto più semplice se si sta semplicemente cercando di incollare due repository insieme e far sembrare che fosse sempre così anziché gestire una dipendenza esterna. Devi semplicemente aggiungere telecomandi ai tuoi vecchi repository, unirli al tuo nuovo master, spostare i file e le cartelle in una sottodirectory, eseguire il trasferimento e ripetere per tutti i repository aggiuntivi. I sottomoduli, le fusioni di sottostrutture e le fantasiose rebase hanno lo scopo di risolvere un problema leggermente diverso e non sono adatti a quello che stavo cercando di fare.

Ecco un esempio di script Powershell per incollare due repository insieme:

# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init

# Before we do a merge, we have to have an initial commit, so we'll make a dummy commit
git commit --allow-empty -m "Initial dummy commit"

# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>

# Merge the files from old_a/master into new/master
git merge old_a/master --allow-unrelated-histories

# Move the old_a repo files and folders into a subdirectory so they don't collide with the other repo coming later
mkdir old_a
dir -exclude old_a | %{git mv $_.Name old_a}

# Commit the move
git commit -m "Move old_a files into subdir"

# Do the same thing for old_b
git remote add -f old_b <OldB repo URL>
git merge old_b/master --allow-unrelated-histories
mkdir old_b
dir exclude old_a,old_b | %{git mv $_.Name old_b}
git commit -m "Move old_b files into subdir"

Ovviamente potresti invece unire old_b in old_a (che diventa il nuovo repository combinato) se preferisci farlo - modifica lo script per adattarlo.

Se vuoi portare anche i rami delle caratteristiche in corso, usa questo:

# Bring over a feature branch from one of the old repos
git checkout -b feature-in-progress
git merge -s recursive -Xsubtree=old_a old_a/feature-in-progress

Questa è l'unica parte non ovvia del processo: non si tratta di un'unione di sottostruttura, ma piuttosto di un argomento alla normale unione ricorsiva che dice a Git che abbiamo rinominato il bersaglio e che aiuta Git a allineare tutto correttamente.

Ho scritto una spiegazione leggermente più dettagliata qui .


16
questa soluzione usando git mvnon funziona così bene. quando in seguito si utilizza uno git logsu uno dei file spostati, si ottiene il commit solo dallo spostamento. tutta la storia precedente è andata persa. questo perché git mvè davvero git rm; git addma in un solo passaggio .
mholm815,

15
È uguale a qualsiasi altra operazione di spostamento / ridenominazione in Git: dalla riga di comando puoi ottenere tutta la cronologia facendo git log --follow, o tutti gli strumenti della GUI lo fanno automaticamente. Con un'unione di sottostruttura non è possibile ottenere la cronologia dei singoli file, per quanto ne so, quindi questo metodo è migliore.
Eric Lee

3
@EricLee Quando il repository old_b viene unito ottengo molti conflitti di unione. È previsto? Ottengo CONFLICT (rinomina / elimina)
Jon

9
Quando provo "dir -exclude old_a |% {git mv $ _. Nome old_a}", ottengo sh.exe ": dir: comando non trovato e sh.exe": git: comando non trovato. Usando questo funziona: ls -I old_a | xargs -I '{}' git mv '{}' old_a /
George

5
Questo è 1(il numero uno) per lse 'occhio' maiuscolo per xargs. Grazie per questo suggerimento!
Dominique Vial,

149

Ecco un modo che non riscrive alcuna cronologia, quindi tutti gli ID di commit rimarranno validi. Il risultato finale è che i file del secondo repository finiranno in una sottodirectory.

  1. Aggiungi il secondo repository come telecomando:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    
  2. Assicurati di aver scaricato tutti i commit di secondrepo:

    git fetch secondrepo
    
  3. Creare un ramo locale dal ramo del secondo repository:

    git branch branchfromsecondrepo secondrepo/master
    
  4. Sposta tutti i suoi file in una sottodirectory:

    git checkout branchfromsecondrepo
    mkdir subdir/
    git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} subdir/
    git commit -m "Moved files to subdir/"
    
  5. Unisci il secondo ramo nel ramo principale del primo repository:

    git checkout master
    git merge --allow-unrelated-histories branchfromsecondrepo
    

Il tuo repository avrà più di un commit di root, ma ciò non dovrebbe costituire un problema.


1
Il passaggio 2 non funziona per me: fatale: non è un nome oggetto valido: 'secondrepo / master'.
Keith,

@Keith: assicurati di aver aggiunto il secondo repository come telecomando chiamato "secondrepo" e che quel repository abbia un ramo denominato "master" (puoi visualizzare i rami su un repository remoto con il comando git remote show secondrepo)
Flimm

Ho dovuto fare un tentativo per farlo cadere. Tra 1 e 2 ho fatto andare a prendere secondrepo
sksamuel il

@monkjack: ho modificato la mia risposta per includere un passaggio di recupero git. Sentiti libero di modificare tu stesso la risposta in futuro.
Flimm,

4
@MartijnHeemels Per la versione precedente di Git, ometti --allow-unrelated-histories. Vedi la cronologia di questo post di risposta.
Flimm,

8

Sono trascorsi alcuni anni e ci sono soluzioni ben votate ben basate, ma voglio condividere le mie perché era un po 'diverso perché volevo unire 2 repository remoti in uno nuovo senza eliminare la cronologia dai repository precedenti.

  1. Crea un nuovo repository in Github.

    inserisci qui la descrizione dell'immagine

  2. Scarica il repository appena creato e aggiungi il vecchio repository remoto.

    git clone https://github.com/alexbr9007/Test.git
    cd Test
    git remote add OldRepo https://github.com/alexbr9007/Django-React.git
    git remote -v
    
  3. Recupera tutti i file dal vecchio repository in modo da creare un nuovo ramo.

    git fetch OldRepo
    git branch -a
    

    inserisci qui la descrizione dell'immagine

  4. Nel ramo master, eseguire un'unione per combinare il vecchio repository con quello appena creato.

    git merge remotes/OldRepo/master --allow-unrelated-histories
    

    inserisci qui la descrizione dell'immagine

  5. Creare una nuova cartella per archiviare tutto il nuovo contenuto creato che è stato aggiunto da OldRepo e spostare i suoi file in questa nuova cartella.

  6. Infine, puoi caricare i file dai repository combinati ed eliminare in modo sicuro l'OldRepo da GitHub.

Spero che questo possa essere utile per chiunque abbia a che fare con l'unione di repository remoti.


1
Questa è l'unica soluzione che ha funzionato per me per preservare la storia di Git. Non dimenticare di rimuovere il collegamento remoto al vecchio repository con git remote rm OldRepo.
Harubiyori,

7

per favore dai un'occhiata all'utilizzo

git rebase --root --preserve-merges --onto

collegare due storie all'inizio della loro vita.

Se hai percorsi che si sovrappongono, correggili con

git filter-branch --index-filter

quando si utilizza il registro, assicurarsi di "trovare le copie più difficili" con

git log -CC

in questo modo troverai eventuali movimenti di file nel percorso.


La documentazione di Git consiglia di non ripetere la revisione ... git-scm.com/book/en/v2/Git-Branching-Rebasing#_rebase_peril
Stephen Turner

7

Ho trasformato la soluzione da @Flimm in una git aliassimile (aggiunta alla mia ~/.gitconfig):

[alias]
 mergeRepo = "!mergeRepo() { \
  [ $# -ne 3 ] && echo \"Three parameters required, <remote URI> <new branch> <new dir>\" && exit 1; \
  git remote add newRepo $1; \
  git fetch newRepo; \
  git branch \"$2\" newRepo/master; \
  git checkout \"$2\"; \
  mkdir -vp \"${GIT_PREFIX}$3\"; \
  git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} \"${GIT_PREFIX}$3\"/; \
  git commit -m \"Moved files to '${GIT_PREFIX}$3'\"; \
  git checkout master; git merge --allow-unrelated-histories --no-edit -s recursive -X no-renames \"$2\"; \
  git branch -D \"$2\"; git remote remove newRepo; \
}; \
mergeRepo"

12
Solo curioso: lo fai davvero abbastanza spesso da aver bisogno di un alias?
Parker Coates,

1
No, ma non ricordo mai come farlo, quindi un alias è solo un modo per me di ricordarlo.
Fredrik Erlandsson,

1
Sì .. ma prova a cambiare computer e a dimenticare di spostare i tuoi alias;)
quetzalcoatl

1
Qual è il valore di $GIT_PREFIX?
neowulf33,

github.com/git/git/blob/… 'GIT_PREFIX' viene impostato come restituito eseguendo 'git rev-parse --show-prefix' dalla directory corrente originale. Vedi linkgit: git-rev-parse [1].
Fredrik Erlandsson,

3

Questa funzione clonerà il repository remoto nella directory repo locale:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

Come usare:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

Avviso. Questo script può riscrivere i commit ma salverà tutti gli autori e le date, significa che i nuovi commit avranno altri hash e se si tenta di inviare le modifiche al server remoto può essere in grado solo con il tasto force, inoltre riscriverà i commit sul server. Quindi, per favore, esegui backup prima di avviarlo.

Profitto!


Sto usando zsh anziché bash e v2.13.0 di git. Non importa quello che ho provato, non sono riuscito a mettermi git filter-branch --index-filteral lavoro. In genere viene visualizzato un messaggio di errore che indica che il file indice .new non esiste. Suona qualche campana?
Patrick Beard

@PatrickBeard Non conosco zsh, puoi creare un file separato git-add-repo.shcon la funzione sopra, alla fine del file metti questa linea git-add-repo "$@". Dopodiché puoi usarlo da zsh like cd current/git/packageebash path/to/git-add-repo.sh https://github.com/example/example dir/to/save
Andrey Izman,

Il problema è stato discusso qui: stackoverflow.com/questions/7798142/… a mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" volte fallisce, quindi devi aggiungere unif test .
Patrick Beard,

1
Non userei questo metodo! Ho provato la sceneggiatura, ingenuamente e alla lettera (posso solo incolpare me stesso per quella parte), e ha bloccato il mio repository git locale. La storia sembrava per lo più corretta, ma facendo un git push back su Github si è verificato il temuto "RPC fallito; curl 55 SSL_write () ha restituito SYSCALL, errno = 32" errore. Ho provato a ripararlo, ma è stato irreparabilmente rotto. Ho finito per dover ricostruire le cose in un nuovo repository locale.
Mason Freed

@MasonFreed questo script crea una nuova cronologia git con mix di entrambi i repository, quindi non può essere trasferito al vecchio repository, è necessario crearne uno nuovo o premere con il tasto force, significa che riscrive il repository sul server
Andrey Izman

2

Seguire i passaggi per incorporare un repository in un altro repository, con un'unica cronologia git unendo entrambe le cronologie git.

  1. Clonare entrambi i repository che si desidera unire.

clone git git@github.com: user / parent-repo.git

clone git git@github.com: user / child-repo.git

  1. Vai al repository figlio

cd child-repo /

  1. eseguire il comando seguente, sostituire path my/new/subdir(3 occorrenze) con la struttura di directory in cui si desidera avere il repository secondario.

git filter-branch --prune-empty --tree-filter 'se [! -e my / new / subdir]; quindi mkdir -p my / new / subdir git ls-tree --name-only $ GIT_COMMIT | xargs -I file mv file my / new / subdir fi '

  1. Vai al repository principale

cd ../parent-repo/

  1. Aggiungi un telecomando al repository principale, indicando il percorso al repository secondario

git remote aggiungi child-remote ../child-repo/

  1. Recupera il repository secondario

git recupera il telecomando per bambini

  1. Unisci le storie

git merge --allow-unrelated-history child-remote / master

Se si controlla ora il registro git nel repository principale, i repository secondari devono essere uniti. Puoi anche vedere il tag che indica dalla fonte di commit.

L'articolo che segue mi ha aiutato a integrare un repository in un altro repository, con un'unica cronologia git unendo entrambe le cronologie git.

http://ericlathrop.com/2014/01/combining-git-repositories/

Spero che questo ti aiuti. Buona programmazione!


Passaggio 3 non riuscito per me con errore di sintassi. Mancano i punti e virgola. Correzionegit filter-branch --prune-empty --tree-filter ' if [ ! -e my/new/subdir ]; then mkdir -p my/new/subdir; git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files my/new/subdir; fi'
Yuri L

1

Diciamo che desidera unire repository ain b(sto supponendo che stanno trovano una accanto all'altra):

cd b
git remote add a ../a
git fetch a
git merge --allow-unrelated-histories a/master
git remote remove a

Nel caso in cui desideri inserire auna sottodirectory, procedi come segue prima dei comandi sopra:

cd a
git filter-repo --to-subdirectory-filter a
cd ..

Per questo è necessario git-filter-repoinstallare ( filter-branchè sconsigliato ).

Un esempio di fusione di 2 grandi repository, inserendone uno in una sottodirectory: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Maggiori informazioni qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.