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

Поэтому представим гипотетическую ситуацию. Есть один проект. Пусть он будет опенсурсным и все его исходники лежат на том же самом гитхабе по адресу (ссылко):

https://github.com/jashkenas/coffee-script

Проект красивый и жирный, имеет в себе кучу веток. В самый раз для нас. Далее мы будем говорить не в понятиях "проектов", а в понятиях "репозиториев". Гит отличается от того же самого SVN (на котором я работал долго-долго) тем, что в нем нет единого центрального репозитория, в который сливаются все изменения кода (коммиты). В гите каждый разработчик по сути - владелец собственного репозитория. "Главный" репозиторий назначается лишь соглашениями. По сути данные с так называемого "главного" репозитория будут просто деплоиться на рабочий сервер.

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

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

1) Есть "главный" репозиторий. В нашем случае это репозиторий за авторством jashkenas что по ссылке. Как правило, в этом репозитории есть (условно) ветки master и development, так же обусловимся, что этот репозиторий будет являться "удаленным" (remote). Ветка master - это ветка с рабочим проектом, это тот код, который деплоится на рабочий сервер, компилится в рабочий продукт и т.д. Ветка development используется для внесения в нее изменений (рефакторинг существующего кода, добавление новой функциональности). После очередной итерации ветка development мержится в master и новая версия деплоится на сервер.

Но т.к. тут (в кофескрипте) деплоить нечего, то и ветки development нет. А есть множество других веток. В данном конкретном примере будем работать с единственной веткой master.

2) Каждый разработчик может создавать собственные ветки. Но чтобы не загаживать ими основной репозиторий, каждый из разработчиков должен работать в пределах собственного репозитория. Что значит "форкнуть" проект на гитхабе? Это значит создать полную копию репозитория. Что ж, форкнем его и получим еще один удаленный репозиторий по адресу (ссылко):

https://github.com/s0ber/coffee-script

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

4) Вот мы и форкнули весь проект. Теперь нам нужно заиметь его на собственном компьютере. Сейчас будет создан еще один репозиторий (уже третий). Этот репозиторий будет условно "локальным". Будем использовать SSH-протокол.

mkdir coffee
cd coffee
git init
git remote add s0ber git@github.com:s0ber/coffee-script.git

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

git remote add s0ber git@github.com:s0ber/coffee-script.git

Этой командой мы сказали "сделай у нашего текущего репозитория ссылку на удаленный репозиторий git@github.com:s0ber/coffee-script.git и для удобства дай этой ссылке понятное имя s0ber". Теперь мы всегда будем знать что s0ber - это ссылка на наш удаленный репозиторий. На данный момент это всего лишь ссылка. Но т.к. других ссылок нет, эта же ссылка по-умолчанию стала основной для связи с удаленными репозиториями. Посмотрим, что мы имеем в файле .git/config на данный момент:

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "s0ber"]
	url = git@github.com:s0ber/coffee-script.git
	fetch = +refs/heads/*:refs/remotes/s0ber/*

Что же, добавим еще одну ссылку. Она понадобится нам для того, чтобы забирать самые свежие данные из "главного" репозитория.

git remote add jashkenas git://github.com/jashkenas/coffee-script.git

Посмотрим, что теперь в нашем .git/config:

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "s0ber"]
	url = git@github.com:s0ber/coffee-script.git
	fetch = +refs/heads/*:refs/remotes/s0ber/*
[remote "jashkenas"]
	url = git://github.com/jashkenas/coffee-script.git
	fetch = +refs/heads/*:refs/remotes/jashkenas/*

5) Получаем данные с сервера в первый раз. Теперь у нас есть ссылки на удаленные репозитории. Одна ссылка на наш форкнутый репозиторий, другая - на исходный ("главный"). Забираем данные из любого из них:

git fetch s0ber

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

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

git branch

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

git merge s0ber/master

Отлично! Теперь у нас есть ветка master с которой можно спокойно себе работать. Но так же нам необходима ветка с кодом, находящимся в "главном" репозитории. Это необходимо для того, чтобы мы всегда имели под рукой текущее состояние проекта. Для этого создадим отдельную ветку.

git checkout -b jashkenas_master
git fetch jashkenas
git merge jashkenas/master 

Возможно, за те несколько минут, что вы читали эту статью, jashkenas внес изменения в свой проект. :) Но вероятнее всего, вы получите надпись "Already up-to-date". Заметьте, гит не будет выкачивать весь проект заново. Особенность и главная фишка гита в том, что он оперирует не ревизиями, а коммитами. Весь проект по сути это набор коммитов. У каждого коммита есть уникальный идентификатор. При получении данных с удаленного сервера, гит просто проверяет, каких коммитов ему не достает, и выкачивает их. Таким же образом производится работа с ветками. Отсюда и невероятная скорость.

6) Теперь разберемся, каким образом мы можем связать ветку на локальном репозитории с веткой на удаленном. Есть два способа. Рассмотрим каждый из них.

git branch --set-upstream jashkenas_master jashkenas/master
# Branch jashkenas_master set up to track remote branch master from jashkenas.

Теперь ветка jashkenas_master следит за аналогичной веткой в удаленном репозитории. С этого момента, находясь в этой ветке, мы всегда можем использовать:

git pull

Это замена для вот этих команд:

git fetch jashkenas
git merge jashkenas/master

Глянем на содержимое .git/config ;)

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
[remote "s0ber"]
	url = git@github.com:s0ber/coffee-script.git
	fetch = +refs/heads/*:refs/remotes/s0ber/*
[remote "jashkenas"]
	url = git://github.com/jashkenas/coffee-script.git
	fetch = +refs/heads/*:refs/remotes/jashkenas/*
[branch "jashkenas_master"]
	remote = jashkenas
	merge = refs/heads/master

Мы видим, что теперь ветка jashkenas_master связана с удаленным репозиторием jashkenas. И что в нее будут мержиться данные из ветки master этого репозитория.

Чтобы понимать что происходит, расскажу о том, как связать ветки другим способом. Завяжем локальную ветку master на аналогичную ветку в удаленном репозитории s0ber.

git checkout master
git push -u s0ber master

Здесь мы впервые использовали команду push. С ее помощью мы отсылаем данные в удаленный репозиторий. Данные из текущей ветки (в нашем случае это ветка master) отсылаются в ветку master удаленного репозитория s0ber. Обратите внимание на флаг -u. По сути это синоним флагу --set-upstream у команды git branch. Здесь мы сразу отправили данные на сервер и связали текущую ветку с той веткой, в которую мы отправляли эти данные. Если сейчас посмотрим в .git/config, то увидим в нем новые строки:

[branch "master"]
	remote = s0ber
	merge = refs/heads/master

7) Теперь о самой разработке кода. Когда мы хотим внести какие-то новые изменения, то мы сначала создаем новую ветку. Заметьте, новая ветка всегда создается от "текущего момента" текущей ветки. Поэтому заходим в jashkenas_master и создаем ветку для разработки:

git checkout jashkenas_master
git checkout -b new_feature
subl new_feature.txt
subl README

Здесь мы используем редактор Sublime Text 2 чтобы создать новый файл и сразу отредактировать его а так же чтобы отредактировать существующий файл README. Посмотрим на текущее положение дел:

git status

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

git diff

Добавим все эти записи в индекс и закоммитим их:

git add .
git commit -m 'new feature'

Теперь ветка new_feature в нашем локальной директории заимела новый коммит.

8) Мержим ветку new_feature с нашей основной веткой. В данном случае репозиторий s0ber и его ветка master необходим нам для разрешения конфликтов. Несмотря на то, что ветка master "привязана" к удаленной ветке s0ber/master, мы всегда можем вручную обновить ее актуальными данными из ветки jashkenas/master. Мы будем делать это для того, чтобы пофиксить конфликты перед пулл-реквестом в репозиторий уважаемого jashkenas. Для этого скачиваем в ветку master данные вручную:

git checkout master
git fetch jashkenas
git merge jashkenas/master

Теперь воспользуемся веткой master для того, чтобы с ее помощью сэмулировать ветку jashkenas/master. Мы воссоздадим ситуацию, в которой были бы конфликты при объединении (merge) веток. s0ber/master на данный момент является копией "главного" репозитория. Сейчас мы внесем в него изменения вручную, но на рабочем проекте это делать не рекомендуется (ветка master всегда должна являть собой копию удаленной ветки jashkenas/master). Всё это исключительно для образовательных целей:

subl README # вносим другие изменения, чтобы вызвать искусственный конфликт
git commit -m 'commit to make conflict :P'
git push

9) Теперь попробуем сделать слияние нашего изменения из коммита new_feature с нашей веткой master.

git checkout new_feature
git merge master
# Auto-merging README
# CONFLICT (content): Merge conflict in README
# Automatic merge failed; fix conflicts and then commit the result.

Как мы видим, ветки объединить не удалось и мы получили конфликт. Посмотрим на текущий статус:

git status

Гит сам говорит нам о то, что произошло и какие файлы не удалось смержить (и почему). Разрешаем конфликт, отредактировав конфликтные файлы и коммитим изменения:

subl README
git add .
git commit -m 'fixing conflicts'

Мы так же можем откатить изменения. Для этого используем (гипотетически, в нашем случае ничего откатывать не нужно):

git reset --hard HEAD

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

git reset --hard ORIG_HEAD

Что мы имеем на данный момент? У нас есть ветка с новой фичей. Так же эта ветка успешно прошла "испытание на слияние с текущим состоянием проекта". Мы можем со спокойной душой пушить ее на наш удаленный сервер:

git merge master
git push s0ber new_feature:master

Заметьте, что при пуше мы указываем ветку, из которой мы пушим, даже при том условии, что она выбрана в качестве текущей. Пуш сделан, коммит можно посмотреть по ссылке.

Теперь мы можем переключиться в локальную ветку master, а ветку new_feature удалить за ненужность.

git checkout master
# Switched to branch 'master'
# Your branch is behind 's0ber/master' by 2 commits, and can be fast-forwarded.
git branch -d new_feature
# error: The branch 'new_feature' is not fully merged.
# If you are sure you want to delete it, run 'git branch -D new_feature'.

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

git pull
git branch -d new_feature
# Deleted branch new_feature (was 10e27d9).

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

11) upd: Написал статью, а теперь вот думаю, зачем же нам ветка jashkenas_master? :) Она всё-таки нужна. Т.к. над кодом могут работать сразу несколько человек и тогда должна быть возможность получать данные и из репозитория другого человека (они будут сливаться в ветку master) и из репозитория jashkenas (они будут сливаться в ветку jashkenas_master). Таким образом вы сможете фиксить конфликты и с вашим напарником по разработке и с основным репозиторием.

Вот и всё, уважаемые читатели. Несколько часов ушло на написание этой статьи. Но, должен признаться, писал я ее в первую очередь для себя, попутно загоняя команды в консоль. Так как для рабочих нужд ВНЕЗАПНО стал использоваться гит, а трогать его и экспериментировать с ним на рабочем проекте рука не поднимается. Вот таким образом, написав статью, я сам разложил всё в своей голове по полочкам и теперь гит не кажется мне неукротимым зверем. Возможно, всё это было излишне и вы даже не дочитали до сюда. Но для меня эта статья оказалась очень важна. ;)

До скорых встреч!