web-dev-qa-db-fra.com

Est-il possible de déplacer / renommer des fichiers dans Git et de conserver leur historique?

Je voudrais renommer/déplacer un sous-arbre de projet dans Git en le déplaçant de

/project/xyz

à

/components/xyz

Si j'utilise un git mv project components simple, alors tout l'historique de validation pour le xyz project est perdu. Y a-t-il un moyen de faire en sorte que l'histoire soit conservée?

615
sgargan

Git détecte les renames plutôt que de continuer l'opération avec le commit, donc peu importe que vous utilisiez git mv ou mv.

La commande log prend un argument --follow qui continue l’historique avant une opération de changement de nom, c’est-à-dire qu’elle recherche un contenu similaire à l’aide de la méthode heuristique:

http://git-scm.com/docs/git-log

Pour rechercher l'historique complet, utilisez la commande suivante:

git log --follow ./path/to/file
611
Troels Thomsen

Il est possible de renommer un fichier et de conserver l'historique, même si le fichier est renommé tout au long de l'historique du référentiel. Ceci est probablement réservé aux amateurs obsolètes de git-log, et a de graves implications, notamment:

  • Vous pourriez réécrire un historique partagé, qui est le plus important à ne pas faire lors de l'utilisation de Git. Si quelqu'un d'autre a cloné le référentiel, vous le supprimerez. Ils devront se re-cloner pour éviter les maux de tête. Cela peut être acceptable si le changement de nom est suffisamment important, mais vous devrez réfléchir à cela attentivement - vous pourriez finir par déranger toute une communauté opensource!
  • Si vous avez référencé le fichier en utilisant son ancien nom plus tôt dans l'historique du référentiel, vous supprimez les versions précédentes. Pour remédier à cela, vous devrez faire un peu plus de sauts en cerceau. Ce n'est pas impossible, juste fastidieux et peut-être pas la peine.

Maintenant, puisque vous êtes toujours avec moi, vous êtes probablement un développeur solo renommant un fichier complètement isolé. Déplaçons un fichier en utilisant filter-tree!

Supposons que vous allez déplacer un fichier old dans un dossier dir et lui donner le nom new.

Cela pourrait être fait avec git mv old dir/new && git add -u dir/new, mais cela casse l’histoire.

Au lieu:

git filter-branch --tree-filter 'if [ -f old ]; then mkdir dir && mv old dir/new; fi' HEAD

will refaire chaque validation de la branche, en exécutant la commande dans les ticks pour chaque itération. Beaucoup de choses peuvent mal se passer lorsque vous faites cela. Je teste normalement pour voir si le fichier est présent (sinon il n’est pas encore disponible pour le déplacer), puis j’effectue les étapes nécessaires pour placer l’arbre à ma guise. Ici, vous pouvez parcourir des fichiers pour modifier les références au fichier, etc. Assommez-vous! :)

Une fois terminé, le fichier est déplacé et le journal est intact. Vous vous sentez comme un pirate ninja.

Aussi; Le répertoire mkdir n’est nécessaire que si vous déplacez le fichier dans un nouveau dossier, bien sûr. Le if permettra d'éviter la création de ce dossier plus tôt dans l'historique que votre fichier existe déjà.

88
Øystein Steimler

Non.

La réponse courte est NO, il n'est pas possible de renommer un fichier dans Git et de se souvenir de l'historique. Et c'est pénible.

Selon la rumeur, _git log --follow_ --find-copies-harder fonctionnera, mais cela ne fonctionne pas pour moi, même si le contenu du fichier ne subit aucune modification, et ont été réalisés avec git mv .

(Au départ, j’ai utilisé Eclipse pour renommer et mettre à jour des paquetages en une seule opération, ce qui a peut-être confondu git. Mais c’est une chose très courante à faire. _--follow_ semble fonctionner si seulement un mv est exécuté et alors un commit et le mv n'est pas trop loin.)

Linus dit que vous êtes censé comprendre tout le contenu d'un projet logiciel de manière globale, sans avoir à suivre les fichiers individuels. Eh bien, malheureusement, mon petit cerveau ne peut pas faire ça.

Il est vraiment ennuyeux que tant de personnes aient répété stupidement la déclaration selon laquelle git suivrait automatiquement les déplacements. Ils ont perdu mon temps. Git ne fait rien de tel. De par sa conception (!), Git ne suit pas les déplacements.

Ma solution consiste à renommer les fichiers à leur emplacement d'origine. Modifiez le logiciel pour qu’il corresponde au contrôle de source. Avec git, il semble que vous ayez juste besoin de bien faire les choses la première fois.

Malheureusement, cela rompt Eclipse, qui semble utiliser _--follow_.
git log --follow N'affiche parfois pas l'historique complet des fichiers avec des historiques de renommage compliqués même si _git log_ le fait. (Je ne sais pas pourquoi.)

(Il y a des astuces trop astucieuses qui remontent à l'ancien travail, mais elles sont plutôt effrayantes. Voir GitHub-Gist: emiller/git-mv-with-history .)

78
Tuntable
git log --follow [file]

vous montrera l'histoire à travers les renommés.

41
Erik Hesselink

Je fais:

git mv {old} {new}
git add -u {new}
17
James M. Greene

Je voudrais renommer/déplacer un sous-arbre de projet dans Git en le déplaçant de

/project/xyz

à

/ composants/xyz

Si j'utilise un git mv project components simple, alors tout l'historique de validation du projet xyz est perdu.

Non (8 ans plus tard, Git 2.19, Q3 2018), car Git détectera le répertoire renommé , ce qui est maintenant mieux documenté.

Voir commit b00bf1c , commit 1634688 , commit 0661e49 , commit 4d34dff , commit 983f464 , commit c840e1a , commit 99294 (27 juin 2018), et commit d4e8062 , commit 5dacd4a (25 juin 2018 ) par Elijah Newren (newren) .
(Fusionné par Junio ​​C Hamano - gitster - dans commit 0ce5a69 , 24 juillet 2018)

Ceci est maintenant expliqué dans Documentation/technical/directory-rename-detection.txt :

Exemple:

Lorsque tous les x/a, x/b et x/c sont passés à z/a, z/b et z/c, il est probable que x/d ajouté entre-temps souhaite également passer à z/d en prenant l’indice que le répertoire entier _ x déplacé 'z'.

Mais il y a beaucoup d'autres cas, comme:

un côté de l’historique renomme x -> z et l’autre renomme un fichier en x/e, ce qui oblige la fusion à renommer de manière transitive.

Pour simplifier la détection de renommage de répertoire, ces règles sont appliquées par Git:

quelques règles de base limitent l'application de la détection de renommage de répertoire:

  1. Si un répertoire donné existe toujours des deux côtés d'une fusion, nous ne considérons pas qu'il a été renommé.
  2. Si un sous-ensemble de fichiers à renommer a un fichier ou un répertoire dans le chemin (ou le serait l'un l'autre), "désactivez" le répertoire renommé pour ces sous-chemins spécifiques et signalez le conflit à l'utilisateur .
  3. Si l’autre partie de l’histoire a renommé un répertoire en un chemin que votre partie de l’histoire a renommé, ignorez ce nom renommé de l’autre côté de l’historique pour tout renommage implicite de répertoire (mais avertissez l’utilisateur).

Vous pouvez voir un grand nombre de tests dans t/t6043-merge-rename-directories.sh , qui indiquent également que:

  • a) Si les renommés divisent un répertoire en deux ou plusieurs autres, le répertoire le plus renommé "gagne".
  • b) Évitez la détection de renommage de répertoire pour un chemin, si ce chemin est la source d'un renommage de part et d'autre d'une fusion.
  • c) Appliquez uniquement les renumérages implicites de répertoires aux répertoires si l'autre côté de l'historique est celui qui renomme.
16
VonC

Objectif

  • Utilisez git am (inspiré de Smar , emprunté à Exherbo )
  • Ajouter l'historique de validation des fichiers copiés/déplacés
  • D'un répertoire à l'autre
  • Ou d'un dépôt à un autre

Limitation

  • Les étiquettes et les branches ne sont pas conservées
  • L'historique est coupé lors du renommage du fichier de chemin (renommer le répertoire)

Sommaire

  1. Extraire l’historique au format email en utilisant
    _git log --pretty=email -p --reverse --full-index --binary_
  2. Réorganiser l'arborescence des fichiers et mettre à jour les noms de fichiers
  3. Ajouter un nouvel historique en utilisant
    _cat extracted-history | git am --committer-date-is-author-date_

1. Extraire l'historique au format email

Exemple: extrait de l'historique de _file3_, _file4_ et _file5_

_my_repo
├── dirA
│   ├── file1
│   └── file2
├── dirB            ^
│   ├── subdir      | To be moved
│   │   ├── file3   | with history
│   │   └── file4   | 
│   └── file5       v
└── dirC
    ├── file6
    └── file7
_

Définir/nettoyer la destination

_export historydir=/tmp/mail/dir       # Absolute path
rm -rf "$historydir"    # Caution when cleaning the folder
_

Extraire l'historique de chaque fichier au format email

_cd my_repo/dirB
find -name .git -Prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';'
_

Malheureusement, l'option _--follow_ ou _--find-copies-harder_ ne peut pas être combinée avec _--reverse_. C'est pourquoi l'historique est coupé lorsque le fichier est renommé (ou lorsqu'un répertoire parent est renommé).

Historique temporaire au format email:

_/tmp/mail/dir
    ├── subdir
    │   ├── file3
    │   └── file4
    └── file5
_

Dan Bonachea suggère d'inverser les boucles de la commande de génération de journaux git dans cette première étape: au lieu d'exécuter git log une fois par fichier, exécutez-le exactement une fois avec une liste de fichiers sur la ligne de commande et générez un journal unique unifié. De cette façon, les validations qui modifient plusieurs fichiers restent une validation unique dans le résultat, et toutes les nouvelles validations conservent leur ordre relatif d'origine. Notez que cela nécessite également des modifications à la deuxième étape ci-dessous lors de la réécriture des noms de fichiers dans le journal (désormais unifié).


2. Réorganiser l'arborescence des fichiers et mettre à jour les noms de fichiers

Supposons que vous souhaitiez déplacer ces trois fichiers dans cet autre référentiel (il peut s'agir du même référentiel).

_my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB              # New tree
│   ├── dirB1         # from subdir
│   │   ├── file33    # from file3
│   │   └── file44    # from file4
│   └── dirB2         # new dir
│        └── file5    # from file5
└── dirH
    └── file77
_

Réorganisez donc vos fichiers:

_cd /tmp/mail/dir
mkdir -p dirB/dirB1
mv subdir/file3 dirB/dirB1/file33
mv subdir/file4 dirB/dirB1/file44
mkdir -p dirB/dirB2
mv file5 dirB/dirB2
_

Votre historique temporaire est maintenant:

_/tmp/mail/dir
    └── dirB
        ├── dirB1
        │   ├── file33
        │   └── file44
        └── dirB2
             └── file5
_

Changer également les noms de fichiers dans l'historique:

_cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'
_

3. Appliquer la nouvelle histoire

Votre autre repo est:

_my_other_repo
├── dirF
│   ├── file55
│   └── file56
└── dirH
    └── file77
_

Appliquer des commits à partir de fichiers d'historique temporaires:

_cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date
_

_--committer-date-is-author-date_ conserve les horodatages de validation d'origine (le commentaire de Dan Bonachea ).

Votre autre repo est maintenant:

_my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB
│   ├── dirB1
│   │   ├── file33
│   │   └── file44
│   └── dirB2
│        └── file5
└── dirH
    └── file77
_

Utilisez _git status_ pour voir le nombre de commits prêts à être envoyés :-)


Astuce supplémentaire: Vérifiez les fichiers renommés/déplacés dans votre dépôt

Pour lister les fichiers ayant été renommés:

_find -name .git -Prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
_

Plus de personnalisations: Vous pouvez compléter la commande _git log_ en utilisant les options _--find-copies-harder_ ou _--reverse_. Vous pouvez également supprimer les deux premières colonnes à l’aide de _cut -f3-_ et du motif complet grepping '{. * =>. *}'.

_find -name .git -Prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
_
15
olibre

Bien que le cœur de git, la plomberie git ne garde pas la trace des renommés, l'historique que vous affichez avec le journal git "porcelaine" peut les détecter si vous le souhaitez.

Pour un git log donné, utilisez l'option -M:

git log -p -M

Avec une version actuelle de git.

Cela fonctionne pour d'autres commandes comme git diff.

Il existe des options pour rendre les comparaisons plus ou moins rigoureuses. Si vous renommez un fichier sans le modifier en même temps, il sera plus facile pour git log et ses amis de le détecter. Pour cette raison, certaines personnes renomment des fichiers dans un commit et les modifient dans un autre.

Il y a un coût en utilisation de processeur lorsque vous demandez à git de localiser les fichiers qui ont été renommés. Ainsi, que vous les utilisiez ou non, et quand, vous le souhaitez.

Si vous souhaitez que votre historique soit toujours signalé avec la détection de renommage dans un référentiel particulier, vous pouvez utiliser:

git config diff.renames 1

Fichiers passant d'un répertoire à un autre est détecté. Voici un exemple:

commit c3ee8dfb01e357eba1ab18003be1490a46325992
Author: John S. Gruber <[email protected]>
Date:   Wed Feb 22 22:20:19 2017 -0500

    test rename again

diff --git a/yyy/power.py b/zzz/power.py
similarity index 100%
rename from yyy/power.py
rename to zzz/power.py

commit ae181377154eca800832087500c258a20c95d1c3
Author: John S. Gruber <[email protected]>
Date:   Wed Feb 22 22:19:17 2017 -0500

    rename test

diff --git a/power.py b/yyy/power.py
similarity index 100%
rename from power.py
rename to yyy/power.py

Veuillez noter que cela fonctionne chaque fois que vous utilisez diff, pas seulement avec git log. Par exemple:

$ git diff HEAD c3ee8df
diff --git a/power.py b/zzz/power.py
similarity index 100%
rename from power.py
rename to zzz/power.py

En guise d’essai, j’ai apporté une petite modification à un fichier dans une branche de fonctionnalité et l’ai validé, puis dans la branche principale, j’ai renommé le fichier, l’a validé, puis j’ai apporté une petite modification dans une autre partie du fichier et l’ai validé. Lorsque je suis allé à la branche de fonctionnalité et fusionné à partir de maître, la fusion a renommé le fichier et fusionné les modifications. Voici le résultat de la fusion:

 $ git merge -v master
 Auto-merging single
 Merge made by the 'recursive' strategy.
  one => single | 4 ++++
  1 file changed, 4 insertions(+)
  rename one => single (67%)

Le résultat était un répertoire de travail avec le fichier renommé et les deux modifications de texte apportées. Il est donc possible pour git de faire le bon choix malgré le fait qu'il ne trace pas explicitement le renommage.

Ceci est une réponse tardive à une ancienne question, les autres réponses ont peut-être été correctes pour la version git de l'époque.

2
John S Gruber