quinta-feira, 3 de junho de 2010

Como eu uso o GIT

Recentemente vi um post do Yehuda Katz sobre como ele usa o GIT e me empolguei para escrever também.

Antes de mais nada, gostaria de dar os devidos créditos ao @rogerio_augusto porque foi ele quem me ensinou boa parte dos itens que vou escrever hoje.

Pull e Push

Bom, vou começar pelo mais básico, enviar e receber commits do repositório remoto.

Geralmente o pessoal acredita que a fórmula é essa:

  • commito alguma coisa
  • faço pull para ver se tem alguma coisa no remoto
  • faço push para enviar para ele

Isso até que funciona, mas eu prefiro uma outra abordagem que deixa a árvore dos commits mais organizada.

O pull nada mais é do que um fetch (baixa objetos e referências do remoto) mais merge (junta duas ou mais histórias).

Imagine que você baixou tudo que estava no remoto e fez um commit. Enquanto você trabalhava outra pessoa enviou alguma coisa para o remoto que você não tem. Se você fizer um pull, a árvore dos commits ficará assim:

Para não ter que criar um repositório remoto para o exemplo, eu fiz o merge entre dois repositórios locais, mas o resultado é o mesmo.

Com o merge ele mostra que os commits foram feitos em paralelo, mas as vezes essa informação não é tão relevante, e vale mais a pena deixar a árvore organizada. Por isso quando eu vou fazer isso, ao invés de eu dar um pull pro remoto, eu faço um rebase. Por exemplo:


git fetch
git rebase origin/master

Ou, mais fácil de usar

git pull --rebase origin master

Com o rebase o git faz um stash de seus commits que ainda não existem no remoto, baixa os branchs do remoto e recoloca seus commits após o que veio dele. Ficaria assim:


Viram? O git passou o seu commit para depois dos commits que vieram do repositório (apesar deles poderem ter ocorrido antes) e assim manteve a árvore organizada e fácil de entender.

Tem gente que fala que assim você perde a real história dos commits, mas eu discordo. Para casos em que a história é relevante eu uso o pull normal para deixar registrado o que aconteceu (por exemplo, estou criando uma aplicação a partir de uma outra open source e a original foi atualizada, aí sim eu faço um pull para mostrar que são commits de equipes diferentes que andaram em paralelo), mas para os casos de commits dentro da mesma equipe trabalhando numa mesma tarefa, acho que vale mais a árvore se manter legível.

Merge com a opção squash

Geralmente eu uso branchs para separar as histórias do projeto que estão sendo criadas, sendo assim, imagine a seguinte situação:

Você criou um branch a partir do master para fazer sua história. Nesse seu branch você fez 23 commits, que foram totalmente úteis para você se organizar, integrar mais facilmente, etc.

Então você terminou sua história e vai fazer o merge para mandar sua nova funcionalidade para o remoto, então você faz:

git checkout master
git pull origin master (imaginando que você não commitou nada no seu master, senão o mais correto é git pull --rebase)
git checkout seu_branch
git rebase master
git checkout master
git merge seu_branch
git push origin master

Com isso você atualizou seu master, integrou as atualizações do master com o seu branch, fez o merge do seu branch no master e aí sim enviou os seus 23 commits.

Até aí você fez tudo perfeitamente, mas existe uma outra ferramenta que pode te ajudar a se organizar, a opção --squash do merge.
Quando você faz o merge com --squash (git merge seu_branch --squash) o git leva as atualizações pro seu branch mas como se elas não tivessem sido commitadas. Aí você faz um único commit e manda só ele para o master.

Para quem está olhando o master, faz mais sentido ver um commit por funcionalidade, enquanto para quem está desenvolvendo a funcionalidade os 23 commits são úteis. Então agindo assim a gente ajuda todo mundo. Ficaria assim:

git checkout master
git pull origin master
git checkout seu_branch
git rebase master
git checkout master
git merge seu_branch --squash
git add .
git commit -am "Minha funcionalidade"
git push origin master

Opção stash do git merge

Uma outra ferramenta útil é o stash. Imagine que você vai fazer uma atualização pequena no seu projeto e resolveu fazer no branch master mesmo (para aplicações que dividem o projeto em dois remotos - produção e desenvolvimento - eu acho isso totalmente normal). Só que enquanto você está fazendo você percebe que será uma atualização grande e seria mais interessante fazer isso num branch a parte (ou alguém te chamou para fazer outra coisa e você não vai poder continuar aquilo no momento). Então você está com algumas modificações sem commitar mas você não vai poder commitar no master, porque não está concluído, etc.

Para isso você pode usar o stash, você faria assim:

git stash (o git guarda todas as suas modificações em um lugar a parte e seu branch atual passa a ficar como se ninguém tivesse mexido - você pode usar git status para confirmar)
git checkout -b seu_branch_novo
git stash apply (suas modificações voltam a aparecer, pode confirmar com git status)
git commit -am "Primeiro commit da minha funcionalidade"
git stash clear (apaga tudo o que estava no stash)
git checkout master

Com isso você fez como se tivesse começado desde o início em um branch novo.

Você consegue também criar e gerenciar mais de um stash, mas geralmente um só satisfaz minha necessidade.

Opção track do git branch

Vamos a mais uma situação. Seu colega começou uma funcionalidade em um branch a parte e depois de alguns dias pediu para você ajudar ele nessa tarefa.

Você tem que trabalhar nesse mesmo branch dele, então você decide criar o branch dele localmente. Você faz:

git branch branch_dele
git checkout branch_dele (você poderia ter usado git checkout -b branch_dele e fazer tudo de uma vez)
git pull origin branch_dele

CUIDADO!

Quando você fez o primeiro comando você criou um novo branch a partir do que você tem em seu branch atual. E pode ser que você tem coisas nele que não deveriam estar no branch do seu colega, pode ser até conflitante com as tarefas dele e além disso o merge vai bagunçar legal a árvore dele. Se você fizer um rebase pode até piorar mais porque você terá que dar um --force para reescrever a história dele e ele terá que se atualizar. Bom, enfim, essa situação pode gerar resultados não esperados.

Para isso o mais indicado seria:

git fetch
git branch branch_dele --track origin/branch_dele
git checkout branch_dele

Com a opção --track o git vai criar o branch a partir do que tem no brach remoto que você informou, sem olhar para o que tem no seu branch atual. Sendo assim você ficará com o branch idêntico ao do seu colega, o que é o ideal para essa situação.

O git fetch antes é importante porque o --squash vai criar o branch com as informações que estiverem no repositório local, não no remoto. Faça o fetch para atualizar o repositório local antes.

Voltando commits

Outra coisa útil é conseguir voltar atrás em algo que está fazendo. Por exemplo:

Você começou uma tarefa e percebe que está indo na direção errada e deseja começar de novo.

Se você ainda não fez nenhum commit basta usar:

git reset --hard

E você voltará ao estado do último commit efetuado.

Se você já tinha commitado algo, então você pode fazer:

git reset --hard id_do_seu_commit

Exemplo:

git reset --hard dbeeeb0369b5bad0f776fc5ae16c5500bd808156

Com o reset você consegue ir para trás e para frente. Para pegar o id de um commit que está a frente você pode usar:

git reflog

Acabou

Bom, tentei lembrar de algumas situações que são comuns em um projeto. Espero ter ajudado e gostaria da opinião de vocês. Pode ser que existam outras abordagens melhores, então estou aberto para discutir e aprender.

Qualquer dúvida podem entrar em contato comigo (comentário, gtalk, twitter, email etc).

Abraços.