-
1. Начало
- 1.1 За Version Control системите
- 1.2 Кратка история на Git
- 1.3 Какво е Git
- 1.4 Конзолата на Git
- 1.5 Инсталиране на Git
- 1.6 Първоначална настройка на Git
- 1.7 Помощна информация в Git
- 1.8 Обобщение
-
2. Основи на Git
-
3. Клонове в Git
-
4. GitHub
-
5. Git инструменти
- 5.1 Избор на къмити
- 5.2 Интерактивно индексиране
- 5.3 Stashing и Cleaning
- 5.4 Подписване на вашата работа
- 5.5 Търсене
- 5.6 Манипулация на историята
- 5.7 Мистерията на командата Reset
- 5.8 Сливане за напреднали
- 5.9 Rerere
- 5.10 Дебъгване с Git
- 5.11 Подмодули
- 5.12 Пакети в Git (Bundling)
- 5.13 Заместване
- 5.14 Credential Storage система
- 5.15 Обобщение
-
6. Настройване на Git
- 6.1 Git конфигурации
- 6.2 Git атрибути
- 6.3 Git Hooks
- 6.4 Примерна Git-Enforced политика
- 6.5 Обобщение
-
7. Git и други системи
- 7.1 Git като клиент
- 7.2 Миграция към Git
- 7.3 Обобщение
-
8. Git на ниско ниво
- 8.1 Plumbing и Porcelain команди
- 8.2 Git обекти
- 8.3 Git референции
- 8.4 Packfiles
- 8.5 Refspec спецификации
- 8.6 Транспортни протоколи
- 8.7 Поддръжка и възстановяване на данни
- 8.8 Environment променливи
- 8.9 Обобщение
-
9. Приложение A: Git в други среди
-
10. Приложение B: Вграждане на Git в приложения
- 10.1 Git от команден ред
- 10.2 Libgit2
- 10.3 JGit
- 10.4 go-git
- 10.5 Dulwich
-
A1. Приложение C: Git команди
- A1.1 Настройки и конфигурация
- A1.2 Издърпване и създаване на проекти
- A1.3 Snapshotting
- A1.4 Клонове и сливане
- A1.5 Споделяне и обновяване на проекти
- A1.6 Инспекция и сравнение
- A1.7 Дебъгване
- A1.8 Patching
- A1.9 Email команди
- A1.10 Външни системи
- A1.11 Административни команди
- A1.12 Plumbing команди
6.4 Настройване на Git - Примерна Git-Enforced политика
Примерна Git-Enforced политика
В тази секция ще видим как да изградим примерен Git работен процес, който проверява за специфичен формат на къмит съобщенията и позволява само на определени потребители да модифицират определени поддиректории в проекта. Ще напишем клиентски скриптове, които помагат на разработчика да разбере дали публикуванията му ще се отхвърлят и сървърни скриптове, които в действителност прилагат политиките.
Примерните скриптове са на Ruby, отчасти поради факта, че използвахме езика в примерите дотук, но и също така защото Ruby е лесен за четене дори да не можете да програмирате на него. Обаче, всеки език би следвало да е приложим — всички примерни hook скриптове фабрично доставени с Git са или на Perl или на Bash.
Сървърен Hook
Цялата работа от страна на сървъра ще се върши от файла update
в директорията hooks
.
Този update
hook се стартира еднократно за всеки клон, в който се публикува и получава три аргумента:
-
Името на клона в който се прави push
-
Старата версия, в която е бил клона
-
Новата версия, която се публикува
Имате също така достъп до потребителя, който публикува, ако това се прави през SSH.
Ако сте позволили на всеки да се свързва с едно и също потребителско име (като “git”) с public-key автентикация, може да трябва да дадете на въпросния потребителски акаунт shell wrapper, който определя реалния потребител чрез информацията от публичния ключ и съответно пази резултата в environment променлива.
За нашия случай приемаме, че свързващият се реален потребител се пази в променливата $USER
, така че update скриптът започва търсейки това:
#!/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]})"
Да, това са глобални променливи. Не е правилно, но е по-лесно за демонстрация.
Налагане на специфичен Commit-Message формат
Първата ни задача е да задължим потребителя да следва специфичен зададен от нас формат за всяко къмит съобщение. Приемаме че всяко съобщение трябва да включва стринг, който изглежда като “ref: 1234”, защото искате всеки къмит да препраща към елемент от вашата ticketing система. Трябва да претърсвате всеки изпратен къмит, да проверявате дали стрингът присъства в съобщението и ако липсва дори в едно от всички, да подадете неуспешен код за изход от скрипта, така че push операцията да бъде отказана.
Можете да получите списък на всички SHA-1 стойности за къмитите, които се публикуват като подадете стойностите в $newrev
и $oldrev
към plumbing командата на Git известна като git rev-list
.
Това е по същество git log
командата, но по подразбиране печата само SHA-1 стойности и никаква друга информация.
И така, за да получите списък на всички чексуми на къмити появили се между два дадени къмита, можете да изпълните нещо такова:
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
Можете да ползвате този резултат, да преминете с цикъл през всяка от SHA-1 стойностите, да извлечете съобщението за всеки къмит и да го тествате с регулярен израз дали отговаря на дадения шаблон.
Сега трябва да откриете как да получите къмит съобщението за всеки от къмитите.
За да получите суровите данни за къмит, може да използвате друга plumbing команда, git cat-file
.
Ще разгледаме в подробности plumbing командите в 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
Change the version number
Прост начин да извлечете съобщението от данните за къмит, когато имате SHA-1 стойността, е да отидете на първия празен ред от данните и да вземете всичко след това.
Това може да се направи с командата sed
под Unix системи:
$ git cat-file commit ca82a6 | sed '1,/^$/d'
Change the version number
Може да приложите това за всеки къмит, който се опитва да бъде публикуван и да излезете със съответния код, ако нещо не е както се очаква да бъде. За да прекратите скрипта и отхвърлите push операцията, подайте non-zero код за изход от него. Целият метод изглежда така:
$regex = /\[ref: (\d+)\]/
# enforced custom commit message format
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
Така изглеждащ, вашият update
скрипт ще отхвърля всички публикувания, чиито къмит съобщения не съответстват на зададеното от вас правило.
Прилагане на User-Based ACL система
Представете си, че искате да добавите механизъм, който използва access control list (ACL) и определящ кои потребители до кои части от проектите ви могат да публикуват.
Искате някои да имат пълен достъп, а други само до определени поддиректории или специфични файлове.
За да реализираме това, ще попълним правилата във файл с име acl
, който се съхранява в bare хранилището на Git сървъра.
Нашият update
hook ще следи за тези правила, ще определи кои файлове ще се променят във всички изпратени къмити и ще реши дали потребителят, който опитва обновяването ще има достъп до тях.
Първо, ще попълним нашия ACL.
Тук използваме формат подобен на CVS ACL механизма: той използва серия от редове, където първото поле е avail
или unavail
, следващото поле е разделен със запетаи списък на потребителите, за които ще важат правилата и последното поле е пътя, към който се прилага правилото (празната стойност тук означава пълен достъп).
Всички тези полета се разделят със символа pipe (|
).
В този случай, имате няколко администратора, имате хора пишещи документация с достъп до директорията doc
и един разработчик с достъп само до директориите lib
и tests
, така че ACL файлът изглежда така:
avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
Започваме прочитайки тези данни в използваема структура.
За да е опростен примерът, ще използваме само avail
директиви.
Ето един метод да получите асоциативен масив, в който ключовете на елементите са имената на потребителите, а стойностите им — масив с пътищата до които потребителят има достъп:
def get_acl_access_data(acl_file)
# read in ACL data
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
Предвид ACL файла, който имаме, този get_acl_access_data
метод ще ни върне структура от данни изглеждаща така:
{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}
Сега имаме правилата за достъп и следва да определим какви са пътищата, които публикуваните къмити опитват да променят и дали съответния потребител има достъп до всички тях.
Можете лесно да видите кои са файловете засегнати от един къмит с флага --name-only
на командата git log
(упоменато в Основи на Git):
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
Ако използвате ACL структурата върната от метода get_acl_access_data
и я сравните със списъка от засегнати файлове за всеки от къмитите, може да определите дали потребителят ще има push достъп за всеки от къмитите си:
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('acl')
# see if anyone is trying to push something they can't
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 # user has access to everything
|| (path.start_with? access_path) # access to this 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
(файла в който се съдържа всичкия ни Ruby код) и след това опитате да публикувате къмит с неотговарящо на правилата съобщение, получавате нещо подобно:
$ 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'
Тук има няколко интересни неща. Първо, виждате следното съобщение, когато hook-ът стартира:
Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)
Спомнете си, че отпечатахте това в самото начало на update скрипта.
Всичко, което скриптът изпраща в 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, който уведомява че update скриптът е завършил с код за неуспех и че публикуването е отказано. Последно, имаме това:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
Ще видим remote rejected съобщение за всяка от референциите, които hook-ът е отхвърлил и то изрично упоменава, че отхвърлянето идва именно от неуспешно завършил hook.
Аналогично, ако някой се опита да промени файл, за който няма права за достъп, ще види нещо подобно.
Например, ако автор на документация се опита да публикува къмит модифициращ съдържанието на lib
директорията, то той ще види
[POLICY] You do not have access to push to lib/test.rb
Отсега нататък, стига update
скриптът да е наличен и изпълним, вашето хранилище няма да приема къмити, чиито съобщения не съдържат вашата задължителна част в себе си и също така всеки потребител ще може да публикува само там, където трябва да може.
Клиентски Hooks
Недостатъкът на този подход са неизбежните оплаквания, които трябва да очаквате от колегите ви, чиито публикувания са били отхвърлени. Да се окажат в положение, при което внимателно свършената им работа бива отхвърлена в последния момент може да бъде изключително конфузно и изнервящо, защото те ще трябва да редактират историята си, за да отговорят на правилата — нещо на което никой няма да се зарадва.
Отговорът на този проблем е да осигурите клиентски hook скриптове, които потребителите могат да използват за да разберат дали нещо от работата им евентуално би могло да бъде отхвърлено на сървъра.
По този начин те могат да коригират всички проблеми преди къмитване и преди да станат трудни за оправяне.
Понеже hook скриптовете не се разпространяват когато клонирате проект, ще трябва да ги изпратите по някакъв друг начин и да се уверите, че потребителите са ги копирали в своите .git/hooks
директории и че са ги направили изпълними.
Можете да ги изпращате като част от проекта или като отделен проект, но Git няма да ги настрои автоматично.
Ако се поставите в ролята на вашите потребители, отначало трябва да проверите дали къмит съобщението ви отговаря на правилата още преди да направите къмита.
За целта, може да използвате commit-msg
hook-а.
Ако го направите така, че да чете съобщението от файл, който му се подава като първи аргумент и сравните това съобщение с даден шаблон, може да накарате 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
Ако този скрипт е на правилното място (в .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 файла, който използвахме преди малко, тогава следния pre-commit
скрипт ще направи проверката локално за вас:
#!/usr/bin/env ruby
$user = ENV['USER']
# [ insert acl_access_data method from above ]
# only allows certain users to modify certain subdirectories in a project
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
Това е почти същия скрипт като този на сървъра, но с две важни разлики.
Първо, ACL файлът е на различно място, защото този скрипт се изпълнява от работната ви директория, а не от .git
директорията.
Затова трябва да смените пътя до него от това:
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`
Изключая тези две разлики, скриптът работи по същия начин.
Един от недостатъците му е, че очаква да бъде изпълнен локално от същия потребител като този, който публикува на сървъра.
Ако това не е така, ще трябва също да коригирате и $user
променливата ръчно.
Друго нещо, което можем да направим тук е да се уверим, че потребителят не се опитва да публикува non-fast-forward референции. За да получим подобна референция, можем да пребазираме последния публикуван къмит или да се опитаме да публикуваме различен локален клон към оригиналния отдалечен такъв.
Приемаме, че сървърът вече е конфигуриран с receive.denyDeletes
и receive.denyNonFastForwards
за да форсира тази политика, така че единственото нещо, което може да се опитате да засечете е пребазирането на вече публикувани къмити.
Ето един примерен 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
Този скрипт използва синтаксис, който не разгледахме в Избор на къмити. Получавате списък на публикуваните къмити с това:
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
Синтаксисът SHA^@
отговаря на всички родители на този къмит.
Търсите за всеки къмит, който е достъпен от последния отдалечен къмит и който не е достъпен от кой да е родител от всички SHA-1 стойности, които се опитвате да публикувате — това означава че е fast-forward.
Основният недостатък на този подход е, че той може да е много бавен и често ненужен — ако не се опитвате да форсирате push операцията с -f
, то сървърът така или иначе ще ви предупреди и няма да я приеме.
Все пак, това е интересно упражнение и на теория може да ви помогне да избегнете пребазирания, които по-късно да се налага да коригирате.