Abbiamo accennato, in precedenza, alla possibilità che durante l’esecuzione di un git pull, git merge o git rebase in Git si possano verificare dei conflitti che impediscono a Git di portare a termine automaticamente l’operazione.
Un conflitto nell’accezione dei version control system è una situazione per la quale il sistema sta provando a integrare due diverse history che hanno entrambe apportato modifiche alle stesse righe di un file.
Il processo di merge può andare in uno stato di conflitto in due diversi momenti:
- all’avvio del merge – avviene quando ci sono modifiche nella working copy o nella staging area; tali modifiche “pending” non possono essere gestite da Git e sarà compito dello sviluppatore “pulire” il proprio spazio di lavoro con git stash, git checkout, git commit o git reset
- durante il merge – in tal caso il conflitto è effettivamente tra il branch in uso e quello che si sta mergiando, conflitto che riguarda modifiche apportate nei commit dei due branch.
NOTA: come visto nella precedente sezione, nel merge portiamo le modifiche presenti in un branch dentro al branch attualmente presente nella working copy. Nel seguito di questa sezione indicheremo tali branch rispettivamente come “incoming” e “current”.
Proviamo a fare qualche “esercizio” di merge conflict in Git. Proviamo a realizzare il caso in cui vogliamo riportare sul branch main delle modifiche fatte da noi su un branch, nella situazione in cui un altro sviluppatore web ha aggiunto un commit sul branch main successivamente alla creazione del nostro branch di sviluppo.
Merge conflict
Creiamo un repository a cui aggiungiamo un file con un commit (con l’opzione –author per simulare meglio chi ha committato cosa).
$ mkdir git-merge-test $ cd git-merge-test $ git init . $ echo "initial content to edit later" > merge.txt $ git add merge.txt $ git commit -m "initial content" --author="Someone <someone@example.com>" [main (root-commit) 0a99708] initial content Author: Someone <someone@example.com> 1 file changed, 1 insertion(+) create mode 100644 merge.txt
Creiamo un nuovo branch, apportiamo modifiche al file e salviamo tali modifiche in un commit.
$ git checkout -b merge-me Switched to a new branch 'merge-me' $ echo "new content to merge later" > merge.txt $ git commit -am "edited the content for conflict" --author "Me <me@example.com>" [merge-me 15416df] edited the content for conflict Author: Me <me@example.com> 1 file changed, 1 insertion(+), 1 deletion(-)
Torniamo sul branch main e aggiungiamo una nuova riga di testo al file, sempre poi salvando in un commit.
$ git checkout main Switched to branch 'main' $ echo "content added to previous" >> merge.txt $ git commit -am "appended content to merge.txt" --author="Frank <frank@example.com>" [main 90640eb] appended content to merge.txt Author: Frank <frank@example.com> 1 file changed, 1 insertion(+)
Proviamo, quindi, a fare merge delle modifiche nel branch merge-me (incoming) nel branch main (current)
$ git merge merge-me Auto-merging merge.txt CONFLICT (content): Merge conflict in merge.txt Automatic merge failed; fix conflicts and then commit the result. $ git status On branch main You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: merge.txt no changes added to commit (use "git add" and/or "git commit -a")
Provando ad eseguire git merge viene segnalata la presenza di un conflitto. Il processo di merge si interrompe, lasciando la propria copia in uno stato di attesa di risoluzione.
Risolvere un merge conflict in Git
Con il comando git status possiamo evidenziare la presenza di un conflitto durante l’esecuzione di un merge. Vediamo come scoprire maggiori dettagli su cosa esattamente è cambiato tra le due versioni della history e come intervenire.
Nell’esempio di conflitto “forzato” sappiamo che il problema è nel file merge.txt, che ha ricevuto modifiche nelle stesse righe in entrambi i branch.
Nel momento in cui il merge si è interrotto, nel file in conflitto ci sono le modifiche provenienti da entrambe le versioni del file:
$ cat merge.txt <<<<<<< HEAD initial content to edit later content added to previous ======= new content to merge later >>>>>>> merge-me
Di particolare interesse sono i marker di conflitto inseriti da Git che ci permettono di sapere che:
- la righe tra <<<<<<< e ======= contengono le righe in conflitto nel file come erano nel commit del branch “current”
- le righe tra ======= e >>>>>>> contengono le righe in conflitto nel file come erano nel commit del branch “incoming”.
Ci sono tre modi per risolvere i conflitti in un file e procedere con il merge, che ovviamente possono e debbono essere scelti caso per caso:
- accettare in toto le modifiche che erano già nel branch “current” (nel nostro caso main) con git checkout –ours merge.txt
- accettare in toto le modifiche che vengono dal branch “incoming” (nel nostro caso new_branch_to_merge_later) con git checkout –their merge.txt
- modificare il file in modo che contenga un contenuto corretto e in linea con entrambe le modifiche, dichiarare che è stato risolto il conflitto con git add merge.txt e proseguire con il merge tramite un commit.
Se volessimo procedere con questa ultima opzione, dovremmo modificare il file fino a raggiungere un contenuto adeguato e poi:
$ cat merge.txt new content to merge later content added to previous $ git status On branch main You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: merge.txt no changes added to commit (use "git add" and/or "git commit -a") $ git add merge.txt $ git status On branch main All conflicts fixed but you are still merging. (use "git commit" to conclude merge) Changes to be committed: modified: merge.txt
Una volta effettuato il commit, la history del nostro repository sarà la seguente:
$ git log --pretty=format:'%h %s (%an)' --date=short --graph * c9802c8 merged content due conflict (Me) |\ | * 15416df edited the content for conflict (Me) * | 90640eb appended content to merge.txt (Frank) |/ * 0a99708 initial content (Someone)
Notare che in questo caso, il nostro commit iniziale sul branch è rimasto intatto (ce lo dice il fatto che abbia ancora lo stesso hash, 15416df) e le modifiche necessarie a risolvere il conflitto sono state incluse nel commit di merge.
Possiamo, infatti, confrontare la differenza tra gli ultimi due commit sul branch in uso con
$ git diff HEAD^1 diff --git a/merge.txt b/merge.txt index c560b6f..732761b 100644 --- a/merge.txt +++ b/merge.txt @@ -1,2 +1,2 @@ -initial content to edit later +new content to merge later content added to previous