Git avançado
Sumário
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.
- A branch
mainé a linha de desenvolvimento principal, e as branchesfeature1efeature2são ramificações que foram criadas para desenvolver novas funcionalidades.
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.
-
1) Podemos iniciar o repositório com a branch principal chamada de
main:git init --initial-branch=main -
2) Em seguida, podemos criar uma nova branch a partir dela com o
git branch <nome_da_branch> <branch_base>:git branch feature-legal main -
Alternativamente, podemos omitir o nome da branch base, e o
gitvai assumir que queremos criar a nova branch a partir da qual estamos atualmente. -
3) Precisamos mudar para a nova branch para começar a trabalhar nela, então usamos o comando
git switch <branch>(que serve apenas para trocar de branches) ougit checkout(mais sobre ele no futuro):git switch feature-legal
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:
-
Estando na target branch:
git merge <source_branch>
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:
- É alinhado à linha de desenvolvimento principal:
- Os commits da branch
featuresão incorporados amain, e oHEADavanç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:
- Note que, de fato, é impossível alcançar o commit F, a partir do commit E, logo, isso significa que as branches
divergiram e que o
gitprecisará realizar o three-way merge.
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:
- Decidir o que manter, editar o arquivo e remover a decoração de conflito.
- Adicionar o arquivo ao stage e consolidar o merge commit.
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:
- Você e seu colega estão trabalhando em duas funcionalidades diferentes no mesmo projeto, e o histórico de commits se parece com isso:
-
Seu colega terminou antes de você e publicou a branch dele remotamente, e já incorporou as alterações dele na
main.Sua versão local Versão remota 

-
Você terminou a sua parte e você incorporou as alterações dele na sua branch, e agora você quer publicar o que foi feito remotamente.
Sua versão local Versão remota 

-
Entretanto, o
gitnão vai permitir que você publique as alterações remotamente, visto que, o histórico de commits da sua branchmaindivergiu completamente da versão remota.
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:
-
Comunique-se constatemente e abertamente com a equipe sobre quais partes do projeto cada um está trabalhando.
-
Faça commits frequentes e pequenos assim como a filosofia do Unix sugere para o desenvolver de software. Isso mantém o repositório atualizado, diminui a chance de conflitos e facilita revisitar o commit no futuro.
-
Mantenha suas branches de feature curtas e mescle-as na
mainfrequentemente, pois branches de longa-duração tendem a se desviar significantemente de outras e criar conflitos.
Exercícios
Exercício 1
-
Baixe o arquivo exercicio1.zip
-
Descompacte o arquivo onde preferir
-
Vá até a pasta
exercicio1/e tente executar o arquivoexercicio1.shpara ver o que acontece. -
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.txtpara a submissão do exercício.
Exercício 2
-
Baixe o arquivo exercicio2.zip
-
Descompacte o arquivo onde preferir
-
Execute o arquivo
responderpergunta.sh -
Explore os branches do repositório utilizando os comandos
git branchpara ver os branches disponíveis egit checkoutpara navegar entre eles. -
Após analisar o conteúdo de cada branch, utilize os aprendizados da aula de hoje para responder corretamente a pergunta feita pelo arquivo!
-
Insira os comandos utilizados num arquivo .txt para a submissão do exercício.
© PET-CC/UFRN 2024 Licenciado sob CC BY-NC-SA.