На протяжении курса я постоянно подчёркиваю две вещи:

  • Отсутствует сервер
  • Локальный репозиторий самодостаточен

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

Рабочий процесс, в котором задействованы несколько разработчиков (или один, но работающий из разных мест), должен выглядеть хотя бы так:

Centralized Workflow

Это не единственный способ работы, но самый простой. Он требует наличия центрального репозитория, через который идёт обмен кодом. С точки зрения Git, этот репозиторий немного особенный и создаётся он специальным образом (у него нет рабочей копии).

$ mkdir example_bare
$ cd example_bare
example_bare$ git init --bare

Если в обычной ситуации инициализация репозитория приводит к созданию вложенной директории .git, то в случае чистого (bare) репозитория, содержимое директории выглядит так:

example_bare$ ls
HEAD    config    description    hooks    info    objects    refs

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

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

$ cd /tmp
# Переименуем папку клонированного репозитория для того чтобы не путаться
tmp$ git clone ~/example_bare clone_of_example
Cloning into 'clone_of_example'...
done.

tmp$ cd clone_of_example

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

А вот как на самом деле связан наш клон с основным репозиторием:

clone_of_example$ git remote -v
# В вашем случае путь будет отличаться,
# он зависит от операционной системы и того где вы инициализировали репозиторий
origin  /home/user/example_bare (fetch)
origin  /home/user/example_bare (push)

# То же самое через чтение конфига
clone_of_example$ cat .git/config
[remote "origin"]
    url = /home/user/example_bare
    fetch = +refs/heads/*:refs/remotes/origin/*

Как видите, нет никакой магии: вся связь – это несколько записей в файле .git/config, в котором origin – это произвольное имя (можно поменять) удалённого репозитория, с которым связан текущий. Кстати, таких удалённых репозиториев может быть сколько угодно. На практике такое поведение действительно бывает нужно, когда изменения отправляются сразу в несколько мест.

Давайте теперь посмотрим на главное — как отправлять изменения:

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

clone_of_example$ echo '# Hello' > README.md
clone_of_example$ git add README.md
clone_of_example$ git commit -am 'add readme'
[master 144d274] replace readme
 1 file changed, 1 insertion(+), 0 deletions(-)

# Отправка
clone_of_example$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 253 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To /home/user/example_bare
   3a64fcc..144d274  master -> master

Итак, у нас добавилась новая команда — git push. Эта команда выполняет отправку новых коммитов из текущего репозитория в удалённый. Здесь origin — имя удалённого репозитория, master — конкретная ветка, в которую отправляются (разг. пушатся) изменения. Каждый раз набирать эту команду полностью довольно утомительно. Поэтому Git позволяет не указывать удалённую ветку, если сделать её «отслеживаемой веткой» (upstream branch).

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

Отслеживаемая ветка устанавливается командой git push --set-upstream origin master. Эта команда выполняет два действия: устанавливает отслеживание и отправляет изменения. Она выполняется ровно один раз, далее можно отправлять изменения набирая просто git push. Информация об отслеживаемой ветке описывается в файле .git/config в виде секций [branch "имя ветки"]:

clone_of_example$ cat .git/config
[branch "master"]
    remote = origin
    merge = refs/heads/master

Получение изменений

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

tmp$ git clone ~/example_bare clone_of_example2
$ cd clone_of_example2
clone_of_example2$ git pull
Updating 3a64fcc..144d274
Fast-forward
 README.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

Команда git pull отработает предсказуемым образом в том случае, если в нашей локальной ветке не было изменений и произойдёт так называемое слияние в режиме fast forward. То есть просто добавятся новые коммиты так, как будто мы их локально прямо сейчас и сделали. А что будет в ситуации, когда мы делаем git pull, а в локальной ветке есть зафиксированные изменения?

А произойдёт самый настоящий git merge. Под капотом git pull делаются две операции — одна git fetch, которая просто получает изменения и складывает их к себе во внутренности директории .git, а вторая — git merge.

Хорошая практика с Git подразумевает отсутствие коммитов слияния (разг. мержкоммиты) при скачивании изменений в локальный репозиторий, поэтому использовать git pull не рекомендуется. Чтобы история была «прямой», нужно использовать rebase, например, так git pull --rebase. Если в процессе возникнут конфликты, то вас попросят их решить. Подробнее про rebase в этой статье.

Ветвление

Если вы хотите посмотреть на то, как выглядит история коммитов с учётом ветвления, вам поможет команда, показанная ниже.

racket$ git log --oneline --decorate --graph
* d7b64b2 (HEAD -> master, new-feature) implement new feature
* c082d77 (origin/master, origin/HEAD) fix stripping
* 442c2fd Correct typo
* b2b53d6 fix test that uses IPv6
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →