Ветки и хирургия

Стеки веток

Стек — это цепочка веток, где каждая надстраивается над предыдущей: main → api → ui. Ревьюить три маленьких пул-реквеста лучше, чем один гигантский.

Стек веток

Gitcito рисует его как маршрут: сверху стартовая ветка, ниже по одной остановке на уровень. PR каждой остановки нацелен на остановку выше, а первая приземляется на стартовую ветку. Остановка показывает свои коммиты, нужен ли ей restack и — после отправки — номер PR.

Правка маршрута

Ничего не выполняется, пока вы не нажмёте «Применить». Выбрать ветку, подвинуть остановку, убрать её с маршрута — всё это правит список на экране. Настоящая операция перебазирует ветки и переключается на них, а этого не должен делать любопытный клик. Когда маршрут читается верно, Применить маршрут выполняет его одним отменяемым шагом; Отменить возвращает рисунок к тому, что говорит репозиторий.

Маршрут нарисован в порядке слияния: верхняя ветка вливается в ту, что ниже, вплоть до ветки, на которую приземляется стек.

Элемент Что делает
Поле Старт Куда приземляется стек. Поменяйте — и вся цепочка перевесится на новую ветку и переиграется.
Поле остановки Меняет, какая ветка занимает это место. Ветка, сошедшая с маршрута, только отвязывается, но не удаляется.
↑ / ↓ Двигает остановку на одну позицию.
✕ Снимает остановку с маршрута; соседи смыкаются.
Добавить остановку Выберите ветку, которая у вас уже есть — она встанет на вершину маршрута, — или введите ещё не существующее имя: ветка будет создана на вершине последней остановки и переключена.
Кнопка со стрелкой Переключает на эту остановку.

Все поля — с подсказками по мере ввода: печатайте, чтобы отфильтровать, ↑/↓ и Enter, чтобы выбрать, а введённое вне списка тоже засчитывается — так что удалённая ссылка вроде origin/main годится как стартовая ветка.

Под капотом все эти правки — одна и та же операция: маршрут целиком, отданный разом. Поэтому один жест — одна отмена (⌘Z), а не след из наполовину применённых связей.

Чего стоит правка маршрута

Всё, что меняет порядок — обмен, сдвиг, другой старт, — переигрывает цепочку: собственные коммиты каждой остановки перебазируются на новую основу. Значит, может быть конфликт, ровно как при restack. Две остановки, трогающие одни и те же строки, не поменяются местами без человека, и тогда ничего не происходит: вся правка откатывается — вершины, родительские связи и незаконченный rebase, — а Gitcito называет две столкнувшиеся остановки. Задетый мышью список не должен бросать вас посреди rebase.

Restack — вторая половина сделки: это rebase, о котором вы попросили прямо, поэтому он останавливается на конфликте и отдаёт вид разрешения — а это и есть способ получить перестановку, в которой Gitcito отказал: разрешите там, потом двигайте остановку.

Отмена переигрывает прежний маршрут. Она не воскрешает старые коммиты: новые — та же работа с другими родителями.

Отправить всё

Отправить всё отправляет каждый уровень с --force-with-lease и на этом останавливается — это gh stack push, ничего не открывающий. Отправить стек как PR делает тот же push, а затем работу с PR; берите Отправить всё, когда ветки нужны на удалённом, а до ревью ещё далеко.

Отправка стека связанными пул-реквестами

Подать сначала спрашивает: сколько pull request’ов откроет, сколько перенацелит, на каком удалённом репозитории и по строке ветка → база на каждый — открытие PR публично и отменяется тяжело. Раздел навигации, вписанный в каждое описание, и есть то, что делает цепочку видимой на GitHub, который о стеках не знает ничего.

Отправка происходит на отдельном экране: сначала план, потом прогресс по шагам, а в конце — ссылки на каждый pull request и на сам стек.

Отправка стека

Отправить стек как пул-реквесты делает в один клик то, за что инструменты для стеков берут деньги:

  1. Пушит каждый уровень с --force-with-lease (свежим веткам это не мешает, а перестроенным — необходимо).
  2. Открывает пул-реквест для каждого уровня, где его нет — каждый на базе родительской ветки, а не main, поэтому каждое ревью показывает только свои коммиты. Заголовок и описание берутся из коммитов самого уровня.
  3. Перенацеливает существующие пул-реквесты, чья база уехала.
  4. Записывает в тело каждого пул-реквеста раздел навигации по стеку, чтобы ревьюер на любом уровне видел всю цепочку и место этого пул-реквеста в ней.

Действие идемпотентно: нажимайте его после каждого перестроения, нового уровня или слитого пул-реквеста, и оно сходится — ничего не дублируется, трогается только то, что уехало.

Когда нижний пул-реквест слит, та же кнопка убирает за ним: потомок слитого уровня переподвешивается на основную ветку, уровень убирается из стека, его локальная ветка удаляется (это безопасно — основная ветка доказуемо её содержит), цепочка перестраивается, а все оставшиеся пул-реквесты перенацеливаются. Сливайте снизу вверх, нажимайте «Отправить», повторяйте.

На GitHub это ещё и настоящий стек

Сцепленные базы понимает любой хостинг, а на GitLab, Bitbucket и Azure DevOps это всё, что есть. У GitHub больше: с выходом превью stacked pull requests стек стал объектом на сервере. Как только pull request’ы созданы, Gitcito регистрирует их как стек — снизу вверх, — и вы получаете карту стека в интерфейсе PR, каскадный rebase на стороне сервера и merge верхнего PR, который приземляет все уровни под ним.

Если репозиторий не в этом превью или токен не умеет управлять стеками, вызов тихо пропускается: цепочка и её раздел навигации держатся сами, ровно как на других хостингах.

Перестроение стека

Когда меняется нижняя ветка — вы поправили замечания ревью в api — все ветки над ней оказываются построены не на той базе. Перестроить каскадно перебазирует всю цепочку через rebase --onto, так что переписывание родителя не дублирует коммиты в его потомках. После перестроения нажмите Отправить ещё раз: переписанные уровни будут запушены принудительно, и пул-реквесты обновятся на месте.

Ограничения

  • Отправка пока работает только с GitHub (создание работает на всех четырёх хостах, но перенацеливание и обновление тела требуют API GitHub).
  • Уборка после слияния низа видит обычные слияния и слияния через ребейз по происхождению, а squash-слияния — спрашивая GitHub, приземлился ли пул-реквест ветки; так что с токеном GitHub убирается любой стиль слияния. На других хостах или без токена уровень, слитый через squash, всё ещё придётся убрать из стека вручную. И сначала сделайте fetch — проверка происхождения читает основную ветку по состоянию на ваш последний fetch.
  • Раздел стека в теле пул-реквеста поддерживается между скрытыми маркерами — ваше собственное описание над ним сохраняется.
  • Переупорядочивание и смена ствола переписывают историю на каждом затронутом уровне. Ветки ваши, и неотправленные уровни ничего не стоят, но уровень, уже находящийся на ревью, получит force-push при следующей отправке.
  • Уровень сдвигается на одну позицию за раз. Два обмена — это два rebase, и остановка на полпути остаётся читаемым состоянием; перетаскивание сразу на три позиции — нет.
  • Остановка перебазируется, поэтому ветка, на которую приземляется стек, никогда не бывает ещё и остановкой — как и защищённая ветка (main и master, пока вы не поменяли список). И то и другое отклоняется, а не переписывает молча общую историю.
  • Перед тем как что-то открывать, отправка спрашивает удалённый репозиторий, какие ветки действительно дошли, и называет недостающие. На отсутствующий head GitHub отвечает сухим «Validation Failed», от которого никому не легче. Ветку, на которую приземляется стек, тоже проверяют: если она только локальная, отправка предложит отправить её и продолжить.

Где хранятся связи

Ссылки на родителя хранятся в конфигурации git, поэтому они путешествуют вместе с репозиторием и переживают повторное клонирование. Ничего не лежит ни в каком сервисе.

См. также: Интерактивный ребейз · Хостинги и пул-реквесты

Улучшить этот перевод на GitHub