Conceitos básicos: branching, merging, rebasing e pull requests

Version Control System (Sistema de Controle de Versão) é um sistema que registra as alterações feitas em um arquivo ou conjunto de arquivos ao longo do tempo, armazenando as modificações junto com a assinatura e a descrição da alteração realizada pelo autor[1]. Dessa forma, ele permite reverter determinados arquivos ou até um projeto inteiro para um estado anterior, possibilitando a recuperação de arquivos perdidos ou a correção de defeitos no projeto. A partir disso, surgiram três tipos de VCS: Sistemas Locais de Controle de Versão, que gerenciam versões de arquivos em um único computador; Sistemas Centralizados de Controle de Versão, que utilizam um repositório central onde todos os arquivos e versões são armazenados; e Sistemas Distribuídos de Controle de Versão, nos quais cada usuário tem uma cópia completa do repositório, incluindo o histórico de versões.

Neste tutorial, serão apresentados alguns conceitos utilizados para gerenciar um projeto desenvolvido e hospedado em um Sistema Distribuído de Controle de Versão, e serão utilizados os comandos do Git[2] — o sistema mais popular desse tipo — como exemplo.

Branching (Ramificação)[3]

editar

Um branch é uma ramificação do código-fonte que permite trabalhar de forma isolada, sem afetar a versão principal do projeto. Isso permite que diferentes funcionalidades, correções ou experimentos sejam desenvolvidos paralelamente, de forma independente.

Exemplo

editar

A figura a seguir ilustra um exemplo de repositório, com os commits representados como círculos, os branches como retângulos e o branch atual destacado com cor verde. Nesse caso, o repositório possui 3 commits, 1 branch, e o branch atual é o master.

 
Histórico de Commit

Criação do Novo Branch

editar

Para criar um novo branch, utiliza-se o seguinte comando descrito abaixo. Quando o branch de origem é omitido, o novo branch é criado a partir do branch em que você está trabalhando no momento.

$ git branch <nome-do-novo-branch> <branch-de-origem>
 
Histórico de Commit criando um novo Branch

Mudar Branch de trabalho

editar

Para mudar o branch de trabalho, utiliza-se o seguinte comando. Note que isso não pode ser feito caso o branch atual em que você está trabalhando possua alterações não commitadas:

$ git checkout <nome-do-branch>
 
Histórico de Commit mudando Branch de trabalho

Merge (Mesclagem)[3]

editar

Quando o trabalho é concluído, as modificações do branch <nome-do-branch> poderão ser integradas ao branch de trabalho atual por meio de um merge, utilizando o seguinte comando no Git:

$ git merge <nome-do-branch>

Exemplo

editar

Considere a seguinte situação, que apresenta um histórico de commits mais complexo, envolvendo múltiplos branches e modificações nos arquivos, destacadas pelas cores diferentes no arquivo:

 
Histórico de commit mais complicado

Fast-Forward Merge

editar

No exemplo mostrado acima, quando é chamado o comando git merge Branch1, como o commit atual do Branch1 pode ser alcançado a partir do commit atual do Master, o Git simplesmente move o ponteiro para a frente, porque não há alterações divergentes para mesclar — isso é conhecido como um merge "Fast-Forward".

 
Histórico de commit realizando um merge fast-forward

Deletar um Branch desnecessário

editar

Como não precisamos mais do Branch1, podemos chamar o seguinte comando para deletá-lo. Dessa forma, deixaremos o histórico mais limpo e facilitaremos a visualização e manutenção do projeto posteriormente. Este passo será omitido nas próximas partes do tutorial para facilitar a leitura. Note que isso não quer dizer que, ao realizar o comando de merge, o Git deleta automaticamente o branch mesclado.

$ git branch -d <nome-do-branch>
 
Histórico de commit apagando um branch

Three-way Merge

editar

Quando tentamos chamar agora o comando git merge Branch2, note que o commit atual do Master e do Branch2 estão em ramificações distintas, ou seja, não é possível acessar diretamente o commit do Branch2 a partir do commit do Master. Nesse caso, ocorre um processo conhecido como "Three-Way Merge", no qual o Git acessará os seguintes três commits: o commit ancestral comum do Master e Branch2, o commit atual do Master e o commit atual do Branch2. Em seguida, o Git compara essas três versões e tenta combinar automaticamente as mudanças feitas em cada branch desde o ponto comum. Se não houver conflitos, o Git mescla as alterações e cria um commit de merge especial, que tem mais de um ancestral, como o exemplo mostrado abaixo.

 
Histórico de commit realizando merge three-way merge sem conflito

Por fim, ao chamar o comando git merge Branch3, note que o arquivo F1 foi modificado em ambos os commits. Nesse caso, o Git reportaria um conflito de merge e solicitaria ao usuário resolver o conflito:

CONFLICT (content): Merge conflict in F1.cpp
Automatic merge failed; fix conflicts and then commit the result.

Para verificar os arquivos em conflito, também pode ser utilizado o comando git status, que retornaria uma mensagem semelhante à seguinte:

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      F1.cpp

no changes added to commit (use "git add" and/or "git commit -a")

Abrindo o arquivo F1 para edição, você encontrará seções no seguinte formato, que indicam um conflito:

<<<<<<< HEAD
cout << "Este é um tutorial feito no curso MAC0332 - Engenharia de Software (2024)";
=======
cout << "O curso MAC0332 é ministrada pelo professor Pedro Henrique Dias Valle no IME-USP";
>>>>>>> Branch3

Todas as linhas acima de ======= e abaixo de >>>>>>> HEAD pertencem ao arquivo que está atualmente no seu branch de trabalho (no nosso caso, o master), enquanto tudo que estiver abaixo de ======= e acima de >>>>>>> Branch3 pertence ao Branch3. Para solucionar o conflito, basta escolher um dos lados ou escrever um conteúdo à sua escolha, substituindo o bloco completo anterior:

cout << "Autor deste tutorial é Fernando Yang";

Depois de resolver todos os conflitos, é necessário usar o comando git add para adicionar os arquivos que estavam em conflito ao staging, e usar o comando git commit para finalizar o merge.

Assim como o merge, o rebase é outra maneira de integrar as mudanças de um branch para outro. Embora os arquivos resultantes sejam semelhantes, o histórico de commits criado por esses métodos é diferente. Para utilizar o rebase para unir as modificações dos branches, utiliza-se o seguinte comando:

git rebase <nome-do-branch>

Exemplo

editar

Considere a seguinte situação, que apresenta um histórico de commits com dois branches não lineares, com alguns commits marcados com seus respectivos nomes:

 
Histórico de Commit com dois branches não lineares

Histórico de Commit utilizando Merge

editar

Quando utilizamos o merge para integrar o conteúdo do Branch no Master, o Git cria um commit de merge C3 que une as mudanças dos dois branches, preservando o histórico original de ambos. Nesse caso, o histórico se torna não linear, com uma bifurcação visível, indicando que os branches foram unidos.

 
Histórico de Commit com dois branches não lineares unidas pelo merge

Histórico de Commit utilizando Rebase

editar

Agora, quando utilizamos o rebase para integrar o conteúdo do Branch no Master, o Git pega todas as alterações que foram confirmadas no Master e as reaplica no branch atual. Nesse caso, o histórico resultante é linear, pois o Git reescreve a sequência de commits de forma que os commits do Master parecem ter sido feitos após os commits do Branch, sem criar um commit de merge. No entanto, isso reescreve o histórico, o que também altera o timestamp dos commits e isso pode ser problemático caso o branch for público.

 
Histórico de Commit com dois branches não lineares unidas pelo rebase

Conflito

editar

Assim como no merge, ao utilizar o rebase para integrar dois branches, pode ocorrer um conflito nas modificações realizadas nos arquivos. Para resolver isso, o processo é semelhante ao procedimento descrito na seção sobre merge. No entanto, após resolver todos os conflitos, em vez de usar git commit, você deve usar o comando git rebase --continue para prosseguir com o rebase.

Pull Requests[6] [7]

editar

Diferente dos conceitos anteriores, o Git, por si só, não possui o conceito de Pull Request. Esse é um mecanismo de colaboração amplamente utilizado em plataformas de hospedagem de código, como GitHub[8] e GitLab[9], que facilita a colaboração de uma comunidade de desenvolvedores em um mesmo projeto.

A ideia central do Pull Request (PR) é que, em vez de permitir que um desenvolvedor integre diretamente suas modificações — geralmente em um branch de desenvolvimento ou branch de feature — sejam integradas a outro branch, ele precisa fazer uma solicitação para que isso aconteça. No processo de PR, os mantenedores do projeto podem revisar e discutir o conjunto de alterações propostas. Eles podem, durante essa revisão, solicitar ajustes no código ou até recusar a solicitação. Esse processo de revisão ajuda a garantir a qualidade do código, evitando bugs indesejáveis e manter uma boa organização do repositório.

Observações

editar

Este tutorial cobre apenas o básico de cada um dos conceitos de branch, merge, rebase e pull requests. Para mais detalhes, consulte a página de ajuda de cada comando e as páginas de referência usadas neste tutorial.

Referências

editar
  1. Começando - Sobre Controle de Versão
  2. Git (Site Oficial)
  3. 3,0 3,1 Branches no Git - O básico de Ramificação (Branch) e Mesclagem (Merge)
  4. Branches no Git - Rebase
  5. Merging vs. rebasing
  6. GitHub - Contribuindo em um projeto
  7. Sobre solicitação de pull
  8. GitHub (Site Oficial)
  9. GitLab (Site Oficial)