Здравствуйте, Sharov, Вы писали:
S>Я не могу с topic-ветки на родительскую сделать rebase? git checkout parent, git pull; git checkout topic; git rebase parent
S>(или как-то так).
S>Что значит
S>S>после предварительного git rebase topic-ветки.
S>?
Почему не можете? Именно так я и делаю, и именно об этом говорил в первом пункте, после чего -- git push --force, конечно. У нас с topic-ветками разрешены любые операции как локально (впрочем, кто запретит-то?), так и удалённо (потому что никто не заморачивается над их защитой). Но запрещено делать слияние в master-ветку без предварительного перебазирования topic-ветки на master (для упрощения будем считать, что единственной родительской веткой является master). Сама механика такой защиты реализуется в git серверными хуками, следуя определёнными правилами, принимающими или отклоняющими измения в ветках (точнее
ссылках на фиксации) (правил множество: локально или удалённо пытаются подвинуть ветку; был ли это force-push; пытается ли кто-то откатить её назад; что там с топологией сливаемых изменений и т.д. и т.п., в чём может помочь git merge-base)).
Допустим, у меня локально было так (визуализацию ветки cool-feature для упрощения "согнул" вправо, хотя git log --graph выровняет её и прижмёт к левому краю, но, снова же, это не имеет значения):
@ "Cool feature change 2" (<- refs/heads/cool-feature; refs/remotes/origin/cool-feature)
* "Cool feature change 1"
* "Cool feature change 0"
/
* (<- refs/heads/master; refs/remotes/origin/master)
|\
| * "Basic feature change"
|/
*
Потом я узнаю, что на master-ветке произошли изменения и они были приняты на master (написали лично, пришло письмо, пропала кнопка "Merge" на том же GitLab). С этого момента, поскольку refs/remotes/origin/cool-feature больше не содержит фиксации, на которую в данный момент указывает refs/heads/master (с точки зрения удалённого репозитория), удалённый репозиторий не позволит принять изменения с ветки cool-feature на master. Локально синхронизируем новые изменения:
$ git checkout master
$ git pull # предпочитаю git fetch --prune && git merge --ff-only @{U}
@ "Merge basic-feature-fix into master" (<- refs/heads/master; refs/remotes/origin/master)
|\
| * "Basic feature fix"
|/
| @ "Cool feature change 2" (<- refs/heads/cool-feature; refs/remotes/origin/cool-feature)
| * "Cool feature change 1"
| * "Cool feature change 0"
|/
* "Merge basic-feature into master"
|\
| * "Basic feature"
|/
*
Делаем git rebase:
$ git checkout cool-feature
$ git rebase master
@ "Cool feature change 2" (<- refs/heads/cool-feature)
* "Cool feature change 1"
* "Cool feature change 0"
/
@ "Merge basic-feature-fix into master" (<- refs/heads/master; refs/remotes/origin/master)
|\
| * "Basic feature fix"
|/
| @ "Cool feature change 2" (<- refs/remotes/origin/cool-feature)
| * "Cool feature change 1"
| * "Cool feature change 0"
|/
* "Merge basic-feature into master"
|\
| * "Basic feature"
|/
*
Уведомляем удалённый репозиторий о своих изменениях:
$ git push -f
@ "Cool feature change 2" (<- refs/heads/cool-feature; refs/remotes/origin/cool-feature)
* "Cool feature change 1"
* "Cool feature change 0"
/
@ "Merge basic-feature-fix into master" (<- refs/heads/master; refs/remotes/origin/master)
|\
| * "Basic feature fix"
|/
* "Merge basic-feature into master"
|\
| * "Basic feature"
|/
*
После этого снова можем сливать свои изменения на удалённом репозитории, где условный GitLab под капотом у меня делает что-то типа такого:
$ git checkout "$TARGET_REF"
$ git merge --no-ff --no-squash "$SOURCE_REF"
... исполнение проверок в ./git/hooks/pre-merge-commit ...
$ test ! -z "$DELETE_SOURCE_REF" && git branch -d "$SOURCE_REF"
Тогда локально:
$ git checkout master
$ git pull
$ git branch -d cool-feature
@ "Merge cool-feature into master" (<- refs/heads/master; refs/remotes/origin/master)
|\
| * "Cool feature change 2" (<- refs/heads/cool-feature) (если ветка на удалённом репозитории не удалена и не было git fetch --prune)
| * "Cool feature change 1"
| * "Cool feature change 0"
|/
* "Merge basic-feature-fix into master"
|\
| * "Basic feature fix"
|/
* "Merge basic-feature into master"
|\
| * "Basic feature"
|/
*
По такой схеме легко увидеть некоторую закономерность. Поэтому:
* --no-ff -- создание отдельной фиксации для слияния ("Merge ... into master"), иначе git на удалённой машине просто переместит refs/heads/master на последнее изменение в cool-feature
__* мотивация 1: упрощение отслеживания времени сливания ветки, иначе имело бы тупо линейный вид как trunk в Subversion;
__* мотивация 2: отчёт лога изменений от версии к версии прямиком из merge-фиксаций с помощью тупого git log --format --merges (но это, к сожалению, если при git merge на удалённом репозитории можно указать свои сообщения)
* контраргумент: "нравится линейность, merge-фиксации засоряют граф" -- можно визуализировать историю с помощью git log --no-merges
* --no-squash -- запретить сжимать все фиксации между master и cool-feature в одно изменение перед слиянием
__* мотивация 1: отслеживание изменений на ветке по отдельности;
__* мотивация 2: возможность git revert отдельного изменения в случае необходимости, если каждая фиксация содержит логически атомарное и завершённое изменение
__* контраргумент: "мне нравится squash, так как он убирает весь промежуточный мусор" -- никто не запрещает регулярно пользоваться commit --amend или интерактивным rebase у себя на ветке по мере необходимости во время рабочего процесса, или позже самому сделать squashed-фиксацию с помощью git rebase -i и команд pick/fixup/squash.
Из минусов такого подхода могу назвать следующее:
* если есть множество открытых PR, каждый из них приходится перебазировать на только что обновлённый master вручную;
* автоматическая генерация сообщений вида "Merge ... into master" на удалённой системе, мягко говоря, малоинформативна, даже если включает в себя ссылки на тикеты в баг-трекерах и на pull-request-ы;
* на удалённом репозитории, если есть CI, это может занимать слишком продолжительное время;
* поскольку rebase это, в общем случае, о применении наборов патчей, со временем неизбежны конфликты с родительских веток в промежуточных изменениях, в то время, как merge просто будет пытаться слить только последние изменения на этих ветках (если не ошиюбаюсь, делая в некоторых случаях исключения) -- это общая проблема rebase, когда его сравнивают с merge; + ещё одна проблема rebase: если при git merge отрезолвили конфликт, при постоянном git rebase изменений, который ещё не слили в родительскую ветку, есть ненулевая возможность неправильно поправить конфликт и даже не заметить этого, что потом можно даже и не вспомнить (но, снова же, по-моему, это решается git rerere);
* иногда на master может попасть что-то нежелательное, что можно удалить только имея права прямой записи в такой master с локальной машины (например, на topic-ветке кто-то сделал commit, а тогда revert (вместо commit --amend, reset, rebase или любого другого способа, убирающего ненужное из истории), таким образом создав zero-diff из двух фиксаций, которые наверняка при review не заметят), при этом предупредив всех, что master придётся подправвить, что может повлиять на уже существующие изменения (как локально, так и удалённо, причём не важно в каком масштабе).
Понимаю, что выложил сумбурно, но как умею, и, надеюсь, это более развёрнуто отвечает на вопрос о применяемых практиках.