Esercizio - Git con branch, merge e conflitto (applicazione web)
In questa pagina 3
Esercizio originale, nello stile delle domande pratiche sugli strumenti di sviluppo. Teoria: Strumenti di sviluppo - git, Maven e DockerTre strumenti rendono un progetto web riproducibile e condivisibile. Git tiene la storia del codice: tre aree (cartella di lavoro, area di staging, repository), il commit è un'istantanea con un hash, il branch è un puntatore mobile a un commit, HEAD indica dove si è; il merge fonde due rami (fast-forward se non c'è divergenza, altrimenti un commit di merge) e può produrre conflitti, che si risolvono a mano nei marcatori di conflitto, poi add e commit; con un remoto si usano clone, fetch, pull (fetch + merge) e push, e un push rifiutato (fetch first) richiede prima un pull; in gruppo si lavora su rami di funzionalità con commit piccoli e main sempre funzionante. Maven descrive il progetto nel pom.xml (coordinate groupId:artifactId:version, packaging war, dipendenze con scope compile, provided, runtime, test), impone la struttura src/main/java, src/main/webapp, src/test, e il ciclo di vita validate, compile, test, package, verify, install, deploy: ogni fase esegue prima tutte le precedenti, mvn clean package produce target/aule.war. Docker confeziona l'applicazione: un'immagine (strati di sola lettura, costruita da un Dockerfile) è il modello, un container è un'istanza in esecuzione; per l'esempio un Dockerfile a due fasi compila il war con Maven e lo copia in Tomcat, e docker compose avvia insieme Tomcat e PostgreSQL con un volume per i dati.Strumenti di sviluppo - git, Maven e Docker →. Le uscite dei comandi sono quelle reali di git 2.x, ottenute rieseguendo tutta la sequenza su un remoto locale; le uniche modifiche sono gli hash (che nel tuo computer saranno diversi) e l'indirizzo del remoto, sostituito da https://example.org/aule.git.
Testo
Anna e Bruno sviluppano insieme l'applicazione di prenotazione delle aule (Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →). Il repository remoto origin è vuoto. Il file schema.sql contiene le tabelle e le tre aule:
CREATE TABLE aula (id SERIAL PRIMARY KEY, nome VARCHAR(30) NOT NULL UNIQUE, posti INTEGER NOT NULL);
INSERT INTO aula (nome, posti) VALUES ('Aula A1', 80);
INSERT INTO aula (nome, posti) VALUES ('Aula B2', 45);
INSERT INTO aula (nome, posti) VALUES ('Lab Info', 30);- Anna clona il repository, crea
schema.sql, fa il primo commit e lo pubblica. Bruno clona il repository. - Anna crea il ramo
rinomina-labe vi fa due commit: rinominaLab InfoinLaboratorio di Informaticae aggiunge unREADME.md. - Nel frattempo Bruno, su
main, cambia i posti del laboratorio da 30 a 36 e pubblica. - Anna torna su
main, si aggiorna e fonde il suo ramo. Che cosa succede? Risolvere come richiesto: il laboratorio deve avere il nome nuovo e i posti nuovi. - Anna pubblica, cancella il ramo; Bruno si aggiorna.
Per ogni passo: comandi, uscita attesa, spiegazione. Poi: (a) disegnare il grafo dei commit prima e dopo la fusione; (b) dire che cosa sarebbe successo se Bruno non avesse toccato la stessa riga; (c) dire che cosa avrebbe stampato git push se Anna, al passo 4, avesse tentato di pubblicare un suo commit senza aggiornarsi.
Soluzione
Passo 1: primo commit e clone
$ git clone https://example.org/aule.git anna
warning: You appear to have cloned an empty repository.
$ cd anna
$ git status --short
?? schema.sql
$ git add schema.sql
$ git commit -m "Schema iniziale con tre aule"
[main (root-commit) 12f56a6] Schema iniziale con tre aule
1 file changed, 4 insertions(+)
create mode 100644 schema.sql
$ git push -u origin main
To https://example.org/aule.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.clonecrea la cartellaannacollegata al remotoorigin; il repository è vuoto, quindi l'avviso.??= file non tracciato.git addlo mette in staging,git commitlo registra (root-commit: primo commit, nessun genitore;12f56a6sono le prime sette cifre dell'hash).git push -u origin maincrea il ramomainsul remoto e ricorda che ilmainlocale segueorigin/main(-u).
Bruno: git clone https://example.org/aule.git bruno, e git log --oneline mostra la stessa storia (12f56a6 Schema iniziale con tre aule).
Passo 2: il ramo di Anna
$ git switch -c rinomina-lab
Switched to a new branch 'rinomina-lab'
$ sed -i "s/('Lab Info', 30)/('Laboratorio di Informatica', 30)/" schema.sql
$ git diff
diff --git a/schema.sql b/schema.sql
index edf2ef3..7c75485 100644
--- a/schema.sql
+++ b/schema.sql
@@ -1,4 +1,4 @@
CREATE TABLE aula (id SERIAL PRIMARY KEY, nome VARCHAR(30) NOT NULL UNIQUE, posti INTEGER NOT NULL);
INSERT INTO aula (nome, posti) VALUES ('Aula A1', 80);
INSERT INTO aula (nome, posti) VALUES ('Aula B2', 45);
-INSERT INTO aula (nome, posti) VALUES ('Lab Info', 30);
+INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 30);
$ git commit -am "Rinomina Lab Info in Laboratorio di Informatica"
$ git add README.md
$ git commit -m "Aggiunge il README"
$ git log --oneline --graph --all --decorate
* 451ec8c (HEAD -> rinomina-lab) Aggiunge il README
* 641258f Rinomina Lab Info in Laboratorio di Informatica
* 12f56a6 (origin/main, main) Schema iniziale con tre aulegit switch -ccrea il ramo e vi si sposta.git diffmostra la modifica non ancora in staging (-riga tolta,+riga aggiunta).commit -amfaadddei file già tracciati e commit in un colpo solo; ilREADME.mdè nuovo e va aggiunto a parte.- Il grafo è una linea:
maineorigin/mainsono rimasti al primo commit,rinomina-lab(conHEAD) ha due commit in più.
Passo 3: Bruno modifica la stessa riga
$ sed -i "s/('Lab Info', 30)/('Lab Info', 36)/" schema.sql
$ git commit -am "Il laboratorio ha 36 posti"
$ git push
To https://example.org/aule.git
12f56a6..9d1f0b7 main -> mainIl push riesce: Bruno ha il remoto aggiornato fino al commit 12f56a6 e lo avanza (fast-forward) a 9d1f0b7. Ora main è avanzato con un commit che Anna non ha.
Passo 4: Anna si aggiorna e fonde
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git pull
From https://example.org/aule
12f56a6..9d1f0b7 main -> origin/main
Updating 12f56a6..9d1f0b7
Fast-forward
schema.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --all --decorate
* 9d1f0b7 (HEAD -> main, origin/main, origin/HEAD) Il laboratorio ha 36 posti
| * 451ec8c (rinomina-lab) Aggiunge il README
| * 641258f Rinomina Lab Info in Laboratorio di Informatica
|/
* 12f56a6 Schema iniziale con tre auleIl pull porta in avanti main locale con un fast-forward (il main di Anna non aveva commit suoi). Il grafo ora è divergente: dal commit 12f56a6 partono due strade, main (Bruno) e rinomina-lab (Anna). È quello che chiedeva il punto (a), "prima".
$ git merge rinomina-lab
Auto-merging schema.sql
CONFLICT (content): Merge conflict in schema.sql
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
Your branch is up to date with 'origin/main'.
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
new file: README.md
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: schema.sqlConflitto. I due rami hanno cambiato la stessa riga di schema.sql (l'ultima) in modo diverso; git non sceglie. Il README.md, toccato solo da un ramo, è stato fuso da solo (new file già in staging). git status elenca schema.sql come both modified. Il file contiene i marcatori:
CREATE TABLE aula (id SERIAL PRIMARY KEY, nome VARCHAR(30) NOT NULL UNIQUE, posti INTEGER NOT NULL);
INSERT INTO aula (nome, posti) VALUES ('Aula A1', 80);
INSERT INTO aula (nome, posti) VALUES ('Aula B2', 45);
<<<<<<< HEAD
INSERT INTO aula (nome, posti) VALUES ('Lab Info', 36);
=======
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 30);
>>>>>>> rinomina-lab- Tra
<<<<<<< HEADe=======: la versione dimain(il ramo in cui ci si trova), di Bruno: 36 posti. - Tra
=======e>>>>>>> rinomina-lab: la versione del ramo che si fonde, di Anna: nome nuovo.
Risoluzione. La richiesta è nome nuovo e posti nuovi, quindi nessuna delle due versioni va bene da sola: si scrive a mano la riga giusta e si cancellano i tre marcatori. Il file diventa:
CREATE TABLE aula (id SERIAL PRIMARY KEY, nome VARCHAR(30) NOT NULL UNIQUE, posti INTEGER NOT NULL);
INSERT INTO aula (nome, posti) VALUES ('Aula A1', 80);
INSERT INTO aula (nome, posti) VALUES ('Aula B2', 45);
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 36);$ git add schema.sql
$ git status
On branch main
Your branch is up to date with 'origin/main'.
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
new file: README.md
modified: schema.sql
$ git commit --no-edit
[main 0c2255f] Merge branch 'rinomina-lab'
$ git log --oneline --graph --decorate
* 0c2255f (HEAD -> main) Merge branch 'rinomina-lab'
|\
| * 451ec8c (rinomina-lab) Aggiunge il README
| * 641258f Rinomina Lab Info in Laboratorio di Informatica
* | 9d1f0b7 (origin/main, origin/HEAD) Il laboratorio ha 36 posti
|/
* 12f56a6 Schema iniziale con tre aulegit add segna il conflitto come risolto; git status dice che si sta ancora fondendo e che il commit conclude l'operazione. Il commit di merge 0c2255f ha due genitori (9d1f0b7 e 451ec8c): il grafo del punto (a), "dopo", è un rombo che si richiude. Prima di pubblicare Anna dovrebbe ricompilare e provare l'applicazione: la fusione senza conflitti testuali non garantisce che il programma funzioni.
Passo 5: pubblicazione e pulizia
$ git push
To https://example.org/aule.git
9d1f0b7..0c2255f main -> main
$ git branch -d rinomina-lab
Deleted branch rinomina-lab (was 451ec8c).Bruno, nella sua copia:
$ git pull
From https://example.org/aule
9d1f0b7..0c2255f main -> origin/main
Updating 9d1f0b7..0c2255f
Fast-forward
README.md | 3 +++
schema.sql | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
create mode 100644 README.md
$ tail -1 schema.sql
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 36);Per Bruno è un fast-forward: il suo main è un antenato del nuovo, quindi basta spostare il puntatore. Ora i due repository sono allineati.
Risposte alle domande
(b) Se Bruno non avesse toccato la stessa riga. Per esempio avesse cambiato la riga di Aula B2, o il README.md: le modifiche sono su righe diverse, git le fonde da solo. git merge rinomina-lab avrebbe stampato Auto-merging schema.sql e Merge made by the 'ort' strategy., creato il commit di merge senza chiedere nulla, e non ci sarebbe stato nessun marcatore. Se invece main non fosse avanzato, sarebbe stato un fast-forward senza commit di merge.
(c) Un push senza aggiornarsi. Se Anna, mentre il remoto ha 9d1f0b7 che lei non ha, tenta git push di un suo commit su main:
$ git push
To https://example.org/aule.git
! [rejected] main -> main (fetch first)
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.Il push viene rifiutato (rejected, fetch first): git impedisce di sovrascrivere il lavoro di Bruno. Si risolve con git pull, che qui produce proprio lo stesso conflitto (se i due commit toccano la stessa riga), da risolvere come sopra, e poi di nuovo git push. Non si usa --force.
Annullare tutto a metà. Durante il conflitto, git merge --abort riporta main e la cartella di lavoro com'erano prima del git merge.
Come ridurre i conflitti in un progetto di gruppo: rami brevi, git pull di main nel proprio ramo spesso, persone diverse su file diversi, nessuna riformattazione di interi file, commit piccoli.
Versione ripasso
Scenario. Anna e Bruno condividono origin. schema.sql ha tre INSERT per le aule Aula A1 (80), Aula B2 (45), Lab Info (30).
- Anna:
git clone, creaschema.sql,git add schema.sql,git commit -m "Schema iniziale con tre aule"([main (root-commit) 12f56a6]),git push -u origin main. Bruno clona. - Anna:
git switch -c rinomina-lab;Lab InfoLaboratorio di Informatica;git diff;git commit -am ...;git add README.md;git commit -m "Aggiunge il README". - Bruno, su
main:Lab Info30 36 posti; commit;git push(12f56a6..9d1f0b7 main -> main). - Anna:
git switch main,git pull(Fast-forward a9d1f0b7), grafo divergente;git merge rinomina-lab.
Auto-merging schema.sql
CONFLICT (content): Merge conflict in schema.sql
Automatic merge failed; fix conflicts and then commit the result.- Perché c'è conflitto: i due rami hanno cambiato la stessa riga;
README.mdè stato fuso da solo.git status:both modified: schema.sql. - Marcatori:
<<<<<<< HEAD(versione dimain, 36 posti),=======,>>>>>>> rinomina-lab(versione di Anna, nome nuovo).
<<<<<<< HEAD
INSERT INTO aula (nome, posti) VALUES ('Lab Info', 36);
=======
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 30);
>>>>>>> rinomina-lab- Risoluzione: modificare il file a mano (nome nuovo e posti nuovi) senza marcatori;
git add schema.sql;git commit --no-edit([main 0c2255f] Merge branch 'rinomina-lab').
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 36);- Grafo dopo: il commit di merge
0c2255fha due genitori (9d1f0b7e451ec8c); prima della fusione il grafo era divergente da12f56a6. Ricompilare e provare prima di pubblicare. - Passo 5:
git push(9d1f0b7..0c2255f),git branch -d rinomina-lab. Bruno:git pullè un fast-forward (README.mdeschema.sql).
Risposte.
- Righe diverse nessun conflitto:
Auto-merging schema.sqleMerge made by the 'ort' strategy.. Semainnon fosse avanzato fast-forward, senza commit di merge. - Push senza aggiornarsi:
! [rejected] main -> main (fetch first); si fagit pull(può dare conflitto), si risolve, poigit push. Mai--forcesu rami condivisi. git merge --abortannulla la fusione in corso.- Ridurre i conflitti: rami brevi,
pullfrequenti, file diversi a persone diverse, commit piccoli.
Errori tipici
- Lasciare i marcatori nel file, oppure scegliere una delle due versioni senza considerare la richiesta.
- Dimenticare
git adddopo aver risolto (il merge resta aperto). - Pubblicare senza ricompilare dopo il merge.
- Usare
--forcedopo un push rifiutato.