Можно ли перемещать / переименовывать файлы в Git и поддерживать их историю? - PullRequest
606 голосов
/ 23 февраля 2010

Я бы хотел переименовать / переместить поддерево проекта в Git, переместив его из

/project/xyz

до

/components/xyz

Если я использую простой git mv project components, то вся история коммитов для xyz project теряется. Есть ли способ переместить это так, чтобы история поддерживалась?

Ответы [ 9 ]

603 голосов
/ 23 февраля 2010

Git обнаруживает переименования, а не сохраняет операцию с фиксацией, поэтому, неважно, используете ли вы git mv или mv.

Команда log принимает аргумент --follow, который продолжает историю до операции переименования, т. Е. Ищет похожий контент с использованием эвристики:

http://git -scm.com / документы / ГИТ-журнал

Чтобы просмотреть полную историю, используйте следующую команду:

git log --follow ./path/to/file
88 голосов
/ 28 февраля 2013

возможно переименовать файл и сохранить историю без изменений, хотя это приводит к тому, что файл переименовывается на протяжении всей истории репозитория. Это, вероятно, только для навязчивых любителей git-log, и имеет некоторые серьезные последствия, включая следующие:

  • Возможно, вы переписываете общую историю, которая является самой важной НЕ при использовании Git. Если кто-то еще клонировал хранилище, вы сломаете его, сделав это. Им придется повторно клонировать, чтобы избежать головной боли. Это может быть хорошо, если переименование достаточно важно, но вы должны тщательно обдумать это - в конечном итоге вы можете расстроить все сообщество с открытым исходным кодом!
  • Если вы ранее ссылались на файл, используя его старое имя в истории репозитория, вы фактически ломаете более ранние версии. Чтобы исправить это, вам придется делать больше прыжков с обручем. Это не невозможно, просто утомительно и, возможно, не стоит.

Теперь, так как вы все еще со мной, вы, вероятно, одинокий разработчик, переименовывающий полностью изолированный файл. Давайте переместим файл, используя filter-tree!

Предположим, вы собираетесь переместить файл old в папку dir и дать ему имя new

Это можно сделать с помощью git mv old dir/new && git add -u dir/new, но это ломает историю.

Вместо того, чтобы:

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

будет повторять каждый коммит в ветви, выполняя команду в тиках для каждой итерации. Много вещей может пойти не так, когда вы сделаете это. Обычно я проверяю, присутствует ли файл (в противном случае он еще не перемещен), а затем выполняю необходимые шаги, чтобы вставить дерево в нужное мне место. Здесь вы можете просматривать файлы, чтобы изменить ссылки на файл и так далее. Сбей себя с ног! :)

По завершении файл перемещается, а журнал не изменяется. Вы чувствуете себя пиратом ниндзя.

Также; Разумеется, каталог mkdir необходим только в том случае, если вы перемещаете файл в новую папку. if позволит избежать создания этой папки ранее в истории, чем существует ваш файл.

76 голосов
/ 28 сентября 2015

номер

Краткий ответ: НЕТ , невозможно переименовать файл в Git и запомнить историю. И это боль.

Ходят слухи, что git log --follow--find-copies-harder будет работать, но у меня это не сработает, даже если в файле нет нулевых изменений, а шаги были сделаны с помощью git mv.

(Изначально я использовал Eclipse для переименования и обновления пакетов в одной операции, что может привести к путанице в git. Но это очень распространенная вещь. --follow, кажется, работает, если выполняется только mv, а затем commit и mv не слишком далеко.)

Линус говорит, что вы должны понимать все содержимое программного проекта целиком, без необходимости отслеживать отдельные файлы. Ну, к сожалению, мой маленький мозг не может этого сделать.

действительно раздражает , что так много людей бездумно повторяют утверждение, что git автоматически отслеживает ходы. Они потратили впустую мое время. Git не делает таких вещей. По замыслу (!) Git вообще не отслеживает ходы.

Мое решение - переименовать файлы обратно в их исходные местоположения. Измените программное обеспечение в соответствии с контролем источника. С git вам, кажется, просто нужно сделать git правильно с первого раза.

К сожалению, это нарушает Eclipse, который, кажется, использует --follow.
git log --follow Иногда не показывает полную историю файлов со сложной историей переименования, хотя git log делает. (Я не знаю почему.)

(Есть некоторые слишком умные хаки, которые возвращаются и возобновляют старую работу, но они довольно пугающие. Смотрите GitHub-Gist: emiller / git-mv-with-history .)

40 голосов
/ 23 февраля 2010
git log --follow [file]

покажет вам историю через переименования.

17 голосов
/ 24 ноября 2012

Я делаю:

git mv {old} {new}
git add -u {new}
14 голосов
/ 24 ноября 2015

Цель

  • Использование git am (вдохновлено Смар , заимствовано у Exherbo )
  • Добавить историю коммитов скопированных / перемещенных файлов
  • Из одного каталога в другой
  • Или из одного хранилища в другое

Ограничение

  • Теги и ветки не сохраняются
  • История вырезана при переименовании файла пути (переименование каталога)

Краткое описание

  1. Извлечение истории в формате электронной почты с использованием
    git log --pretty=email -p --reverse --full-index --binary
  2. Реорганизовать дерево файлов и обновить имена файлов
  3. Добавить новую историю, используя
    cat extracted-history | git am --committer-date-is-author-date

1. Извлечь историю в формате электронной почты

Пример: извлечение истории file3, file4 и file5

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

Установить / очистить пункт назначения

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

Извлечение истории каждого файла в формате электронной почты

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"' {} ';'

К сожалению, опция --follow или --find-copies-harder не может быть объединена с --reverse. Вот почему история удаляется при переименовании файла (или при переименовании родительского каталога).

Временная история в формате электронной почты:

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

Dan Bonachea предлагает инвертировать циклы команды генерации git log на этом первом шаге: вместо того, чтобы запускать git log один раз для файла, запускайте его ровно один раз со списком файлов в командной строке и создать единый лог. Таким образом, коммиты, которые изменяют несколько файлов, остаются в результате одним коммитом, и все новые коммиты сохраняют свой первоначальный относительный порядок. Обратите внимание, что это также требует изменений на втором этапе ниже при перезаписи имен файлов в (теперь объединенном) журнале.


2. Реорганизовать дерево файлов и обновить имена файлов

Предположим, вы хотите переместить эти три файла в этом другом репо (это может быть тот же репо).

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

Поэтому реорганизуйте ваши файлы:

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

Ваша временная история теперь:

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

Изменить также имена файлов в истории:

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

3. Применить новую историю

Ваш другой репо:

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

Применить коммиты из временных файлов истории:

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

--committer-date-is-author-date сохраняет исходные отметки времени коммита ( Dan Bonachea * комментарий 1097 *).

Ваш другой репо сейчас:

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

Используйте git status, чтобы увидеть количество коммитов, готовых к отправке: -)


Дополнительный трюк: проверка переименованных / перемещенных файлов в вашем репо

Для просмотра списка файлов, переименованных:

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

Дополнительные настройки: Вы можете выполнить команду git log, используя опции --find-copies-harder или --reverse. Вы также можете удалить первые два столбца, используя cut -f3- и полный шаблон grepping '{. * =>. *}'.

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
13 голосов
/ 30 июля 2018

Я хотел бы переименовать / переместить поддерево проекта в Git, переместив его из

/project/xyz

до

/ компоненты / хуг

Если я использую обычный git mv project components, то вся история коммитов для проекта xyz теряется.

Нет (8 лет спустя, Git 2.19, Q3 2018), потому что Git обнаружит переименование каталога , и теперь это лучше документировано.

См. commit b00bf1c , commit 1634688 , commit 0661e49 , commit 4d34dff , commit 983f464 , коммит c840e1a , коммит 9929430 (27 июня 2018 г.) и коммит d4e8062 , коммит 5dacd4a (25 июня 2018 г.) от Илии Ньюрен (newren) .
(Объединено с Junio ​​C Hamano - gitster - в коммит 0ce5a69 , 24 июля 2018 г.)

Это теперь объясняется в Documentation/technical/directory-rename-detection.txt:

Пример: * * тысяча пятьдесят-четырь

Когда все x/a, x/b и x/c переместятся в z/a, z/b и z/c, вполне вероятно, что добавленные за это время x/d также захотят перейти к z/d от намекнув, что весь каталог 'x' перемещен в 'z'.

Но есть много других случаев, например:

одна сторона истории переименовывает x -> z, а другая переименовывает какой-либо файл в x/e, вызывая необходимость слияния для транзитивного переименования.

Чтобы упростить обнаружение переименования каталогов, эти правила применяются Git:

ограничение пары основных правил, когда применяется обнаружение переименования каталогов:

  1. Если данный каталог все еще существует по обе стороны от слияния, мы не считаем его переименованным.
  2. Если подмножество переименованных файлов содержит файл или каталог в пути (или будут мешать друг другу), «отключите» переименование каталога для этих конкретных подпутей и сообщите о конфликте пользователю.
  3. Если другая сторона истории переименовала каталог по пути, который ваша сторона истории переименовала, то игнорируйте это конкретное переименование с другой стороны истории для любых неявных переименований каталога (но предупреждайте пользователя).

Вы можете увидеть много тестов в t/t6043-merge-rename-directories.sh, которые также указывают, что:

  • a) Если переименование разделяет каталог на два или более других, каталог с наибольшим количеством переименований «выигрывает».
  • b) Избегайте обнаружения каталогов-переименований для пути, если этот путь является источником переименования по обе стороны от слияния.
  • c) Применить неявные переименования каталогов только к каталогам, если другая сторона истории делает переименование.
2 голосов
/ 23 февраля 2017

В то время как ядро ​​git, git-сантехника не отслеживает переименования, история, которую вы отображаете с помощью git log «фарфор», может обнаружить их, если хотите.

Для данного git log используйте опцию -M:

git log -p -M

С текущей версией git.

Это работает и для других команд, таких как git diff.

Есть варианты сделать сравнения более или менее строгими. Если вы переименуете файл, не внося при этом значительных изменений в файл, то для git log и друзей будет легче обнаружить переименование. По этой причине некоторые люди переименовывают файлы в одном коммите и изменяют их в другом.

Использование ЦП обходится дорого, когда вы просите git найти, где файлы были переименованы, независимо от того, используете ли вы их или нет, и когда решать только вам.

Если вы хотите, чтобы ваша история всегда сообщалась с обнаружением переименования в определенном хранилище, вы можете использовать:

git config diff.renames 1

Обнаружены файлы, перемещающиеся из одного каталога в другой . Вот пример:

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

Обратите внимание, что это работает всякий раз, когда вы используете diff, а не только с git log. Например:

$ 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

В качестве пробной версии я внес небольшое изменение в один файл в ветви функций и зафиксировал его, а затем в основной ветви переименовал файл, зафиксировал, а затем внес небольшие изменения в другую часть файла и подтвердил это. Когда я перешел в ветку Feature и слился с master, слияние переименовало файл и объединило изменения. Вот вывод из слияния:

 $ 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%)

В результате получился рабочий каталог с переименованным файлом и обоими текстовыми изменениями. Так что git может делать правильные вещи, несмотря на то, что он явно не отслеживает переименования.

Это поздний ответ на старый вопрос, поэтому другие ответы могли быть верными для версии git в то время.

0 голосов
/ 18 декабря 2016

Я делаю перемещение файлов, а затем делаю

git add -A

, которые помещают в область сатурации все удаленные / новые файлы. Здесь git понимает, что файл перемещен.

git commit -m "my message"
git push

Я не знаю почему, но у меня это работает.

...