-
1. Primeiros Passos
- 1.1 Sobre Controle de Versão
- 1.2 Uma Breve História do Git
- 1.3 O que é Git?
- 1.4 A Linha de Comando
- 1.5 Instalando o Git
- 1.6 Configuração inicial do Git
- 1.7 Obtendo Ajuda
- 1.8 Resumo
-
2. Fundamentos do Git
-
3. Ramificação (Branching) no Git
-
4. Git no Servidor
- 4.1 Os Protocolos
- 4.2 Colocando o Git em um Servidor
- 4.3 Gerando a sua Chave Pública SSH
- 4.4 Configurando o Servidor
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Opções de Hospedagem de Terceiros
- 4.10 Resumo
-
5. Git Distribuído
-
6. GitHub
-
7. Ferramentas do Git
- 7.1 Seleção de Revisão
- 7.2 Área de Stage Interativa
- 7.3 Fazendo Stash e Limpando
- 7.4 Assinando Seu Trabalho
- 7.5 Pesquisando (Searching)
- 7.6 Reescrevendo o Histórico
- 7.7 Reset Desmistificado
- 7.8 Merging Avançado
- 7.9 Rerere
- 7.10 Depurando com Git
- 7.11 Submódulos
- 7.12 Empacotando (Bundling)
- 7.13 Substituir (Replace)
- 7.14 Armazenamento de Credenciais
- 7.15 Resumo
-
8. Customizando o Git
- 8.1 Configuração do Git
- 8.2 Atributos do Git
- 8.3 Hooks do Git
- 8.4 Uma Política de Exemplo Imposta pelo Git
- 8.5 Resumo
-
9. Git e Outros Sistemas
- 9.1 Git como Cliente
- 9.2 Migrando para o Git
- 9.3 Resumo
-
10. Git Internals (Por Dentro do Git)
- 10.1 Encanamento e Porcelana (Plumbing and Porcelain)
- 10.2 Objetos do Git
- 10.3 Referências do Git
- 10.4 Arquivos de pacote (Packfiles)
- 10.5 Refspec
- 10.6 Protocolos de Transferência
- 10.7 Manutenção e Recuperação de Dados
- 10.8 Variáveis de Ambiente
- 10.9 Resumo
-
A1. Appendix A: O Git em Outros Ambientes
- A1.1 Interfaces Gráficas
- A1.2 Git no Visual Studio
- A1.3 Git no Visual Studio Code
- A1.4 Git no IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine
- A1.5 Git no Sublime Text
- A1.6 Git no Bash
- A1.7 Git no Zsh
- A1.8 Git no PowerShell
- A1.9 Resumo
-
A2. Appendix B: Incorporando o Git nos seus Aplicativos
- A2.1 Linha de Comando do Git
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. Appendix C: Comandos do Git
- A3.1 Configuração e Ajustes
- A3.2 Obtendo e Criando Projetos
- A3.3 Snapshots Básicos
- A3.4 Branches e Merges
- A3.5 Compartilhando e Atualizando Projetos
- A3.6 Inspeção e Comparação
- A3.7 Depuração
- A3.8 Patches
- A3.9 E-mail
- A3.10 Sistemas Externos
- A3.11 Administração
- A3.12 Comandos de Encanamento
8.4 Customizando o Git - Uma Política de Exemplo Imposta pelo Git
Uma Política de Exemplo Imposta pelo Git
Nesta seção, você usará o que aprendeu para estabelecer um fluxo de trabalho do Git que verifica um formato de mensagem de commit personalizado e permite que apenas determinados usuários modifiquem determinados subdiretórios em um projeto. Você construirá scripts do cliente que ajudam o desenvolvedor a saber se seu push será rejeitado e scripts do servidor que de fato impõem as políticas.
Os scripts que mostraremos são escritos em Ruby; em parte por causa de nossa inércia intelectual, mas também porque o Ruby é fácil de ler, mesmo que você não saiba necessariamente escrevê-lo. No entanto, qualquer linguagem funcionará – todos os scripts de gancho de amostra distribuídos com o Git estão em Perl ou Bash; portanto, você também pode ver muitos exemplos de ganchos nessas linguagens examinando as amostras.
Gancho no Lado do Servidor
Todo o trabalho no lado do servidor irá para o arquivo update no seu diretório hooks.
O gancho update é executado uma vez por branch que sofre push e recebe três argumentos:
-
O nome da referência que sofre push
-
A revisão antiga de onde esse branch estava
-
A nova revisão sendo enviada (push)
Você também tem acesso ao usuário que faz o push se o push estiver sendo executado via SSH.
Se você permitiu que todos se conectassem a um único usuário (como “git”) por meio de autenticação de chave pública, pode ser necessário fornecer a esse usuário um invólucro do shell (shell wrapper) que determine qual usuário está se conectando com base na chave pública e definir uma variável de ambiente conforme necessário.
Aqui vamos assumir que o usuário conectado está na variável de ambiente $USER; portanto, o script de atualização (update) começa reunindo todas as informações necessárias:
#!/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]})"
Sim, essas são variáveis globais. Não julgue – é mais fácil demonstrar dessa maneira.
Impondo um Formato de Mensagem de Commit Específico
Seu primeiro desafio é impor que cada mensagem de commit siga a um formato específico. Apenas para ter um objetivo, assuma que cada mensagem deve incluir uma string que se pareça com “ref: 1234” porque você deseja que cada commit seja vinculado a um item de trabalho em seu sistema de bilhetagem (ticketing). Você deve examinar cada commit sendo enviado via push, ver se essa string está na mensagem de commit e, se a string estiver ausente de algum dos commits, sair sem zero, para que o push seja rejeitado.
Você pode obter uma lista dos valores de SHA-1 de todos os commits que sofrem push obtendo os valores $newrev e $oldrev e passando-os a um comando de encanamento do Git chamado git rev-list.
Este é basicamente o comando git log, mas por padrão ele imprime apenas os valores do SHA-1 e nenhuma outra informação.
Portanto, para obter uma lista de todos os SHA-1 de commit introduzidos entre um SHA-1 de commit e outro, você pode executar algo como isto:
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
Você pode obter essa saída, percorrer cada um desses SHA-1s de commit, pegar a mensagem para ele e testar essa mensagem contra uma expressão regular que procura por um padrão.
Você precisa descobrir como obter a mensagem de commit de cada um desses commits para testar.
Para obter os dados brutos de commit, você pode usar outro comando de encanamento chamado git cat-file.
Analisaremos todos esses comandos de encanamento em detalhes em Git Internals (Por Dentro do Git); mas, por enquanto, eis o que esse comando fornece a você:
$ 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
Uma maneira simples de obter a mensagem de commit de um commit quando você tem o valor de SHA-1 é ir para a primeira linha em branco e levar tudo a partir de lá.
Você pode fazer isso com o comando sed nos sistemas Unix:
$ git cat-file commit ca82a6 | sed '1,/^$/d'
Change the version number
Você pode usar essa técnica para pegar a mensagem de commit de cada commit que estiver tentando fazer push e sair se você ver algo que não corresponde. Para sair do script e rejeitar o push, saia sem zero. O método inteiro se parece com isso:
$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
Colocar isso em seu script update rejeitará atualizações que contenham commits que possuam mensagens que não estejam de acordo com sua regra.
Impondo um Sistema ACL Baseado em Usuário
Suponha que você queira adicionar um mecanismo que use uma lista de controle de acesso (ACL - Access Control List) que especifica quais usuários têm permissão para fazer push das alterações para quais partes de seus projetos.
Algumas pessoas têm acesso total e outras podem fazer push das alterações apenas a determinados subdiretórios ou arquivos específicos.
Para impor isso, você gravará essas regras em um arquivo chamado acl que reside no seu repositório Git bare no servidor.
Você fará com que o gancho update analise essas regras, veja quais arquivos estão sendo introduzidos para todos os commits sendo submetidos a um push e determine se o usuário que realiza o push tem acesso para atualizar todos esses arquivos.
A primeira coisa que você fará será escrever sua ACL.
Aqui você usará um formato muito parecido com o mecanismo de ACL do CVS: ele usa uma série de linhas, onde o primeiro campo é avail (disponível) ou unavail (indisponível), o próximo campo é uma lista delimitada por vírgula dos usuários aos quais a regra se aplica, e o último campo é o caminho ao qual a regra se aplica (em branco significa acesso aberto).
Todos esses campos são delimitados por um caractere pipe (|).
Nesse caso, você tem alguns administradores, alguns escritores de documentação com acesso ao diretório doc e um desenvolvedor que só tem acesso aos diretórios lib e tests, e seu arquivo ACL se parece com este:
avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
Você começa com a leitura desses dados em uma estrutura que você pode usar.
Nesse caso, para manter o exemplo simples, você só executará as diretivas avail.
Aqui está um método que fornece um array associativo em que a chave é o nome de usuário e o valor é um array de caminhos para o qual o usuário tem acesso de gravação:
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
No arquivo ACL examinado anteriormente, esse método get_acl_access_data retorna uma estrutura de dados semelhante a esta:
{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}
Agora que você classificou as permissões, você precisa determinar quais caminhos os commits submetidos a push foram modificados, para garantir que o usuário que estiver fazendo o push tenha acesso a todos eles.
Você pode ver facilmente quais arquivos foram modificados em um único commit com a opção --name-only do comando git log (mencionado brevemente em Fundamentos do Git):
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
Se você usar a estrutura da ACL retornada pelo método get_acl_access_data e a verificar com os arquivos listados em cada um dos commits, poderá determinar se o usuário tem acesso a todos os seus commits de 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
Você obtém uma lista de novos commits sendo enviados para o servidor com git rev-list.
Em seguida, para cada um desses commits, você descobre quais arquivos foram modificados e verifica se o usuário que está fazendo push tem acesso a todos os caminhos sendo modificados.
Agora, seus usuários não podem fazer um push para qualquer commit com mensagens mal formadas ou arquivos modificados fora de seus caminhos designados.
Testando
Se você executar chmod u+x .git/hooks/update, que é o arquivo no qual você deve ter colocado todo esse código, e tentar dar um push de um commit com uma mensagem não compatível, você obterá algo parecido com isto:
$ 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'
Existem algumas coisas interessantes aqui. Primeiro, você vê onde o gancho começa a rodar.
Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)
Lembre-se de que você o imprimiu no início do script de atualização (update script).
Qualquer eco de script para stdout será transferido para o cliente.
A próxima coisa que você notará é a mensagem de erro.
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
A primeira linha foi impressa por você, as outras duas era o Git dizendo a você que o script de atualização (update script) saiu sem ser zero e foi isso que negou o seu push. Por fim, você tem isso:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
Você verá uma mensagem rejeitada remota (remote rejected) para cada referência que o seu gancho recusou e diz a você que ela foi recusada especificamente por causa de uma falha de gancho.
Além disso, se alguém tenta editar um arquivo no qual eles não têm acesso e dar um push de um commit contendo ele, eles verão algo parecido.
Por exemplo, se um autor da documentação tenta dar um push em um commit modificando algo no diretório lib, ele vê:
[POLICY] You do not have access to push to lib/test.rb
A partir de agora, contanto que o script de atualização (update script) esteja lá e que seja executável, seu repositório nunca terá uma mensagem de commit que não contenha o seu padrão, e seus usuários ficarão contidos.
Ganchos do Lado do Cliente
A desvantagem dessa abordagem são as reclamações que inevitavelmente resultarão quando os commits de push de seus usuários forem rejeitados. Ter seu trabalho cuidadosamente elaborado e ser rejeitado no último minuto pode ser extremamente frustrante e confuso; e, além disso, eles terão que editar seu histórico para corrigi-lo, o que nem sempre é para os fracos de coração.
A resposta para esse dilema é fornecer alguns ganchos do lado do cliente (client-side hooks) que os usuários podem executar para notificá-los quando estão fazendo algo que o servidor provavelmente rejeitará.
Dessa forma, eles podem corrigir quaisquer problemas antes de comitar e antes que esses problemas se tornem mais difíceis de resolver.
Como os ganchos não são transferidos com o clone de um projeto, você deve distribuir esses scripts de outra forma e, em seguida, pedir aos usuários que os copiem para seu diretório .git/hooks e os tornem executáveis.
Você pode distribuir esses ganchos dentro do projeto ou em um projeto separado, mas o Git não os configurará automaticamente.
Para começar, você deve verificar sua mensagem de commit logo antes de cada commit ser gravado, para saber que o servidor não rejeitará suas alterações devido a mensagens de commit mal formatadas.
Para fazer isso, você pode adicionar o gancho commit-msg.
Se você fizer com que ele leia a mensagem do arquivo passado como o primeiro argumento e os comparar com o padrão, poderá forçar o Git a abortar o commit, se não houver correspondência:
#!/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
Se o script estiver em vigor (em .git/hooks/commit-msg) e ser um executável, e você fizer um commit com uma mensagem que não esteja formatada corretamente, você verá isto:
$ git commit -am 'Test'
[POLICY] Your message is not formatted correctly
Nenhum commit foi concluído nessa instância. No entanto, se sua mensagem contém o padrão adequado, o Git permite que você comite:
$ git commit -am 'Test [ref: 132]'
[master e05c914] Test [ref: 132]
1 file changed, 1 insertions(+), 0 deletions(-)
Em seguida, você deseja certificar-se de que não está modificando arquivos que estejam fora do escopo da sua ACL.
Se o diretório .git do seu projeto contiver uma cópia do arquivo ACL usado anteriormente, o script de pre-commit subsequente imporá essas restrições para você:
#!/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
Isto é, grosseiramente, o mesmo script do que a parte do lado do servidor (server-side), mas com duas diferenças importantes.
Primeiro, o arquivo ACL fica em um lugar diferente, porque este script roda a partir do seu diretório de trabalho, não do seu diretório .git.
Você tem que mudar o caminho para o arquivo ACL disso:
access = get_acl_access_data('acl')
para isto:
access = get_acl_access_data('.git/acl')
A outra diferença importante é a maneira de obter uma lista dos arquivos que foram alterados. Como o método do servidor (server-side) examina o log de commits e, nesse ponto, o commit ainda não foi gravado, você deve obter sua lista de arquivos da área de preparação (staging area). Em vez de:
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
você tem de usar:
files_modified = `git diff-index --cached --name-only HEAD`
Mas essas são as duas únicas diferenças – por outro lado, o script funciona da mesma forma.
Uma advertência é que ele espera que você esteja executando localmente como o mesmo usuário com o qual realiza o push para a máquina remota.
Se for diferente, você deve definir a variável $user manualmente.
Outra coisa que podemos fazer aqui é ter a certeza de que o usuário não faça um push nas referências non-fast-forwarded. Para obter uma referência que não seja um avanço rápido (fast-forward), é necessário rebasear um commit no qual você já enviou via push ou tentar colocar em push um branch local diferente para o mesmo branch remoto.
Presumivelmente, o servidor já está configurado com receive.denyDeletes e receive.denyNonFastForwards para impor esta política, de modo que a única coisa acidental que você pode tentar buscar é rebasear (rebasing) commits que já foram passados por um push.
Aqui está um exemplo de script pré-rebase que verifica isso. Ele recebe uma lista de todos os commits que você está prestes a reescrever e verifica se eles existem em alguma de suas referências remotas. Se ele vir um que esteja acessível em uma das suas referências remotas, ele aborta a refazagem (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
Este script usa uma sintaxe que não foi coberta em Seleção de Revisão. Você obtém uma lista dos commits que já sofreram o push executando isso:
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
A sintaxe SHA^@ resolve a todos os pais do commit em questão.
Você está procurando por qualquer commit que seja acessível a partir do último commit no controle remoto e que não seja acessível a partir de nenhum pai de nenhum dos SHA-1s em que você está tentando fazer push - ou seja, é um avanço rápido (fast-forward).
A principal desvantagem dessa abordagem é que ela pode ser muito lenta e geralmente é desnecessária - se você não tentar forçar o push com -f, o servidor o avisará e não aceitará o push.
No entanto, é um exercício interessante e pode, em teoria, ajudá-lo a evitar um rebase no qual você pode ter que voltar e consertar posteriormente.