Git protip : préserver certains fichiers au merge
Par Christophe Porteneuve • Publié le 28 novembre 2014
• 4 min
Les branches, c’est formidable. Ça permet d’avoir des versions complètement différentes d’un même fichier selon le contexte.
Seulement voilà, dans certains cas rares, il arrive que vous souhaitiez versionner un fichier, qui change de branche à branche, mais le préserver quand vous fusionnez une branche vers la vôtre.
Le cas classique, ce sont les fichiers non-sensibles mais dépendants de l’environnement d’exécution (développement, staging, production), parce qu’ils indiquent des URLs, domaines ou numéros de port qui varient suivant le contexte :
- configuration d’envoi e-mail qui utiliserait une gestion locale hors production (par exemple l’excellent mailcatcher)
- configuration de logs qui serait en fichiers locaux en dev, mais consolidée vers un service central autrement
- etc.
Ce genre de fichiers gagne à être versionné, naturellement. Mais lorsqu’on fusionne une branche vers la nôtre (par exemple, on récupère master
pour faire un [merge de contrôle](/fr/commandes/git-rerere/#le-souci classique : les merges de contrôle)), comment garder notre version à nous juste pour ceux-là, sans avoir à faire à chaque fois des manips spéciales ?
C’est tout simple, en fait. Voyons ça.
Pour conserver notre propre version d’un fichier lors d’une fusion (forcément entrante : git merge
fusionne vers la branche courante), on fait appel à une fonction peu connue de Git : les attributs Git.
Les attributs Git
Ce mécanisme permet d’associer à des fichiers ou dossiers (on utilise en fait des schémas de glob, tels que secure/*
ou *.svg
) des propriétés techniques particulières.
Ces informations sont généralement versionnées, comme celles du .gitignore
, mais sont stockées dans .gitattributes
(et tout comme .gitignore
a un équivalent strictement local dans .git/info/exclude
, on a .git/info/attributes
).
Le format est simple : chaque ligne qui n’est ni vide ni un commentaire (démarre par #
) est au format schema-de-glob info-attribut
(le nombre d’espaces n’ayant aucune importance).
Un attribut peut être activé (présent sans valeur spécifique), désactivé (présent mais négatif), valué (défini avec une valeur précise) ou absent. Dans ce qui nous intéresse ici, on utilisera un attribut valué.
Outre qu’on peut créer nos propres attributs à des fins de scripting personnalisé, ou regrouper des combos classiques en meta-attributs, Git fournit de nombreux attributs prédéfinis qui font des trucs de dingue…
Pilotes de fusion[^1]
Celui qui nous intéresse ici est l’attribut merge
, qui permet de définir un pilote de fusion (merge driver), c’est-à-dire une commande qui va décider comment procéder à la fusion d’un fichier précis.
Cet attribut a généralement une valeur automatique en fonction de la nature détectée du fichier : on utilisera normalement le pilote text
pour des fichiers textuels, binary
pour les autres.
Il est toutefois possible de construire son propre pilote (on le définit dans la configuration classique de Git, par exemple notre .gitconfig
) pour l’associer par attributs à certains fichiers, auquel Git passera jusqu’à 3 arguments, dans l’ordre qu’on veut : la version d’origine du fichier (ancêtre commun), la nôtre, et celle d’en face (la branche qu’on fusionne).
La clé du truc, c’est qu’un tel pilote est censé déposer son résultat dans notre propre fichier à l’issue d’un traitement réussi (code de retour zéro). Du coup, un pilote qui ne touche pas aux fichiers et renvoie zéro laisse notre fichier local tranquille lors de la fusion.
Eureka !
On n’a même pas besoin d’écrire un script vide ou qui ferait juste exit 0
, car tout environnement Bash/zsh/etc. a déjà une telle commande, qui est souvent un builtin du shell : true
. Utilisons-la donc.
Mise en place
Commençons par définir un pilote de fusion qui privilégierait toujours notre propre version d’un fichier, en reposant sur la commande existante true
. Appelons-le ours
, pour être dans l’esprit des stratégies de fusion similaires :
git config --global merge.ours.driver true
Si vous avez déjà un dépôt Git pour tester, pas de souci, pourrissons-le (?!). Sinon, on s’en fait un p’tit vite fait :
mkdir tmp
cd tmp
git init
git commit --allow-empty -m "chore: Initial commit"
À présent, ajoutons un fichier .gitattributes
à la racine de notre dépôt, qui dirait que notre fichiers email.json
exploite ce pilote plutôt que celui standard :
echo 'email.json merge=ours' >> .gitattributes
git add .gitattributes
git commit -m 'chore: Préservation du email.json courant en cas de fusion'
Et voilà, on est prêts !
Préparation d’une démo
Mettons-nous en situation de test, avec d’abord un fichier qui va bouger des deux côtés :
echo 'Youpi la frite' > demo-shared
git add demo-shared
git commit -m "chore(demo): fichier qui fusionnera normalement"
Puis faisons une branche demo-prod
et mettons-y du taf mixte :
git checkout -b demo-prod
echo '{"server":"smtp.mandrillapp.com","port":587,"starttls":"auto"}' > email.json
git add email.json
git commit -m "chore(email): email.json de production"
echo -e "Tu sais quoi ?\nYoupi la frite" > demo-shared
git commit -am "fix(demo): En-tête fichier pour fusion normale"
Enfin, revenons sur notre branche précédente, et ajoutons du taf mixte :
git checkout -
echo '{"server":"localhost","port":1025,"starttls":false}' > email.json
git add email.json
git commit -m "chore(email): email.json de dev/stage"
echo "Eh oui ma pauv' Lucette" >> demo-shared
git commit -am "fix(demo): Footer fichier pour fusion normale"
C’est parti !
OK, nous sommes prêts à tester. Si on tente de fusionner notre branche actuelle dans demo-prod
, on devrait fusionner normalement demo-shared
(qui ne devrait pas avoir de conflit), mais conserver la version de production de email.json
:
$ git checkout demo-prod
$ git merge -
Auto-merging demo-shared
Merge made by the 'recursive' strategy.
demo-shared | 1 +
1 file changed, 1 insertion(+)
$ cat email.json
{"server":"smtp.mandrillapp.com","port":587,"starttls":"auto"}
Victoire ! \o/
Merci à Scott Chacon qui, dans le chapitre dédié aux attributs de son livre Pro Git, décrit cette astuce ; et à Julien Hedoux, qui en me posant la question m’a amené à fouiller cet aspect.
Envie d’en savoir plus ?
Déjà, vous pouvez voir par le menu tout ce qui est apparu d’intéressant dans Git depuis la 1.7, grâce à notre article dédié. Vous pouvez aussi aller fouiller dans le détail la bonne utilisation de merge vs. rebase, si ce n’est déjà fait.
[^1]: Ça pète trop la classe, ce titre. On dirait un pilote d’essai sur un avion supersonique à moteur nucléaire, ou je ne sais trop quoi…