Заменить все состояние приложения на состояние другого коммита - PullRequest
0 голосов
/ 19 мая 2019

Я хотел бы сделать "самую сложную версию" cherry-pick / merge / rebase / checkout, что означает, что состояние приложения в моей ветке начинает выглядеть точно так же, как в вишневом коммите (но с сохранением истории моей ветки). На самом деле я мог дублировать репо, удалить все в моей ветке и затем скопировать весь контент из дублированной версии, установленной на необходимый коммит. Но это не удобно, и я считаю, что есть более простой способ.

Я уже попробовал git cherry-pick <hash> --strategy-option theirs, но это не идеально, потому что он не удаляет файлы, не существующие в вишне-выбранном коммите, что приводит к большому беспорядку в моем случае.

Итак, как я могу это сделать?

Редактировать: Я уточнил, что мне нужно также сохранить свою историю, что было неочевидным в первую очередь.

Ответы [ 3 ]

3 голосов
/ 20 мая 2019

Это совсем не вишня. Не используйте git cherry-pick, чтобы сделать это: используйте git commit, чтобы сделать это. Вот очень простой рецепт 1 , чтобы сделать это:

$ git checkout <branch>                # get on the target branch
$ cd $(git rev-parse --show-toplevel)  # ensure you're at the top of the work-tree
$ git rm -r .                          # remove all tracked files from index and work-tree
$ git checkout <hash> -- .             # create every file anew from <hash>
$ git commit                           # make a new commit with all new info

Если вы хотите скопировать сообщение о коммите и тому подобное из коммита <hash>, попробуйте добавить -c <hash> в строку git commit.


1 Это не самое простое, но это должно быть понятно. Более простые используют сантехнические команды после начального git checkout, например ::

git read-tree -u <hash>
git commit

или

git merge --ff-only $(git commit-tree -p HEAD <hash>^{tree} < /tmp/commit-msg)

(не проверено и для второго вам нужно будет создать сообщение коммита).

Long

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

Когда у вас есть хэш-идентификатор какого-либо коммита, мы говорим, что вы указываете на коммит. Если один коммит имеет хэш-идентификатор другого коммита, коммит с хэш-идентификатором указывает на другой коммит.

Это означает, что эти хэш-идентификаторы, встроенные в каждый коммит, образуют обратную цепочку. Если мы будем использовать отдельные буквы для фиксации или нумеруем их по порядку C1, C2 и так далее, мы получим:

A <-B <-C <-D ... <-G <-H

или

C1 <-C2 <-C3 ... <-C7 <-C8

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

Место, где у нас есть Git, хранит эти хэш-идентификаторы в названиях ветвей . Таким образом, имя ветви, такое как master, просто хранит реальный хеш-идентификатор коммита H, тогда как H сам хранит хеш-идентификатор своего родителя G, в котором хранится хеш-идентификатор его родителя F, и т. Д. на:

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

Эти обратные ссылки, от H до G до F, плюс снимки, сохраненные с каждым коммитом, плюс метаданные о том, кто сделал коммит и почему, являются историей в ваш репозиторий. Чтобы сохранить историю, которая заканчивается на H, вам просто нужно убедиться, что следующий коммит, когда вы его делаете, имеет H в качестве родителя:

...--F--G--H--I   <-- master

Создавая новый коммит, Git меняет имя master, чтобы запомнить хэш-идентификатор нового коммита I, чей родитель H, чей родитель (все еще) G и т. Д.

Ваша цель - сделать коммит I, используя снимок , связанный с другим коммитом, например K ниже:

...--F--G--H   <-- master
   \
    J------K------L   <-- somebranch

Git фактически создает новые коммиты из всего, что есть в index , а не из дерева исходных текстов. Итак, мы начинаем с git checkout master, чтобы сделать commit H текущим коммитом и master текущим ответвлением, которое заполняет индекс и рабочее дерево из содержимого commit H.

Далее мы хотим, чтобы индекс соответствовал commit K - без других файлов, кроме тех, которые находятся в K - поэтому мы начнем с удаления каждого файла из индекса. Для здравомыслия (то есть, чтобы мы могли видеть, что мы делаем), мы позволяем Get делать то же самое с рабочим деревом, что он делает автоматически. Поэтому мы запускаем git rm -r . после того, как убедимся, что . относится ко всей паре индекс / рабочее дерево, убедившись, что мы находимся на вершине рабочего дерева, а не в некотором подкаталоге / подпапке.

Теперь в нашем рабочем дереве остаются только неотслеживаемые файлы. Мы также можем удалить их, если захотим, используя обычные rm или git clean, хотя в большинстве случаев они безвредны. Если вы хотите удалить их, не стесняйтесь делать это. Затем нам нужно заполнить индекс - рабочее дерево снова отправляется в поездку - из коммита K, поэтому мы запускаем git checkout <hash-of-K> -- .. -- . важен: он сообщает Git не switch commits, просто извлеките все из указанного здесь коммита. Наш индекс и рабочее дерево теперь соответствуют commit K.

(Если в commit K есть все файлы, которые есть в H, мы можем пропустить шаг git rm. Нам нужно только от git rm до удалить файлы, которые находятся в H, но не в K.)

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

Если вы хотите объединить, используйте git merge --no-commit

Приведенная выше последовательность приводит к:

...--F--G--H--I   <-- master
   \
    J-------K-----L   <-- somebranch

, где сохраненный снимок источника в коммите I в точности совпадает с коммитом K. Тем не менее, история генерируется чтением master, обнаружением, что оно указывает на I, а затем чтением commit I и обратно в H и G и F и так далее, никогда упоминает коммит K вообще.

Вместо этого вам может потребоваться история, которая выглядит следующим образом:

...--F--G--H--I   <-- master
   \         /
    J-------K-----L   <-- somebranch

Здесь коммит I возвращается к и коммитам H и K.

Создание этого варианта коммита I немного сложнее, потому что кроме использования git commit-tree сантехнической команды, единственный способ make commit I - это использование git merge.

Здесь проще всего запустить git merge -s ours --no-commit, например:

$ git merge -s ours --no-commit <hash>  # start the merge but don't finish it
$ git rm -r .                           # discard everything
$ git checkout <hash> -- .              # replace with their content
$ git commit                            # and now finish the merge

Мы используем -s ours здесь, чтобы все шло быстрее и плавнее. То, что мы строим, действительно является результатом git merge -s theirs, за исключением того факта, что git merge -s theirs нет. -s ours означает игнорировать их коммит, просто сохранить содержимое нашего коммита H. Затем мы выбрасываем это и заменяем его контентом из их коммита K, и затем мы завершаем слияние получить коммит слияния I, который указывает на H и K.

Как и прежде, есть хитрости командных команд, которые делают это еще проще. Они просто не очевидные , если вы не понимаете формат хранения низкого уровня, который Git использует внутри себя. Метод «удалить все, затем проверить содержимое другого коммита» действительно очевиден и его легко запомнить.

2 голосов
/ 19 мая 2019

Вам нужно использовать git reset --soft. Во-первых, слияние ... не имеет значения стратегия или результаты. Если у вас есть конфликты, это не имеет значения ... добавьте все файлы и сохраните (не волнуйтесь ... мы исправим нужный контент всего за секунду).

Теперь проверьте версию, из которой вы хотите получить контент. Если это ветка, используйте --detach. Теперь git reset --soft к исходной ветке (где мы делали слияние ранее).

А теперь, чтобы закончить трюк, бегите git commit --amend --no-edit и все готово. Если вам понравились результаты, переместите указатель ветви исходной ветви с помощью git branch -f the-original-branch, и все готово.

Оригинал, без сохранения истории другой ветки Что вы хотите сделать, это git reset --soft. Посмотрите ревизию (или ветку с --detach), которую вы хотите, чтобы другая ветка выглядела (по содержанию). Тогда сделай git reset --soft the-other-branch. Это установит указатель ветки на эту ревизию или ветвь, рабочее дерево будет точно таким же, как и ранее, и все различия между ними будут в индексе. Теперь, когда вы фиксируете, вы получите единственную ревизию, в которой ветвь будет выглядеть так же, как оригинальная, которую вы извлекли первой ... если вам нравится конечный результат, переместите указатель ветки и проверьте его.

0 голосов
/ 19 мая 2019
$ git checkout --orphan new-branch-name
$ git cherry-pick <hash>
...