Controle de versões com o Git parte 2
Dando seqüência ao post anterior
Branching
Branches são usados quando preciso modificar algum estado anterior do projeto sem alterar o estado atual. Se lembrarmos do grafo de versões, veremos que isso é exatamente uma linha divergente da linha de tempo do projeto. Como um universo alternativo no “De volta para o futuro 2”.
Para criar um “branch” no git basta usar o comando git branch [nome_do_branch]. É importante que tudo seja commitado antes no projeto. Abaixo um exemplo onde eu inicio um repositório, adiciono um arquivo chamado “teste”, crio um primeiro “commit” e crio um “branch” chamado “novo_branch”.

No próximo exemplo eu crio um arquivo chamado “arquivo_so_no_master”, que só aparece no “branch master”, pois pertence ao estado mais recente do projeto. Depois crio um “commit” contendo esse arquivo e, a partir disso, o “branch master” passa a apontar o último “commit” contendo o arquivo novo, enquanto o “branch novo_branch” continua apontando para um “commit” onde esse arquivo ainda não foi criado.

Quando eu faço o git checkout novo_branch observem que o arquivo desaparece do meu diretório de trabalho, pois o estado atual do projeto passa a ser aquele apontado pelo “head novo_branch” e o git sincroniza a nossa arvore de arquivos com o conteúdo do “commit” no “branch” atual.
Se executarmos o commando git diff head1...head2 podemos ver a diferença de head2 até o ancestral em comum com head1, isso permite identificar facilmente as modificações feitas em um branch específico a partir da sua divergência. No exemplo abaixo usamos ele para ver o que se passou no branch “master” a partir da criação de “novo_branch”:

É importante que em algum ponto da história dos branches seja possível unir essas duas linhas de tempo divergentes. Isso é comum em desenvolvimento de software quando queremos integrar recursos de uma versão de desenvolvimento à versão estável, ou quando queremos portar modificações de segurança da versão estável para a versão de desenvolvimento. Nesse ponto entram dois novos conceitos. O “merge” e o “cherry-pick”.
O “merge” faz a união de dois estados distintos do projeto e é feito com os comandos git merge ou git pull. Por enquanto falaremos apenas sobre o git merge e mais tarde falaremos sobre o git pull.
Fazendo um merge
Antes de um merge é importante commitar todas as modificações que desejamos preservar. Depois é só executar o comando git merge [head] onde head é o nome do branch que queremos unir ao “branch” atual. Lembrando que podemos ver qual o branch atual com o comando git branch. Abaixo um exemplo onde verificamos o branch atual e executamos um merge trazendo todas as modificações do master para o novo_branch.

O processo de merge é realizado da seguinte forma:
O git identifica o ancestral comum dos dois branches
Se current = ancestral então fast-forward
Caso contrário tenta fazer o “merge” em current
Se não existirem conflitos cria um “commit” com dois pais, o current e o merge.
Se existir conflitos insere marcadores e não cria commit.
Mesmo quando existem conflitos e o “commit” não é criado o git lembra que estamos no meio de uma resolução de conflito, de modo que o próximo commit terá referência aos dois commits pais utilizados no merge.
Colaborando
Até aqui vimos como criar um repositório e trabalhar sozinhos nele. No entanto não é isso que normalmente fazemos no desenvolvimento de software, pois temos uma equipe e precisamos trabalhar de forma colaborativa. Depois de compreender os conceitos explicados até aqui, entender como a colaboração funciona é trivial. Basta ver que assim como a nossa história de desenvolvimento é um grande grafo direcionado acíclico, a mesma história de repositórios distintos são grafos paralelos com os quais podemos fazer merges e branches. Veremos agora alguns comandos e dicas para trabalhar com repositórios remotos.
O primeiro passo é copiar os dados de um repositório remoto para que possamos ver seu conteúdo, isso é feito com o comando git clone. Abaixo um exemplo onde eu copio o repositório do guia gitmagic, usado como base para esse post (veja os links no post anterior).

A seqüência de operações de um git clone é a seguinte:
Cria o diretório e executa o git init
Copia todos os commits e heads
Adiciona uma referência ao repositório chamada origin
Adiciona heads remotos
Configura uma head para rastrear o head corrente remoto
Depois do clone um branch remoto está disponível e pode ser visualizado com o comando git branch -r como no exemplo abaixo.

Podemos ver na imagem que foi criado o branch origin/master que é o branch local que rastreia o repositório remoto. Agora sempre que executarmos o comando git fetch as modificações serão baixadas do repositório remoto. Agora podemos entender melhor a função do git pull. O git pull [repositorio] [head remoto] faz o fetch das informações remotas e executa um merge do head remoto passado como parâmetro para o head corrente (HEAD). No entanto, quando temos um branch já configurado para rastrear um branch remoto não precisamos passar o parâmetro para o pull.
Para criar um branch que rastreia outro podemos criá-lo com o comando git branch --track [novo-branch-local] [branch-remoto]
Depois de trabalhar com dados dos branches remotos é natural que queiramos enviar as modificações feitas. Para isso usamos o git push [repositório remoto] [head remoto]. Sem argumentos o git push envia todos os branches rastreados. O push envia todos os commits, reaponta os heads remotos e reaponta os branches locais que rastreiam os branches remotos. Para fazer um “push” é importante que o merge remoto seja um fast-forward, para isso geralmente realizamos um pull pouco antes do push para evitar modificações no estado do projeto entre os dois comandos.
Por enquanto era isso, ainda tenho mais um post com algumas dicas de manipulação do histórico, mas fica para depois.
Posted by Pedro Axelrud