Introdução ao Git
Publicado em 16/03/2015
Antigamente, quando a gente escrevia usando lápis e papel, não era difícil vermo-nos obrigados a revirar o lixo, atrás de alguma coisa descartada por engano, muitas vezes sem sucesso… e lá se iam horas ou até mesmo dias de trabalho, irremediavelmente perdidos.
Sistemas de Controle de Versão
Com a advento da editoração eletrônica, esse tipo de problema cresceu sobremaneira. A facilidade de apagar um arquivo no computador, seja intencionalmente ou não, é enorme. Algumas vezes, como no caso dos papéis jogados no lixo, é possível recuperá-los, mas na maioria delas temos que recomeçar do zero.
Quando estamos criando alguma coisa, escrevendo um livro ou um programa de computador, por exemplo, vamos constantemente modificando o texto ou o código e, não raro, lamentamos ter sobrescrito algum trecho que gostaríamos de trazer de volta.
Quem já não passou por esse tipo de situação?
Pois é exatamente para tentar resolver, ou ao menos amenizar, esse problema, que surgiram os sistemas de controle de versão (VCS, do inglês Version Control System), também conhecidos como gerenciamento de código fonte (SCM, do inglês Source Code Management), ou simplesmente versionamento, em português.
Sistema de Controle de Versão
O contrôle de versão é um sistema que registra as mudanças num arquivo ou conjunto de arquivos ao longo do tempo de forma que se possa recuperar versões específicas futuramente.
Trataremos neste artigo do controle de versão para desenvolvimento de software, embora ele possa ser usado para quase todo o tipo de arquivo que existe num computador.
Na verdade, os VCS foram desenvolvidos exatamente para controlar o desenvolvimento de software, permitindo não apenas a reversão de um projeto para um estado anterior, mas também para comparar modificações ao longo do tempo, descobrir quem introduziu determinada mudança que esteja causando problemas, quem apresentou uma sugestão ou reportou um bug, e muito mais.
O Diferencial do Git
A Wikipédia lista entre as soluções de código aberto mais comuns: CVS, Mercurial, Git e SVN; e entre as comerciais, destaca: SourceSafe, TFS, PVCS (Serena) e ClearCase.
A primeira pergunta que surge, então, é: o que torna o Git diferente dos outros VCS e qual a razão da sua crescente popularidade?
Certamente a diferença mais óbvia é que o Git é um sistema distribuído (ao contrário do SVN ou TFS, por exemplo). Isso significa que você possui um repositório local que fica numa pasta especial chamada .git
e normalmente (mas não obrigatoriamente), tem um repositório remoto central onde diferentes colaboradores podem contribuir para o desenvolvimento do código. Observe que cada um desses colaboradores possui um clone exato do repositório em suas estações de trabalho locais.
Na documentação do Git há uma figura, reproduzida abaixo, que ilustra perfeitamente o controle de versão baseado num sistema distribuído.
Como podemos ver, nos sistemas distribuídos, os clientes não apenas verificam o último snapshot (instantâneo) dos arquivos, mas espelham o repositório inteiro. Uma enorme vantagem dessa solução é que, se porventura o servidor perder os dados por algum problema técnico, eles podem ser recuperados de qualquer um dos clientes que estejam colaborando no projeto, pois cada clone possui um backup completo dos dados.
Quem quiser se aprofundar mais no funcionamento cliente/servidor dos sistemas de controle de versão, poderá tomar a referência mencionada da Wikipédia como ponto de partida.
A seguir, numa adaptação do excelente artigo de Juri Strumpflohner, publicado no seu blog em abril de 2013, vamos analisar o funcionamento do Git observando o repositório Git sob o ponto de vista das árvores que ele constrói. Para isso, utilizaremos algumas funcionalidades comuns, tais como:
- acrescentar/modificar um arquivo novo;
- criar e mesclar uma branch (ramificação) com e sem conflitos de mesclagem;
- ver o histórico/changelog;
- executar um rollback até determinado commit e
- compartilhar/sincronizar o código num repositório remoto central.
A próxima figura, editada a partir da original contida na documentação oficial do Git, ilustra com clareza as três fases principais por que passa um arquivo num diretório controlado pelo Git:
Terminologia Básica
Vamos começar examinando alguns termos usados no Git que são fundamentais:
- master — a branch principal do repositório. Normalmente é onde as pessoas trabalham e na qual a integração acontece.
- clone — copia um repositório
git
existente, usualmente de alguma localização remota, para o seu ambiente local. - commit — envio de arquivos para o repositório local.
- fetch ou pull — pega as últimas atualizações de um repositório remoto. A diferença entre
fetch
epull
é quepull
combina as duas coisas: pega as atualizações de código do repositório remoto e efetua sua mesclagem com o repositório local. - push — é usado para enviar arquivos para um repositório remoto.
- remote — são os locais remotos do seu repositório, normalmente em algum servidor central, como o GitHub.
- SHA — cada
commit
ou node na árvore Git é identificado por uma chave SHA única. Pode-se utilizá-las em vários comandos para manipular um node específico. - head — é uma referência a um node para o qual nosso espaço de trabalho no repositório aponta no momento.
- branch — uma branch no Git nada mais é do que um rótulo particular em determinado node.
Configuração Elementar
Não vou entrar em detalhes aqui sobre a instalação do Git, pois isso vai depender do sistema operacional de cada um e, além disso, já existem instruções detalhadas para isso na documentação oficial do Git, com tradução em português.
Usarei a sintaxe da linha de comando dos sistemas baseados em Unix, tais como o Linux e o OS X. No Windows, além da interface gráfica, há um programa para emular um terminal tipo Unix, de forma que este tutorial poderá ser seguido sem problemas, seja qual for seu sistema operacional. O símbolo $
representa o prompt da linha de comando e não deve ser digitado.
Pressupondo, então, que o Git já está instalado no seu computador e o atalho correspondente ao diretório de sua instalação foi devidamente incluído na variável PATH do seu sistema, a primeira coisa a ser feita é configurá-lo com seu nome e endereço de email. Vamos aproveitar também para configurar um editor-padrão — vou escolher o vim, mas esteja à vontade para personalizar esta seleção de acordo com o seu gosto pessoal. Para isso, entre os seguintes comandos, com as devidas substituições pessoais:
$ git config --global user.name "J A Gaeta Mendes"
$ git config --global user.email "meu_email@example.com"
$ git config --global core.editor vim
As configurações podem ser conferidas com o comando git config --list
, cujo resultado, após os comandos acima, será:
$ git config --list
user.name=J A Gaeta Mendes
user.email=meu_email@example.com
core.editor=vim
Criação de um novo repositório Git
Antes de prosseguir, vamos criar um diretório onde pretendemos instalar um novo repositório Git e entrar neste diretório, com os comandos:
$ mkdir meu_repo_git
$ cd meu_repo_git
Em seguida, vamos inicializar nosso novo repositório git:
$ git init
Initialized empty Git repository in /home/gaeta/meu_repo_git/.git/
Use o comando git status
para verificar o estado atual do repositório:
$ git status
On branch master
Initial commit
nothing to commit (create/copy files and use "git add" to track)
Criação e commit de um novo arquivo
O próximo passo é criar e adicionar algum conteúdo ao repositório. Vamos criar um arquivo de uma forma bem simples, com os comandos:
$ touch ola.txt
$ echo Ola, mundo! > ola.txt
Uma nova verificação do estado do repositório vai apresentar agora a seguinte situação:
$ git status
On branch master
Initial commit
Untracked files:
(use "git add <file>..." to include in what will be committed)
ola.txt
nothing added to commit but untracked files present (use "git add" to track)
Antes de fazer um commit
, precisamos registrar o arquivo, o que fazemos com o comando add
:
$ git add ola.txt
Mais uma verificação do estado:
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: ola.txt
Podemos agora, finalmente, fazer seu commit
para o repositório:
$ git commit -m "Acrescenta meu primeiro arquivo"
[master (root-commit) 3a40877] Acrescenta meu primeiro arquivo
1 file changed, 1 insertion(+)
create mode 100644 ola.txt
É uma prática comum indicar “ação” nas mensagens de commit. Assim, ao invés de escrever “acrescentado meu primeiro arquivo”, escrevemos “acrescenta meu primeiro arquivo”.
Se examinarmos neste instante a árvore do repositório, teríamos a seguinte situação:
Há um node para o qual o "rótulo" master
aponta.
Acrescentando outro arquivo
Vamos agora adicionar um outro arquivo ao repositório:
$ echo "Oi, sou um outro arquivo" > outro_arquivo.txt
$ git add .
$ git commit -m "acrescenta outro arquivo com outro conteúdo"
[master ad5c381] acrescenta outro arquivo com outro conteudo
1 file changed, 1 insertion(+)
create mode 100644 outro_arquivo.txt
Observe que agora usamos git add .
, que acrescenta todos os arquivos do diretório corrente (.
). Do ponto de vista da árvore temos agora outro node e o master
moveu-se para ele.
Criando uma ramificação (de recursos)
Ramificação (branching) e mesclagem (merging) são as duas coisas que tornam o Git tão poderoso, e é para isso que ele foi otimizado, em se tratando de um sistema de controle de versão distribuído (VCS). De fato, as ramificações de recursos (feature branches) são criadas para cada novo tipo de funcionalidade que você acrescentar ao seu sistema e são normalmente apagadas mais tarde, depois que o recurso tenha sido mesclado outra vez na ramificação principal de integração (usualmente a ramificação master). A grande vantagem disso é que você pode experimentar a nova funcionalidade num “playground” separado e isolado, movendo-se rapidamente para frente ou para trás da ramificação “master” original quando necessário. Ademais, ela poderá ser facilmente descartada outra vez (no caso de não ser mais necessária), bastando eliminar a ramificação do recurso. Há um ótimo artigo para entender as ramificações no Git, que você definitivamente deve ler.
Mas vamos começar. Primeiro de tudo, criamos uma nova ramificação de recurso:
$ git branch minha-feature-branch
Em seguida, podemos usar o comando git branch
para obter uma lista das ramificações atuais:
$ git branch
* master
minha-feature-branch
O *
na frente de master
indica que no momento estamos naquele ramo. Vamos agora mudar para o ramo minha-feature-branch
.
$ git checkout minha-feature-branch
Switched to branch 'minha-feature-branch'
Conferindo:
$ git branch
master
* minha-feature-branch
Pode-se usar o comando direto git checkout -b minha-feature-branch
para criar e ativar uma nova ramificação num único passo.
A diferença de outros VCS é que há apenas um diretório de trabalho. Todas as ramificações ficam nele e não há uma pasta separada para cada ramificação criada. Ao invés disso, quando se alterna entre as ramificações, o Git atualiza o conteúdo do diretório de trabalho para refletir aquele da ramificação para a qual se está mudando.
Vamos modificar um dos nossos arquivos existentes:
$ echo "Oi" >> ola.txt
$ cat ola.txt
Ola, mundo!
Oi
…e então vamos fazer um commit
dele para nossa nova ramificação:
$ git commit -a -m "modifica arquivo acrescentando oi"
[minha-feature-branch 8cff170] modifica arquivo acrescentando oi
1 file changed, 1 insertion(+)
Dessa vez usamos o comando git commit -a -m
para fazer o add e commit da modificação num único passo. Isso funciona apenas nos arquivos que já tenham sido adicionados previamente ao repositório git. Arquivos novos não podem ser adicionados dessa maneira e precisam ser inseridos com o comando git add
como visto antes.
Vejam, na figura a seguir, como ficou a árvore do git depois de todos esses comandos:
Até aqui tudo parece bem normal e ainda temos uma linha reta na árvore, mas observe que agora o master continuou onde estava e movemos adiante minha-feature-branch
.
Vamos trocar para master
e modificar o mesmo arquivo lá também.
$ git checkout master
Switched to branch 'master'
Como era de se esperar, alo.txt não foi modificado:
$ cat ola.txt
Ola, mundo!
Vamos modificá-lo e dar um commit
no master
também (isso vai gerar um belo conflito mais tarde):
$ echo "Oi, fui modificado no master" >> ola.txt
$ git commit -a -m "acrescenta linha em ola.txt"
[master 33b65ea] acrescenta linha em ola.txt
1 file changed, 1 insertion(+)
Nossa árvore agora visualiza a ramificação:
Mesclando e resolvendo conflitos
O próximo passo será reintegrar nossa feature branch
de volta em master
. Isso é feito usando o comando merge
:
$ git merge minha-feature-branch
Auto-merging ola.txt
CONFLICT (content): Merge conflict in ola.txt
Automatic merge failed; fix conflicts and then commit the result.
Como esperado, temos um conflito de mesclagem em ola.txt.
$ cat ola.txt
Ola, mundo!
<<<<<<< HEAD
Oi, fui modificado no master
=======
Oi
>>>>>>> minha-feature-branch
Vamos consertar isso num editor de textos:
Ola, mundo!
Oi, fui modificado no master
Oi
…e fazer o commit
novamente:
$ git commit -a -m "resolve conflitos de merge"
[master 838d26a] resolve conflitos de merge
A árvore reflete nosso merge
:
Indo para um determinado Commit
Suponhamos que queremos voltar a um dado commit
. Podemos usar o comando git log
para obter todos os identificadores SHA que identificam de forma única cada node da árvore.
Escolha um dos identificadores (mesmo que não seja o número completo, pouco importa) e vá para aquele node usando o comando checkout
:
$ git checkout 33b65ea
Note: checking out '33b65ea'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 33b65ea... acrescenta linha em ola.txt
Observe o comentário exibido pelo git. O que ele significa? Detached head significa que a head não está mais apontando para um rótulo de branch, mas, ao invés disso, para um commit
específico da árvore.
Pense na HEAD como a “branch atual”. Quando você troca de "branches" com git checkout, a revisão HEAD muda para apontar a dica da nova "branch". É possível para a HEAD referir-se a uma revisão específica que não esteja associada com um nome de "branch". Esta situação é chamada detached HEAD. (v. também esta resposta no Stackoverflow).
Depois de modificar o arquivo ola.txt e fazer commit
da mudança, a árvore ficará assim:
Como você pode ver, o node recem-criado não possui nenhum rótulo. A única referência que atualmente aponta para ele é a head. Todavia, se trocarmos para master
novamente, o commit
anterior será perdido, pois não teremos como voltar para aquele node.
$ git checkout master
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
576bcb8 change file undoing previous changes
If you want to keep them by creating a new branch, this may be a good time
to do so with:
git branch new_branch_name 576bcb8239e0ef49d3a6d5a227ff2d1eb73eee55
Switched to branch 'master'
Como se observa, o git nos alerta desse fato.
Rollback
Voltar para trás é bom, mas e se quisermos desfazer tudo, voltando ao estado antes do merge
da feature branch? Muito fácil:
$ git reset --hard 33b65ea
HEAD is now at 33b65ea acrescenta linha em ola.txt
A sintaxe genérica aqui é git reset --hard <tag/branch/commit id>
.
Usando “revert” para fazer rollback das mudanças do jeito fácil
Se você precisar fazer o rollback
de um commit
inteiro e (o que é pior) você já sincronizou com um repositório remoto, então usar git reset --hard
pode não ser adequado, uma vez que dessa forma você estaria reescrevendo a história, o que não é permitido se a sincronização com o servidor remoto já foi efetuada.
Em tais situações pode-se usar o comando revert
, o qual basicamente cria um novo commit
desfazendo todas as mudanças de um determinado commit que for especificado. Considere, por exemplo, que você queira fazer o rollback de um commit com o ID 41b8684
:
$ git revert 41b8684
Desfazendo mudanças quando não foi feito Commit
Outro cenário comum no terreno de “desfazer coisas” é simplesmente descartar mudanças locais quando ainda não foi feito o commit
.
Arquivos não estagiados para um Commit
Vamos pressupor que você tenha modificado um arquivo. A execução do comando git status
resultaria:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: ola.txt
no changes added to commit (use "git add" and/or "git commit -a")
Até aqui nada foi adicionado ao seu repositório Git, nem foi estagiado (registrado) para fazer commit
. O que significa descartar aquelas mudanças? Pense na árvore Git. Basta pegar (checkout
) a última versão daquele arquivo, certo?
Então:
$ git checkout ola.txt
desfaz a mudança, como o comprova um novo git status
:
$ git status
On branch master
nothing to commit, working directory clean
Arquivos estagiados para um Commit
A outra situação é quando você modificou o arquivo e já o estagiou para dar um commit
através do comando git add
.
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: ola.txt
O comando git checkout
não teria nenhum efeito neste caso, mas, ao invés disso, (se você leu o que o git escreveu na saída do parâmetro de status) temos que fazer um reset. Por que? Porque o comando git add
já criou um node na árvore Git que, todavia, ainda não foi objeto de um commit
(na verdade isso não é 100% correto: veja Git index vs. working tree para mais detalhes). Assim sendo, precisamos resetar o ponteiro corrente para a HEAD que é o topo da nossa branch corrente.
$ git reset HEAD ola.txt
Unstaged changes after reset:
M ola.txt
e consequentemente:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: ola.txt
no changes added to commit (use "git add" and/or "git commit -a")
Estamos novamente no estado em que temos mudanças locais ainda não estagiadas para um commit e podemos então usar o comando checkout
para descartá-las. Uma maneira rápida de fazer isso é usar:
$ git reset --hard HEAD
juntando num único comando a retirada de estágio e checkout das mudanças.
Compartilhando/Sincronizando seu Repositório
Ao fim e ao cabo, vamos querer compartilhar nosso código, normalmente sincronizando-o com um repositório central. Para fazer isso, temos que adicionar um remote.
$ git remote add origin git@github.com:user-name/exemplo.git
Para verificar se obteve êxito, basta digitar:
$ git remote -v
o qual lista todos os remotes. Agora é preciso publicar nossa branch master local para o repositório remoto. Isso é feito da seguinte forma:
$ git push -u origin master
E terminamos.
Uma coisa realmente poderosa é que pode-se acrescentar repositórios remotos múltiplos. Isso é usado frequentemente em combinação com soluções de hospedagem em nuvem para distribuição do código no servidor. Por exemplo, você pode acrescentar um remoto chamado deploy
que aponta para algum servidor de repositório hospedado na nuvem, tal como:
$ git remote add deploy git@somecloudserver.com:user-name/meuprojeto
e então, sempre que você quiser publicar sua branch, basta executar:
$ git push deploy
Clonagem
Tudo funciona da mesma forma se você pretende iniciar a partir de um repositório remoto já existente. O primeiro passo é fazer um checkout
do código-fonte, o que é chamado clonagem (cloning) na terminologia do Git. Deve-se, então, fazer algo assim:
$ git clone git@github.com:user-name/exemplo.git
Cloning into 'exemplo'...
remote: Counting objects: 430, done.
remote: Compressing objects: 100% (293/293), done.
remote: Total 430 (delta 184), reused 363 (delta 128)
Receiving objects: 100% (430/430), 419.70 KiB | 102 KiB/s, done.
Resolving deltas: 100% (184/184), done.
Isso vai criar uma pasta (neste caso) chamada “exemplo” e se entrarmos nela:
$ cd exemplo/
e verificarmos os repositórios remotos, constatamos que a informação para seu rastreamento já está configurada:
$ git remote -v
origin git@github.com:juristr/intro.js.git (fetch)
origin git@github.com:juristr/intro.js.git (push)
Podemos recomeçar agora o ciclo commit
/branch
/push
normalmente.
Recursos e Links (em inglês)
Os cenários acima são simples, mas provavelmente são também, ao mesmo tempo , os mais usados. O Git, contudo, é capaz de fazer muito mais. Para obter mais detalhes, consulte os links abaixo:
- Git Ready
- Book: Pro Git by Scott Chacon
- Visual Git Cheat-Sheet
- My personal Git Cheat Sheet where I continuously add stuff I want to remember
- Git Immersion
- Git Branching Model
- DZone Refcard: Getting Started with Git
- DZone Refcard: Git Patterns and Antipatterns
- Git Man-Page generator
- Understanding branches in Git
- 8 Tips to help you work better with Git
- Getting Git Right