2013-09-12

Агрегатор систем контроля версий, aka "workspace configurator"

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

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

Поэтому уже пару лет я хожу с идеей конфигуратора рабочего пространства. Буду рад, если у меня все-таки дойдут руки это сделать…

Задача

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

Требования

  • Сопоставление любой папки или файла в проекте, в том числе корневой, любой папке или файлу произвольного репозитория.
  • Единый интерфейс отслеживания изменений и управления ветками.
  • Транслирование изменений и веток в соответствующие системы контроля версий.
  • Использование различных алгоритмов отслеживания изменений, а именно:
    • Standard - в обе стороны;
    • Library - только обновления из репозитория в проект;
    • Upstream - обновления из репозитория + версионированные патчи проекта;
    • Publish - только из проекта в репозиторий;
    • Fixed - закрепление кода в определенной версии.
  • Поддержка перекрытия одной папки несколькими репозиториями.
  • Отслеживание изменений и веток конфигурации рабочего пространства в отдельном репозитории.
  • Возможность исключить установленное соответствие с репозиторием при создании ветки конфигурации.
    • Инициализация рабочего пространства из конфигурационного репозитория.

    Существующие решения

    Ни одно из существующих решений не удовлетворяет требованиям полностью. Наиболее близки по идеологии "google repo" и "perforce".

    svn externals

    • Только для Subversion.
    • Нет перекрытия репозиториев в одной папке.
    • Изменения и ветки внешнего репозитория отслеживаются отдельно.

    git submodules

    • Только для Git.
    • Изменения и ветки внешнего репозитория отслеживаются отдельно.
    • Не отслеживаются обновления внешнего репозитория.

    git subtree

    • Только для Git.
    • Поддерживается только механизм перекрытия папок.
    • По сути - набор хаков через низкоуровневый интерфейс git.

    google repo

    • Только для Git.
    • По умолчанию работает по алгоритму "Standard".
    • Отступление от стандартного алгоритма требует работы с конкретными репозиториями.
    • Идейно наиболее близкое к задаче решение.

    perforce git fusion

    • Только для Git.
    • Удобный механизм управления из Perforce.
    • Интеграция с Perforce.
    • Проприетарное решение.

    hg subrepository

    • Поддержка Mercurial, Subversion, Git.
    • Не отслеживаются обновления внешнего репозитория.
    • Изменения и ветки внешнего репозитория отслеживаются отдельно.
    • Не рекомендовано к использованию (feature of last resort).

    Сценарии использования

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

    1. Проект в Git и публикация части во внешний SVN

    Андрей поддерживает в SVN большой многокомпонентный продукт Alpha.
    Борис разрабатывает в Git компонент Beta продукта Alpha.

    Ветка Beta/master должна публиковаться в ветку Alpha/beta, причем для папок должны выполняться следующие соответствия (слева git beta, справа svn alpha):
        /src -> /scripts/beta
        /tests -> /tests/beta
        /doc/src -> /doc/src/beta

    Для решения задачи Борис создает такую конфигурацию рабочего пространства:
        / -> git beta - branch master
        /src -> svn alpha - /scripts/beta - mode publish - skip branching
        /tests -> svn alpha - /tests/beta - mode publish - skip branching
        /doc/src -> svn alpha - /doc/src/beta - mode publish - skip branching

    2. Проект в Git, внешняя библиотека в Git и собственные патчи

    Андрей разрабатывает в Git проект Alpha.
    Борис разрабатывает в Git библиотеку Beta.

    Андрей сделал исправления в библиотеке Beta и предложил их Борису для внесения в основной репозиторий. До тех пор, пока Борис не внесет исправления в библиотеку Beta, Андрей вынужден накладывать свои исправления поверх всех обновлений библиотеки Beta.

    Для решения задачи Андрей создает такую конфигурацию рабочего пространства:
        / -> git alpha
        /ext/beta -> git beta - branch stable - mode upstream
    При этом инструмент управления рабочим пространством автоматически отслеживает все изменения, которые Андрей сделал в Beta и накладывает их поверх стабильной ветки этой библиотеки.

    2011-08-18

    Еще один велосипед для Django

    Вчера выложил в открытый доступ под лицензией BSD свой новый велосипедик для Django.

    Называется творение "django-supergeneric". Исходный код доступен тут: https://github.com/lig/django-supergeneric

    Причиной написания этого стало желание перестать постоянно писать одно и то же. Почти всегда для каждой модели приходится писать похожие друг на друга, как близнецы-братья, view для просмотра списка объектов, одного объекта, создания объекта, редактирования объекта и удаления объекта. Понятно, что есть generic views, но их тоже надо конфигурировать, причем многие параметры повторяются. Да, и каждый раз для всех пяти view надо прописать urlpatterns, которые тоже для всех объектов очень похожи.

    Вот поэтому и появился django-supergeneric.

    Пример использования можно посмотреть в демо-проекте: https://github.com/lig/django-supergeneric/tree/master/project

    Upd: страничка на ohloh: https://www.ohloh.net/p/django-supergeneric, плюсуйте, кто будет пользоваться, если не лень.

    2010-12-22

    Пам-парам, по "монгам"

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

    Я не хочу здесь пересказывать документацию. Однако, хочу описать некоторые вкусности, неочевидности и, конечно, подводные камни, с которыми я столкнулся в самом начале пути. Возможно, кому-то это сэкономит нервы в начале освоения этой документ-ориентированной базы данных.

    В основном я использую MongoDB из Python, для чего использую PyMongo напрямую или MongoEngine (подобие ORM для MongoDB с несколькими фишками для использования его с Django). И вот как раз с взаимодействием с MongoDB из Python-а и связаны пара подводных камушков. Точнее с параллельным использованием JavaScript встроенного в MongoDB и PyMongo.

    Вообще говоря, "питоновский" тип DateTime прекрасно обрабатывается MongoDB. Внимательный читатель документации сразу обратит внимание (я этого не сделал), что PyMongo нужно передавать дату/время в UTC, т.е. нужно использовать datetime.utcnow(), а не привычное большинству datetime.now(). Это первый подводный камень, который, впрочем, вполне очевиден и в явном виде отражен в документации.

    Есть еще один подводный камень связанный с хранением времени. Предположим, что, по каким-то причинам, мы хотим хранить время в формате unixtimestamp, т.е. вещественное количество секунд прошедших с 1.01.1970 с тремя знаками после запятой или даже просто целым числом. Это может быть нужно чтобы упростить и ускорить запись журнала каких-либо событий в MongoDB. Пока мы работаем с этими числами через PyMongo, все хорошо. Но потом встает необходимость оперировать с этим значением времени из функции JavaScript, например, при выполнении операции Map/Reduce или просто в консоли MongoDB. Пусть в базе лежит такой документ (timestamp получен сейчас, т.е., как минимум, в 2010 году):
    
    { "_id" : ObjectId("4d125e16f52cb12e01d5d041"), "timestamp" : 1293038527, "name" : "jump" }
    
    
    Пусть нам нужно получить год, месяц и число, в который произошло событие. Вот с чего начал я:
    
    db.events.find().forEach(function (event) {
      var date = new Date(event.timestamp);
      print(date.getFullYear(), date.getMonth(), date.getDate());
    })
    
    
    Какого же было мое удивление, когда я увидел в ответ "1970 0 16", т.е. нулевой (!) месяц и всего 16-й день от начала эпохи. Чтение документации по JavaScript Date Object открыло мне глаза прежде всего на то, что в JavaScript timestamp - это целое число миллисекунд прошедших от начала эпохи. И код превратился в такой:
    
    db.events.find().forEach(function (event) {
      var date = new Date(event.timestamp * 1000);
      print(date.getFullYear(), date.getMonth(), date.getDate());
    })
    
    
    Этот код вернул уже "2010 11 22", но сегодня 22.12.2010. Да, да, да, вспоминаем нулевой месяц. Из той же документации по JavaScript Date Object я узнаю, что, оказывается, именно месяц считается в JavaScript начиная с нуля (зачем???), тогда код приобретает окончательный вид:
    
    db.events.find().forEach(function (event) {
      var date = new Date(event.timestamp * 1000);
      print(date.getFullYear(), date.getMonth() + 1, date.getDate());
    })
    
    
    Теперь ответ правильный: "2010 12 22".

    На сегодня, думаю, этого достаточно. В следующий раз поделюсь опытом использования MapReduce в MongoDB. Покажу как возвращать из Reduce списки значений и какого подхода к расчету значений в Reduce следует избегать.

    2010-12-20

    Git mass push. Как заслать во все "ремоуты".

    Думаю, все, кто работал с git-ом, сталкивались с ситуацией, когда удаленных репозиториев несколько и хотелось бы сделать "git push" во все. Например, у меня проекты лежат одновременно на sf.net, github.com и в собственном приватном хранилище.

    Я для себя написал маленький скриптик на bash:
    #~/bin/bash
    git remote show | while read repo_name
    do
    git push $repo_name $*
    done
    Постоянная ссылка на этот кусок кода: http://paste.nophp.ru/1Nu. Код раскрашен с помощью Paste.NoPHP.ru.
    Если положить скрипт в файл "~/bin/git-mass-push", то его можно будет запускать командой "git mass-push", для которой будет работать стандартное автодополнение git-а. При этом, этой команде можно передать любые параметры, которые принимает "git push" после имени репозитория, что видно из кода.

    Пользуемся, радуемся;)

    2010-03-30

    Yammy? Dummy? ... Botty!:) Простой бот на xmpppy

    Сейчас появляется всё больше сервисов, использующих протокол XMPP (он же Jabber).

    Например: juick.com, facebook.com, identi.ca.

    Часто хочется написать своего бота, чтобы общаться с такими сервисами, может быть написать свой сервис или просто автоматически отвечать своим друзьям на глупые сообщения;)

    В этом может помочь библиотека xmpppy.

    Я использую вот такой класс, в качестве основы для моих ботов:


    class BotBase(object, xmpp.Client):
        
        bot_name = None
        
        class InvalidCommandError(Exception):
            pass
        
        def __init__(self, *args, **kwargs):
            self.bot_settings = BOT[self.bot_name]
            self.__class__.__name__ = 'Client'
            xmpp.Client.__init__(self, server=self.bot_settings['HOST'],
                *args, **kwargs)
        
        def connect(self):
            xmpp.Client.connect(self, use_srv=self.bot_settings['SERVER'])
            return self.auth(self.bot_settings['USERNAME'],
                self.bot_settings['PASSWWORD'], self.bot_settings['XMPP_RESOURCE'])
        
        def run(self):
            self.UnregisterDisconnectHandler(self.DisconnectHandler)
            self.RegisterDisconnectHandler(self.reconnectAndReauth)
            self.RegisterHandler('message', self.messageHandler)
            self.RegisterHandler('presence', self.presenceHandler)
            self.sendInitPresence(0)
            while self.Process(1): pass
            self.disconnect()
        
        def messageHandler(self, connection, message):
            raise NotImplementedError
        
        def presenceHandler(self, connection, presence):
            raise NotImplementedError
    

    Как видно, в коде используются настройки бота. Они выглядят вот так:


    BOT['dummy'] = {
        'HOST': 'serge.matveenko.ru',
        'USERNAME': 'dummy',
        'PASSWWORD': 'dummy5',
        'XMPP_RESOURCE': 'mybot-0.1',
        'USER_AGENT': 'MyBot/',
        'SERVER': 'serge.matveenko.ru',
        'JID': 'lig@serge.matveenko.ru',
    }
    

    Для любого нового бота теперь нужно создать класс-наследник BaseBot, в котором установить атрибут bot_name и реализовать методы messageHandler и presenceHandler. В словаре BOT должен существовать ключ соответствующий этому bot_name с настройками для этого бота.

    Запуск бота осуществляется так:


    bot = MyBot.initBot(debug=False)
    if not bot.connect():
        raise IOError('Can not auth with server.')
    bot.run()
    

    , где MyBot — класс вашего бота, а параметр debug говорит сам за себя.

    That's all folks!