====== Git ======
//Git// é um sistema de controle de versionamento de código aberto projetado para a manipulação de projetos com rapidez e eficiência.
//Git// é um sistema de controle de versionamento rápido, escalável e distribuído com um conjunto rico de comandos para prover um alto nível de operabilidade.
Este link apresenta as [[infotic:sistemas:git:definicao|abstrações]] empregadas pelo Git.
===== Instalando =====
Os binários do Git podem ser obtidos no endereço [[https://git-scm.com/download]].
No Windows é comum o Git ser executado no [[git:comandos#git_bash|Git Bash]] que é um terminal com mais recursos que o terminal nativo do windows ''cmd.exe''.
===== Verificando a versão =====
Para verificar a versão do Git instalada em sua máquina podemos utilizar o comando
$ git --version
===== Clientes gráficos =====
Apesar do Git ser uma ferramenta de linha de comando, dispomos de alguns programas com interface gráfica para trabalhar com o Git:
* [[git:git#git_gui|Git Gui]]
* [[gitlab:gitlab|GitLab]]
===== Configurando =====
Para usar o Git você precisa ter no mínimo a sua identidade configurada:
$ git config --global user.name "John Doe"
$ git config --global user.email "johndoe@example.com"
Configurações Globais
(mover para página infotic:git)
Para conseguir acesso aos projetos da TIC em https://git.ufrj.br, faça as seguintes configurações:
$ git config --global user.email ""
$ git config --global user.name ""
Onde ''e_mail'' e ''nome'' são seu e-mail e nome cadastrados no GitLab da TIC, respectivamente.
Crie um par de chaves SSH sem senha caso não tenha uma. Em geral, estarão no diretório ''.ssh'' de seu ''$HOME'' no Linux. Se nesse diretório tiver um par de arquivos ''id_rsa'' e ''id_rsa.pub'', então não precisa criar um novo par. Para criar um novo par, faça:
$ ssh-keygen
Vá para a sessão SSH Keys de seu perfil do GitLab e crie uma nova chave, copiando o conteúdo de ''id_rsa.pub'' para a caixa de texto 'Key'.
Para não haver conflitos com espaços em branco ao mesclar ramos, caso espaços em branco não sejam relevantes em seu projeto, faça:
$ git config –-global core.whitespace nowarn
===== Utilizando =====
Um repositório Git está contido em um diretório .git, que contem os históricos de revisão e outros metadados. O diretório rastreado pelo repositório, por padrão o diretório onde o .git está contido, é chamado de diretório de trabalho. Mudanças no diretório de trabalho devem ser marcadas (stagged) antes de serem gravadas (committed) no repositório. O Git também permite que você restaure árvores de diretórios previamentes gravadas (committed).
==== Obtendo um repositório ====
* [[git:comandos#iniciar_repositorio_novo|Iniciando um novo repositório]]
* [[git:comandos#clonando_um_repositorio|Clonando um repositório]]
==== Registrando mudanças ====
Os projetos Git possuem uma área de preparação (//staging area//), um arquivo de índice no diretório ''.git'' que armazena as mudanças que irão para o seu próximo //commit//. Para registrar um arquivo modificado você precisa primeiro adicioná-lo a esse índice. O comando ''git commit'' então irá gravar o índice atual de mudanças em um novo //commit//.
=== Preparando as mudanças ===
| [[git:comandos#marcar_para_ser_comitado|Adicionar mudanças no índice]] | ''git add pathspec'' |
| Remover mudanças do índice| ''git reset pathspec'' |
| Mostrar mudanças a serem //committed//, mudanças //unstaged//, e arquivos não rastreados| ''git status'' |
Além de [[git:comandos#adicionar_arquivos|adicionar arquivos]] com o comando ''git add'', podemos [[git:comandos#remover_arquivos|remover o rastreamento de um arquivo]] com o comando ''git rm'' e adicionar uma [[git:comandos#renomear_ou_mover_arquivos|renomeação ou movimentação de arquivo]] com o comando ''git mv''.
Mudanças em determinados arquivos e diretórios podem ser [[git:comandos#ignorar_arquivos_e_diretorios|ignoradas]] pelo Git através de um arquivo especial ''.gitignore''.
=== Gravando as mudanças ===
O comando ''git commit'' [[git:comandos#comitar_alteracoes|grava as mudanças]] no repositório (diretório ''.git'').
* ''-m'' -- fornece a mensagem do //commit// como um argumento ao invés de abrir o seu editor de textos para escrevê-la.
* ''-a'' -- automaticamente adiciona ao índice arquivos que foram modificados ou deletados.
* ''--amend'' -- [[git:comandos#refazer_o_ultimo_commit|refaz o último commit]], consertando a mensagem ou os arquivos //committed//.
=== Seleção da revisão ===
O Git oferece diversas formas de especificar revisões. Muitos comandos Git recebem revisões como argumentos. Um //commit// pode ser identificado por qualquer umas das formas a seguir:
* O código hash do //commit// (todo ou apenas os 7 primeiros dígitos).
* Qualquer rótulo de //commit// como um nome de ramo (//branch//) ou etiqueta (//tag//).
* O rótulo ''HEAD'' sempre se refere ao //commit// respectivo do diretório de trabalho ativo (//checked-out//). Ou seja, pode ser o último //commit// do ramo, ou um //commit// antigo se você usou o ''git checkout'' para saltar para uma revisão antiga.
* Qualquer uma das opções acima combinada com o ''~''. Por exemplo, ''HEAD~'' refere-se à um //commit// antes do ''HEAD'' e ''HEAD~5'' refere-se à cinco //commits// antes do ''HEAD''.
=== Visualizando as mudanças ===
O comando a seguir mostra a diferença entre dois //commits//:
$ git diff HEAD HEAD~3
ou entre a área de preparação (//staging area//) e a árvore do diretório de trabalho:
$ git diff
O comando a seguir mostra o histórico de mudanças:
$ git log -p
Veja mais [[git:comandos#visualizar_alteracoes| aqui]]
===== Ramos =====
Para ver a lista de ramos e o ramo atual:
$ git branch
Para ver mais informações sobre os ramos, inclusive se os mesmos estão à frente, atrás, ambos (há modificações locais e no remoto), igual ou é um ramo novo, em relação ao que está nas últimas referências aos ramos
do repositório (pegos no último ''fetch''), use o parâmetro ''-vv''.
Para criar um ramo baseado no ramo atual (o novo será idêntico ao atual):
$ git checkout -b
Para apagar um ramo:
$ git branch -d
Esse comando não permite apagar um ramo que não tenha sido mesclado em outro. Para apagar mesmo assim, troque o ''-d'' por ''-D'' (**cuidado: se o ramo não tiver sido jogado para o servidor, não poderá ser recuperado!**).
Note que o ramo atual não pode ser o que será apagado. Para isso, é necessário mudar para outro ramo, o que pode ser feito com:
$ git checkout
Para mesclar dois ramos, faça:
$ git merge
o que fará com que as mudanças do ramo com ''nome_ramo'' sejam jogadas no ramo atual. Se a mesclagem não puder ser feita automaticamente por causa de conflitos (alterações em ambos os ramos nas mesmas partes de determinados arquivos), terá que abrir os arquivos e resolver os conflitos. Haverá marcas no texto mostrando os conflitos (''<<<<<<<''). Resolva-os, marque os arquivos para serem comitados e comite-os para finalizar o processo de mesclagem. Se a mesclagem for feita automaticamente, não será necessário comitá-la, pois isso será feito automaticamente.
Quando há conflitos numa mesclagem, é possível que se queira pegar apenas o que foi feito em um dos ramos e descartar as alterações do outro. Para fazer isso automaticamente, use a opção ''-Xours'' para ficar com as alterações conflitantes do ramo corrente ou ''-Xtheirs'', caso queira as do ramo passado como parâmetro durante a mesclagem.
Caso queira abortar o processo de resolução de conflitos por algum motivo, faça:
$ git merge --abort
Infelizmente não é possível abortar uma mesclagem que tenha bem sucedida (sem conflitos) com esse comando. Será necessário fazer um ''reset --hard''
===== Rebase =====
Quando se faz mesclagem de ramos, ao olhar o histórico de //commits//, nota-se que ele muitas vezes não fica linear, ficando parecendo mais um grafo. Para deixar o histórico mais organizado, é possível usar o ''rebase'', que joga os //commits// do ramo que será juntado após o outro. Suponha que um ramo ''B'' tenha sido criado a partir do ''A'', foi alterado, e o ''A'' também tenha sido alterado por outro desenvolvedor. Ao invés de aplicar o comando ''merge'' para jogar o conteúdo de ''B'' em ''A'', ao aplicar o ''rebase'', todos os commits que foram feitas em ''B'' serão colocados por último em cima do ''A''. É como se o ''A'' fosse pego já com as alterações do outro desenvolvedor e só aí as alterações que foram feitas em ''B'' fossem feitas. Então, ao invés de fazer:
$ git checkout A
$ git merge B
faz-se:
$ git checkout B
$ git rebase A
$ git checkout A
$ git merge B
Para simplificar, pode-se fazer:
$ git rebase A B
$ git checkout A
$ git merge B
O primeiro comando acima basicamente diz: "reaplique as alterações de ''B'' no topo de ''A''".
Com ''rebase'' dá para fazer operações mais sofisticadas para deixar a árvore de //commits// mais organizada. Dê uma olhada no manual para ver mais opções.
**Cuidado!** Não faça ''rebase'' em //commits// que já foram para o servidor remoto se trabalhar com outros desenvolvedores. Se fizer isso poderá bagunçar o trabalho de seus colegas!
Assim como no caso do ''merge'', o ''rebase'' também pode gerar conflitos, mas a maneira de lidar com eles é diferente. A medida que o Git vai reaplicando os //commits//, se der um conflito em um, ele para para que o desenvolvedor resolva os conflitos e só aí continua com o comando. O ''rebase'' pode facilitar bastante caso um ''merge'' gere muitos conflitos, pois os mesmos serão resolvidos aos poucos.
O ''rebase'' também pode servir para simplesmente alterar //commits// antigos do histórico, já que o ''commit --ammend'' não permite alterar //commits// do histórico. Basta fazer:
$ git rebase -i HEAD~
Esse comando vai permitir alterar os últimos ''n + 1'' //commits// de maneira iterativa. Novamente, não deve se fazer isso para //commits// que já foram para o repositório remoto, pois isso vai alterar os //hashes// dos //commits// (para o Git serão novos objetos de //commit//).
O ''rebase'' iterativo também permite a remoção, mudança de ordem e junção (//squash//) de //commits//.
===== Alterar Vários Commits de uma vez =====
Suponha que seja necessário tirar ou adicionar um arquivo em todo o histórico de //commits// do Git, ou ainda alterar informações em um arquivo em todo esse histórico. Isso é possível com o comando ''filter-branch''. Esse comando pode ser muito útil. Porém, novamente, não deve ser usado se o repositório já tiver sido publicado e outro desenvolvedor tenha trabalhado no projeto. Esse comando tem várias opções que podem ser exploradas para alterar toda uma árvore de //commits//.
Um exemplo importante de uso dele é se um arquivo com uma senha tiver sido comitado por engano; deve-se tirá-lo de todo o histórico do repositório.
Consulte a documentação para conhecer todos os parâmetros e comandos possíveis, para ver as alterações que o comando permite fazer em todo o repositório.
Como as alterações são radicais, rode o comando num ramo de teste e depois redefina (//hard-reset//) o ramo original para ficar com o mesmo estado desse ramo teste após se certificar que as alterações saíram como o esperado. Esse comando também permite alterar todos os ramos de uma vez usando a opção ''--all''.
===== Cherry-Pick =====
//Cherry-pick// significa escolher a dedo. Esse comando no Git serve para pegar um //commit// de um
ramo e jogar em outro. Isso pode ser útil quando apenas se quer pegar uma funcionalidade/correção de um ramo e jogar em outro. Por exemplo, há um ramo com várias alterações e apenas uma delas quer que vá para a próxima versão. Por isso é tão importante que cada //commit// seja algo único e completo.
Para rodar o comando, estando no ramo de destino, faça:
$ git cherry-pick
Ao fazer isso, o //commit// será aplicado no ramo de destino. Ao fazer isso, será criado um
novo //commit// no histórico, com outro //hash//.
===== Rerere =====
Muitas vezes a maneira de se resolver conflitos manualmente durante //merges// e //rebases// é semelhante. Portanto, seria interessante que o Git "aprendesse" e aplicasse algumas dessas soluções automaticamente. Para isso, existe o ''rerere''. Para permitir isso, faça:
$ git config --global rerere.enabled true
Ao invés de ligar o ''rerere'' automaticamente, é possível também rodar o comando durante um //merge//:
$ git merge
===== Repositório remoto =====
Para associar o repositório local a um remoto, faça:
$ git remote add origin
onde ''origin'' é um ''alias'' para o repositório remoto. É possível usar outro nome, mas ''origin'' é o nome padrão que se usa quando há apenas um repositório remoto (o que ocorre na maioria dos casos).
Para ver a URL do repositório remoto, faça:
$ git remote show origin
Para trocar a URL do repositório remoto, faça:
$ git remote set-url origin
Para acessar algo de um ramo remoto, como para fazer uma comparação, use:
origin/
Já se quiser criar um repositório local a partir de um remoto, faça:
$ git clone [origin]
Neste caso, apenas o ramo ''master'' local será criado como uma cópia idêntica do remoto (se ele existir). Para criar uma cópia local de outro ramo remoto, crie um ramo local a partir do ramo remoto desejado, preferencialmente com o mesmo nome.
Para mandar os //commits// no ramo local para o respectivo ramo remoto, faça:
$ git push origin
Já para mandar os //commits// de todos os ramos locais para os respectivos ramos remotos, faça:
$ git push origin --all
Para obter as atualizações do repositório remoto para o correspondente ramo local, faça:
$ git pull origin
Deve-se usá-lo sempre antes de começar a alterar os arquivo para não ter conflito com a versão que está no repositório remoto.
Já para pegar as atualizações do repositório remoto mas sem atualizar os ramos locais, faça:
$ git fetch origin
Dessa forma, as alterações vão ficar na referência ao ramos remotos e precisarão ser mescladas ao ramos locais.
Para apagar ramos remotos, quando não precisar mais deles, faça:
$ git fetch origin --delete
**Muito cuidado com esse comando para não apagar um ramo indevidamente!** Todavia, se o ramo tiver sido apagado recentemente, talvez seja possível resgatá-lo caso o coletor de lixo do Git ainda não tenha rodado.
===== Desfazer alterações =====
Para reverter um ou mais //commit//, faça
$ git revert
onde ''commit_hash'' é o //hash// do //commit// para o qual o ramo atual irá apontar.
Para voltar ao //commit// anterior, mas criando um novo //commit//, faça:
$ git revert HEAD
Para retirar um arquivo que foi marcado para ser comitado, faça:
$ git reset HEAD
Para fazer um arquivo modificado ser sobrescrito com o conteúdo do último //commit//, faça (cuidado pois todas as modificações serão perdidas!):
$ git checkout --
Para voltar ao //commit// anterior, mas descartando por completo todas as alterações, faça:
$ git reset --hard HEAD
**Cuidado com esse procedimento pois não há como recuperar!**
Já para fazer o ramo atual voltar ao estado de outro ramo, faça:
$ git reset --hard
===== Stash =====
Quando se está no meio de alterações em um ramo e é necessário trocar de ramo para alterá-lo, e depois voltar ao trabalho, não é necessário desfazer tudo ou comitar pela metade no ramo de origem. Existe um comando muito útil que evita essa trabalheira toda. No ramo de origem, faça:
$ git stash
Com isso, seu ramo voltará ao estado do último //commit//. As alterações ficarão salvas em uma pilha. Note que é possível fazer vários //stashs//, um em cima do outro, mas é algo que não costuma ser necessário. Agora, é possível fazer as alterações no outro ramo, comitá-las e voltar ao ramo anterior, onde deverá ser feito o comando:
$ git stash pop
**Atenção:**
Cuidado para não fazer o comando acima no ramo errado. Desfazer esse tipo de erro costuma ser bem complicado.
Desta forma, as modificações em andamento voltarão e poderão ser continuadas. Com o ''pop'', o que foi parar na pilha é removido. Já se usar ''apply'' ao invés de ''pop'', as alterações são reaplicadas e ainda podem ser utilizadas. Se a pilha tiver mais de um grupo de alterações, é possível aplicar uma delas com o comando:
$ git stash apply stash@{i}
onde ''i'' é o índice do histórico na pilha, começado em ''0''. Para remover da pilha um histórico de alterações não comitadas, faça:
$ git stash drop stash@{i}
===== Submódulos =====
Muitas vezes é interessante que um projeto possua código de um outro projeto dentro do diretório. Ou seja, internamente, dentro do repositório há uma pasta que possuirá outro repositório. Para isso, use o comando:
$ git submodule add
Com isso, será criada uma pasta dentro do diretório do projeto principal de acordo com o caminho passado, juntamente com o conteúdo do repositório. Se quiser usar o nome padrão do subprojeto, é só omitir o segundo parâmetro. Também será criado um arquivo ''.gitmodules'', semelhante ao ''.gitignore'', com uma entrada para cada submódulo utilizado pelo projeto.
Ao clonar um projeto com submódulos, é possível baixar todos os submódulos de uma vez ou não. Se passar a opção ''--recursive'' ao comando ''clone'', todos eles serão baixados. Caso contrário, terá que dar os seguintes comandos nas pastas que até então estarão vazias para baixar cada módulo:
$ git submodule init
$ git submodule update
===== Buscas =====
O ''grep'' é um comando Linux útil para buscar ocorrências de //strings// no projeto, mas não funciona para buscar ocorrências no histórico do repositório. Para isso, existe o comando ''git grep'', que é similar ao do terminal do Linux.
Para buscar quando determinada ocorrência passou a ou deixou de existir no repositório, deve-se usar o ''git log'' com o parâmetro ''-S'' concatenado com a ocorrência a ser buscada:
$ git log -S
===== Etiquetas =====
Quando uma versão é lançada, ou seja, quando o conteúdo do ramo ''master'' (ou outro ramo usado para produção) é posto em produção, é interessante guardar essa versão, caso alguma correção precise ser feita em versões anteriores ou o ramo ''master'' possa ser modificado logo após o lançamento com algo que não seja a correção de pequenos problemas. Se outro ramo for usado como sendo o de produção, a etiqueta deve ser feita nesse ramo.
Para fazer isso, apenas use o comando:
$ git tag -a -m ""
A //tag// é uma etiqueta e pode ser usada como se fosse um ramo. Porém, não é aconselhável modificá-la e mantê-la com o mesmo nome. Para listar todas as etiquetas, use o comando acima sem nenhum parâmetro.
Para listar todas as //tags//, faça:
$ git tag
Uma //tag// não é mandada para o repositório remoto automaticamente. Para fazê-lo (algo altamente recomendável), faça:
$ git push origin --tags
===== Apelidos =====
Com o ''alias'', você pode criar apelidos aos comando do GIT. Desta forma, comandos usados com frequência podem ser encurtados. Alguns exemplos:
$ git config –global alias.st status
$ git config –global alias.co checkout
Com isso, ao invés de digitar ''git status'' e ''git checkout'', poderá digitar ''git st'' e ''git co'', respectivamente.
===== Forks =====
Suponha que haja um projeto no Git a partir do qual queira iniciar um projeto novo que deve ter o desenvolvimento desvinculado do atual (não querendo fazer mesclagem com o mesmo). Ou seja, serão projetos que seguirão trajetórias distintas. Para isso, é possível fazer um //fork//, gerando um projeto idêntico do atual.
Usando o GitLab, pode-se clicar no botão Fork.
{{ :git:fork_1.jpg|}}
Ao fazer isso, terá que mover o projeto para outro //namespace//. Após fazer isso, se for o caso, renomeie o projeto e mova-o para o //namespace// original.
{{ :git:fork_2.jpg|}}
===== Limpeza =====
As operações no Git com o tempo podem deixá-lo com vários objetos soltos e arquivos que não estão no repositórios e que não serão usados perdidos. Além disso, há a criação de vários ramos que muitas vezes são criados e ficam esquecidos. Com isso, o Git fica lento e o repositório excessivamente grande. Para resolver o problema, há os comandos:
$ git gc --aggressive
para realizar coleta de lixo de objetos soltos.
$ git clean -d -f
para remover todos os arquivos que não fazem do repositório. Útil para jogar fora sobras de //build//, etc.
$ git clean -f -X
para remover apenas arquivos que são ignorados pelo Git; útil para limpar o diretório sem remover junto arquivos que foram criados manualmente. Com isso é possível, por exemplo, limpar o diretório de todos os arquivos gerados automaticamente (porque eles são ignorados pelo Git), sem apagar arquivos que você mesmo criou, como uma nova classe que ainda não foi comitada. Cuidado com os arquivos de configuração, como o ''.env'' do Laravel.
Para saber o que será excluído, use o ''-n'' para fazer um //dry-run//, que apenas mostrará o que será excluído. Outra opção é o ''-i'', para a exclusão ser iterativa.
$ git remote prune origin
Apaga a referência a ramos do seu ''origin'' local que já foram removidos do repositório remoto. Caso haja a possibilidade de algum ramo ter sido removido indevidamente do ''origin'', faça novo ''push'' a partir do seu Git antes de dar esse comando, pois ele não pode ser desfeito e se algo tiver sido apagado do ''origin'' indevidamente, será perdido para sempre. Se não tiver certeza de que pode remover as referências dos ramos remotos, use a opção ''--dry-run'' que mostrará quais ramos serão eliminados com essa opção.
===== Ajuda =====
Para saber detalhes sobre qualquer comando do Git, faça:
$ git --help
===== Troubleshooting =====
==== Forçar sobrescrita do repositório local pelo remoto ====
$ git fetch --all
$ git reset --hard origin/
===== Referências =====
* Git. Disponível em: https://git-scm.com/. Acessado em: 19/03/2019
* Git - ArchWiki. Disponível em: https://wiki.archlinux.org/title/Git. Acessado em: 26/08/2021