git - зафиксировать поэтапные изменения в другой ветке и объединить - PullRequest
0 голосов
/ 20 декабря 2018

Я нахожу, что часто, работая над большой веткой функций, я делаю изменения в частях кодовой базы, которые действительно принадлежат их собственным ветвям.Я знаю, что мог бы использовать git add -p для внесения необходимых изменений, фиксации, сохранения ненужных изменений, создания нового ветвления из master, выбора вишни ранее сделанного коммита, возврата к оригиналупереходите, сбрасывайте, объединяйте в функциональную ветвь и высовывайте мои изменения, но это кажется большой работой для чего-то, что должно быть легче сделать.Должен быть способ сделать это, не затрагивая мою рабочую директорию, верно?

Вот чертеж того, что я пытаюсь сделать.

A crude drawing depicting branches

Я бы хотел иметь такую ​​команду, как

$ git commit --onto master --as new

, которая создаст new ветвь с master, зафиксирует изменения, затем объединит их с моей веткой HEAD, всене касаясь моего рабочего каталога.Существует ли такая команда?

1 Ответ

0 голосов
/ 20 декабря 2018

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

Предупреждение: это (а) долго и (б) все непроверено

(я пишу это как своего рода учебное упражнение, иллюстрирующее, как работает Git и как все его различные части можно соединить вместе, чтобы написать new Команды Git, в основном в виде сценариев оболочки.)

Помните, что когда вы запускаете git commit, Git создает новый коммит из всего, что у вас есть в index ,Индекс - это невидимая структура данных, похожая на кеш, которая занимает пространство между вашим HEAD (или текущим) коммитом и вашим рабочим деревом.

На чертеже пунктирный круг с меткой staged это коммит, который у вас будет иметь после того, как git commit превратит ваш индекс в дерево, а затем обернет этот объект дерева новым объектом коммита.Ваше рабочее дерево не используется в этом процессе (за исключением того, что кто-то запустил git commit --only или git commit --include, которые создают новые временные индексные файлы, а затем внутренне используют git add для копирования из работы.дерево в новый временный индекс, но давайте здесь избежим этой особой логики).

Разбивка процесса создания обычного коммита

Обычно вам не нужно знать все это:команда git commit позаботится обо всем этом.И на самом деле, вы можете использовать эту команду, за исключением того факта, что вы не хотите обычный коммит, вы хотите коммит слияния .Таким образом, нам нужно будет делать вещи вручную и / или идти по более длинному и более сложному маршруту.Давайте начнем с просмотра того, как git commit сделает новый коммит, который он сделает, если бы мы просто запустили git commit сейчас.

Обратите внимание, что каждый коммит содержит полный и полный снимок всех файлов.Пунктирный круг, помеченный staged , будет превращен в реальный коммит, который также автоматически обновит dev, что соответствует следующему процессу.Вся проверка ошибок была опущена для ясности.Здесь я предполагаю, что сообщение журнала доступно в переменной оболочки, хотя использование -F <em>file</em> также будет работать для получения сообщения журнала из файла.Мы немного разберем это после того, как рассмотрим четыре команды здесь, но посмотрим также отдельные страницы руководства для каждой команды:

current_branch=$(git symbolic-ref HEAD)  # will fail if HEAD is detached
tree=$(git write-tree)                   # will fail if, e.g., index is unmerged
commit=$(git commit-tree -p HEAD -m "$message" $tree)         # can fail
git update-ref -m "commit: $subject" $current_branch $commit  # can fail

Команда git symbolic-ref читает имя текущегофилиал от HEAD.Большинство операций Git получают хэш-идентификатор текущего коммита из HEAD, но нам нужно имя - в данном случае refs/heads/dev, поскольку вы находитесь на dev ветвь - для последнего шага.

write-tree упаковывает индекс как объект дерева.Это, по сути, навсегда замораживает содержимое файлов, которые сейчас находятся в индексе, в том виде, в котором они находятся сейчас.Результирующий объект дерева верхнего уровня подходит для нового коммита.

commit-tree создает объект коммита, который использует это замороженное дерево.Необходимо знать, что является parent нового коммита;это все, что предоставляет хеш-код HEAD, через -p HEAD.Это нуждается в сообщении журнала;для этого и нужен аргумент -m (или -F).И ему нужен хеш-идентификатор объекта tree , который входит в коммит;Вот для чего $tree.

(Коммит состоит из самого объекта фиксации, который только что написал git commit-tree, плюс объекта дерева, который написал git write-tree, плюс все объекты BLOB-объектов, которые являютсяуже в индексе, вместе с любыми поддеревьями, необходимыми для того, чтобы связать их все вместе, что git write-tree написал, когда написал дерево верхнего уровня.)

Этот делает коммитом, но текущая ветвь refs/heads/dev все еще называет старый текущий коммит - тот, который был текущим до того, как мы только что сделалиэтот новый коммит.Теперь мы должны исправить это, так что, хотя HEAD сам по-прежнему просто ссылается на refs/heads/dev, refs/heads/dev сам относится к new commit.Это заставляет новый коммит стать текущим коммитом!Команда Git для этого является последней из наших четырех команд git update-ref.Аргумент -m предоставляет сообщение для перехода в наш reflog.Обычная команда git commit использует в качестве этого сообщения журнала строку commit:, за которой следует тема (первая строка, более или менее) нашего полного сообщения журнала, поэтому мы помещаем ее в переменную оболочки $subject и используем ееВот.Также необходимо знать новый хэш-идентификатор для добавления в ссылочное имя, которое, конечно, является новым коммитом, который мы только что сделали, $commit из git commit-tree.

Вот что такое git commit прямо сейчас для вас будет : в ветке dev будет сделан обычный однополочный коммит, обновляющий имя ветки dev, чтобы он указывал на вновь созданный коммит.Новый коммит через свой древовидный объект навсегда замораживает содержимое всех файлов, находящихся в индексе.К сожалению, это не то, что вам нужно. Вам нужно, чтобы Git сделал новый коммит, тип которого не является обычным коммитом с одним родителем, а скорее типом, который является коммит слиянием: коммит с двумя родителями.Родитель first этого коммита слияния должен быть текущим (HEAD) коммитом, как обычно, но родитель second этого коммита должен быть new commitсделанный из ... хорошо, вот где это становится сложным.

Чтобы сделать ваш новый коммит слияния, вы должны сначала сделать свой новый другой коммит

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

Чтобы сделать этот коммит, мы должны построитьснимок в индекс того, как будут выглядеть все файлы.Обратите внимание, что здесь я говорю индекс , а не индекс .Мы начинаем поднимать некоторые осложнения!(Это тоже самое, что делают git commit --only и git commit --include.)

Поскольку Git построен на снимках , а не change-sets ,сначала мы должны превратить индекс current в набор изменений.То есть мы должны сравнить текущую фиксацию с индексом, чтобы увидеть, какие файлы мы здесь изменяем и что мы с ними делаем:

git diff-index --cached -p HEAD

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

Обратите внимание, что этот тип сравнения сравнивает дерево в HEADк дереву, представленному индексом / промежуточной областью.Он полностью игнорирует дерево в рабочем дереве.Это то, что мы хотим, потому что это то, что git commit совершит: что бы ни было в индексе.Мы хотим, чтобы все, что находится в индексе прямо сейчас, по сравнению с замороженным деревом в HEAD в форме патча.

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

При обычном использовании Git мы применили этот патч к этомудерево предназначено для извлечения дерева, связанного с вершиной master, в рабочее дерево 1166 *.Но это то, что вы не хотите.Более того, если не возникает неразрешимых конфликтов при применении этого патча, мы бы не хотели делать временным рабочим деревом вообще.

Тем не менее, давайте сначала немного разберемся с этим.

Использование добавленного рабочего дерева

Мы можемld, здесь используйте git worktree add, доступно с Git 2.5.Из-за довольно неприятной небольшой ошибки, разумно избегать хранить их более двух недель, если у вас нет достаточно современного Git, но наш план здесь, вероятно, заключается в том, чтобы использовать его не более чем на несколько секунд , так что все будет в порядке.Ошибка исправлена ​​в Git 2.15.

Добавленное рабочее дерево поставляется со своим собственным HEAD и собственным индексом.Он также предоставляет все пространство, необходимое для полного git apply -3 и разрешения конфликтов слияния.Таким образом, мы могли бы:

path=$(mktemp -d)
git worktree add -b new $path master

создать новую ветку с именем new, указывающую на тот же коммит, что и master, сохранив добавленное рабочее дерево в $path, который является новым временным каталогом.

Создав эту новую ветку в своем частном рабочем дереве, теперь нам нужно только применить только что извлеченный патч:

# this bit of clumsiness is due to the subshell problem
# (there are multiple workarounds, this one is simple)
status_file=$(mktemp)
echo fail > $status_file

git diff-index --cached -p --full-index HEAD | 
    (cd $path
    if git apply -3; then
        git commit -m "$message" && echo success > $status_file
    fi
    )

read status < $status_file; rm $status_file

case $status in
success)
    new_commit=$(cd $path && git rev-parse HEAD)
    git worktree remove $path
    ... finish up the job (see below) ...
    ;;
fail)
    echo "oops, sorry, things went wrong"
    echo "the mess is left in $path"
    echo "you will need to finish the merge and finish the job"
    ;;
esac

Команда git apply применяет патч.Флаг -3 указывает при необходимости использовать трехстороннее слияние.Я также добавил --full-index к операции git diff, чтобы мы получили полные хэш-идентификаторы в патче, что облегчает работу git apply, хотя технически это не нужно в современном Git (который обеспечивает достаточность строки индекса)- со старой версией Git --full-index требуется в больших репозиториях).

Обратите внимание, что мы могли бы использовать git cherry-pick здесь, а не git diff... | git apply.С технической точки зрения это было бы лучше, так как оно обрабатывало бы некоторые случаи переименования файлов, которые не может обработать метод сравненияНо мы смотрим на это без добавления рабочего дерева, и когда мы это сделаем, мы не сможем использовать git cherry-pick.

Используя временный индекс вместо добавленного рабочего дерева

Теперь мы можем указать Git использовать временный индекс, используя специальную переменную окружения GIT_INDEX_FILE.Здесь есть несколько особенностей: какой бы путь ни был в $GIT_INDEX_FILE, Git требует, чтобы файл либо не существовал , либо имел форму допустимого индекса .Таким образом, мы можем сделать это следующим образом:

tf=$(mktemp)
rm $tf

Это создаст временный файл с уникальным именем, а затем удалит его.Теперь $tf подходит для использования в качестве GIT_INDEX_FILE, потому что он называет файл, который не существует .

Мы можем поместить временный файл в каталог .git какхорошо:

tf=$(TMPDIR=$(git rev-parse --git-dir) mktemp)

но я думаю здесь не нужно.

Или мы можем позаимствовать метод, используемый git stash:

TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$

но замените stash на имя нашего собственного сценария, что бы это ни было - и я использую $tf ниже, а не TMPindex.Обратите внимание, что git rev-parse --git-path index сам по себе является новым в Git 2.13, поэтому, если ваш Git старше, не используйте этот метод.

Теперь, когда у нас есть временный индекс, мы можем указать различным командам Git использовать это вместо регулярного индекса.

Чтобы построить наш новый коммит, мы должны:

  1. Извлечь дерево из кончика master в этот индекс
  2. Примените наш патч к этому индексу, не касаясь какого-либо рабочего дерева.Это может не получиться!
  3. Используйте этот индекс для создания нового коммита, точно так же, как мы использовали обычный индекс для создания нового коммита в иллюстрации того, что происходит с обычным коммитом.Как только мы сделаем все это, мы будем готовы создать merge commit.

Игнорируя случаи сбоев, нам теперь нужно [ edit : perкомментарии, я опустил --full-index и -3 ниже;Режим --cached не может выполнить трехстороннее объединение]:

GIT_INDEX_FILE=$tf git read-tree refs/heads/master
git diff-index --cached -p HEAD |
    GIT_INDEX_FILE=$tf git apply --cached
tree=(GIT_INDEX_FILE=$tf git write-tree)
new_commit=$(git commit-tree -p refs/heads/master -m "$message_for_new" $tree)
git update-ref -m "$subect_for_new" refs/heads/new $new_commit

Команда read-tree извлекает дерево из заданного коммита - в данном случае кончик master - в индексный файл,который мы перенаправляем на наш временный индекс.

Команда diff-index - это то, что мы уже видели.Он использует реальный индекс.

В этот раз в команду apply добавлено --cached, поэтому он применяет изменения only к индексу, выполняет трехстороннее объединениеесли требуется .Мы используем временный индекс для этого.(Мы теряем способность выполнять правильное трехстороннее объединение, поэтому существует больше возможностей сбоев, чем раньше!)

ThКоманда e write-tree записывает временный индекс в дерево, которое теперь готово войти в коммит, а команда commit-tree превращает это дерево в коммит.Мы видели все это раньше - на этот раз разница в том, что родителем нового коммита является конец ветви master (refs/heads/master), и, конечно, у нас есть другое сообщение о коммите.update-ref создает или обновляет ветку с именем new - довольно грубо теряет любую предыдущую ветку с именем new, поэтому было бы разумно быть осторожным с этим или, возможно, не беспокоиться о ветке имя вообще (т. Е. Полностью исключить шаг git update-ref).

Создание коммита слияния

Теперь, когда у нас есть наш новый коммит, чей ID хеша у нас естьв переменной $new_commit мы готовы вернуться к нашей исходной последовательности из четырех команд, которая создает новый коммит для dev и затем обновляет dev.Чтобы создать этот новый коммит как коммит слияния , а не как обычный коммит, нам нужно дать ему только двух родителей.

Следовательно, снова игнорируя всю обработку ошибок, последовательность команд:

current_branch=$(git symbolic-ref HEAD)
tree=$(git write-tree)
commit=$(git commit-tree -p HEAD -p $new_commit -m "$message" $tree)
git update-ref -m "commit: $subject" $current_branch $commit

Собираем все вместе

Вот как выглядит полностью непроверенный, несколько опасный сценарий без обработки ошибок:

#! /bin/sh

current_branch=$(git symbolic-ref HEAD)
other_branch=refs/heads/master

message_for_new="magic new commit from script

I am a commit made by applying a diff.  I was made automatically by a script.
This is a terrible commit message, indicating that the script needs improvement."

message="new merge from script

I am a merge commit made by pretend-merging a magic commit made on ${other_branch#refs/heads/},
but actually using the staged files on ${current_branch#refs/heads/}.
This is a terrible commit message, indicating that the script needs improvement."

subject=$(printf '%s\n' "$message" | sed -n -e 1p)

# create a temporary index, and be sure to clean it up on exit
tf=$(mktemp); rm $tf; trap "rm -f $tf" 0 1 2 3 15

# create new ordinary commit via patch from current index
# this commit has $other_branch as its (single) parent
GIT_INDEX_FILE=$tf git read-tree $other_branch
git diff-index --cached -p HEAD |
    GIT_INDEX_FILE=$tf git apply --cached
tree=$(GIT_INDEX_FILE=$tf git write-tree)
new_commit=$(git commit-tree -p $other_branch -m "$message_for_new" $tree)

# create new merge commit on current branch, using this index and
# the commit just created above
tree=$(git write-tree)
commit=$(git commit-tree -p HEAD -p $new_commit -m "$message" $tree)
git update-ref -m "commit: $subject" $current_branch $commit
...