Git
Chapters ▾ 2nd Edition

7.8 Εργαλεία του Git - Συγχωνεύσεις για προχωρημένους

Συγχωνεύσεις για προχωρημένους

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

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

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

Συγκρούσεις συγχωνεύσεων

Έχουμε καλύψει κάποια βασικά στοιχεία για την επίλυση των συγκρούσεων συγχώνευσης στην ενότητα Βασικές συγκρούσεις συγχωνεύσεων· για πιο πολύπλοκες συγκρούσεις το Git παρέχει μερικά εργαλεία για να μας βοηθήσει να καταλάβουμε τι συμβαίνει και πώς να αντιμετωπίσουμε καλύτερα τη σύγκρουση.

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

Ας δούμε ένα πολύ απλό παράδειγμα. Έχουμε ένα πολύ απλό αρχείο Ruby που εκτυπώνει hello world.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

Στο αποθετήριό μας, δημιουργούμε έναν νέο κλάδο που ονομάζεται whitespace και προχωράμε στην αλλαγή όλων των τερματισμών γραμμής Unix σε τερματισούς γραμμής DOS, δηλαδή ουσιαστικά αλλάζουμε κάθε γραμμή του αρχείου, αλλά μόνο με κενά. Στη συνέχεια, αλλάζουμε τη γραμμή hello world στο hello mundo.

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'converted hello.rb to DOS'
[whitespace 3270f76] converted hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 1 file changed, 1 insertion(+), 1 deletion(-)

Τώρα επιστρέφουμε στον κλάδο master και προσθέτουμε κάποια τεκμηρίωση στη συνάρτηση.

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# εκτυπώνει έναν χαιρετισμό
 def hello
   puts 'hello world'
 end

$ git commit -am 'document the function'
[master bec6336] document the function
 1 file changed, 1 insertion(+)

Προσπαθούμε να συγχωνέψουμε τον κλάδο μας whitespace· θα έχουμε συγκρούσεις εξαιτίας των αλλαγών στα λευκά διαστήματα.

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

Απόρριψη συγχώνευσης

Έχουμε κάποιες επιλογές. Πρώτον, ας δούμε πώς θα βγούμε από αυτήν την κατάσταση. Εάν δεν αναμέναμε τις συγκρούσεις και δεν θέλουμε να ασχοληθούμε με αυτήν την κατάσταση, μπορούμε απλά να βγούμε από τη συγχώνευση με την εντολή git merge --abort.

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

Η επιλογή git merge --abort προσπαθεί να μας επιστρέψει στην κατάστασή μας πριν εκτελέσουμε τη συγχώνευση. Οι μόνες περιπτώσεις στις οποίες μπορεί να μην είναι σε θέση να το κάνει τέλεια, είναι εάν είχαμε μη παρακατατεθειμένες, μη υποβεβλημένες αλλαγές στον κατάλογο εργασίας όταν την τρέξαμε, αλλιώς θα έπρεπε να δουλέψει μια χαρά.

Αν για κάποιο λόγο θέλουμε απλώς να αρχίσουμε από την αρχή, μπορούμε επίσης να εκτελέσουμε την git reset --hard HEAD και το αποθετήριό μας θα επιστρέψει στην τελευταία υποβεβλημένη κατάσταση. Είναι σημαντικό να Θυμόμαστε ότι οποιαδήποτε μη υποβεβλημένη εργασία θα χαθεί, οπότε πρέπει να είμαστε σίγουροι ότι δεν θέλουμε αυτές τις αλλαγές.

Αγνόηση των λευκών χαρακτήρων

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

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

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

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

Χειροκίνητη επανασυγχώνευση αρχείου

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

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

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

Η λήψη των τριών εκδόσεων αρχείων είναι πραγματικά εύκολη. Το Git αποθηκεύει όλες αυτές τις εκδόσεις στο ευρετήριο κάτω από τα stages τα οποία έχουν το καθένα από αυτά έναν συσχετισμένο αριθμό. Το στάδιο 1 είναι ο κοινός πρόγονος, το στάδιο 2 είναι η δική μας έκδοση και το στάδιο 3 είναι από το MERGE_HEAD, δηλαδή, την έκδοση που συγχωνεύουμε (theirs).

Μπορούμε να εξάγουμε ένα αντίγραφο από καθεμία από αυτές τις εκδόσεις του αρχείου που βρίσκεται σε σύγκρουση με την εντολή git show και μια ειδική σύνταξη.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

Αν θέλουμε να το γίνουμε λίγο πιο σκληροπυρηνικοί, μπορούμε επίσης να χρησιμοποιήσουμε την εντολή ls-files -u για να λάβουμε τα πραγματικά SHA-1 των blob του Git blobs για καθένα από αυτά τα αρχεία.

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

Το :1:hello.rb είναι απλά μια συντομογραφία για να αναζητήσει κανείς τον αριθμό SHA-1 εκείνου του blob.

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

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# εκτυπώνει έναν χαιρετισμό
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

Σε αυτό το σημείο έχουμε συγχωνεύσει το αρχείο ωραιότατα. Στην πραγματικότητα, αυτός ο τρόπος είναι καλύτερος από την επιλογή ignore-space-change, διότι επιδιορθώνει πραγματικά τις αλλαγές των λευκών διαστημάτω πριν από τη συγχώνευση αντί απλά να τις αγνοεί. Στη συγχώνευση ignore-space-change, στην πραγματικότητα καταλήξαμε με μερικές γραμμές με τερματισμό γραμμής DOS και με μερικές γραμμές με τερματισμό γραμμής Unix, άρα μπερδέψαμε τα πράγματα.

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

Για να συγκρίνουμε το αποτέλεσμά μας με αυτό που είχαμε στον κλάδο μας πριν από τη συγχώνευση, με άλλα λόγια, για να δούμε τι εισήγαγε η συγχώνευση, μπορούμε να εκτελέσουμε την git diff --ours

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # εκτυπώνει έναν χαιρετισμό
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

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

Αν θέλουμε να δούμε πώς το αποτέλεσμα της συγχώνευσης διέφερε από αυτό που υπήρχε στη δική τους πλευρά, μπορούμε να εκτελέσουμε την git diff --theirs. Σε αυτό και στο επόμενο παράδειγμα, πρέπει να χρησιμοποιήσουμε την επιλογή -b για να εξαλείψουμε τον λευκό χαρακτήρα επειδή το συγκρίνουμε με αυτό που υπάρχει στο Git, όχι το καθαρισμένο αρχείο μας hello.theirs.rb.

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# εκτυπώνει έναν χαιρετισμό
 def hello
   puts 'hello mundo'
 end

Τέλος, βλέπουμε πώς το αρχείο έχει αλλάξει και από τις δύο πλευρές με το git diff --base.

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# εκτυπώνει έναν χαιρετισμό
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Σε αυτό το σημείο μπορούμε να χρησιμοποιήσουμε την εντολή git clean για να διαγράψουμε τα επιπλέον αρχεία που δημιουργήσαμε ώστε να κάνουμε τη χειροκίνητη συγχώνευση αλλά δεν χρειαζόμαστε πλέον.

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

Checking Out Conflicts

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

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

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed text to hello mundo
|/
* b7dcc89 initial hello world code

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

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

Θα θέλαμε να δούμε τι είναι αυτή η σύγκρουση. Αν ανοίξουμε το αρχείο, θα δούμε κάτι τέτοιο:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

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

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

Ένα χρήσιμο εργαλείο είναι η εντολή git checkout με την επιλογή --conflict. Αυτή η εντολή θα ξανά-μεταβεί στο αρχείο και θα αντικαταστήσει τις επισημάνσεις σύγκρουσης. Αυτό είναι χρήσιμο αν θέλουμε να αλλάξουμε τη μορφή των επισημάνσεων και να ξαναπροσπαθήσουμε να επιλύσουμε τις συγκρούσεις.

Μπορούμε να περάσουμε στην επιλογή --conflict είτε την τιμή diff3 είτε την merge (που είναι η προεπιλογή). Εάν της περάσουμε την diff3, το Git θα χρησιμοποιήσει μια ελαφρώς διαφορετική έκδοση των επισημάνσεων σύγκρουσης, όχι μόνο να μας δώσει τις εκδόσεις ours και theirs αλλά και την έκδοση base για να πάρουμε περισσότερες πληροφορίες για το πλαίσιο της σύγκρουσης.

$ git checkout --conflict=diff3 hello.rb

Μόλις το τρέξουμε, το αρχείο θα μοιάζει με αυτό:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

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

$ git config --global merge.conflictstyle diff3

Η εντολή git checkout μπορεί επίσης να πάρει τις επιλογές --ours και ‘--their’, οι οποίες μπορεί να είναι ένας πολύ γρήγορος τρόπος για να επιλέξουμε απλά τη μια πλευρά ή την άλλη χωρίς να συγχωνεύσουμε τίποτα.

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

Συγχώνευση μητρώου

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

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

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo

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

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

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo

Αντίθετα αν την τρέξουμε με την επιλογή -p, θα πάρουμε μόνον αντί να πάρουμε μόνο τα diff του αρχείου που κατέληξε σε σύγκρουση. Αυτό μπορεί να είναι πραγματικά χρήσιμο για να μας δώσει γρήγορα το πλαίσιο που χρειαζόμαστε για να καταλάβουμε γιατί κάτι συγκρούεται και πώς να επιλύσουμε πιο έξυπνα τη σύγκρουση.

Μορφή συνδυασμένου diff

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

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

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

Η μορφή αυτή ονομάζεται “Συνδυασμένη diff” και μας δίνει δύο στήλες δεδομένων δίπλα σε κάθε γραμμή. Η πρώτη γραμμή μας δείχνει αν αυτή η γραμμή διαφέρει (προστέθηκε ή αφαιρέθηκε) ανάμεσα στον κλάδο ours και το αρχείο στον κατάλογο εργασίας μας και η δεύτερη στήλη κάνει το ίδιο αλλά ανάμεσα στον κλάδο theirs και τον κατάλογο εργασίας μας.

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

Αν επιλύσουμε τη σύγκρουση και τρέξουμε ξανά την git diff, θα δούμε το ίδιο πράγμα αλλά είναι λίγο πιο χρήσιμο.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

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

Μπορούμε επίσης να πάρουμε αυτό από την git log για οποιαδήποτε συγχώνευση μετά τη συγχώνευση για να δούμε πώς κάτι επιλύθηκε. Το Git θα εκτυπώσει σε αυτήν τη μορφή αν εκτελέσουμε την git show σε μια υποβολή συγχώνευσης ή εάν προσθέσουμε την επιλογή --cc 'σε μία `git log -p (η οποία εκ προεπιλογής εμφανίζει μόνο επιθέματα για υποβολές που δεν είναι συγχωνεύσεις).

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Αναίρεση συγχώνευσης

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

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

Ακούσια υποβολή συγχώνευσης.
Figure 137. ακούσια υποβολή συγχώνευσης

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

Fix the references

Εάν η ανεπιθύμητη υποβολή συγχώνευσης υπάρχει μόνο στο τοπικό αποθετήριο, η ευκολότερη και καλύτερη λύση είναι να μετακινήσουμε τους κλάδους ώστε να δείχνουν εκεί όπου θέλουμε. Στις περισσότερες περιπτώσεις, αν μετά από μία git merge εκτελέσουμε την git reset --hard HEAD~, αυτό θα επαναφέρει τους δείκτες κλάδων, οπότε θα μοιάζουν με αυτό:

Το ιστορικό μετά την `git reset --hard HEAD~`.
Figure 138. Το ιστορικό μετά την git reset --hard HEAD~.

Καλύψαμε την reset στην ενότητα Απομυθοποίηση της reset, επομένως δεν πρέπει να είναι πολύ δύσκολο να καταλάβουμε τι συμβαίνει εδώ. Ορίστε μία γρήγορη υπενθύμιση: η reset --hard συνήθως περνάει από τρία βήματα:

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

  2. Κάνουμε το ευρετήριο να μοιάζει με τον HEAD.

  3. Κάνουμε τον κατάλογο εργασίας να μοιάζει με το ευρετήριο.

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

Eπαναφορά της υποβολής

Εάν η μετακίνηση των δεικτών των κλάδων από δω κι από κει δεν μας βοηθά, το Git μάς δίνει τη δυνατότητα να κάνουμε μια νέα υποβολή, η οποία ακυρώνει όλες τις αλλαγές από μία υπάρχουσα. Το Git ονομάζει αυτήν τη λειτουργία revert (“επαναφορά”) και σε αυτό το συγκεκριμένο σενάριο, θα τη χρησιμοποιήσουμε ως εξής:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

Η επισήμανση -m 1 υποδεικνύει ποιος γονέας είναι η “κύρια γραμμή” και θα πρέπει να διατηρηθεί. Όταν ξεκινάμε μια συγχώνευση στον HEAD (git merge topic'), η νέα υποβολή έχει δύο γονείς: ο πρώτος είναι ο `HEAD (C6) και ο δεύτερος είναι η κορυφή του κλάδου που συγχωνεύεται C4). Σε αυτήν την περίπτωση, θέλουμε να αναιρέσουμε όλες τις αλλαγές που εισήχθησαν με τη συγχώνευση του γονέα #2 (C4), διατηρώντας όλο το περιεχόμενο από τον γονέα #1 (C6).

Το ιστορικό μετά την επαναφορά της υποβολής μοιάζει ως εξής:

Ιστορικό μετά την  `git revert -m 1`.
Figure 139. ιστορικό μετά την git revert -m 1

Η νέα υποβολή ^M έχει ακριβώς το ίδιο περιεχόμενο με την C6, οπότε ξεκινώντας από εδώ είναι σαν να μην συνέβη ποτέ η συγχώνευση, εκτός από το γεγονός ότι οι υποβολές που τώρα δεν έχουν μεσολαβήσει εξακολουθούν να βρίσκονται στο ιστορικό του HEAD. Το Git θα μπερδευτεί αν προσπαθήσουμε να συγχωνεύσουμε ξανά τον topic στον master:

$ git merge topic
Already up-to-date.

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

Ιστορικό με κακή συγχώνευση.
Figure 140. ιστορικό με κακή συγχώνευση

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

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
στορικό μετά την επανασυγχώνευση μίας συγχώνευσης που είχε επαναφερθεί.
Figure 141. ιστορικό μετά την επανασυγχώνευση μίας συγχώνευσης που είχε επαναφερθεί

Σε αυτό το παράδειγμα, οι M και ^M αλληλοεξουδετερώνονται. Η ^^M ουσιαστικά συγχωνεύει τις αλλαγές από C3 και C4, και η C8 συγχωνεύει τις αλλαγές από την C7, έτσι τώρα ο topic συγχωνεύεται πλήρως.

Άλλα είδη συγχωνεύσεων

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

Προτίμηση “δική μας” ή “δική τους”

Πρώτα απ 'όλα, υπάρχει ένα άλλο χρήσιμο πράγμα που μπορούμε να κάνουμε με τον συνήθη “αναδρομικό” τρόπο συγχώνευσης. Έχουμε ήδη δει τις επιλογές ignore-all-space και 'ignore-space-change` που έχουν περάσει με την επιλογή -X, αλλά μπορούμε επίσης να πούμε στο Git να ευνοεί τη μία ή την άλλη πλευρά όταν βλέπει μια σύγκρουση.

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

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

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

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Ωστόσο, αν την τρέξουμε με -Xours ή -XTheirs δεν προκαλεί συγκρούσεις.

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

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

Αυτή η επιλογή μπορεί επίσης να μεταβιβαστεί στην εντολή git merge-file που είδαμε νωρίτερα τρέχοντας κάτι σαν το git merge-file --ours για συγχωνεύσεις μεμονωμένων αρχείων.

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

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

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

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

Αυτό μπορεί συχνά να είναι χρήσιμο στο να ξεγελάσει το Git ώστε να νομίζει ότι ένας κλάδος είναι ήδη συγχωνευμένος όταν κάνει μία συγχώνευση αργότερα. Για παράδειγμα, ας πούμε ότι διακλαδώσαμε από έναν κλάδο release, κάναμε κάποια εργασία σε αυτόν την οποία θα θελήσουμε να συγχωνεύσουμε ξανά τον κλάδο master σε κάποια στιγμή. Εν τω μεταξύ, η διόρθωση ενός bug, bugfix στον master πρέπει να μεταφερθεί στον κλάδο release. Μπορούμε να συγχωνεύσουμε τον κλάδο bugfix στον κλάδο release και επίσης να τρέξουμε merge -s ours για να συγχωνεύσουμε τον ίδιο κλάδο στον master (παρά το ότι η διόρθωση του bug υπάρχει ήδη εκεί) έτσι ώστε όταν συγχωνεύσουμε ξανά τον κλάδο release να μην υπάρχουν συγκρούσεις από τη διόρθωση σφαλμάτων.

Συγχώνευση υποδένδρων

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

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

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

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

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

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

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

Σε αυτήν την περίπτωση, θέλουμε να έλξουμε το έργο Rack στο έργο του κλάδου master ως υποκατάλογο. Αυτό μπορούμε να το κάνουμε στο Git με την εντολή git read-tree. Θα μάθουμε περισσότερα σχετικά με την read-tree και τους φίλους της στην ενότητα Εσωτερική λειτουργία του Git, αλλά προς το παρόν αρκεί να γνωρίζουμε ότι διαβάζει το ριζικό δέντρο ενός κλάδου στον τρέχον στάδιο καταχώρισης και τον κατάλογο εργασίας. Επιστρέφουμε τώρα στον κύριο κλάδο μας και έλκουμε τον κλάδο rack_branch στον υποκατάλογο rack του κύριου κλάδου master του κύριου έργου μας:

$ git read-tree --prefix=rack/ -u rack_branch

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

$ git checkout rack_branch
$ git pull

Στη συνέχεια, μπορούμε να συγχωνεύσουμε αυτές τις αλλαγές πίσω στον κλάδο master. Για να έλξουμε τις αλλαγές και να προεπικαλύψουμε το μήνυμα commit, χρησιμοποιούμε την επιλογή --squash, καθώς και την επιλογή -Xsubtree της στρατηγικής αναδρομικής συγχώνευσης. (Η αναδρομική στρατηγική είναι η προεπιλογή εδώ, αλλά τη συμπεριλαμβάνουμε για λόγους σαφήνειας.)

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

Όλες οι αλλαγές από το έργο Rack έχουν συγχωνευτεί και είναι έτοιμες να υποβληθούν τοπικά. Μπορούμε επίσης να κάνουμε το αντίθετο —να κάνουμε αλλαγές στον υποκατάλογο rack του κύριου κλάδου μας και στη συνέχεια να τις συγχωνεύσουμε στον κλάδο rack_branch αργότερα για να τις υποβάλουμε στους διαχειριστές ή να τις ωθήσουμε προς τα πάνω.

Αυτό μας δίνει έναν τρόπο να έχουμε μια ροή εργασίας κάπως παρόμοια με τη ροή εργασίας των λειτουργικών υπομονάδων χωρίς να χρησιμοποιούμε υπομονάδες (που θα καλύψουμε στην ενότητα [_git_submodules]). Μπορούμε να διατηρούμε κλάδους με άλλα σχετικά έργα στο αποθετήριό μας και να τα συγχωνεύουμε στο έργο μας περιστασιακά. Είναι ωραίο κατά κάποιους τρόπους· για παράδειγμα όλος ο κώδικας υποβάλλεται σε ένα μόνο μέρος. Ωστόσο, έχει άλλα μειονεκτήματα: είναι λίγο πιο περίπλοκο και είναι πιο εύκολο να κάνουμε κάποιο λάθος στην επανένταξη των αλλαγών ή να ωθήσουμε κατά λάθος έναν κλάδο σε ένα άσχετο αποθετήριο.

Κάτι ακόμα ελαφρώς περίεργο είναι ότι για να πάρουμε μια diff ανάμεσα σε αυτό που έχουμε στον υποκατάλογό μας rack και τον κώδικα στον κλάδο rack_branch —για να δούμε αν πρέπει να τα συγχωνεύσουμε— δεν μπορούμε να χρησιμοποιήσουμε την κανονική εντολή diff. Αντ' αυτού, πρέπει να τρέξουμε την git diff-tree με τον κλάδο με τον οποίο θέλουμε να συγκρίνουμε:

$ git diff-tree -p rack_branch

Ή για να συγκρίνουμε τι υπάρχει στον υποκατάλογό μας rack σε σχέση με αυτά που υπήρχαν στον κλάδο master του διακομιστή την τελευταία φορά που ανακτήσαμε, μπορούμε να εκτελέσουμε

$ git diff-tree -p rack_remote/master