-
1. Primeiros Passos
- 1.1 Sobre Controle de Versão
- 1.2 Uma Breve História do Git
- 1.3 O que é Git?
- 1.4 A Linha de Comando
- 1.5 Instalando o Git
- 1.6 Configuração inicial do Git
- 1.7 Obtendo Ajuda
- 1.8 Resumo
-
2. Fundamentos do Git
-
3. Ramificação (Branching) no Git
-
4. Git no Servidor
- 4.1 Os Protocolos
- 4.2 Colocando o Git em um Servidor
- 4.3 Gerando a sua Chave Pública SSH
- 4.4 Configurando o Servidor
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Opções de Hospedagem de Terceiros
- 4.10 Resumo
-
5. Git Distribuído
-
6. GitHub
-
7. Ferramentas do Git
- 7.1 Seleção de Revisão
- 7.2 Área de Stage Interativa
- 7.3 Fazendo Stash e Limpando
- 7.4 Assinando Seu Trabalho
- 7.5 Pesquisando (Searching)
- 7.6 Reescrevendo o Histórico
- 7.7 Reset Desmistificado
- 7.8 Merging Avançado
- 7.9 Rerere
- 7.10 Depurando com Git
- 7.11 Submódulos
- 7.12 Empacotando (Bundling)
- 7.13 Substituir (Replace)
- 7.14 Armazenamento de Credenciais
- 7.15 Resumo
-
8. Customizando o Git
- 8.1 Configuração do Git
- 8.2 Atributos do Git
- 8.3 Hooks do Git
- 8.4 Uma Política de Exemplo Imposta pelo Git
- 8.5 Resumo
-
9. Git e Outros Sistemas
- 9.1 Git como Cliente
- 9.2 Migrando para o Git
- 9.3 Resumo
-
10. Git Internals (Por Dentro do Git)
- 10.1 Encanamento e Porcelana (Plumbing and Porcelain)
- 10.2 Objetos do Git
- 10.3 Referências do Git
- 10.4 Arquivos de pacote (Packfiles)
- 10.5 Refspec
- 10.6 Protocolos de Transferência
- 10.7 Manutenção e Recuperação de Dados
- 10.8 Variáveis de Ambiente
- 10.9 Resumo
-
A1. Appendix A: O Git em Outros Ambientes
- A1.1 Interfaces Gráficas
- A1.2 Git no Visual Studio
- A1.3 Git no Visual Studio Code
- A1.4 Git no IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine
- A1.5 Git no Sublime Text
- A1.6 Git no Bash
- A1.7 Git no Zsh
- A1.8 Git no PowerShell
- A1.9 Resumo
-
A2. Appendix B: Incorporando o Git nos seus Aplicativos
- A2.1 Linha de Comando do Git
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. Appendix C: Comandos do Git
- A3.1 Configuração e Ajustes
- A3.2 Obtendo e Criando Projetos
- A3.3 Snapshots Básicos
- A3.4 Branches e Merges
- A3.5 Compartilhando e Atualizando Projetos
- A3.6 Inspeção e Comparação
- A3.7 Depuração
- A3.8 Patches
- A3.9 E-mail
- A3.10 Sistemas Externos
- A3.11 Administração
- A3.12 Comandos de Encanamento
7.8 Ferramentas do Git - Merging Avançado
Merging Avançado
Fazer merge no Git é tipicamente bem fácil. Já que o Git facilita o merge de outra branch várias vezes, isso significa que você pode ter uma branch de vida muito longa, mas você pode mantê-la atualizada conforme avança, resolvendo pequenos conflitos frequentemente, em vez de ser surpreendido por um conflito enorme no final da série.
No entanto, às vezes ocorrem conflitos complicados. Ao contrário de alguns outros sistemas de controle de versão, o Git não tenta ser excessivamente esperto sobre a resolução de conflitos de merge. A filosofia do Git é ser inteligente para determinar quando a resolução de um merge é inequívoca, mas se houver um conflito, ele não tenta ser esperto para resolvê-lo automaticamente. Portanto, se você esperar muito tempo para fazer merge de duas branches que divergem rapidamente, você pode ter alguns problemas.
Nesta seção, abordaremos quais podem ser alguns desses problemas e quais ferramentas o Git fornece para ajudar a lidar com essas situações mais complicadas. Também abordaremos alguns dos diferentes tipos não padronizados de merges que você pode fazer, bem como veremos como desfazer merges que você já fez.
Conflitos de Merge
Embora tenhamos coberto alguns conceitos básicos sobre como resolver conflitos de merge em Conflitos Básicos de Mesclagem (Merge Conflicts), para conflitos mais complexos, o Git fornece algumas ferramentas para ajudá-lo a descobrir o que está acontecendo e como lidar melhor com o conflito.
Em primeiro lugar, se possível, tente certificar-se de que o seu diretório de trabalho está limpo antes de fazer um merge que possa ter conflitos. Se você tiver trabalho em andamento, ou comite-o em uma branch temporária ou faça o stash dele. Isso faz com que você possa desfazer qualquer coisa que tentar aqui. Se você tiver alterações não salvas no seu diretório de trabalho ao tentar um merge, algumas dessas dicas podem ajudá-lo a preservar esse trabalho.
Vamos analisar um exemplo muito simples. Temos um arquivo Ruby super simples que imprime 'hello world'.
#! /usr/bin/env ruby
def hello
puts 'hello world'
end
hello()
No nosso repositório, nós criamos uma nova branch chamada whitespace e procedemos à alteração de todos os finais de linha do Unix para os finais de linha do DOS, alterando essencialmente todas as linhas do arquivo, mas apenas com espaços em branco (whitespace).
Então nós alteramos a linha “hello world” para “hello mundo”.
$ git checkout -b whitespace
Switched to a new branch 'whitespace'
$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert hello.rb to DOS
1 file changed, 7 insertions(+), 7 deletions(-)
$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
#! /usr/bin/env ruby
def hello
- puts 'hello world'
+ puts 'hello mundo'^M
end
hello()
$ git commit -am 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
1 file changed, 1 insertion(+), 1 deletion(-)
Agora nós mudamos de volta para a nossa branch master e adicionamos alguma documentação para a função.
$ git checkout master
Switched to branch 'master'
$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
puts 'hello world'
end
$ git commit -am 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
1 file changed, 1 insertion(+)
Agora tentamos fazer o merge da nossa branch whitespace e teremos conflitos por causa das alterações de espaço em branco.
$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.
Abortando um Merge
Nós agora temos algumas opções.
Primeiro, vamos cobrir como sair desta situação.
Se talvez você não estivesse esperando conflitos e não quer lidar bem com a situação ainda, você pode simplesmente cancelar o merge com git merge --abort.
$ git status -sb
## master
UU hello.rb
$ git merge --abort
$ git status -sb
## master
A opção git merge --abort tenta reverter de volta para o seu estado antes de você rodar o merge.
Os únicos casos onde ele pode não conseguir fazer isso perfeitamente seria se você tivesse alterações não comitadas e não stashadas no seu diretório de trabalho quando você o rodou, caso contrário, deve funcionar bem.
Se por alguma razão você só quer começar de novo, você também pode rodar git reset --hard HEAD, e seu repositório voltará para o último estado comitado.
Lembre-se que qualquer trabalho não comitado será perdido, então certifique-se de que você não quer nenhuma das suas alterações.
Ignorando Espaços em Branco (Whitespace)
Neste caso específico, os conflitos estão relacionados a espaços em branco. Nós sabemos isso porque o caso é simples, mas também é bem fácil dizer em casos reais ao olhar para o conflito porque toda linha é removida de um lado e adicionada de novo no outro. Por padrão, o Git vê todas estas linhas como tendo sido alteradas, então ele não consegue fazer o merge dos arquivos.
No entanto, a estratégia de merge padrão pode receber argumentos, e alguns deles são sobre ignorar adequadamente alterações de espaços em branco.
Se você ver que tem muitos problemas de espaços em branco em um merge, você pode simplesmente abortá-lo e fazê-lo novamente, desta vez com -Xignore-all-space ou -Xignore-space-change.
A primeira opção ignora o espaço em branco completamente ao comparar linhas, a segunda trata sequências de um ou mais caracteres de espaço em branco como equivalentes.
$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
hello.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Já que neste caso, as alterações reais no arquivo não eram conflitantes, uma vez que ignoramos as alterações de espaços em branco, tudo faz merge perfeitamente.
Isso é um salva-vidas se você tem alguém na sua equipe que gosta de ocasionalmente reformatar tudo de espaços para tabs ou vice-versa.
Re-merging Manual de Arquivos
Embora o Git lide muito bem com o pré-processamento de espaços em branco, existem outros tipos de alterações que talvez o Git não consiga lidar automaticamente, mas são correções scriptáveis. Como exemplo, vamos fingir que o Git não conseguiu lidar com a alteração de espaços em branco e nós precisamos fazer isso à mão.
O que realmente precisamos fazer é rodar o arquivo que estamos tentando fazer merge através de um programa dos2unix antes de tentar o merge real do arquivo.
Então, como faríamos isso?
Primeiro, nós entramos no estado de conflito de merge. Então nós queremos obter cópias da nossa versão do arquivo, a versão deles (da branch que estamos fazendo merge) e a versão comum (de onde ambos os lados ramificaram). Em seguida, queremos corrigir o lado deles ou o nosso e tentar novamente o merge apenas para este único arquivo.
Obter as três versões do arquivo é na verdade bem fácil.
O Git armazena todas estas versões no index sob “estágios” (stages) que cada um tem números associados a eles.
O Estágio 1 é o ancestral comum, o estágio 2 é a sua versão e o estágio 3 é do MERGE_HEAD, a versão que você está fazendo merge (“theirs”).
Você pode extrair uma cópia de cada uma dessas versões do arquivo conflitante com o comando git show e uma sintaxe especial.
$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb
Se você quiser ser um pouco mais hardcore, você também pode usar o comando de encanamento (plumbing) ls-files -u para obter os SHA-1 reais dos blobs do Git para cada um desses arquivos.
$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1 hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2 hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3 hello.rb
O :1:hello.rb é apenas um atalho (shorthand) para procurar aquele SHA-1 do blob.
Agora que temos o conteúdo de todos os três estágios em nosso diretório de trabalho, podemos consertar manualmente a versão deles para resolver a questão do espaço em branco e fazer re-merge do arquivo com o pouco conhecido comando git merge-file que faz exatamente isso.
$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...
$ git merge-file -p \
hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb
$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
Neste ponto, fizemos o merge do arquivo perfeitamente.
Na verdade, isso funciona melhor do que a opção ignore-space-change porque isso realmente conserta as alterações de espaço em branco antes do merge, em vez de simplesmente ignorá-las.
No merge com ignore-space-change, nós na verdade terminamos com algumas linhas com finais de linha do DOS, deixando as coisas misturadas.
Se você quiser ter uma ideia, antes de finalizar este commit, sobre o que foi realmente alterado entre um lado e outro, você pode pedir ao git diff para comparar o que está no seu diretório de trabalho que você está prestes a comitar como resultado do merge para qualquer um desses estágios.
Vamos analisar todos eles.
Para comparar o seu resultado com o que você tinha na sua branch antes do merge, ou seja, para ver o que o merge introduziu, você pode rodar git diff --ours:
$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@
# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
Então, aqui podemos ver facilmente que o que aconteceu na nossa branch, o que estamos realmente introduzindo neste arquivo com este merge, é a alteração daquela única linha.
Se quisermos ver como o resultado do merge diferiu do que estava no lado deles, você pode rodar git diff --theirs.
Neste e no exemplo seguinte, nós temos que usar -b para retirar os espaços em branco porque estamos comparando com o que está no Git, não o nosso arquivo limpo hello.theirs.rb.
$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
puts 'hello mundo'
end
Finalmente, você pode ver como o arquivo mudou a partir de ambos os lados com git diff --base.
$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
Neste ponto, podemos usar o comando git clean para limpar os arquivos extras que criamos para fazer o merge manual mas que não precisamos mais.
$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb
Fazendo Checkout de Conflitos
Talvez nós não estejamos felizes com a resolução neste ponto por algum motivo, ou talvez editar manualmente um ou ambos os lados ainda não tenha funcionado bem e precisamos de mais contexto.
Vamos mudar o exemplo um pouco. Para este exemplo, temos duas branches de vida mais longa que cada uma tem alguns commits nelas mas criam um conflito de conteúdo legítimo quando é feito o merge.
$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) Update README
* 9af9d3b Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code
Nós agora temos três commits únicos que vivem apenas na branch master e outros três que vivem na branch mundo.
Se tentarmos fazer o merge da branch mundo, nós teremos um conflito.
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.
Gostaríamos de ver qual é o conflito do merge. Se abrirmos o arquivo, veremos algo assim:
#! /usr/bin/env ruby
def hello
<<<<<<< HEAD
puts 'hola world'
=======
puts 'hello mundo'
>>>>>>> mundo
end
hello()
Ambos os lados do merge adicionaram conteúdo a este arquivo, mas alguns dos commits modificaram o arquivo no mesmo lugar que causou esse conflito.
Vamos explorar um par de ferramentas que você tem agora à sua disposição para determinar como este conflito ocorreu. Talvez não seja óbvio como exatamente você deveria consertar este conflito. Você precisa de mais contexto.
Uma ferramenta útil é o git checkout com a opção --conflict.
Isso fará o re-checkout do arquivo novamente e substituirá os marcadores de conflito do merge.
Isso pode ser útil se você quiser resetar os marcadores e tentar resolvê-los novamente.
Você pode passar para --conflict o valor diff3 ou merge (que é o padrão).
Se você passar diff3, o Git usará uma versão ligeiramente diferente de marcadores de conflito, não apenas dando a você as versões “ours” e “theirs”, mas também a versão “base” inline para lhe dar mais contexto.
$ git checkout --conflict=diff3 hello.rb
Uma vez que rodarmos isso, o arquivo ficará assim em vez disso:
#! /usr/bin/env ruby
def hello
<<<<<<< ours
puts 'hola world'
||||||| base
puts 'hello world'
=======
puts 'hello mundo'
>>>>>>> theirs
end
hello()
Se você gostar desse formato, você pode defini-lo como padrão para conflitos de merge futuros, ajustando a configuração merge.conflictstyle para diff3.
$ git config --global merge.conflictstyle diff3
O comando git checkout também pode receber as opções --ours e --theirs, o que pode ser uma maneira muito rápida de simplesmente escolher um lado ou o outro sem fazer merge de nada.
Isso pode ser particularmente útil para conflitos de arquivos binários onde você pode simplesmente escolher um lado, ou onde você só quer fazer merge de certos arquivos de outra branch — você pode fazer o merge e então fazer checkout de certos arquivos de um lado ou do outro antes de comitar.
Log de Merge (Merge Log)
Outra ferramenta útil ao resolver conflitos de merge é o git log.
Isso pode ajudá-lo a obter contexto sobre o que pode ter contribuído para os conflitos.
Revisar um pouco do histórico para lembrar o porquê de duas linhas de desenvolvimento estarem mexendo na mesma área de código pode ser muito útil às vezes.
Para obter uma lista completa de todos os commits únicos que foram incluídos em qualquer uma das branches envolvidas neste merge, nós podemos usar a sintaxe de “três pontos” (triple dot) que aprendemos em Ponto Triplo (Triple Dot).
$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'
Essa é uma boa lista dos seis commits totais envolvidos, bem como em qual linha de desenvolvimento cada commit estava.
No entanto, podemos simplificar ainda mais isso para nos dar um contexto muito mais específico.
Se adicionarmos a opção --merge ao git log, ele mostrará apenas os commits de ambos os lados do merge que tocam em um arquivo que está atualmente em conflito.
$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'
Se você rodar isso com a opção -p em vez disso, você obtém apenas os diffs do arquivo que acabou em conflito.
Isso pode ser muito útil para dar rapidamente a você o contexto necessário para ajudar a entender o porquê algo entrou em conflito e como resolvê-lo de forma mais inteligente.
Formato Combinado de Diff (Combined Diff Format)
Como o Git faz stage de quaisquer resultados de merge que sejam bem-sucedidos, quando você roda git diff enquanto está em um estado de merge em conflito, você só obtém o que atualmente ainda está em conflito.
Isso pode ser útil para ver o que você ainda tem que resolver.
Quando você roda git diff logo após um conflito de merge, ele lhe dará informações em um formato de saída de diff bastante único.
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
#! /usr/bin/env ruby
def hello
++<<<<<<< HEAD
+ puts 'hola world'
++=======
+ puts 'hello mundo'
++>>>>>>> mundo
end
hello()
O formato é chamado “Combined Diff” (Diff Combinado) e lhe dá duas colunas de dados ao lado de cada linha. A primeira coluna mostra se aquela linha é diferente (adicionada ou removida) entre a branch “ours” e o arquivo no seu diretório de trabalho e a segunda coluna faz o mesmo entre a branch “theirs” e a sua cópia do diretório de trabalho.
Então, nesse exemplo você pode ver que as linhas <<<<<<< e >>>>>>> estão na cópia de trabalho, mas não estavam em nenhum dos lados do merge.
Isso faz sentido porque a ferramenta de merge as colocou lá para nosso contexto, mas espera-se que nós as removamos.
Se resolvermos o conflito e rodarmos git diff novamente, veremos a mesma coisa, mas é um pouco mais útil.
$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
#! /usr/bin/env ruby
def hello
- puts 'hola world'
- puts 'hello mundo'
++ puts 'hola mundo'
end
hello()
Isso nos mostra que “hola world” estava no nosso lado mas não na cópia de trabalho, que “hello mundo” estava no lado deles mas não na cópia de trabalho e finalmente que “hola mundo” não estava em nenhum dos lados mas está agora na cópia de trabalho. Isso pode ser útil para revisar antes de comitar a resolução.
Você também pode obter isso a partir do git log para qualquer merge para ver como algo foi resolvido depois do fato.
O Git enviará este formato se você rodar git show em um commit de merge, ou se você adicionar uma opção --cc a um git log -p (que por padrão só mostra patches para commits que não são de merge).
$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date: Fri Sep 19 18:14:49 2014 +0200
Merge branch 'mundo'
Conflicts:
hello.rb
diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
#! /usr/bin/env ruby
def hello
- puts 'hola world'
- puts 'hello mundo'
++ puts 'hola mundo'
end
hello()
Desfazendo Merges
Agora que você sabe como criar um commit de merge, provavelmente você fará alguns por engano. Uma das grandes coisas sobre trabalhar com o Git é que não há problema em cometer erros, porque é possível (e em muitos casos fácil) corrigi-los.
Commits de merge não são diferentes.
Digamos que você começou a trabalhar em uma branch de tópico, acidentalmente fez o merge dela na master, e agora o seu histórico de commits está assim:
Existem duas maneiras de abordar este problema, dependendo de qual é o seu resultado desejado.
Consertar as referências
Se o commit de merge indesejado existe apenas no seu repositório local, a solução mais fácil e melhor é mover as branches para que elas apontem para onde você quer que elas apontem.
Na maioria dos casos, se você seguir o git merge errôneo com git reset --hard HEAD~, isso resetará os ponteiros da branch para que eles fiquem assim:
git reset --hard HEAD~
Nós cobrimos o reset anteriormente em Reset Desmistificado, então não deve ser muito difícil descobrir o que está acontecendo aqui.
Aqui está uma rápida recapitulação: reset --hard geralmente passa por três passos:
-
Move a branch para a qual o HEAD aponta. Neste caso, queremos mover a
masterpara onde ela estava antes do commit de merge (C6). -
Faz o index ficar igual ao HEAD.
-
Faz o diretório de trabalho ficar igual ao index.
A desvantagem dessa abordagem é que ela está reescrevendo o histórico, o que pode ser problemático com um repositório compartilhado.
Dê uma olhada em Os Perigos do Rebase (Perils of Rebasing) para mais informações sobre o que pode acontecer; a versão curta é que se outras pessoas têm os commits que você está reescrevendo, você provavelmente deve evitar o reset.
Esta abordagem também não funcionará se quaisquer outros commits foram criados desde o merge; mover as referências efetivamente perderia essas alterações.
Reverter o commit
Se mover os ponteiros das branches não vai funcionar para você, o Git dá a opção de fazer um novo commit que desfaz todas as alterações de um existente. O Git chama esta operação de “revert” (reverter), e neste cenário em particular, você a invocaria assim:
$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"
A flag -m 1 indica qual pai é a linha principal (“mainline”) e deve ser mantido.
Quando você invoca um merge no HEAD (git merge topic), o novo commit tem dois pais: o primeiro é o HEAD (C6), e o segundo é a ponta (tip) da branch sendo mesclada (C4).
Neste caso, queremos desfazer todas as alterações introduzidas ao mesclar o pai #2 (C4), enquanto mantemos todo o conteúdo do pai #1 (C6).
O histórico com o commit revertido fica assim:
git revert -m 1
O novo commit ^M tem exatamente o mesmo conteúdo que C6, então começando daqui é como se o merge nunca tivesse acontecido, exceto que os commits agora não mesclados ainda estão no histórico do HEAD.
O Git ficará confuso se você tentar fazer o merge de topic em master novamente:
$ git merge topic
Already up-to-date.
Não há nada em topic que já não seja alcançável a partir de master.
O que é pior, se você adicionar trabalho ao topic e fizer merge novamente, o Git apenas trará as alterações desde o merge revertido:
A melhor maneira de contornar isso é desfazer o reverso (un-revert) do merge original, já que agora você quer trazer as alterações que foram revertidas, depois criar um novo commit de merge:
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
Neste exemplo, M e ^M se cancelam.
^^M efetivamente faz o merge das alterações de C3 e C4, e C8 faz o merge das alterações de C7, então agora topic está totalmente mesclado.
Outros Tipos de Merges
Até agora cobrimos o merge normal de duas branches, normalmente lidado com o que é chamado de estratégia “recursive” (recursiva) de merge. No entanto, existem outras maneiras de fazer merge de branches. Vamos cobrir algumas delas rapidamente.
Preferência por Our ou Theirs
Antes de tudo, há outra coisa útil que podemos fazer com o modo “recursive” normal de merge.
Nós já vimos as opções ignore-all-space e ignore-space-change que são passadas com um -X, mas também podemos dizer ao Git para favorecer um lado ou outro quando ele vê um conflito.
Por padrão, quando o Git vê um conflito entre duas branches sofrendo merge, ele adicionará marcadores de conflito de merge ao seu código e marcará o arquivo como em conflito e deixará que você o resolva.
Se você preferir que o Git simplesmente escolha um lado específico e ignore o outro lado em vez de permitir que você resolva manualmente o conflito, você pode passar ao comando merge um -Xours ou -Xtheirs.
Se o Git vir isso, ele não adicionará marcadores de conflito. Quaisquer diferenças que sejam mescláveis (mergeable), ele fará o merge. Quaisquer diferenças que conflitem, ele simplesmente escolherá o lado que você especificar por inteiro, incluindo arquivos binários.
Se voltarmos ao exemplo do “hello world” que estávamos usando antes, podemos ver que fazer o merge da nossa branch causa conflitos.
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
No entanto, se rodarmos isso com -Xours ou -Xtheirs, ele não causa.
$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
hello.rb | 2 +-
test.sh | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 test.sh
Nesse caso, em vez de obter marcadores de conflito no arquivo com “hello mundo” de um lado e “hola world” do outro, ele simplesmente escolherá “hola world”. No entanto, todas as outras alterações não conflitantes naquela branch são mescladas com sucesso.
Esta opção também pode ser passada para o comando git merge-file que vimos anteriormente rodando algo como git merge-file --ours para merges de arquivos individuais.
Se você quer fazer algo parecido mas nem que o Git tente fazer o merge de alterações do outro lado, há uma opção mais draconiana, que é a estratégia de merge “ours”. Isso é diferente da opção recursiva de merge “ours”.
Isso basicamente fará um merge falso. Ele gravará um novo commit de merge com ambas as branches como pais, mas nem olhará para a branch que você está fazendo merge. Ele simplesmente gravará como o resultado do merge o código exato da sua branch atual.
$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$
Você pode ver que não há diferença entre a branch em que estávamos e o resultado do merge.
Isso muitas vezes pode ser útil para basicamente enganar o Git a pensar que uma branch já foi mesclada ao fazer um merge mais tarde.
Por exemplo, digamos que você ramificou de uma branch release e fez algum trabalho nela que você vai querer fazer o merge de volta na sua branch master em algum momento.
Enquanto isso, algum bugfix na master precisa ser feito backport para a sua branch release.
Você pode fazer o merge da branch do bugfix na branch release e também merge -s ours na mesma branch para a sua branch master (embora a correção já esteja lá) de modo que quando mais tarde você fizer o merge da branch release de novo, não haja conflitos do bugfix.
Merging de Sub-árvore (Subtree Merging)
A ideia do merge de sub-árvore (subtree merge) é que você tem dois projetos, e um dos projetos mapeia para um subdiretório do outro. Quando você especifica um merge de sub-árvore, o Git muitas vezes é esperto o suficiente para descobrir que um é uma sub-árvore do outro e faz o merge apropriadamente.
Nós veremos um exemplo de adição de um projeto separado em um projeto existente e então o merge do código do segundo em um subdiretório do primeiro.
Primeiro, adicionaremos a aplicação Rack ao nosso projeto. Adicionaremos o projeto Rack como uma referência remota em nosso próprio projeto e então faremos o checkout dele em sua própria branch:
$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
* [new branch] build -> rack_remote/build
* [new branch] master -> rack_remote/master
* [new branch] rack-0.4 -> rack_remote/rack-0.4
* [new branch] rack-0.9 -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"
Agora nós temos a raiz do projeto Rack na nossa branch rack_branch e nosso próprio projeto na branch master.
Se você fizer checkout de uma e depois da outra, você pode ver que eles têm raízes de projeto diferentes:
$ ls
AUTHORS KNOWN-ISSUES Rakefile contrib lib
COPYING README bin example test
$ git checkout master
Switched to branch "master"
$ ls
README
Este é um conceito um tanto estranho. Nem todas as branches no seu repositório têm que ser branches do mesmo projeto na verdade. Isso não é comum, porque raramente é útil, mas é bem fácil ter branches que contenham históricos completamente diferentes.
Neste caso, nós queremos puxar (pull) o projeto Rack para o nosso projeto master como um subdiretório.
Nós podemos fazer isso no Git com git read-tree.
Você vai aprender mais sobre o read-tree e seus amigos em Git Internals (Por Dentro do Git), mas por agora saiba que ele lê a árvore (tree) raiz de uma branch para a sua área de stage atual e diretório de trabalho.
Nós acabamos de mudar de volta para a nossa branch master, e nós puxamos (pull) a branch rack_branch para o subdiretório rack da nossa branch master do nosso projeto principal:
$ git read-tree --prefix=rack/ -u rack_branch
Quando nós comitamos, parece que nós temos todos os arquivos Rack sob aquele subdiretório — como se nós os tivéssemos copiado de um tarball. O que fica interessante é que nós podemos fazer merge de alterações de uma branch para a outra de forma bem fácil. Então, se o projeto Rack atualiza, nós podemos puxar alterações do upstream mudando para aquela branch e dando pull:
$ git checkout rack_branch
$ git pull
Então, podemos fazer o merge dessas alterações de volta para a nossa branch master.
Para puxar as alterações e pré-popular a mensagem de commit, use a opção --squash, bem como a opção -Xsubtree da estratégia de merge recursiva.
A estratégia recursiva é o padrão aqui, mas a incluímos para maior clareza.
$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
Todas as alterações do projeto Rack são mescladas (merged in) e prontas para serem comitadas localmente.
Você também pode fazer o oposto — fazer alterações no subdiretório rack da sua branch master e então fazer merge delas na sua branch rack_branch mais tarde para enviá-las aos mantenedores ou fazer o push delas para o upstream.
Isso nos dá uma forma de ter um fluxo de trabalho (workflow) um tanto similar ao fluxo de submodules sem usar submodules (que cobriremos em Submódulos). Nós podemos manter branches com outros projetos relacionados no nosso repositório e fazer o merge de sub-árvore deles no nosso projeto ocasionalmente. É bacana de algumas formas, por exemplo todo o código é comitado em um único lugar. No entanto, tem outras desvantagens em que é um pouco mais complexo e mais fácil cometer erros ao reintegrar as alterações ou acidentalmente dar push em uma branch em um repositório não relacionado.
Outra coisa um pouco estranha é que para obter um diff entre o que você tem no seu subdiretório rack e o código na sua branch rack_branch — para ver se você precisa fazer o merge deles — você não pode usar o comando diff normal.
Em vez disso, você deve rodar git diff-tree com a branch com a qual você deseja comparar:
$ git diff-tree -p rack_branch
Ou, para comparar o que está no seu subdiretório rack com o que a branch master no servidor era da última vez que você deu fetch, você pode rodar:
$ git diff-tree -p rack_remote/master