Git
Chapters ▾ 2nd Edition

7.6 Εργαλεία του Git - Η ιστορία ξαναγράφεται

Η ιστορία ξαναγράφεται

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

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

Αλλαγή της τελευταίας υποβολής

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

Αν θέλουμε να τροποποιήσουμε μόνο το τελευταίο μας μήνυμα, τα πράγματα είναι πολύ απλά:

$ git commit --amend

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

Αν έχουμε υποβάλει και στη συνέχεια θέλουμε να αλλάξουμε το στιγμιότυπο που υποβάλαμε προσθέτοντας ή αλλάζοντας αρχεία, ενδεχομένως επειδή ξεχάσαμε να προσθέσουμε ένα νεο αρχείο στην αρχική υποβολή, η διαδικασία λειτουργεί βασικά με τον ίδιο τρόπο. Μπορούμε να επεξεργαστούμε τις αλλαγές που θέλουμε, επεξεργαζόμενοι ένα αρχείο και τρέχοντας το git add σε αυτό ή git rm σε ένα παρακολουθούμενο αρχείο και εφόσον ακολουθήσει μία git commit --amend θα πάρει το τρέχον στάδιο καταχώρισης και θα το κάνει στιγμιότυπο για τη νέα υποβολή.

Πρέπει να είμαστε προσεκτικοί με αυτήν την τεχνική επειδή η amend τροποποιεί τον αριθμό SHA-1 της υποβολής. Είναι σαν ένα πολύ μικρό rebase —δεν πρέπει να τροποποιοιούμε την τελευταία μας υποβολή εάν την έχουμε ήδη ωθήσει.

Αλλαγή πολλών μηνυμάτων υποβολών

Για να τροποποιήσουμε μια υποβολή που είναι πιο πίσω στο ιστορικό μας, πρέπει να χρησιμοποιήσουμε πιο πολύπλοκα εργαλεία. Το Git δεν διαθέτει ένα εργαλείο τροποποίησης ιστορικού, αλλά μπορούμε να χρησιμοποιήσουμε το εργαλείο rebase για να αλλάξουμε τη βάση μιας σειράς υποβολών στον HEAD στον οποίο είχαν αρχικά βασιστεί αντί να τις μετακινήσουμε σε κάποιον άλλο. Με το εργαλείο διαδραστικής αλλαγής βάσης, μπορούμε στη συνέχεια να σταματήσουμε μετά από κάθε υποβολή που θέλουμε να τροποποιήσουμε και να αλλάξουμε το μήνυμα, να προσθέσουμε αρχεία ή να κάνουμε ό,τι επιθυμούμε. Μπορούμε να εκτελέσουμε την rebase διαδραστικά προσθέτοντας την επιλογή -i στην git rebase. Πρέπει να υποδείξουμε πόσο πίσω θέλουμε να ξαναγράψουμε τις υποβολές λέγοντας στην εντολή ποια υποβολή να είναι η νέα βάση.

Για παράδειγμα, εάν θέλουμε να αλλάξουμε τα τελευταία τρία μηνύματα υποβολής ή κάποιο από αυτά τα μηνύματα υποβολής, δίνουμε ως όρισμα στην git rebase -i τον γονέα της τελευταίας υποβολής που θέλουμε να επεξεργαστούμε, που είναι HEAD~2^ ή HEAD~3. Μπορεί να είναι πιο εύκολο να θυμηθούμε το ~3 επειδή προσπαθούμε να επεξεργαστούμε τις τελευταίες τρεις υποβολές· αλλά καλό είναι θυμόμαστε ότι στην πραγματικότητα πηγαίνουμε τέσσερις υποβολές πιο πριν, στον γονέα της τελευταίας υποβολής που θέλουμε να επεξεργαστούμε:

$ git rebase -i HEAD~3

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

Η εκτέλεση αυτής της εντολής μάς δίνει μια λίστα υποβολών στον επεξεργαστή κειμένου μας που μοιάζει με κάτι σαν αυτό:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

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

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit

Η σειρά είναι αντίστροφη. Η διαδραστική αλλαγή βάσης μάς δίνει ένα script, το οποίο και θα τρέξει. Θα ξεκινήσει από την υποβολή που καθορίσαμε στη γραμμή εντολών (HEAD~3) και θα “ξαναπαίξει” τις αλλαγές που εισήχθησαν σε καθεμία από αυτές τις υποβολές από πάνω προς τα κάτω. Εμφανίζει την παλαιότερη στην κορυφή, αντί για την πιο πρόσφατη, επειδή είναι η πρώτη που θα ξαναπαίξει.

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

edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

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

$ git rebase -i HEAD~3
Stopped at f7f3f6d... changed my name a bit
You can amend the commit now, with

       git commit --amend

Once you’re satisfied with your changes, run

       git rebase --continue

Αυτές οι οδηγίες μάς λένε ακριβώς τι να κάνουμε. Πληκτρολογούμε:

$ git commit --amend

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

$ git rebase --continue

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

Αλλαγή βάσης υποβολών

Μπορούμε επίσης να χρησιμοποιήσουμε διαδραστικές αλλαγές βάσης για να αναδιατάξουμε ή να αφαιρέσουμε πλήρως υποβολές. Εάν θέλουμε να αφαιρέσουμε την υποβολή add cat-file και να αλλάξουμε τη σειρά με την οποία εισήχθησαν οι άλλες δύο υποβολές, μπορούμε να αλλάξουμε το script αλλαγής βάσης από αυτό:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

σε αυτό:

pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

Όταν αποθηκεύουμε και εξερχόμαστε από τον επεξεργαστή, το Git επιστρέφει τον κλάδο μας στον γονέα αυτών των υποβολών, εφαρμόζει την 310154e και στη συνέχεια f7f3f6d και στη συνέχεια σταματά. Ουσιαστικά αλλάζουμε τη σειρά αυτών των υποβολών και αφαιρούμε πλήρως την υποβολή add cat-file.

Squashing Commits

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

#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

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

pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

Όταν αποθηκεύσουμε και βγούμε από τον επεξεργαστή, το Git εφαρμόζει και τις τρεις αλλαγές και στη συνέχεια μας επαναφέρει στον επεξεργαστή για να συγχωνεύσουμε τα τρία μηνύματα υποβολής:

# This is a combination of 3 commits.
# The first commit's message is:
changed my name a bit

# This is the 2nd commit message:

updated README formatting and added blame

# This is the 3rd commit message:

added cat-file

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

Διαχωρισμός υποβολών

Ο διαχωρισμός μιας υποβολής αναιρεί την υποβολή και στη συνέχεια προσθέτει στο στάδιο καταχώρισης και υποβάλλει τόσες υποβολές όσες θέλουμε. Για παράδειγμα, ας υποθέσουμε ότι θέλουμε να χωρίσουμε τη μεσαία από τις τρεις υποβολές μας. Αντί της updated README formatting and added blame, θέλουμε να τη χωρίσουμε σε δύο υποβολές: updated README formatting για την πρώτη και added blame για τη δεύτερη. Μπορούμε να το κάνουμε αυτό στο script rebase -i αλλάζοντας την εντολή για την υποβολή που θέλουμε να χωρίσουμε σε edit:

pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

Στη συνέχεια, όταν το script μάς βγάζει στη γραμμή εντολών, επαναφέρουμε (reset) αυτήν την υποβολή, παίρνουμε τις αλλαγές που έχουν αναιρεθεί και δημιουργούμε πολλαπλές υποβολές από αυτές τις αλλαγές. Όταν αποθηκεύουμε και βγαίνουμε από τον επεξεργαστή κειμένου, το Git επιστρέφει στον γονέα της πρώτης υποβολής της λίστας, εφαρμόζει την πρώτη υποβολή (f7f3f6d), εφαρμόζει τη δεύτερη (310154e) και μας επαναφέρει στην κονσόλα. Εκεί, μπορούμε να εκτελέσουμε μια μεικτή επαναφορά αυτής της υποβολής με την git reset HEAD^, το οποίο ουσιαστικά αναιρεί αυτήν την υποβολή και αφήνει τα τροποποιημένα αρχεία εκτός σταδίου καταχώρισης. Τώρα μπορούμε να προσθέσουμε αρχεία στο στάδιο καταχώρισης και να τα υποβάλλουμε μέχρι να έχουμε αρκετές υποβολές και τρέχουμε την git rebase --continue όταν τελειώσουμε:

$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue

Το Git εφαρμόζει την τελευταία υποβολή (a5f4a0d) στο script και το ιστορικό μας μοιάζει με αυτό:

$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

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

Η πυρηνική επιλογή: filter-branch

Υπάρχει μια άλλη επιλογή επανεγγραφής ιστορικού που μπορούμε να χρησιμοποιήσουμε αν χρειάζεται να ξαναγράψουμε έναν μεγαλύτερο αριθμό υποβολών χρησιμοποιώντας script —π.χ. για να αλλάξουμε τη διεύθυνση e-mail μας παντού ή να αφαιρέσουμε ένα αρχείο από κάθε υποβολή. Η σχετική εντολή είναι η filter-branch και μπορεί να ξαναγράψει τεράστια τμήματα του ιστορικού μας, επομένως ενδεχομένως δεν πρέπει να τη χρησιμοποιήσουμε, εκτός εάν το έργο μας δεν είναι ακόμη δημόσια και άλλοι δεν έχουν βασίσει τη δουλειά τους στις υποβολές που πρόκειται να ξαναγράψουμε. Ωστόσο, μπορεί να είναι πολύ χρήσιμο. Θα δούμε μερικές κοινές χρήσεις της, ώστε να έχουμε μια ιδέα για κάποια από τα πράγματα που είναι σε θέση να κάνει.

Αφαίρεση ενός αρχείου από κάθε υποβολή

Αυτό συμβαίνει αρκετά συχνά. Κάποιος υποβάλλει ένα τεράστιο δυαδικό αρχείο με ένα άσκοπο git add και θέλουμε να το αφαιρέσουμε από παντού. Ίσως υποβάλαμε κατά λάθος ένα αρχείο που περιείχε έναν κωδικό πρόσβασης και θέλουμε να κάνουμε το έργο μας ανοιχτή πρόσβασης. Η filter-branch είναι το εργαλείο που θέλουμε να χρησιμοποιήσουμε για να συμμαζέψουμε ολόκληρο το ιστορικό μας. Για να καταργήσουμε ένα αρχείο που ονομάζεται passwords.txt παντού στο ιστορικό μας, μπορούμε να χρησιμοποιήσουμε την επιλογή --tree-filter με την filter-branch:

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

Η επιλογή --tree-filter εκτελεί την εντολή που έχει οριστεί μετά από κάθε ενημέρωση (checkout) του έργου και κατόπιν ξαναϋποβάλει τα αποτελέσματα. Σε αυτήν την περίπτωση, αφαιρούμε ένα αρχείο που ονομάζεται passwords.txt από κάθε στιγμιότυπο, είτε υπάρχει είτε όχι. Αν θέλουμε να καταργήσουμε όλα τα αρχεία backup του επεξεργαστή κειμένου που υποβλήθηκαν κατά λάθος, μπορούμε να εκτελέσουμε κάτι σαν την git filter-branch --tree-filter 'rm -f *~' HEAD.

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

Κάνοντας έναν υποκατάλογο τη νέα ρίζα

Ας υποθέσουμε ότι έχουμε πραγματοποιήσει εισαγωγή από ένα άλλο σύστημα ελέγχου και έχουμε υποκαταλόγους που δεν έχουν νόημα (κορμός, ετικέτες κ.ο.κ.). Αν θέλουμε ο υποκατάλογος trunk να γίνει ο νέος ριζικός κατάλογος του έργου για κάθε υποβολή, η filter-branch μπορεί να μας βοηθήσει και σ' αυτό:

$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

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

Αλλαγή διευθύνσεων e-mail παντού

Μια άλλη συνηθισμένη περίπτωση είναι ότι ξεχάσαμε να εκτελέσουμε την git config για να ορίσουμε το όνομα και τη διεύθυνση e-mail μας πριν αρχίσουμε να εργαζόμαστε ή ίσως θέλουμε να κάνουμε δημόσιο ένα έργο που είχαμε στον χώρο εργασίας μας και γι' αυτό θέλουμε να αλλάξουμε όλες τις διευθύνσεις e-mail από αυτήν της εργασίας μας στην προσωπική μας διεύθυνση. Σε κάθε περίπτωση, μπορούμε να αλλάξουμε τις διευθύνσεις e-mail σε πολλαπλές υποβολές με τη μία επίσης με την filter-branch. Πρέπει να είμαστε προσεκτικοί ώστε να αλλάξουμε μόνο τις διευθύνσεις e-mail που είναι δικές μας, γι' αυτό χρησιμοποιούμε την επιλογή ‘--commit-filter’:

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
        then
                GIT_AUTHOR_NAME="Scott Chacon";
                GIT_AUTHOR_EMAIL="schacon@example.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

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