Сохранить историю файла до того, как он был перемещен в разделяемый каталог

Предположим, у меня есть репозиторий git, содержащий файл text.txt внутри каталога dir_a. Позже я решил переместить text.txt в новый каталог под названием dir_b.

Через некоторое время я решил, что должен разделить dir_b на отдельный репозиторий git, используя git subtree split. По умолчанию самая ранняя фиксация в репозитории dir_b — это фиксация, в которой я переместил text.txt с dir_a на dir_b, что очень неудачно, потому что, например. вина не будет работать, как задумано.

Есть ли способ сохранить в новом репозитории git изменения, внесенные в text.txt, когда он еще был в dir_a?

Чтобы было понятно, в исходном репозитории фиксация, в которой я перемещаю text.txt из dir_a в dir_b, успешно регистрирует операцию перемещения как переименование, поэтому, например. git diff там нормально работает. Моя проблема в том, что в новом репозитории коммиты, сделанные до перемещения, не переносятся в новый репозиторий.


person Romário    schedule 08.03.2017    source источник


Ответы (1)


Редактировать: я совершенно пропустил git subtree split -P prefix часть этого. Первоначальный ответ по-прежнему применим, но с возможно фатальным поворотом.

Когда вы запускаете git subtree split -P prefix [ options ] [ commit-range ], вы указываете Git скопировать некоторые коммиты в новые. У вас есть Git, скопируйте любые коммиты, содержащие любые файлы в заданном prefix, но со следующими изменениями:

  1. Отбрасывать все файлы, которые не начинаются с заданного prefix.
  2. Переименуйте оставшиеся файлы, чтобы убрать prefix (и косую черту).
  3. Если фиксация совпадает с ранее скопированной фиксацией, полностью удалите ее.

(Вы можете сделать это и с git filter-branch, хотя это будет медленнее, чем git subtree split, и для этого потребуется сначала создать новую ветвь для фильтрации.)

Результатом является новый непересекающийся граф коммитов (или подграф, поскольку он теперь добавлен к вашему основному графу коммитов), укореняющийся в первом скопированном коммите и заканчивающийся в последнем скопированном. (Процесс копирования должен перечислять коммиты, в обычном обратном порядке Git, из одного коммита, а не из нескольких коммитов. это должно быть.) Затем вы можете дать этому новому подграфу имя ветви, используя опцию -b branch git subtree. Если вы не дадите ему имя, у вас будет короткий период (по умолчанию 14 дней), в течение которого вы можете что-то сделать с хэш-идентификатором подсказки, который печатает git subtree split, и после этого копии имеют право на автоматическую сборку мусора.

В качестве краткой иллюстрации рассмотрим следующий график:

     C--D--E
    /       \
A--B         H--I--J--K   <-- master
    \       /
     F-----G

Допустим, коммит A имеет в README (и ничего больше), B добавляет первую часть проекта, C-D-E больше проекта, F и G были из ветки функций и добавляют поддерево с именем subbie, содержащее различные файлы, H объединяет поддерево, в I оно переименовывается в feature, в J с ним ничего не происходит, а в K добавляется feature/README_TOO.

Если теперь вы разделите feature как поддерево, это заставит Git скопировать:

  • I: feature сначала появляется как имя, содержащее, например, feature/__init.py и feature/impl.py.
  • K: появляется feature/README_TOO.

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

     C--D--E
    /       \
A--B         H--I--J--K   <-- master
    \       /
     F-----G

I'--K'                    <-- dash-b-argument

Обратите внимание, что мы не копировали F, G и H: у них нет файлов, имя которых начинается с feature/. В коммите J такие файлы есть, но они такие же, как и в коммите I, поэтому мы его пропустили. При этом имена файлов в коммитах I' и K' не feature/__init__.py и так далее, а просто __init__.py и так далее.

Как я отметил в исходном ответе, история в репозитории это коммиты. Мы просматриваем историю, начиная с фиксации кончика ветки и работая в обратном направлении. Если мы начнем с K' и вернемся к I', в истории останутся только эти два коммита. Чтобы обнаружить переименование, нам нужно было бы также скопировать коммиты F и G по крайней мере, а может быть, и H (на этот раз H нечего объединять, так как мы пропустим A-B-C-D-E, так что мы, вероятно, просто отбросьте H полностью). Но для этого нам нужно знать, как сохранить subbie/*.

Вы можете изменить код git subtree, чтобы разрешить дополнительные аргументы префикса «сохраняется как-как». Однако нет четкого способа изменить это постфактум. Базовый код git subtree основан на уникальном префиксе: он был всегда удален, поэтому, чтобы отменить преобразование, мы всегда добавляем его обратно. Два очевидных варианта: никогда не удалять префиксы (поэтому никогда ничего не добавлять) или требовать, чтобы дополнительные неудаленные префиксы никогда не "сталкивались" с именами без префиксов. То есть для любой произвольно скопированной фиксации, если ее снимок имеет файл с именем pa/th/to/file.ext, либо pa/th/to является не префиксом, "сохраненным на месте" (поэтому префикс -P добавляется обратно), либо pa/th/to является таким префиксом (так что ничего не добавляется).


Оригинальный ответ

В Git у файлов нет истории. Нечего сохранять!

В Git только коммиты имеют (точнее, являются) историю. Каждый коммит представляет собой полный снимок исходного дерева плюс некоторые метаданные: имя, адрес электронной почты и отметка времени (как автора фиксации), еще одна тройка имя/адрес электронной почты/отметка времени (для коммиттера); сообщение журнала фиксации; и — важно для формирования истории — идентификатор родительского коммита.

(Некоторые коммиты, которые мы называем коммитами слияния, имеют двух или более родительских элементов. По крайней мере, один коммит — а именно первый сделанный — не имеет никаких родителей; мы называем это < em>корневой коммит. Но у большинства коммитов есть только один родитель, которым обычно является коммит, являющийся верхушкой некоторой ветки, непосредственно перед тем, как коммиттер сделал новый коммит, который стал кончик этой ветки.)

Сравнивая коммит с его родителем, мы узнаем, что произошло с течением времени. Если в предыдущей (родительской) фиксации было 10 файлов, а в последующей (дочерней) фиксации было 11 файлов, то кто-то должен был добавить файл. Если дочерний коммит имеет новую строку 20 в README.txt, они должны были добавить эту строку. Но мы обнаруживаем их только динамически, сравнивая родительский и дочерний элементы. Это история, сформированная коммитами.

Код git blame по мере того, как он работает от дочернего элемента обратно к родительскому (и затем рассматривает этот родитель как еще один дочерний элемент другого родителя), будет искать строки, взятые из других файлов, или целые файлы, переименованные из одного места. другому. Насколько хорошо работает этот поиск, это отдельный вопрос, но, как правило, если какой-то файл p/a/t/h.ext существует в родительском, но не в дочернем, и какой-то другой файл n/e/w.name существует в дочернем, но не в родительском, Git поместит эти два файла в список «кандидатов на обнаружение переименования».

Если два файла с разными именами абсолютно, на 100 % идентичны друг другу, Git почти всегда1 объединит их в пары. Чем менее идентичными они становятся, тем меньше вероятность того, что Git объединит их в пару. У этой пары есть ручки управления: у git diff и друзей они имеют значение --find-renames. Также есть --find-copies и --find-copies-harder. В git blame аргумент -C управляет вещами несколько по-другому. Я недостаточно экспериментировал с этим, чтобы точно сказать, как это работает, но один или два аргумента -C определенно должны обнаружить переименование всего файла на основе документацию.


1Для git diff поиск переименования полностью отключенпо умолчанию в версиях Git до 2.9, но включенпо умолчанию в версиях Git 2.9 и выше. Вы можете установить diff.renames на true, чтобы включить его без настройки определенного порога -M / --find-renames в более старых версиях Git.

Существует также максимальный размер очереди сопряжения, настраиваемый как diff.renameLimit. Преодолеть этот предел удается редко, хотя переименование каждого файла в каталоге (как Git обрабатывает переименование каталога) с большей вероятностью может его достичь. Лимит по умолчанию вырос с годами; раньше было 100, потом 200, а теперь 400 файлов.

person torek    schedule 08.03.2017
comment
Хорошо, но в оригинальном репозитории переход от dir_a к dir_b успешно регистрируется как переименование, то есть dir_a/text.txt и dir_b/text.txt сочетаются в паре, поэтому git видит их как один и тот же файл. Проблема не в исходном репозитории, а в новом репозитории, созданном с помощью git subtree split. Я отредактирую свой вопрос, чтобы сделать его более понятным. - person Romário; 09.03.2017
comment
Ах, верно. Проблема в том, как вы заметили, что сам git subtree split копирует только те коммиты, которые используют новые пути. Чтобы сохранить другие коммиты, вам понадобится вариант git subtree split, который принимает несколько префиксов и не перемещает файлы в новый корень, т. е. вообще не меняет префикс /foo/bar на foo/bar. (Вы можете сделать это с помощью git filter-branch, но вы потеряете возможность git subtree merge и т. д.) Или у вас может быть вариант git subtree split, который переименовывает второй префикс, но результатом снова будет не слияние поддеревьев. в состоянии, поскольку это разрушительное изменение: [продолжение] - person torek; 09.03.2017
comment
если вы импортируете такое переименованное дерево и у вас есть файл с именем foo/bar, какой префикс к нему относится? - person torek; 09.03.2017
comment
Я превратил эти два комментария в довольно серьезное редактирование с некоторыми дополнительными мыслями о процессе реверсирования для расширенной версии git subtree merge, если кто-то решится написать расширенную версию git subtree split. - person torek; 09.03.2017
comment
Этот ответ был очень тщательным нет :P. Большое спасибо за ваш ответ, мне стало намного понятнее, как работает git subtree. - person Romário; 09.03.2017