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?
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
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:
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à.
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 .)
git log --follow [file]
vous montrera l'histoire à travers les renommés.
Je fais:
git mv {old} {new}
git add -u {new}
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 projetxyz
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
etx/c
sont passés àz/a
,z/b
etz/c
, il est probable quex/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 enx/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:
- 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é.
- 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 .
- 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.
git am
(inspiré de Smar , emprunté à Exherbo )git log --pretty=email -p --reverse --full-index --binary
_cat extracted-history | git am --committer-date-is-author-date
_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é).
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"' {} ';'
_
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 :-)
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 '{.* => .*}'
_
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.