Как сохранить историю предварительного переименования при перемещении нескольких файлов из одного GIT-хранилища в другое? - PullRequest
1 голос
/ 24 марта 2019

Краткое содержание вопроса

Мне нужно переместить несколько файлов из одного хранилища в другое, сохраняя при этом их историю изменений.Я уже переместил их в исходный репозиторий в специальную папку с git mv (согласно широко цитируемому Грэгу Бауэру post , что приводит к тому, что вся история перемещения перед папкой не копируется в целевой репозиторий при следовании сценарию Грега.

У меня есть только основная ветвь в каждом из задействованных репозиториев.

В случае первого исходного репозитория исходные файлы использовались в корневой папке перед перемещением в выделенную папку.

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

ЦельВ репозитории уже есть некоторые другие файлы и папки, которые мне нужно сохранить, с его историей коммитов.

Наконец, если все правильно скопировано в репозиторий назначения, мне нужен чистый способудалить (скрыть?) исходные файлы из исходных репозиториев.

Обновление 2019-03-25 12:00 UTC: Некоторыеболее подробно о моей ситуации, после блестящего объяснения Торека :

  1. Я был и являюсь единственным пользователем из всех трех указанных репозиториев (как исходных, так и одного целевого);каждый из исходных репозиториев используется на одной рабочей станции
  2. Один исходный репозиторий размещен на GitHub;другой в GitLab (как частный проект).Целевой репозиторий размещен на GitLab, как частный проект.На данный момент нет «нескольких репозиториев, хранящих один и тот же коммит», если я правильно понимаю, что конкретно означает «репозиторий».
  3. Мои локальные папки «.git» для этих репозиториев довольно малы;самый большой - всего 12 МБ на диске, 2,5 тыс. файлов.Таким образом, производительность не кажется большой проблемой.
  4. Что меня больше всего интересует в целевом репозитории, так это: (а) существенное, diff "до vs после" рассматриваемых файлов;(б) достаточно важная отметка времени первоначального изменения;(c) приятно иметь имя исходного коммиттера (всегда самого себя)
  5. В будущих ситуациях мне нужно будет мигрировать из частного репозитория (содержащего другие личные файлы) в публичный репозиторий, который не должен иметь никакихупоминание этих личных файлов или их содержимого.Однако в моей сегодняшней ситуации это не так.

Что-то, что я рассмотрел, но не смог использовать "с полки":

Я не знаком с тем, как устроен репозиторий GIT, поэтому 'git ls-files ... | grep ... INDEX_FILE ... git update-index ... из Этап 1, шаг 5 звучит для меня как волшебство.

От ответ на другой вопрос, неясно, поможет ли это с отдельными файлами, уже перемещенными в выделенную папку (и / или безопасно ли откатить перемещение до миграции).

Кроме того, как я могу выбрать между не /используя эти шаги :

git reflog expire --expire=now --all
git reset --hard
git gc --aggressive
git prune

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

1 Ответ

3 голосов
/ 25 марта 2019

Ни один ответ не будет полностью удовлетворительным для всех в каждом случае.Это потому, что вы буквально не можете скопировать файл историю из одного репозитория Git в другой по той простой причине, что Git не имеет историю файлов.Вы не можете удалить файл из (существующей) истории по другой, но связанной причине.Но то, что вы можете получить , может быть достаточно хорошим.

История Git - это коммитов, а коммиты неизменны

КакЯ уже много раз говорил, Git's raison d'être - это коммит.Что делает Git - это сохраняет коммиты, плюс немного больше, чтобы сделать их более полезными. extra означает, что иногда , вы можете сделать то, что достаточно для того, что вы хотите - хотя это, конечно, зависит именно от того, что вы хотите - или, возможно,что вы согласитесь.Давайте внимательно посмотрим на коммиты и посмотрим, как они являются историей.

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

$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800

Fourth batch after 2.20

Signed-off-by: Junio C Hamano <gitster pobox.com>

Это не такКонечно, GitHub отображает его, но это внутренний объект Git, в котором полностью сохраняется коммит.Сохраненный снимок получается через строку tree.Строка parent перечисляет коммит, который приходит до этого коммита , который сам по себе является коммитом слияния, так что он имеет две parent строки.

здесь важны следующие вещи:

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

  • В коммите содержится список tree, который является сохраненным моментальным снимком.

  • В коммите перечислены хэш-идентификаторы) его родителей или родителей.

Что означает этот , так это то, что Git нужен только хэш-идентификатор последнего коммита.Предположим, мы представляем этот большой некрасивый хэш-идентификатор одной буквой, например H (для hash).Мы говорим, что commit H хранит хеш-идентификатор своего родителя, который мы представим как G вместо другой большой уродливой строки.Затем совершите H очков до commit G:

          G <-H

Но G - это коммит.Это означает, что он хранит хеш-идентификатор его родителя, который мы можем назвать F:

... <-F <-G <-H

и, конечно, F хранит хеш-идентификатор E, ии так далее, в цепочке задом наперед.Цепочка может разветвляться и повторно объединяться, и если бы мы шли вперед, а не назад, разветвление происходило бы, когда мы делали ветви, и повторное объединение происходило бы, когда мы объединяли ветви.Но поскольку Git фактически работает задом наперед, разветвление происходит при слиянии;повторное объединение происходит, когда у нас заканчивается объединенное содержимое:

             I--J
            /    \
...--F--G--H      M--N--...--T   <-- master
            \    /
             K--L

В любом случае эта цепочка является историей Git.Элемент, который предоставляет хэш-идентификатор последнего коммита в цепочке, как показано на рисунке выше, имя ветви , например master.

Это все, что есть в Git. Нет истории файлов, есть только коммиты.Мы находим коммиты, начиная с tip commit, например, T, чей ID хеша мы находим по имени, например master.Мы добавляем новую историю - новые коммиты - в репозиторий, делая новый коммит U, чей parent равен T, а затем изменяя имя master, чтобы указать нановый коммит U.

фиксируетнеизменны , потому что их настоящие имена - их хэш-идентификаторы - вычисляются путем запуска криптографической контрольной суммы над всего содержимого фиксации. Если бы мы взяли вышеупомянутый коммит и изменили что-нибудь о нем - например, сохраненные отметки даты в строке author или committer, или сообщение журнала, или снимок tree - мы должны были бы вычислить новую контрольную сумму по новым данным. Эта контрольная сумма будет другой, и вместо изменения существующего коммита H мы просто получим новый коммит H':

...--F--G--H--I--J   <-- master
         \
          H'  <-- need-a-name-here

Этот новый коммит H' имеет G в качестве родителя, поэтому H' это просто ветвь. Теперь мы должны изобрести имя ветки, чтобы хранить хэш-идентификатор нового коммита H', который является копией H, но что-то изменилось. Мы не изменили ни одного коммита, мы просто добавили новый коммит.

Но я могу запустить git log --follow somefile.ext, разве это не история файлов?

Может быть, это так! Но это не хранится в Git . То, что хранится в Git - это коммиты. git log сделал, чтобы начать с какого-то имени ветви, например master, и найти там коммит - коммит tip ветви. Этот коммит имеет хэш-идентификатор, сообщение журнала и снимок. Конечно, Git смог найти коммит parent коммита, как это было сохранено в коммите tip.

Теперь самое сложное. Все это происходит в большом цикле, работая над каждым коммитом, по одному коммиту за раз. Git выбирает показывать или нет коммит, на котором он работает , и для git log somefile.ext:

  • Git извлекает снимок родительского коммита во временную область.

  • Git извлекает снимок фиксации во временную область.

    (Это на самом деле не извлекает коммитов, но если вы подумаете об этом таким образом, это может иметь больше смысла. На самом деле он просто сравнивает хэш-идентификаторы внутри дерева, что достаточно. Позже, если вы попросили git log показать различия, это действительно делает частичное извлечение. Но на самом деле это всего лишь оптимизация.)

  • Теперь git log сравнивает два снимка. somefile.ext изменился? Если это так, показать этот коммит.

  • Показав или не показав этот коммит, перейдите к его родителю.

Без --follow, это все , что git log somefile.ext делает. Вы видите синтетическую «историю файлов», состоящую из подмножества истории фиксации, в которой файл изменился с родительского на дочерний. Вот и все! То, что вы видели, было выбранная история коммитов . Вы можете вызвать эту "историю файлов", если хотите, но она вычисляется динамически из истории фиксации, которую Git фактически хранит.

Добавление --follow говорит git log сделать еще одну вещь: сравнивая два коммита, проверьте, не предполагает ли сравнение, что в родительском коммите somefile.ext имел другое имя пути, Если родительский коммит вызвал файл oldname.dat, например, git log --follow переключает имена , когда он возвращается на один шаг назад в истории коммитов.

Здесь есть некоторые проблемы, особенно в отношении коммитов слияния. Коммит слияния - это коммит с двумя родителями вместо одного. Git буквально не может показать оба пути одновременно - он движется назад по истории коммитов, по одному коммиту за раз. Таким образом, когда он сталкивается с этими слияниями - вот где история расходится, потому что Git работает задом наперед - он обычно выбирает только одну ветвь истории , чтобы следовать.

(ДетальЗдесь довольно сложно. См. Раздел «Упрощение истории» документации git log, но это тяжело. При запуске без определенных имен файлов, чтобы показать все коммиты, git log по умолчанию опускается на обе ветви слияния, что немного сложно описать правильно : здесь мы должны ввести понятие очереди приоритетов . Линейная история, без слияний, позволяет избежать всей этой путаницы, и о ней легче думать.)

Теперь вернемся к проблеме

Давайте вернемся к исходному, краткому изложению желаемого результата:

Мне нужно переместить несколько файлов из одного хранилища в другое, сохраняя при этом их историю изменений.

То есть мы хотим, чтобы файлы, взятые из коммитов из RepoA, каким-то образом появлялись в коммитах, находящихся в RepoB.

Мы можем сразу увидеть проблему: история этих файлов действительно всех коммитов в RepoA или, в лучшем случае, некоторое подмножество коммитов из RepoA . Каждый из этих коммитов представляет собой полный снимок из всех своих файлов.

Более того, если мы возьмем эти снимки - либо в целом, либо в некоторой уменьшенной форме - и поместим их в RepoB, , эти снимки не будут такими же, как любой существующие снимки в RepoB. Давайте рассмотрим простой конкретный пример, в котором RepoA имеет четыре снимка A-B-C-D в виде красивой линейной цепочки, а RepoB - еще четыре E-F-G-H, аналогично:

RepoA:

A--B--C--D   <-- master

RepoB:

E--F--G--H   <-- master

Если мы просто скопируем все коммиты из RepoA в RepoB без изменений, мы получим это в RepoB:

E--F--G--H   <-- master

A--B--C--D   <-- invent-a-name-here

Это явно не то, что мы хотим. Мы можем сделать что-то, и это то, о чем все ответы, на которые вы смотрели,

Что мы можем сделать здесь

Если мы хотим somefile.ext из RepoA, и он сначала создается в коммите B, а затем изменяется в коммите D, то, что мы можем сделать, это сделать два новых коммита I и J с только одним файлом . Мы можем сделать их где угодно - все Gits равны - поэтому давайте сделаем RepoC, клонируя RepoA, а затем сделаем их в RepoC, в основном только для иллюстрации:

$ git clone <url-of-RepoA> repo-c
$ cd repo-c
$ git checkout --orphan for-transplanting
$ git rm -rf .                              # empty the index and work-tree
$ git checkout <hash-of-B> -- somefile.ext  # get the first copy of the file
$ git commit -m 'initial commit of somefile.ext'  # and commit it
$ git checkout master -- somefile.ext       # get the 2nd and last copy
$ git commit -m 'update somefile.ext'       # and commit that one

Теперь RepoC содержит:

A--B--C--D   <-- master, origin/master

I--J   <-- for-transplanting

Теперь мы можем копировать коммиты I и J в RepoB:

$ cd <path-to-repo-B>
$ git fetch <path-to-repo-C> for-transplanting:for-transplanting

, что дает нам это в RepoB:

E--F--G--H   <-- master

I--J   <-- for-transplanting

где коммиты I и J имеют нужный файл.

Этот файл находится в истории J -then- I -then-stop , которая состоит из этих двух коммитов. (Трюк git checkout --orphan убедился, что когда мы сделали коммит I, у него не было родителя - это был корневой коммит, как и самый первый коммит, который мы сделали бы в новом, пустом репозитории. Помните, что все коммиты, с их уникальные хеш-идентификаторы универсальны для каждого Git-репозитория: у вас либо этот коммит с его хеш-идентификатором, либо у вас его нет. У RepoB их не было, и теперь, после git fetch, у RepoB имеет их.)

Эти истории, очевидно, не связаны: невозможно прыгнуть с J на H -обратную цепь и обратно, и наоборот. Но теперь мы можем сказать Git «жениться» на коммитах H и J, создав новый коммит K:

$ git checkout master
$ git merge --allow-unrelated-histories for-transplant

При этом используется (несуществующий, подделанный через пустое дерево ) действительно пустой коммит в качестве базы слияния, так что все файлы в H создаются заново и все файлы в J (только один файл) являются (являются) вновь созданными. Он объединяет эти изменения - добавляет все файлы в ничто и добавляет somefile.ext в ничто - что легко сделать, применяет эти изменения к пустому дереву, в котором нет файлов, и фиксирует результат как новый коммит K:

E--F--G--H--K   <-- master
           /
I---------J   <-- for-transplanting

СинтИзящная «история файлов» вашего нового файла somefile.ext теперь можно найти, посмотрев на K, увидев, что файл существует в J, но не в H, и следуя этой ветви в обратном направлении.Файл существует в I и J и отличается, поэтому будет показан коммит J.Затем Git переходит к I.Файл не существует в несуществующем коммите до I, поэтому он явно отличается в I и показывается коммит I.Тогда нет больше коммита, к которому можно вернуться, поэтому git log останавливается.

Обратите внимание, что мы можем сделать I и J в RepoA напрямую.Или мы можем скопировать все коммиты RepoA (A-B-C-D) в RepoB, затем сделать I и J в RepoB, затем удалить все следы имен, которые привели к коммитам A-B-C-D.Неиспользуемые / не имеющие ссылок коммиты в конечном итоге уйдут по-настоящему (обычно через 30 дней), и тем временем вы их не увидите, и они не будут вас беспокоить;они просто займут немного места на диске.Реальное преимущество использования RepoC состоит в том, что мы можем экспериментировать там, и если что-то пошло не так, просто взорвать все это и начать все сначала.

Теперь у вас есть более сложная проблема

Наконец, если все скопировано правильно в целевой репозиторий, мне нужен чистый способ удалить (скрыть?) Исходные файлы из исходных репозиториев.

Тамне одинЕсть только грязные пути. Насколько грязный, или насколько грязный, зависит от ваших потребностей.

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

A--B--C--D   <-- master

с somefile.ext, впервые появившимся в B, оставшимся неизменным в D, а затем сохраненным с другим содержимым вD.

Поскольку файл не находится в A, вы можете сохранить коммит A.Но вы должны создать замену B', которая похожа на B - имеет те же метаданные, включая родительский A, что и раньше - но имеет сохраненный снимок, который пропускает файл:

A--B--C--D   <-- master
 \
  B'  <-- ??? (we'll get to this)

СделавB' из B, теперь вам нужно сделать новый коммит C', который похож на C за исключением двух вещей:

  • его родительский элемент B'вместо B и
  • он пропускает somefile.ext

Как только вы сделали эту копию C' из C, у вас есть:

A--B--C--D   <-- master
 \
  B'-C'  <-- ??? (we'll get to this)

Теперь вы должны скопировать D в D' таким же образом:

A--B--C--D   <-- master
 \
  B'-C'-D'  <-- ??? (we'll get to this)

и теперь пришло время перейти к , какое имя ветви идет в вопросительных знаках проблема.

Очевидная вещь, которую нужно сделать, это очистить имя ветви master от коммита D и вместо него указать D':

A--B--C--D   [abandoned]
 \
  B'-C'-D'  <-- master

Любой, кто придет сейчаси просмотр этого хранилища начнется с name master, чтобы получить хеш-код D'.Они даже не заметят, что D' имеет совершенно другой хэш-идентификатор, чем D.Они посмотрят на D' и вернутся к C', а оттуда к B', а затем обратно к A.

Ну, почти любому.Что если придет еще один Git ?Что если этот другой Git уже имеет A-B-C-D? Что Git имеет их и знает их по их хэш-идентификаторам.Хэш-идентификаторы являются универсальной валютой обмена Git.

Другие Git, которые могут прийти, - это любой клон, который вы сделали из исходного хранилища. Все клонов RepoA имеют оригинальные идентификаторы хэшей, перечисленные под собственным именем master.Теперь вы должны убедить всех этих клонов переключить их master с D на новую замену D'.

Если вы готовысделайте это - и они тоже - тогда у вас есть ответ: сделайте это с RepoA и заставьте всех переключатьсяЭто оставляет только необходимый механизм: как вы будете делать это с RepoA, и в этом отношении, как вы получите правильные коммиты в RepoC, если вы не сделаете это вручную?

git filter-branch

GiУ t есть встроенная команда, которая может сделать это: git filter-branch. Команда filter-branch работает путем копирования коммитов. Логически (но не физически, за исключением самого медленного фильтра, --tree-filter), то, что делает ветвь фильтра:

  • проверить каждый коммит;
  • применить ваши фильтры;
  • map родительский хеш исходного коммита в соответствии с пока что картой; и
  • построить новый коммит из отфильтрованного результата и ввести в карту.

Если новый коммит равен 100%, бит за битом идентичен оригинальному коммиту, то получается , являющийся исходным коммитом. Запись карты говорит, что коммит A остается коммитом A. Фильтр для фиксации B вносит изменения - он удаляет файл. Таким образом, родительский элемент для следующего коммита A (потому что A отображается на A), но новый коммит получает новый хэш-идентификатор B', и теперь карта говорит A = A но B = B'. Теперь происходит фильтр для C, удаляющий файл и делающий родителем нового коммита B', так что в результате получается новый коммит C', который входит в карту. Наконец, фильтр для D происходит, делая новый коммит D' с родителем C'.

Теперь, когда все коммиты отфильтрованы, git filter-branch использует встроенную карту для замены хеш-идентификатора, хранящегося в master. На карте написано, что D становится D', поэтому ветвь фильтра хранит хэш D' под именем master, и у нас есть то, что мы хотели.

Эту же технику можно использовать в RepoC. Помните, что RepoC является временным, где мы можем нанести любой ущерб, который нам нравится. Вместо удаления somefile.ext в нашем фильтре мы хотим удалить все , кроме somefile.ext. Мы также почти наверняка захотим аргумент --prune-empty.

То, что делает --prune-empty, достаточно просто описать. Давайте начнем с того, как все работает без --prune-empty. В процессе копирования каждый исходный коммит копируется в новый. Это верно , даже если новый коммит, после применения фильтров, не вносит изменений . Если у нас есть коммит, подобный C, который не трогает somefile.ext, он, вероятно, затрагивает другие файлы. (Обычно Git не позволяет вам делать два коммита подряд с одинаковым содержимым - для этого нужно использовать git commit --allow-empty.) Но если мы удалим все другие файлы ... хорошо, тогда у нас фактически есть B и C, являющиеся такими же , поэтому после того, как мы скопируем B в B', чтобы иметь только somefile.ext, мы будем скопируйте C в C', чтобы иметь только somefile.ext. Две копии будут совпадать. По умолчанию, ответвление фильтра в любом случае будет составлять C', так что C есть что сопоставить.

Добавление --prune-empty говорит Git: Не делайте C', просто сопоставьте C с B'. Когда мы делаем это, мы получаем именно то, что хотим: Git не делает t делает A' вообще, делает B' - что мы вместо этого называем I - из B с I, имеющим нет родителя, не make C' и делает D' - что мы называем J - из D, используя B', er I, в качестве родителя:

RepoC:

A--B--C--D   [abandoned]

   I-----J   <-- master

Что осталось сделать

Осталось только выяснить, как написать фильтры для git filter-branch. Вот о чем читают существующие ответы.

Простой фильтр для использования - --tree-filter. Когда вы используете этот фильтр, Git запускает ваш фрагмент скрипта во временной директории. В этом временном каталоге находятся все файлы из коммита, которые фильтруются (но у него нет каталога .git, и не ваше рабочее дерево!). Ваш фильтр просто должен изменить файлы на месте, или удалить некоторые файлы или добавить некоторые файлы. Git сделает новый коммит из того, что ваш фильтр оставляет во этом временном каталоге.

Это ятакже, безусловно, самый медленный фильтр. При использовании этого в большом хранилище будьте готовы подождать несколько часов или дней. (Это помогает использовать аргумент -d, чтобы указать git filter-branch на основанную на памяти «файловую систему», в которой можно выполнять всю свою работу, но она все еще очень медленная.) Поэтому большинство ответов сосредоточены на выяснении того, как Джиггер один из других, более быстрых фильтров для выполнения работы.

Вы можете работать с ними или использовать действительно медленный --tree-filter. В любом случае, если вы используете filter-branch, вы теперь знаете, что делаете и почему.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...