TL; DR
Вы не можете получить то, что вы хотите - по крайней мере, без написания какого-то, по крайней мере, полуфантастического инструмента самостоятельно.
Возможно, вы сможете получить что тебе нужно легко. Вам придется подумать о том, что вам нужно, и решить, стоит ли пытаться написать полуфантастический инструмент.
Long
Git не имеет истории файлов . Git имеет коммитов , а коммиты являются историей. (Сравните, например, с ClearCase, у которого есть истинная история файлов, со всеми вытекающими отсюда последствиями.)
В Git каждый коммит имеет список предшественников или родительский коммит , и каждый коммит содержит полный снимок всех файлов. Таким образом, в вашем примере есть четыре коммита или, по крайней мере, четыре интересных . Здесь я предполагаю, что в общей сложности существует пять коммитов и мы можем нарисовать эти коммиты следующим образом:
A <-B <-C <-D <-E <--master
Имя master
содержит фактический га sh ID последнего коммита E
. Этот коммит содержит файлы. Он также содержит необработанный идентификатор ha sh его родительского коммита D
.
Для простоты, скажем, все коммиты содержат еще один файл с именем README.md
. Commit A
состоит только из этого README.md
, т. Е. Если мы git checkout
commit A
, мы получим рабочее дерево с одним файлом README.md
.
In commit B
, вы добавили файл с именем testfile.txt
. Вы сделали это с:
... create the file ...
git add testfile.txt
git commit -m "second commit"
Это сделало коммит B
, который указывал на существующий коммит A
. Коммит B
теперь содержит два файла: README.md
- запускается из коммита A
(и фактически повторно используется во внутреннем хранимом формате Git) и testfile.txt
.
Затем вы изменили копия рабочего дерева testfile.txt
, снова использовала git add
и запустила git commit
для создания коммита C
. Commit C
теперь указывает назад на commit B
; commit C
содержит как README.md
(все еще без изменений), так и новую версию testfile.txt
.
. На этом этапе вы запустили:
mkdir sub/module/path
git mv testfile.txt
git commit -m "fourth commit"
(или что-нибудь эквивалентное) для сделать коммит D
, который указывает на C
. Коммит D
содержит два файла: README.md
(все еще без изменений) и sub/module/path/testfile.txt
: файл с длинным именем с косой чертой в нем. содержимое второго файла совпадает с содержимым файла с более коротким именем в коммите C
, но имя другое.
Последнее, вы изменили файл рабочего дерева с именем testfile
каталог / папка рабочего дерева с именем sub/module/path
, использовал на нем git add
и запустил git commit
, чтобы сделать коммит E
. E
указывает на D
и содержит два файла.
Учитывая эту историю - эту серию коммитов - вы теперь сообщаете Git:
- Используйте имя
master
чтобы найти последний коммит. Для каждого коммита посмотрите на пару родитель-потомок и посмотрите, изменяет ли он файл с именем sub/module/path/testfile.txt
каким-либо образом:
- Если это так, выведите имя (ha sh ID) дочернего коммита, его сообщение в журнале и, возможно, также тип изменения файла.
- Если вид изменений - это переименовать , начните искать старое имя сейчас.
В любом случае, переместите на предыдущий коммит, если он есть. Остановитесь, когда у вас закончатся коммиты.
(Это ваша команда git log --follow -- sub/module/path/testfile.txt
.)
Теперь вы конвертируете в подмодуль. Подмодуль - это репозиторий Git.
Каждый последующий набор файлов подмодуля git checkout
будет находиться в подкаталоге sub/module/path
рабочего дерева суперпроекта, так что если подмодуль содержит коммит, содержащий файл с именем testfile.txt
, этот файл появится в sub/module/path/testfile.txt
. Если подмодуль содержит коммит, содержащий файл с именем sub/module/path/testfile.txt
, этот файл появится в sub/module/path/sub/module/path/testfile.txt
, а это не то, что вам нужно.
Поэтому ваша задача - сделать серию коммитов, которые являются новым хранилищем. В этой серии коммитов файл будет называться testfile.txt
. Этот новый репозиторий, вероятно, будет иметь все новые коммиты: в этом случае ни один из идентификаторов ha sh в этом новом хранилище не будет соответствовать ни одному из идентификаторов ha sh в исходном репозитории.
Это Вы выбираете, сохранять ли некоторые или все файлы из исходного коммита B
и, если да, что делать с тем фактом, что в коммите B
файл, который вам нужен, называется testfile.txt
, а не sub/module/path/testfile.txt
. Точно так же вы можете сохранить некоторые или все исходные файлы из коммита C
.
В любом случае вы сохраните части коммитов D
и E
более легким способом: просто выкиньте все, что не sub/module/path/
и убрать часть sub/module/path/
из имен файлов.
Если вы делаете сохраняете некоторые или все (файлы из) коммитов B
и / или C
, testfile.txt
в двух сохраненных коммитах должен быть назван testfile.txt
, чтобы он приземлился в нужном месте. Трюк с полосой-1158 * автоматически дает правильные имена для оставшихся коммитов.
Команда преобразования, которую вы используете для копирования оригинальной серии коммитов в новую серию коммитов can быть git filter-branch
со своим --subdirectory-filter
. Но фильтр подкаталогов не может оставить эти части коммитов B
и C
для вас. По сути, git filter-branch
и его фильтр подкаталогов просто не так умны. Ответвление фильтра делает для вас следующее:
- Начните с первого коммита и go в направлении вперед (это редко случается в Git потому что Git плох в этом: Git настоятельно предпочитает работать в обратном направлении).
Для каждого коммита:
- применить некоторые фильтры ;
- использовать результат, чтобы сделать новый коммит или полностью пропустить коммит;
- автоматически связывает new коммиты с новыми цепочками совершаемые коммиты, т. е. подставлять в правильные обратные ссылки.
Повторить для всех коммитов, ведущих к выбранным именам веток или всем именам ветвей, в качестве конечных коммитов.
Последнее, сохраните окончательный отфильтрованный commit ha sh ID в имени каждой ветви.
Если ваш ввод серия коммитов:
A--B--C--G <-- branch1
\
D--E--F <-- branch2
и ваш фильтр сохраняет B
(с некоторыми внесенными изменениями) и все последующие коммиты (возможно, с другими изменениями e) конечный результат:
A--B--C--G [abandoned]
\
D--E--F [abandoned]
B'-C'-G' <-- branch1
\
D'-E'-F' <-- branch2
Теперь, работая так, как обычно работает Git, начиная с имени branch1
и работая в обратном направлении, мы видим скопированные и отфильтрованные коммиты B'-C'-G'
(в другом порядке) и работая с branch2
мы видим B'-C'-D'-E'-F'
(в другом порядке). Итак, git filter-branch
теперь сделал свою работу. Если мы добавим sh новые коммит-цепочки и два имени в новый репозиторий, у нас будет репозиторий, который вообще не имеет коммита A
.
(Обратите внимание, что все исходные коммиты все еще существуют. Мы просто не можем увидеть их. Если мы снова клонируем этот отфильтрованный клон, они все выпадают и действительно исчезают, или мы можем удалить след от хлебных крошек, который оставляет ветвь фильтра, если вы захотите отмените эффект, и Git в конечном итоге очистит исходные коммиты.)
--subdirectory-filter
в ветви фильтра работает путем выбрасывания всех файлов, не входящих в выбранный префикс подкаталога, и переименование оставшихся файлов для удаления выбранного префикса. Если результатом выброса, но этих файлов является «никаких файлов вообще» или «такой же, как ранее урезанный коммит», сам коммит также удаляется. Но это отбрасывает копию testfile.txt
, которой не было в подкаталоге.
Обычно это то, что хочет , потому что исходный репозиторий все еще существует и все еще имеет этот файл в " пре-субмодуль "фиксирует. Вы не меняете эти коммиты; на самом деле, вы не можете изменить любой коммит, никогда. Вот почему Git делает все это копирование: буквально должно. Лучшее, что мы можем получить - это new , фиксирующее формирование новой истории, которую мы (и Git) находим, начиная с обновленных имен и работая в обратном направлении, как это делает Git.
Но это не то, что вы хотите. Этого может быть достаточно - это может быть все, что вам действительно нужно - в этом случае существующий фильтр подкаталогов будет работать для вас.
filter-branch имеет универсальную опцию "произвольный скрипт"
Вот эти два встроенных фильтра, которые git filter-branch
поддерживает:
- - индексный фильтр
- - древовидный фильтр
Оба эти принять команду в стиле командной строки для запуска. Эта команда может использовать любую программу, написанную вами на любом языке, или просто представлять собой последовательность команд оболочки. Основное различие между этими двумя понятиями - как они выполняют вашу команду - среда, в которой работают команды.
(Вместо этого вы можете использовать новую команду git filter-repo
, которая записана в Python и выполняет то же, что и фильтр-ветвь, но позволяет вам выполнять функции Python. У меня нет примеров того, как его использовать, и он еще не встроен в Git: вы должны установите его отдельно.)
Индексный фильтр намного быстрее, но также гораздо труднее писать. Чтобы понять, как его использовать, сначала нужно понять фильтр дерева.
Фильтр дерева прост в использовании. Перед выполнением команды tree filter ветвь фильтра выполняет то, что он извлекает весь снимок во временное дерево каталогов.
(Это временное дерево не является вашим рабочим деревом! Не ожидайте, что оно будет ваше рабочее дерево. Оно находится во временном подкаталоге, скрытом где-то, чего вы не ожидаете. Ничего не думайте об этом, за исключением того, что в нем есть все ваши файлы, и только ваши файлы из этого коммита извлечены в папки, однако ваша ОС требуется.)
Задача вашей команды теперь: делать с этими файлами все, что вам нравится . Вы можете редактировать их на месте, переименовывать их, изменять их разрешения, добавлять или удалять флаг «исполняемый файл» (chmod
их) и так далее. Все файлы, которые вы оставляете в этом дереве, будут go в замене, которую сделает ветвь фильтра. Таким образом, вы можете переименовывать и удалять файлы. Например, вы можете проверить, существует ли testfile.txt
на верхнем уровне, и если это так, оставить его на месте. Вы можете удалить все остальные файлы, которых нет в sub/module/path
, а затем переместить все файлы sub/module/path
на верхний уровень. Это было бы очень вероятно, что вы хотели бы получить в новом коммите замены, здесь.
Затем, выполнив все это, ваша команда должна получить sh статус успешного. Если вы пишете программу для выполнения работы, используйте функцию уровня ОС exit(0)
. Если это сценарий оболочки, такой как /tmp/shuffle-the-files.sh
, он завершается со статусом ноль.
Дерево-фильтр теперь скажет сам себе: Ах, команда выполнена успешно; Теперь я делаю новый коммит из набора файлов, которые остаются во скрытом временном каталоге.
Код ветви фильтра будет повторять этот процесс для каждого коммита в цепях, которые будут скопированы . Это может занять долго время: часы или дни. Но в конечном итоге у вас есть новые коммиты, сделанные путем копирования оригиналов, и git filter-branch
обновляет имена веток, как описано.
Фильтр индекса такой же, как фильтр дерева, но вместо:
- извлечение всего снимка во временную область
- выполнение произвольной команды
- превращение временной области в новый снимок
фильтр индекса использует Git индекс Git читает коммит для копирования в его индекс - как это было бы для обычного git checkout
, действительно - что очень быстро. Затем он запускает вашу команду. Задача вашей команды - обновить индекс на месте . Вы можете удалить или переименовать файлы в индексе, а затем выйти из нуля. Git затем делает новый замещающий коммит из того, что находится в индексе, что очень быстро. Таким образом, индексный фильтр обычно в сотни раз быстрее, чем древовидный фильтр.
К сожалению, единственный файл переименования в индексном инструменте, который существует в стандартном Git, - это git mv
, и он требует, чтобы файл также существовал в рабочем дереве, чего не будет. Итак, чтобы использовать индексный фильтр, вам придется проделать фантастическую работу git update-index
, что, вероятно, означает написание программы. Если у вас есть только несколько сотен коммитов или даже несколько тысяч, вам, вероятно, лучше использовать древовидный фильтр, который намного проще в использовании.
(Общая медлительность и сложность использования git filter-branch
, поэтому он постепенно сокращается в пользу git filter-repo
.)