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

Работая над общим кодом на локальной машине, иногда бывает нужно делать в нём строго локальные правки: пути к файлам, адреса серверов, отладочное логирование. При этом эти правки никогда не должны попадать в основной бранч проекта.

Часть из них можно и нужно вынести в отдельные локальные конфигурационные файлы, которые просто заигнорировать. Но вот с упомянутым отладочным логированием так не получится — это произвольный код, временно раскиданный по файлам. Ещё один пример такой правки — полное вырезание куска кода, который не критичен для работы, но не даёт проекту завестись с локальном окружении.

В bzr для этой проблемы мне известны два решения: одно прямое, другое — более удобное, с помощью плагина bzr-pipeline. Беда только в том, что в его документации описан несколько другой юзкейс, и я с первого раза вообще не понял, как оно мне поможет. Потом разобрался и решил восполнить пробел.

Не решение

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

По-моему, это страшно неудобно и прямо таки просится на ошибки от того, что что-то забудешь или наоборот вкоммитишь лишнего. Хотя кое-кто утверждает, что это нормально :-)

Прямое решение

Вы заводите два локальных бранча:

С этими двумя бранчами удобней всего работать в 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'ообразную среду:

Дальше надо от 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

  1. Powerman

    А Вы не пробовали просто избавиться от проблемы, вместо того, чтобы героически её решать?

    строго локальные правки: пути к файлам, адреса серверов, отладочное логирование

    Всё это по своей сути конфигурационная информация, и её просто не должно быть в коде. Вынесите её в базу данных, или конфиг-файл, а лучше в каталог ./config/ с файлами (один файл обычно содержит одну строку текста и по сути является одной конфиг-переменной - стиль сервисов DJB, с такими файлами очень удобно атомарно работать из любого ЯП, в т.ч. из sh-скриптов). И настройте bzr на игнорирование этих файлов, т.к. их содержимое всегда индивидуально для каждой системы (наша система контроля версий обрабатывает добавление/удаление файлов в каталоге ./config/ - т.е. добавление новых конфигурационных переменных в систему - но не изменение содержимого этих файлов).

  2. http://sash-kan.blogspot.com
    1. что-то не верится, что в bzr нельзя сделать exclude some files.

    2. разве bzr не позволяет держать два хранилища в одном каталоге? на случай, если за-exclude-нные файлы надо всё-таки версионировать.

  3. evasive.ru

    У нас для таких целей просто подключается localsetting сразу после settings, в котором, при необходимости, выполняется переопределение указанного в settings. localsettings, естественно, в репозиторий не включается.

  4. Ivan Sagalaev

    Так, я понял :-). Я не объяснил до конца юзкейс. Нет, это не локальная конфигурация. Вот, добавил в статью:

    Часть из них можно и нужно вынести в отдельные локальные конфигурационные файлы, которые просто заигнорировать. Но вот с упомянутым отладочным логированием так не получится — это произвольный код, временно раскиданный по файлам. Ещё один пример такой правки — полное вырезание куска кода, который не критичен для работы, но не даёт проекту завестиcь с локальном окружении.

    То есть это могут быть какие-то абсолютно произвольные патчи, а не заранее предусмотренные переменные.

  5. Ivan Sagalaev

    Вот для наглядности diff того, чем у меня сейчас PSHB-хаб, работающий на сервере, отличается от основного кода:

    === modified file 'views.py'
    --- views.py    2010-02-17 16:10:45 +0000
    +++ views.py    2010-02-17 23:02:21 +0000
    @@ -1,17 +1,29 @@
    +import logging
    +
     from django.views.decorators.http import require_POST
     from django.views.decorators.csrf import csrf_exempt
     from django import http
    
     from subhub import models
    
    +log = logging.getLogger('subhub.views')
    +
     def _get_verify(verifies):
         for v in verifies:
             if v in ('sync', 'async'):
                 return v
         raise KeyError('hub.verify is missing or not recognized')
    
    +def log_request(func):
    +    def wrapper(request, *args, **kwargs):
    +        response = func(request, *args, **kwargs)
    +        log.debug('%s %s (%s)' % (response.status_code, response.content, request.raw_post_data.replace('\n', ' ')))
    +        return response
    +    return wrapper
    +
     @csrf_exempt
     @require_POST
    +@log_request
     def hub(request):
         try:
             callback = request.POST['hub.callback']
    

    Конфигом это не лечится. И оставлять это логирование навсегда я тоже не хочу.

  6. pyobject.ru/blog

    И оставлять это логирование навсегда я тоже не хочу.

    А чем тебе логирование помешало?

  7. Alex Lebedev

    +1 к предыдущему комментатору, я стараюсь все машинно-специфичные изменения изолировать в одном файле.

    Конфиги для каждого сервера кладутся в source control, образец девелоперского конфига тоже. Для Django это, обычно, settings.(dev|stage|prod).py, для Rail — config/environments/(development|staging|production).rb.

    Нужный для конкретной машины файл подключается симлинком (django) либо переменной окружения (Rails).

  8. Alex Lebedev

    Иван, логгинг отлично настраивается в конфиг-файле. В данном примере просто ставим этому логгеру уровень INFO, если не нужна отладочная информация. Если логгер много где используется, а изменение нужно строго в одном месте — создаем вложенный логгер и меняем настройки уже ему.

  9. Ivan Sagalaev

    Сначала отвечу про логирование.

    Я в курсе, что логирование настраивается в конфиг-файле :-). Но настройка не заставит появиться отладочный логирующий код в том месте, где мне что-то вдруг стало интересно посмотреть. Там его надо реально написать. Идея о том, чтобы логировать на всякий случай всё меня никогда не увлекала, потому что а) бесполезно (если я знаю, где может быть ошибка, я бы её просто не сделал) и б) нечитаемо. И по этой же причине я не хочу оставлять отладочное логирование здесь после отладки. Зачем оно тем, кто будет смотреть в код через год?

    Теперь по сути.

    Логирование — это частный пример. Вот ещё один частный пример из жизни. У нас в Яндексе в Джанго-проектах вёрстка шапки делается не средствами шаблонов самой Джанго, а с подключением внешней бинарной библиотеки. И так вышло, что на одной локальной машине у человека эта библиотека просто не собиралась по неким временным, локальным, никому не интересным причинам. А код зависит от неё достаточно жёстко, и без неё проект просто не стратует.

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

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

  10. Ivan Sagalaev

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

  11. wiz

    дада. Ещё кроме логгирования очень "весело" бывает закоммитить pdb.set_trace()

  12. 3BEP

    Подобный подход несет в себе мину замедленного действия.

    Первое: Становится реальной ситуация "Но ведь на моей машине все же работает!" когда патч будет ошибочно добавлен в локальные патчи.

    Второе: Появляется необходимость поддерживать локальные патчи в актуальном состоянии. Чем больше локальных патчей наберется, тем больше времени это потребует. Вполне реальна ситуация когда к коду придется вернуться после большого перерыва и после того как в этот код были внесены значительные изменения.

    Ситуация когда разработчик не может исправить/настроить свое окружение и не может найти замену в случае аппаратного сбоя - вызывает удивление.

  13. Ivan Sagalaev

    Совершенно согласен, что это неверный подход в долгосрочной перспективе. Но это может быть самым простым решением немедленной задачи. И "исправление" окружения — это вопрос не крутости разработчика, а целесобразности в данных условиях.

  14. 3BEP

    Решение может быть и самое простое, но это только на первый взгляд. Я бы в подобной ситуации написал заметку в стиле How to... в девелоперский вики или в деплоймент гайд.

    Да и вопрос не в крутости, а в организации - я знаю куда перебраться когда возникают проблемы с виртуальной песочницей, я знаю кто должен решать эти проблемы и я знаю кто будет стимулировать решателей при задержках. Но при этом я должен знать целевую платформу - потому что проблемы на продакшене - проблемы которые не смогут решить - в порядке эскалации - СисОпы и КьюЭи - эти проблемы буду решать я и за мной уже никого нет.

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

  15. kekssw

    Идея локальных правок мне очень нравится (яляясь, по-моему, полезным дополнением, а не альтернативой - как многие здесь пытаются представить - локальных конфигов), но так сложилось, что предпочитаю Mercurial, а патч-кью я не использую. Это все-таки отдельный work flow, и испольовать его для отделения "локальных мух" кажется сомнительной затеей. Может быть кто-нибудь знает/использует более прямой hg-аналог, и может поделиться советом?

  16. Александр Соловьёв

    Может быть кто-нибудь знает/использует более прямой hg-аналог, и может поделиться советом?

    mq? Это, правда, superset, но сгодится. Если хочется попроще - можно attic, но mq в комплекте идëт.

  17. Greg

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

    Кстати, вот закоммитили вы правки без логирования, а что дальше? Логирование у вас так и будет висеть локально? Все равно ведь когда-нибудь его откатите. Вопрос когда, почему не до коммита?

  18. Ivan Sagalaev

    Greg, потому что оно там будет висеть некоторое время. И нормальных коммитов я успею сделать с десяток-другой. Очень не хочется каждый раз в ручную ханки из diff'ов выдирать, а потом обратно возвращать.

  19. Bazaar User

    Когда пытаюсь выполнить bzr switch upstream те изменения, которые были сделаны в local-patches откатываются, и нет возможности закоммитить их в upstrem:

    $ bzr swp upstream
    Uncommitted changes stored in pipe "local-patches".
    Switched from "local-patches" to "upstream".
    

    Поведение плагина изменили в версии 2.1?

  20. Ivan Sagalaev

    М-м-м... А что такое "swp"?

    У плагина есть своя команда "switch-pipe", и она ведёт себя именно так, сохраняя незакомиченные изменения вместе с тем pipe'ом, где они делаются. Именно поэтому я её не упомянул, потому что в моём юзкейсе нужен обычный switch, который незакомиченные изменения переносит в новый активный бранч.

  21. Bazaar User

    У плагина есть своя команда "switch-pipe" [...] в моём юзкейсе нужен обычный switch...

    Понял. Не обратил внимания, что в статье используется не пайплайновский свитч.

    P.S. swp - алиас для switch-pipe, предопределенный в плагине.

Добавить комментарий