Git
Chapters ▾ 2nd Edition

7.13 Εργαλεία του Git - Replace

Replace

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

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

Για παράδειγμα, ας υποθέσουμε ότι έχουμε ένα τεράστιο ιστορικό και θέλουμε να χωρίσουμε το αποθετήριό μας σε ένα σύντομο, πρόσφατο ιστορικό για τους νέους προγραμματιστές και ένα πολύ εκτενέστερο για όσους ενδιαφέρονται για εξόρυξη δεδομένων. Μπορούμε να μεταμοσχεύσουμε το ένα ιστορικό στο άλλο με την replace, αντικαθιστώντας την παλιότερη υποβολή στη νέα γραμμή παραγωγής με την πιο πρόσφατη υποβολή στην παλαιότερη. Αυτό είναι ωραίο διότι σημαίνει ότι δεν χρειάζεται να ξαναγράψουμε κάθε υποβολή στο νέο ιστορικό, όπως θα έπρεπε κανονικά να κάνουμε για να τα ενώσουμε μαζί (επειδή η γονικότητα επηρεάζει τον αριθμό SHA-1).

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

Θα χρησιμοποιήσουμε ένα απλό αποθετήριο με πέντε απλές υποβολές:

$ git log --oneline
ef989d8 fifth commit
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

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

replace1

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

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
replace2

Τώρα μπορούμε να ωθήσουμε τον νέο κλάδο history στον κλάδο master του νέου αποθετηρίου μας:

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

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

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

Για να γίνει αυτό, πρέπει να επιλέξουμε ένα σημείο στο οποίο θα γίνει η διακλάδωση· αυτό για εμάς είναι η τρίτη υποβολή, δηλαδή, η 9c68fdc στη γλώσσα του SHA. Έτσι, η υποβολή-βάση θα διακλαδώνεται από αυτό το δέντρο. Μπορούμε να δημιουργήσουμε την υποβολή μας χρησιμοποιώντας την εντολή commit-tree, η οποία παίρνει ακριβώς ένα δέντρο και θα μας επιστρέψει τον αριθμό SHA-1 ενός ολοκαίνουριου ορφανού αντικειμένου υποβολής.

$ echo 'get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Note

Η εντολή commit-tree είναι μία από ένα σύνολο εντολών που συνήθως αναφέρονται ως εντολές “διοχέτευσης”. Είναι εντολές που γενικά δεν προορίζονται για άμεση χρήση αλλά χρησιμοποιούνται από άλλες εντολές του Git για να κάνουν μικρότερες εργασίες. Σε ορισμένες περιπτώσεις, όταν κάνουμε πιο περίεργα πράγματα όπως αυτό, μας επιτρέπουν να κάνουμε πράγματα πραγματικά χαμηλού επιπέδου αλλά δεν προορίζονται για καθημερινή χρήση. Θα δούμε περισσότερα σχετικά με τις εντολές υδραυλικών στην ενότητα Διοχετεύσεις και πορσελάνες.

replace3

Τώρα, λοιπόν, που έχουμε μια υποβολή-βάση, μπορούμε να αλλάξουμε τη βάση του υπόλοιπου του ιστορικού μας πάνω σε αυτήν με την git rebase --onto. Το όρισμα --onto θα είναι το SHA-1 που μόλις πήραμε από την commit-tree και η νέα βάση θα είναι η τρίτη υποβολή (ο γονέας της πρώτης υποβολής που θέλουμε να κρατήσουμε, δηλαδή της 9c68fdc):

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
replace4

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

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

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

Τώρα έχει τις πρόσφατες υποβολές του στον κλάδο master και τις “αρχαίες” υποβολές στον κλάδο project-history/master.

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Για να τις συνδυάσουμε, μπορούμε απλά να καλέσουμε το git replace με την υποβολή που θέλουμε να αντικαταστήσουμε και μετά την υποβολή με την οποία θέλουμε να την αντικαταστήσουμε. Αφού θέλουμε να αντικαταστήσουμε την “τέταρτη” υποβολή στον κύριο κλάδο με την “τέταρτη” υποβολή στον κλάδο project-history/master:

$ git replace 81a708d c6e1e95

Αν κοιτάξουμε τώρα το ιστορικό του κλάδου master, φαίνεται να μοιάζει με αυτό:

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Καλή φάση, σωστά; Χωρίς να χρειάζεται να αλλάξουμε όλα τα SHA-1s upstream, μπορέσαμε να αντικαταστήσουμε μια υποβολή στο ιστορικό μας με μια εντελώς διαφορετική υποβολή και όλα τα συνήθη εργαλεία (bisect, blame κ.λπ.) θα δουλεύουν με τον τρόπο με τον οποίο θα αναμέναμε.

replace5

Κάτι ενδιαφέρον είναι ότι εξακολουθεί να εμφανίζεται η 81a708d ως SHA-1, παρόλο που στην πραγματικότητα χρησιμοποιούνται τα δεδομένα της c6e1e95 με την οποία την αντικαταστήσαμε. Ακόμα και αν εκτελέσουμε μια εντολή όπως η cat-file, θα μας δείξει τα αντικατασταθέντα δεδομένα:

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

Ας θυμηθούμε ότι ο πραγματικός γονέας της 81a708d ήταν η προσωρινή υποβολή 622e88e και όχι η 9c68fdce όπως δηλώνεται εδώ.

Κάτι ακόμα ενδιαφέρον είναι ότι αυτά τα δεδομένα διατηρούνται στις αναφορές μας:

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

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