Пользователь
0,0
рейтинг
28 августа 2011 в 22:14

Разработка → Git и публикация сайта перевод

Git*
При попытке отредактировать этот старый пост слетело всё форматирование. Может быть я его когда-нибудь исправлю.

Я потратил несколько месяцев на борьбу с глюками Git-svn и обдумывание разных вариантов, прежде чем пришёл к этому методу организации рабочего процесса с сайтом — простому, гибкому и удобному в работе.

Основные преимущества:
  • Делая push из удалённой копии мы автоматически обновляем live-копию сайта
  • Правки файлов на сервере не будут разрушать историю коммитов
  • Простота, не нужны особые правила выполнения коммитов
  • Можно применить к уже запущенному сайту, без повторного деплоя или перемещения файлов

Обзор


Главная идея системы — создение на сервере двух репозиториев: пустого bare-репозитория и обычного репозитория с рабочей копией сайта. Эта пара связана парой простых хуков, которые автоматизируют push и pull изменений.

Схема работы

Итак, два репозитория:
  • Hub — bare-репозиторий. Все репозитории разработчиков клонируются именно от него.
  • Prime — обычный репозиторий. Сайт запускается из рабочего каталога этого репозитория.

Работа с двумя репозиториями простая и очень гибкая. Удалённые копии, имеющие ssh-доступ могут легко обновлять live-версию сайта просто выполнив push в Hub-репозиторий. Любые изменения, выполненные в live-версии на сервере мгновенно вливаются в Hub при коммите. В общем, всё работает очень просто — и неважно, где делаются изменения.

Небольшие приготовления перед стартом


Естественно, в первую очередь нужно, что бы Git был установлен на сервере и на всех компах разработчиков. Если на вашем shared-хостинге не установлен Git — вы очень легко можете это исправить (en).

Если вы первый раз работаете с Git на своём сервере, не забудьте указать глобальные настройки. Я указываю особое значения для user.name, чтобы потом видеть в истории проекта изменения, выполненные на сервере:
$ git config --global user.name "Джо, фигачу на сервере"


Поехали!


В первую очередь создадим новый git-репозиторий в live-каталоге нашего сайта, а затем добавим и зафиксируем все файлы сайта. Это будет Prime-репозиторий и рабочая копия. Даже если уже есть история проекта в других местах, содержимое сайта будет базовой точкой, в которую потом будут слиты все остальные копии.
$ cd ~/www
$ git init
$ git add .
$ git commit -m "Импорт всех существующих файлов сайта"


Поскольку мы сделали инициализацию репозитория в рабочей копии – нет нужды отключать сайт на обслуживание и повторно выкладывать все файлы, Git просто содаст репозиторий из имеющихся файлов.

Теперь, когда наш сайт уже находится в Git, создадим bare-репозиторий где-нибудь вне рабочего каталога сайта.
$ cd
$ mkdir site_hub.git
$ cd site_hub.git
$ git --bare init
Initialized empty Git repository in /home/joe/site_hub.git

Ура! Снова вернёмся в рабочий каталог сайта и добавим Hub как удалённый репозиторий, а затем вольём в Hub содержимое ветки master из Prime-репозитория.
$ cd ~/www
$ git remote add hub ~/site_hub.git
$ git remote show hub
* remote hub
 URL: /home/joe/site_hub.git
$ git push hub master

Хуки


Как я уже упоминал в начале, Hub и Prime синхронизируются между собой, используя два простых скрипта.

Одно из основных правил при работе с Git — никогда не делайте push в репозтирий, у которого есть рабочая копия. Мы следуем этому правилу и создали репозиторий «Hub». Вместо того, чтобы делать push из Hub, который никак не повлияет на рабочую копию, мы будем использовать хук, который заставит Prime выполнить pull из Hub-репозитория.

Post-update — в Hub-репозитории

Как только в Hub поступит новая порция изменений, сразу будет выполнен этот скрипт. Мы переходим в рабочий каталог Prime-репозитория, и вытягиваем измениния из Hub'а. Проталкивание изменений (push) не изменяет состояния рабочего каталога репозитория, поэтому и нужно делать pull, находясь в рабочем каталоге.
#!/bin/sh

echo
echo "**** Вытягиваем изменения в Prime [Hub's post-update hook]"
echo

cd $HOME/www || exit
unset GIT_DIR
git pull hub master

exec git update-server-info

Post-commit — в Prime-репозитори

Этот скрипт запускается после каждого коммита в Prime-репозитории и проталкивает изменения в Hub. В идеальном мире, конечно, мы вообще никогда ничего не будем править прямо на сервере. Но в нашем несовершенном мире возможно всё, что угодно, поэтому давайте автоматизируем процесс проталкивания изменений, чтобы не разрушать историю проекта и избежать возможных конфликтов.
#!/bin/sh

echo
echo "**** pushing changes to Hub [Prime's post-commit hook]"
echo

git push hub

Итак, используя этот хук, мы сразу же получаем в Hub-репозитория все изменения, выполненные в master-ветке Prime-резпозитория. Прочие ветки также можно клонировать, но они не будут влиять на сайт. Поскольку все удалённые копии получают доступ через SSH-адрес к Hub, то выполнить push и запустить обновление сайта напрямую могут только пользователи, имеющие прямой доступ к shell'у.

Конфликты


«Положить» сайт при такой системе синхронизации двух репозиториев очень сложно. Каждое изменение, сделанное в Prime автоматически попадает в Hub и все конфликты будут сразу видны при попытке выполнить push из клонов репозитория.

Но всё же есть несколько ситуаций, при которых состояние Prime может отличаться от Hub'а, и для исправления ситуации потребуется выполнить несколько дополнительных телодвижений. Если мы что-то правим на Prime и не зафиксировали изменения, а в этот момент сработает post-update в Hub, то все завершится ошибкой с сообщением «Entry ‘foo’ not uptodate. Cannot merge.». Фиксация изменений в рабочем каталоге Prime позволит зачистить его состояние, и post-update хук сможет соединить все неотправленные изменения.

Также я обнаружил, что если конфликт возникает вследствие того, что изменения в Prime не могут быть объединены с Hub'ом, то наилучшим решением будет протолкнуть текущее состояние Prime'а в новую ветку на Hub. Эта команда, выполненная из рабочего каталога Prime создаст удалённую ветку «fixme», основанную на текущем стоянии Prime-репозитория.
$ git push hub master:refs/heads/fixme

Как только изменения окажутся в Hub'е, мы сможем получить ветку в любом из клонов, разрешить конфликт и смержить ветки. Попытка разрешить конфликт прямо на сервере почти наверняка приведёт к проблемам в работе сайте из-за появления маркеров конфликтных областей.

Держим всё в чистоте


Каталог .git Prime-репозитория находится в корневом каталоге сайта, и, верояно, доступен для публичного доступа. Чтобы никто не смог сунуть свой нос, куда не следует, добавьте эти строки в файл .htaccess верхнего уровня:
# deny access to the top-level git repository:
RewriteEngine On
RewriteRule \.git - [F,L]

Прим. переводчика: есть и другие способы закрыть доступ к каталогу на сервере.

Прочие проблемы


Если при попытке выполнить push в репозиторий на сервере вы видите эту ошибку:
git-receive-pack: command not found
fatal: The remote end hung up unexpectedly

В этом случае просто добавьте export PATH=${PATH}:~/bin в ваш файл .bashrc, лежащий на сервере.
Перевод: Joe Maller
Михаил Баранов @404
карма
48,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (49)

  • –25
    Именно так работает сайт erlyvideo.ru/ (он же и erlyvideo.org/)

    Из одной папки в гите берутся файлы, выбирается русский или английский вариант и смотрим.
  • +1
    Спасибо, хороший пост. Отличная идея: хранить рабочую копию сайта и bare-репозиторий отдельно. Применяли ли вы ваш метод на практике?
  • +3
    Я как раз на прошлой сделал что-то похожее на Mercurial.

    Смысл такой: к существующей системе контроля версий добавился репозиторий на тестовом сервере, в котором стоит хук на пуш. Поэтому для обновления версии достаточно выбрать удалённый репозиторий и ввести логин-пароль (отличающийся от пароля в рабочем репозитории, чтобы избежать случайного пуша куда не надо).
  • +2
    У меня в кроне стоит так:

    * * * * * cd /home/project/project/ && /usr/bin/hg -q pull && /usr/bin/hg update production | grep -v «0 files updated, 0 files merged, 0 files removed, 0 files unresolved» && /bin/touch web/django.wsgi

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

    Не делал хуков на push (или всякие fabric'и) именно по причине требования к нулевому общему даунтайму.

    Ну а само обновление продакшн-нод досигается просто перемещением метки production на нужную ревизию.
  • +10
    А зачем закрывать через .htaccess папку .git когда можно использовать detached рабочий каталог и хранить сам репозиторий в недоступном для веб-сервера месте?
    Опция --work-tree служит для этой цели. www.kernel.org/pub/software/scm/git/docs/
  • 0
    Зачем?! Зачем использовать систему контроля версия для деплоя?!

    Вас ничему история не учит? http://habrahabr.ru/blogs/infosecurity/70330/

    Нет, я, конечно, прочитал, что в статье есть предложение по закрытию доступа к .git директории, но что, если админ ошибется и не сможет правильно настроить mod_rewrite (или правила nginx)?

    Почему для деплоя не пользоваться утилитами, специально для этого предназначенными? К примеру, тот же Capistrano (а еще лучше capistrano-ext со стейджингом, чтобы случайно не деплойнуть на продакшн неподготовленный код).
    • +6
      Ну… с гитом подобной проблемы, как у svn, нет. Достаточно только, чтобы DocumentRoot хоста находился внутри каталога проекта, или вообще в отдельном каталоге.

      Все свои потроха гит содержит аккуратно, не разбрасывая их по подпапкам проекта.
    • +2
      Для небольших проектов специальные утилиты потребуют слишком много лишних и непонятных телодвижений. Вариант с --work-tree мне нравится намного больше.
      • 0
        А в статье где-нибудь указано, что данный подход желательно применять только на небольших проектах?

        И еще, настройка репозиториев, хуков, mod_rewrite, а также системы сообщения между prime и hub репозиториями затмит собой настройку скрипта для capistrano и пробросу пары ключей между машинами :)
        • –2
          я в отличие от автора с двумя репами, сделал один реп с бранчами мастер (в который пушу) и продакшн (который чекаутнут на сервере). далее deploy.sh:
          git checkout master
          git merge newfeature
          git merge bugfix
          git push server master
          ssh xxx@librapost.ru "cd /xxx/xxx/librapost && git merge master"

          а сколько строк в вашем капистрано-конфиге?
        • –2
          ах да. public-папка проектов всегда внутри репа (не корневая), поэтому никаких мод-реврайтов не нужно
  • 0
    Поищите на гитхабе строчку «git ftp»
  • +4
    как-то не правильно. Лучше использовать capistrano или подобное, ну на крайний случай делать экспорт в нужный каталог по hook'у.
    • 0
      я свои рельсы уже пару лет деплою через гит, и мне, честно говоря, пользователей капистраны немного жалко.
      • 0
        Откатите продакшен на 1 версию назад находясь на рабочей машине. Через git деплоить хорошо, да, но не так как это сделал автор, зачем второй репозиторий? Делать экспорт прямо из bare в нужный каталог.

        > пользователей капистраны немного жалко.

        Вы просто не умеете ее готовить. Тоже самое говорят хомячки про пользователей unix like os и эникейщики про сервера на unix like.
        • –2
          > Тоже самое говорят хомячки про пользователей unix like os и эникейщики про сервера на unix like
          я работаю на Ubuntu 11.04, продакшн-сервера все на Ubuntu. и капистрано тем не менее для меня монструозен.

          выше я написал, что в отличие от автора у меня один реп, да.
          • +2
            Вы кажется не поняли метафору…

            > выше я написал, что в отличие от автора у меня один реп, да.

            Тоже как-то не так вас…

            git archive master | tar -x -C /somewhere/else

            ну или так:

            git-archive --format=tar --remote=ssh://remote_server/remote_repository master | tar -xf — в пост-апдейт хук и все.

            > deploy.sh:
            git checkout master
            git merge newfeature
            git merge bugfix
            git push server master

            git алисы вы не осилили, ога.
            • 0
              зачем мне алиасы там, где я не ввожу команды руками?

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

              метафору я понял. только вот не понял, зачем вы её здесь привели. удобство капистраны настолько субъективная вещь, что сравнивать её с *nix системами — как сравнивать тех, кто не любит анчоусы с теми, кто не ест жирное.
      • 0
        А как производите обновление БД?
        • 0
          я работаю с mongodb, поэтому миграции мне не нужны. а менять схему (к примеру перекидывать одно поле в другое) нужно нечасто, в этих случаях захожу через ssh
      • 0
        в смысле через ssh, да?
  • 0
    С моей точки зрения это очень не правильный подход. При данном подходе все новые изменения сразу попадут на продакшн сервер. А если в этих изменениях будет регрессионные ошибки? Они сразу положат продакшн сервер… Для того что бы изменения не убили продакшн сервер, умные люди придумали вехи и версии, которые в репозитории будут отмечены тэгами. Продакшн сервер нужно обновлять только на определенный тэг, содержащий фиксированное число изменений проверенных тестировщиками. Таким образом вы сможете более менее гарантировать качество кода и сайта в целом. Описанный у вас способ подходит для деплоя на девелопмент сервер и в меньшей мере на стэйджинг сервер.
    • 0
      Ничего не мешает пушать на сервер тэг, который перед тем был на стейджинге и на тестинге.
    • 0
      видимо, так и появилась возможность пушить коммиты из prime в hub — вполне себе решение, если нет возможности содержать процесс с блэк^H^H^H^H стэйджингом и тестировщиками)
  • 0
    да, согласен, так и надо делать, но статья предлагает другой подход, который заключается в моментальном обновлении продакшн сервера при любом пуше.
  • 0
    как разрешаются конфликты при git pull?

    этой проблемы не возникает, если с девелоперского сайта делать git push на продакшн.
    • 0
      #!/bin/bash

      txtrst='\e[0m' # Text Reset
      txtred='\e[0;31m' # Red
      txtgrn='\e[0;32m' # Green
      WORK_TREE='/some/path/on/dev/server/'

      while read oldrev newrev ref
      do
      case $ref in
      refs/heads/dev )
      echo "========================================"
      git --work-tree=$WORK_TREE checkout -f dev
      if [ $? -eq 0 ]; then
      echo -e "${txtgrn}DEVELOPER SERVER successfully updated${txtrst}"
      else
      echo -e "${txtred}Failed to checkout DEVELOPER SERVER!${txtrst}"
      fi
      ;;
      refs/heads/master )
      echo "========================================"
      git push gsl master:master
      if [ $? -eq 0 ]; then
      echo -e "${txtgrn}PRODUCTION SERVER successfully updated${txtrst}"
      else
      echo -e "${txtred}Failed to push to PRODUCTION SERVER!${txtrst}"
      fi
      ;;
      * )
      echo "NO UPDATES FOR $1"
      ;;
      esac
      done
      echo "========================================"


      вот мой post-receive hook на девелоперском сервере.
      принцип работы: есть две ветки — master и dev. изменения в ветке dev checkout'ятся в DOCUMENT_ROOT девелопеского сервера. А изменения в ветке master push'атся в bare-репозиторий на продакшн-сервере, а оттуда уже аналогичным образом chekout'ятся в его DOCUMENT_ROOT
  • +5
    Деплоить должен человек. Руками. Если оставить это на хук то в один прекрасный момент вы получите неработающий продакшн. А делать исправления на работающем сервере и заливать их в репозиторий — это вообще за гранью добра и зла! Вы там что, на Друпале пишете??
    • 0
      Да что уж там говорить, и при деплое руками то ломается время от времени :)
  • 0
    Пытаюсь найти еще информацию по теме выкладывания новой версии на продакшен, но не могу найти.

    Подскажите ключевые слова :)
  • +1
    Я как-то пока что не особо с джитом, поэтому вопрос: операция деплоя в данном случае атомарна? Если нет, тогда не вижу смысла в ней принципе, не говоря уже об habrahabr.ru/blogs/Git/127213/#comment_4200454
    • –1
      а у вас что, часто соединение обрывается во время деплоя? при чём здесь атомарность?
      • +1
        Не, меня интересует, на новые исходники сайт переключается атомарно или в какой-то момент времени половина сырцов предыдущей ревизии, а половина — новой ревизии.
        Мы себе не можем позволить обновлять файлы прямо в живом каталоге проекта на сервере.
        Поэтому у нас сначала льется на сервер архив, который распаковывается в новую папку и потом производится переключение на эту папку.
        Кроме того, не можем себе позволить удалить старые файлы, потому что на них еще могут какие-то кроны работать.
        • 0
          у меня несколько сотен файлов в гите меняется за доли секунды. но тут же зависит от языка и фреймворка. и такой вопрос, а нельзя отрубать сервер и кроны на секунды во время деплоя? zero-downtime-deploy в конце концов?
          ну и последний вопрос: а где вы вообще нашли такой инструмент, который моментально атомарно несколько сотен файлов может заменить? на это как-то даже и современные файловые системы не способны. да одновременно сменить сотни записей даже совеменные БД не очень умеют атомарно
          • 0
            У нас за доли секунды несколько сотен запросов на сервера приходит…
            Во время деплоя ничего отрубать нельзя.

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

            В общем, мы сейчас о немного разных вещах говорим… Наверное, нужно было просто разобраться немного в джите и не задавать того вопроса :)
          • 0
            Вообще-то речь не про «одновременно», а про «атомарно». Вариант с изменением рабочего каталога — операция хоть и атомарная, но не решает проблему атомарного обновления частей проекта, которые находятся вне рабочего каталога (база данных, настройки cron, почтовые хуки в ~/.qmail, etc.).

            Наша deploy-система блокирует проект, обновляет файлы, структуру базы, настройки cron, перезапускает отдельные сервисы, etc. а потом снимает блокировку. Это гарантирует, что в процессе обновления не получится так, что часть файлов/базы уже обновилась, часть ещё нет, и тут приходит запрос пользователя и он обрабатывается смесью старого и нового кода с в принципе непредсказуемыми последствиями. Кроме того, это гарантирует, что если обновление затянется или сломается в процессе, то пользователь получит статическую страничку «server maintenance, please try again later».

            Системы контроля версий всё это делать не умеют и они вообще не предназначены для реализации deploy. Можно, конечно, навесить на них сложные системы хуков и получить почти то, что требуется, но это сложно и не правильно. Для deploy нужно использовать предназначенные для этого системы. И выкатывать новую версию на production сервер нужно ручками, а не автоматически (не в том смысле, что файлы/базу ручками обновлять, а в том смысле, что во-первых желательно перед обновлением глазками просмотреть все изменения — т.е. patch/sql/etc.-файлы — и во-вторых присутствовать при установке этих изменений на сервере на случай если что-то пойдёт не так).

            Единственная сложность такого надёжного подхода в том, что все приложения проекта (cgi-шки, fastcgi-сервер, сервисы, cron-скрипты, qmail-скрипты, консольные утилиты) должны поддерживать единую систему блокировок, чтобы было возможно их всех одним махом заблокировать на время обновления системы (все приложения ставят shared-lock в процессе работы, а при обновлении ставится exclusive-lock). Но при наличии таких блокировок решается не только проблема атомарного обновления проекта, но и консистентного бэкапа.
            • 0
              У нас вот SQL база особо часто не обновляется да и вообще, по большей части, уходим от SQL решений в последнее время, так что проект не приходится останавливать по причине обновления. Да и даже если обновляется, то обычно это добавление поля в табличку или новой таблички, а значит можно сначала базу обновить, не останавливая или, в крайнем случае, блокируя по частям (а значит проект будет работать, максимум какое-то относительно небольшое количество пользователей получит информацию о server maintenance), а уже потом можно и код, работающий с БД, обновить.
  • 0
    на мой вкус я бы при ините добавил shared,
    если предполагается, что пушить в bare будут несколько человек:
    git --bare init --shared

    или даже сделать так после init:
    cd ..
    sudo chown -R www-data:www-data site_hub.git
    sudo chmod -R g+wX,o= site_hub.git
    sudo find site_hub.git -type d -exec chmod g+s '{}' ';'

    еще обновить server-info лучше сразу после первого пуша руками так:
    git update-server-info
    а в post-receive делать так:
    cd /home/okertanov/public_html/html-templates.espectrale.com/public || exit
    #unset GIT_DIR
    #unset GIT_WORK_TREE
    env -i git pull origin #or whatever
    env -i git update-server-info
    env -i git submodule init
    env -i git submodule update

    если предплолагается, что в репозитории есть субмодули.
  • 0
    Прошу при первом упоминании слова bare, упомянуть в скобочках, что это «репозиторий без собственной рабочей копии»
    • 0
      и фразу «Одно из основных правил при работе с Git — никогда не делайте push в репозтирий, у которого есть рабочая копия.» куда-нибудь во вступление бы поместить, ибо она — ключевая.
  • +1
    У нас на «prime» заливается по ftp с помощью модуля ftp для hg. Хук на «hub» у всех репов такой: changegroup = hg update -C; hg ftp -r tip -u. Заливаются не все файлы, а только изменившиеся (это и делает модуль ftp).

    Тут чуть подробнее: edhel.krasu.ru/node/328

    • 0
      Минус в том, что используется нешифрованный протокол (ftp), а hg-модуль ftp по ssh/sftp заливать изменения не умеет. Но у нас заливка идёт по локалке, поэтому не критично. И ftp-порт на «prime» открыт только для «hub», поэтому и утечка логина-пароля от ftp не критична.
  • 0
    Извините, возможно мой вопрос покажется странным, но я пока не могу управиться с правами на директории/файлы при работе через гит.
    Например у меня есть сайт который разрабатывается через гит. Над сайтом работают 3 разработчика, все ходят по ssh.
    Ситуация:
    Первый разработчик изменяет/создает файл в своем локальном репозитории, коммитит, пушит в bare репозиторий, идет на рабочий сайт и делает pull в результате первый разработчик становится хозяином файла. Потом надо второму разработчику работать с этим же файлом, но он не может его править, т.к. он не хозяин. Можно добавить разработчиков в одну группу, но гит вроде бы создает файлы с правами 644. Плюс ко всему файлы могут создаваться апачем. Может кто нибудь подскажет как решить этот вопрос? Может я что-то упускаю?
    • 0
      Вам надо пулить от имени веб-сервера.
      Как лучше это сделать зависит от вашей ситуации.
      Можно на веб-сервере скрипт сделать, можно по задачке в кроне отслеживать, можно еще вариантов придумать.
      • 0
        Дело в том, что в некоторых директориях апач не должен иметь право на изменение файлов, а разработчики наоборот должны иметь право на изменение файлов в этих директориях. Пока решение такое — использовать setfacl и определять через него кто будет писать, а кто нет. С помощью setfacl для файла/директории можно будет указать сразу несколько хозяев и несколько групп. И ночью в определенное время, по крону, восстанавливать права на файлы.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.