Custom merge drivers: resolvendo conflitos em arquivos binários ou especializados
1. Por que os merge drivers padrão falham em cenários especiais
O Git utiliza uma estratégia textual para resolução de conflitos: ele compara arquivos linha por linha, aplicando algoritmos como o "three-way merge" baseado no ancestral comum. Essa abordagem funciona bem para código-fonte, mas falha em cenários onde o conteúdo não é texto puro ou onde a estrutura do arquivo exige tratamento semântico.
Problemas típicos incluem:
- Arquivos binários (imagens, PDFs, DLLs): o Git não consegue interpretar o conteúdo, resultando em conflitos impossíveis de resolver automaticamente. Cada merge gera um conflito que exige intervenção manual.
- Formatos especializados (JSON, XML, arquivos de lock): o merge textual pode gerar falsos conflitos. Por exemplo, ao modificar chaves diferentes em um
package-lock.json, o Git pode sinalizar conflito mesmo quando as alterações são compatíveis. - Consequências práticas: merges impossíveis, perda de dados (quando uma versão binária é sobrescrita incorretamente) e retrabalho manual constante.
2. Anatomia de um custom merge driver
Um custom merge driver é definido em duas etapas:
- No
.gitattributes: associa um padrão de arquivo a um nome de driver. - No
.gitconfig: define o comportamento do driver.
A estrutura do driver no .gitconfig inclui:
[merge "meu-driver"]
name = "Driver para arquivos binários"
driver = script-merge.sh %O %A %B %P
recursive = binary
Os parâmetros passados ao driver são:
%O: caminho temporário para a versão ancestral (base comum)%A: caminho temporário para a versão "nossa" (branch atual)%B: caminho temporário para a versão "deles" (branch sendo mesclada)%P: caminho real do arquivo no repositório
O driver deve modificar o arquivo em %A com o resultado do merge. Se retornar código de saída diferente de zero, o Git considera que houve conflito.
3. Implementando um merge driver para arquivos binários
Para arquivos binários, uma abordagem comum é escolher automaticamente entre as versões. Exemplo simples que sempre aceita a versão "nossa":
#!/bin/bash
# driver-sempre-nosso.sh
# Sempre mantém a versão do branch atual (%A)
cp "$2" "$1" # $2 é %A (já está em $1), mas garantimos
exit 0
Para um merge condicional baseado em data de modificação:
#!/bin/bash
# driver-mais-recente.sh
# Escolhe a versão mais recente baseada em timestamp
ANCESTRAL="$1"
NOSSO="$2"
DELES="$3"
# Compara timestamps de modificação
if [ "$NOSSO" -nt "$DELES" ]; then
# Nossa versão é mais recente, mantém
exit 0
else
# Versão deles é mais recente, copia para nossa
cp "$DELES" "$NOSSO"
exit 0
fi
Para tratamento de erros com fallback:
#!/bin/bash
# driver-com-fallback.sh
if [ ! -f "$1" ] || [ ! -f "$2" ] || [ ! -f "$3" ]; then
echo "ERRO: Arquivos temporários ausentes" >&2
exit 1 # Delega ao merge manual
fi
# Tenta merge binário simples
if cmp -s "$1" "$2"; then
# Nossa versão igual ao ancestral, usa deles
cp "$3" "$2"
exit 0
elif cmp -s "$1" "$3"; then
# Versão deles igual ao ancestral, mantém nossa
exit 0
else
# Ambos modificados, mantém nossa versão com aviso
echo "AVISO: Ambas versões modificadas, mantendo nossa" >&2
exit 0
fi
4. Merge driver para formatos especializados (JSON, XML, lock files)
Para package-lock.json, uma estratégia de união pode mesclar chaves únicas. Exemplo com Python:
#!/usr/bin/env python3
# merge-lock.py - Merge driver para package-lock.json
import json
import sys
def merge_locks(ancestral, nosso, deles):
with open(ancestral) as f:
base = json.load(f)
with open(nosso) as f:
a = json.load(f)
with open(deles) as f:
b = json.load(f)
# Merge profundo: combina dicionários recursivamente
resultado = {}
for chave in set(list(a.keys()) + list(b.keys())):
if chave in a and chave in b:
if isinstance(a[chave], dict) and isinstance(b[chave], dict):
resultado[chave] = {**a[chave], **b[chave]}
else:
resultado[chave] = b[chave] if chave not in base else a[chave]
elif chave in a:
resultado[chave] = a[chave]
else:
resultado[chave] = b[chave]
with open(nosso, 'w') as f:
json.dump(resultado, f, indent=2)
return 0
if __name__ == "__main__":
sys.exit(merge_locks(sys.argv[1], sys.argv[2], sys.argv[3]))
Para merge inteligente de JSON com arrays sem duplicatas:
#!/usr/bin/env node
// merge-json.js
const fs = require('fs');
function mergeJSON(ancestral, nosso, deles) {
const base = JSON.parse(fs.readFileSync(ancestral, 'utf8'));
const a = JSON.parse(fs.readFileSync(nosso, 'utf8'));
const b = JSON.parse(fs.readFileSync(deles, 'utf8'));
function deepMerge(obj1, obj2) {
const result = { ...obj1 };
for (const key in obj2) {
if (Array.isArray(obj2[key])) {
result[key] = [...new Set([...(obj1[key] || []), ...obj2[key]])];
} else if (typeof obj2[key] === 'object' && obj2[key] !== null) {
result[key] = deepMerge(obj1[key] || {}, obj2[key]);
} else {
result[key] = obj2[key];
}
}
return result;
}
const merged = deepMerge(a, b);
fs.writeFileSync(nosso, JSON.stringify(merged, null, 2));
return 0;
}
const [,, ancestral, nosso, deles] = process.argv;
process.exit(mergeJSON(ancestral, nosso, deles));
5. Configuração e ativação do driver no repositório
Registro global do driver (no ~/.gitconfig):
git config --global merge.meu-driver.driver "/caminho/para/script.sh %O %A %B %P"
git config --global merge.meu-driver.name "Meu Driver Customizado"
Ou local (no repositório):
git config merge.meu-driver.driver "/caminho/para/script.sh %O %A %B %P"
Arquivo .gitattributes para associar padrões:
*.bin merge=meu-driver
package-lock.json merge=lock-merge
*.json merge=json-merge
Testando o driver manualmente com git merge-file:
git merge-file -p nosso.txt base.txt deles.txt --merge-driver=meu-driver
6. Depuração e tratamento de conflitos complexos
Para capturar logs do driver, redirecione a saída de erro:
[merge "debug-driver"]
driver = /script.sh %O %A %B %P 2>> /tmp/merge-debug.log
Use GIT_MERGE_VERBOSITY para diagnóstico:
GIT_MERGE_VERBOSITY=5 git merge branch-alvo
Variáveis de ambiente úteis:
GIT_MERGE_VERBOSITY: controla nível de detalhes (0-5)GIT_TRACE: ativa rastreamento geralGIT_TRACE_PERFORMANCE: medições de performance
Estratégias de fallback: se o driver retornar código 1, o Git marca o arquivo como conflitado e permite resolução manual. Use isso quando o merge automático não for possível.
7. Boas práticas e limitações
- Versionamento do script: mantenha o script do driver dentro do repositório (ex.:
scripts/merge-drivers/) e referencie-o por caminho relativo ou absoluto. - Portabilidade: evite dependências de sistema específicas. Use shebangs universais (
#!/usr/bin/env python3) e teste em Linux, macOS e Windows (com Git Bash ou WSL). - Quando não usar: para arquivos de texto simples, o merge textual do Git é superior. Custom drivers são para casos onde a semântica do arquivo importa mais que as linhas.
Limitações importantes:
- Drivers não funcionam em merges com
--oursou--theirs(que ignoram o driver) - O driver é executado para cada arquivo, não para o merge completo
- Performance pode ser impactada se o script for complexo
Custom merge drivers são ferramentas poderosas para domar conflitos em arquivos que o Git não entende nativamente. Com eles, você automatiza decisões que antes exigiam intervenção manual, reduzindo retrabalho e prevenindo erros.
Referências
- Git Documentation: Custom Merge Drivers — Documentação oficial do Git sobre definição de drivers de merge customizados, incluindo sintaxe e parâmetros.
- Git Attributes: Merging Binary Files — Capítulo do Pro Git sobre como configurar merge para arquivos binários usando atributos.
- Atlassian: Advanced Git Merge Strategies — Tutorial da Atlassian sobre estratégias avançadas de merge, incluindo menção a drivers customizados.
- Git Merge Drivers for JSON Files — Artigo técnico no DZone mostrando implementação prática de merge drivers para arquivos JSON.
- Custom Git Merge Drivers: A Practical Guide — Guia prático com exemplos reais de implementação de drivers de merge, incluindo tratamento de erros e depuração.