Git
Chapters ▾ 2nd Edition

3.2 Διακλαδώσεις στο Git - Βασικές έννοιες διακλαδώσεων και συγχωνεύσεων

Βασικές έννοιες διακλαδώσεων και συγχωνεύσεων

Ας δούμε ένα απλό παράδειγμα διακλάδωσης και συγχώνευσης με μία ροή εργασίας που είναι πιθανό να αντιμετωπίσουμε στην πραγματικότητα. Θα ακολουθήσουμε τα παρακάτω βήματα:

  1. θα κάνουμε αλλαγές σε μία ιστοσελίδα.

  2. θα δημιουργήσουμε έναν κλάδο για μία νέα ιστορία την οποία επεξεργαζόμαστε.

  3. θα κάνουμε αλλαγές σε αυτόν τον κλάδο.

Σε αυτό το στάδιο θα δεχτούμε ένα τηλεφώνημα ότι υπάρχει ένα άλλο κρίσιμο πρόβλημα και πρέπει να αναπτύξουμε μία άμεση λύση. Θα κάνουμε τα παρακάτω:

  1. Θα μεταβούμε στον κλάδο παραγωγής.

  2. θα δημιουργήσουμε έναν κλάδο στον οποίο θα προσθέσουμε τον κώδικά μας που λύνει το πρόβλημα.

  3. Αφού ο κώδικάς μας δοκιμαστεί, θα συγχωνεύσουμε τον κλάδο που περιέχει τον νέο αυτό κώδικα και θα τον ωθήσουμε στην παραγωγή.

  4. Θα επιστρέψουμε στην αρχική ιστορία μας και θα συνεχίσουμε να την επεξεργαζόμαστε.

Διακλαδώσεις —τα βασικά

Αρχικά ας υποθέσουμε ότι δουλεύουμε σε ένα έργο και έχουμε κάνει ήδη μερικές υποβολές στον κλάδο master.

Ένα απλό ιστορικό υποβολών.
Figure 18. Ένα απλό ιστορικό υποβολών

Αποφασίσαμε ότι θα δουλέψουμε στο πρόβλημα #53 του συστήματος παρακολούθησης προβλημάτων που χρησιμοποιεί η εταιρία μας. Για να δημιουργήσουμε έναν κλάδο και μεταβούμε σε αυτόν συγχρόνως, μπορούμε να τρέξουμε την εντολή git checkout με την επιλογή -b:

$ git checkout -b iss53
Switched to a new branch "iss53"

Η παραπάνω εντολή είναι συντομογραφία για τις:

$ git branch iss53
$ git checkout iss53
Δημιουργία ενός νέου δείκτη κλάδου.
Figure 19. Δημιουργία ενός νέου δείκτη κλάδου

Επεξεργαζόμαστε την ιστοσελίδα μας και κάνουμε μερικές υποβολές. Με τις υποβολές ο κλάδος iss53 ωθείται, διότι τον έχουμε κάνει checkout (με άλλα λόγια έχουμε μεταβεί σε αυτόν), δηλαδή ο HEAD δείχνει σε αυτόν τον κλάδο:

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
Ο κλάδος `iss53` μετατοπίστηκε εξαιτίας των αλλαγών μας.
Figure 20. Ο κλάδος iss53 μετατοπίστηκε εξαιτίας των αλλαγών μας

Τώρα λαμβάνουμε το τηλεφώνημα ότι υπάρχει ένα άλλο επείγον πρόβλημα στην ιστοσελίδα και πρέπει να το αντιμετωπίσουμε άμεσα. Χάρη στο Git, δεν είναι απαραίτητο να αναπτύξουμε την επίλυση του προβλήματος παράλληλα με τις αλλαγές που έχουμε κάνει στον κλάδο iss53 και δεν χρειάζεται να καταβάλλουμε μεγάλη προσπάθεια ώστε να αναιρέσουμε όλες τις αλλαγές που έχουμε κάνει και να δουλέψουμε στο επείγον πρόβλημα και να εφαρμόσουμε τη λύση μας σε ό,τι βρίσκεται εκείνη τη στιγμή στη γραμμή της παραγωγής. Το μόνο που έχουμε να κάνουμε είναι να μεταβούμε στον κλάδο master.

Ωστόσο πρέπει να τονιστεί ότι αν πριν το κάνουμε αυτό υπάρχουν στον κατάλογο εργασίας μας ή στο στάδιο καταχώρισης αλλάγες που δεν έχουν υποβληθεί και έρχονται σε σύγκρουση με τον κλάδο στον οποίο θέλουμε να μεταβούμε, το Git δεν θα μας αφήσει να αλλάξουμε κλάδο. Το καλύτερο είναι να έχουμε μία καθαρή κατασταση εργασίας όταν μεταβαίνουμε από έναν κλάδο σε άλλο. Υπάρχουν τρόποι να παρακάμψουμε αυτήν τη συμπεριφορά (με τις εντολές git stash και git commit -amend που θα καλύψουμε στη συνέχεια, στην ενότητα stash και clean. Προς το παρόν, ας υποθέσουμε ότι έχουμε υποβάλλει όλες τις αλλαγές μας, ώστε να μπορούμε να μεταβαίνουμε από και προς τον κλάδο master χωρίς προβλήματα:

$ git checkout master
Switched to branch 'master'

Σε αυτό το σημείο, ο κατάλογος εργασίας του έργου μας βρίσκεται ακριβώς στην κατάσταση στην οποία βρισκόταν πριν ξεκινήσουμε να δουλεύουμε για το πρόβλημα #53 και μπορούμε να εστιάσουμε στην επίλυση του επείγοντος προβλήματος. Αυτό είναι ένα σημαντικό σημείο που αξίζει να θυμόμαστε: όταν μεταπηδούμε από έναν κλάδο σε άλλο, το Git επαναφέρει τον κατάλογο εργασίας στην κατάσταση που είχε την τελευταία φορά που είχατε κάνει υποβολή σε αυτόν τον κλάδο. Προσθέτει, διαγράφει και τροποποιεί αρχεία αυτόματα ώστε να βεβαιωθεί ότι το αντίγραφο εργασίας μας είναι ίδιο με την κατάσταση του κλάδου αμέσως μετά την τελευταία υποβολή σε αυτόν τον κλάδο.

Στη συνέχεια, πρέπει να δουλέψουμε στο επείγον πρόβλημα. Ας φτιάξουμε έναν κλάδο hotfix στον οποίο θα δουλέψουμε μέχρι να το λύσουμε:

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
 1 file changed, 2 insertions(+)
Κλάδος `hotfix` που βασίζεται στον κλάδο `master`.
Figure 21. Κλάδος hotfix που βασίζεται στον κλάδο master

Τώρα μπορούμε να τρέξουμε τις δοκιμές μας, να βεβαιωθούμε ότι ο κώδικάς μας κάνει αυτό που θέλουμε, να τον συγχωνεύσουμε με τον κλάδο master και να τον ωθήσουμε στην παραγωγή. Το τελευταίο το κάνουμε με την εντολή git merge:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

Σε αυτήν τη συγχώνευση υπάρχει η έκφραση fast-forward. Επειδή η υποβολή στην οποία έδειχνε ο κλάδος τον οποίο συγχωνεύσαμε ήταν αμέσως μετά την υποβολή στην οποία βρισκόμαστε τώρα, το Git απλά μετατοπίζει τον δείκτη προς τα μπροστά. Με άλλα λόγια όταν προσπαθούμε να συγχωνεύσουμε μία υποβολή με μία άλλη υποβολή στην οποία μπορούμε να φτάσουμε ακολουθώντας το ιστορικό της πρώτης, το Git απλοποιεί τη διαδικασία ωθώντας τον δείκτη σε εκείνο το σημείο, διότι δεν υπάρχει άλλη αποκλίνουσα εργασία που θα πρέπει να συγχωνευτεί —αυτό ονομάζεται “ταχυπροώθηση” (“fast-forward”).

Η αλλαγή μας τώρα υπάρχει στο στιγμιότυπο της υποβολής στην οποία δείχνει ο κλάδος master και μπορούμε να ωθήσουμε το επίθεμά μας.

Ο κλάδος `master` ωθείται στον κλάδο `hotfix`.
Figure 22. Ο κλάδος master ταχυπροωθείται στον κλάδο hotfix.

Αφού ο σημαντικότατος διορθωτικός μας κώδικας έχει ωθηθεί στην παραγωγή, είμαστε έτοιμοι να επανέλθουμε στην εργασία την οποία κάνατε πριν μας διακόψει το τηλεφώνημα. Πριν όμως συνεχίσουμε, θα διαγράψουμε τον κλάδο hotfix, διότι δεν τον χρειάζόμαστε πλέον —ο κλάδος master δείχνει ακριβώς στην ίδια θέση. Μπορούμε να τον διαγράψουμε με την επιλογή -d στην εντολή git branch:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

Τώρα μπορούμε να επανέλθουμε στον κλάδο εργασίας του προβλήματος #53 και να συνεχίσουμε να δουλεύουμε σ' αυτό.

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)
Η εργασία συνεχίζει στον κλάδο `iss53`.
Figure 23. Η εργασία συνεχίζει στον κλάδο iss53

Σε αυτό το σημείο αξίζει να σημειωθεί ότι οι αλλαγές που κάναμε στον κλάδο hotfix δεν περιέχονται στα αρχεία του κλάδου iss53. Αν θέλουμε να τα ενσωματώσουμε, μπορούμε να συγχωνεύσουμε τον κλάδο master στον κλάδο iss53 τρέχοντας την εντολή git merge master ή μπορούμε να αναβάλουμε την ενσωμάτωση αυτών των αλλαγών μέχρι να αποφασίσουμε να έλξουμε τον κλάδο iss53 μέσα στον κλάδο master αργότερα.

Συγχωνεύσεις —τα βασικά

Ας υποθέσουμε τώρα ότι έχουμε αποφασίσει ότι η εργασία μας για το πρόβλημα #53 έχει ολοκληρωθεί και είναι έτοιμη να συγχωνευτεί στον κλάδο master. Για να το κάνουμε αυτό, αρκεί να συγχωνεύσουμε τον κλάδο iss53 στον κλάδο master, λίγο-πολύ με τον ίδιο τρόπο που συγχωνεύσατε τον κλάδο hotfix προηγουμένως. Το μόνο που έχουμε να κάνουμε είναι να μεταβούμε στον κλάδο στον οποίο θέλουμε να ενσωματώσουμε τον άλλο κλάδο και να τρέξουμε την εντολή git merge:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

Το μήνυμα στην οθόνη διαφέρει λίγο από εκείνο που πήραμε όταν συγχωνεύσαμε τον κλάδο hotfix προηγουμένως. Σε αυτήν την περίπτωση, το ιστορικό των αλλαγών απέκλινε σε κάποιο παλιότερο σημείο. Επειδή η υποβολή στον κλάδο στον οποίο βρίσκόμαστε δεν είναι άμεσος πρόγονος του κλάδου τον οποίο ενσωματώνουμε, το Git πρέπει να κάνει λίγη δουλίτσα. Σε αυτήν την περίπτωση, το Git κάνει μία απλή τριμερή συγχώνευση, χρησιμοποιώντας τα στιγμιότυπα στο τέλος του κάθε κλάδου και τον κοινό πρόγονο των δύο.

Τρία στιγμιότυπα που χρησιμοποιούνται σε μία τυπική συγχώνευση.
Figure 24. Τρία στιγμιότυπα που χρησιμοποιούνται σε μία τυπική συγχώνευση

Αντί, λοιπόν, το Git να μετακινήσει τον δείκτη του κλάδου προς τα εμπρός, δημιουργεί ένα νέο στιγμιότυπο που προκύπτει από αυτήν την τριμερή συγχώνευση και αυτόμάτως δημιουργεί μία νέα υποβολή που δείχνει σε αυτήν τη συγχώνευση. Αυτό ονομάζεται υποβολή συγχώνευσης (merge commit) και έχει την ιδιαιτερότητα ότι έχει περισσότερους από έναν γονείς.

Μία υποβολή συγχώνευσης.
Figure 25. Μία υποβολή συγχώνευσης

Αξίζει να σημειωθεί ότι το ίδιο το Git αποφασίζει ποιος είναι ο κοινός πρόγονος που αποτελεί την καλύτερη δυνατή βάση για τη συγχώνευση· αυτή είναι μία διαφορά ανάμεσα στο Git και παλιότερα εργαλεία όπως το CVS ή το Subversion (πριν την έκδοση 1.5), στα οποία ο χρήστης που έκανε τη συγχώνευση έπρεπε ο ίδιος να βρει ποια πρότερη κατάσταση ήταν η καλύτερη δυνατή βάση. Αυτό κάνει τις συγχωνεύσεις στο Git πολύ ευκολότερες από ότι σε αυτά τα παλιότερα συστήματα.

Τώρα που η εργασία μας έχει συγχωνευτεί, δεν χρειάζόμαστε πλέον τον κλάδο iss53. Μπορούμε να κλείσουμε το ζήτημα στο σύστημα παρακολούθησης προβλημάτων μας και να διαγράψουμε τον κλάδο:

$ git branch -d iss53

Βασικές συγκρούσεις συγχωνεύσεων

Ενίοτε, η διαδικασία συγχώνευσης δεν εξελισσεται τόσο ομαλά. Αν έχουμε τροποποιήσει το ίδιο σημείο του ίδιου αρχείου με διαφορετικό τρόπο στους δύο κλάδους που συγχωνεύουμε, το Git δεν θα μπορέσει να τους συγχωνεύσει καθαρά. Αν η λύση μας για το πρόβλημα #53 και η λύση μας για το επείγον πρόβλημα τροποποίησαν το ίδιο τμήμα ενός αρχείου, θα πάρουμε ένα μήνυμα σύγκρουσης συγχώνευσης περίπου σαν το παρακάτω:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Το Git δεν μπόρεσε να δημιουργήσει αυτόματα μία νέα υποβολή συγχώνευσης. Διέκοψε τη διαδικασία, ώστε να επιλύσουμε τη σύγκρουση. Αν θέλουμε να δούμε ποια αρχεία δεν έχουν συγχωνευτεί σε οποιοδήπτε σημείο μετά από μία σύγκρουση συγχώνευσης, τρέχουμε την εντολή git status:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

Ο,τιδήποτε εμπλέκεται σε σύγκρουση συγχώνευσης και δεν έχει επιλυθεί καταγράφεται ως unmerged. Το Git προσθέτει τυποποιημένους σημειωτές “επίλυσης σύγκρουσης” στα αρχεία που εμπλέκονται σε συγκρούσεις, ώστε να τα ανοίξουμε ιδιοχείρως και να επιλύσουμε αυτές τις διαφορές. Το αρχείο μας θα περιέχει ένα τμήμα που θα φαίνεται κάπως έτσι:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

Προκειμένου να επιλύσουμε τη σύγκρουση, πρέπει είτε να επιλέξουμε τη μία ή την άλλη έκδοση είτε να συγχωνεύσουμε τα περιεχόμενα οι ίδιοι. Για παράδειγμα, μπορεί να θέλουμε να επιλύσουμε αυτήν τη σύγκρουση αντικαθιστώντας ολόκληρο το τμήμα με το παρακάτω:

<div id="footer">
please contact us at email.support@github.com
</div>

Αυτή η επίλυση της σύγκρουσης περιέχει λίγο από κάθε τμήμα και οι γραμμές που περιέχουν τα <<<<<<<, ======= και >>>>>>> έχουν αφαιρεθεί εντελώς. Αφού έχουμε επιλύσει όλα τα τμήματα σε κάθε αρχείο που εμπλέκεται σε σύγκρουση, τρέχουμε git add σε καθένα από αυτά τα αρχεία, ώστε να επισημανθεί ως επιλυμένο. Η προσθήκη ενός αρχείου στο στάδιο καταχώρισης το επισημαίνει ως επιλυμένο.

Αν θέλουμε αν χρησιμοποιήσουμε κάποιο γραφικό εργαλείο για να επιλύσουμε αυτές τις συγκρούσεις, τρέξτε git mergetool για να εκκινήσουμε ένα κατάλληλο γραφικό εργαλείο συγχώνευσης που θα μας καθοδηγήσει ανάμεσα στις συγκρούσεις:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

Αν θέλουμε να χρησιμοποιήσουμε κάποιο εργαλείο συγχώνευσης διαφορετικό από το προεπιλεγμένο (το Git έχει επιλέξει το opendiff σε αυτήν την περίπτωση, διότι η τρέξαμε την εντολή σε Mac), μπορούμε να δούμε όλα τα εργαλεία που υποστηρίζονται στο πάνω μέρος μετά από το one of the following tools: Απλά γράφουμε το όνομα του εργαλείου που θέλουμε να χρησιμοποιήσουμε.

Note

Πιο προχωρημένα εργαλεία για να επιλύσουμε περίπλοκες συγκρούσεις συγχωνεύσεων, θα αναφέρουμε στην ενότητα Συγχωνεύσεις για προχωρημένους.

Αφού βγούμε από το εργαλείο συγχώνευσης, το Git μάς ρωτάει αν η συγχώνευση ήταν επιτυχής. Αν του πούμε ότι ήταν, ωθεί το αρχείο στο στάδιο καταχώρισης ώστε να επισημανθεί ως επιλυμένο. Μπορούμε να τρέξουμε την εντολή git status ξανά για να επιβεβαιώσουμε ότι όλες οι συγκρούσεις έχουν επιλυθεί:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   index.html

Αν είμαστε ευχαριστημένοι με το αποτέλεσμα και επιβεβαιώσουμε ότι όλα τα εμπλεκόμενα σε συγκρούσεις αρχεία έχουν ωθηθεί στο στάδιο καταχώρισης, μπορούμε να πληκτρολογήσουμε git commit για να οριστικοποιήσουμε την υποβολή συγχώνευσης. Το μήνυμα υποβολής είναι εκ προεπιλογής κάπως έτσι:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#	.git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#	modified:   index.html
#

Μπορούμε να τροποποιήσουμε αυτό το μήνυμα με λεπτομέρειες σχετικά με το πώς επιλύσατε τη συγχώνευση, εφόσον θεωρούμε ότι θα είναι χρήσιμο σε όσους δουν αυτήν τη συγχώνευση στο μέλλον —γιατί κάνατε ό,τι κάνατε, εφόσον δεν είναι προφανές.