четверг, 11 сентября 2014 г.

Азы git

Пришлось на днях объяснять азы гита. 

Часть 1

В отличие от многих других систем контроля версий (cvs) git оперирует не файлами, а строками. Т.е. для него первичны строки, а файлы им воспринимаются как некие контейнеры для строк. Например, если удалить какой-либо файл, то гит запишет это в своем репозитории (базе данных) именно как удаление всех строк из файла, впрочем сам файл он тоже пометит как удаленный, чтоб немелькал зазря в списках файлов (например, выдаваемый по команде git ls-files).

С git можно не беспокоится чего-то потерять. Он сохраняет ВСЕ изменения, в том числе и удаленные строки и файлы, и их всегда можно посмотреть и при необходимости откатить обратно. (Однако бойтесь команд с опциями --force или --hard - с ними все-таки можно что-то потерять).



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

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

Для начала покажу как работать с git в одиночку. При этом не нужен какой-то центральный репозиторий на сервере. Репозиторий создается прямо в папке проекта в виде подпапки .git (имя начинается с точки). Давайте сделаем его.
Примечание: все команды вводят в консоли. Под винду поставьте себе что-то вроде Total Commander или Far Manager, ну или на худой конец, запустите cmd.

Зайдем в папку с проектом и в командной строке пишем:
git init
Появится папка .git. Это пустой пока репозиторий. Если написать:
git status
то гит покажет список новых файлов (Untracked files) и даже даст подсказку: use "git add <file>..." to include in what will be committed. Т.к. репозиторий пустой, то для гита все файлы новые.

Давайте сделаем, что он просит - поместим туда все файлы проекта:
git add .
(точка значит содержимое всей текущей папки с подпапками). Теперь команда status покажет этот же список но уже как Changes to be committed. Файлы все еще не в репозитории, а лишь проиндексированы, т.е. подготовлены для помещения туда.

Зачем такие сложности? Представьте, что вы не послушались моего совета и таки за день написали 5 новых фич и пофиксили 10 багов, а теперь все-таки решили разделить свою работу за день на несколько комитов. (Кстати просмотреть эти изменения можно командой diff). Тогда, командами git add <файл> и git reset <файл> (убирает из комита) вы подготавливаете к комиту не все измененные за день файлы, а только те, которые относятся к какой-то одной фишке или багу и делаете комит. Конечно, может случиться так, что изменения в одном файле относятся к нескольким фичам или багам, тогда разделить их на отдельные комиты не получится (по секрету: таки можно разделить командой add с параметром -p). Вот поэтому я и советовал комитить сразу же, а не в конце дня.

И, наконец, делаем комит:
git commit -m"init"
В кавычках указывается комментарий к комиту. Комментарии обязательны. Если не указать комментарий, то гит запустит встроенный консольный редактор vi, в котором без поллитры не разберешься.

Теперь копия проекта находится в репозитории, что нам и показывает
git status
ответ:
nothing to commit, working directory clean.

Лезть в папку .git и искать там исходники безполезно - там все сжато и закодировано. Список файлов в базе можно получить командой git ls-files, хотя более полезна команда git log, которая показывает сделаные комиты (свежие сверху).

Комиты имеют совершенно дикие 40-буквенные имена. Чтобы дать человеческое имя последнему комиту (например, очередному релизу), используется команда git tag <имя>.

Посмотреть содержимое комита можно командой show <имя комита или его тэг>. (Кстати, везде где требуется указывать имя комита можно писать только первые 4 символа).

Про откаты изменений будет позже, а то с непривычки можно запутаться. А пока резюмирую, то что уже сказано.

Создание локального репозитория и помещение файлов проекта:
git init
git add .
git commit -m"init"

Просмотр состояния: git status
Просмотр последних еще незакомиченных изменений: git diff
Просмотр последних закомиченных изменений: git show
Список комитов: git log
Просмотр комита: git show <имя комита или его тэг>
Выход из режима просмотра, в котором вы окажитесь по командам show и diff - клавишей 'Q'.
Почти все команды имеют много дополнительных опций. Курите маны: git help <команда>


Часть 2

Теперь про командную работу с проектом.

Хотя я и говорил, что можно просто расшарить папку с репозиторием и работать с ней удаленно, но все же правильнее будет сделать некий центральный репозиторий на сервере, который будет доступен 24 часа в сутки по определенному url-у. Для работы с ним участникам проекта достаточно знать лишь этот url.

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

В начале работы возможны 2 ситуации: 1) новый участник команды подключается а уже существующему проекту и 2) команда начинает новый проект.

В первом случае на сервере уже есть git-репозиторий и новуму участнику нужно получить его копию. Для этого в папке где у вас хранятся проекты пишем:

git clone <url, который вам дали>
создастся папка со всеми файлами проекта и с локальным .git репозиторием внутри. Все. Дальше можно работать как было описано в части 1.

Во втором случае кому-то из команды придется создать на сервере новый репозиторий и залить туда болванку нового проекта. Для этого на сервере в отдельной папке (традиционно имя этой папки должно заканчиваться на .git) пишем:
git init --bare
(--bare означает что этот репозиторий предназначен только для удаленной работы).

Затем, уже со своей машины, делается синхронизация локального (не пустого) репозитория c серверным (пока пустым).
Для начала нужно их познакомить, т.е. сообщить локальному репозиторию координаты сервера. Для этого пишем:
git remote add origin <url сервера и путь к проекту>
(Эта команда фактически дает алиас (короткое имя) данному url-у. Традиционно центральный репозиторий называют origin).
Затем заталкиваем на сервер комиты из локального:
git push -u origin master
эта команда заталкивает коммиты из ветки master в удаленный репозиторий origin
(master - это имя главной ветки, а ключ -u задает ветке master по умолчанию репозиторий origin, т.е. в дальнейшем можно просто писать git push не указывая что и куда, но об этом тоже потом).

Дальше остальные участники команды действуют как описано в п. 1), т.е.
git clone <url, который вам дали>

Часть 3

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

Давайте разберем все более подробно, по шагам на конкретном примере.

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

1. Создадим 3 папки: srv, dev1, dev2. srv - это наш сервер, а dev1 и dev2 - это машины двух членов команды.

2. В папке srv создаем подпапку prj, заходим в нее и пишем:
git init --bare
это наш центральный репозиторий.

3. На машине dev1 (в папке dev1) сделаем заготовку нового проекта. Для этого создаем в dev1 папку prj, а в ней один файл (назовем его aaa) со следующим содержимым:
1 str
2 str
3 str

4. Создаем локальный репозиторий и комитим этот файл:
git init
git add .
git commit -m"init"
Связываем с центральным репозиторием и синхронизируем:
git remote add origin ../srv/prj
git push -u origin master

5. На машине dev2 (заходим в папку dev2) получаем копию проекта:
git clone ../srv/prj

На этом этапе у нас 3 синхронизированных репозитория и 2 одинаковые копии проекта.

Предположим теперь, что наши 2 разработчика изменили по 1 строке в файле ааа.
dev1/prj/aaa:
1 str
dev1 str
2 str
3 str

dev2/prj/aaa:
1 str
2 str
dev2 str
3 str

Оба закомитили свои изменения:
dev1: git commit -am"dev1 string "

dev2: git commit -am"dev2 string"
(ключ -a позволяет обойтись без команды add, все модифицированные файлы буут автоматически добавлены в индекс и закомичены, однако, все новые файлы будут проигнорированы).

Предположим, что dev1 успел первым затолкать свой комит в центральный репозиторий:
dev1:
git push

Если теперь dev2 попытается сделать тоже самое, то git ответит ошибкой:
! [rejected]        master -> master (fetch first)
error: failed to push some refs to...
и посоветует сделать вначале git pull.
Так произошло потому, что у dev2 устаревшая копия центрального репозитория и ее требуется обновить. Это и делает команда pull.
Обновляем:
git pull
При этом гит нам сообщит, что:
Auto-merging aaa
Merge made by the 'recursive' strategy.
 aaa | 1 +
В файле ааа произошло автоматическое слияние изменений полученных из центрального репозитория (их туда внес dev1) и изменений сделанных dev2. Заглянем в файл ааа:
1 str
dev1 str
2 str
dev2 str
3 str

Видим, что git слил 2 варианта файла в один. В нем теперь присутствуют изменения сделанные и dev1 и dev2.

Но этот результат слияния пока что находится у dev2, его все еще нужно залить на сервер:
git push
На этот раз все завершилось успешно.

Правда теперь у dev1 могут возникнуть точно такая же проблема. Поэтому заведите себе за правило делать git pull перед началом работы и пред push.
dev1:
git pull

Как говорилось ранее, git работает не с файлами, а со строками. Вставлять, удалять и модифицировать строки внутри файла, для него совершенно естественно. Но что будет если два разработчика по-разному изменили одну и ту же строку в файле? Давайте попробуем.
dev1/prj/aaa:
1 str
dev1 str
2 dev1 was here
dev2 str
3 str

dev2/prj/aaa:
1 str
dev1 str
2 dev2 write smth
dev2 str
3 str

Далее оба комитят и пушат, но dev1 опять успел первый.
dev1:
git commit -am"change 1"
git pull
git push

dev2:
git commit -am"change 1"
git pull
А вот теперь проблема:
Auto-merging aaa
CONFLICT (content): Merge conflict in aaa
Automatic merge failed; fix conflicts and then commit the result.
git не может автоматически слить изменения, т.к. они затрагивают одну и туже строку, поэтому просит разобраться вас. Открываем файл aaa и видим следующий ужас:
1 str
dev1 str
<<<<<<< HEAD
2 dev2 write smth
=======
2 dev1 was here
>>>>>>> d5f8ee639f7792636cd05ba6140c607ae47292de
dev2 str
3 str

git имеет 2 варианта одной и тойже строки (сверху ======= ваш вариант, снизу из центрального репозитория) и не знает какой выбрать. Вам придется либо самому решить какой вариант правильный, либо идти к своему напарнику dev1 и выяснить это с ним. Когда конфликт будет разрешен вы удаляете из файла все от строки <<<<<<< до >>>>>>> включительно, оставив вместо них правильный вариант строки:
1 str
dev1 str
2 dev2 love dev1
dev2 str
3 str

Комитим, пушим:
git commit -am"merge"
git push

Теперь все ок.

Кстати, конфликт возможен даже если вы с напарником просто дописали по одной строке в конец файла. Можно было бы предположить, что в этом случае нужно оставить обе строки - и вашу и напарника, но чья строка должна идти первой?

Комментариев нет:

Отправить комментарий