Come unire due repository Git?


1622

Considera il seguente scenario:

Ho sviluppato un piccolo progetto sperimentale A nel suo repository Git. Ora è maturato e vorrei che A facesse parte del più grande progetto B, che ha un suo grande repository. Ora vorrei aggiungere A come sottodirectory di B.

Come posso unire A in B, senza perdere la storia da nessuna parte?


8
Se stai solo cercando di combinare due repository in uno, senza la necessità di conservare entrambi i repository, dai un'occhiata a questa domanda: stackoverflow.com/questions/13040958/…
Flimm

Per la fusione repo git a dir personalizzato con il salvataggio di tutti comits utilizzano stackoverflow.com/a/43340714/1772410
Andrey Izman

Risposte:


437

Un singolo ramo di un altro repository può essere facilmente inserito in una sottodirectory mantenendo la sua cronologia. Per esempio:

git subtree add --prefix=rails git://github.com/rails/rails.git master

Questo apparirà come un unico commit in cui tutti i file del ramo master di Rails vengono aggiunti nella directory "rails". Tuttavia, il titolo del commit contiene un riferimento all'albero della cronologia precedente:

Aggiungi 'rails /' da commit <rev>

Dov'è <rev>un hash di commit SHA-1. Puoi ancora vedere la cronologia, incolpare alcune modifiche.

git log <rev>
git blame <rev> -- README.md

Si noti che non è possibile visualizzare il prefisso della directory da qui poiché si tratta di un ramo vecchio effettivo lasciato intatto. Dovresti considerare questo come un normale commit di spostamento dei file: avrai bisogno di un salto in più quando lo raggiungi.

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

Esistono soluzioni più complesse come farlo manualmente o riscrivere la cronologia come descritto in altre risposte.

Il comando git-subtree fa parte di git-contrib ufficiale, alcuni gestori di pacchetti lo installano di default (OS X Homebrew). Ma potresti doverlo installare da solo oltre a Git.


2
Ecco le istruzioni su come installare Git SubTree (da giugno 2013): stackoverflow.com/a/11613541/694469 (e ho sostituito git co v1.7.11.3 con ... v1.8.3).
KajMagnus,

1
Grazie per il testa a testa sulla risposta qui sotto. A partire da git 1.8.4 'subtree' non è ancora incluso (almeno non su Ubuntu 12.04 git ppa (ppa: git-core / ppa))
Matt Klein,

1
Posso confermare che dopo questo, git log rails/somefilenon visualizzerà la cronologia di commit di quel file tranne il commit di unione. Come suggerito da @artfulrobot, controlla la risposta di Greg Hewgill . E potrebbe essere necessario utilizzare git filter-branchil repository che si desidera includere.
Jifeng Zhang,

6
Oppure leggi "Unire due repository Git in un solo repository senza perdere la cronologia dei file" di Eric Lee saintgimp.org/2013/01/22/…
Jifeng Zhang

4
Come altri hanno già detto, git subtreepotrebbe non fare quello che pensi! Vedi qui per una soluzione più completa.
Paul Draper,

1909

Se vuoi unirti project-aa project-b:

cd path/to/project-b
git remote add project-a path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

Tratto da: git unisce diversi repository?

Questo metodo ha funzionato abbastanza bene per me, è più corto e secondo me molto più pulito.

Nel caso in cui si desideri inserire project-ain una sottodirectory, è possibile utilizzare git-filter-repo( filter-branchè sconsigliato ). Esegui i seguenti comandi prima dei comandi sopra:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

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

Nota: il --allow-unrelated-historiesparametro esiste solo da git> = 2.9. Vedi la documentazione di Git - git merge / --allow-unrelated-history

Aggiornamento : aggiunto --tagscome suggerito da @jstadler per mantenere i tag.


8
Questo ha funzionato per me. Ha funzionato come un incantesimo la prima volta con un solo conflitto nel file .gitignore! Ha conservato perfettamente la storia del commit. Il grande vantaggio rispetto ad altri approcci - oltre alla semplicità - è che con questo non c'è bisogno di un riferimento continuo al repository unito. Una cosa da tenere d'occhio, tuttavia, se sei uno sviluppatore iOS come me, è fare molta attenzione a inserire il file di progetto del repository di destinazione nell'area di lavoro.
Max MacLeod,

30
Grazie. Ha funzionato per me. Avevo bisogno di spostare la directory unita in una sottocartella, quindi dopo aver seguito i passaggi precedenti ho semplicemente usatogit mv source-dir/ dest/new-source-dir
Sid

13
Il git mergepasso qui fallisce con fatal: refusing to merge unrelated histories; --allow-unrelated-historiesrisolve ciò come spiegato nei documenti .
ssc,

19
--allow-unrelated-historiesè stato introdotto in git 2.9 . Nelle versioni precedenti si trattava di un comportamento predefinito.
Douglas Royds,

11
Shorter: git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.
jill

614

Ecco due possibili soluzioni:

sottomoduli

Copia il repository A in una directory separata nel progetto B più grande o (forse meglio) clona il repository A in una sottodirectory del progetto B. Quindi usa git submodule per rendere questo repository un sottomodulo di un repository B.

Questa è una buona soluzione per i depositi debolmente accoppiati, dove lo sviluppo in un repository continua, e la parte principale di sviluppo è uno sviluppo autonomo separato in A. Vedi anche SubmoduleSupport e GitSubmoduleTutorial pagine Git Wiki.

Unione sottotree

È possibile unire il repository A in una sottodirectory di un progetto B utilizzando la strategia di unione delle sottostrutture . Questo è descritto in Subtree Merging and You di Markus Prinz.

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(L'opzione --allow-unrelated-historiesè necessaria per Git> = 2.9.0.)

Oppure puoi usare lo strumento git subtree ( repository su GitHub ) di apenwarr (Avery Pennarun), annunciato ad esempio nel suo post sul blog Una nuova alternativa ai sottomoduli Git: git subtree .


Penso che nel tuo caso (A debba far parte del più grande progetto B) la soluzione corretta sarebbe quella di utilizzare l' unione di sottostruttura .


1
Funziona e sembra preservare la cronologia, ma non è tale da poterlo utilizzare per diff diff file o bisecare attraverso l'unione. Mi manca un passo?
jettero

56
questo è incompleto . Sì, ottieni un sacco di commit, ma non fanno più riferimento ai percorsi giusti. git log dir-B/somefilenon mostrerà nulla tranne quello dell'unione. Vedi la risposta di Greg Hewgill fa riferimento a questo importante problema.
artfulrobot,

2
IMPORTANTE: git pull --no-rebase -s subtree Bproject master Se non lo fai, e hai pull impostato su rebase automaticamente, finirai con "Impossibile analizzare l'oggetto". Vedi osdir.com/ml/git/2009-07/msg01576.html
Eric Bowman - abstracto -

4
Questa risposta può essere confusa perché ha B come sottostruttura unita quando nella domanda era A. Risultato di una copia e incolla?
vfclists

11
Se stai cercando di incollare semplicemente due repository insieme, i sottomoduli e le fusioni di sottostruttura sono lo strumento sbagliato da utilizzare perché non conservano tutta la cronologia dei file (come hanno notato altri commentatori). Vedi stackoverflow.com/questions/13040958/… .
Eric Lee,

194

L'approccio sottomodulo è valido se si desidera mantenere il progetto separatamente. Tuttavia, se vuoi davvero unire entrambi i progetti nello stesso repository, hai ancora un po 'di lavoro da fare.

La prima cosa sarebbe usare git filter-branchper riscrivere i nomi di tutto nel secondo repository per essere nella sottodirectory in cui vorresti che finissero. Quindi invece di foo.c, bar.htmlavresti projb/foo.ce projb/bar.html.

Quindi, dovresti essere in grado di fare qualcosa di simile al seguente:

git remote add projb [wherever]
git pull projb

La git pullfarà un git fetchseguito da un git merge. Non dovrebbero esserci conflitti se il repository a cui stai eseguendo il pull non ha ancora una projb/directory.

Ulteriore ricerca indica che qualcosa di simile è stato fatto a fondersi gitkin git. Junio ​​C Hamano ne scrive qui: http://www.mail-archive.com/git@vger.kernel.org/msg03395.html


4
l'unione di sottostruttura sarebbe una soluzione migliore e non richiederebbe la riscrittura della storia del progetto incluso
Jakub Narębski,

8
Mi piacerebbe sapere come usare git filter-branchper raggiungere questo obiettivo. Nella pagina man dice il contrario: fare in modo che subdir / diventi la radice, ma non viceversa.
artfulrobot,

31
questa risposta sarebbe ottima se spiegasse come usare il filtro-ramo per ottenere il risultato desiderato
Anentropico

14
Ho trovato come utilizzare il filtro-ramo qui: stackoverflow.com/questions/4042816/…
David Minor,

3
Vedi questa risposta per l'implementazione del profilo di Greg.
Paul Draper,

75

git-subtree è carino, ma probabilmente non è quello che vuoi.

Ad esempio, se projectAè la directory creata in B, dopo git subtree,

git log projectA

elenca un solo commit: l'unione. I commit del progetto unito sono per percorsi diversi, quindi non vengono visualizzati.

La risposta di Greg Hewgill arriva più vicino, anche se in realtà non dice come riscrivere i percorsi.


La soluzione è sorprendentemente semplice.

(1) In A,

PREFIX=projectA #adjust this

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

Nota: questo riscrive la cronologia, quindi se si intende continuare a utilizzare questo repository A, è consigliabile clonare (copiare) prima una copia usa e getta.

Nota Bene: è necessario modificare lo script sostitutivo all'interno del comando sed nel caso in cui si utilizzino caratteri non ascii (o caratteri bianchi) nei nomi di file o nel percorso. In tal caso la posizione del file all'interno di un record prodotto da "ls-files -s" inizia con le virgolette.

(2) Quindi in B, corri

git pull path/to/A

Ecco! Hai una projectAdirectory in B. Se esegui git log projectA, vedrai tutti i commit da A.


Nel mio caso, volevo due sottodirectory projectAe projectB. In quel caso, ho fatto anche il passaggio da (1) a B.


1
Sembra che tu abbia copiato la tua risposta da stackoverflow.com/a/618113/586086 ?
Andrew Mao,

1
@AndrewMao, penso di sì ... non me lo ricordo proprio. Ho usato un po 'questo script.
Paul Draper,

6
Aggiungo che \ t non funziona su OS X e devi inserire <tab>
Muneeb Ali

2
"$GIT_INDEX_FILE"deve essere citato (due volte), altrimenti il ​​metodo fallirà se, ad esempio, il percorso contiene spazi.
Rob W,

4
Se ti stai chiedendo, per inserire un <tab> in osx, deviCtrl-V <tab>
casey

48

Se entrambi i repository hanno lo stesso tipo di file (come due repository Rails per progetti diversi), è possibile recuperare i dati del repository secondario nel repository corrente:

git fetch git://repository.url/repo.git master:branch_name

e poi uniscilo al repository corrente:

git merge --allow-unrelated-histories branch_name

Se la tua versione Git è inferiore a 2.9, rimuovila --allow-unrelated-histories.

Successivamente, possono verificarsi conflitti. Puoi risolverli ad esempio con git mergetool. kdiff3può essere utilizzato esclusivamente con la tastiera, quindi sono necessari 5 file di conflitto durante la lettura del codice in pochi minuti.

Ricorda di finire l'unione:

git commit

25

Ho continuato a perdere la cronologia durante l'utilizzo di Merge, quindi ho finito con ReBase poiché nel mio caso i due repository sono abbastanza diversi da non finire per unire ad ogni commit:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=> risolvi i conflitti, quindi continua, tutte le volte che è necessario ...

git rebase --continue

In questo modo un progetto ha tutti i commit di projA seguiti da commit di projB


25

Nel mio caso, avevo un my-pluginrepository e un main-projectrepository e volevo fingere che my-pluginfosse sempre stato sviluppato nella pluginssottodirectory di main-project.

Fondamentalmente, ho riscritto la storia del my-pluginrepository in modo che apparisse tutto lo sviluppo nella plugins/my-pluginsottodirectory. Quindi, ho aggiunto la storia di sviluppo my-pluginalla main-projectstoria e ho unito i due alberi insieme. Dato che non era plugins/my-plugingià presente una directory nel main-projectrepository, questa era una banale fusione senza conflitti. Il repository risultante conteneva tutta la storia di entrambi i progetti originali e aveva due radici.

TL; DR

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

Versione lunga

Innanzitutto, crea una copia del my-pluginrepository, perché riscriveremo la cronologia di questo repository.

Ora, vai alla radice del my-pluginrepository, controlla il tuo ramo principale (probabilmente master) ed esegui il seguente comando. Ovviamente, dovresti sostituire my-plugine pluginsqualunque sia il tuo vero nome.

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

Ora per una spiegazione. git filter-branch --tree-filter (...) HEADesegue il (...)comando su ogni commit da cui è possibile raggiungere HEAD. Si noti che questo funziona direttamente sui dati memorizzati per ciascun commit, quindi non dobbiamo preoccuparci delle nozioni di "directory di lavoro", "indice", "stadiazione" e così via.

Se si esegue un filter-branchcomando che ha esito negativo, lascerà dietro di sé alcuni file nella .gitdirectory e al successivo tentativo filter-branchsi lamenterà di questo, a meno che non si fornisca l' -fopzione a filter-branch.

Per quanto riguarda il comando effettivo, non ho avuto molta fortuna bashnel fare quello che volevo, quindi invece uso zsh -cper zsheseguire un comando. Innanzitutto ho impostato l' extended_globopzione, che è ciò che abilita la ^(...)sintassi nel mvcomando, così come l' glob_dotsopzione, che mi permette di selezionare dotfile (come .gitignore) con un glob (^(...) ).

Successivamente, utilizzo il mkdir -pcomando per creare sia pluginseplugins/my-plugin allo stesso tempo.

Infine, utilizzo la funzione zsh"negativo globale" ^(.git|plugins)per abbinare tutti i file nella directory principale del repository ad eccezione di .gite la my-plugincartella appena creata . (esclusa.git potrebbe non essere necessaria qui, ma provare a spostare una directory in se stessa è un errore.)

Nel mio repository, il commit iniziale non includeva alcun file, quindi il mvcomando ha restituito un errore sul commit iniziale (poiché non era disponibile nulla da spostare). Pertanto, ho aggiunto un || truemodo che git filter-branchnon si interrompesse.

L' --allopzione dice filter-branchdi riscrivere la cronologia per tutti i rami nel repository e il extra --è necessario per dire gitdi interpretarla come una parte dell'elenco delle opzioni per i rami da riscrivere, invece che come opzione a filter-branchse stessa.

Ora, vai al tuo main-projectrepository e controlla in quale ramo vuoi unirti. Aggiungi la tua copia locale del my-pluginrepository (con la sua cronologia modificata) come telecomando di main-projectcon:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

Ora avrai due alberi non correlati nella tua cronologia di commit, che puoi visualizzare bene usando:

$ git log --color --graph --decorate --all

Per unirli, utilizzare:

$ git merge my-plugin/master --allow-unrelated-histories

Si noti che in Git precedente alla 2.9.0, l' --allow-unrelated-historiesopzione non esiste. Se si utilizza una di queste versioni, è sufficiente omettere l'opzione: il messaggio di errore che --allow-unrelated-historiesimpedisce è stato aggiunto anche in 2.9.0.

Non dovresti avere alcun conflitto di unione. Se lo fai, probabilmente significa che il filter-branchcomando non ha funzionato correttamente o che c'era già una plugins/my-plugindirectory main-project.

Assicurati di inserire un messaggio esplicativo di commit per tutti i futuri contributori che si chiedono cosa stia succedendo un hacker per creare un repository con due radici.

È possibile visualizzare il nuovo grafico di commit, che dovrebbe avere due commit di root, usando il git logcomando sopra . Si noti che solo il masterramo verrà unito . Ciò significa che se si ha un lavoro importante su altri my-pluginrami che si desidera unire main-projectall'albero, è necessario astenersi dall'eliminare il my-plugintelecomando fino a quando non si sono eseguite queste fusioni. In caso contrario, i commit da quei rami rimarranno comunque nel main-projectrepository, ma alcuni saranno irraggiungibili e suscettibili all'eventuale raccolta dei rifiuti. (Inoltre, dovrai fare riferimento a loro da SHA, poiché l'eliminazione di un telecomando rimuove i suoi rami di tracciamento remoto.)

Facoltativamente, dopo aver unito tutto ciò che si desidera mantenere my-plugin, è possibile rimuovere il my-plugintelecomando utilizzando:

$ git remote remove my-plugin

Ora è possibile eliminare in sicurezza la copia del my-pluginrepository di cui è stata modificata la cronologia. Nel mio caso, ho anche aggiunto un avviso di deprecazione al my-pluginrepository reale dopo che l'unione è stata completata e inviata.


Testato su Mac OS X El Capitan con git --version 2.9.0e zsh --version 5.2. Il tuo chilometraggio può variare.

Riferimenti:


1
Da dove --allow-unrelated-historiesviene?
xpto

3
@MarceloFilho Check man git-merge. Per impostazione predefinita, il comando git merge rifiuta di unire le storie che non condividono un antenato comune. Questa opzione può essere utilizzata per ignorare questa sicurezza quando si uniscono le storie di due progetti che hanno iniziato la loro vita in modo indipendente. Poiché si tratta di un'occasione molto rara, non esiste alcuna variabile di configurazione per abilitarla per impostazione predefinita e non verrà aggiunta.
Radon Rosborough,

Dovrebbe essere disponibile su git version 2.7.2.windows.1?
xpto

2
@MarceloFilho È stato aggiunto in 2.9.0, ma nelle versioni precedenti non dovresti passare l'opzione (funzionerà). github.com/git/git/blob/…
Radon Rosborough,

Questo ha funzionato bene. E sono stato in grado di utilizzare il ramo del filtro per riscrivere i nomi dei file nel punto desiderato nella struttura prima dell'unione. Suppongo che ci sia più lavoro da fare se devi spostare la storia oltre al ramo principale.
codice

9

Ho provato a fare la stessa cosa per giorni, sto usando git 2.7.2. Sottostruttura non conserva la storia.

È possibile utilizzare questo metodo se non si utilizzerà nuovamente il vecchio progetto.

Vorrei suggerire di ramificare prima B e lavorare nel ramo.

Ecco i passaggi senza diramazione:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

Se ora registri uno dei file nel sottodirectory A otterrai la cronologia completa

git log --follow A/<file>

Questo è stato il post che mi ha aiutato a fare questo:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/


8

Se si desidera inserire i file da un ramo nel repository B in una sottostruttura del repository A e conservare anche la cronologia, continuare a leggere. (Nell'esempio seguente, presumo che vogliamo che il ramo master del repository B sia unito nel ramo master del repository A.

Nel repository A, eseguire innanzitutto le seguenti operazioni per rendere disponibile il repository B:

git remote add B ../B # Add repo B as a new remote.
git fetch B

Ora creiamo una nuova filiale (con un solo commit) nel repository A che chiamiamo new_b_root. Il commit risultante avrà i file che sono stati impegnati nel primo commit del ramo master del repository B. ma inseriti in una sottodirectory chiamata path/to/b-files/.

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

Spiegazione: L' --orphanopzione per il comando di checkout estrae i file dal ramo master di A ma non crea alcun commit. Avremmo potuto selezionare qualsiasi commit perché in seguito cancelleremo comunque tutti i file. Quindi, senza ancora il commit ( -n), selezioniamo il primo commit dal ramo master di B. (Cherry-pick preserva il messaggio di commit originale che un checkout diretto non sembra fare.) Quindi creiamo la sottostruttura in cui vogliamo mettere tutti i file dal repository B. Dobbiamo quindi spostare tutti i file che sono stati introdotti nel scegli la ciliegia per la sottostruttura. Nell'esempio sopra, c'è solo unREADME file da spostare. Quindi impegniamo il nostro commit root B-repo e, allo stesso tempo, conserviamo anche il timestamp del commit originale.

Ora creeremo un nuovo B/masterramo sopra quello appena creato new_b_root. Chiamiamo la nuova filiale b:

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

Ora uniamo la nostra bfiliale in A/master:

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

Infine, puoi rimuovere i Brami remoti e temporanei:

git remote remove B
git branch -D new_b_root b

Il grafico finale avrà una struttura come questa:

inserisci qui la descrizione dell'immagine


Ottima risposta, grazie! Mi è mancato molto nelle altre risposte con "git subtree" o "merge --allow-unrelated-histories" di Andresch Serj che la directory secondaria non aveva il registro.
Ilendir,

8

Ho raccolto molte informazioni qui su Stack OverFlow, ecc., E sono riuscito a mettere insieme uno script che risolve il problema per me.

L'avvertenza è che prende in considerazione solo il ramo "sviluppo" di ciascun repository e lo unisce in una directory separata in un repository completamente nuovo.

I tag e gli altri rami vengono ignorati: potrebbe non essere quello che desideri.

Lo script gestisce anche rami e tag delle funzionalità: rinominandoli nel nuovo progetto in modo da sapere da dove provengono.

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            eitch@eitchnet.ch
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=$'\n'

# Detect proper usage
if [ "$#" -ne "2" ] ; then
  echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"
  exit 1
fi


## Script variables
PROJECT_NAME="${1}"
PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}"
TIMESTAMP="$(date +%s)"
LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log"
REPO_FILE="${2}"
REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}"


# Script functions
function failed() {
  echo -e "ERROR: Merging of projects failed:"
  echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1
  echo -e "$1"
  exit 1
}

function commit_merge() {
  current_branch="$(git symbolic-ref HEAD 2>/dev/null)"
  if [[ ! -f ".git/MERGE_HEAD" ]] ; then
    echo -e "INFO:   No commit required."
    echo -e "INFO:   No commit required." >>${LOG_FILE} 2>&1
  else
    echo -e "INFO:   Committing ${sub_project}..."
    echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"
    fi
  fi
}


# Make sure the REPO_URL_FILE exists
if [ ! -e "${REPO_URL_FILE}" ] ; then
  echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!"
  exit 1
fi


# Make sure the required directories don't exist
if [ -e "${PROJECT_PATH}" ] ; then
  echo -e "ERROR: Project ${PROJECT_NAME} already exists!"
  exit 1
fi


# create the new project
echo -e "INFO: Logging to ${LOG_FILE}"
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..."
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
cd ${ROOT_DIR}
mkdir ${PROJECT_NAME}
cd ${PROJECT_NAME}
git init
echo "Initial Commit" > initial_commit
# Since this is a new repository we need to have at least one commit
# thus were we create temporary file, but we delete it again.
# Deleting it guarantees we don't have conflicts later when merging
git add initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
git rm --quiet initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
echo


# Merge all projects into the branches of this project
echo -e "INFO: Merging projects into new repository..."
echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ "${url:0:1}" == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO: Project ${sub_project}"
  echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1
  echo -e "----------------------------------------------------"
  echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1

  # Fetch the project
  echo -e "INFO:   Fetching ${sub_project}..."
  echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote add "${sub_project}" "${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then
    failed "Failed to fetch project ${sub_project}"
  fi

  # add remote branches
  echo -e "INFO:   Creating local branches for ${sub_project}..."
  echo -e "INFO:   Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read branch ; do
    branch_ref=$(echo $branch | tr " " "\t" | cut -f 1)
    branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-)

    echo -e "INFO:   Creating branch ${branch_name}..."
    echo -e "INFO:   Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1

    # create and checkout new merge branch off of master
    if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi
    if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi

    # Merge the project
    echo -e "INFO:   Merging ${sub_project}..."
    echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}"
    fi

    # And now see if we need to commit (maybe there was a merge)
    commit_merge "${sub_project}/${branch_name}"

    # relocate projects files into own directory
    if [ "$(ls)" == "${sub_project}" ] ; then
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level."
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1
    else
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..."
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1
      mkdir ${sub_project}
      for f in $(ls -a) ; do
        if  [[ "$f" == "${sub_project}" ]] ||
            [[ "$f" == "." ]] ||
            [[ "$f" == ".." ]] ; then
          continue
        fi
        git mv -k "$f" "${sub_project}/"
      done

      # commit the moving
      if ! git commit --quiet -m  "[Project] Move ${sub_project} files into sub directory" ; then
        failed "Failed to commit moving of ${sub_project} files into sub directory"
      fi
    fi
    echo
  done < <(git ls-remote --heads ${sub_project})


  # checkout master of sub probject
  if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "sub_project ${sub_project} is missing master branch!"
  fi

  # copy remote tags
  echo -e "INFO:   Copying tags for ${sub_project}..."
  echo -e "INFO:   Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read tag ; do
    tag_ref=$(echo $tag | tr " " "\t" | cut -f 1)
    tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3)

    # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0
    tag_name="${tag_name_unfixed%%^*}"

    tag_new_name="${sub_project}/${tag_name}"
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..."
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1
    if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}"
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1
    fi
  done < <(git ls-remote --tags --refs ${sub_project})

  # Remove the remote to the old project
  echo -e "INFO:   Removing remote ${sub_project}..."
  echo -e "INFO:   Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote rm ${sub_project}

  echo
done


# Now merge all project master branches into new master
git checkout --quiet master
echo -e "INFO: Merging projects master branches into new repository..."
echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ ${url:0:1} == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO:   Merging ${sub_project}..."
  echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
  if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "Failed to merge branch ${sub_project}/master into master"
  fi

  # And now see if we need to commit (maybe there was a merge)
  commit_merge "${sub_project}/master"

  echo
done


# Done
cd ${ROOT_DIR}
echo -e "INFO: Done."
echo -e "INFO: Done." >>${LOG_FILE} 2>&1
echo

exit 0

Puoi anche ottenerlo da http://paste.ubuntu.com/11732805

Innanzitutto creare un file con l'URL per ciascun repository, ad esempio:

git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git

Quindi chiama lo script dando un nome al progetto e il percorso dello script:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

La sceneggiatura stessa ha molti commenti che dovrebbero spiegare cosa fa.


Invece di indirizzare i lettori a una risposta, pubblica qui la risposta (ovvero modifica ciò che hai detto in quel commento in questa risposta).
josliber

1
Certo, ho solo pensato di non ripetermi ... =)
eitch

Se ritieni che questa domanda sia identica all'altra, puoi contrassegnarla come duplicata usando il link "flag" sotto la domanda stessa e indicando l'altra domanda. Se non è una domanda duplicata ma pensi che la stessa risposta esatta possa essere utilizzata per risolvere entrambi i problemi, pubblica semplicemente la stessa risposta per entrambi i problemi (come hai fatto ora). Grazie per aver contribuito!
josliber

Sorprendente! Non ha funzionato sul prompt della bash di Windows, ma ha funzionato perfettamente da una scatola di Vagrant con Ubuntu. Che risparmio di tempo!
xverges

Happy to be of service =)
eitch

7

So che è passato molto tempo dal fatto, ma non ero contento delle altre risposte che ho trovato qui, quindi ho scritto questo:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

2
Questo era esattamente quello che stavo cercando. Grazie! Tuttavia, ho dovuto cambiare la riga 22 in:if [[ $dirname =~ ^.*\.git$ ]]; then
heyman

2
^. * blarg $ è inutilmente avido RE. Meglio dire .blarg $ e saltare l'ancora anteriore.
Jettero,

7

Se stai cercando di incollare semplicemente due repository insieme, i sottomoduli e le fusioni di sottostruttura sono lo strumento sbagliato da utilizzare perché non conservano tutta la cronologia dei file (come hanno notato le persone su altre risposte). Vedi questa risposta qui per il modo semplice e corretto per farlo.


1
La tua soluzione funziona bene solo per il nuovo repository, ma che ne dici di unire un repository in un altro con conflitti di file?
Andrey Izman,

6

Avevo una sfida simile, ma nel mio caso avevamo sviluppato una versione della base di codice nel repository A, quindi clonata in un nuovo repository, repository B, per la nuova versione del prodotto. Dopo aver corretto alcuni bug nel repository A, dovevamo FI le modifiche nel repository B. Alla fine abbiamo fatto quanto segue:

  1. Aggiunta di un telecomando al repository B che puntava al repository A (git remote add ...)
  2. Estrarre il ramo corrente (non stavamo usando master per la correzione di bug) (git pull remoteForRepoA bugFixBranch)
  3. Spingendo si unisce a github

Ha funzionato a meraviglia :)


5

Simile a @Smar ma utilizza i percorsi del file system, impostati in PRIMARY e SECONDARY:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

Quindi si uniscono manualmente.

(adattato dalle poste di Anar Manafov )


5

Unione di 2 repository

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master

4

Quando si desidera unire tre o più progetti in a unico commit, esegui i passaggi descritti nelle altre risposte ( remote add -f, merge). Quindi, (soft) ripristina l'indice sulla vecchia testa (dove non si è verificata alcuna unione). Aggiungi tutti i file ( git add -A) e commettili (messaggio "Unire i progetti A, B, C e D in un unico progetto). Questo è ora il commit-id del master.

Ora crea .git/info/graftscon il seguente contenuto:

<commit-id of master> <list of commit ids of all parents>

Correre git filter-branch -- head^..head head^2..head head^3..head . Se hai più di tre rami, aggiungi semplicemente quanti head^n..headrami hai. Per aggiornare i tag, aggiungi --tag-name-filter cat. Non aggiungerlo sempre, perché ciò potrebbe causare una riscrittura di alcuni commit. Per i dettagli, consultare la pagina man del filtro-ramo , cercare "innesti".

Ora, il tuo ultimo commit ha i genitori giusti associati.


1
Aspetta, perché dovresti unire tre progetti in un unico commit?
Steve Bennett,

Ho iniziato con repository, repository-client e modeler come progetti git separati. Questo è stato difficile per i colleghi, quindi mi sono unito a loro in un unico progetto git. Per essere in grado che la "radice" del nuovo progetto provenga da altri tre progetti, volevo avere un unico commit di unione.
koppor,

4

Per unire una A in B:

1) Nel progetto A

git fast-export --all --date-order > /tmp/ProjectAExport

2) Nel progetto B

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

In questo ramo fai tutte le operazioni che devi fare e impegnale.

C) Quindi di nuovo al maestro e una fusione classica tra i due rami:

git checkout master
git merge projectA

2

Questa funzione clonerà il repository remoto nella directory repo locale, dopo aver salvato tutti i commit, git logverranno mostrati i commit originali e i percorsi corretti:

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

Se si apportano alcune modifiche, è anche possibile spostare i file / le directory del repository unito in percorsi diversi, ad esempio:

repo="https://github.com/example/example"
path="$(pwd)"

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

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

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

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"

Avvisi I
percorsi vengono sostituiti da sed, quindi assicurarsi che si siano spostati nei percorsi corretti dopo l'unione.
Il --allow-unrelated-historiesparametro esiste solo da git> = 2.9.


1

Il comando dato è la migliore soluzione possibile che suggerisco.

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master

1

Unisco i progetti leggermente manualmente, il che mi consente di evitare la necessità di affrontare i conflitti di unione.

per prima cosa, copia i file dall'altro progetto come preferisci.

cp -R myotherproject newdirectory
git add newdirectory

prossimo pull nella storia

git fetch path_or_url_to_other_repo

di 'a git di fondersi nella storia dell'ultima cosa recuperata

echo 'FETCH_HEAD' > .git/MERGE_HEAD

ora commetti comunque normalmente ti impegneresti

git commit

0

Volevo spostare un piccolo progetto in una sottodirectory di uno più grande. Dal momento che il mio piccolo progetto non ha avuto molti commit, ho usato git format-patch --output-directory /path/to/patch-dir. Quindi sul progetto più grande, ho usatogit am --directory=dir/in/project /path/to/patch-dir/* .

Questo sembra molto meno spaventoso e molto più pulito di un ramo di filtro. Concesso, potrebbe non essere applicabile a tutti i casi.

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.