Рассмотрим пример. Я поместил Главу 1 настоящей книги в Git, добавил немного текста в один параграф и сохранил документ. Затем я выполнил git diff, чтобы увидеть, что изменилось:

$ git diff diff --git a/chapter1.doc b/chapter1.doc index c1c8a0a..b93c9e4 100644 --- a/chapter1.doc +++ b/chapter1.doc @@ -8,7 +8,8 @@ re going to cover Version Control Systems (VCS) and Git basics re going to cover how to get it and set it up for the first time if you don t already have it on your system. In Chapter Two we will go over basic Git usage - how to use Git for the 80% -s going on, modify stuff and contribute changes. If the book spontaneously +s going on, modify stuff and contribute changes. If the book spontaneously +Let's see if this works.

Git коротко и ясно дал мне знать, что я добавил строку "Let’s see if this works", так оно и есть. Работает не идеально, так как добавляет немного лишнего в конце, но определённо работает. Если вы сможете найти или написать хорошо работающую программу для конвертации документов Word в обычный текст, то такое решение скорее всего будет невероятно эффективно. Тем не менее, strings доступен на большинстве Mac и Linux-систем, так что он может быть хорошим первым вариантом для того, чтобы сделать подобное со многими бинарными форматами.


Текстовые файлы в формате OpenDocument

Тот же подход, который мы использовали для файлов MS Word (*.doc), может быть использован и для текстовых файлов в формате OpenDocument, созданных в OpenOffice.org.

Добавим следующую строку в файл .gitattributes:

*.odt diff=odt

Теперь настроим фильтр odt в .git/config:

[diff "odt"] binary = true textconv = /usr/local/bin/odt-to-txt

Файлы в формате OpenDocument на самом деле являются запакованными zip'ом каталогами с множеством файлов (содержимое в XML-формате, таблицы стилей, изображения и т.д.). Мы напишем сценарий для извлечения содержимого и вывода его в виде обычного текста. Создайте файл /usr/local/bin/odt-to-txt (можете создать его в любом другом каталоге) со следующим содержимым:

#! /usr/bin/env perl # Сценарий для конвертации OpenDocument Text (.odt) в обычный текст. # Автор: Philipp Kempgen if (! defined($ARGV[0])) { print STDERR "Не задано имя файла!\n"; print STDERR "Использование: $0 имя файла\n"; exit 1; } my $content = ''; open my $fh, '-|', 'unzip', '-qq', '-p', $ARGV[0], 'content.xml' or die $!; { local $/ = undef; # считываем файл целиком $content = <$fh>; } close $fh; $_ = $content; s/]*>//g; # удаляем span'ы s/]*>/\n\n***** /g; # заголовки s/]*>\s*]*>/\n -- /g; # элементы списков s/]*>/\n\n/g; # списки s/]*>/\n /g; # параграфы s/<[^>]+>//g; # удаляем все XML-теги s/\n{2,}/\n\n/g; # удаляем подряд идущие пустые строки s/\A\n+//; # удаляем пустые строки в начале print "\n", $_, "\n\n";

Сделайте его исполняемым

chmod +x /usr/local/bin/odt-to-txt

Теперь git diff сможет сказать вам, что изменилось в .odt файлах.


Изображения

Ещё одна интересная проблема, которую можно решить таким способом, это сравнение файлов изображений. Один из способов сделать это — прогнать PNG-файлы через фильтр, извлекающий их EXIF-информацию — метаданные, которые дописываются в большинство форматов изображений. Если скачаете и установите программу exiftool, то сможете воспользоваться ею, чтобы извлечь из изображений текстовую информацию о метаданных, так чтобы diff хоть как-то показал вам текстовое представление произошедших изменений:

$ echo '*.png diff=exif' >> .gitattributes $ git config diff.exif.textconv exiftool

Если вы замените в проекте изображение и запустите git diff, то получите что-то вроде такого:

diff --git a/image.png b/image.png index 88839c4..4afcb7c 100644 --- a/image.png +++ b/image.png @@ -1,12 +1,12 @@ ExifTool Version Number : 7.74 -File Size : 70 kB -File Modification Date/Time : 2009:04:21 07:02:45-07:00 +File Size : 94 kB +File Modification Date/Time : 2009:04:21 07:02:43-07:00 File Type : PNG MIME Type : image/png -Image Width : 1058 -Image Height : 889 +Image Width : 1056 +Image Height : 827 Bit Depth : 8 Color Type : RGB with Alpha

Легко можно заметить, что размер файла, а также высота и ширина изображения поменялись.

Развёртывание ключа

Разработчики, привыкшие к SVN или CVS, часто хотят получить в Git возможность развёртывания ключа в стиле этих систем. Основная проблема с реализацией этой функциональности в Git это то, что нельзя записать в файл информацию о коммите после того, как коммит был сделан, так как Git сначала считает контрольную сумму для файла. Не смотря на это, вы можете вставлять текст в файл во время его выгрузки и удалять его перед добавлением в коммит. Атрибуты Git предлагают два варианта сделать это.

Во-первых, вы можете внедрять SHA-1-сумму блоба в поле $Id$ в файл автоматически. Если установить соответствующий атрибут для одного или нескольких файлов, то в следующий раз, когда вы будете выгружать данные из этой ветки, Git будет заменять это поле SHA-суммой блоба. Обратите внимание, что это SHA-1 не коммита, а самого блоба.

$ echo '*.txt ident' >> .gitattributes $ echo '$Id$' > test.txt $ git add test.txt

В следующий раз, когда вы будете выгружать этот файл, Git автоматически вставит в него SHA его блоба:

$ rm test.txt $ git checkout -- test.txt $ cat test.txt $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

Однако, такой результат мало применим. Если вы раньше пользовались развёртыванием ключа в CVS или Subversion, можете добавлять метку даты — SHA не особенно полезен, так как он довольно случаен, и к тому же, глядя на две SHA-суммы, никак не определить какая из них новее.

Как оказывается, можно написать свои собственные фильтры, которые будут делать подстановки в файлах при коммитах и выгрузке файлов. Для этого надо задать фильтры "clean" и "smudge". В файле .gitattributes можно задать фильтр для определённых путей и затем установить сценарии, которые будут обрабатывать файлы непосредственно перед выгрузкой ("smudge", смотри Рисунок 7-2) и прямо перед коммитом ("clean", смотри Рисунок 7-3). Эти фильтры можно настроить на совершение абсолютно любых действий.

В сообщении первоначального коммита, добавляющего эту функциональность, дан простой пример того, как можно пропустить весь свой исходный код на C через программу indent перед коммитом. Сделать это можно, задав атрибут filter в файле .gitattributes так, чтобы он пропускал файлы *.c через фильтр "indent":

*.c filter=indent

Затем укажите Git'у, что должен делать фильтр "indent" при smudge и clean:

$ git config --global filter.indent.clean indent $ git config --global filter.indent.smudge cat

В нашем случае, когда вы будете делать коммит, содержащий файлы, соответствующие шаблону *.c, Git прогонит их через программу indent перед коммитом, а потом через программу cat перед тем как выгрузить их на диск. Программа cat по сути является холостой — она выдаёт те же данные, которые получила. Фактически эта комбинация профильтровывает все файлы с исходным кодом на C через indent перед тем, как сделать коммит.

Ещё один интересный пример — это развёртывание ключа $Date$ в стиле RCS. Чтобы сделать его правильно, нам понадобится небольшой сценарий, который принимает на вход имя файла, определяет дату последнего коммита в проекте и вставляет эту дату в наш файл. Вот небольшой сценарий на Ruby, который делает именно это:

#! /usr/bin/env ruby data = STDIN.read last_date = `git log --pretty=format:"%ad" -1` puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

Всё, что делает этот сценарий, это получает дату последнего коммита с помощью команды git log, засовывает её во все строки $Date$, которые видит в stdin, и выводит результат — такое должно быть несложно реализовать на любом удобном вам языке. Давайте назовём этот файл expand_date и поместим в путь. Теперь в Git'е необходимо настроить фильтр (назовём его dater) и указать, что надо использовать фильтр expand_date при выполнении smudge во время выгрузки файлов. Воспользуемся регулярным выражением Perl, чтобы убрать изменения при коммите:

$ git config filter.dater.smudge expand_date $ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

Этот фрагмент кода на Perl'е вырезает всё, что находит в строке $Date$ так, чтобы вернуть всё в начальное состояние. Теперь, когда наш фильтр готов, можете протестировать его, создав файл с ключом $Date$ и установив для этого файла Git-атрибут, который задействует для него новый фильтр:

$ echo '# $Date$' > date_test.txt $ echo 'date*.txt filter=dater' >> .gitattributes

Если мы сейчас добавим эти изменения в коммит и снова выгрузим файл, то мы увидим, что ключевое слово было заменено правильно:

$ git add date_test.txt .gitattributes $ git commit -m "Testing date expansion in Git" $ rm date_test.txt $ git checkout date_test.txt $ cat date_test.txt # $Date: Tue Apr 21 07:26:52 2009 -0700$

Как видите, такая техника может быть весьма мощной для настройки проекта под свои нужды. Но вы должны быть осторожны, ибо файл .gitattributes вы добавите в коммит и будете его распространять вместе с проектом, а драйвер (в нашем случае dater) — нет. Так что не везде оно будет работать. Когда будете проектировать свои фильтры, постарайтесь сделать так, чтобы при возникновении в них ошибки проект не переставал работать правильно.

Экспорт репозитория

Ещё атрибуты в Git позволяют делать некоторые интересные вещи при экспортировании архива с проектом.

export-ignore

Вы можете попросить Git не экспортировать определённые файлы и каталоги при создании архива. Если у вас есть подкаталог или файл, который вы не желаете включать в архив, но хотите, чтобы в проекте он был, можете установить для такого файла атрибут export-ignore.

Например, скажем, у вас в подкаталоге test/ имеются некоторые тестовые файлы, и нет никакого смысла добавлять их в тарбол при экспорте проекта. Тогда добавим следующую строку в файл с Git-атрибутами:

test/ export-ignore

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

export-subst

Ещё одна вещь, которую можно сделать с архивами, — это сделать какую-нибудь простую подстановку ключевых слов. Git позволяет добавить в любой файл строку вида $Format:$ с любыми кодами форматирования, доступными в --pretty=format (многие из этих кодов мы рассматривали в Главе 2). Например, если вам захотелось добавить в проект файл с именем LAST_COMMIT, в который при запуске git archive будет автоматически помещаться дата последнего коммита, то такой файл вы можете сделать следующим образом:

$ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT $ echo "LAST_COMMIT export-subst" >> .gitattributes $ git add LAST_COMMIT .gitattributes $ git commit -am 'adding LAST_COMMIT file for archives'

После запуска git archive этот файл у вас в архиве будет иметь содержимое следующего вида:

$ cat LAST_COMMIT Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$

Стратегии слияния

Атрибуты Git могут также быть использованы для того, чтобы попросить Git использовать другие стратегии слияния для определённых файлов в проекте. Одна очень полезная возможность — это сказать Git'у, чтобы он не пытался слить некоторые файлы, если для них есть конфликт, а просто выбрал ваш вариант, предпочтя его чужому.

Это полезно в том случае, если ветка в вашем проекте разошлась с исходной, но вам всё же хотелось бы иметь возможность слить изменения из неё обратно, проигнорировав некоторые файлы. Скажем, у вас есть файл с настройками базы данных, который называется database.xml, и в двух ветках он разный, и вы хотите влить другую свою ветку, не трогая файл с настройками базы данных. Задайте атрибут следующим образом:

database.xml merge=ours

При вливании другой ветки, вместо конфликтов слияния для файла database.xml, вы увидите следующее:

$ git merge topic Auto-merging database.xml Merge made by recursive.

В данном случае database.xml остался в том варианте, в каком и был изначально.

Перехватчики в Git

Как и во многих других системах управления версиями, в Git есть возможность запускать собственные сценарии в те моменты, когда происходят некоторые важные действия. Существуют две группы подобных перехватчиков (hook): на стороне клиента и на стороне сервера. Перехватчики на стороне клиента предназначены для клиентских операций, таких как создание коммита и слияние. Перехватчики на стороне сервера нужны для серверных операций, таких как приём отправленных коммитов. Перехватчики могут быть использованы для выполнения самых различных задач. О некоторых из таких задач мы и поговорим.

Установка перехватчика

Все перехватчики хранятся в подкаталоге hooks в Git-каталоге. В большинстве проектов это .git/hooks. По умолчанию Git заполняет этот каталог кучей примеров сценариев, многие из которых полезны сами по себе, но кроме того в них задокументированы входные значения для каждого из сценариев. Все эти примеры являются сценариями для командной оболочки с вкраплениями Perl'а, но вообще-то будет работать любой исполняемый сценарий с правильным именем — вы можете писать их на Ruby или Python или на чём-то ещё, что вам нравится. В версиях Git'а старше 1.6 эти файлы с примерами перехватчиков оканчиваются на .sample; вам надо их переименовать. Для версий Git'а меньше чем 1.6 файлы с примерами имеют правильные имена, но не имеют прав на исполнение.

Чтобы активировать сценарий-перехватчик, положите файл в подкаталог hooks в Git-каталоге, дайте ему правильное имя и права на исполнение. С этого момента он будет вызываться. Основные имена перехватчиков мы сейчас рассмотрим.

Перехватчики на стороне клиента

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

Перехватчики для работы с коммитами

Первые четыре перехватчика относятся к процессу создания коммита. Перехватчик pre-commit запускается первым, ещё до того, как вы наберёте сообщение коммита. Его используют для проверки снимка состояния перед тем, как сделать коммит, чтобы проверить не забыли ли вы что-нибудь, чтобы убедиться, что вы запустили тесты, или проверить в коде ещё что-нибудь, что вам нужно. Завершение перехватчика с ненулевым кодом прерывает создание коммита, хотя вы можете обойти это с помощью git commit --no-verify. Можно, например, проверить стиль кодирования (запускать lint или что-нибудь аналогичное), проверить наличие пробельных символов в конце строк (перехватчик по умолчанию занимается именно этим) или проверить наличие необходимой документации для новых методов.

Перехватчик prepare-commit-msg запускается до появления редактора с сообщением коммита, но после создания сообщения по умолчанию. Он позволяет отредактировать сообщение по умолчанию перед тем, как автор коммита его увидит. У этого перехватчика есть несколько опций: путь к файлу, в котором сейчас хранится сообщение коммита, тип коммита и SHA-1 коммита (если в коммит вносится правка с помощью git commit --amend). Как правило данный перехватчик не представляет пользы для обычных коммитов; он скорее хорош для коммитов с автогенерируемыми сообщениями, такими как шаблонные сообщения коммитов, коммиты-слияния, уплотнённые коммиты (squashed commits) и коммиты c исправлениями (amended commits). Данный перехватчик можно использовать в связке с шаблоном для коммита, чтобы программно добавлять в него информацию.

Перехватчик commit-msg принимает один параметр, и снова это путь к временному файлу, содержащему текущее сообщение коммита. Когда сценарий завершается с ненулевым кодом, Git прерывает процесс создания коммита. Так что можно использовать его для проверки состояния проекта или сообщений коммита перед тем, как его одобрить. В последнем разделе главы я продемонстрирую, как использовать данный перехватчик, чтобы проверить, что сообщение коммита соответствует требуемому шаблону.

После того, как весь процесс создания коммита завершён, запускается перехватчик post-commit. Он не принимает никаких параметров, но вы с лёгкостью можете получить последний коммит, выполнив git log -1 HEAD. Как правило, этот сценарий используется для уведомлений или чего-то в этом роде.

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

Перехватчики для работы с e-mail

Для рабочих процессов, основанных на электронной почте, есть три специальных клиентских перехватчика. Все они вызываются командой git am, так что, если вы не пользуетесь этой командой в процессе своей работы, то можете смело переходить к следующему разделу. Если вы принимаете патчи, отправленные по e-mail и подготовленные с помощью git format-patch, то некоторые из них могут оказать для вас полезными.

Первый запускаемый перехватчик — это applypatch-msg. Он принимает один аргумент — имя временного файла, содержащего предлагаемое сообщение коммита. Git прерывает наложение патча, если сценарий завершается с ненулевым кодом. Это может быть использовано для того, чтобы убедиться, что сообщение коммита правильно отформатировано или, чтобы нормализовать сообщение, отредактировав его на месте из сценария.

Следующий перехватчик, запускаемый во время наложения патчей с помощью git am — это pre-applypatch. У него нет аргументов, и он запускается после того, как патч наложен, поэтому его можно использовать для проверки снимка состояния перед созданием коммита. Можно запустить тесты или как-то ещё проверить рабочее дерево с помощью этого сценария. Если чего-то не хватает, или тесты не пройдены, выход с ненулевым кодом так же завершает сценарий git am без применения патча.

Последний перехватчик, запускаемый во время работы git am — это post-applypatch. Его можно использовать для уведомления группы или автора патча о том, что вы его применили. Этим сценарием процесс наложения патча остановить уже нельзя.

Другие клиентские перехватчики

Перехватчик pre-rebase запускается перед перемещением чего-либо, и может остановить процесс перемещения, если завершится с ненулевым кодом. Этот перехватчик можно использовать, чтобы запретить перемещение любых уже отправленных коммитов. Пример перехватчика pre-rebase, устанавливаемый Git'ом, это и делает, хотя он предполагает, что ветка, в которой вы публикуете свои изменения, называется next. Вам скорее всего нужно будет заменить это имя на имя своей публичной стабильной ветки.

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

И наконец, перехватчик post-merge запускается после успешного выполнения команды merge. Его можно использовать для восстановления в рабочем дереве данных, которые Git не может отследить, таких как информация о правах. Этот перехватчик может также проверить наличие внешних по отношению к контролируемым Git'ом файлов, которые вам нужно скопировать в каталог при изменениях рабочего дерева.

Перехватчики на стороне сервера

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

pre-receive и post-receive

Первым сценарий, который выполняется при обработке отправленных клиентом данных, — это pre-receive. Он принимает на вход из stdin список отправленных ссылок; если он завершается с ненулевым кодом, ни одна из них не будет принята. Этот перехватчик можно использовать, чтобы, например, убедиться, что ни одна из обновлённых ссылок не выполняет ничего кроме перемотки, или, чтобы убедиться, что пользователь, запустивший git push, имеет права на создание, удаление или изменение для всех файлов модифицируемых этим push'ем.

Перехватчик post-receive запускается после того, как весь процесс завершился, и может быть использован для обновления других сервисов или уведомления пользователей. Он получает на вход из stdin те же данные, что и перехватчик pre-receive. Примерами использования могут быть: отправка писем в рассылку, уведомление сервера непрерывной интеграции или обновление карточки (ticket) в системе отслеживания ошибок — вы можете даже анализировать сообщения коммитов, чтобы выяснить, нужно ли открыть, изменить или закрыть какие-то карточки. Этот сценарий не сможет остановить процесс приёма данных, но клиент не будет отключён до тех пор, пока процесс не завершится; так что будьте осторожны, если хотите сделать что-то, что может занять много времени.

update

Сценарий update очень похож на сценарий pre-receive, за исключением того, что он выполняется для каждой ветки, которую отправитель данных пытается обновить. Если отправитель пытается обновить несколько веток, то pre-receive выполнится только один раз, в то время как update выполнится по разу для каждой обновляемой ветки. Сценарий не считывает параметры из stdin, а принимает на вход три аргумента: имя ссылки (ветки), SHA-1, на которую ссылка указывала до запуска push, и тот SHA-1, который пользователь пытается отправить. Если сценарий update завершится с ненулевым кодом, то только одна ссылка будет отклонена, остальные ссылки всё ещё смогут быть обновлены.

Пример навязывания политики с помощью Git

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

Для их написания я использовал Ruby, и потому что это мой любимый язык сценариев, и потому что из всех языков сценариев он больше всего похож на псевдокод; таким образом, код должен быть вам понятен в общих чертах, даже если вы не пользуетесь Ruby. Однако любой язык сгодится. Все примеры перехватчиков, распространяемые вместе с Git'ом, написаны либо на Perl, либо на Bash, так что вы сможете просмотреть достаточно примеров перехватчиков на этих языках, заглянув в примеры.

Перехватчик на стороне сервера

Вся работа для сервера будет осуществляться в файле update из каталога hooks. Файл update запускается по разу для каждой отправленной ветки и принимает на вход ссылку, в которую сделано отправление, старую версию, на которой ветка находилась раньше, и новую присланную версию. Кроме того, вам будет доступно имя пользователя, приславшего данные, если push был выполнен по SSH. Если вы позволили подключаться всем под одним пользователем (например, "git") с аутентификацией по открытому ключу, то вам может понадобиться создать для этого пользователя обёртку командной оболочки, которая на основе открытого ключа будет определять, какой пользователь осуществил подключение, и записывать этого пользователя в какой-нибудь переменной окружения. Тут я буду предполагать, что имя подключившегося пользователя находится в переменной окружения $USER, так что начнём наш сценарий со сбора всей необходимой информации:

#!/usr/bin/env ruby $refname = ARGV[0] $oldrev = ARGV[1] $newrev = ARGV[2] $user = ENV['USER'] puts "Enforcing Policies... \n(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"

Да, я использую глобальные переменные. Не судите строго — в таком виде получается нагляднее.

Установка особого формата сообщений коммитов

Первая наша задача — это заставить все сообщения коммитов обязательно придерживаться определённого формата. Просто чтобы было чем заняться, предположим, что каждое сообщение должно содержать строку вида "ref: 1234", так как мы хотим, чтобы каждый коммит был связан с некоторым элементом в нашей системе с карточками. Нам необходимо просмотреть все присланные коммиты, выяснить, есть ли такая строка в сообщении коммита, и, если строка отсутствует в каком-либо из этих коммитов, то завершить сценарий с ненулевым кодом, чтобы push был отклонён.

Список значений SHA-1 для всех присланных коммитов можно получить, взяв значения $newrev и $oldrev и передав их служебной команде git rev-list. По сути, это команда git log, но по умолчанию она выводит только SHA-1 значения и больше ничего. Таким образом, чтобы получить список SHA для всех коммитов, сделанных между одним SHA коммита и другим, достаточно выполнить следующее:

$ git rev-list 538c33..d14fc7 d14fc7c847ab946ec39590d87783c69b031bdfb7 9f585da4401b0a3999e84113824d15245c13f0be 234071a1be950e2a8d078e6141f5cd20c1e61ad3 dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a 17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

Можно взять этот вывод, пройти в цикле по SHA-хешам всех этих коммитов, беря их сообщения и проверяя с помощью регулярного выражения, совпадает ли сообщение с шаблоном.

Нам нужно выяснить, как из всех этих коммитов получить их сообщения, для того, чтобы их протестировать. Чтобы получить данные коммита в сыром виде, можно воспользоваться ещё одной служебной командой, которая называется git cat-file. Мы рассмотрим все эти служебные команды более подробно в Главе 9, но пока что, вот, что эта команда нам выдала:

$ git cat-file commit ca82a6 tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 author Scott Chacon 1205815931 -0700 committer Scott Chacon 1240030591 -0700 changed the version number

Простой способ получить сообщение коммита для коммита, чьё значение SHA-1 известно, — это дойти в выводе команды git cat-file до первой пустой строки и взять всё, что идёт после неё. В Unix-системах это можно сделать с помощью команды sed:

$ git cat-file commit ca82a6 | sed '1,/^$/d' changed the version number

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

$regex = /\[ref: (\d+)\]/ # принуждает использовать особый формат сообщений def check_message_format missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") missed_revs.each do |rev| message = `git cat-file commit #{rev} | sed '1,/^$/d'` if !$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end end end check_message_format

Добавив это в свой сценарий update, мы запретим обновления, содержащие коммиты, сообщения которых не соблюдают наше правило.

Настройка системы контроля доступа для пользователей

Предположим, что нам хотелось бы добавить какой-нибудь механизм для использования списков контроля доступа (ACL), где указано, какие пользователи могут отправлять изменения и в какие части проекта. Несколько людей будут иметь полный доступ, а остальные будут иметь доступ на изменение только некоторых подкаталогов или отдельных файлов. Чтобы обеспечить выполнение такой политики, мы запишем правила в файл acl, который будет находиться в нашем "голом" репозитории на сервере. Нам нужно будет, чтобы перехватчик update брал эти правила, смотрел на то, какие файлы были изменены присланными коммитами, и определял, имеет ли пользователь, выполнивший push, право на обновление всех этих файлов.

Первое, что мы сделаем, — это напишем свой ACL. Мы сейчас будем использовать формат очень похожий на механизм ACL в CVS. В нём используется последовательность строк, где первое поле — это avail или unavail, следующее поле — это разделённый запятыми список пользователей, для которых применяется правило, и последнее поле — это путь, к которому применяется правило (пропуск здесь означает открытый доступ). Все эти поля разделяются вертикальной чертой (|).

В нашем примере будет несколько администраторов, сколько-то занимающихся написанием документации с доступом к каталогу doc и один разработчик, который имеет доступ только к каталогам lib и tests, и наш файл acl будет выглядеть так:

avail|nickh,pjhyett,defunkt,tpw avail|usinclair,cdickens,ebronte|doc avail|schacon|lib avail|schacon|tests

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

def get_acl_access_data(acl_file) # считывание данных ACL acl_file = File.read(acl_file).split("\n").reject { |line| line == '' } access = {} acl_file.each do |line| avail, users, path = line.split('|') next unless avail == 'avail' users.split(',').each do |user| access[user] ||= [] access[user] << path end end access end

Для рассмотренного ранее ACL-файла, метод get_acl_access_data вернёт структуру данных следующего вида:

{"defunkt"=>[nil], "tpw"=>[nil], "nickh"=>[nil], "pjhyett"=>[nil], "schacon"=>["lib", "tests"], "cdickens"=>["doc"], "usinclair"=>["doc"], "ebronte"=>["doc"]}

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

Мы довольно легко можем определить, какие файлы были изменены в одном коммите, с помощью опции --name-only для команды git log (мы упоминали о ней в Главе 2):

$ git log -1 --name-only --pretty=format:'' 9f585d README lib/test.rb

Если мы воспользуемся ACL-структурой, полученной из метода get_acl_access_data, и сверим её со списком файлов для каждого коммита, то мы сможем определить, имеет ли пользователь право на отправку своих коммитов:

# некоторые подкаталоги в проекте разрешено модифицировать только определённым пользователям def check_directory_perms access = get_acl_access_data('acl') # проверим, что никто не пытается прислать чего-то, что ему нельзя new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n") new_commits.each do |rev| files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n") files_modified.each do |path| next if path.size == 0 has_file_access = false access[$user].each do |access_path| if !access_path # пользователь имеет полный доступ || (path.index(access_path) == 0) # доступ к этому пути has_file_access = true end end if !has_file_access puts "[POLICY] You do not have access to push to #{path}" exit 1 end end end end check_directory_perms

Большую часть этого кода должно быть не сложно понять. Мы получаем список присланных на сервер коммитов с помощью git rev-list. Затем для каждого из них мы узнаём, какие файлы были изменены, и убеждаемся, что пользователь, сделавший push, имеет доступ ко всем изменённым путям. Один Ruby'изм, который может быть непонятен это path.index(access_path) == 0. Это условие верно, если path начинается с access_path — оно гарантирует, что access_path это не просто один из разрешённых путей, а что каждый путь, к которому запрашивается доступ, начинается с одного из разрешённых путей.

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

Разрешение только обновлений-перемоток

Единственное, что нам осталось — это оставить доступными только обновления-перемотки. В Git версии 1.6 и более новых можно просто задать настройки receive.denyDeletes и receive.denyNonFastForwards. Но осуществление этого с помощью перехватчика будет работать и в старых версиях Git, и к тому же вы сможете изменить его так, чтобы запрет действовал только для определённых пользователей, или ещё как-то, как вам захочется.

Логика здесь такая — мы проверяем, есть ли такие коммиты, которые достижимы из старой версии и не достижимы из новой. Если таких нет, то сделанный push был перемоткой; в противном случае мы его запрещаем:

# разрешаем только обновления-перемотки def check_fast_forward missed_refs = `git rev-list #{$newrev}..#{$oldrev}` missed_ref_count = missed_refs.split("\n").size if missed_ref_count > 0 puts "[POLICY] Cannot push a non fast-forward reference" exit 1 end end check_fast_forward

Всё готово. Если вы выполните chmod u+x .git/hooks/update (а это тот файл, в который вы должны были поместить весь наш код) и затем попытаетесь отправить ссылку, для которой нельзя выполнить перемотку, то вы получите что-то типа такого:

$ git push -f origin master Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 323 bytes, done. Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. Enforcing Policies... (refs/heads/master) (8338c5) (c5b616) [POLICY] Cannot push a non-fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master To git@gitserver:project.git ! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git'

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

Enforcing Policies... (refs/heads/master) (fb8c72) (c56860)

Обратите внимание, что мы выводили это в stdout в самом начале нашего сценария update. Важно отметить, что всё, что сценарий выводит в stdout, будет передано клиенту.

Следующая вещь, которую мы видим, это сообщение об ошибке:

[POLICY] Cannot push a non fast-forward reference error: hooks/update exited with error code 1 error: hook declined to update refs/heads/master

Первую строку напечатали мы, а в остальных двух Git сообщает, что сценарий update завершился с ненулевым кодом, и это именно то, что отклонило ваш push. И наконец мы видим это:

To git@gitserver:project.git ! [remote rejected] master -> master (hook declined) error: failed to push some refs to 'git@gitserver:project.git'

Сообщение "remote rejected" будет появляться для каждой отклонённой перехватчиком ссылки. Оно сообщает нам, что ссылка была отклонена именно из-за сбоя в перехватчике.

Кроме того, при отсутствии отметки "ref" в каком-либо из коммитов, вы увидите сообщение об ошибке, которое мы для этого напечатали.

[POLICY] Your message is not formatted correctly

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

[POLICY] You do not have access to push to lib/test.rb

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

Перехватчики на стороне клиента

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

Решение данной проблемы — предоставить пользователям какие-нибудь перехватчики, которые будут работать на стороне пользователя и будут сообщать ему, если он делает что-то, что скорее всего будет отклонено. При таком подходе, пользователи смогут исправить любые проблемы до создания коммита и до того, как эти проблемы станет сложно исправить. Так как перехватчики не пересылаются при клонировании проекта, вам придётся распространять эти сценарии каким-то другим способом и потом сделать так, чтобы ваши пользователи скопировали их в свой каталог .git/hooks и сделали их исполняемыми. Эти перехватчики можно поместить в свой проект или даже в отдельный проект, но способа установить их автоматически не существует.

Для начала, перед записью каждого коммита нам надо проверить его сообщение, чтобы быть уверенным, что сервер не отклонит изменения из-за плохо отформатированного сообщения коммита. Чтобы сделать это, добавим перехватчик commit-msg. Если мы сможем прочитать сообщение из файла, переданного в качестве первого аргумента, и сравнить его с шаблоном, то можно заставить Git прервать создание коммита при обнаружении несовпадения:

#!/usr/bin/env ruby message_file = ARGV[0] message = File.read(message_file) $regex = /\[ref: (\d+)\]/ if !$regex.match(message) puts "[POLICY] Your message is not formatted correctly" exit 1 end

Если этот сценарий находится на своём месте (в .git/hooks/commit-msg) и имеет права на исполнение, то при создании коммита с неправильно оформленным сообщением, вы увидите это:

$ git commit -am 'test' [POLICY] Your message is not formatted correctly

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

$ git commit -am 'test [ref: 132]' [master e05c914] test [ref: 132] 1 files changed, 1 insertions(+), 0 deletions(-)

Далее мы хотим убедиться, что пользователь не модифицирует файлы вне своей области, заданной в ACL. Если в проекте в каталоге .git уже есть копия файла acl, который мы использовали ранее, то сценарий pre-commit следующего вида применит эти ограничения:

#!/usr/bin/env ruby $user = ENV['USER'] # [ insert acl_access_data method from above ] # некоторые подкаталоги в проекте разрешено модифицировать только определённым пользователям def check_directory_perms access = get_acl_access_data('.git/acl') files_modified = `git diff-index --cached --name-only HEAD`.split("\n") files_modified.each do |path| next if path.size == 0 has_file_access = false access[$user].each do |access_path| if !access_path || (path.index(access_path) == 0) has_file_access = true end if !has_file_access puts "[POLICY] You do not have access to push to #{path}" exit 1 end end end check_directory_perms

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

access = get_acl_access_data('acl')

на этот:

access = get_acl_access_data('.git/acl')

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

files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`

мы должны использовать

files_modified = `git diff-index --cached --name-only HEAD`

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

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

Так как сервер в любом случае сообщит вам о том, что нельзя отправлять обновления, не являющиеся перемоткой, а перехватчик запрещает принудительные push'и, единственная оплошность, которую вы можете попробовать предотвратить, это перемещение коммитов, которые уже были отправлены на сервер.

Вот пример сценария pre-rebase, который это проверяет. Он принимает на вход список всех коммитов, которые вы собираетесь переписать, и проверяет, нет ли их в какой-нибудь из ваших удалённых веток. Если найдётся такой коммит, который достижим из одной из удалённых веток, сценарий прервёт выполнение перемещения:

#!/usr/bin/env ruby base_branch = ARGV[0] if ARGV[1] topic_branch = ARGV[1] else topic_branch = "HEAD" end target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n") remote_refs = `git branch -r`.split("\n").map { |r| r.strip } target_shas.each do |sha| remote_refs.each do |remote_ref| shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}` if shas_pushed.split(“\n”).include?(sha) puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}" exit 1 end end end

Этот сценарий использует синтаксис, который мы не рассматривали в разделе "Выбор ревизии" в Главе 6. Мы получили список коммитов, которые уже были отправлены на сервер, выполнив это:

git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}

Запись SHA^@ означает всех родителей указанного коммита. Мы ищем какой-нибудь коммит, который достижим из последнего коммита в удалённой ветке и не достижим ни из одного из родителей какого-либо SHA, который вы пытаетесь отправить на сервер — это значит, что это перемотка.

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

Итоги

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

Git и другие системы управления версиями

Наш мир несовершенен. Как правило, вы не сможете моментально перевести любой проект, в котором вы участвуете, на использование Git. Иногда вам придется иметь дело с проектами, использующими другую систему контроля версий, и, в большинстве случаев, этой системой будет Subversion. Первая часть этого раздела научит вас обращаться с git svn — встроенным в Git двухсторонним интерфейсом обмена с Subversion.

В какой-то момент, вы, возможно, захотите перевести свой существующий проект на Git. Вторая часть раздела расскажет о том, как провести миграцию: сначала с Subversion, потом с Perforce, и наконец, с помощью написания собственного сценария для нестандартных вариантов миграции.

Git и Subversion

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

Одна из замечательных особенностей Git — возможность двустороннего обмена с Subversion через интерфейс, называемый git svn. Этот инструмент позволяет вам использовать Git в качестве корректного клиента при работе с сервером Subversion. Таким образом, вы можете пользоваться всеми локальными возможностями Git, а затем сохранять изменения на сервере Subversion так, как если бы использовали Subversion локально. То есть вы можете делать локальное ветвление и слияние, использовать индекс, перемещение и отбор патчей для переноса из одной ветви в другую (cherry-picking) и т.д., в то время, как ваши коллеги будут продолжать использовать в разработке подход времён каменного века. Это хороший способ протащить Git в рабочее окружение своей компании, чтобы помочь коллегам разработчикам стать более эффективными, в то время как вы будете лоббировать переход полностью на Git. Интерфейс обмена с Subversion это ворота в мир распределённых систем управления версиями.

git svn

Базовой командой в Git для всех команд работающих с мостом к Subversion является git svn. Ей предваряется любая команда. Она принимает довольно порядочное число команд, поэтому мы изучим из них, те которые наиболее часто используются, рассмотрев несколько небольших вариантов работы.

Важно отметить, что при использовании git svn, вы взаимодействуете с Subversion — системой, которая намного менее «продвинута», чем Git. Хоть вы и умеете с лёгкостью делать локальное ветвление и слияние, как правило лучше всего держать свою историю в как можно более линейном виде, используя перемещения (rebase) и избегая таких вещей, как одновременный обмен с удалённым репозиторием Git.

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

Настройка

Для того, чтобы попробовать этот функционал в действии, вам понадобится доступ с правами на запись к обычному SVN-репозиторию. Если вы хотите повторить рассматриваемые примеры, вам нужно сделать доступную на запись копию моего тестового репозитория. Это можно сделать без труда с помощью утилиты svnsync, входящей в состав последних версий Subversion (по крайней мере после версии 1.4). Для этих примеров, я создал новый Subversion-репозиторий на Google Code, который был частичной копией проекта protobuf (утилита шифрования структурированных данных для их передачи по сети).

Чтобы мы могли продолжить, прежде всего создайте новый локальный репозиторий Subversion:

$ mkdir /tmp/test-svn $ svnadmin create /tmp/test-svn

Затем разрешите всем пользователям изменять revprops — самым простым способом сделать это будет добавление сценария pre-revprop-change, который просто всегда завершается с кодом 0:

$ cat /tmp/test-svn/hooks/pre-revprop-change #!/bin/sh exit 0; $ chmod +x /tmp/test-svn/hooks/pre-revprop-change

Теперь вы можете синхронизировать проект со своей локальной машиной, вызвав svnsync init с параметрами задающими исходный и целевой репозиторий:

$ svnsync init file:///tmp/test-svn http://progit-example.googlecode.com/svn/

Эта команда подготовит процесс синхронизации. Затем склонируйте код выполнив:

$ svnsync sync file:///tmp/test-svn Committed revision 1. Copied properties for revision 1. Committed revision 2. Copied properties for revision 2. Committed revision 3. ...

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

Приступим к работе

Теперь, когда в вашем распоряжении имеется SVN-репозиторий, для которого вы имеете право на запись, давайте выполним типичные действия по работе с СУВ. Начнём с команды git svn clone, которая импортирует весь SVN-репозиторий в локальный Git-репозиторий. Помните, что если вы производите импорт из настоящего удалённого SVN-репозитория, вам надо заменить file:///tmp/test-svn на реальный адрес вашего SVN-репозитория:

$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags Initialized empty Git repository in /Users/schacon/projects/testsvnsync/svn/.git/ r1 = b4e387bc68740b5af56c2a5faf4003ae42bd135c (trunk) A m4/acx_pthread.m4 A m4/stl_hash.m4 ... r75 = d1957f3b307922124eec6314e15bcda59e3d9610 (trunk) Found possible branch point: file:///tmp/test-svn/trunk => \ file:///tmp/test-svn /branches/my-calc-branch, 75 Found branch parent: (my-calc-branch) d1957f3b307922124eec6314e15bcda59e3d9610 Following parent with do_switch Successfully followed parent r76 = 8624824ecc0badd73f40ea2f01fce51894189b01 (my-calc-branch) Checked out HEAD: file:///tmp/test-svn/branches/my-calc-branch r76

Эта команда эквивалентна выполнению для указанного вами URL двух команд — git svn init, а затем git svn fetch. Процесс может занять некоторое время. Тестовый проект имеет всего лишь около 75 коммитов, и кода там не очень много, так что скорее всего, вам придётся подождать всего несколько минут. Однако, Git должен по отдельности проверить и выполнить коммит для каждой версии. Для проектов, имеющих историю с сотнями и тысячами изменений, этот процесс может занять несколько часов или даже дней.

Часть команды -T trunk -b branches -t tags сообщает Git, что этот SVN-репозиторий следует стандартным соглашениям о ветвлении и метках. Если вы используете не стандартные имена: trunk, branches и tags, а какие-то другие, то должны изменить эти параметры соответствующим образом. В связи с тем, что такие соглашения являются общепринятыми, вы можете использовать короткий формат, заменив всю эту часть на -s, заменяющую собой все эти параметры. Следующая команда эквивалента предыдущей:

$ git svn clone file:///tmp/test-svn -s

Таким образом, вы должны были получить корректный Git-репозиторий с импортированными ветками и метками:

$ git branch -a * master my-calc-branch tags/2.0.2 tags/release-2.0.1 tags/release-2.0.2 tags/release-2.0.2rc1 trunk

Важно отметить, что эта утилита именует ваши ссылки на удалённые ресурсы по-другому. Когда вы клонируете обычный репозиторий Git, вы получаете все ветки с удалённого сервера на локальном компьютере в виде: origin/[branch] — в пространстве имён с именем удалённого сервера. Однако, git svn полагает, что у вас не будет множества удалённых источников данных и сохраняет все ссылки на всякое, находящееся на удалённом сервере, без пространства имён. Для просмотра всех имён ссылок вы можете использовать служебную команду Git show-ref:

$ git show-ref 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/heads/master aee1ecc26318164f355a883f5d99cff0c852d3c4 refs/remotes/my-calc-branch 03d09b0e2aad427e34a6d50ff147128e76c0e0f5 refs/remotes/tags/2.0.2 50d02cc0adc9da4319eeba0900430ba219b9c376 refs/remotes/tags/release-2.0.1 4caaa711a50c77879a91b8b90380060f672745cb refs/remotes/tags/release-2.0.2 1c4cb508144c513ff1214c3488abe66dcb92916f refs/remotes/tags/release-2.0.2rc1 1cbd4904d9982f386d87f88fce1c24ad7c0f0471 refs/remotes/trunk

Обычный Git-репозиторий выглядит скорее так:

$ git show-ref 83e38c7a0af325a9722f2fdc56b10188806d83a1 refs/heads/master 3e15e38c198baac84223acfc6224bb8b99ff2281 refs/remotes/gitserver/master 0a30dd3b0c795b80212ae723640d4e5d48cabdff refs/remotes/origin/master 25812380387fdd55f916652be4881c6f11600d6f refs/remotes/origin/testing

Здесь два удалённых сервера: один с именем gitserver и веткой master, и другой с именем origin с двумя ветками: master и testing.

Обратите внимание, что в примере, где ссылки импортированы из git svn, метки добавлены так, как будто они являются ветками, а не так, как настоящие метки в Git. Импортированные из Subversion данные выглядят так, как будто под именами меток с удалённого ресурса скрываются ветки.

Коммит в Subversion

Теперь, когда у вас есть рабочий репозиторий, вы можете выполнить какую-либо работу с кодом и выполнить коммит в апстрим, эффективно используя Git в качестве клиента SVN. Если вы редактировали один из файлов и закоммитили его, то вы внесли изменение в локальный репозиторий Git, которое пока не существует на сервере Subversion:

$ git commit -am 'Adding git-svn instructions to the README' [master 97031e5] Adding git-svn instructions to the README 1 files changed, 1 insertions(+), 1 deletions(-)

После этого, вам надо отправить изменения в апстрим. Обратите внимание, как Git изменяет способ работы с Subversion — вы можете сделать несколько коммитов оффлайн, а затем отправить их разом на сервер Subversion. Для передачи изменений на сервер Subversion требуется выполнить команду git svn dcommit:

$ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M README.txt Committed r79 M README.txt r79 = 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk

Это действие возьмёт все коммиты, сделанные поверх того, что есть в SVN-репозитории, выполнит коммит в Subversion для каждого из них, а затем перепишет ваш локальный коммит в Git, чтобы добавить к нему уникальный идентификатор. Это важно, поскольку это означает, что изменятся все SHA-1 контрольные суммы ваших коммитов. В частности и поэтому работать с одним и тем же проектом одновременно и через Git, и через Subversion это плохая идея. Взглянув на последний коммит, вы увидите, что добавился новый git-svn-id:

$ git log -1 commit 938b1a547c2cc92033b74d32030e86468294a5c8 Author: schacon Date: Sat May 2 22:06:44 2009 +0000 Adding git-svn instructions to the README git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029

Обратите внимание — контрольная сумма SHA, которая начиналась с 97031e5 когда вы делали коммит, теперь начинается с 938b1a5. Если вы хотите отправить изменения как на Git-сервер, так и на SVN-сервер, вы должны отправить их (dcommit) сначала на сервер Subversion, поскольку это действие изменит отправляемые данные.

Получение новых изменений

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

$ git svn dcommit Committing to file:///tmp/test-svn/trunk ... Merge conflict during commit: Your file or directory 'README.txt' is probably \ out-of-date: resource out of date; try updating at /Users/schacon/libexec/git-\ core/git-svn line 482

Для разрешения этой проблемы, вам нужно выполнить команду git svn rebase, которая получит все изменения, имеющиеся на сервере, которых ещё нет на вашей локальной машине и переместит все ваши недавние изменения поверх того, что было на сервере:

$ git svn rebase M README.txt r80 = ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk) First, rewinding head to replay your work on top of it... Applying: first user change

Теперь все ваши изменения находятся сверху того, что есть на SVN-сервере, так что вы можете спокойно выполнить dcommit:

$ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M README.txt Committed r81 M README.txt r81 = 456cbe6337abe49154db70106d1836bc1332deed (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk

Следует помнить, что в отличие от Git'а, который требует сливать себе изменения в апстриме, которых у вас ещё нет локально, перед тем как отправить свои изменения, git svn заставляет делать такое только в случае конфликта правок. Если кто-либо внесёт изменения в один файл, а вы внесёте изменения в другой, команда dcommit сработает без ошибок:

$ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M configure.ac Committed r84 M autogen.sh r83 = 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk) M configure.ac r84 = cdbac939211ccb18aa744e581e46563af5d962d0 (trunk) W: d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs/remotes/trunk differ, \ using rebase: :100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 \ 015e4c98c482f0fa71e4d5434338014530b37fa6 M autogen.sh First, rewinding head to replay your work on top of it... Nothing to do.

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

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

$ git svn rebase M generate_descriptor_proto.sh r82 = bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk) First, rewinding head to replay your work on top of it... Fast-forwarded master to refs/remotes/trunk.

Выполняйте команду git svn rebase периодически, чтобы быть уверенным в том, что ваш код имеет самую свежую версию. Перед выполнением этой команды убедитесь, что ваш рабочий каталог чист. Если нет, вы должны либо «спрятать» свои изменения, либо временно закоммитить их перед выполнением git svn rebase, иначе выполнение этой команды прекратится, если она обнаружит возникновение конфликта слияния.

Проблемы с ветвлением в Git

После того как вы привыкли к работе с Git, вы наверняка будете создавать ветки для работы над отдельными задачами, а затем сливать их. Если вы отправляете изменения на сервер Subversion через git svn, вам скорее всего потребуется перемещать свою работу каждый раз в одну ветку, а не сливать ветки вместе. Причина, по которой предпочтение должно быть отдано именно такому подходу, заключается в том, что Subversion имеет линейную историю изменений и не может обрабатывать слияния так, как это делает Git. Таким образом git svn проходит только по первым родителям при конвертации снимков состояния в коммиты Subversion.

Допустим, что история изменений выглядит следующим образом: вы создали ветку experiment, сделали два коммита, а затем слили их в ветку master. Если вы выполните dcommit, результат будет следующим:

$ git svn dcommit Committing to file:///tmp/test-svn/trunk ... M CHANGES.txt Committed r85 M CHANGES.txt r85 = 4bfebeec434d156c36f2bcd18f4e3d97dc3269a2 (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk COPYING.txt: locally modified INSTALL.txt: locally modified M COPYING.txt M INSTALL.txt Committed r86 M INSTALL.txt M COPYING.txt r86 = 2647f6b86ccfcaad4ec58c520e369ec81f7c283c (trunk) No changes between current HEAD and refs/remotes/trunk Resetting to the latest refs/remotes/trunk

Выполнение dcommit для ветки с объединённой историей не вызовет никаких проблем. Однако, если вы посмотрите на историю проекта в Git, то увидите, что ни один из коммитов, которые вы сделали в ветке experiment не были переписаны — вместо этого, все эти изменения появятся в SVN версии как один объединённый коммит.

Когда кто-нибудь склонирует себе эту работу, всё, что он увидит — это коммит, в котором все изменения слиты воедино; он не увидит данных о том, откуда они взялись и когда они были внесены.

Ветвление в Subversion

Работа с ветвями в Subversion отличается от таковой в Git; если у вас есть возможность избегать её, то это наверное лучший вариант. Хотя, вы можете создавать и вносить изменения в ветки Subversion используя git svn.

Создание новой ветки в SVN

Для того, чтобы создать новую ветку в Subversion, выполните git svn branch [имя ветки]:

$ git svn branch opera Copying file:///tmp/test-svn/trunk at r87 to file:///tmp/test-svn/branches/opera... Found possible branch point: file:///tmp/test-svn/trunk => \ file:///tmp/test-svn/branches/opera, 87 Found branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f Following parent with do_switch Successfully followed parent r89 = 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera)

Эта команда эквивалентна команде Subversion svn copy trunk branches/opera и выполняется на сервере Subversion. Важно отметить, что эта команда не переключает вас на указанную ветку. Так что, если вы сейчас сделаете коммит, он попадёт на сервере в trunk, а не в opera.

Переключение активных веток

Git определяет ветку, куда вносятся ваши коммиты, путём выбора самой последней Subversion-ветки в вашей истории — она должна быть единственной и она должна быть последней в текущей истории веток, имеющей метку git-svn-id.

Если вы хотите работать одновременно с несколькими ветками, вы можете настроить локальные ветки на внесение изменений через dcommit в конкретные ветки Subversion, начиная их на основе импортированного SVN-коммита для нужной ветки. Если вам нужна ветка opera, в которой вы можете поработать отдельно, можете выполнить:

$ git branch opera remotes/opera

Теперь, если вы захотите слить ветку opera в trunk (вашу ветку master), вы можете сделать это с помощью обычной команды git merge. Однако вам потребуется добавить подробное описание к коммиту (через параметр -m), иначе при слиянии комментарий будет иметь вид «Merge branch opera», вместо чего-нибудь полезного.

Помните, что хотя вы и используете git merge для этой операции, и слияние скорее всего произойдёт намного проще, чем было бы в Subversion (потому, что Git автоматически определяет подходящую основу для слияния), это не является обычным коммитом-слиянием Git. Вы должны передать данные обратно на сервер Subversion, который не способен справиться с коммитом, имеющим более одного родителю, так что после передачи этот коммит будет выглядеть как один коммит, в который затолканы все изменения с другой ветки. После того, как вы сольёте одну ветку в другую, вы не сможете просто так вернуться к работе над ней, как вы могли бы в Git. Команда dcommit удаляет всю информацию о том, какая ветка была влита, так что последующие вычисления базы слияния будут неверными — команда dcommit сделает результаты выполнения git merge такими же, какими они были бы после выполнения git merge --squash. К сожалению, избежать подобной ситуации вряд ли удастся — Subversion не способен сохранять подобную информацию, так что вы всегда будете связаны этими ограничениями. Во избежание проблем вы должны удалить локальную ветку (в нашем случае opera) после того, как вы вольёте её в trunk.

Команды Subversion

Набор утилит git svn предоставляет в ваше распоряжение несколько команд для облегчения перехода на Git, путём предоставления функциональности, подобной той, которую вы имеете в Subversion. Ниже приведены несколько команд, которые дают вам то, что вы имели в Subversion.

Просмотр истории в стиле SVN

Если вы привыкли к Subversion и хотите просматривать историю в стиле SVN, выполните команду git svn log, чтобы увидеть историю коммитов в формате таком же как в SVN:

$ git svn log ------------------------------------------------------------------------ r87 | schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) | 2 lines autogen change ------------------------------------------------------------------------ r86 | schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May 2009) | 2 lines Merge branch 'experiment' ------------------------------------------------------------------------ r85 | schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May 2009) | 2 lines updated the changelog

Вы должны знать две важные вещи о команде git svn log. Во-первых, она работает в оффлайне, в отличие от оригинальной команды svn log, которая запрашивает информацию с сервера Subversion. Во-вторых, эта команда отображает только те коммиты, которые были переданы на сервер Subversion. Локальные коммиты Git, которые вы ещё не отправили с помощью dcommit не будут отображаться, равно как и коммиты, отправленные на сервер Subversion другими людьми с момента последнего выполнения dcommit. Результат действия этой команды скорее похож на последнее известное состояние изменений на сервере Subversion.

SVN-Аннотации

Так же как команда git svn log симулирует в оффлайне команду svn log, эквивалентом команды svn annotate является команда git svn blame [ФАЙЛ]. Её вывод выглядит следующим образом:

$ git svn blame README.txt 2 temporal Protocol Buffers - Google's data interchange format 2 temporal Copyright 2008 Google Inc. 2 temporal http://code.google.com/apis/protocolbuffers/ 2 temporal 22 temporal C++ Installation - Unix 22 temporal ======================= 2 temporal 79 schacon Committing in git-svn. 78 schacon 2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol 2 temporal Buffer compiler (protoc) execute the following: 2 temporal

Опять же, эта команда не показывает коммиты, которые вы сделали локально в Git или те, которые за то время были отправлены на Subversion-сервер.

Информация о SVN-сервере

Вы можете получить ту же информацию, которую даёт выполнение команды svn info, выполнив команду git svn info:

$ git svn info Path: . URL: https://schacon-test.googlecode.com/svn/trunk Repository Root: https://schacon-test.googlecode.com/svn Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029 Revision: 87 Node Kind: directory Schedule: normal Last Changed Author: schacon Last Changed Rev: 87 Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)

Так же, как blame и log, эта команда выполняется оффлайн и выводит информацию, актуальную на момент последнего вашего обращения к серверу Subversion.

Игнорирование того, что игнорирует Subversion

Если вы клонируете репозиторий Subversion, в котором где-то установлены свойства svn:ignore, скорее всего вы захотите создать соответствующие им файлы .gitignore, чтобы ненароком не добавить в коммит те файлы, которые не стоит добавлять. Для решения этой проблемы в git svn имеется две команды. Первая — git svn create-ignore — автоматически создаст соответствующие файлы .gitignore, и затем вы можете добавить их в свой следующий коммит.

Вторая команда — git svn show-ignore, которая выводит на стандартный вывод строки, которые вы должны включить в файл .gitignore. Таким образом вы можете перенаправить вывод этой команды в файл исключений вашего проекта:

$ git svn show-ignore > .git/info/exclude

Поступая таким образом, вы не захламляете проект файлами .gitignore. Это правильный подход, если вы являетесь единственным пользователем Git в команде, использующей Subversion, и ваши коллеги выступают против наличия файлов .gitignore в проекте.

Заключение по Git-Svn

Утилиты git svn полезны в том случае, если ваша разработка по каким-то причинам требует наличия рабочего сервера Subversion. Однако, вам стоит смотреть на Git использующий мост в Subversion как на урезанную версию Git. В противном случае вы столкнётесь с проблемами в преобразованиях, которые могут сбить с толку вас и ваших коллег. Чтобы избежать неприятностей, старайтесь следовать следующим рекомендациям:

Держите историю в Git линейной, чтобы она не содержала коммитов-слияний, сделанных с помощью git merge. Перемещайте всю работу, которую вы выполняете вне основной ветки обратно в неё; не выполняйте слияний.

Не устанавливайте отдельный Git-сервер для совместной работы. Можно иметь один такой сервер для того, чтобы ускорить клонирование для новых разработчиков, но не отправляйте на него ничего, не имеющего записи git-svn-id. Возможно, стоит даже добавить перехватчик pre-receive, который будет проверять каждый коммит на наличие git-svn-id и отклонять git push, если коммиты не имеют такой записи.

При следовании этим правилам, работа с сервером Subversion может быть более-менее сносной. Однако, если возможен перенос проекта на реальный сервер Git, преимущества от этого перехода дадут вашему проекту намного больше.

Миграция на Git

Если вы решили начать использовать Git, а у вас уже есть база исходного кода в другой СУВ, вам придётся как-то мигрировать свой проект. Этот раздел описывает некоторые из инструментов для импортирования проектов, включённых в состав Git для самых распространённых систем, в конце описывается создание вашего собственного инструмента для импортирования.

Импортирование

Вы научитесь импортировать данные из двух самых популярных систем контроля версий — Subversion и Perforce — поскольку они охватывают большинство пользователей, которые переходят на Git, а также потому, что для обеих систем созданы высококлассные инструменты, которые поставляются в составе Git.

Subversion

Если прочли предыдущий раздел об использовании git svn, вы можете с лёгкостью использовать все инструкции, имеющиеся там для клонирования репозитория через git svn clone. Затем можете отказаться от использования сервера Subversion и отправлять изменения на новый Git-сервер, и использовать уже его. Вытащить историю изменений, можно так же быстро, как получить данные с сервера Subversion (что, однако, может занять какое-то время).

Однако, импортирование не будет безупречным. И так как оно занимает много времени, стоит сделать его правильно. Первая проблема это информация об авторах. В Subversion каждый коммитер имеет свою учётную запись в системе, и его имя пользователя отображается в информации о коммите. В примерах из предыдущего раздела выводилось schacon в некоторых местах, например, в выводе команд blame и git svn log. Если вы хотите преобразовать эту информацию для лучшего соответствия данным об авторах в Git, вам потребуется отобразить пользователей Subversion в авторов в Git. Создайте файл users.txt, в котором будут содержаться данные об этом отображении в таком формате:

schacon = Scott Chacon selse = Someo Nelse

Для того, чтобы получить список авторов, который использует SVN, вы можете выполнить следующее:

$ svn log --xml | grep author | sort -u | perl -pe 's/.>(.?)<./$1 = /'

Это даст вам на выходе журнал в формате XML — в нём вы можете просмотреть информацию об авторах, создать из неё список с уникальными записями и избавиться от XML разметки. (Конечно, эта команда сработает только на машине с установленными grep, sort, и perl). Затем перенаправьте вывод этого скрипта в свой файл users.txt, чтобы потом можно было добавить к каждой записи данные о соответствующих пользователях из Git.

Вы можете передать этот файл как параметр команде git svn, для более точного преобразования данных об авторах. Кроме того, можно дать указание git svn не включать метаданные, обычно импортируемые Subversion, передав параметр --no-metadata команде clone или init. Таким образом, команда для импортирования будет выглядеть так:

$ git-svn clone http://my-project.googlecode.com/svn/ \ --authors-file=users.txt --no-metadata -s my_project

Теперь в вашем каталоге my_project будут находиться более приятно выглядящие данные после импортирования. Вместо коммитов, которые выглядят так:

commit 37efa680e8473b615de980fa935944215428a35a Author: schacon Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de- be05-5f7a86268029

они будут выглядеть так:

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2 Author: Scott Chacon Date: Sun May 3 00:12:22 2009 +0000 fixed install - go to trunk

Теперь не только поле Author выглядит намного лучше, но и строк с git-svn-id больше нет.

Вам потребуется сделать небольшую «уборку» после импорта. Сначала вам нужно убрать странные ссылки, оставленные git svn. Сначала мы переставим все метки так, чтобы они были реальными метками, а не странными удалёнными ветками. А затем мы переместим остальные ветки так, чтобы они стали локальными.

Для приведения меток к корректному виду меток Git выполните:

$ cp -Rf .git/refs/remotes/tags/* .git/refs/tags/ $ rm -Rf .git/refs/remotes/tags

Эти действия переместят ссылки, которые были удалёнными ветками, начинающимися с tag/ и сделают их настоящими (легковесными) метками.

Затем, переместите остальные ссылки в refs/remotes так, чтобы они стали локальными ветками:

$ cp -Rf .git/refs/remotes/* .git/refs/heads/ $ rm -Rf .git/refs/remotes

Теперь все старые ветки стали реальными Git-ветками, а все старые метки — реальными метками Git. Последнее, что осталось сделать, это добавить свой Git-сервер в качестве удалённого ресурса и отправить на него данные. Вот пример добавления сервера как удалённого источника:

$ git remote add origin git@my-git-server:myrepository.git

Так как вы хотите, чтобы все ваши ветви и метки были переданы на этот сервер, выполните:

$ git push origin --all

Теперь все ваши ветки и метки должны быть импортированы на новый Git-сервер в чистом и опрятном виде.

Perforce

Следующей системой, для которой мы рассмотрим процедуру импортирования, будет Perforce. Утилита импортирования для Perforce также входит в состав Git, но только в секции contrib исходного кода — она не доступна по умолчанию, как git svn. Для того, чтобы запустить её, вам потребуется получить исходный код Git, располагающийся на git.kernel.org:

$ git clone git://git.kernel.org/pub/scm/git/git.git $ cd git/contrib/fast-import

В каталоге fast-import вы найдёте исполняемый скрипт Python, с названием git-p4. Вы должны иметь на вашем компьютере установленный Python и утилиту p4 для того, чтобы эта утилита смогла работать. Допустим, например, что вы импортируете проект Jam из Perforce Public Depot. Для настройки вашей клиентской машины, вы должны установить переменную окружения P4PORT, указывающую на депо Perforce:

$ export P4PORT=public.perforce.com:1666

Запустите команду git-p4 clone для импортирования проекта Jam с сервера Perforce, передав в качестве параметров депо и путь к проекту, а также путь к месту, куда вы хотите импортировать проект:

$ git-p4 clone //public/jam/src@all /opt/p4import Importing from //public/jam/src@all into /opt/p4import Reinitialized existing Git repository in /opt/p4import/.git/ Import destination: refs/remotes/p4/master Importing revision 4409 (100%)

Если вы теперь перейдёте в каталог /opt/p4import и выполните команду git log, вы увидите импортированную информацию:

$ git log -2 commit 1fd4ec126171790efd2db83548b85b1bbbc07dc2 Author: Perforce staff Date: Thu Aug 19 10:18:45 2004 -0800 Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls. Only 16 months later. [git-p4: depot-paths = "//public/jam/src/": change = 4409] commit ca8870db541a23ed867f38847eda65bf4363371d Author: Richard Geiger Date: Tue Apr 22 20:51:34 2003 -0800 Update derived jamgram.c [git-p4: depot-paths = "//public/jam/src/": change = 3108]

Как видите, в каждом коммите есть идентификатор git-p4. Оставить этот идентификатор будет хорошим решением, если позже вам понадобится узнать номер изменения в Perforce. Однако, если вы всё же хотите удалить этот идентификатор — теперь самое время это сделать, до того, как вы начнёте работать в новом репозитории. Можно воспользоваться командой git filter-branch для одновременного удаления всех строк с идентификатором:

$ git filter-branch --msg-filter ' sed -e "/^\[git-p4:/d" ' Rewrite 1fd4ec126171790efd2db83548b85b1bbbc07dc2 (123/123) Ref 'refs/heads/master' was rewritten

Если вы теперь выполните git log, то увидите, что все контрольные суммы SHA-1 изменились, и что строки содержащие git-p4 больше не появляются в сообщениях коммитов:

$ git log -2 commit 10a16d60cffca14d454a15c6164378f4082bc5b0 Author: Perforce staff Date: Thu Aug 19 10:18:45 2004 -0800 Drop 'rc3' moniker of jam-2.5. Folded rc2 and rc3 RELNOTES into the main part of the document. Built new tar/zip balls. Only 16 months later. commit 2b6c6db311dd76c34c66ec1c40a49405e6b527b2 Author: Richard Geiger Date: Tue Apr 22 20:51:34 2003 -0800 Update derived jamgram.c

Ваш импортируемый репозиторий готов к отправке на новый Git-сервер.

Собственная утилита для импорта

Если вы используете систему отличную от Subversion или Perforce, вы можете поискать утилиту для импорта под свою систему в интернете — для CVS, Clear Case, Visual Source Safe и даже для простого каталога с архивами уже существуют качественные инструменты для импортирования. Если ни один из этих инструментов не подходит для ваших целей, либо если вам нужен больший контроль над процессом импортирования, вам стоит использовать утилиту git fast-import. Эта команда читает простые инструкции со стандартного входа, управляющие процессом записи специфичных данных в Git. Намного проще создать необходимые объекты в Git используя такой подход, чем запуская базовые команды Git, либо пытаясь записать сырые объекты (см. главу 9). При использовании git fast-import, вы можете создать сценарий для импортирования, который считывает всю необходимую информацию из импортируемой системы и выводит прямые инструкции на стандартный вывод. Затем вы просто запускаете этот скрипт и используя конвейер (pipe) передаёте результаты его работы на вход git fast-import.

Чтобы быстро продемонстрировать суть этого подхода, напишем простую утилиту для импорта. Положим, что вы работаете в каталоге current, и время от времени делаете резервную копию этого каталога добавляя к имени дату — back_YYYY_MM_DD, и вы хотите импортировать это всё в Git. Допустим, ваше дерево каталогов выглядит таким образом:

$ ls /opt/import_from back_2009_01_02 back_2009_01_04 back_2009_01_14 back_2009_02_03 current

Для того, чтобы импортировать всё это в Git, надо вспомнить, как Git хранит данные. Как вы помните, Git в своей основе представляет собой связный список объектов коммитов, указывающих на снимки состояния их содержимого. Всё, что вам требуется, это сообщить команде fast-import что является данными снимков состояния, какие данные коммитов указывают на них и порядок их следования. Стратегией наших действий будет обход всех снимков состояния по очереди и создание соответствующих коммитов с содержимым каждого каталога, с привязкой каждого коммита к предыдущему.

Так же как и в главе 7 в разделе «Пример создания политики в Git», мы напишем сценарий на Ruby, поскольку это то, с чем я обычно работаю, и кроме того он легко читается. Но вы можете создать его на любом другом языке, которым владеете — он просто должен выводить необходимую информацию на стандартный вывод. Если вы работаете под Windows, то должны особым образом позаботиться о том, чтобы в конце строк не содержались символы возврата каретки — git fast-import принимает только символ перевода строки (LF), а не символ перевода строки и возврата каретки (CRLF), который используется в Windows.

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

last_mark = nil # loop through the directories Dir.chdir(ARGV[0]) do Dir.glob("*").each do |dir| next if File.file?(dir) # move into the target directory Dir.chdir(dir) do last_mark = print_export(dir, last_mark) end end end

Вы запускаете функцию print_export внутри каждого каталога, она берёт запись и отметку предыдущего снимка состояния и возвращает запись и отметку текущего; таким образом они соединяются нужным образом между собой. «Отметка» — это термин утилиты fast-import, обозначающий идентификатор, который вы даёте коммиту; когда вы создаёте коммиты, вы назначаете каждому из них отметку, которую можно использовать для связывания с другими коммитами. Таким образом, первая операция, которую надо включить в метод print_export, это генерация отметки из имени каталога:

mark = convert_dir_to_mark(dir)

Мы сделаем это путём создания массива каталогов и используя значение порядкового номера каталога в массиве, как его отметку, поскольку отметка должна быть целым числом:

$marks = [] def convert_dir_to_mark(dir) if !$marks.include?(dir) $marks << dir end ($marks.index(dir) + 1).to_s end

Теперь, когда мы имеем целочисленное представление нашего коммита, нам нужны даты, чтобы указывать их в метаданных коммитов. Поскольку дата записана в имени каталога, мы выделяем её оттуда. Следующей строкой в сценарии print_export будет:

date = convert_dir_to_date(dir)

где метод convert_dir_to_date определён как:

def convert_dir_to_date(dir) if dir == 'current' return Time.now().to_i else dir = dir.gsub('back_', '') (year, month, day) = dir.split('_') return Time.local(year, month, day).to_i end end

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

$author = 'Scott Chacon '

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

# print the import information puts 'commit refs/heads/master' puts 'mark :' + mark puts "committer #{$author} #{date} -0700" export_data('imported from ' + dir) puts 'from :' + last_mark if last_mark

Мы жёстко задаём часовой пояс (-0700), поскольку так проще. Если вы импортируете данные из другой системы, вы должны указать часовой пояс в виде смещения. Сообщение коммита должно быть представлено в особом формате:

data (size)\n(contents)

Формат состоит из слова data, размера данных, которые требуется прочесть, символа переноса строки и, наконец, самих данных. Поскольку нам потребуется использовать такой же формат позже, для описания содержимого файла, создадим вспомогательный метод export_data:

def export_data(string) print "data #{string.size}\n#{string}" end

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

puts 'deleteall' Dir.glob("**/*").each do |file| next if !File.file?(file) inline_data(file) end

Примечание: поскольку многие системы рассматривают свои ревизии как изменения от одного коммита до другого, fast-import также может принимать команды задающие для каждого коммита, какие файлы были добавлены, удалены или модифицированы, а также что является новым содержимым файлов. В нашем примере вы могли бы вычислить разность между снимками состояния и предоставить только эти данные, но это сложнее. С таким же успехом можно предоставить Git все данные для того, чтобы он сам вычислил разницу. Если с вашими данными проще предоставлять разницу между снимками состояния, обратитесь к странице руководства fast-import для получения подробностей о том как предоставлять данные таким способом.

Формат для задания содержимого нового файла, либо указания нового содержимого изменённого файла следующий:

M 644 inline path/to/file data (size) (file contents)

Здесь, 644 — это права доступа (если в проекте есть исполняемые файлы, вам надо выявить их и назначить им права доступа 755), а параметр inline говорит о том, что содержимое будет выводиться непосредственно после этой строки. Метод inline_data выглядит следующим образом:

def inline_data(file, code = 'M', mode = '644') content = File.read(file) puts "#{code} #{mode} inline #{file}" export_data(content) end

Мы повторно используем метод export_data, определённый ранее, поскольку он работает тут так же, как и при задании сообщений коммитов.

Последнее, что вам осталось сделать, это вернуть текущую отметку, чтобы её можно было передать для использования в следующую итерацию:

return mark

ПРИМЕЧАНИЕ: Если вы работаете под Windows, то должны убедиться, что добавили ещё один дополнительный шаг. Мы уже упоминали, что Windows использует CRLF для перехода на новую строку, тогда как git fast-import ожидает только LF. Для того, чтобы избежать этой проблемы и сделать процесс импорта безошибочным, вам нужно сказать Ruby использовать LF вместо CRLF:

$stdout.binmode

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

$ ruby import.rb /opt/import_from commit refs/heads/master mark :1 committer Scott Chacon 1230883200 -0700 data 29 imported from back_2009_01_02deleteall M 644 inline file.rb data 12 version two commit refs/heads/master mark :2 committer Scott Chacon 1231056000 -0700 data 29 imported from back_2009_01_04from :1 deleteall M 644 inline file.rb data 14 version three M 644 inline new.rb data 16 new version one (...)

Для того, чтобы запустить утилиту импорта, перенаправьте этот вывод на вход git fast-import, находясь в каталоге Git, в который хотите совершить импортирование. Вы можете создать новый каталог, а затем выполнить в нём git init и потом запустить свой сценарий:

$ git init Initialized empty Git repository in /opt/import_to/.git/ $ ruby import.rb /opt/import_from | git fast-import git-fast-import statistics: --------------------------------------------------------------------- Alloc'd objects: 5000 Total objects: 18 ( 1 duplicates ) blobs : 7 ( 1 duplicates 0 deltas) trees : 6 ( 0 duplicates 1 deltas) commits: 5 ( 0 duplicates 0 deltas) tags : 0 ( 0 duplicates 0 deltas) Total branches: 1 ( 1 loads ) marks: 1024 ( 5 unique ) atoms: 3 Memory total: 2255 KiB pools: 2098 KiB objects: 156 KiB --------------------------------------------------------------------- pack_report: getpagesize() = 4096 pack_report: core.packedGitWindowSize = 33554432 pack_report: core.packedGitLimit = 268435456 pack_report: pack_used_ctr = 9 pack_report: pack_mmap_calls = 5 pack_report: pack_open_windows = 1 / 1 pack_report: pack_mapped = 1356 / 1356 ---------------------------------------------------------------------

Как видите, после успешного завершения, Git выдаёт большое количество информации о проделанной работе. В нашем случае мы на итог импортировали 18 объектов для 5 коммитов в одну ветку. Теперь выполните git log, чтобы увидеть свою новую историю изменений:

$ git log -2 commit 10bfe7d22ce15ee25b60a824c8982157ca593d41 Author: Scott Chacon Date: Sun May 3 12:57:39 2009 -0700 imported from current commit 7e519590de754d079dd73b44d695a42c9d2df452 Author: Scott Chacon Date: Tue Feb 3 01:00:00 2009 -0700 imported from back_2009_02_03

Ну вот, вы получили чистый и красивый Git-репозиторий. Важно отметить, что пока у вас нет никаких файлов в рабочем каталоге — вы должны сбросить свою ветку на ветку master:

$ ls $ git reset --hard master HEAD is now at 10bfe7d imported from current $ ls file.rb lib

С помощью утилиты fast-import можно делать намного больше — манипулировать разными правами доступа, двоичными данными, несколькими ветками, совершать слияния, назначать метки, отображать индикаторы прогресса и многое другое. Некоторое количество примеров более сложных сценариев содержится в каталоге contrib/fast-import исходного кода Git; один из самых лучших из них — сценарий git-p4, о котором я уже рассказывал.

Итоги

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

Git изнутри

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

Итак, приступим. Во-первых, напомню, что Git это по сути контентно-адресуемая файловая система с пользовательским СУВ-интерфейсом поверх неё. Довольно скоро станет понятнее, что это значит.

На заре развития Git (примерно до версии 1.5), интерфейс был значительно сложнее, поскольку был более похож на интерфейс доступа к файловой системе, чем на законченную СУВ. За последние годы, интерфейс значительно улучшился и по удобству не уступает аналогам; у некоторых, тем не менее, с тех пор сохранился стереотип о том, что интерфейс у Git чересчур сложный и труден для изучения.

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

Сантехника и фарфор

В этой книге было описано как пользоваться Git используя примерно три десятка команд, например, checkout, branch, remote и т.п. Но так как сначала Git был скорее инструментарием для создания СУВ, чем СУВ удобной для пользователей, в нём полно команд, выполняющих низкоуровневые операции, которые спроектированы так, чтобы их можно было использовать в цепочку в стиле UNIX, а также использовать в сценариях. Эти команды как правило называют служебными ("plumbing" — трубопровод), а более ориентированные на пользователя называют пользовательскими ("porcelain" — фарфор).

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

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

$ ls HEAD branches/ config description hooks/ index info/ objects/ refs/

Там могут быть и другие файлы, но непосредственно после git init вы увидите именно это. Каталог branches не используется новыми версиями Git, а файл description требуется только программе GitWeb, на них не стоит обращать особого внимания. Файл config содержит настройки проекта, а каталог info — файл с глобальным фильтром, игнорирующим те файлы, которые вы не хотите поместить в .gitignore. В каталоге hooks располагаются клиентские и серверные хуки, подробно рассмотренные в главе 7.

Итак, осталось четыре важных элемента: файлы HEAD, index и каталоги objects, refs. Это ключевые элементы хранилища Git. В каталоге objects находится, собственно, база данных, в refs — ссылки на объекты коммитов в этой базе (ветки). Файл HEAD указывает на текущую ветку, и в файле index хранится информация индекса. В последующих разделах данные элементы будут рассмотрены более подробно.

Объекты в Git

Git — контентно-адресуемая файловая система. Здорово. Но что это означает? А означает это, что в своей основе Git — простое хранилище ключ-значение. Можно добавить туда любое содержимое, в ответ будет выдан ключ, по которому это содержимое можно извлечь. Для примера, можно воспользоваться служебной командой hash-object, которая добавляет данные в каталог .git и возвращает ключ. Для начала создадим новый Git-репозиторий и убедимся, что каталог objects пуст:

$ mkdir test $ cd test $ git init Initialized empty Git repository in /tmp/test/.git/ $ find .git/objects .git/objects .git/objects/info .git/objects/pack $ find .git/objects -type f $

Git проинициализировал каталог objects и создал в нём подкаталоги pack и info, пока без файлов. Теперь добавим кое-какое текстовое содержимое в базу Git'а:

$ echo 'test content' | git hash-object -w --stdin d670460b4b4aece5915caf5c68d12f560a9fe3e4

Ключ -w команды hash-object указывает, что объект необходимо сохранить, иначе команда просто выведет ключ и всё. Флаг --stdin указывает, что данные необходимо считать со стандартного ввода, в противном случае hash-object ожидает имя файла. Вывод команды — 40-символьная контрольная сумма. Это хеш SHA-1 — контрольная сумма содержимого и заголовка, который будет рассмотрен позднее. Теперь можно увидеть, в каком виде будут сохранены ваши данные:

$ find .git/objects -type f .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

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

Получить обратно содержимое объекта можно командой cat-file. Это своеобразный швейцарский армейский нож для проверки объектов в Git. Ключ -p означает автоматическое определение типа содержимого и вывод содержимого на печать в удобном виде:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 test content

Теперь вы умеете добавлять данные в Git и извлекать их обратно. То же самое можно делать и с файлами. Рассмотрим пример. Наиболее простой контроль версий файла можно осуществить, создав его и сохранив в базе:

$ echo 'version 1' > test.txt $ git hash-object -w test.txt 83baae61804e65cc73a7201a7252750c76066a30

Теперь изменим файл и сохраним его в базе ещё раз:

$ echo 'version 2' > test.txt $ git hash-object -w test.txt 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

Теперь в базе содержатся две версии файла test.txt, а также самый первый сохранённый объект:

$ find .git/objects -type f .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Теперь можно откатить файл к его первой версии:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt $ cat test.txt version 1

или второй:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt $ cat test.txt version 2

Однако запоминать хеш для каждой версии неудобно, к тому же теряется само имя файла, сохраняется лишь содержимое. Объекты такого типа называют блобами (англ. binary large object). Имея SHA-1 объекта, можно попросить Git показать нам его тип с помощью команды cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob

Объекты-деревья

Рассмотрим другой тип объектов Git — деревья. Они решают проблему хранения имён файлов, а также позволяют хранить группы файлов вместе. Система хранения данных Git подобна файловым системам UNIX в упрощённом виде. Содержимое хранится в объектах-деревьях и блобах, дерево соответствует записи каталога в ФС, а блоб более или менее соответствует inode или содержимому файла. Объект-дерево может содержать одну и более записей, каждая из которых представляет собой набор из SHA-1 хеша, соответствующего блобу или поддереву, режима доступа к файлу, типа и имени файла. Например, в проекте simplegit дерево на момент написания выглядит так:

$ git cat-file -p master^{tree} 100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README 100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile 040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib

Запись master^{tree} означает объект-дерево, на который указывает последний коммит ветки master. Заметьте, что подкаталог lib — не блоб, а указатель на другое дерево:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb

Схематически, данные, которые хранятся в Git, выглядят примерно так, как это изображено на рисунке 9-1.

Вручную можно создавать не только блобы, но и деревья. Git обычно создаёт дерево исходя из состояния индекса и затем сохраняет соответствующий объект-дерево. Поэтому для создания объекта-дерева необходимо проиндексировать какие-нибудь файлы. Для создания индекса из одной записи — первой версии файла text.txt, воспользуемся командой update-index. Данная команда может искусственно добавить более раннюю версию test.txt в новый индекс. Необходимо передать опции --add, т.к. файл ещё не существует в индексе (да и самого индекса ещё нет), и --cacheinfo, т.к. добавляемого файла нет в рабочем каталоге, но он есть в базе данных. Также необходимо передать режим доступа, хеш и имя файла:

$ git update-index --add --cacheinfo 100644 \ 83baae61804e65cc73a7201a7252750c76066a30 test.txt

В данном случае режим доступа — 100644, что означает обычный файл. Другие возможные варианты: 100755 — исполняемый файл, 120000 — символическая ссылка. Режимы доступа в Git сделаны по аналогии с режимами доступа в UNIX, но они гораздо менее гибки: данные три режима — единственные доступные для файлов (блобов) в Git (хотя существуют и другие режимы используемые для каталогов и подмодулей).

Теперь можно воспользоваться командой write-tree для сохранения индекса в объект-дерево. Здесь опция -w не требуется — вызов write-tree автоматически создаст объект-дерево по состоянию индекса, если такого дерева ещё не существует:

$ git write-tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 $ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579 100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt

Также можно проверить, что мы действительно создали объект-дерево:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree

Создадим новое дерево со второй версией файла test.txt и ещё одним файлом:

$ echo 'new file' > new.txt $ git update-index test.txt $ git update-index --add new.txt

Теперь в индексе содержится новая версия файла test.txt и новый файл new.txt. Запишем это дерево (сохранив состояние индекса в объект-дерево) и посмотрим, что из этого получилось:

$ git write-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 $ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

Заметьте, что в данном дереве находятся записи для обоих файлов, а также, что хеш файла test.txt это хеш "второй версии" этого файла (1f7a7a). Для интереса, добавим первое дерево как подкаталог для текущего. Зачитать дерево в индекс можно командой read-tree. В нашем случае, чтобы прочитать уже существующее дерево в индекс и сделать его поддеревом, необходимо использовать опцию --prefix:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579 $ git write-tree 3c4e9cd789d88d8d89c1073707c3585e41b0e614 $ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614 040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

Если бы вы создали рабочий каталог, соответствующий только что созданному дереву, вы бы получили два файла в корне и подкаталог bak со старой версией файла test.txt. Данные, которые хранит Git для такой структуры, представлены на рисунке 9-2.

Объекты-коммиты

У нас есть три дерева, соответствующих разным состояниям проекта, но предыдущая проблема с необходимостью запоминать все три значения SHA-1, чтобы иметь возможность восстановить какое-либо из этих состояний, ещё не решена. К тому же у нас нет никакой информации о том, кто, когда и почему сохранил их. Такие данные — основная информация, которая хранится в объекте-коммите.

Для создания объекта-коммита необходимо вызвать commit-tree и задать SHA-1 нужного дерева и, если необходимо, родительские объекты-коммиты. Для начала создадим коммит для самого первого дерева:

$ echo 'first commit' | git commit-tree d8329f fdf4fc3344e67ab068f836878b6c4951e3b15f3d

Просмотреть вновь созданный объект-коммит можно командой cat-file:

$ git cat-file -p fdf4fc3 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author Scott Chacon 1243040974 -0700 committer Scott Chacon 1243040974 -0700 first commit

Формат объекта-коммита прост: в нём указано дерево верхнего уровня, соответствующее состоянию проекта на некоторый момент; имя автора и коммитера берутся из полей конфигурации user.name и user.email; также добавляется текущая временная метка, пустая строка и затем сообщение коммита.

Далее, создадим ещё два объекта-коммита, каждый из которых будет ссылаться на предыдущий коммит:

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3 cac0cab538b970a37ea1e769cbbde608743bc96d $ echo 'third commit' | git commit-tree 3c4e9c -p cac0cab 1a410efbd13591db07496601ebc7a059dd55cfe9

Каждый из трёх объектов-коммитов указывает на одно из состояний проекта. Может показаться странным, но теперь у нас есть полноценная Git-история, которую можно посмотреть командой git log, указав хеш последнего коммита:

$ git log --stat 1a410e commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Author: Scott Chacon Date: Fri May 22 18:15:24 2009 -0700 third commit bak/test.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) commit cac0cab538b970a37ea1e769cbbde608743bc96d Author: Scott Chacon Date: Fri May 22 18:14:29 2009 -0700 second commit new.txt | 1 + test.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletions(-) commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d Author: Scott Chacon Date: Fri May 22 18:09:34 2009 -0700 first commit test.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)

Поразительно. Мы только что выполнили низкоуровневые операции для построения истории без использования высокоуровневых интерфейсов. По существу, именно это делает Git, когда выполняются команды git add и git commit — сохраняет блобы для изменённых файлов, обновляет индекс, записывает объекты-деревья и коммит-объекты, ссылающиеся на объекты-деревья верхнего уровня и предшествующие коммиты. Эти три основных вида объектов в Git: блоб, дерево и коммит — сначала сохраняются как отдельные файлы в каталоге .git/objects. Вот все объекты, которые сейчас лежат в каталоге с примером (в комментариях написано чему объекты соответствует):

$ find .git/objects -type f .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2 .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2 .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1 .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content' .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1 .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Если пройти по всем внутренним ссылкам, получится граф объектов такой, как на рисунке 9-3.

Хранение объектов

Ранее я упоминал, что заголовок сохраняется вместе с содержимым. Давайте посмотрим, как сохраняются объекты Git на диске. Мы рассмотрим сохранение блоб-объекта, в данном случае это будет строка "есть проблемы, шеф?". Пример будет выполнен на языке Ruby. Для запуска интерактивного интерпретатора воспользуйтесь командой irb:

$ irb >> content = "есть проблемы, шеф?" => "есть проблемы, шеф?"

Git создаёт заголовок, начинающийся с типа объекта, в данном случае это блоб. Далее добавляется пробел, размер содержимого и в конце нулевой байт:

>> header = "blob #{content.length}\0" => "blob 34\000"

Git дописывает содержимое после заголовка и вычисляет SHA-1 сумму для полученного результата. В Ruby значение SHA-1 для строки можно получить, подключив соответствующую библиотеку командой require и затем воспользовавшись вызовом Digest::SHA1.hexdigest():

>> store = header + content => "blob 34\000\320\225\321\201\321\202\321\214 \320\277\321\200\320\276\320\261\320\273\320\265\320\274\321\213, \321\210\320\265\321\204?" >> require 'digest/sha1' => true >> sha1 = Digest::SHA1.hexdigest(store) => "d8a734f44240bdf766c8df342664fde23d421d64"

Git сжимает новые данные при помощи zlib, что решается в Ruby соответствующей библиотекой. Сперва, необходимо подключить её, а после вызвать Zlib::Deflate.deflate() с данными в качестве параметра:

>> require 'zlib' => true >> zlib_content = Zlib::Deflate.deflate(store) => "x\234\001*\000\325\377blob 34\000\320\225\321\201\321\202\321\214 \320\277\321\200\320\276\320\261\320\273\320\265\320\274\321\213, \321\210\320\265\321\204?\3453\030S"

После этого, запишем сжатую zlib'ом строку в объект на диск. Определим путь к файлу, который будет записан (первые два символа хеша используются в качестве названия подкаталога, оставшиеся 38 — в качестве имени файла в этом каталоге). В Ruby для этой задачи можно использовать функцию FileUtils.mkdir_p() для создания подкаталога, если он не существует. Далее, откроем файл вызовом File.open() и запишем наши сжатые данные вызовом write() для полученного файлового дескриптора:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38] => ".git/objects/d8/a734f44240bdf766c8df342664fde23d421d64" >> require 'fileutils' => true >> FileUtils.mkdir_p(File.dirname(path)) => ".git/objects/bd" >> File.open(path, 'w') { |f| f.write zlib_content } => 32

Вот и всё, мы создали корректный объект-блоб для Git. Все другие объекты создаются аналогично, меняется только запись о типе в заголовке (blob, commit, tree). Стоит добавить, что хотя в блобе может храниться почти любое содержимое, содержимое объектов-деревьев и объектов-коммитов записывается в очень строгом формате.

Ссылки в Git

Для просмотра всей истории можно выполнить команду вроде git log 1a410e, но, опять же, требуется помнить, что именно коммит 1a410e является последним, чтобы иметь возможность найти все наши объекты. Нам нужен файл-указатель с простым именем, который бы содержал это значение хеша SHA-1, чтобы можно было пользоваться этим файлом вместо хеша.

В Git такие файлы, содержащие SHA-1, называются ссылками ("refs") и располагаются в каталоге .git/refs. В нашем проекте этот каталог пока пуст, но в нём уже существует некоторая структура каталогов:

$ find .git/refs .git/refs .git/refs/heads .git/refs/tags $ find .git/refs -type f $

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

$ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" > .git/refs/heads/master

Теперь можно использовать только что созданную ссылку из каталога heads вместо хеша в командах Git:

$ git log --pretty=oneline master 1a410efbd13591db07496601ebc7a059dd55cfe9 third commit cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Тем не менее, редактировать данные файлы напрямую не рекомендуется. Git предоставляет безопасную команду update-ref для изменения ссылок:

$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

Вот что такое по сути ветка в Git — простой указатель или ссылка на последнюю версию в работе. Для создания ветки, соответствующей состоянию второго коммита, можно выполнить следующее:

$ git update-ref refs/heads/test cac0ca

Данная ветка будет содержать только коммиты, предшествующие выбранному:

$ git log --pretty=oneline test cac0cab538b970a37ea1e769cbbde608743bc96d second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Теперь наша база данных Git схематично выглядит так, как показано на рисунке 9.4.

Когда выполняется команда git branch (имя ветки), Git, по сути, выполняет update-ref для добавления хеша последнего коммита текущей ветки под указанным именем в виде новой ссылки.

HEAD

Вопрос в том, как же Git получает хеш последнего коммита при выполнении git branch (имя ветки)? Ответ содержится в файле HEAD. Данный файл является символической ссылкой на текущую ветку. Символическая ссылка отличается от обычной тем, что она содержит не сам хеш SHA-1, а указатель на другую ссылку. Если вы загляните в этот файл, то увидите что-то такое:

$ cat .git/HEAD ref: refs/heads/master

Если выполнить git checkout test, то содержимое файла изменится:

$ cat .git/HEAD ref: refs/heads/test

При выполнении git commit, Git создаёт объект-коммит, указывая его родителем тот объект, SHA-1 которого содержится в файле, на который ссылается HEAD.

Данный файл, конечно, можно редактировать вручную, но безопаснее использовать команду symbolic-ref. Получить значение HEAD данной командой можно так:

$ git symbolic-ref HEAD refs/heads/master

Изменить значение HEAD можно так:

$ git symbolic-ref HEAD refs/heads/test $ cat .git/HEAD ref: refs/heads/test

Символическую ссылку на файл вне refs поставить нельзя:

$ git symbolic-ref HEAD test fatal: Refusing to point HEAD outside of refs/

Метки

Мы рассмотрели три основных типа объектов в Git, но есть и четвёртый. Объект-метка очень похож на объект-коммит: он содержит имя поставившего метку, дату, сообщение и указатель. Разница же в том, что метка указывает на коммит, а не на дерево. Она похожа на ветку, которая никогда не перемещается — она всегда указывает на один и тот же коммит, она просто даёт ему понятное имя.

Как было сказано в главе 2, метки бывают двух типов: аннотированные и легковесные. Легковесную метку можно сделать следующей командой:

$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d

Вот и всё! Легковесная метка — это ветка, которая никогда не перемещается. Аннотированная метка имеет более сложную структуру. При создании аннотированной метки, Git создаёт специальный объект, на который будет указывать ссылка, а не просто указатель на коммит. Мы можем увидеть это создав аннотированную метку (-a задаёт аннотированные метки):

$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'

Вот значение SHA-1 созданного объекта:

$ cat .git/refs/tags/v1.1 9585191f37f7b0fb9444f35a9bf50de191beadc2

Теперь выполним cat-file для этого хеша:

$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2 object 1a410efbd13591db07496601ebc7a059dd55cfe9 type commit tag v1.1 tagger Scott Chacon Sat May 23 16:48:58 2009 -0700 test tag

Заметьте, в поле object записан SHA-1 коммита, для которого мы делали метку. Также стоит отметить, что это поле не обязательно указывает на коммит, но на любой объект в Git. Например, в исходный код Git мейнтейнер добавил свой открытый GPG-ключ в качестве блоба и поставил для него метку. Увидеть этот ключ можно, выполнив команду

$ git cat-file blob junio-gpg-pub

в репозитории с исходным кодом Git. В репозитории ядра Linux также есть метка, указывающая не на коммит — первая метка указывает на дерево первичного импорта.

Ссылки на удалённые ветки

Третий тип ссылок, который мы рассмотрим — ссылка на удалённую ветку. Если вы добавили удалённый репозиторий и отправили (push) на него изменения, Git сохранит последнее отправленное значение SHA-1 в каталоге refs/remotes для всех отправленных веток. Например, можно добавить удалённый репозиторий origin и отправить туда ветку master:

$ git remote add origin git@github.com:schacon/simplegit-progit.git $ git push origin master Counting objects: 11, done. Compressing objects: 100% (5/5), done. Writing objects: 100% (7/7), 716 bytes, done. Total 7 (delta 2), reused 4 (delta 1) To git@github.com:schacon/simplegit-progit.git a11bef0..ca82a6d master -> master

Позже, вы сможете посмотреть где находилась ветка master с сервера origin во время последнего соединения с сервером заглянув в файл refs/remotes/origin/master:

$ cat .git/refs/remotes/origin/master ca82a6dff817ec66f44342007202690a93763949

Ссылки на удалённые ветки отличаются от обычных веток (ссылки в refs/heads) тем, что на них нельзя переключиться с помощью git checkout. Git работает с ними как с закладками, указывающими на последнее состояние соответствующих веток на ваших серверах.

Pack-файлы

Вернёмся к базе объектов в нашем тестовом репозитории. К этому моменту их должно быть 11 штук: 4 блоба, 3 дерева, 3 коммита и одна метка:

$ find .git/objects -type f .git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2 .git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3 .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2 .git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1 .git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag .git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content' .git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1 .git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt .git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Git сжал содержимое этих файлов при помощи zlib, к тому же мы не записывали много данных, поэтому все эти файлы вместе занимают всего 925 байт. Для того, чтобы продемонстрировать одну интересную возможность Git, добавим файл побольше. Добавим файл repo.rb из библиотеки Grit, с которой мы работали ранее, он занимает примерно 12 Кбайт:

$ curl http://github.com/mojombo/grit/raw/master/lib/grit/repo.rb > repo.rb $ git add repo.rb $ git commit -m 'added repo.rb' [master 484a592] added repo.rb 3 files changed, 459 insertions(+), 2 deletions(-) delete mode 100644 bak/test.txt create mode 100644 repo.rb rewrite test.txt (100%)

Если мы посмотрим на полученное дерево, мы увидим значение SHA-1, которое получил блоб для файла repo.rb:

$ git cat-file -p master^{tree} 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e repo.rb 100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt

Для определения размера этого объекта воспользуемся командой git cat-file:

$ git cat-file -s 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e 12898

Теперь изменим немного данный файл и посмотрим на результат:

$ echo '# testing' >> repo.rb $ git commit -am 'modified repo a bit' [master ab1afef] modified repo a bit 1 files changed, 1 insertions(+), 0 deletions(-)

Взглянув на дерево полученное в результате коммита, мы увидим любопытную вещь:

$ git cat-file -p master^{tree} 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt 100644 blob 05408d195263d853f09dca71d55116663690c27c repo.rb 100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt

Теперь файлу repo.rb соответствует другой объект-блоб. Это означает, что даже одна единственная строка добавленная в конец 400-строчного файла требует создания абсолютно нового объекта:

$ git cat-file -s 05408d195263d853f09dca71d55116663690c27c 12908

Итак, мы имеем два почти одинаковых объекта по 12 Кбайт занимающих место на диске. Было бы неплохо, если бы Git сохранял только один объект целиком, а другой как разницу между ними.

Оказывается, что Git так и делает. Первоначальный формат для сохранения объектов в Git называется рыхлым форматом (англ. loose format) объектов. Однако, время от времени Git упаковывает несколько таких объектов в один pack-файл (pack в пер. с англ. — упаковывать, уплотнять) для сохранения места на диске и повышения эффективности. Это происходит, когда "рыхлых" объектов становится слишком много, а также при вызове git gc вручную, и при отправке изменений на удалённый сервер. Чтобы посмотреть, как происходит упаковка, можно выполнить команду git gc:

$ git gc Counting objects: 17, done. Delta compression using 2 threads. Compressing objects: 100% (13/13), done. Writing objects: 100% (17/17), done. Total 17 (delta 1), reused 10 (delta 0)

Если вы загляните в каталог с объектами, вы обнаружите, что большая часть объектов исчезла, зато появились два новых файла:

$ find .git/objects -type f .git/objects/71/08f7ecb345ee9d0084193f147cdad4d2998293 .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 .git/objects/info/packs .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack

Оставшиеся объекты — блобы, на которые не указывает ни один коммит. В нашем случае это созданные ранее объекты: содержащий строку "есть проблемы, шеф?", и блоб содержащий "test content". В силу того, что ни в одном коммите данные файлы не присутствуют, они считаются "висячими" и не упаковываются.

Остальные файлы — это pack-файл и его индекс. Pack-файл — это файл, который теперь содержит все объекты, которые были удалены. А индекс — это файл, в котором записаны их смещения в pack-файле, что даёт возможность быстро найти нужный объект. Упаковка данных положительно повлияла на общий размер файлов, если до вызова gc они занимали примерно 12 Кбайт, то pack-файл занимает всего 6 Кбайт. Упаковкой объектов мы смогли сократить место занятое на диске в два раза.

Как Git это делает? При упаковке Git ищет файлы, которые похожи по имени и размеру и сохраняет только разницу между двумя версиями файла. Можно рассмотреть pack-файл подробнее и понять, какие действия были выполнены для сжатия. Для просмотра содержимого упакованного файла существует служебная команда git verify-pack:

$ git verify-pack -v \ .git/objects/pack/pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.idx 0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 5400 05408d195263d853f09dca71d55116663690c27c blob 12908 3478 874 09f01cea547666f58d6a8d809583841a7c6f0130 tree 106 107 5086 1a410efbd13591db07496601ebc7a059dd55cfe9 commit 225 151 322 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 5381 3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 101 105 5211 484a59275031909e19aadb7c92262719cfcdf19a commit 226 153 169 83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 5362 9585191f37f7b0fb9444f35a9bf50de191beadc2 tag 136 127 5476 9bc1dc421dcd51b4ac296e3e5b6e2a99cf44391e blob 7 18 5193 1 05408d195263d853f09dca71d55116663690c27c \ ab1afef80fac8e34258ff41fc1b867c702daa24b commit 232 157 12 cac0cab538b970a37ea1e769cbbde608743bc96d commit 226 154 473 d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 36 46 5316 e3f094f522629ae358806b17daf78246c27c007b blob 1486 734 4352 f8f51d7d8a1760462eca26eebafde32087499533 tree 106 107 749 fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 856 fdf4fc3344e67ab068f836878b6c4951e3b15f3d commit 177 122 627 chain length = 1: 1 object pack-7a16e4488ae40c7d2bc56ea2bd43e25212a66c45.pack: ok

Здесь блоб 9bc1d, который, как мы помним, был первой версией файла repo.rb, ссылается на блоб 05408, который был второй его версией. Третья колонка в выводе — это размер объекта. Как видите, 05408 занимает в файле 12 Кбайт, при этом 9bc1d занимает всего лишь 7 байт. Что интересно, вторая версия сохраняется "как есть", а исходная — в виде дельты. Это из-за того, что необходимость получения доступа к последней версии файла является более вероятной.

Также здорово, что переупаковку можно выполнять в любое время. Время от времени Git будет выполнять её автоматически, чтобы сэкономить место на диске, если вдруг этого недостаточно, всегда можно выполнить git gc вручную.

Спецификации ссылок

Во всей книге использовались простые связи между ветками в удалённых репозиториях и локальными ветками, но они могут быть и более сложными. Предположим, мы добавили следующий удалённый репозиторий:

$ git remote add origin git@github.com:schacon/simplegit-progit.git

Данный вызов добавляет секцию в файл .git/config, в которой заданы имя удалённого репозитория (origin), его URL и спецификация ссылок для извлечения данных:

[remote "origin"] url = git@github.com:schacon/simplegit-progit.git fetch = +refs/heads/*:refs/remotes/origin/*

Формат спецификации следующий: опциональный +, далее пара :, где — шаблон ссылок в удалённом репозитории, а — соответствующий шаблон локальных ссылок. Символ + сообщает Git, что обновление необходимо выполнять даже в том случае, если оно не является перемоткой.

В случае настроек по умолчанию, которые записываются во время выполнения git remote add, Git выбирает все ссылки из refs/heads/ на стороне сервера, и записывает их в локальный каталог refs/remotes/origin/. Таким образом, если на сервере есть ветка master, журнал данной ветки можно получить, вызвав:

$ git log origin/master $ git log remotes/origin/master $ git log refs/remotes/origin/master

Все эти команды эквивалентны, так как Git развернёт каждую запись до refs/remotes/origin/master.

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

fetch = +refs/heads/master:refs/remotes/origin/master

Данный refspec будет использоваться по умолчанию при вызове git fetch для данного удалённого репозитория. Если же вам нужно изменить спецификацию всего раз, можно задать refspec в командной строке. Например, чтобы получить данные из ветки master из удалённого репозитория в локальную origin/mymaster, можно выполнить

$ git fetch origin master:refs/remotes/origin/mymaster

Конечно, можно задать несколько спецификаций. Получить данные нескольких веток из командной строки можно так:

$ git fetch origin master:refs/remotes/origin/mymaster \ topic:refs/remotes/origin/topic From git@github.com:schacon/simplegit ! [rejected] master -> origin/mymaster (non fast forward) * [new branch] topic -> origin/topic

В данном случае, слияние ветки master выполнить не удалось, поскольку слияние не было просто перемоткой. Такое поведение можно изменить, добавив перед спецификацией знак +.

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

[remote "origin"] url = git@github.com:schacon/simplegit-progit.git fetch = +refs/heads/master:refs/remotes/origin/master fetch = +refs/heads/experiment:refs/remotes/origin/experiment

Задавать частичные регулярные выражения в спецификации нельзя, следующая запись неверна:

fetch = +refs/heads/qa*:refs/remotes/origin/qa*

Тем не менее, можно использовать пространства имён для получения похожего результата. Если имеется команда QA (сокр. от quality assurance — контроль качества), которая использует свои несколько веток, и вы хотите получать только ветку master и все ветки команды QA, а остальные — нет, то можно добавить в конфигурацию следующее:

[remote "origin"] url = git@github.com:schacon/simplegit-progit.git fetch = +refs/heads/master:refs/remotes/origin/master fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*

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

Спецификации ссылок для команды push

Это хорошо, что мы научились получать данные по ссылкам в отдельных пространствах имён, но нам же ещё надо сделать так, чтобы команда QA сначала смогла отправить свои ветки в пространство имён qa/. Мы решим эту задачу используя спецификации ссылок для команды push.

Если разработчик из команды QA хочет отправить изменения из локальной ветки master в qa/master на удалённом сервере, он может выполнить команду

Загрузка...