Давно хочу поделиться bzr-специфичным решением одной практической ситуации, возникающей при групповой работе над проектом.
Работая над общим кодом на локальной машине, иногда бывает нужно делать в нём строго локальные правки: пути к файлам, адреса серверов, отладочное логирование. При этом эти правки никогда не должны попадать в основной бранч проекта.
Часть из них можно и нужно вынести в отдельные локальные конфигурационные файлы, которые просто заигнорировать. Но вот с упомянутым отладочным логированием так не получится — это произвольный код, временно раскиданный по файлам. Ещё один пример такой правки — полное вырезание куска кода, который не критичен для работы, но не даёт проекту завестись с локальном окружении.
В bzr для этой проблемы мне известны два решения: одно прямое, другое — более удобное, с помощью плагина bzr-pipeline. Беда только в том, что в его документации описан несколько другой юзкейс, и я с первого раза вообще не понял, как оно мне поможет. Потом разобрался и решил восполнить пробел.
Не решение
Коротко остановлюсь на одном м-м-м... забавном способе. Можно локальные правки вообще не коммитить никогда, а вместо этого при каждом коммите вручную интерактивно откладывать их (shelve'ом) или всегда явно перечислять файлы в коммите, если вам повезло, и нужные изменения не затронули файлы с локальными правками.
По-моему, это страшно неудобно и прямо таки просится на ошибки от того, что что-то забудешь или наоборот вкоммитишь лишнего. Хотя кое-кто утверждает, что это нормально :-)
Прямое решение
Вы заводите два локальных бранча:
- "upstream", который вы периодически merge'ите с внешним бранчом, и из которого ваши полезные правки попадают во внешний бранч
- "local-patches", в который периодически merge'ится из upstream, но помимо его правок содержит ещё специфичные только для него локальные правки
С этими двумя бранчами удобней всего работать в git-стиле, имея одно рабочее дерево файлов, и переключаясь между двумя бранчами. Впрочем, всё равно неудобство от наличия двух бранчей остаётся: нужно помнить, что пишите и отлаживаете код вы в бранче local-patches, а вот коммитить его надо в бранч upstream, да потом ещё и merge'ить обратно:
$ bzr switch local-patches
... hack-hack-hack
... test-test-test
$ bzr switch upstream
$ bzr commit
$ bzr switch local-patches
$ bzr merge
$ bzr commit -m 'merged with upstream'
Впрочем, случайный коммит в local-patches — небольшая беда, потому что ему тут же можно сделать uncommit.
Ещё неудобство такой схемы проявляется, когда вам нужно merge'ить изменения извне. Их надо ручками прогнать в оба бранча, что довольно нудно делать каждый раз:
$ bzr switch upstream
$ bzr merge
$ bzr commit -m 'merged with trunk'
$ bzr switch local-patches
$ bzr merge
$ bzr commit -m 'merged with upstream'
Вот этот ворох и автоматизируется плагином bzr-pipeline.
bzr-pipeline
На момент написания статьи с версией bzr 2.0.x работает бранч lp:bzr-pipeline/stable, а lp:bzr-pipeline работает с версией bzr 2.1.x
Первое, что просит сделать документация bzr-pipeline — сделать из обычного бранча собственно pipeline:
$ bzr branch bzr+ssh://external-project upstream
$ bzr reconfigure-pipeline
Это не какой-то новый отдельный вид бранчей, как мне показалось в начале. На самом деле эта команда берёт ваш standalone бранч с рабочими файлами и автоматически делает, то что нужно было бы сделать руками для того, чтобы из него сделать git'ообразную среду:
- создаёт внутри .bzr новую дирекотрию pipelines, которая служит репозиторием для нескольких бранчей
- перетаскивает ваш бранч туда
- заново привязывает рабочую копию файлов к этому бранчу (получается lightweight checkout в терминах bzr)
Дальше надо от upstream сделать новый бранч local-patches:
$ bzr add-pipe local-patches
Вот на этом месте я не знаю, почему плагин настаивает на своей команде, потому что по идее то же самое должно делаться стандартным switch -b local-patches. Но видимо ему ещё нужен какой-то bookkeeping дополнительный.
После этого вы в local-patches коммитите локальные правки и работаете с этим бранчом в обычном режиме, не забывая, как и раньше, для нормальных коммитов переключаться в upstream. Здесь bzr-pipline предоставляет небольшое удобство: автоматизированную команду bzr pump
, которая merge'ит и автоматически коммитит изменения из текущего бранча (upstream) вниз по конвейеру (local-patches). Соответственно рабочий коммит выглядит так:
... hack-hack-hack ... test-test-test
$ bzr switch upstream
$ bzr commit -m '...'
$ bzr pump
$ bzr switch local-patches
А ещё крайне полезная команда bzr show-pipeline
, которая показывает, какой из бранчей сейчас активен.
Ну а главная вкусность происходит тогда, когда вам надо вмерджить по всем бранчам изменения извне. Это выглядит так:
$ bzr pump --from-submit
И всё. Команда "pump" merge'ит и автоматически коммитит изменения по всем бранчам. Ключик "--from-submit" нужен, чтобы она это делала не от текущего бранча (который обычно самый последний — local-patches), а с самого верху.
Gotcha
Небольшая gotcha с этим "--from-submit" заключается в том, что надо убедиться, что первый бранч (upstream) знает про свой submit-бранч, из которого он должен merge'иться. У меня в первый раз вышло так, что он собрался merge'иться из local-patches (!!!), а не из своего родителя.
Чтобы Базару это объяснить, надо один раз переключиться в upstream и сделать явный merge извне:
$ bzr switch upstream
$ bzr merge bzr+ssh://external-project --remember
Краткая справка
Как принято в документациях к плагинам, вот кратко всё то же самое, что написано выше:
# Сделать pipeline
$ bzr branch bzr+ssh://external-project upstream
$ bzr reconfigure-pipeline
$ bzr merge bzr+ssh://external-project --remember
# Локальные правки
$ bzr add-pipe local-patches
... patch
$ bzr commit -m 'local changes'
# Рабочий коммит
... hack-hack-hack ... test-test-test
$ bzr switch upstream
$ bzr commit -m '...'
$ bzr pump
$ bzr switch local-patches
# Merge извне
$ bzr pump --from-submit
Комментарии: 21
А Вы не пробовали просто избавиться от проблемы, вместо того, чтобы героически её решать?
Всё это по своей сути конфигурационная информация, и её просто не должно быть в коде. Вынесите её в базу данных, или конфиг-файл, а лучше в каталог ./config/ с файлами (один файл обычно содержит одну строку текста и по сути является одной конфиг-переменной - стиль сервисов DJB, с такими файлами очень удобно атомарно работать из любого ЯП, в т.ч. из sh-скриптов). И настройте bzr на игнорирование этих файлов, т.к. их содержимое всегда индивидуально для каждой системы (наша система контроля версий обрабатывает добавление/удаление файлов в каталоге ./config/ - т.е. добавление новых конфигурационных переменных в систему - но не изменение содержимого этих файлов).
что-то не верится, что в bzr нельзя сделать exclude some files.
разве bzr не позволяет держать два хранилища в одном каталоге? на случай, если за-exclude-нные файлы надо всё-таки версионировать.
У нас для таких целей просто подключается localsetting сразу после settings, в котором, при необходимости, выполняется переопределение указанного в settings. localsettings, естественно, в репозиторий не включается.
Так, я понял :-). Я не объяснил до конца юзкейс. Нет, это не локальная конфигурация. Вот, добавил в статью:
То есть это могут быть какие-то абсолютно произвольные патчи, а не заранее предусмотренные переменные.
Вот для наглядности diff того, чем у меня сейчас PSHB-хаб, работающий на сервере, отличается от основного кода:
Конфигом это не лечится. И оставлять это логирование навсегда я тоже не хочу.
А чем тебе логирование помешало?
+1 к предыдущему комментатору, я стараюсь все машинно-специфичные изменения изолировать в одном файле.
Конфиги для каждого сервера кладутся в source control, образец девелоперского конфига тоже. Для Django это, обычно,
settings.(dev|stage|prod).py
, для Rail —config/environments/(development|staging|production).rb
.Нужный для конкретной машины файл подключается симлинком (django) либо переменной окружения (Rails).
Иван, логгинг отлично настраивается в конфиг-файле. В данном примере просто ставим этому логгеру уровень INFO, если не нужна отладочная информация. Если логгер много где используется, а изменение нужно строго в одном месте — создаем вложенный логгер и меняем настройки уже ему.
Сначала отвечу про логирование.
Я в курсе, что логирование настраивается в конфиг-файле :-). Но настройка не заставит появиться отладочный логирующий код в том месте, где мне что-то вдруг стало интересно посмотреть. Там его надо реально написать. Идея о том, чтобы логировать на всякий случай всё меня никогда не увлекала, потому что а) бесполезно (если я знаю, где может быть ошибка, я бы её просто не сделал) и б) нечитаемо. И по этой же причине я не хочу оставлять отладочное логирование здесь после отладки. Зачем оно тем, кто будет смотреть в код через год?
Теперь по сути.
Логирование — это частный пример. Вот ещё один частный пример из жизни. У нас в Яндексе в Джанго-проектах вёрстка шапки делается не средствами шаблонов самой Джанго, а с подключением внешней бинарной библиотеки. И так вышло, что на одной локальной машине у человека эта библиотека просто не собиралась по неким временным, локальным, никому не интересным причинам. А код зависит от неё достаточно жёстко, и без неё проект просто не стратует.
Если следовать логике, которую уважаемые комментаторы пытаются продвигать, нам надо было залезть в код и весь его перерефакторить для того, чтобы он справлялся с отсутствием библиотеки. Это внесло бы в код реальную сложность, и увы, вероятнее всего, не пригодилось бы больше нигде и никогда (эта библиотека чудесно работает во всех остальных местах). Вместо этого мы просто взяли и вырезали всю шапку целиком из шаблона и выкинули библиотеку из зависимостей. Это и есть тот самый локальный патч, который никакой конфигурацией не предусматривается.
Обобщая всё это, я хочу сказать, что не вижу смысла бесконечно увеличивать общую гибкость (а следовательно — сложность) системы для обхода всевозможных частных случаев вместо того, чтобы просто хранить локальные патчи в отдельном бранче. Они ведь и для этого придуманы, в конце концов :-).
Да, ещё скажу, что такие случаи действительно редки, и обычно локальных конфигов хватает. Подозреваю, что именно поэтому меня все уговаривают, что это не нужно: вам, видимо, повезло ни разу не встретиться с такой ситуацией :-)
дада. Ещё кроме логгирования очень "весело" бывает закоммитить pdb.set_trace()
Подобный подход несет в себе мину замедленного действия.
Первое: Становится реальной ситуация "Но ведь на моей машине все же работает!" когда патч будет ошибочно добавлен в локальные патчи.
Второе: Появляется необходимость поддерживать локальные патчи в актуальном состоянии. Чем больше локальных патчей наберется, тем больше времени это потребует. Вполне реальна ситуация когда к коду придется вернуться после большого перерыва и после того как в этот код были внесены значительные изменения.
Ситуация когда разработчик не может исправить/настроить свое окружение и не может найти замену в случае аппаратного сбоя - вызывает удивление.
Совершенно согласен, что это неверный подход в долгосрочной перспективе. Но это может быть самым простым решением немедленной задачи. И "исправление" окружения — это вопрос не крутости разработчика, а целесобразности в данных условиях.
Решение может быть и самое простое, но это только на первый взгляд. Я бы в подобной ситуации написал заметку в стиле How to... в девелоперский вики или в деплоймент гайд.
Да и вопрос не в крутости, а в организации - я знаю куда перебраться когда возникают проблемы с виртуальной песочницей, я знаю кто должен решать эти проблемы и я знаю кто будет стимулировать решателей при задержках. Но при этом я должен знать целевую платформу - потому что проблемы на продакшене - проблемы которые не смогут решить - в порядке эскалации - СисОпы и КьюЭи - эти проблемы буду решать я и за мной уже никого нет.
Например, тот же логгинг можно вынести в отдельный модуль и минимизировать чендж до подключения модуля и декорирования интересных в данный момент методов. А отдельный модуль уже можно выложить в общий репозитарий. И написать инструкцию по употреблению - откуда сделать чекаут, как подключить и как использовать.
Идея локальных правок мне очень нравится (яляясь, по-моему, полезным дополнением, а не альтернативой - как многие здесь пытаются представить - локальных конфигов), но так сложилось, что предпочитаю Mercurial, а патч-кью я не использую. Это все-таки отдельный work flow, и испольовать его для отделения "локальных мух" кажется сомнительной затеей. Может быть кто-нибудь знает/использует более прямой hg-аналог, и может поделиться советом?
mq? Это, правда, superset, но сгодится. Если хочется попроще - можно attic, но mq в комплекте идëт.
Вообще я перед коммитом внимательно смотрю дифф того, что хочу закоммитить, поэтому у меня такой ситуации не возникает...
Кстати, вот закоммитили вы правки без логирования, а что дальше? Логирование у вас так и будет висеть локально? Все равно ведь когда-нибудь его откатите. Вопрос когда, почему не до коммита?
Greg, потому что оно там будет висеть некоторое время. И нормальных коммитов я успею сделать с десяток-другой. Очень не хочется каждый раз в ручную ханки из diff'ов выдирать, а потом обратно возвращать.
Когда пытаюсь выполнить bzr switch upstream те изменения, которые были сделаны в local-patches откатываются, и нет возможности закоммитить их в upstrem:
Поведение плагина изменили в версии 2.1?
М-м-м... А что такое "swp"?
У плагина есть своя команда "switch-pipe", и она ведёт себя именно так, сохраняя незакомиченные изменения вместе с тем pipe'ом, где они делаются. Именно поэтому я её не упомянул, потому что в моём юзкейсе нужен обычный switch, который незакомиченные изменения переносит в новый активный бранч.
Понял. Не обратил внимания, что в статье используется не пайплайновский свитч.
P.S. swp - алиас для switch-pipe, предопределенный в плагине.