Git
Chapters ▾ 2nd Edition

8.4 Εξατομίκευση του Git - Ένα παράδειγμα επιβολής πολιτικής από το Git

Ένα παράδειγμα επιβολής πολιτικής από το Git

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

Τα script που θα δούμε είναι γραμμένα σε Ruby· εν μέρει εξαιτίας πνευματικής αδράνειας από την πλευρά μας, αλλά και επειδή η Ruby διαβάζεται εύκολα ακόμα κι αν δεν γνωρίζουμε πώς να γράφουμε σε αυτήν. Ωστόσο, οποιαδήποτε γλώσσα θα δουλέψει —όλα τα δείγματα script αγκίστρων που διανέμονται με το Git είναι είτε Perl είτε Bash, έτσι μπορούμε να δούμε και πολλά παραδείγματα αγκίστρων σε αυτές τις γλώσσες εξετάζοντας τα δείγματα.

Άγκιστρο διακομιστή

Όλες οι εργασίες από την πλευρά του διακομιστή θα μπουν στο αρχείο update στον κατάλογο hooks. Το άγκιστρο update εκτελείται μία φορά ανά κλάδο που ωθείται και παίρνει τρία ορίσματα:

  • Το όνομα της αναφοράς στην οποία ωθείται

  • Την παλιά αναθεώρηση στην οποία βρισκόταν ο κλάδος

  • Τη νέα αναθεώρηση που ωθείται.

Επίσης έχουμε πρόσβαση στον χρήστη που κάνει την ώθηση, εφόσον η ώθηση γίνεται μέσω SSH. Αν έχουμε επιτρέψει σε όλους να συνδεθούν ως ένας μόνο χρήστης (όπως git) με ταυτοποίηση δημόσιου κλειδιού, ίσως χρειαστεί να δώσουμε σε αυτόν τον χρήστη ένα wrapper κελύφους που καθορίζει με βάση το δημόσιο κλειδί ποιος χρήστης συνδέεται και αντίστοιχα ορίζει μια μεταβλητή περιβάλλοντος. Εδώ θα υποθέσουμε ότι ο συνδεόμενος χρήστης βρίσκεται στη μεταβλητή περιβάλλοντος $USER, έτσι ώστε το σενάριο ενημέρωσης ξεκινά συγκεντρώνοντας όλες τις πληροφορίες που χρειαζόμαστε:

#!/usr/bin/env ruby

$refname = ARGV[0]
$oldrev  = ARGV[1]
$newrev  = ARGV[2]
$user    = ENV['USER']

puts "Enforcing Policies..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"

Ναι, αυτές είναι καθολικές μεταβλητές. Η επίδειξη με αυτόν τον τρόπο είναι ευκολότερη.

Επιβολή συγκεκριμένης μορφής μηνύματος υποβολής

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

Μπορούμε να πάρουμε μια λίστα με τις τιμές SHA-1 για όλες τις υποβολές που ωθούμε παίρνοντας τις $newrev και $oldrev και περνώντας τις σε μια συνδετική εντολή του Git που ονομάζεται git rev-list. Αυτή είναι βασικά η εντολή git log, αλλά εκ προεπιλογής εκτυπώνει μόνο τις τιμές SHA-1 και καμία άλλη πληροφορία. Έτσι, για να πάρουμε μια λίστα με όλα τα SHA-1 των υποβολών που εισήχθησαν μεταξύ των SHA-1 μίας υποβολής και μίας άλλης, μπορούμε να τρέξουμε κάτι σαν αυτό:

$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

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

Πρέπει να σκεφτούμε έναν τρόπο για το πώς θα αποκτήσουμε το μήνυμα υποβολής καθεμίας από αυτές τις υποβολές που θέλουμενα ελέγξουμε. Για να λάβουμε τα ανεπεξέργαστα δεδομένα των υποβολών, μπορούμε να χρησιμοποιήσουμε μια άλλη συνδετική εντολή που ονομάζεται git cat-file. Θα εκτελέσουμε όλες αυτές τις συνδετικές εντολές λεπτομερώς στην ενότητα Εσωτερική λειτουργία του Git· για τώρα, να τι μας δίνει αυτή η εντολή:

$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

changed the version number

Ένας απλός τρόπος για να λάβουμε το μήνυμα υποβολής από μια υποβολή όταν έχουμε την τιμή SHA-1 είναι να πάμε στην πρώτη κενή γραμμή και να πάρουμε τα πάντα μετά από αυτήν. Σε συστήματα Unix αυτό μπορούμε να το κάνουμε με την εντολή sed:

$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number

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

$regex = /\[ref: (\d+)\]/

# επιβολή του εξατομικευμένης μορφής μηνύματος υποβολής
def check_message_format
  missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
  missed_revs.each do |rev|
    message = `git cat-file commit #{rev} | sed '1,/^$/d'`
    if !$regex.match(message)
      puts "[POLICY] Your message is not formatted correctly"
      exit 1
    end
  end
end
check_message_format

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

Επιβολή συστήματος ΑCL με βάση τους χρήστες

Ας υποθέσουμε ότι θέλουμε να προσθέσουμε έναν μηχανισμό που χρησιμοποιεί μια λίστα ελέγχου πρόσβασης (Access Control List --ACL) που καθορίζει σε ποιους χρήστες επιτρέπεται να ωθούν αλλαγές σε ποια μέρη των έργων μας. Κάποιοι έχουν πλήρη πρόσβαση και κάποιοι άλλοι μπορούν να ωθήσουν αλλαγές μόνο σε ορισμένους υποκαταλόγους ή συγκεκριμένα αρχεία. Για να επιβάλουμε κάτι τέτοιο, θα γράψουμε αυτούς τους κανόνες σε ένα αρχείο με όνομα acl που ζει στο γυμνό αποθετήριο Git στον διακομιστή. Θα βάλουμε το άγκιστρο update να βλέπει αυτούς τους κανόνες, να βλέπει ποια αρχεία εισάγονται σε όλες τις υποβολές που ωθούνται και να καθορίζει αν ο χρήστης που κάνει την ώθηση έχει πρόσβαση να ενημερώσει όλα αυτά τα αρχεία.

Το πρώτο πράγμα που θα κάνουμε είναι να γράψουμε τη λίστα ACL. Εδώ θα χρησιμοποιήσουμε μια μορφή που μοιάζει πολύ με τον μηχανισμό ACL στα CVS: χρησιμοποιεί μια σειρά γραμμών, στις οποίες το πρώτο πεδίο είναι avail ή` unavail`, το επόμενο πεδίο είναι η λίστα χρηστών, που διαχωρίζονται με κόμμα, για τους οποίους ισχύει ο κανόνας και το τελευταίο πεδίο είναι η διαδρομή στην οποία εφαρμόζεται αυτός ο κανόνας (αν αυτό το πεδίο είναι κενό εξυπονοείται ανοικτή πρόσβαση). Όλα αυτά τα πεδία οριοθετούνται από έναν χαρακτήρα παροχέτευσης (|).

Σε αυτήν την περίπτωση, έχουμε μερικούς διαχειριστές, μερικούς συγγραφείς τεκμηρίωσης με πρόσβαση στον κατάλογο doc και έναν προγραμματιστή που έχει πρόσβαση μόνο στους καταλόγους` lib` και tests· το αρχείο ACL μοιάζει με αυτό:

avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests

Ξεκινάμε διαβάζοντας αυτά τα δεδομένα σε μια δομή που μπορούμε να χρησιμοποιήσουμε. (Για να κρατήσουμε το παράδειγμα απλό, θα επιβάλλουμε μόνο τις οδηγίες 'avail`.) Ακολουθεί μια μέθοδος που μας δίνει έναν συσχετιστικό πίνακα (associative array) στον οποίο το κλειδί είναι το όνομα χρήστη και η τιμή είναι μια σειρά από διαδρομές στις οποίες ο χρήστης έχει πρόσβαση εγγραφής:

def get_acl_access_data(acl_file)
  # διάβασε δεδομένα ACL
  acl_file = File.read(acl_file).split("\n").reject { |line| line == '' }
  access = {}
  acl_file.each do |line|
    avail, users, path = line.split('|')
    next unless avail == 'avail'
    users.split(',').each do |user|
      access[user] ||= []
      access[user] << path
    end
  end
  access
end

Αυτή η μέθοδος get_acl_access_data όταν διαβάσει το αρχείο ACL που είδαμε νωρίτερα, επιστρέφει μια δομή δεδομένων που μοιάζει με αυτή:

{"defunkt"=>[nil],
 "tpw"=>[nil],
 "nickh"=>[nil],
 "pjhyett"=>[nil],
 "schacon"=>["lib", "tests"],
 "cdickens"=>["doc"],
 "usinclair"=>["doc"],
 "ebronte"=>["doc"]}

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

Μπορούμε πολύ εύκολα να δούμε ποια αρχεία έχουν τροποποιηθεί σε κάθε υποβολή με την επιλογή --name-only στην εντολή git log (έγινε μία σύντομη σχετική αναφορά στο κεφάλαιο 2):

$ git log -1 --name-only --pretty=format:'' 9f585d

README
lib/test.rb

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

# επιτρέπει μόνον ορισμένους χρήστες να τροποποιήσουν ορισμένους υποκαταλόγους του έργου
def check_directory_perms
  access = get_acl_access_data('acl')

  # έλεγξε αν κάποιος προσπαθεί να ωθήσει κάτι που δεν μπορεί
  new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
  new_commits.each do |rev|
    files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
    files_modified.each do |path|
      next if path.size == 0
      has_file_access = false
      access[$user].each do |access_path|
        if !access_path  # ο χρήστης έχει πρόσβαση στα πάντα
           || (path.start_with? access_path) # πρόσβαση σε αυτήν τη διαδρομή
          has_file_access = true
        end
      end
      if !has_file_access
        puts "[POLICY] You do not have access to push to #{path}"
        exit 1
      end
    end
  end
end

check_directory_perms

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

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

Δοκιμασία της λίστας ελέγχου πρόσβασης

Αν εκτελέσουμε την chmod u+x .git/hooks/update, δηλαδή στο αρχείο στο οποίο θα πρέπει να βάλουμε όλο αυτόν τον κώδικα και στη συνέχεια προσπαθήσουμε να ωθήσουμε μια υποβολή με ένα μη συμμορφούμενο μήνυμα, θα πάρουμε κάτι σαν αυτό:

$ git push -f origin master
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 323 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Enforcing Policies...
(refs/heads/master) (8338c5) (c5b616)
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

Υπάρχουν καναδυό ενδιαφέροντα πράγματα εδώ. Πρώτον, βλέπουμε το παρακάτω όταν αρχίζει να τρέχει το άγκιστρο.

Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)

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

Το επόμενο πράγμα που παρατηρούμε είναι το μήνυμα σφάλματος.

[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master

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

To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

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

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

[POLICY] You do not have access to push to lib/test.rb

Στο εξής, εφόσον το script update είναι εκεί και είναι εκτελέσιμο, το αποθετήριό μας δεν θα έχει ποτέ ένα μήνυμα υποβολής χωρίς το πρότυπο που έχουμε καθορίσει και οι χρήστες μας θα είναι περιορισμένοι.

Άγκιστρα από την πλευρά του πελάτη

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

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

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

#!/usr/bin/env ruby
message_file = ARGV[0]
message = File.read(message_file)

$regex = /\[ref: (\d+)\]/

if !$regex.match(message)
  puts "[POLICY] Your message is not formatted correctly"
  exit 1
end

Εάν το script είναι στη θέση του (στο .git/hooks/commit-msg) και εκτελέσιμο και υποβάλουμε με ένα μήνυμα που δεν είναι σωστά μορφοποιημένο, θα δούμε αυτό:

$ git commit -am 'test'
[POLICY] Your message is not formatted correctly

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

$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
 1 file changed, 1 insertions(+), 0 deletions(-)

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

#!/usr/bin/env ruby

$user    = ENV['USER']

# [ insert acl_access_data method from above ]

# επιτρέπει μόνον ορισμένους χρήστες να τροποποιήσουν ορισμένους υποκαταλόγους του έργου
def check_directory_perms
  access = get_acl_access_data('.git/acl')

  files_modified = `git diff-index --cached --name-only HEAD`.split("\n")
  files_modified.each do |path|
    next if path.size == 0
    has_file_access = false
    access[$user].each do |access_path|
    if !access_path || (path.index(access_path) == 0)
      has_file_access = true
    end
    if !has_file_access
      puts "[POLICY] You do not have access to push to #{path}"
      exit 1
    end
  end
end

check_directory_perms

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

access = get_acl_access_data('acl')

σε αυτό:

access = get_acl_access_data('.git/acl')

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

files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`

πρέπει να χρησιμοποιήσουμε

files_modified = `git diff-index --cached --name-only HEAD`

Αλλά αυτές είναι οι δύο μοναδικές διαφορές —κατά τ' άλλα, το script λειτουργεί με τον ίδιο τρόπο. Μια προειδοποίηση· το script αναμένει ότι το τρέχουμε τοπικά ως ο ίδιος χρήστης με αυτόν που ωθεί προς το απομακρυσμένο μηχάνημα. Αν είναι διαφορετικοί, πρέπει να ορίσουμε τη μεταβλητή $USER μη αυτόματα.

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

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

Εδώ είναι ένα παράδειγμα ενός script pre-rebase που ελέγχει ακριβώς αυτό. Παίρνει μια λίστα με όλες τις υποβολές που πρόκειται να ξαναγράψουμε και ελέγχει εάν υπάρχουν σε οποιαδήποτε από τις απομακρυσμένες αναφορές μας. Αν βλέπει κάποια που είναι προσβάσιμη από μία από τις απομακρυσμένες αναφορές μας, ακυρώνει την αλλαγή της βάσης.

#!/usr/bin/env ruby

base_branch = ARGV[0]
if ARGV[1]
  topic_branch = ARGV[1]
else
  topic_branch = "HEAD"
end

target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n")
remote_refs = `git branch -r`.split("\n").map { |r| r.strip }

target_shas.each do |sha|
  remote_refs.each do |remote_ref|
    shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
    if shas_pushed.split("\n").include?(sha)
      puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
      exit 1
    end
  end
end

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

`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`

Η σύνταξη SHA^@ επιλύεται σε όλους τους γονείς αυτής της υποβολής. Ψάχνουμε για οποιαδήποτε υποβολή είναι προσβάσιμη από την τελευταία υποβολή στο απομακρυσμένο αποθετήριο και δεν είναι προσβάσιμη από οποιονδήποτε γονέα ενός από τα SHA-1 που προσπαθούμε να ωθήσουμε —που σημαίνει ότι είναι ταχυπροώθηση.

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