Salta al contenuto
Note per Studenti Esercizio - Git con branch, merge e conflitto (applicazione web)

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:

sql
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);
  1. Anna clona il repository, crea schema.sql, fa il primo commit e lo pubblica. Bruno clona il repository.
  2. Anna crea il ramo rinomina-lab e vi fa due commit: rinomina Lab Info in Laboratorio di Informatica e aggiunge un README.md.
  3. Nel frattempo Bruno, su main, cambia i posti del laboratorio da 30 a 36 e pubblica.
  4. 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.
  5. 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

bash
$ 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'.
  • clone crea la cartella anna collegata al remoto origin; il repository è vuoto, quindi l'avviso.
  • ?? = file non tracciato. git add lo mette in staging, git commit lo registra (root-commit: primo commit, nessun genitore; 12f56a6 sono le prime sette cifre dell'hash).
  • git push -u origin main crea il ramo main sul remoto e ricorda che il main locale segue origin/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

bash
$ 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 aule
  • git switch -c crea il ramo e vi si sposta. git diff mostra la modifica non ancora in staging (- riga tolta, + riga aggiunta). commit -am fa add dei file già tracciati e commit in un colpo solo; il README.md è nuovo e va aggiunto a parte.
  • Il grafo è una linea: main e origin/main sono rimasti al primo commit, rinomina-lab (con HEAD) ha due commit in più.

Passo 3: Bruno modifica la stessa riga

bash
$ 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 -> main

Il 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

bash
$ 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 aule

Il 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".

bash
$ 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.sql

Conflitto. 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 <<<<<<< HEAD e =======: la versione di main (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);
bash
$ 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 aule

git 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

bash
$ 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:

bash
$ 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:

bash
$ 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).

  1. Anna: git clone, crea schema.sql, git add schema.sql, git commit -m "Schema iniziale con tre aule" ([main (root-commit) 12f56a6]), git push -u origin main. Bruno clona.
  2. Anna: git switch -c rinomina-lab; Lab Info →\to Laboratorio di Informatica; git diff; git commit -am ...; git add README.md; git commit -m "Aggiunge il README".
  3. Bruno, su main: Lab Info 30 →\to 36 posti; commit; git push (12f56a6..9d1f0b7 main -> main).
  4. Anna: git switch main, git pull (Fast-forward a 9d1f0b7), 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 di main, 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 0c2255f ha due genitori (9d1f0b7 e 451ec8c); prima della fusione il grafo era divergente da 12f56a6. Ricompilare e provare prima di pubblicare.
  • Passo 5: git push (9d1f0b7..0c2255f), git branch -d rinomina-lab. Bruno: git pull è un fast-forward (README.md e schema.sql).

Risposte.

  • Righe diverse →\to nessun conflitto: Auto-merging schema.sql e Merge made by the 'ort' strategy.. Se main non fosse avanzato →\to fast-forward, senza commit di merge.
  • Push senza aggiornarsi: ! [rejected] main -> main (fetch first); si fa git pull (può dare conflitto), si risolve, poi git push. Mai --force su rami condivisi.
  • git merge --abort annulla la fusione in corso.
  • Ridurre i conflitti: rami brevi, pull frequenti, 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 add dopo aver risolto (il merge resta aperto).
  • Pubblicare senza ricompilare dopo il merge.
  • Usare --force dopo un push rifiutato.

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 →.

Teoria collegata