Git é uma ferramenta de controle de versão distribuída (Distributed Version Control System ou DVCS) criada por Linus Torvalds (sim, o mesmo criador do Linux), tem uma arquitetura diferenciada que cria cópias completas do repositório em cada máquina de trabalho, ao contrário da maioria das outras ferramentas que criam um único repositóriocentralizado. Dessa forma o Git ganha uma velocidade incrível, visto que quase toda operação é local. Daí, de tempos em tempos, basta sincronizar seu repositório local com o centralizado e todos verão suas alterações.
Outro diferencial da arquitetura do Git é que ele guarda versões completas dos arquivos de cada commit ao invés de apenas as alterações de um commit para outro, como a maioria das ferramentas de controle de versões o fazem, o que o permite ter uma flexibilidade (e poder) maiores e faz com que ele se porte como um mini-sistema de arquivos.
Abaixo estão algumas dicas para iniciar no Git e um guia de referência que lista os principais comandos utilizados com uma breve descrição.
Iniciando…
Minhas dicas para iniciar na ferramenta Git são os cursos on-line do site codeschool.com. O curso “Try Git” é grátis, e já ajuda a entender os mecanismos do Git. Mas quem puder, faça os cursos avançados, pois te darão um conhecimento legal em um tempo reduzido.
Se quiser um conhecimento um pouco mais aprofundado, entendendo mais detalhes dos mecanismos do Git, o livro grátis Pro Git é bem completo e de fácil leitura.
Guia de referência
Áreas do Git
O Git possui algumas divisões de áreas de trabalho. A versão (snapshot) dos arquivos que estamos trabalhando fica no diretório de trabalho. Quando queremos fazer um commit, precisamos adicionar os arquivos a serem comitados numa área conhecida como staging (preparação) e, sem seguida, efetivar a operação (commit).
Figura 1: Operações locais
Criação de repositórios
Há basicamente duas formas de conseguir um repositório no Git. Um deles é criando um novo a partir de um diretório já existente e o outro é “clonando” um repositório remoto (visto mais a frente):
$ git init
Com esse comando é criado um repositório na pasta atual e são inseridos alguns arquivos de metadados do repositório numa pasta .git criada automaticamente. Para remover um repositório, basta remover este diretório .git.
Configuração
Antes de começar a utilizar repositórios no Git, é interessante configurar algumas informações, como o nome do usuário e e-mail, que constarão nos detalhes de cada commit:
$ git config user.name "Vanessa Schissato" $ git config user.email "abobrinha@gmail.com"
Se quiser que as configurações sejam globais, ou seja, valham para todos os repositórios:
$ git config --global user.name "Vanessa Schissato"
Ciclo de vida dos arquivos
Criar
Cada arquivo no repositório pode ter o status monitorado (tracked) ou não-monitorado (untracked). Arquivos monitorados são os que estavam presentes no último commit. Portanto, um arquivo novo criado no diretório fica com o status não-monitorado. Quando queremos realizar um commit, precisamos antes marcar os arquivos que serão comitados. Isto é feito adicionando-os numa área conhecida como staging. Após todos os arquivos necessários marcados para o commit, basta efetivar a operação. Segue o fluxo de um arquivo desde seu nascimento até seu commit:
Figura 2: Ciclo de vida de um arquivo
As operações necessárias para criar e comitar um arquivo são:
$ touch arq1 # Cria um novo arquivo 'arq1' vazio (não-monitorado) no sistema de arquivo $ git add arq1 # Prepara o arquivo 'arq1' para ser adicionado no próximo commit (staged) $ git commit -m "Primeiro commit" # Cria um novo commit com todas as alterações na área de staging e torna os arquivos monitorados
Alguns atalhos são possíveis, como adicionar todos os arquivos na área de staging sem precisar discriminá-los um a um:
$ git add --all # Adiciona todos os arquivos modificados na área de staging
Outro atalho importante é o de adicionar e comitar num único comando. O detalhe importante aqui é que ele só adiciona na área de staging e, consequentemente, comita, arquivos que já estejam monitorados:
$ git commit -a -m "Primeiro commit" # Adiciona todos os arquivos monitorados que tenham sofrido modificações na área de staging e os comita
Uma coisa importante a se notar é que quando um arquivo é adicionado na área de staging, ele é armazenado tal qual no momento. Se alguma nova alteração for feita no mesmo arquivo, ela não será refletida na versão staged automaticamente. Se for necessário que as novas alterações sejam adicionadas no próximo commit, deve ser feito novamente o git add.
Por fim, um commit é apenas um ponteiro para um snapshot do conteúdo adicionado na área de staging na hora do commit:
Um comando essencial no uso do Git é o de verificação de status dos arquivos, que discrimina qual a situação dos arquivos do repositório:
$ git status
É possível adicionar arquivos que serão ignorados do sistema de versionamento. Para isso devem ser adicionadas entradas no arquivo .gitignore.
Alterar
Os arquivos sofrem alterações com o passar do tempo. Quando o conteúdo de um arquivo é modificado e queremos comitar estas alterações, basta seguir o fluxo staging -> commit.
$ git add arq1 $ git commit -m "Segundo commit"
Remover
Vimos como controlar o versionamento de arquivos de algumas maneiras, mas, e quando queremos remover esse arquivo do versionamento? Bom, a opção mais intuitiva:
$ rm [NOME_ARQUIVO] # Comando do SO para remover o arquivo $ git add [NOME_ARQUIVO] # Adiciona modificações do arquivo no staging (nesse caso a sua remoção)
Mas, há um atalho para fazer as duas operações simultaneamente:
$ git rm [NOME_ARQUIVO]
Mover/Renomear
Bom, vimos comandos para o nascimento, vida e morte de um arquivo no controle de versionamento. Mas e se o arquivo tem seu próprio caminho alterado (alterada pasta e/ou nome do arquivo)? A idéia mais intuitita seria fazer uma remoção do arquivo antigo e uma adição do novo arquivo no Git:
$ mv arq1 arqNovo # Comando do SO para mover o arquivo $ git rm arq1 # Comando para adicionar essa remoção no staging $ git add arqNovo # Comando para adicionar o novo arquivo no staging
Funciona, mas existe um atalho mais prático que engloba essas 3 operações:
$ git mv arq1 arq1Femea
Revertendo alterações
Bom, na vida temos que aprender a conviver com nossos erros, mas o Git é muito mais poderoso, e nos permite fazer uma espécie de volta ao tempo e reverter quase todas as operações. Por exemplo, quer descartar as alterações locais (ainda não no staging e nem comitadas)?
$ git checkout -- [NOME_ARQUIVO] # Descarta alterações no arquivo e mantém a versão do último commit
Adicionou o arquivo no staging erroneamente (marcou para o próximo commit)?
$ git reset HEAD [NOME_ARQUIVO] # Remove o arquivo do staging e mantém a versão atual no diretório de trabalho
Chegou a commitar algo que não deveria? É possível também reverter o commit:
$ git reset HEAD^ # Reverte commit, mas mantém a versão do arquivo comitado no diretório de trabalho
Foi além e precisa reverter mais de um commit?
$ git reset HEAD^^ # Reverte últimos dois commits $ git reset HEAD^^^ # Reverte últimos três commits
Ou seja, se eu quiser reverter os últimos 100 commits vou precisar seguir essa lógica aí do “um elefante incomoda muita gente”, um circunflexo pra cada commit? Não, podemos usar o atalho:
$ git reset HEAD~100
Só tem um problema…quando eu utilizo estes comandos, meus arquivos ficam com a versão do último commit no diretório de trabalho. Mas se for necessário inclusive descartar as alterações destes commits:
$ git reset --hard HEAD^ # Reverte o último commit, descartando as alterações que o commit introduziu
Esse argumento “–hard” diz para que todas as alterações sejam descartadas. O contrário seria usar o “–soft“, mas como ele é o default, resolvi omití-lo.
Mas, e no caso em que eu fiz um commit, mas esqueci de adicionar algum arquivo nele? Bom, a idéia intuitiva seria reverter o commit mantendo os arquivos e refazer:
$ git reset --soft HEAD^ $ git add [NOME_ARQUIVO_ESQUECIDO] $ git commit -m "Recomitando"
Há um atalho para fazer esse remendo no commit:
$ git add [NOME_ARQUIVO_ESQUECIDO] $ git commit --amend -m "Mensagem commit"
Histórico
Como em qualquer ferramenta de controle de versão, a análise do histórico pode ser uma importante ferramenta, seja para acompanhar o desenvolvimento do sistema, coletar métricas de commits, etc. O comando básico para verificar os commits efetuados é:
$ git log [-LIMITE_COMMITS_EXIBIDOS] [--stat] [-p]
Alguns argumentos mais utilizados são para limitar o número de commits exibidos, o comando “–stat” que traz algumas estatísticas do commit (como linhas adicionadas, linhas removidas, etc), além do argumento”p” que indica que deve ser exibido o diff do commit em relação ao anterior.
Diff
Para verificar as modificações nos arquivos é possível utilizar o comando diff:
$ git diff
Este comando acima exibe a comparação apenas dos arquivos que estão no seu diretório de trabalho com relação ao que está na área de staging. Se a necessidade é comparar os arquivos da área de staging com os do último commit:
$ git diff --staged
Além disso, pode ser necessário um maior detalhamento das informações alteradas, como, por exemplo, detalhamento em nível de palavra, e não de linha:
$ git diff --word-diff
Autorização
O Git por si só não realiza nenhum controle de acesso (autorização). Se for necessário, há algumas ferramentas no mercado, como Github (host), BitBucket (host) e Gitosis, Gitorious.
Repositórios remotos
Quando temos um repositório local e queremos torná-lo remoto:
$ git remote add [NOME_REMOTO] http://github.com/Teste/repo-git.git # Adiciona um repositório remoto do Github como 'origin', por exemplo. $ git push [NOME_REMOTO] [NOME_BRANCH] # Sincroniza as alterações locais (branch master, por exemplo) com o repositório remoto dado por 'origin', por exemplo
Pode ser necessário fazer o passo contrário, clonar um repositório remoto já existente. Por exemplo, vamos supor que desejamos criar um repositório que aponte para um repositório remoto no Github:
$ git clone http://github.com/Teste/repo-git.git
Este comando acima já baixa o código, cria o repositório (init), adiciona uma entrada ‘origin‘ na lista de repositórios remotos (remote add), além de já ir para o branch master (checkout).
Para listar, adicionar ou remover ‘remotes’, ou seja, entradas de repositórios remotos:
$ git remote add origin http://github.com/Teste/repo-git.git $ Adiciona uma entrada para o repositório do Github como 'origin' $ git remote rm origin # Remove entrada do repositório remoto 'origin' $ git remote -v # Lista os repositórios remotos que o repositório local conhece
Branches (trabalhando numa feature)
O Git incentiva o uso de branches, já que são de rápida criação e relativamente simples de mergear em relação as outras ferramentas de controle de versão. Um branch é apenas um ponteiro para um commit específico. Para indicar qual o branch atual de trabalho, o Git guarda um ponteiro especial conhecido como HEAD.
Figura 3: Branch
Estes branches locais são também conhecidos como branches tópicos. Para criar e trabalhar numa branch:
$ git branch [NOME_BRANCH] # Cria a branch, mas sem alterar os arquivos do diretório de trabalho $ git checkout [NOME_BRANCH] # Vai para o diretório de trabalho da branch
Há um atalho para fazer as duas operações acima (criar um branch e já apontar pra ela):
$ git checkout -b [NOME_BRANCH]
Se quiser ver todos os branches locais criados:
$ git branch [-v]
Se quiser listar apenas os branches que você já fez merge no branch atual, ou os que vocês ainda não fez o merge:
$ git branch --merged # Lista apenas os branches já mergeados com o branch atual $ git branch --no-merged # Lista apenas os branches não mergeados com o branch atual
Para remover um branch local:
$ git branch -d [NOME_BRANCH]
Quando um branch ainda não está mergeado, para que se possa removê-lo, é necessário usar a seguinte sintaxe:
$ git branch -D [NOME_BRANCH]
Merge
Para realizar um merge entre dois branches, por exemplo, de um branch para o master:
$ git checkout master # Vai para o branch master $ git merge [NOME_BRANCH] # Faz o merge das alterações no branch para o master
Cuidado! Arquivos ainda não monitorados ficam visíveis em todos os branches.
Ao tentar mudar de branch quando existem arquivos monitorados modificados, é necessário antes comitar as alterações ou descartá-las.
Ao fazer um merge, podem ocorrer duas situações distintas, um dos casos possíveis é quando o branch ‘pai’ no qual se quer realizar o merge não teve commits posteriores à criação do branch filho (ancestral direto). Nesse caso, para fazer o merge, basta avançar o ponteiro do branch ‘pai’ até o commit para o qual aponta o branch filho, num processo conhecido como Fast Forward.
Figura 4: Merge com Fast Forward
Outro caso possível ao fazer um merge é que ambos os branches (pai e filho) tenham evoluído em paralelo, ocasionando uma bifurcação no histórico de commits. Nesse caso, o Git faz um processo de merge um pouco mais complexo, encontrando automaticamente um ancestral comum e fazendo o merge das duas branches gerando um novo commit automático de merge.
Figura 5: Merge com recursão
As vezes, ao realizar um merge, pode ocorrer um conflito devido a alterações diferentes na mesma parte do mesmo arquivo. Neste caso, os arquivos são listados como unmerged e devem ser tratados manualmente os conflitos e comitada a resolução:
$ git commit -a -m "Merge manual" # Adicionar correção no stage e commitar para resolver o conflito
Branches remotos
É muito importante entender o conceito de branches remotos, que são referências ao estado dos seus branches no seu repositório remoto. São representados localmente como branches locais que você não pode mover, eles se movem automaticamente a cada vez que é feita uma sincronização (push ou pull). Ou seja, são ponteiros para o estado do branch remoto no momento da última sincronização.
Figura 6: Branches remotos
Depois de entendido que os branches remotos são copiados no repositório local como branches [REMOTE_NAME]/[BRANCH_NAME], todas as operações comuns de branches se aplicam, como merge, etc.
A boa prática diz que se for ser trabalhado mais de um dia num branch, este deve ser tornado remoto. Para fazer um branch local se tornar remoto:
$ git push origin master # Adiciona o branch master remotamente no repositório representado por origin
ou
$ git push origin master:master_heroku # Caso o nome do branch remoto seja difernete (like Heroku)
Para atualizar o repositório local (mas sem alterar os arquivos do diretório de trabalho):
$ git fetch origin # Atualiza o repositório local ('origin', por exemplo), mas sem mergear com os dados do diretório de trabalho
Figura 7: Operação fetch
Para fazer o merge dos commits remotos com o do branch local:
$ git merge origin/master # Faz o merge do branch master do repositório local origin 'origin/master' com o branch 'master' local
Há um atalho para fazer a atualização do repositório local e já fazer o merge com o branch local atual:
$ git pull
A operação contrária (enviar as alterações locais para o repositório remoto), pode ser feita com o comando:
$ git push origin master
Importante! Não fazer alterações nos commits depois de realizadas sincronizações com o repositório remoto, por exemplo: reset, commit –ammed.
Para obter um branch remoto:
$ git pull # Atualiza os branches
Para listar os branches remotos:
$ git branch -r # Lista branches remotos atualizados no repositório local
Para ver mais informações sobre um remote específico:
$ git remote show origin # Traz mais informações do remote origin, por exemplo
Para remover um branch remoto:
$ git push origin :master
E, para remover as referências locais de branches removidos remotamente:
$ git remote prune origin
Ao clonar um repositório remoto, é criado automaticamente um remote origin e um branch local master seguidor do branch origin/master (tracking branches). Por isso, ao fazer git pull e git push sem argumentos é subentendido git pull origin master.
Para criar um branch local seguindo (tracking) um branch remoto:
$ git checkout --track origin/outro_branch
Rebase
Além do comando merge, existe outra alternativa para integrar mudanças de um branch em outro, que é o comando rebase:
$ git checkout experiment $ git rebase master
Estes comandos acima fazem com que as alterações feitas no branch experiment a partir do ponto do ancestral comum sejam adicionadas no branch master como se fossem commits feitos no próprio master, o que torna o histórico um pouco mais linear e limpo, embora funcionalmente nada mude.
É possível realizar o rebase sem a necessidade de checkoutar para o branch tópico:
$ git rebase master experiment
Figura 8: Operação rebase
É possível fazer algumas coisas mais avançadas com rebase, como no exemplo abaixo, onde existe um branch derivado de outro branch, e queremos apenas mandar para o master o branch neto. Nesse caso, pode-se usar o comando:
$ git rebase --onto master server client
Figura 9: Operação rebase com parâmetro –onto
Cuidado! Ao fazer rebase de commits que já se tornaram remotos, pode haver problemas com outras pessoas que se basearam nos commits que deixaram de existir.
Tags
Assim como em outros sistemas de controle de versão, tags são snapshots do código, referentes a um determinado commit, em geral representando uma release version. Para criar tags:
$ git tag -a v0.1 -m "Adiciona versão 0.1" # Cria tag local 'anotada', ou seja, que é uma cópia completa dos arquivos (recomendado) $ git tag v0.1 # Cria uma tag leve, ou seja, apenas um ponteiro para um commit $ git tag -s v0.1 # Cria uma tag assinada $ git tag -a v0.1 -m "Adiciona versão 0.1" [HASH_COMMIT] # Cria uma tag a partir do commit passado
Para tornar as tags remotas:
$ git push --tags # Adiciona todas as tags ao repositório remoto
ou
$ git push origin [NOME_TAG] # Adiciona apenas a tag referenciada ao repositório remoto
Para ver as tags existentes:
$ git tag
Para filtrar as tags existentes:
$ git tag -l 'v1.1*'
Da mesma forma que em branches, para visualizar o código de uma tag, basta dar o checkout para a mesma:
$ git checkout [NOME_TAG]
Mas, cuidado! Uma tag é apenas um ponteiro para um determinado commit. Ao fazer o checkout para uma tag, na prática se está fazendo o checkout para as versões de arquivo do commit específico, o que significa que você não estará apontando seu diretório de trabalho para nenhum branch, algo conhecido como detached.
Referências
Livro Grátis “Pro Git” – Scott Chacon
Cursos “Git” – codeschool.com