Desenvolvimento

9 ago, 2012

Dica Git da semana: Revendo Merging

Publicidade

A Dica Git da semana é sobre merging.

Nós já demos uma olhada em merging em abril, mas mencionamos o índice na semana passada, e os números do índice, que desempenha um papel nos merges. Esta semana, veremos o que isso significa com diferentes tipos de merges. Se você não está familiarizado com merges, dê uma olhada no artigo anterior primeiro.

Merging permite a você reunir dois (ou mais) commits em um único merge commit única cometer, enquanto, ao mesmo tempo, reúne todos os arquivos diferentes combinados em dois. Se houver conflitos de merge, eles precisam ser resolvidos individualmente. Merges nos quais um dos commits é um ancestral do commit atual são referidos como merges fast-forward, e não resultam em um novo merge de commits. Quando você faz um pull a partir de uma fonte remota, você está na verdade fazendo um fetch e um merge em uma única etapa – embora, normalmente, você possa simplesmente avançar rapidamente quando você faz.

Para os propósitos deste artigo, eu vou criar três branches chamados de ‘Alex’, ‘Bob’ e “master”, em homenagem a Alice e Bob. Cada um terá um arquivo chamado Hello, que conterá o texto “Olá, meu nome é Alex”, bem como um AlexsFile, BobsFile etc.

(master) $ git ls-tree Alex
100644 blob ada4c4c4f33cd190fe40769d5ca9826adb9fb7ce AlexsFile
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259 Hello
(master) $ git ls-tree Bob
100644 blob eea826732acee08a8cf83445e3b98cf58f11ce5c BobsFile
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259 Hello
(master) $ git ls-tree master
100644 blob ca4eef2f4e3f1fe92028176cb547b590a08c2259 Hello

Estratégias

Quando você cria um merge, você tem a opção de dizer como ele será processado. A estratégia usual é recursiva. Isso significa que o Git vai andar por cada diretório (tree) e descobrir quais arquivos têm diferenças em relação à revisão de base, e, em seguida usar aquele com alterações. (Se ambos tiverem alterações, o conteúdo do arquivo novo sofrerá um merge textualmente e, se há algum problema com isso, acontece um conflito). É por isso que você verá a mensagem “Merge made by recursive” depois de fazer a operação:

(master) $ git checkout Alex
Switched to branch 'Alex'
(Alex) $ git merge Bob
Merge made by recursive.
BobsFile | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 BobsFile
(Alex) $ git log --oneline --graph
* d612aab Merge branch 'Bob' into Alex
|\
| * 8afb2d3 Bob's File
* | 4abf59e Alex's File
|/
* 5cba624 Hello
(Alex) $ git show d612aa
commit d612aab9858289ed027230d3b9a7b2a7a5e75945
Merge: 4abf59e 8afb2d3

Merge branch 'Bob' into Alex

O commit d612aa… é um merge node e ele junta dois branches separados juntos. Uma vez que nenhum é um antepassado do outro, eles não podem ser fast-foward rapidamente, e, como tal, o merge node é criado. Podemos determinar os pais dos commits com HEAD^1 e HEAD^2:

(Alex) $ git rev-parse HEAD^1
4abf59ef73c186e93db25e8b7bc4423fbd11bbd0
(Alex) $ git rev-parse HEAD^2
8afb2d368ce26ca71cec539c31400c7001a18efc

É ainda mais fácil quando não há alterações para se fazer merge:

(Alex) $ git checkout master
Switched to branch 'master'
(master) $ git merge Bob
Updating 5cba624..8afb2d3
Fast-forward
BobsFile | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 BobsFile

O fast-forward aqui indica que o master está atrás de Bob, e assim podemos simplesmente mover o ponteiro para frente – como resultado, não precisamos criar um merge node.

Portanto, duas estratégias que o Git usa estão fazendo um fast-forward ou merge recursivo. Isso abrange 99% dos merges que você precisa fazer, mas vale a pena notar que o Git possui alguns truques na manga que podem ajudar em certas situações.

Merge Octopus

Os exemplos acima mencionados têm um ou dois pais. No entanto, um Git merge node é capaz de representar mais de dois heads em um merge, e ele usa uma estratégia chamada de octopus. Isso é selecionado por padrão quando você fizer o merge de mais de dois branches:

(master) $ git reset --hard 5cba624b94a7a622183c960697867c8bba73aa91
HEAD is now at 5cba624 Hello
(master) $ date > NewFile
(master) $ git add NewFile
(master) $ git commit -m "New File"
[master 598ad85] New File
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 NewFile
(master) $ git merge Alex Bob
Trying simple merge with Alex
Trying simple merge with Bob
Merge made by octopus.
AlexsFile | 1 +
BobsFile | 1 +
2 files changed, 2 insertions(+), 0 deletions(-)
create mode 100644 AlexsFile
create mode 100644 BobsFile
(master) $ git log --oneline --graph
*-. 5a2aa0d Merge branches 'Alex' and 'Bob'
|\ \
| | * 8afb2d3 Bob's File
| * | 4abf59e Alex's File
| |/
* | 598ad85 New File
|/
* 5cba624 Hello
(master) $ git show
commit 5a2aa0da3d3b3365703d710dad8aeebc0770b8ef
Merge: 598ad85 4abf59e 8afb2d3

Merge branches 'Alex' and 'Bob'
(master) $ git rev-parse HEAD^1 HEAD^2 HEAD^3
598ad850114e1f7445ee8b02e93ee23060439560
4abf59ef73c186e93db25e8b7bc4423fbd11bbd0
8afb2d368ce26ca71cec539c31400c7001a18efc

Nesse caso, temos três commits fazendo merge em conjunto para o merge node; o master (que divergiu), e os branches Alex e Bob de antes. Já que o nosso merge node possui agora três pais, podemos usar git rev-parse para converter o HEAD^1/2/3 no primeiro, segundo e terceiro heads.

E se os arquivos estão em conflito?

(master) $ git checkout Alex
Switched to branch 'Alex'
(Alex) $ echo Hello, my name is Alex > Hello
(Alex) $ git commit -a -m "My name is Alex"
[Alex c2cb955] My name is Alex
1 files changed, 1 insertions(+), 1 deletions(-)
(Alex) $ git checkout Bob
Switched to branch 'Bob'
(Bob) $ echo Hello, my name is Bob > Hello
(Bob) $ git commit -a -m "My name is Bob"
[Bob 7cb6225] My name is Bob
1 files changed, 1 insertions(+), 1 deletions(-)
(Bob) $ git checkout master
(master) $ git merge Alex Bob
Trying simple merge with Alex
Trying simple merge with Bob
Simple merge did not work, trying automatic merge.
Auto-merging Hello
ERROR: content conflict in Hello
fatal: merge program failed
Automatic merge failed; fix conflicts and then commit the result.
(master|MERGING) $ git ls-files --stage
100644 ada4c4c4f33cd190fe40769d5ca9826adb9fb7ce 0 AlexsFile
100644 eea826732acee08a8cf83445e3b98cf58f11ce5c 0 BobsFile
100644 ca4eef2f4e3f1fe92028176cb547b590a08c2259 1 Hello
100644 a5a820416bae2c7b77340e5b2120aab9595d2bfc 2 Hello
100644 98b16693fe64acb9d002af1fe5f5162d58bd40b4 3 Hello
100644 09d774502f97ba9a46f25f8f11601b653c376828 0 NewFile
(master|MERGING) $

Podemos lançar uma ferramenta de diff de três vias com git mergetool:

(master|MERGING) $ git mergetool
(master|MERGING) $ git mergetool
merge tool candidates: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse ecmerge p4merge araxis bc3 emerge vimdiff
Merging:
Hello

Normal merge conflict for 'Hello':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):

(master|MERGING) $ git commit -a -m "Merged"
[master 999a938] Merged

Note que o merge Octopus não lida com conflitos de mais de dois arquivos. Se fizer isso, você acaba com uma mensagem de erro diferente:

(master) $ echo Hello World > Hello 
(master) $ git commit -a -m "Hello World"
[master fe79e59] Hello World
1 files changed, 1 insertions(+), 1 deletions(-)
(master) $ git merge Alex Bob
Trying simple merge with Alex
Simple merge did not work, trying automatic merge.
Auto-merging Hello
ERROR: content conflict in Hello
fatal: merge program failed
Automated merge did not work.
Should not be doing an Octopus.
Merge with strategy octopus failed.

Ours

Finalmente, a última estratégia que vale a pena conhecer é a ours. Ela pega qualquer número de heads, e cria um merge node, mas sem fazer quaisquer alterações. Em outras palavras, um git diff HEAD^1 sempre vai voltar vazio para uma estratégia ours:

(master) $ git diff HEAD^1
(master) $ git log --oneline --graph --decorate
*-. c5f84cc (HEAD, master) Merge branches 'Alex' and 'Bob'
|\ \
| | * 7cb6225 (Bob) My name is Bob
| | * 8afb2d3 Bob's File
| * | c2cb955 (Alex) My name is Alex
| * | 4abf59e Alex's File
| |/
* | 598ad85 New File
|/
* 5cba624 Hello
(master) $ git rev-parse HEAD^{tree}
78784bb4dea678c157d8711bc56c5478a74588c3
(master) $ git rev-parse HEAD^1^{tree}
78784bb4dea678c157d8711bc56c5478a74588c3

Podemos verificar que estes são idênticos, uma vez que a tree apontada pelo HEAD é a mesma tree apontada pelo HEAD^1 (isto é, o pai). O sufixo^{tree} é usado para mostrar a tree associada com o commit.

O argumento –decorate para git log adiciona os nomes do branch para a saída, o que pode ser útil para indicar de onde merges vieram. O argumento –graph é essencialmente usado com o argumento –oneline; embora você possa executar um git –graph, as mensagens completas de commit tendem a ocultar a estrutura do gráfico.

A estratégia ours só é realmente útil se você quiser codificar um conjunto de commits anterior, mas não afetar o mestre atual (por exemplo, porque você escolheu alguns dos conteúdos e não quer as outras partes, mas preservá-los no próprio histórico de alguma forma).

Mensagem de Merge e e Fast Fowards

A mensagem de merge será criada automaticamente, com base nos nomes dos branches que você está fazendo merge. No entanto, é possível passar uma opção -m, como com git commit, para fornecer uma mensagem adicional. Isso pode ser útil se a mensagem de merge necessita de informação adicional codificada (tais como quais bugs foram consertados).

Também é possível forçar um merge, mesmo se não for necessário. Se você tiver tópicos baseados em branches, eles podem ser uteis para indicar que o trabalho foi realizado em um branch separado antes de ser feito o merge com o master. Executar git merge –no-ff criará um merge node, independentemente se o branch pode ser fast-foward rapidamente ou não. Desde que o merge commit tem o nome do branch a partir do qual você está fazendo o merge como parte do commit, você pode acabar com nomes descritivos para mostrar a característica sendo concluída:

(master) $ git checkout -b "bug12345"
Switched to a new branch 'bug12345'
(bug12345) $ echo BugFix >> Hello
(bug12345) $ git commit -a -m "Fixing bug 12345"
[bug12345 e2bd64e] Fixing bug 12345
1 files changed, 2 insertions(+), 0 deletions(-)
(bug12345) $ git checkout master
Switched to branch 'master'
(master) $ git merge bug12345 # without --no-ff
Updating 999a938..e2bd64e
Fast-forward
Hello | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
(master) $ git reset --hard HEAD^
HEAD is now at 999a938 Merged
(master) $ git merge --no-ff bug12345 # with --no-ff
Merge made by recursive.
Hello | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
(master) $ git log --oneline --graph --decorate
* 4a905c8 (HEAD, master) Merge branch 'bug12345'
|\
| * e2bd64e (bug12345) Fixing bug 12345
|/
*-. 999a938 Merged

Ser capaz de fazer um merge com um –no-ff pode ser útil ao usar alguns tipos de fluxos de trabalho de commit, onde muitos branches são usados para desenvolver características individuais e, em seguida, levados para um branch master em seguida. É importante notar também o argumento –merges permite filtrar apenas os merges em um repositório:

(master) apple[merge] $ git log --oneline --merges --decorate
4a905c8 (HEAD, master) Merge branch 'bug12345'
999a938 Merged

Da próxima vez, veremos o que podemos fazer com os diferentes fluxos de trabalho Git usando merge –no-ff.

?

Texto original disponível em http://alblue.bandlem.com/2011/10/git-tip-of-week-merging-revisited.html