Ответ 2
Вы правы, что git checkout --orphan
создает только новые ветки-сироты. Фокус в том, что этот процесс не меняет индекс. Таким образом, Ответ Ника Волынкина будет работать, пока ваш Git не слишком древний.
Если вы хотите сохранить исходное сообщение фиксации, вы можете заменить его:
$ git commit -m'first commit in orphan'
с:
$ git commit -C master~2
Если ваш Git достаточно старый, что у вас нет git checkout --orphan
, это также должно сделать это:
$ commit=<hash> # or, e.g., commit=$(git rev-parse master~2)
$ git branch newbranch $( \
git log --no-walk --pretty=format:%B $commit | \
git commit-tree -F - "${commit}^{tree}" \
)
$ git checkout newbranch
$ git cherry-pick $commit..master # may want -x; see below
где вы выбираете начальную точку из git log
или используя синтаксис ~
с именем существующей ветки (это продолжает использовать master~2
, как в ответе Ника).
Если бы все, что вы хотели, было рецептом, это должно было сделать трюк, но если вы хотите знать, что происходит и почему это работает (а когда нет), читайте дальше.: -)
Вещи, которые вам нужно знать о ветвях
Прежде чем идти дальше, вам кажется, что это хорошая идея, чтобы определить некоторые элементы и описать, что происходит.
Имена ветвей и граф фиксации
Сначала дайте четкое различие между именем ветки, например master
или newbr
, и различными частями графа фиксации. Имя ветки просто указывает на одну фиксацию, обозначенную подсказкой наконечника или ветвью, внутри графика:
*--o--o---o--o <-- master
\ /
o--o--o--o <-- brA
\
o <-- brB
Этот граф имеет три ответвления, на которые указывают master
, brA
и brB
. Например, родословная вершины brB
восходит по вигглистой линии, всегда перемещаясь влево и иногда вверх, в (одиночный) корневой фиксатор *
(в отличие от всех остальных non-root o
> коммиты). Тот факт, что commit *
не имеет коммитов в его левую сторону - ни один из родителей не совершает привязку к точкам - это то, что делает его фиксацией root.
Этот корневой фиксатор находится на всех ветвях. Другие коммиты также находятся на нескольких ветвях. Там происходит объединение слиянием на master
, которое приносит коммиты из brA
внутри, даже если brA
имеет две коммиты, которые master
не выполняет, например. Чтобы следовать за master
назад к корню, вы должны идти прямо влево, а также вниз и влево при слиянии, а затем вернуться вверх и вниз, где brA
отпадает.
Обратите внимание, что мы можем иметь несколько названий ветвей, указывающих на одну фиксацию или имена ветвей, указывающие на "подсказку", которые встроены в другую ветвь:
*--o--o---o--o <-- master
\ /
o--o--o <-- brA
\
o <-- brB, brC
Здесь мы "перематываем" ветвь brA
с помощью одной фиксации, так что фиксация средней строки правой стороны является концом brA
, даже если она вернется с кончика brB
. Мы добавили новую ветвь brC
, которая указывает на тот же фиксат, что и brB
(сделав его совет дважды, давайте надеяться, что это коммит не является подсказкой в британско-английском "совете мусора", смысл слова: "тьфу, это совершение является абсолютным советом!" ).
DAG
Граф имеет ряд узлов o
, каждый из которых указывает на некоторые родительские элементы, которые обычно находятся слева. Линии (или стрелки, действительно), соединяющие узлы, являются направленными краями: односторонние улицы или железнодорожные линии, если хотите, соединяете дочерние узлы на графике с родителями.
Узлы, а также направленные ссылки ребер от дочернего к родительскому, образуют граф фиксации. Поскольку этот график направлен (от дочернего к родительскому) и ацикличен (после того, как вы отпустите node, вы никогда не сможете вернуться к нему), это называется D, направленным A циклический G raph или DAG. DAG имеют все виды хороших теоретических свойств, большинство из которых мы можем игнорировать для этого SO-ответа.
DAG могут иметь отключенные подграфы
Теперь рассмотрим этот альтернативный график:
*--o--o---o--o <-- master
\ /
o--o--o <-- brA
*--o--o--o <-- orph
Эта новая ветвь, совет которой называется orph
, имеет свой собственный корень и полностью отключен от двух других ветвей.
Обратите внимание, что несколько корней являются необходимым предварительным условием наличия (непустых) непересекающихся подграфов, но в зависимости от того, как вы хотите просмотреть эти графики, их может быть недостаточно. Если мы должны были слить (tip commit of) brA
в orph
1 мы получили бы это:
*--o--o---o--o <-- master
\ /
o--o--o <-- brA
\
*--o--o--o---o <-- orph
и два "фрагмента графов" теперь соединены. Однако существуют субграфы (например, те, которые начинаются с orph^1
и brA
, двух родителей orph
), которые не пересекаются. (Это не особенно важно для создания сиротских ветвей, это просто то, что вы должны понимать о них.)
1 Современный Git отвергает случайную попытку сделать такое слияние, так как у двух ветвей нет базы слияния. Старые версии Git выполняют слияние, не обязательно с разумными результатами.
git checkout --orphan
Разветвление orph
- это вид ветки, который git checkout --orphan
делает: ветвь, у которой будет новый, отключенный корень.
То, как это происходит, состоит в том, чтобы создать имя ветки, которое указывает на отсутствие фиксации вообще. Git называет это "нерожденной ветвью", а ветки в этом состоянии имеют только своеобразное полусуществование, потому что Git утечка реализации через.
Нерожденные ветки
Имя ветки, по определению, всегда указывает на фиксацию самого конца в этой ветки. Но это оставляет Git с проблемой, особенно в совершенно новом новом репозитории, который вообще не имеет никаких коммитов: где может master
указать?
Дело в том, что нерожденная ветвь не может указывать нигде, и потому что Git реализует имена ветвей, записывая их как имя < имя, commit-ID > пара, 2 он просто не может записать ветвь до тех пор, пока не будет фиксация. Git решение этой дилеммы - обмануть: имя ветки вообще не входит в записи ветки, а вместо этого только в запись HEAD
.
HEAD
, в Git, записывает текущее имя ветки. Для режима "отсоединенный HEAD" HEAD
записывает фактический идентификатор фиксации, и на самом деле это означает, что Git определяет, находится ли репозиторий/рабочее дерево в режиме "отсоединенный HEAD": если его файл HEAD
содержит имя ветки, оно не отделяется, и если оно содержит идентификатор фиксации, оно отключается. (Никакие другие государства не разрешены.)
Следовательно, для создания "сиротской ветки" или в течение этого неудобного периода, когда еще нет фиксации для master
, Git хранит имя в HEAD
, но на самом деле не создает имя ветки. (То есть для него нет записи .git/refs/heads/
, а для него нет строки в .git/packed-refs
.)
В качестве своеобразного побочного эффекта это означает, что у вас может быть только одна неродившаяся ветвь. Имя неродившегося ветки хранится в HEAD
. Проверка другой ветки с помощью или без --orphan
или любого фиксации по идентификатору - любое действие, которое обновляет HEAD
, - уничтожает все следы неродившегося ветки. (Новый git checkout --orphan
, конечно, заменяет его на след новой нерожденной ветки.)
Как только вы делаете первый фиксатор, новая ветвь возникает, потому что...
2 С помощью "распакованных" ссылок имя - это всего лишь путь в файловой системе: .git/refs/heads/master
. Идентификатор commit-is - это просто содержимое этого файла. Упакованные ссылки хранятся по-разному, а Git разрабатывает другие способы обработки сопоставления имен к идентификатору, но это самый простой и в настоящее время все еще необходимо для работы Git.
Существует два очевидных способа сохранить неродинные ветки, но Git не использует ни один из них. (Для записи это: создать пустой файл или использовать специальный "нулевой хеш". У пустой трюк файла есть очевидный недостаток: он будет очень хрупким перед лицом сбоев команды или компьютера, намного больше, чем использование нулевой хэш.)
Процесс фиксации
В общем, процесс создания нового commit в Git выполняется следующим образом:
-
Обновить и/или заполнить индекс, также называемый промежуточной областью или кешем: git add
различные файлы. Этот шаг создает объекты Git blob, которые сохраняют фактическое содержимое файла.
-
Запишите индекс в один или несколько объектов дерева (git write-tree
). Этот шаг создает или, в редких случаях, повторное использование, по крайней мере, одного (верхнего уровня) дерева. Это дерево имеет записи для каждого файла и подкаталога; для файлов он отображает blob-ID и для подкаталогов, он перечисляет (после создания) дерево, содержащее файлы подкаталогов и деревья. Обратите внимание, кстати, что это оставляет индекс невозмутимым, готовым к следующей фиксации.
-
Напишите объект фиксации (git commit-tree
). Этот шаг требует кучу предметов. Для наших целей основными интересными являются (единый) древовидный объект, который идет с этим фиксацией, - тот, который мы только что получили от шага 2, - и список идентификаторов родительских кодов.
-
Введите новый идентификатор фиксации в текущее имя ветки.
Шаг 4: как и почему имена ветвей всегда указывают на фиксацию наконечника. Команда git commit
получает имя ветки от HEAD
. Он также на этапе 3 получает первичный (или первый и только один) родительский код фиксации таким же образом: он читает HEAD
, чтобы получить имя ветки, затем считывает идентификатор фиксации наконечника из ветки. (Для слияния коммитов он считывает дополнительные родительские идентификаторы - обычно один - от MERGE_HEAD
.)
Git commit
знает, конечно, о нерожденных и/или сиротских ветвях. Если HEAD
говорит refs/heads/master
, но ветвь master
не существует... ну, тогда master
должна быть нерожденной ветвью! Таким образом, у этого нового коммита нет родительского идентификатора. Он по-прежнему имеет то же дерево, что и всегда, но это новый корневой фиксатор. Он по-прежнему получает свой идентификатор, записанный в файл ветки, который имеет побочный эффект создания ветки.
Следовательно, он делает первую фиксацию на новой сиротской ветке, которая фактически создает ветвь.
Вещи, которые вам нужно знать о вишневом пике
Git cherry-pick
команда очень простая в теории (практика иногда немного усложняется). Вернемся к нашим примерным графикам и проиллюстрируем типичную операцию вишневого захвата. На этот раз, чтобы поговорить о некоторых конкретных коммитах внутри графика, я дам им одноименные буквы:
...--o--o--A--B--C <-- mong
\
o--o <-- oose
Предположим, что мы хотели бы перевести commit B
из ветки mong
в ветвь oose
. Это просто, мы просто делаем:
$ git checkout oose; git cherry-pick mong~1
где mong~1
обозначает commit B
. (Это работает, потому что mong
обозначает commit C
, а C
parent - B
, а mong~1
означает "переместить один родительский фиксатор вдоль основной строки исходных ссылок. Точно так же mong~2
обозначает commit A
и mong~3
обозначает o
непосредственно перед A
и т.д. Пока мы не совершаем транзакцию слиянием, в которой есть несколько родителей, здесь все очень просто.)
Но как работает git cherry-pick
? Ответ: сначала он запускается git diff
. То есть он создает патч, который показан как git log -p
или git show
.
Задания имеют полные деревья
Помните (из нашего предыдущего обсуждения), что каждая фиксация имеет прикрепленный объект дерева. Это дерево содержит все исходное дерево, как и для этой фиксации: моментальный снимок всего, что было в области index/staging, когда мы сделали это commit.
Это означает, что commit B
имеет целое полное дерево работы, связанное с ним. Но мы хотим, чтобы вишня выбрала изменения, сделанные нами в B
, а не дерево B
. То есть, если мы изменили README.txt
, мы хотим получить сделанное изменение: не старую версию README.txt
, а не новую версию, просто изменения.
То, как мы находим это, состоит в том, что мы переходим от commit B
к его родительскому объекту, который является commit A
. Commit A
также имеет полное полное дерево. Мы просто запускаем git diff
на двух коммитах, что показывает нам, что мы изменили в README.txt
, а также любые другие изменения, которые мы сделали.
Теперь, когда у нас есть diff/patch, мы вернемся к тому, где мы сейчас находимся: наконечник конца ветки oose
и файлы, которые у нас есть в нашем дереве и в нашем индексе/промежуточной области, которые соответствуют к этой фиксации. (Команда git cherry-pick
по умолчанию отказывается запускаться вообще, если наш индекс не соответствует нашему дереву, поэтому мы знаем, что они одинаковы.) Теперь Git просто применяется (как и к git apply
), патч, который мы только что получили, с помощью различных коммитов A
и B
.
Следовательно, любые изменения, которые мы сделали, чтобы перейти от A
в B
, мы делаем их теперь, в наш текущий файл commit/index/work-tree. Если все идет хорошо, это дает нам измененные файлы, которые Git автоматически git add
относятся к нашему индексу; а затем Git запускает git commit
, чтобы сделать новую фиксацию, используя сообщение журнала из commit B
. Если мы запустили git cherry-pick -x
, Git добавляет фразу "cherry-pick from..." в наше новое сообщение журнала фиксации.
( Подсказка: обычно вы хотите использовать -x
. Это, вероятно, должно быть по умолчанию. Главное исключение - когда вы не собираетесь сохранять исходный коммит, который вы только что выбрали из вишни Можно также утверждать, что использование cherry-pick
обычно неверно - это указание на то, что вы сделали что-то неправильно раньше, на самом деле, и теперь им приходится печатать на бумаге, а перепечатка может не задерживаться в долгосрочной перспективе, - но это для другой [длинной] публикации полностью.)
Вишневый сбор в сиротской ветке
VonC отметил, что в Git 2.9.1 и более поздних версиях git cherry-pick
работает в сиротской ветке; в предстоящем выпуске он работает как для последовательностей, так и для отдельных коммитов. Но есть причина, по которой это было невозможно так долго.
Помните, что cherry-pick
превращает дерево в патч, отличая фиксацию против своего родителя (или, в случае слияния, родителя, который вы выбираете с опцией -m
). Затем он применяет этот патч к текущему фиксации. Но сиротская ветвь - ветвь, которую мы еще не сделали, - не имеет никаких коммитов, следовательно, нет текущей фиксации и, по крайней мере, в философском смысле - нет индекса и рабочего дерева. Просто нечего исправлять.
Фактически, мы можем (и Git теперь) просто обойти это полностью. Если бы у нас когда-либо была текущая фиксация - если бы мы кое-что проверили в какой-то момент - тогда у нас все еще есть индекс и дерево работы, оставшееся от последнего "текущего фиксации", всякий раз, когда мы его использовали.
Это то, что делает git checkout --orphan orphanbranch
. Вы проверяете некоторые существующие фиксации и, следовательно, заполняете индекс и дерево работы. Затем вы git checkout --orphan newbranch
и git commit
, а новый commit использует текущий индекс для создания или фактического повторного использования дерева. Это дерево - это то же дерево, что и сделанное вами чек, прежде чем вы сделали git checkout --orphan orphanbranch
. 3
Вот откуда взялась основная часть моего рецепта для очень старого Git:
$ commit=$(git rev-parse master~2)
$ git branch newbranch $( \
git log --no-walk --pretty=format:%B $commit | \
git commit-tree -F - "${commit}^{tree}" \
)
$ git checkout newbranch
Сначала мы найдем искомое коммит и его дерево: дерево, связанное с master~2
. (На самом деле нам не нужна переменная commit
, но запись ее так же позволяет нам вырезать и вставлять хэш из вывода git log
, не рассчитывая, как далеко назад он находится от кончика master
или какую ветвь мы будем использовать здесь.)
Использование ${commit}^{tree}
сообщает Git найти фактическое дерево, связанное с фиксацией (это стандартный gitrevisions
синтаксис). Команда git commit-tree
записывает новую фиксацию в репозиторий, используя это дерево, которое мы только что предоставили. Родитель нового коммита берутся из родительских идентификаторов, которые мы поставляем, используя параметры -p
: мы используем none, поэтому новый коммит не имеет родителей, т.е. Является корневым фиксатором.
Сообщение журнала для этого нового коммита - это то, что мы поставляем на стандартном входе. Чтобы получить это сообщение журнала, мы используем git log --no-walk --pretty=format:%B
, который просто печатает полный текст сообщения на стандартный вывод.
Команда git commit-tree
выводит в качестве своего вывода идентификатор новой фиксации:
$ ... | git commit-tree "master~2^{tree}"
80c40c288811ebc44e0c26a5b305e5b13e8f8985
(каждый прогон генерирует другой идентификатор, если все они не выполняются в тот же один-второй период, поскольку каждый из них имеет другой набор штампов времени, настоящий идентификатор здесь не очень важен). Мы приводим этот идентификатор git branch
, чтобы создать новое имя ветки, указывающее на это новое коммандное коммит, по мере того как его наконечник совершает.
Как только мы получим новый корневой фиксатор на новой ветке, мы можем git checkout
создать новую ветку, и мы готовы к зависанию оставшихся коммитов.
3 Фактически вы можете комбинировать их, как обычно:
git checkout --orphan orphanbranch master~2
который сначала проверяет (помещает в индекс и рабочее дерево) содержимое фиксации, идентифицированное master~2
, затем устанавливает HEAD
так, чтобы вы были на неродившейся ветке orphanbranch
.
Использование git cherry-pick
в сиротской ветки не так полезно, как нам бы хотелось
У меня есть более новая версия Git built (она терпит неудачу в некоторых своих тестах-умирает во время t3404-rebase-interactive.sh, но в остальном выглядит в основном ОК):
$ alias git=$HOME/.../git
$ git --version
git version 2.9.2.370.g27834f4
Пусть выберете с --orphan
, master~2
с новым именем orphanbranch
:
$ git checkout --orphan orphanbranch master~2
Switched to a new branch 'orphanbranch'
$ git status
On branch orphanbranch
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: a.py
new file: ast_ex.py
[snip]
Поскольку это новая ветвь, она выглядит как Git, как будто все новое. Если я сейчас попробую git cherry-pick
либо master~2
, либо master~1
:
$ git cherry-pick master~2
error: Your local changes would be overwritten by cherry-pick.
hint: Commit your changes or stash them to proceed.
fatal: cherry-pick failed
$ git cherry-pick master~1
error: Your local changes would be overwritten by cherry-pick.
hint: Commit your changes or stash them to proceed.
fatal: cherry-pick failed
Что мне нужно сделать, это очистить все, и в этом случае применение изменения от master~3
до master~2
вряд ли сработает или просто сделает начальный git commit
в любом случае, чтобы сделать новую команду root на основе дерева из master~2
.
Заключение
Если у вас есть git checkout --orphan
, просто используйте это, чтобы проверить целевую фиксацию oldbranch~N
(или с помощью идентификатора хэша, который вы можете вырезать и вставить из вывода git log
):
$ git checkout --orphan newbranch oldbranch~N
затем сделайте новый коммит немедленно, как Ник Волынкин сказал (вы можете скопировать его сообщение):
$ git commit -C oldbranch~N
чтобы создать ветвь; а затем используйте git cherry-pick
с oldbranch~N..oldbranch
, чтобы получить остальные фиксации:
$ git cherry-pick oldbranch~N..oldbranch
(И, возможно, используйте -x
, в зависимости от того, планируете ли вы лишить коммиты из oldbranch
.) Помните, что oldbranch~N..oldbranch
исключает сам commit oldbranch~N
, но это действительно хорошо, потому что тот, который мы сделали как новый корневой фиксатор.