Come e / o perché la fusione in Git è migliore che in SVN?


400

Ho sentito in alcuni punti che uno dei motivi principali per cui brillano i sistemi di controllo della versione distribuita è la fusione molto migliore rispetto agli strumenti tradizionali come SVN. Questo è in realtà dovuto alle differenze intrinseche nel modo in cui funzionano i due sistemi o le implementazioni DVCS specifiche come Git / Mercurial hanno algoritmi di fusione più intelligenti rispetto a SVN?


Non ho ancora ricevuto una risposta completa dalla lettura delle grandi risposte qui. Ripubblicato - stackoverflow.com/questions/6172037/...
ripper234


dipende dal tuo modello. in casi più semplici, svn è spesso migliore perché non chiama accidentalmente fusioni a 2 vie fusioni a 3 vie come git può fare se si spinge / unisce / tira / spinge su un singolo ramo di sviluppo. vedi: svnvsgit.com
Erik Aronesty,

Risposte:


556

L'affermazione del perché la fusione sia migliore in un DVCS che in Subversion era in gran parte basata sul modo in cui la ramificazione e l'unione funzionavano in Subversion qualche tempo fa. Subversion prima della 1.5.0 non memorizzava alcuna informazione su quando le filiali fossero unite, quindi quando si voleva unire si doveva specificare quale intervallo di revisioni che dovevano essere unite.

Quindi perché le fusioni di Subversion fanno schifo ?

Rifletti su questo esempio:

      1   2   4     6     8
trunk o-->o-->o---->o---->o
       \
        \   3     5     7
b1       +->o---->o---->o

Quando vogliamo unire le modifiche di b1 nel trunk, emettiamo il seguente comando, mentre ci troviamo su una cartella con il trunk estratto:

svn merge -r 2:7 {link to branch b1}

... che tenterà di unire le modifiche b1nella directory di lavoro locale. E quindi esegui il commit delle modifiche dopo aver risolto eventuali conflitti e verificato il risultato. Quando esegui il commit, l'albero della revisione sarà simile al seguente:

      1   2   4     6     8   9
trunk o-->o-->o---->o---->o-->o      "the merge commit is at r9"
       \
        \   3     5     7
b1       +->o---->o---->o

Tuttavia, questo modo di specificare intervalli di revisioni sfugge rapidamente alla mano quando l'albero delle versioni cresce poiché la sovversione non aveva metadati su quando e quali revisioni si sono fuse insieme. Rifletti su cosa succede dopo:

           12        14
trunk  …-->o-------->o
                                     "Okay, so when did we merge last time?"
              13        15
b1     …----->o-------->o

Questo è in gran parte un problema del design del repository che Subversion ha, al fine di creare un ramo è necessario creare una nuova directory virtuale nel repository che ospiterà una copia del trunk ma non memorizza alcuna informazione su quando e cosa le cose sono state ricongiunte. Ciò a volte porterà a cattivi conflitti di fusione. Ciò che è stato anche peggio è che Subversion ha usato la fusione a due vie per impostazione predefinita, che ha alcune limitazioni paralizzanti nella fusione automatica quando due teste di ramo non vengono confrontate con il loro antenato comune.

Per mitigare questo Subversion ora memorizza i metadati per diramazione e unione. Ciò risolverebbe tutti i problemi, giusto?

E oh, a proposito, Subversion fa ancora schifo ...

Su un sistema centralizzato, come la sovversione, le directory virtuali fanno schifo. Perché? Perché tutti hanno accesso per vederli ... anche quelli spazzatura sperimentali. La ramificazione è buona se vuoi sperimentare ma non vuoi vedere la sperimentazione di tutti e delle loro zie . Questo è un grave rumore cognitivo. Più rami aggiungi, più schifezze vedrai.

Più filiali pubbliche hai in un repository, più difficile sarà tenere traccia di tutte le diverse filiali. Quindi la domanda che avrai è se il ramo è ancora in sviluppo o se è davvero morto, il che è difficile da dire in qualsiasi sistema di controllo versione centralizzato.

La maggior parte delle volte, da quello che ho visto, un'organizzazione utilizzerà comunque un ramo grande per impostazione predefinita. Il che è un peccato perché a sua volta sarà difficile tenere traccia dei test e delle versioni di rilascio, e tutto il resto viene dalla ramificazione.

Allora perché i DVCS, come Git, Mercurial e Bazaar, sono meglio di Subversion per ramificarsi e fondersi?

C'è una ragione molto semplice per cui: la ramificazione è un concetto di prima classe . Non ci sono directory virtuali in base alla progettazione e i rami sono oggetti duri in DVCS che deve essere tale per funzionare semplicemente con la sincronizzazione dei repository (ovvero push and pull ).

La prima cosa che fai quando lavori con un DVCS è clonare i repository (git's clone, hg's clonee bzr's branch). La clonazione è concettualmente la stessa cosa della creazione di un ramo nel controllo versione. Alcuni lo chiamano biforcazione o ramificazione (sebbene quest'ultimo sia spesso usato anche per riferirsi a filiali localizzate), ma è la stessa cosa. Ogni utente esegue il proprio repository, il che significa che hai una diramazione per utente in corso.

La struttura della versione non è un albero , ma piuttosto un grafico . Più specificamente un grafico aciclico diretto (DAG, che significa un grafico che non ha alcun ciclo). In realtà non è necessario soffermarsi sulle specifiche di un DAG diverso da ogni commit con uno o più riferimenti principali (su cui si basava il commit). Quindi i seguenti grafici mostreranno le frecce tra le revisioni al contrario per questo motivo.

Un esempio molto semplice di fusione sarebbe questo; immagina un repository centrale chiamato origine un utente, Alice, che clona il repository sulla sua macchina.

         a…   b…   c…
origin   o<---o<---o
                   ^master
         |
         | clone
         v

         a…   b…   c…
alice    o<---o<---o
                   ^master
                   ^origin/master

Ciò che accade durante un clone è che ogni revisione è copiata su Alice esattamente come erano (che è validata dagli hash-id identificabili in modo univoco) e segna dove si trovano i rami dell'origine.

Alice lavora quindi sul suo repository, impegnandosi nel suo repository e decide di spingere le sue modifiche:

         a…   b…   c…
origin   o<---o<---o
                   ^ master

              "what'll happen after a push?"


         a…   b…   c…   d…   e…
alice    o<---o<---o<---o<---o
                             ^master
                   ^origin/master

La soluzione è piuttosto semplice, l'unica cosa che il originrepository deve fare è prendere tutte le nuove revisioni e spostare il suo ramo sulla revisione più recente (che git chiama "avanzamento rapido"):

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

         a…   b…   c…   d…   e…
alice    o<---o<---o<---o<---o
                             ^master
                             ^origin/master

Il caso d'uso, che ho illustrato sopra, non ha nemmeno bisogno di unire nulla . Quindi il problema non riguarda gli algoritmi di fusione poiché l'algoritmo di fusione a tre vie è praticamente lo stesso tra tutti i sistemi di controllo della versione. Il problema riguarda più la struttura che altro .

Che ne dici di mostrarmi un esempio che ha una vera unione?

Certamente l'esempio sopra è un caso d'uso molto semplice, quindi facciamolo molto più contorto sebbene più comune. Ricordi che è origininiziato con tre revisioni? Bene, il ragazzo che li ha fatti, lo chiama Bob , ha lavorato da solo e si è impegnato sul suo repository:

         a…   b…   c…   f…
bob      o<---o<---o<---o
                        ^ master
                   ^ origin/master

                   "can Bob push his changes?" 

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

Ora Bob non può inviare le sue modifiche direttamente al originrepository. Il modo in cui il sistema lo rileva è verificando se le revisioni di Bob discendono direttamente da quelle origin, cosa che in questo caso non accade. Qualsiasi tentativo di spingere si tradurrà nel sistema dicendo qualcosa di simile a " Uh ... temo di non poterti permettere Bob ."

Quindi Bob deve fare il pull-in e quindi unire le modifiche (con git's pull; o hg's pulle merge; o bzr's merge). Questo è un processo in due fasi. Prima Bob deve recuperare le nuove revisioni, che le copieranno così come sono dal originrepository. Ora possiamo vedere che il grafico differisce:

                        v master
         a…   b…   c…   f…
bob      o<---o<---o<---o
                   ^
                   |    d…   e…
                   +----o<---o
                             ^ origin/master

         a…   b…   c…   d…   e…
origin   o<---o<---o<---o<---o
                             ^ master

Il secondo passo del processo pull è quello di unire i suggerimenti divergenti e fare un commit del risultato:

                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+
                             ^ origin/master

Si spera che l'unione non si scontri con conflitti (se li anticipi puoi fare i due passaggi manualmente in git con fetche merge). Ciò che in seguito deve essere fatto è inserire nuovamente tali modifiche origin, che si tradurranno in un'unione di avanzamento rapido poiché il commit di unione è un discendente diretto dell'ultimo nel originrepository:

                                 v origin/master
                                 v master
         a…   b…   c…   f…       1…
bob      o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+

                                 v master
         a…   b…   c…   f…       1…
origin   o<---o<---o<---o<-------o
                   ^             |
                   |    d…   e…  |
                   +----o<---o<--+

C'è un'altra opzione per unire in git e hg, chiamata rebase , che sposterà le modifiche di Bob dopo le ultime modifiche. Dal momento che non voglio che questa risposta sia più prolissa, ti lascio invece leggere i documenti git , mercurial o bazar su questo.

Come esercizio per il lettore, prova a capire come funzionerà con un altro utente coinvolto. Allo stesso modo viene fatto come nell'esempio sopra con Bob. La fusione tra i repository è più semplice di quanto si pensi, poiché tutte le revisioni / commit sono identificabili in modo univoco.

C'è anche il problema di inviare patch tra ogni sviluppatore, che era un grosso problema in Subversion che è mitigato in git, hg e bzr da revisioni identificabili in modo univoco. Una volta che qualcuno ha unito le sue modifiche (ovvero ha effettuato un commit di merge) e lo ha inviato a tutti gli altri membri del team, spingendolo verso un repository centrale o inviando patch, non devono preoccuparsi della fusione, perché è già successo . Martin Fowler chiama questo modo di lavorare l' integrazione promiscua .

Poiché la struttura è diversa da Subversion, impiegando invece un DAG, consente la ramificazione e l'unione in modo più semplice non solo per il sistema ma anche per l'utente.


6
Non sono d'accordo con i tuoi rami == argomento del rumore. Molti rami non confondono le persone perché lo sviluppatore principale dovrebbe dire alle persone quale ramo utilizzare per le grandi funzionalità ... quindi due sviluppatori potrebbero lavorare sul ramo X per aggiungere "dinosauri volanti", 3 potrebbero lavorare su Y per "farti lanciare macchine alla gente "
Mr. Boy,

16
John: Sì, per un numero limitato di rami c'è poco rumore ed è gestibile. Ma ritorna dopo aver assistito a oltre 50 rami e tag in sovversione o in casi chiari in cui la maggior parte di essi non è in grado di dire se sono attivi o meno. Problema di usabilità dagli strumenti a parte; perché hai tutta quella lettiera nel tuo repository? Almeno in p4 (poiché lo "spazio di lavoro" di un utente è essenzialmente un ramo per utente), git o hg hai la possibilità di non far conoscere a tutti i cambiamenti che fai fino a quando non li spingi a monte, il che è un sicuro- fare attenzione a quando le modifiche sono rilevanti per gli altri.
Spoike,

24
Non capisco neanche che i tuoi "troppi rami sperimentali siano argomento di rumore, @Spoike. Abbiamo una cartella" Utenti "in cui ogni utente ha la sua cartella. Qui può ramificare tutte le volte che vuole. I rami sono economici in Subversion e se ignori le cartelle degli altri utenti (perché dovresti preoccupartene comunque), allora non vedrai rumore, ma per me la fusione in SVN non fa schifo (e lo faccio spesso, e no, non è un piccolo progetto.) Quindi forse faccio qualcosa di sbagliato;) Tuttavia la fusione di Git e Mercurial è superiore e tu l'hai sottolineato bene.
John Smithers

11
In svn è facile uccidere rami inattivi, basta eliminarli. Il fatto che le persone non rimuovano i rami inutilizzati, quindi creando disordine è solo una questione di pulizie. Potresti anche finire facilmente con molti rami temporanei in Git. Nel mio posto di lavoro usiamo una directory di livello superiore "temp-rami" oltre a quelle standard - le filiali personali e le filiali sperimentali entrano lì invece di ingombrare la directory delle filiali dove sono conservate le linee di codice "ufficiali" (non utilizzare i rami funzione).
Ken Liu

10
Ciò significa quindi che dalla sovversione v1.5 è possibile almeno unire e git può?
Sam,

29

Storicamente, Subversion è stato in grado di eseguire solo un'unione bidirezionale perché non ha memorizzato alcuna informazione di unione. Ciò implica prendere una serie di modifiche e applicarle a un albero. Anche con le informazioni di unione, questa è ancora la strategia di unione più comunemente utilizzata.

Git utilizza un algoritmo di unione a 3 vie per impostazione predefinita, che prevede la ricerca di un antenato comune alle teste da fondere e l'utilizzo della conoscenza esistente su entrambi i lati della fusione. Ciò consente a Git di essere più intelligente nell'evitare i conflitti.

Git ha anche un sofisticato codice di ricerca della ridenominazione, che aiuta anche. Essa non memorizza gruppi di modifiche o di memorizzare tutte le informazioni di tracking - semplicemente memorizza lo stato dei file in ogni commit e utilizza l'euristica per individuare rinomina e movimenti di codice, come richiesto (la memorizzazione su disco è più complicato di così, ma l'interfaccia si presenta al livello logico non espone alcun tracciamento).


4
Hai un esempio che svn ha un conflitto di unione ma git no?
Gqqnbig,

17

In parole povere, l'implementazione della fusione viene eseguita meglio in Git che in SVN . Prima dell'1.5 SVN non registrava un'azione di unione, quindi non era possibile effettuare future fusioni senza l'aiuto dell'utente che doveva fornire informazioni che SVN non aveva registrato. Con 1.5 è migliorato e in effetti il ​​modello di archiviazione SVN è leggermente più capace del DAG di Git. Ma SVN ha archiviato le informazioni di unione in una forma piuttosto contorta che consente alle fusioni di prendere molto più tempo rispetto a Git - ho osservato fattori di 300 nel tempo di esecuzione.

Inoltre, SVN afferma di tenere traccia dei nomi per favorire l'unione dei file spostati. Ma in realtà li memorizza ancora come una copia e un'azione di eliminazione separata e l'algoritmo di unione si imbatte ancora in loro in situazioni di modifica / rinomina, cioè in cui un file viene modificato su un ramo e rinominato sull'altro, e quei rami sono essere unito. Tali situazioni produrranno ancora conflitti di unione spuria e, nel caso della ridenominazione di directory, porta persino alla perdita silenziosa delle modifiche. (Le persone SVN tendono quindi a sottolineare che le modifiche sono ancora nella storia, ma ciò non aiuta molto quando non sono in un risultato di unione dove dovrebbero apparire.

Git, d'altra parte, non tiene nemmeno traccia dei nomi, ma li capisce dopo il fatto (al momento della fusione), e lo fa in modo abbastanza magico.

Anche la rappresentazione di unione SVN presenta dei problemi; in 1.5 / 1.6 potresti unire da tronco a ramo ogni volta che ti piaceva, automaticamente, ma una fusione nell'altra direzione doveva essere annunciata ( --reintegrate), e lasciato il ramo in uno stato inutilizzabile. Molto più tardi hanno scoperto che in realtà non è così e che a) è --reintegrate possibile capire automaticamente eb) sono possibili ripetute fusioni in entrambe le direzioni.

Ma dopo tutto ciò (che IMHO mostra una mancanza di comprensione di ciò che stanno facendo), sarei (OK, lo sono) molto cauto nell'usare SVN in qualsiasi scenario di ramificazione non banale, e idealmente proverei a vedere cosa pensa Git il risultato della fusione.

Altri punti evidenziati nelle risposte, come la visibilità globale forzata delle filiali in SVN, non sono rilevanti per unire le capacità (ma per l'usabilità). Inoltre, i "negozi Git cambiano mentre i negozi SVN (qualcosa di diverso)" sono per lo più fuori punto. Git memorizza concettualmente ogni commit come un albero separato (come un file tar ), quindi usa un po 'di euristica per archiviarlo in modo efficiente. Il calcolo delle modifiche tra due commit è separato dall'implementazione della memoria. Ciò che è vero è che Git memorizza il DAG storico in una forma molto più semplice che SVN fa la sua mergeinfo. Chiunque cerchi di capire quest'ultimo capirà cosa intendo.

In poche parole: Git utilizza un modello di dati molto più semplice per archiviare le revisioni rispetto a SVN, e quindi potrebbe mettere molta energia negli algoritmi di unione effettivi piuttosto che cercare di far fronte alla rappresentazione => praticamente migliore fusione.


11

Una cosa che non è stata menzionata nelle altre risposte, e che è davvero un grande vantaggio di un DVCS, è che puoi impegnarti localmente prima di spingere le tue modifiche. In SVN, quando ho apportato alcune modifiche, volevo fare il check-in e nel frattempo qualcuno aveva già eseguito un commit sullo stesso ramo, ciò significava che dovevo fare un svn updatecommit prima di poter eseguire il commit. Ciò significa che i miei cambiamenti e quelli dell'altra persona sono ora mescolati insieme, e non c'è modo di interrompere l'unione (come con git reseto hg update -C), perché non c'è impegno a cui tornare. Se l'unione non è banale, ciò significa che non è possibile continuare a lavorare sulla funzionalità prima di aver ripulito il risultato dell'unione.

Ma poi, forse questo è solo un vantaggio per le persone che sono troppo stupide per usare filiali separate (se ricordo bene, avevamo solo una filiale che è stata utilizzata per lo sviluppo nella società in cui ho usato SVN).


10

EDIT: Questo riguarda principalmente questa parte della domanda:
è in realtà dovuto a differenze intrinseche nel modo in cui funzionano i due sistemi o le implementazioni DVCS specifiche come Git / Mercurial hanno algoritmi di fusione più intelligenti rispetto a SVN?
TL; DR: quegli strumenti specifici hanno algoritmi migliori. La distribuzione ha alcuni vantaggi del flusso di lavoro, ma è ortogonale ai vantaggi della fusione.
MODIFICA FINE

Ho letto la risposta accettata. È semplicemente sbagliato.

La fusione di SVN può essere una seccatura e può anche essere ingombrante. Ma ignora come funziona effettivamente per un minuto. Non ci sono informazioni che Git conserva o può derivare che SVN non conserva o può derivare. Ancora più importante, non vi è alcun motivo per cui conservare copie separate (a volte parziali) del sistema di controllo della versione fornirà informazioni più effettive. Le due strutture sono completamente equivalenti.

Supponiamo che tu voglia fare "qualche cosa intelligente" Git è "migliore in". E la tua cosa è controllata in SVN.

Converti il ​​tuo SVN nel modulo Git equivalente, fallo in Git, quindi controlla il risultato, magari usando più commit, alcuni rami extra. Se riesci a immaginare un modo automatizzato per trasformare un problema SVN in un problema Git, Git non ha alcun vantaggio fondamentale.

Alla fine, qualsiasi sistema di controllo della versione me lo permetterà

1. Generate a set of objects at a given branch/revision.
2. Provide the difference between a parent child branch/revisions.

Inoltre, per la fusione è anche utile (o critico) sapere

3. The set of changes have been merged into a given branch/revision.

Mercurial , Git e Subversion (ora nativamente, precedentemente usando svnmerge.py) possono fornire tutte e tre le informazioni. Al fine di dimostrare qualcosa di fondamentalmente migliore con DVC, si prega di indicare alcune quarte informazioni che sono disponibili in Git / Mercurial / DVC non disponibili in SVN / VC centralizzato.

Questo non vuol dire che non sono strumenti migliori!


1
Sì, ho risposto alla domanda nei dettagli, non al titolo. svn e git hanno accesso alle stesse informazioni (in realtà svn ne ha di più), quindi svn potrebbe fare qualunque cosa git. Ma hanno preso diverse decisioni di progettazione, e in realtà non lo è. La prova sul DVC / centralizzato è che puoi eseguire git come un VC centralizzato (forse con alcune regole imposte) e puoi eseguire svn distribuito (ma fa totalmente schifo). Tuttavia, questo è troppo accademico per la maggior parte delle persone: git e hg si ramificano e si fondono meglio di svn. Questo è davvero ciò che conta quando si sceglie uno strumento :-).
Peter

5
Fino alla versione 1.5, Subversion non memorizzava tutte le informazioni necessarie. Anche con SVN post-1.5 le informazioni memorizzate sono diverse: Git memorizza tutti i genitori di un commit di unione, mentre Subversion memorizza quali revisioni sono state già unite nella succursale.
Jakub Narębski,

4
È uno strumento difficile da implementare nuovamente in un repository svn git merge-base. Con git puoi dire "rami aeb divisi alla revisione x". Ma svn archivia "i file sono stati copiati da foo a bar", quindi è necessario utilizzare l'euristica per capire che la copia su barra stava creando un nuovo ramo invece di copiare i file all'interno di un progetto. Il trucco è che una revisione in svn è definita dal numero di revisione e dal percorso di base. Anche se è possibile assumere "tronco" per la maggior parte del tempo, morde se in realtà ci sono rami.
Douglas,

2
Ri: "Non ci sono informazioni che Git conserva o può derivare che svn non mantiene o può derivare". - Ho scoperto che SVN non ricordava quando le cose erano state unite. Se ti piace tirare il lavoro dal tronco al tuo ramo e andare avanti e indietro, la fusione può diventare difficile. In Git ogni nodo nel suo grafico di revisione sa da dove proviene. Ha fino a due genitori e alcuni cambiamenti locali. Mi piacerebbe che Git fosse in grado di unire più di SVN. Se unisci in SVN ed elimini il ramo, la cronologia del ramo viene persa. Se unisci in GIT ed elimini il ramo, il grafico rimane e con esso il plugin "biasimo".
Richard Corfield,

1
Non è vero che git e mercurial hanno tutte le informazioni necessarie a livello locale, sebbene svn abbia bisogno di guardare sia i dati locali che quelli centrali per ricavare le informazioni?
Warren Dew,

8

SVN tiene traccia dei file mentre Git tiene traccia delle modifiche al contenuto . È abbastanza intelligente tenere traccia di un blocco di codice che è stato refactored da una classe / file a un'altra. Usano due approcci completamente diversi per tracciare la tua fonte.

Uso ancora SVN pesantemente, ma sono molto contento delle poche volte in cui ho usato Git.

Una bella lettura se hai tempo: perché ho scelto Git


È quello che ho letto anche io, ed è quello su cui contavo, ma in pratica non funziona.
Rolf,

Git tiene traccia del contenuto dei file, mostra solo il contenuto come modifiche
Ferrybig,

6

Ho appena letto un articolo sul blog di Joel (purtroppo il suo ultimo). Questo riguarda Mercurial, ma in realtà parla dei vantaggi dei sistemi VC distribuiti come Git.

Con il controllo della versione distribuita, la parte distribuita non è in realtà la parte più interessante. La parte interessante è che questi sistemi pensano in termini di cambiamenti, non in termini di versioni.

Leggi l'articolo qui .


5
Quello era uno degli articoli a cui stavo pensando prima di pubblicare qui. Ma "pensa in termini di cambiamenti" è un termine di marketing molto vago (ricorda che la società di Joel vende DVCS ora)
Mr. Boy

2
Ho pensato che fosse anche vago ... Ho sempre pensato che i changeset fossero parte integrante delle versioni (o piuttosto delle revisioni), il che mi sorprende che alcuni programmatori non pensino in termini di cambiamenti.
Spoike,

Per un sistema che "pensa in termini di cambiamenti", dai un'occhiata a Darcs
Max

@Max: certo, ma quando arriva la spinta, Git consegna dove Darcs è fondamentalmente doloroso quanto Subversion quando si tratta di fondersi effettivamente.
Tripleee

I tre svantaggi di Git è a) non è così buono per i binari come la gestione dei documenti dove è molto improbabile che le persone vorranno ramificarsi e fondersi b) presume che tu voglia clonare TUTTO c) memorizza anche la storia di tutto nel clone per i binari che cambiano frequentemente causando il gonfiamento del clone. Penso che un VCS centralizzato sia molto meglio per questi casi d'uso. Git è molto meglio per lo sviluppo regolare, in particolare per la fusione e la ramificazione.
Locka,
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.