Git avançado

Desfazendo alterações

No último tópico, tivemos o nosso primeiro contato com o git, e aprendemos a criar repositórios para nos ajudar a rastrear as alterações feitas no nosso projeto localmente e remotamente. Entretanto, durante o desenvolvimento de um projeto, é comum que ocorram erros ou alterações indesejáveis. Por exemplo, podemos alterar um arquivo sem querer, ou adicionar um arquivo que não deveria ser adicionado, ou até mesmo fazer um commit com uma mensagem errada.

Além disso, quando ainda estamos desenvolvendo certa maturidade em relação ao uso do git, é muito comum tomar medidas extremas para solucionar diferentes tipos de problemas, sem de fato usar os meios que a ferramenta nos oferece . Quem nunca deletou e baixou o repositório novamente para se livrar de um simples commit errado?

Por isso, vamos apresentar algumas formas seguras e mais “elegantes” de lidar com alguns tipos de problemas que podem surgir durante o desenvolvimento de um projeto.

Desfazendo commits sem ter publicado

Imagine, por exemplo, que você tem um repositório com seguinte histórico de commits:


Você estava desenvolvendo a funcionalidade E, e agora é momento de finalmente fazer o commit e salvar essa mudança:

$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
 E

nothing added to commit but untracked files present (use "git add" to track)

$ git add E ; git commit -m R
[main 0156e00] R
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 E

Porém, imediatamente após fazer o commit, você percebe que cometeu um erro e que o nome do commit na verdade deveria ser “E” ao invés de “R”. E agora, o que fazer?

Revisitando o nosso histórico de commits (podemos fazer isso com o comando git log --oneline), urge a necessidade de voltar para o commit anterior e corrigir o nome do commit.


Para isso, existem algumas possibilidades, dentre as quais:

“git reset”

A primeira ideia é usar o comando git reset, visto que ele tem a capacidade de mover o HEAD para um commit anterior , onde, por padrão, arquivos alterados são preservados mas não ficam na stagin area.

Então, para simplesmente voltar para o commit anterior, podemos fazer:

git reset HEAD~1

aqui, o ~1 indica a quantidade de commits que queremos voltar, no caso, 1 commit. Agora, nosso histórico de commits fica assim:


Note que o commit “R” fica inacessível, mas o arquivo E continua presente no nosso diretório de trabalho. Naturalmente, podemos corrigir o nome do commit e fazer um novo commit:

git add E ; git commit -m E

Note que também podemos usar o comando git restore para restaurar qualquer arquivo que esteja no estado staged para fazer alterações antes de commitar novamente.

“git commit –amend”

Alternativamente, podemos usar o comando git commit --amend, que nos permite alterar o commit mais recente, inclusive os arquivos que foram adicionados a ele.

git commit --amend

Será aberto um editor de texto, onde você poderá alterar a mensagem do commit e, após salvar e fechar o editor, o commit será alterado.

Workflow avançado

Todas essas funcionalidades que vimos até agora sobre o git são muito úteis, mas, até então, só trabalhamos individualmente em pequenos projetos num ambiente controlado. Nesse sentido, é dada a hora de finalmente começarmos a apreciar todo o potencial das ferramentas oferecidas pelo git para trabalhar em ambientes de coloboração. A primeira dessas ferramentas que vamos explorar são as branches.

Git branching

Se você pensar no seu histórico de commits como uma árvore, você pode visualizar branches como ramificações ou galhos dessa árvore. A ideia por trás das branches é permitir que você e seus companheiros de projeto trabalhem em diferentes partes do projeto, sem interferir diretamente no trabalho dos outros.

O uso dessa ferramenta pode variar a depender da necessidade e política de desenvolvimento de cada projeto, entretanto, uma pratica comum é definir uma branch principal, geralmente chamada de master ou main, e a cada nova funcionalidade ou correção de bug, criar uma nova branch a partir da principal. Essas branches secundárias são o que chamamos de topic branches ou feature branches, e assim que elas cumprem o seu propósito, são incorporadas na branch principal e deletadas.


Como você já deve ter visto, por padrão, o quando usamos o comando git init, o programa cria automaticamente uma branch principal chamada de master. Uma vez criada, podemos alterar o nome dela para um nome mais significativo, como main, e criar novas branches a partir dela e trabalhar em novas funcionalidades para o projeto.

Pronto! Já temos quase tudo que precisamos para trabalhar efetivamente com branches, podemos “commitar” e fazer tudo que já sabemos fazer, mas agora, de forma isolada do restante do projeto, sem correr grandes riscos. Porém, ainda não sabemos como incorporar mudanças feias numa branch e como criar branches remotas.

Branches locais e remotas

Quando trabalhamos com repositórios remotos, é importante saber que existem duas referências à branch que estamos trabalhando atualmente: uma local e outra remota. Quando criamos uma nova branch, essa referência remota não é criada automaticamente, então, cabe a nós fazer isso manualmente.

Por exemplo, suponha que criamos uma nova branch local chamada `feature-legal’, fizemos alguns commits nela, e, então, queremos compartilhar essa branch com colegas de trabalho ou apenas salvar o progresso na nuvem. Para isso, podemos criar a referência remota com o seguinte comando:

git push -u origin feature-legal

(Se o nome do repositório remoto for origin)

Git merging

Nos últimos tópicos, vimos um punhado sobre branches, e como elas podem ser úteis para trabalhar em equipe, mas, como que podemos concretizar um projeto usando branches, se não sabemos como juntar o que foi feito em cada uma delas?

Dada essa preocupação, o git nos oferece o git merge que serve para integrar as alterações feitas em uma branch a outra. Em qualquer “merge” ou “mesclagem”, a branch que está sendo mesclada é chamada de * source branch* (ou “ramificação fonte”, em Português) e a branch que está recebendo as alterações é chamada de target branch (ou “ramificação alvo”), e seu uso consiste em:

Mas, nem sempre é tão simples assim, e existem diferentes formas que o git pode realizar essa mesclagem, as quais impactam diretamente no seu histórico de commits.

Fast-forward merge

Uma das formas que o merge ocorre é fast-foward, e a ideia é que a target branch vai apenas avançar o seu histórico em relação a source branch, por exemplo, imagine o seguinte histórico de commits:


Suponha que a branch vermelha (feature) cumpriu seu propósito e agora você quer mesclar o que foi feito nela a linha de desenvolvimento principal (main). Pensando de forma lúdica, o git realizaria a mesclagem apenas descendo essas bolinhas vermelhas e deixando equiparadas com a main e avançando o HEAD para o commit mais recente da feature. Visualmente, isso ocorre da seguinte maneira:

  1. É alinhado à linha de desenvolvimento principal:


  1. Os commits da branch feature são incorporados a main, e o HEAD avança:


Mas, e se a divergência não for assim tão simples e seu histórico estiver análogo à figura abaixo, seria possível fazer esse avanço?

Three-way merge

A outra forma de o git realizar merges é o three-way merge. Ele acontece quando é impossível alcançar a cabeça da target branch seguindo os commits parentes a partir da cabeça da source branch. Visualmente, isso seria:


Para conseguir realizar a mesclagem, o git precisa criar um novo commit que tenha como parente os dois últimos commits da source e target branch, de forma que o histórico das duas fiquem acessíveis a partir da mesma branch.


E esse processo só ocorre se alterações feitas em uma branch não interferirem diretamente nas alterações feitas na outra, caso contrário, o git não conseguirá realizar a mesclagem e você terá que resolver cada conflito manualmente, para consolidar o merge commit.

Lidando com conflitos

Para além do three-way merge e do git, um dos desafios mais clássicos que enfretamos ao trabalhar com projetos e com outras pessoas é a resolução de conflitos. Seja por falta de comunicação entre a equipe, planejamento, erro humano, ou qualquer outra razão, é muito comum que duas ou mais pessoas acabem trabalhando na mesma parte do projeto ao mesmo tempo e isso resultar em conflitos.

Consequentemente, com o git não é diferente, e conflitos ocorrem no momento em que o git não consegue mesclar duas branches automaticamente via three-way merge. Para resolver o conflito, você precisa intervir diretamente na parte do arquivo que é conflitante entre as branches e decidir o que será mantido.

Portanto, vamos investigar quais são algumas das principais causas de conflitos e como resolvê-los.

Alterações no mesmo arquivo

Um conflito no three-way merge é dado quando duas ou mais pessoas trabalham na mesma parte de um arquivo, visto que, ao mesclar as alterações, o git é incapaz de decidir qual versão manter. Dessa forma, o git vai te dizer em qual arquivo houve conflito, e vai decorar o arquivo com marcações especiais para lhe mostrar as diferentes versões de determinadas linhas do arquivo. Essas marcações são:

<<<<<<< HEAD
{ Conteúdo da target branch }
=======
{ Conteúdo da source branch* }
>>>>>>> { source branch }

Para resolver esse tipo de conflito, voce vai precisar:

Estado do repositório local e repositório remoto

Em qualquer projeto que envolva mais de uma pessoa, naturalmente, ocorrerão mudanças recorrentes no repositório, e sempre alguém vai terminar antes ou depois de outra pessoa. Nesse sentido, tente visualizar comigo o seguinte cenário:


Dada a problemática, o que fazer?

O git vai te dar a oportunidade de mesclar a sua branch local com a remota via three-way merge, para que você consiga publicar as alterações com sucesso. Mas, note que será criado um commit desnecessário por descuido, além da alta chance de haverem conflitos por possíveis alterações no mesmo arquivo.

Num cenário ideal, sempre antes e depois de trabalhar, atualize o seu repositório local com os comandos que já aprendeu, para evitar esse tipo de problema.

Divergências significativas

É mais comum do que se imagina, especialmente em equipes grandes ou entre novatos no uso do git, a criação de divergências significativas em uma ou mais branches. Por exemplo, se você está trabalhando em uma branch feature enquanto seu colega está na main e ambos fazem mudanças significativas que afetam o mesmo arquivo, é provável que não será possível incorporar suas alterações na branch principal sem enfrentar conflitos.

A causa desse tipo de conflito, é, principalmente, a falta de comunicação e planejamento entre as partes.

Prevenindo conflitos

A maioria dos conflitos no git não fogem muito do que foi apresentado até agora, então, para previnir esses tipos de conflitos, alguma práticas são recomendadas:

Exercícios

Exercício 1

  1. Baixe o arquivo exercicio1.zip

  2. Descompacte o arquivo onde preferir

  3. Vá até a pasta exercicio1/ e tente executar o arquivo exercicio1.sh para ver o que acontece.

  4. O arquivo está cheio de erros! Sua missão é corrigir o arquivo sem alterá-lo diretamente em um editor de texto, apenas com os conhecimentos que aprendemos hoje em sala. Quando terminar, insira os comandos utilizados num arquivo resposta.txt para a submissão do exercício.

Exercício 2

  1. Baixe o arquivo exercicio2.zip

  2. Descompacte o arquivo onde preferir

  3. Execute o arquivo responderpergunta.sh

  4. Explore os branches do repositório utilizando os comandos git branch para ver os branches disponíveis e git checkout para navegar entre eles.

  5. Após analisar o conteúdo de cada branch, utilize os aprendizados da aula de hoje para responder corretamente a pergunta feita pelo arquivo!

  6. Insira os comandos utilizados num arquivo .txt para a submissão do exercício.


© PET-CC/UFRN 2024 Licenciado sob CC BY-NC-SA.