O Git é uma ferramenta de controle de versão que facilita muito o trabalho das equipes de desenvolvimento. Muitos desenvolvedores utilizam o Git para realizar o controle dos artefatos de código que vão sendo gerados ao longo do tempo. E, certamente, este controle envolve administrar os possíveis conflitos em arquivos quando dois ou mais desenvolvedores alteram o mesmo trecho de código ao mesmo tempo. Nesse tipo de situação, temos o famoso e temido “merge”, onde precisamos mesclar as alterações de todos os desenvolvedores, de modo que nenhum deles perca seu trabalho.
Quando falamos exclusivamente sobre o Git, nós temos duas maneiras de fazermos este trabalho de “mistura” de código: utilizando o comando rebase ou utilizando o comando merge. Vamos verificar nesse artigo as diferenças entre estes dois comandos e quando é a melhor hora de usar cada um. Ambos os comandos resolvem o mesmo problema, porém, de maneiras diferentes.
Curso Git e GitHub - Controle de versão
Conhecer o cursoO que é o git merge?
Vamos imaginar que você precisa começar a trabalhar em uma tarefa dentro de um projeto controlado pelo Git. Se você seguir corretamente o Git Flow, você precisará criar um branch para que você consiga trabalhar em sua tarefa. Podemos ilustrar esta situação da seguinte maneira:
À medida que você for trabalhando na sua tarefa, você provavelmente irá gerar vários commits em seu branch. Nossa ilustração poderia ficar da seguinte maneira:
Até aí, tudo ocorrendo na normalidade. Porém, o grande problema que podemos enfrentar é quando o branch master em nosso cenário fica “dessincronizado” com relação ao branch da nossa funcionalidade. Essa dessincronização pode acontecer por uma situação bem comum: outras pessoas mexeram no branch master enquanto trabalhávamos no branch da nossa tarefa. E a grande confusão vem do fato que o branch da nossa tarefa também foi gerado através do master, porém, em um momento anterior destas alterações. Podemos ilustrar essa situação da seguinte maneira:
Nós precisamos, de alguma maneira, misturar os commits do branch da nossa tarefa com os commits posteriores que foram feitos no branch master, quando este for “fundido” novamente. Uma das maneiras de se fazer isso é justamente utilizar o git merge.
O merge basicamente cria um novo commit no branch onde o merge é realizado. Este commit puxa consigo a última referência do branch a partir do qual o merge é realizado. Este commit “especial” é chamado de merge commit. Supondo então que você rode os comandos abaixo:
git checkout feature # indo para o branch da feature
git merge master # fazendo o merge entre o feature e o master
Nós podemos ter a ilustração abaixo:
O merge é legal principalmente porque ele não altera os branches envolvidos. Na ilustração anterior, você pode perceber que tanto o branch feature quanto o branch master não são gerados. O master nem teve suas estruturas envolvidas na operação, enquanto o branch feature teve somente um commit adicional.
O fato de a estrutura dos branches envolvidos não ser diretamente modificada reduz a possibilidade de haver problemas durante a operação de merge, mas também cria um commit adicional “esquisito” a cada operação. Esse commit adicional pode prejudicar a leitura do histórico dos commits através do git log, principalmente para quem ainda não está tão ambientado ao Git e seus conceitos.
Curso Git - Fundamentos
Conhecer o cursoO que é git rebase?
Vamos, mais uma vez, considerar a situação na qual os branches feature e master estão “dessincronizados” e precisam ser misturados. Vamos relembrar a ilustração que mostra esta situação.
Nós também podemos resolver esta situação através do rebase. O rebase literalmente unifica os branches envolvidos, puxando os commits para frente do branch de destino. É como se ele estivesse “refazendo” a base do branch onde o comando é executado.
Sendo assim, se executarmos os comandos abaixo:
git checkout feature # indo para o branch da feature
git rebase master # fazendo o rebase entre o feature e o master
Nós podemos ter a ilustração abaixo.
Pela ilustração acima, podemos ver que algo legal que o rebase faz é produzir um histórico linear, mais limpo e mais fácil de ser lido, pois os branches são literalmente fundidos. Pela fusão, também não é gerado aquele commit adicional “estranho” que acontece no merge.
Agora, o grande problema é que o rebase mexe com toda a estrutura dos branches envolvidos, reescrevendo inclusive o histórico de commits destes branches. Se este processo de reescrita não for bem executado, podem ocorrer vários problemas no histórico dos branches envolvidos, causando até mesmo perda de trabalho durante o processo de mesclagem. Existe também o ponto de que, pelo fato de o rebase não gerar o merge commit, você não consegue ter a “rastreabilidade” de quando dois branches de fato foram fundidos, já que o rebase gera um branch linear no final do processo.
Quando usar o git merge e quando utilizar o git rebase?
Ambos os comandos são muito úteis, porém, em situações diferentes. Quando seguimos corretamente o Git Flow, existe uma regra que chamamos de “Golden Rule of Rebasing”. Basicamente, essa regra diz que o rebase não deve ser utilizado em branches públicos, já que ele tem um alto potencial catastrófico e destrói o histórico de commits, causando sempre divergências entre os branches locais e os branches remotos. Porém, existe um fluxo de trabalho muito legal que pode ser utilizado, combinando ambos os comandos.
Vamos voltar à nossa situação inicial: você precisa desenvolver uma tarefa. Você provavelmente irá gerar um novo branch para desenvolver sua tarefa a partir do master. Podemos ter então o comando abaixo:
git checkout –b feature # criando o branch de trabalho
Isso gerará a seguinte ilustração:
Depois, nós poderemos gerar nossos commits, salvando nosso trabalho gradativamente…
… enquanto outras pessoas podem ir misturando seus commits ao master.
Agora chega a hora de integrarmos nossos commits ao master. Nós precisamos fazer isso em duas vias, basicamente:
1) Trazer as alterações que foram inseridas no master posteriormente para dentro de nosso branch. Precisamos deste passo para garantir que as alterações posteriores não irão “quebrar” as alterações que fizemos em nosso branch;
2) Levar as alterações do nosso branch para o master, atualizando-o com o nosso trabalho e o disponibilizando para os demais desenvolvedores.
Vamos tratar do primeiro passo neste momento. Se pararmos para analisar a situação, faz mais sentido trazer os commits de master para feature de maneira “transparente”, ou seja: sem a necessidade de um commit adicional. Isso vai manter o nosso histórico completamente linear e irá garantir que os novos commits advindos do master serão aplicados a nosso branch no “momento correto”, preservando a ordem no qual eles foram gerados. Por isso, para esta primeira etapa, o rebase faz muito mais sentido. Sendo assim, supondo que já estejamos no branch feature, podemos executar o comando abaixo:
git rebase master
Isso irá produzir a seguinte ilustração:
O primeiro passo foi concluído! Nós temos o nosso branch feature devidamente sincronizado. Porém, nosso branch master, que é o branch principal, ainda está defasado, pois ele ainda não possui os nossos commits. Nós precisamos puxar nossos commits do branch feature para o branch master. Como estamos falando do branch principal, é importante mantermos a rastreabilidade do processo de gestão do código, deixando claro inclusive quando os commits de branches locais foram puxados para o branch master.
Por isso, para essa segunda etapa, o merge é muito mais interessante do que o rebase. Nesta situação, todos os possíveis problemas de “mistura” dos dois branches (como conflitos) foram resolvidos durante o processo de rebase. Nesta segunda etapa, nós podemos simplesmente trocar o ponteiro do nosso branch master, apontando-o para o último commit do nosso branch feature. Quando esse processo acontece, nós chamamos esta estratégia de merge de fast-foward. O fast-foward é possível quando não existem conflitos entre os branches envolvidos e quando o branch de desenvolvimento está à frente do branch principal. E, pela ilustração anterior, é exatamente esta a situação.
Portanto, se executarmos os comandos abaixo:
git checkout master # indo para o branch do master
git merge feature # fazendo o merge entre o master e o feature
Teremos no final a ilustração abaixo:
Por isso, nosso branch master agora está da seguinte maneira:
Veja que, dessa maneira, temos o melhor dos dois mundos: não temos o histórico de commits poluído pelos commits secundários no branch feature (graças ao rebase) e temos a rastreabilidade do momento onde o branch master foi modificado por uma integração (por causa do merge).