Вопрос философии Git: "родословная" и "история" (a.k.a. хронология) разные вещи? ...

Вот, у меня есть следующий репозиторий git:

      A---B---C develop
     /
D---E---F---G master

И я хочу, чтобы в голове мастера было именно то, что есть в его разработке:

      A---B---C develop
     /         \ 
D---E---F---G---C master

Кроме , я не хочу слияния. Я хочу, чтобы это было что-то вроде этого :

      A---B---C   develop
     /         \ 
D---E---F---G===C--- master

Где глава мастера НЕ будет иметь нескольких предков, он просто станет тем, чем является глава Develop. === в моей диаграмме подразумевает хронологическую связь, но не происхождение.

Для пояснения я не хочу использовать git merge -X theirs или git merge -r ours, потому что это все еще настоящие слияния ... Я просто не хочу слияния: я хочу "вставить". Однако мне также нужно иметь возможность просматривать журнал git на master и видеть коммит C, затем коммит G, затем коммит F и т. Д., Даже если между C и G. нет отцовских связей. Кроме того, мои ограничения запрещают мне от стирания мастера или развития, или возиться с их историями. Есть ли простой способ сделать это?

0
JCollier 3 Мар 2018 в 00:55

3 ответа

Лучший ответ

Вы можете достичь чего-то подобного, но это не так, как работает Git, и вы не должны этого делать.

Вы можете использовать git reset для перемещения master и перетащить в текущее состояние develop. Вам необходимо переместить ветвь master из ее текущего коммита G в нужный коммит C, а затем выполнить программный сброс master обратно к исходному коммиту {{X7 }}, перетаскивая текущее состояние C вместе с ним. Затем вы можете зафиксировать эти изменения, создав новый коммит (НЕ C) с изменениями, существующими в develop.

git checkout master
git reset --hard develop
git reset <commit id of C> # or git reset HEAD@{1}
# the working directory now contains the difference between C and G
git commit -m 'a message for C'

На этом этапе master и develop будут содержать идентичное содержимое, но с разными историями коммитов. График коммитов будет выглядеть так:

      A---B---C   develop
     /          
D---E---F---G---C' master

Где C' и C содержат идентичный контент, но расходящиеся истории.

Вместо этого вы можете сначала объединить, а затем выполнить шаги точно так же, как указано выше, и создать новый коммит, который содержит master и develop в качестве предков, но с содержанием C:

Мастер git checkout git merge развернуть сброс git --hard развернуть сброс git HEAD @ {1} git commit -m 'сообщение коммита для C'.

Это даст вам что-то вроде следующего, где M - коммит слияния:

      A---B---C       develop
     /         \ 
D---E---F---G---M---C' master

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

3
meagar 2 Мар 2018 в 23:28

Я не хочу слияния: я хочу "вставить". Однако мне также нужно иметь возможность просмотреть журнал git на master и увидеть commit C, затем commit G, затем F и т. Д., Даже если между C и G. нет никаких отцовских связей.

Это противоречиво. Вы разорвали родственные связи. Вы полностью отказались от истории.

Git checkout -B master develop

Это то, как вы отказываетесь от существующей истории master, заставляя master ссылаться на текущую историю develop.

Если вы хотите увидеть старую заброшенную историю в новой истории master, вы можете тогда

git merge -s ours master@{1}

Записать происхождение, не беря никакого контента, но вы либо записали происхождение, либо нет. Вы не можете разорвать свои родственные связи и иметь их тоже.

0
jthill 2 Мар 2018 в 22:24

Изменить: позвольте мне начать с вопроса философии (который я пропустил ранее).

Вопрос философии Git: "родословная" и "история" (a.k.a. хронология) разные вещи? ...

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

Каждый коммит имеет некоторый набор родителей: обычно только один родитель, но коммиты слияния имеют два или более, и по крайней мере один коммит в репозитории, называемый root коммит, имеет нет родители. Как правило, вы делаете новый коммит, выбирая существующий коммит, выбирая или создавая дерево (снимок) для перехода в новый коммит, и записывая новый коммит с помощью:

  • вы как автор и коммиттер, и «сейчас» как метки времени нового коммита;
  • ваше дерево как снимок;
  • сообщение произвольной фиксации; а также
  • идентификатор вашего текущего коммита - и, возможно, больше идентификаторов коммитов - как родитель (ы) нового коммита.

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

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

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

A <-B <-C

Commit C является последним (даже если его временная метка говорит, что он был сделан в 1970-х годах); commit B является родителем C; и root commit A является родителем B. Эти факты встроены в каждый коммит, который доступен только для чтения и не поврежден.

Git находит C через имя ветви, как master. C имеет некоторый хэш-идентификатор - криптографическую контрольную сумму содержимого C - и Git помещает этот хэш-идентификатор в хранилище значений ключей, причем ключом является имя master. Поэтому, учитывая master, Git находит C, который Git использует для поиска B, и так далее. Чтобы поместить новый коммит в ветку master, мы сейчас напишем некоторые новые вещи, запустим git add и git commit и получим новый коммит D с новым снимком, чей родитель - commit C. Каким бы ни был хеш-идентификатор D, Git запишет это в master, и теперь master будет указывать на новый коммит D.

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

Команда git log не обязательно всегда показывает ваши коммиты в порядке их появления на этом графике. Например, и, в частности, если git log имеет в своей очереди "commit to show" два или более коммитов, он должен выбрать one , чтобы показать первым. Он выбирается на основе критериев сортировки, указанных в командной строке git log. Однако, если он показывает только один коммит, он показывает этот. Показав, что один коммит, git log обычно добавляет его родителей (всех) в очередь, если они еще не были показаны.

По умолчанию git log показывает коммит HEAD, помещая этот (одиночный) коммит в очередь. Теперь в очереди только один коммит, поэтому Git удаляет его и показывает. Если это обычный коммит с одним родителем, вы увидите его, а затем git log поместит его (одного) родителя в пустую очередь. Затем Git покажет вам родителя: вы увидите коммиты в их графическом порядке, независимо от отметок времени коммитов.

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

(Обратите внимание, что вы можете настроить одну или обе метки времени для любого нового коммита в то время, когда вы делаете это, изменяя часы вашего компьютера, или более просто и воспроизводимо, устанавливая переменные среды GIT_AUTHOR_DATE и / или GIT_COMMITTER_DATE при запуске git commit. Некоторые команды, включая сам git commit, также принимают флаги --author-date и т. п.)

На остальной части вопроса

Как говорили вам несколько человек, вы не можете получить то, что хотите, в Git.

Более того, этот рисунок бессмысленный:

      A---B---C develop
     /         \
D---E---F---G---C master

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

Так же и этот рисунок по той же причине:

      A---B---C   develop
     /         \ 
D---E---F---G===C--- master

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

      A---B---C   <-- develop
     /         \
D---E---F---G---H    <-- master

То есть имя master выбирает коммит H, история которого включает в себя все коммиты, а имя develop выбирает существующий коммит C, история которого восходит к {{X4} }} в A, а затем обратно в E и так далее. Или, если вы предпочитаете, мы можем получить график:

      A---B---C   <-- develop
     /
D---E---F---G---H    <-- master

снимок , связанный с коммитом H, может быть тем, чем вы хотите. (Чтобы сделать такой снимок полностью вручную, вы можете работать в своем рабочем дереве, запустить git add, чтобы скопировать обновленные файлы рабочего дерева поверх их копий, находящихся в данный момент в индексе, а затем написать git write-tree для записи индекс для формирования дерева для моментального снимка H - но вам это, похоже, не нужно.)

Если вы хотите, чтобы commit H имел тот же моментальный снимок, что и commit C, так что git diff develop master вообще не производит никакого вывода, это особенно просто: мы можем использовать так называемый сантехник Git команды для создания коммита H из дерева для коммита C. Есть три шага, в конце концов:

vim /tmp/msg
# write appropriate log message to file /tmp/msg

Затем создайте новый коммит H и сохраните где-нибудь его хэш-идентификатор. (Я использую здесь переменную. Затем вы можете проверить ее, например, git log --graph $h, если хотите, прежде чем фактически использовать ее где-либо.)

h=$(git commit-tree -p master -p develop -F /tmp/msg develop^{tree})

Это создает коммит с двумя родителями, master и develop (то есть G и C) в этом порядке. Если вам нужен только один родитель, оставьте один -p и аргумент.

Наконец, если все выглядит хорошо, и вы находитесь на master и хотите сделать H коммитом, сбросом или ускоренным слиянием в H:

git checkout master      # if necessary
git merge --ff-only $h

Это обновляет текущее имя ветки, т. Е. Master, так, чтобы оно указывало на коммит $h (т. Е. Новый коммит H), и соответственно обновляет индекс и рабочее дерево, чтобы содержимое коммита {{ X2}} теперь занимают обычные постановочные и рабочие пространства.

(Между прочим, если вы делаете окончательный коммит H с двумя родителями и делаете первый родитель коммитом G, а второй коммит C, но используете контент commit C , это было бы эквивалентом git merge -s theirs, если бы он существовал. Это не так, но вы можете синтезировать его любым количеством способов, в том числе и выше. См. также ответ VonC здесь и некоторые соответствующие ссылки.)

2
torek 3 Мар 2018 в 06:59