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

je voudrais renommer/déplacer un projet de sous-arborescence dans Git en le déplaçant d'

/project/xyz

à

/components/xyz

si j'utilise un simple git mv project components , alors toute l'histoire de propagation du xyz project est perdue. Est-il un moyen de déplacer ce tels que l'histoire est maintenue?

555
demandé sur Micha Wiedenmann 2010-02-23 01:11:07

9 réponses

git détecte les renommages plutôt que de persister l'opération avec le commit, donc que vous utilisiez git mv ou mv n'a pas d'importance.

la commande log prend un argument --follow qui poursuit l'histoire avant une opération de renommage, c'est-à-dire qu'elle recherche un contenu similaire en utilisant les heuristiques:

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

pour rechercher l'historique complet, utilisez le suivant commande:

git log --follow ./path/to/file
566
répondu Troels Thomsen 2018-01-19 11:40:27

Il est possible pour renommer un fichier et de conserver l'historique intact, bien qu'il provoque le fichier sera renommé au long de toute l'histoire du référentiel. Ce n'est probablement que pour les obsessionnels git-log-lovers, et a quelques implications sérieuses, dont celles-ci:

  • vous pourriez réécrire une histoire partagée, qui est le plus important Ne pas tout en utilisant Git. Si quelqu'un d'autre a cloné le dépôt, vous aurez briser il faire ce. Ils devront recloniser pour éviter les maux de tête. Cela pourrait être correct si le renommage est assez important, mais vous devrez considérer cela avec soin -- vous pourriez finir par bouleverser toute une communauté opensource!
  • si vous avez Référencé le fichier en utilisant son ancien nom plus tôt dans l'histoire du dépôt, vous cassez effectivement les versions précédentes. Pour remédier à cela, vous aurez à faire un peu plus de saut de cerceau. Il 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 brise l'histoire.

au lieu de:

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

sera refaire chaque commit de la branche, l'exécution de la commande dans le tiques pour chaque itération. Beaucoup de choses peuvent mal tourner quand tu fais ça. Normalement je teste pour voir si le fichier est présent (sinon il n'est pas encore là pour bouger) et puis je fais les démarches nécessaires pour dénouer l'arbre à mon goût. Ici, vous pouvez passer par des fichiers pour modifier les références au fichier et ainsi de suite. Faites-vous plaisir! :)

une fois terminé, le fichier est déplacé et le journal est intact. Tu te sens comme un pirate ninja.

aussi; le dir mkdir n'est nécessaire que si vous déplacez le fichier vers un nouveau dossier, bien sûr. Le si évitera la création de ce dossier plus tôt dans l'histoire que votre fichier existe.

76
répondu Øystein Steimler 2015-05-31 10:08:03

Pas de.

la réponse courte est Non , il n'est pas possible de renommer un fichier en Git et se souvenir de l'histoire. Et c'est une douleur.

la rumeur dit que git log --follow --find-copies-harder va fonctionner, mais il ne fonctionne pas pour moi, même si il ya zéro changements dans le contenu du fichier, et les mouvements ont été effectués avec git mv .

(initialement J'ai utilisé Eclipse pour renommer et mettre à jour les paquets dans une opération, ce qui a pu confondre git. Mais c'est une chose très commune à faire. --follow ne semble pas fonctionner si seulement un mv est effectué et un commit et le mv n'est pas trop loin.)

Linus dit que vous êtes censé comprendre le contenu entier d'un projet de logiciel de manière holistique, sans avoir besoin de suivre des fichiers individuels. Malheureusement, mon petit cerveau ne peut pas faire ça.

c'est vraiment ennuyeux que tant de gens ont répété sans réfléchir la déclaration que git suit automatiquement les mouvements. Ils ont perdu mon temps. Git ne fait rien de tel. par conception (!) Git ne suit pas du tout les mouvements.

Ma solution est de renommer les fichiers vers leur emplacement d'origine. Changez le logiciel pour qu'il corresponde au contrôle source. Avec git, vous semblez avoir besoin de git dès la première fois.

malheureusement, cela brise L'éclipse, qui semble utiliser --follow .

git log --follow Parfois ne montre pas l'histoire complète des fichiers avec des histoires de renommage compliquées, même si git log faire. (Je ne sais pas pourquoi.)

(il y a des hacks trop intelligents qui reviennent en arrière et réengagent de vieux travaux, mais ils sont plutôt effrayants. Voir Github-Gist: emiller/git-mv-with-history .)

62
répondu Tuntable 2016-11-25 12:25:36
git log --follow [file]

vous montrera l'histoire à travers les renoms.

39
répondu Erik Hesselink 2010-02-22 22:25:05

je n':

git mv {old} {new}
git add -u {new}
16
répondu James M. Greene 2016-02-29 10:58:14

objectif

  • utilisation git am (inspiré de Smar , emprunté de Exherbo )
  • Ajouter commettre l'histoire de copié/déplacé les fichiers
  • D'un répertoire à un autre
  • ou d'un dépôt à un autre

Limitation

  • les étiquettes et les branches ne sont pas conservées
  • l'Histoire est coupée sur le chemin d'accès du fichier rename (renommer le répertoire)

résumé

  1. extraire l'historique dans le format de courriel 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 une nouvelle histoire en utilisant

    cat extracted-history | git am --committer-date-is-author-date

1. Historique d'extrait dans le format de courriel

exemple: extrait de l'histoire de file3 , file4 et file5

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

régler / nettoyer la destination

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

extrait historique de chaque fichier dans le format de courriel

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 -- ""151920920"" > "$historydir/"151920920""' {} ';'

malheureusement l'option --follow ou --find-copies-harder ne peut pas être combinée avec --reverse . Ce est pourquoi l'histoire est coupée lorsque le fichier est renommé (ou lorsqu'un parent directory est renommé).

Temporaire de l'histoire au format électronique:

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

Dan Bonachea suggère d'inverser les boucles de la commande git log generation dans cette première étape: plutôt que 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 de générer un seul log unifié. De cette façon, s'engage à modifier plusieurs fichiers rester un seul commit dans le résultat, et tous les nouveaux commits maintiennent leur ordre relatif d'origine. Notez que cela nécessite également des changements dans la deuxième étape ci-dessous lors de la réécriture des noms de fichiers dans le journal (maintenant unifié).


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

Supposons que vous souhaitez déplacer ces trois fichiers dans ce repo (peut être le même repo).

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 votre dossiers:

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 histoire temporaire est maintenant:

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

Modifier aussi les noms de fichiers dans l'histoire:

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

3. Appliquer la nouvelle histoire

votre autre pension est:

my_other_repo
├── dirF
│   ├── file55
│   └── file56
└── dirH
    └── file77

Appliquer s'engage temporaires, l'historique des fichiers:

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

--committer-date-is-author-date préserve les horodatage d'origine ( Dan Bonachea commentaire).

votre autre pension est maintenant:

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

utiliser git status pour voir la quantité de propagations prêtes à être poussées: -)


astuce supplémentaire: vérifiez les fichiers renommés / déplacés dans votre rapport

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 utilisant les options --find-copies-harder ou --reverse . Vous pouvez également supprimer les deux premières colonnes en utilisant cut -f3- et gropping complete pattern '{.* = > .*}'.

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
12
répondu olibre 2017-10-20 07:17:58

alors que le noyau de git, la plomberie git ne garde pas trace de renom, l'histoire que vous affichez avec le git log "porcelaine" peut les détecter si vous le souhaitez.

pour un git log utiliser l'option-M:

git log-p-M

Avec une version actuelle de git.

cela fonctionne aussi pour d'autres commandes comme git diff .

il y a des options pour rendre les comparaisons plus ou moins rigoureuses. Si vous renommez un fichier sans apporter de changements significatifs au fichier en même temps, il est plus facile pour git log et ses amis de détecter le renommage. Pour cette raison, certaines personnes renomment des fichiers dans une propagation et les changent dans une autre.

il y a un coût dans l'utilisation cpu chaque fois que vous demandez à git de trouver où les fichiers ont été renommés, donc que vous l'utilisiez ou non, et quand, c'est à vous de décider.

si vous souhaitez que votre historique soit toujours signalé avec la détection de renommage dans un dépôt particulier, vous pouvez utiliser:

git config diff.renommes 1

fichiers se déplaçant d'un répertoire à un autre est détecté. Voici un exemple:

commit c3ee8dfb01e357eba1ab18003be1490a46325992
Author: John S. Gruber <JohnSGruber@gmail.com>
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 <JohnSGruber@gmail.com>
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, et 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

Comme un essai que j'ai fait une petite modification à un fichier dans une branche et l'a commis, puis dans la branche master j'ai renommé le fichier, commis, puis fait un petit changement dans une autre partie du fichier et engagé. Quand je suis allé à la branche des fonctionnalités et ai fusionné à partir de master la fusion a renommé le fichier et a fusionné les changements. Voici la sortie 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 a été un répertoire de travail avec le fichier renommé et les deux modifications apportées au texte. Il est donc possible pour git de faire la bonne chose malgré le fait qu'il ne trace pas explicitement les renames.

il s'agit d'une réponse tardive à une ancienne question, de sorte que les autres réponses étaient peut-être correctes pour la version git à l'époque.

2
répondu John S Gruber 2017-02-23 04:54:04

je voudrais renommer/déplacer un projet de sous-arborescence dans Git en le déplaçant d'

/project/xyz

à

/composants/xyz

si j'utilise un simple git mv project components , alors toute l'histoire de propagation du projet xyz est perdue.

Non (8 ans plus tard, Git 2.19, Q3 2018), parce que Git va détecter le répertoire renommé , et c'est maintenant mieux documentées.

Voir commettre b00bf1c , commettre 1634688 , commettre 0661e49 , commettre 4d34dff , commettre 983f464 , commettre c840e1a , commettre 9929430 (27 Juin 2018), et commettre d4e8062 , commettre 5dacd4a (25 Juin 2018) par Élie Newren ( newren ) .

(fusionné par Junio CA Hamano -- gitster -- in commit 0ce5a69 , 24 juil 2018)

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

exemple:

lorsque tous les x/a , x/b et x/c ont été déplacés à z/a , z/b et z/c , il est probable que x/d ajouté dans l'intervalle voudrait également passer à z/d par prendre le soupçon que l'ensemble du répertoire ' x 'déplacé' z '.

, Mais ils sont beaucoup d'autres cas, comme:

d'un côté de l'histoire renomme x -> z , et l'autre renomme un fichier pour x/e , ce qui rend la fusion nécessaire pour faites un changement de nom.

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

quelques règles de base limite quand la détection de renommage de répertoire s'applique:

  1. si un répertoire donné existe encore des deux côtés d'une fusion, nous ne le considérons pas comme ayant été renommé.
  2. si un sous-ensemble de fichiers à renommer a un fichier ou un répertoire manière (ou serait dans la voie de l'autre), "désactiver" le répertoire renommé pour ces sous-chemins spécifiques et signaler le conflit à l'utilisateur.
  3. si l'autre côté de l'histoire a renommé un répertoire en un chemin que votre côté de l'histoire a renommé ailleurs, alors ignorez ce renommage particulier de l'autre côté de l'histoire pour tout renommage implicite du répertoire (mais prévenez l'utilisateur).

, Vous pouvez voir beaucoup d'essais en t/t6043-merge-rename-directories.sh , qui soulignent aussi que:

  • a) Si renomme diviser un répertoire en deux ou plusieurs autres, le répertoire avec le plus renomme, "gagne".
  • B) éviter la détection de répertoire-renommé pour un chemin, si ce chemin est la source d'un renommé de chaque côté d'une fusion.
  • c) n'appliquer les renomages implicites de répertoire aux répertoires que si le autre côté l'histoire est celle de faire le changement de nom.
1
répondu VonC 2018-07-30 19:37:40

je fais le déplacement des fichiers et ensuite faire

git add -A

qui ont mis dans la zone de chargement tous les fichiers supprimés/nouveaux. Ici git se rend compte que le fichier est déplacé.

git commit -m "my message"
git push

je ne sais pas pourquoi, mais cela fonctionne pour moi.

-1
répondu Xelian 2016-12-18 14:34:03