Git
Chapters ▾ 2nd Edition

3.6 Διακλαδώσεις στο Git - Αλλαγή βάσης

Αλλαγή βάσης

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

Η βασική μορφή αλλαγής βάσης

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

Απλό αποκλίνον ιστορικό.
Figure 35. Απλό αποκλίνον ιστορικό.

Ο ευκολότερος τρόπος ενσωμάτωσης των κλάδων, όπως έχουμε ήδη δει, είναι η εντολή merge (συγχώνευση). Επιτελεί μια τριμερή συγχώνευση μεταξύ των δύο τελευταίων στιγμιότυπων διακλάδωσης (C3 και`C4`) και του πιο πρόσφατου κοινού προγόνου τους (C2), δημιουργώντας ένα νέο στιγμιότυπο (και μία νέα υποβολή).

Συγχώνευση και ενσωμάτωση αποκλίνοντος ιστορικού εργασίας.
Figure 36. Συγχώνευση και ενσωμάτωση αποκλίνοντος ιστορικού εργασίας.

Ωστόσο, υπάρχει κι ένας άλλος τρόπος: μπορούμε να πάρουμε μόνον το επίθεμα με τις τροποποιήσεις που εισήχθησαν με την υποβολή C4 και να το εφαρμόσουμε ξανά στο στιγμιότυπο C3. Στο Git, αυτό ονομάζεται αλλαγή βάσης ή επανατοποθέτηση (rebasing). Με την εντολή rebase μπορούμε να πάρουμε όλες τις αλλαγές που υποβλήθηκαν σε ένα κλάδο και να τις επαναλάβουμε σε έναν άλλο.

Σε αυτό το παράδειγμα, θα τρέχαμε τα παρακάτω:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Η διαδικασία που ακολουθείται με την εντολή rebase είναι η εξής: μεταβαίνει στον κοινό πρόγονο των δύο κλάδων (εκείνου στον οποίο βρισκόμαστε και εκείνου ο οποίος θα γίνει η νέα βάση), παίρνει τις διαφορές (diff) που εισάγονται από κάθε υποβολή του κλάδου στον οποίο βρισκόμαστε, αποθηκεύει αυτές τις διαφορές σε προσωρινά αρχεία, μετατοπίζει τον τρέχοντα κλάδο στην ίδια υποβολή στην οποία βρίσκεται και ο κλάδος ο οποίος θα γίνει η νέα βάση και, τέλος, εφαρμόζει τις αλλαγές τη μία μετά την άλλη διαδοχικά.

Αλλαγή της βάσης των τροποποιήσεων της `C4` από τη `C2` στη `C3`.
Figure 37. Αλλαγή της βάσης των τροποποιήσεων της C4 από τη C2 στη C3.

Σε αυτό το σημείο, μπορούμε να επιστρέψουμε στον κλάδο master και να κάνουμε μία συγχώνευση ταχυπροώθησης.

$ git checkout master
$ git merge experiment
Ταχυπροώθηση του κύριου κλάδου.
Figure 38. Ταχυπροώθηση του κύριου κλάδου.

Πλέον το στιγμιότυπο στο οποίο δείχνει η υποβολή C4' είναι ακριβώς ίδιο με αυτό στο οποίο δείχνει η C5 στο παλιότερο παράδειγμα με τη συγχώνευση. Το τελικό προϊόν της ενσωμάτωσης των αλλαγών είναι ακριβώς το ίδιο, αλλά η αλλαγή της βάσης κρατά το ιστορικό πιο συμμαζεμένο. Αν εξετάσουμε το μητρώο ενός επανατοποθετημένου (σε νέα βάση) κλάδου, φαίνεται σαν το ιστορικό να είναι γραμμικό, δηλαδή σαν όλη η εργασία να συνέβη ακολουθιακά παρά το ότι αρχικά γινόταν παράλληλα.

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

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

Μερικές ενδιαφέρουσες αλλαγές βάσης

Μπορούμε επίσης να επανατοποθετήσουμε έναν κλάδο πάνω σε κάποιον κλάδο διαφορετικό από αυτόν στον οποίο βασιζόταν αρχικά. Για παράδειγμα, ας πάρουμε ένα ιστορικό όπως το ιστορικό με έναν θεματικό κλάδο να βασίζεται σε έναν άλλο θεματικό κλάδο. Έχουμε δημιουργήσει έναν θεματικό κλάδο (server) για να προσθέσουμε κάποια λειτουργικότητα από την πλευρά του διακομιστή στο έργο μας και πραγματοποιήσαμε μια υποβολή. Στη συνέχεια, δημιουργήσαμε μία ακόμα διακλάδωση για να κάνουμε τις αλλαγές από την πλευρά του πελάτη (client) και κάναμε επίσης μερικές υποβολές. Τέλος, επιστρέψαμε στον κλάδο server και κάναμε μερικές ακόμη υποβολές.

Ιστορικό με έναν έναν θεματικό κλάδο να βασίζεται σε έναν άλλο θεματικό κλάδο.
Figure 39. ιστορικό με έναν θεματικό κλάδο να βασίζεται σε έναν άλλο θεματικό κλάδο

Ας υποθέσουμε ότι αποφασίζουμε να συγχωνεύσουμε τις αλλαγές από την πλευρά του πελάτη στην κεντρική γραμμή που θα δημοσιευτεί, αλλά θέλουμε να αναβάλλουμε τις αλλαγές από την πλευρά του διακομιστή μέχρι να εξεταστούν περαιτέρω. Μπορούμε να πάρουμε τις αλλαγές από τον κλάδο client που δεν βρίσκονται στον κλάδο server (C8 και`C9`) και να τις αναπαράγουμε στον κύριο κλάδο μας χρησιμοποιώντας την επιλογή --onto της git rebase:

$ git rebase --onto master server client

Αυτή η εντολή ουσιαστικά λέει, “Πήγαινε στον κλάδο client, εντόπισε τα επιθέματα από τότε που απέκλινε από τον κλάδο server και εφάρμοσέ τα στον κλάδο client σαν ο κλάδος client να ήταν κλάδος που απέκλινε από τον κλάδο master”. Είναι λίγο περίπλοκο, αλλά το αποτέλεσμα είναι μια ομορφιά.

Αλλαγή βάσης ενός θεματικού κλάδου που βασίζεται σε έναν άλλο θεματικό κλάδο.
Figure 40. Αλλαγή βάσης ενός θεματικού κλάδου που βασίζεται σε έναν άλλο θεματικό κλάδο.

Τώρα μπορούμε να ταχυπροωθήσουμε τον κλάδο master (βλ. Ταχυπροώθηση του κλάδου master ώστε να περιλάβει τις αλλαγές του κλάδου client.):

$ git checkout master
$ git merge client
Ταχυπροώθηση του κλάδου `master` ώστε να περιλάβει τις αλλαγές του κλάδου `client`.
Figure 41. Ταχυπροώθηση του κλάδου master ώστε να περιλάβει τις αλλαγές του κλάδου client.

Ας πούμε ότι τώρα αποφασίζουμε να ενσωματώσουμε και τις αλλάγες του κλάδου server. Μπορούμε να αλλάξουμε τη βάση του κλάδου server (στον κλάδο master) χωρίς να έχουμε προηγουμένως μεταβεί σε αυτόν εκτελώντας την git rebase <νέα_βάση> <θεματικός_κλάδος>, η οποία μας μεταφέρει στον θεματικό κλάδο (σε αυτήν την περίπτωση, τον server) και εφαρμόζει τις αλλαγές του στη νέα βάση (master) συγχρόνως:

$ git rebase master server

Το αποτέλεσμα της παραπάνω εντολής φαίνεται στην Αλλαγή της βάσης του κλάδου server στον κλάδο master.

Αλλαγή της βάσης του κλάδου `server` στον κλάδο `master`.
Figure 42. Αλλαγή της βάσης του κλάδου server στον κλάδο master

Στη συνέχεια μπορούμε να ταχυπροωθήσουμε τον κλάδο-βάση (master):

$ git checkout master
$ git merge server

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

$ git branch -d client
$ git branch -d server
Τελικό ιστορικό υποβολών.
Figure 43. Τελικό ιστορικό υποβολών

Οι κίνδυνοι της αλλαγής βάσης

Όμως η ευδαιμονία που μας προσφέρει η αλλαγή βάσης έχει κάποιο αντίτιμο, το οποίο μπορεί να συνοψιστεί σε μία γραμμή:

Δεν αλλάζουμε τη βάση υποβολών που υπάρχουν εκτός του αποθετηρίου μας.

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

Όταν αλλάζουμε τη βάση ενός κλάδου, εγκαταλείπουμε τις υπάρχουσες υποβολές και δημιουργούμε νέες που είναι παρόμοιες μεν, διαφορετικές δε. Εάν ωθήσουμε υποβολές κάπου και άλλοι τις έλξουν, βασίσουν τη δουλειά τους σε αυτές και στη συνέχεια ξαναγράψουμε αυτές τις υποβολές με την git rebase και τις ωθήσουμε ξανά, οι συνεργάτες μας θα πρέπει να ξανασυγχωνεύσουν τη δουλειά τους και τα πράγματα θα μπλέξουν όταν προσπαθήσουμε να έλξουμε τη δουλειά τους στη δική μας.

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

Κλωνοποίηση αποθετήριου και επεξεργασία του.
Figure 44. Κλωνοποίηση αποθετηρίου και επεξεργασία του.

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

Ανάκτηση περισσότερων υποβολών και συγχώνευσή τους στην εργασία μας.
Figure 45. Ανάκτηση περισσότερων υποβολών και συγχώνευσή τους στην εργασία μας.

Στη συνέχεια, αυτός που ώθησε τη συγχωνευμένη δουλειά αποφασίζει να επιστρέψει και να αλλάξει τη βάση της εργασίας του· κάνει git push --force για να επανεγγράψει το ιστορικό στον διακομιστή. Στη συνέχεια, ανακτούμε από τον διακομιστή και φέρνουμε τις νέες υποβολές.

Κάποιος ωθεί επανατοποθετημένες υποβολές
Figure 46. Κάποιος ωθεί επανατοποθετημένες υποβολές, εγκαταλείποντας τις υποβολές στις οποίες έχουμε βασίσει τη δουλειά μας

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

Συγχώνευση της ίδιας εργασίας σε μία νέα υποβολή συγχώνευσης.
Figure 47. Συγχώνευση της ίδιας εργασίας σε μία νέα υποβολή συγχώνευσης

Εφόσον το ιστορικό μας μοιάζει με το παραπάνω, αν τρέξουμε git log, θα δούμε δύο υποβολές που έχουν τον ίδιο συγγραφέα, ημερομηνία και μήνυμα, κάτι που προκαλεί σύγχυση. Επιπλέον, αν ωθήσουμε αυτό το ιστορικό πίσω στον διακομιστή, θα επαναφέρουμε όλες εκείνες τις επανατοποθετημένες υποβολές στον κεντρικό εξυπηρετητή, κάτι που θα μπερδέψει ακόμα περισσότερο τον κόσμο. Είναι αρκετά σίγουρο ότι ο άλλος προγραμματιστής δεν θέλει οι C4 και C6 να βρίσκονται στο ιστορικό· άλλωστε αυτός είναι ο λόγος για τον οποίο έκανε την αλλαγή της βάσης.

Επανατοποθέτηση σε επανατοποθετημένες υποβολές

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

Το Git εκτός από το άθροισμα ελέγχου SHA-1 υπολογίζει επίσης ένα άθροισμα ελέγχου που βασίζεται ακριβώς στο επίθεμα που εισήχθη με την υποβολή. Αυτό ονομάζεται “ταυτότητα επιθέματος” (patch-id).

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

Για παράδειγμα, αν στο προηγούμενο σενάριο, όταν βρισκόμαστε στο Κάποιος ωθεί επανατοποθετημένες υποβολές, εγκαταλείποντας τις υποβολές στις οποίες έχουμε βασίσει τη δουλειά μας αντί να κάνουμε συγχώνευση, τρέξουμε git rebase teamone/master, το Git:

  • Θα προσδιορίσει ποια δουλειά βρίσκεται μόνον στον κλάδο μας (C2, C3, C4, C6, C7)

  • Θα προσδιορίσει ποιες υποβολές δεν είναι υποβολές συγχώνευσης (C2, C3, C4)

  • Θα προσδιορίσει ποιες υποβολές δεν έχουν ξαναγραφτεί στον κλάδο στόχο (μόνο οι C2 και C3, δεδομένου ότι η C4 είναι το ίδιο επίθεμα με την C4')

  • Θα εφαρμόσει αυτές τις υποβολές στον κλάδο teamone/master.

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

Επανατοποθέτηση πάνω σε επανατοποθετημένη εργασία.
Figure 48. Επανατοποθέτηση πάνω σε επανατοποθετημένη εργασία

Αυτό θα έχει το επιθυμητό αποτέλεσμα μόνον εάν οι C4 και C4' που έφτιαξε ο συνεργάτης μας είναι σχεδόν ακριβώς το ίδιο επίθεμα. Διαφορετικά, η git rebase δεν θα είναι σε θέση να καταλάβει ότι πρόκειται για ουσιαστικά την ίδια υποβολή και θα προσθέσει ένα ακόμη επίθεμα παρόμοιο με το C4 (το οποίο πιθανότατα θα αποτύχει να εφαρμοστεί χωρίς συγκρούσεις, αφού οι αλλαγές θα έχουν, τουλάχιστον εν μέρει, εφαρμοστεί ήδη).

Μπορούμε επίσης να απλοποιήσουμε τη διαδικασία τρέχοντας μία git pull --rebase αντί για κανονικό git pull. Ή θα μπορούσαμε να το κάνουμε χειροκίνητα με μία git fetch ακολουθούμενο από μία git rebase teamone/master στη συγκεκριμένη περίπτωση.

Εάν χρησιμοποιούμε την git pull και θέλουμε να κάνουμε --rebase την προεπιλογή, μπορούμε να ορίσουμε την τιμή του pull.rebase με κάτι σαν git config --global pull.rebase true.

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

Αν αυτό θεωρηθεί απαραίτητο σε κάποιο σημείο, θα πρέπει να βεβαιωθούμε ότι ο καθένας ξέρει να τρέχει την git pull --rebase για να προσπαθήσει να καταπραΰνει τον πόνο εκ των υστέρων.

Σύγκριση αλλαγής βάσης και συγχώνευσης

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

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

Η αντίθετη άποψη είναι ότι το ιστορικό της υποβολής είναι η μία αφήγηση του πώς έγινε το έργο μας. Δεν θα δημοσιεύαμε το προσχέδιο ενός βιβλίου και το εγχειρίδιο για το πώς θα συντηρήσουμε το λογισμικό μας αξίζει προσεκτική επεξεργασία. Αυτό είναι το στρατόπεδο που χρησιμοποιεί εργαλεία όπως οι rebase και filter-branch για να αφηγηθεί την ιστορία με τον τρόπο που είναι ο καλύτερος δυνατός για τους μελλοντικούς αναγνώστες.

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

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