Expertus metuit
Кратко о git
Опубликовано 2014-02-17 в 15:20

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

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

Я буду рассказывать только о классической программе git, работающей из командной строки. Все примеры использования предназначены для UNIX-подобных систем, например, линукса или макоси. Все примеры создания или модификации файлов также рассчитаны на UNIX-подобные системы. Почему именно консольный вариант: в гуёвых программах типа TortoiseGit очень сложно понять концепты, их авторы подразумевают, что вы всё уже знаете, а это не так. Почему линукс и макось: терминал в windows очень некомфортен, работать там больно и мучительно. Если у вас Windows, советую поставить убунту в Virtualbox/VMWare, также в комментариях Искандер упоминает git-bash для windows, но я об этом ничего сказать не могу.

Перед вами именно туториал, то есть в идеале вы должны читать его последовательно с начала и до конца, выполняя примеры из текста. У git обширная собственная терминология и куча общепринятых (в рамках git-экосистемы) названий сущностей; я постараюсь рассказать о самых важных из них, а вы постарайтесь их запомнить, они действительно очень важны и пригодятся, когда вы будете читать документацию или книги. Часть терминов я буду давать в переводе (например, «ветка» для слова «branch»), а часть — в транслитерации, так как адекватного перевода слова нет (например, «дифф» как перевод слова «diff»). Я очень не люблю многословные переводы простых слов и стараюсь по возможности ими не пользоваться, для понимания короткие слова важнее длинных фраз. Поэтому привыкайте к словам типа «коммит» или «репозиторий».

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

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

Подготовка рабочего окружения

Листинги shell-команд в этом тексте обычно выглядят так:

[user@shell]$ ls -la
total 0
drwxr-xr-x   3 sigsergv  staff   102 22 фев 14:50 .
drwxr-xr-x+ 73 sigsergv  staff  2482 22 фев 14:51 ..

В листинге строчки, начинающиеся с [user@shell]$, обозначают команды, которые нужно ввести, а после них — вывод команды в терминале.

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

[user@shell]$ mkdir $HOME/learning-git

Все команды дальше по тексту написаны именно для такого каталога, $HOME — это переменная окружения, в которой записан путь до вашего домашнего каталога в системе, при исполнении команды/программы она разворачивается до полного пути и в собственно команду или программу уже передаётся полный путь, который для каждого юзера разный.

Естественно, нужно установить git, в убунте/дебиане это делается командой sudo apt-get install git, в макоси git уже установлен. После установки нужно доконфигурировать — указать, какое имя и e-mail будет использоваться для коммитов (записываем в глобальный конфиг, который используется для всех репозиториев на этой машине):

[user@shell]$ git config --global user.email "[email protected]"
[user@shell]$ git config --global user.name "Your Name"
[user@shell]$

Git, как практически любая unix-команда, поддерживает команду man для выдачи справки, вызывается обычно в виде man git-SUBCOMMAND, например, man git-push или man git-commit.

Первое знакомство

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

Начнём с самых простых операций: создание репозитория, добавление файла и коммит изменений.

Заходим в каталог $HOME/learning-git и создаём первый git-репозиторий:

[user@shell]$ cd $HOME/learning-git
[user@shell]$ git init first-git-repo
Initialized empty Git repository in /Users/sigsergv/learning-git/first-git-repo/.git/
[user@shell]$

После выполнения этой команды в каталоге $HOME/learning-git/first-git-repo будет создан новый пустой git-репозиторий. Строго говоря, репозиторий находится в каталоге $HOME/learning-git/first-git-repo/.git, а в каталоге first-git-repo находится рабочая копия или рабочий каталог репозитория, в рабочем каталоге лежат файлы, с которыми и происходит работа, но в данный момент там нет ни одного файла, так как репозиторий новый.

Зайдём в каталог $HOME/learning-git/first-git-repo, создадим в нём новый файл и посмотрим на результат:

[user@shell]$ cd $HOME/learning-git/first-git-repo
[user@shell]$ echo "bla-bla-bla" > README.txt
[user@shell]$ git status
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   README.txt
nothing added to commit but untracked files present (use "git add" to track)
[user@shell]$

Команда git status показывает текущее состояние рабочего каталога, в данном случае у нас есть один ещё не добавленный в репозиторий файл README.txt, а ещё выдаётся подсказка, как его добавить. Добавим:

[user@shell]$ git add README.txt
[user@shell]$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   README.txt
#

Здесь мы видим, что файл подготовлен для коммита. Команда git add подготавливает указанные файлы для записи в репозиторий, по сути она запоминает состояние файлов во временном буфере (который называется накопительным буфером3, по-английски — staging area), и далее буфер можно закоммитить командой git commit:

[user@shell]$ git commit -m "First commit"
[master (root-commit) d383df1] First commit
 1 file changed, 1 insertion(+)
 create mode 100644 README.txt

Мы получили первый коммит с комментарием First commit, можно на него посмотреть в истории коммитов:

[user@shell]$ git log
commit d383df155ca2481f392cde6791da3bec7aeec03d
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:50:21 2014 +0700

    First commit

Обратите внимание на аргумент -m "First commit", он позволяет задать комментарий прямо в самой команде. Если его опустить, то при выполнении команды откроется текстовый редактор, где вам нужно будет ввести комментарий, а затем закрыть редактор, чтобы коммит завершился.

Наш коммит является объектом1 с идентификатором d383df155ca2481f392cde6791da3bec7aeec03d, этот идентификатор является хеш-суммой всего коммита (файлов, даты коммита, имени автора и т.д.).

Но давайте ещё поиграемся — обновим файл README.txt и посмотрим статус:

[user@shell]$ echo "another line" >> README.txt
[user@shell]$ git status
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   README.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

Файл изменился и для него опять нужно выполнить серию команд git add, git commit, чтобы записать коммит в репозиторий. Это очень важный момент, в отличие от subversion или mercurial, файл коммитится за два шага: через git add подготавливаем выбранные файлы для коммита и потом уже командой git commit коммитим подготовленные файлы. Впрочем, мы можем оба шага сгруппировать и командой git commit README.txt сразу закоммитить указанный файл или файлы. Подробнее об этой схеме работы я расскажу в разделе Накопительный буфер.

Добавим ещё один каталог с одним файлом внутри:

[user@shell]$ mkdir doc
[user@shell]$ echo "Documentation index" > doc/index.txt
[user@shell]$ git status
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   README.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   doc/
no changes added to commit (use "git add" and/or "git commit -a")

У нас есть один модифицированный файл из репозитория (README.txt), есть один ещё не добавленный в репозиторий каталог (doc/). Добавим их все и закоммитим:

[user@shell]$ git add README.txt doc
[user@shell]$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   README.txt
#   new file:   doc/index.txt
[user@shell]$ git commit -m "Updated docs"
[master db31b7c] Updated docs
 2 files changed, 2 insertions(+)
 create mode 100644 doc/index.txt

Коммиты и ветки

В предыдущем разделе мы сделали два простых последовательных коммита в репозиторий. Каждый коммит является объектом1 в репозитории и у него есть свой уникальный идентификатор (например, d383df155ca2481f392cde6791da3bec7aeec03d). Также у коммита есть набор атрибутов, например, комментарий или дата коммита. Коммиты образуют историю коммитов (commit log), которую можно смотреть командой git log.

Линейная цепочка коммитов образует ветку (по-английски branch). У каждой ветки есть название, при инициализации репозитория git создаёт одну ветку с названием master, и все коммиты по умолчанию попадают именно в неё. Веток может быть много, команда git branch выводит список всех веток:

[user@shell]$ git branch
* master
[user@shell]$

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

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

  • в имени можно использовать алфавитно-цифровые символы, включая буквы русского алфавита, например;
  • можно создавать «иерархичные» имена, используя символ прямого слеша / в качестве разделителя, например, bug/im-1324, однако нельзя завершать имя таким слешем, нельзя начинать компонент имени точкой (то есть имя bug/.test некорректное);
  • нельзя в имени использовать символы типа пробела или табуляции;
  • нельзя использовать две точки подряд;
  • нельзя использовать эти символы: ~^:?*[;
  • нельзя использовать служебные ASCII-символы (с кодом меньше 0x20).

Ветка как правило «растёт» из конкретного коммита, за исключением веток типа master, у которых нет родительского коммита. Ветка создаётся командой git branch <BRANCH_NAME>:

[user@shell]$ git branch development
[user@shell]$ git branch
  development
* master
[user@shell]$

Обратите внимание, что после создания ветки активной остаётся изначальная ветка. Чтобы переключиться на новую, используется команда git checkout <BRANCH_NAME>:

[user@shell]$ git checkout development
Switched to branch 'development'
[user@shell]$ git branch
* development
  master
[user@shell]$

Мы получили новую ветку, унаследовав все коммиты из родительской ветки, теперь команды типа git commit будут изменять ветку development и новые коммиты пойдут именно в неё. Создадим в этой ветке новый файл и закоммитим:

[user@shell]$ echo "GPL" > LICENSE.txt
[user@shell]$ git add LICENSE.txt
[user@shell]$ git commit -m "Added license file"
[development c1f6047] Added license file
 1 file changed, 1 insertion(+)
 create mode 100644 LICENSE.txt

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

[user@shell]$ git log
commit c1f60477b1541aaf7704cb0389d69915b7b750ff
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:56:29 2014 +0700

    Added license file

commit db31b7c9d0a3dde765936c866b6d94201c6e149d
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:52:48 2014 +0700

    Updated docs

commit d383df155ca2481f392cde6791da3bec7aeec03d
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:50:21 2014 +0700

    First commit

Переключимся назад на ветку master и посмотрим на историю коммитов и содержимое рабочего каталога:

[user@shell]$ git checkout master
Switched to branch 'master'
[user@shell]$ git log
commit db31b7c9d0a3dde765936c866b6d94201c6e149d
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:52:48 2014 +0700

    Updated docs

commit d383df155ca2481f392cde6791da3bec7aeec03d
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:50:21 2014 +0700

    First commit
[user@shell]$ ls -l
total 8
-rw-r--r--  1 sigsergv  staff   25 19 мар 19:51 README.txt
drwxr-xr-x  3 sigsergv  staff  102 19 мар 19:52 doc
[user@shell]$

В ветке master всё осталось по-прежнему: тот же набор файлов, та же история коммитов. Можно также посмотреть, на каком коммите произошло разделение веток:

[user@shell]$ git merge-base master development
db31b7c9d0a3dde765936c866b6d94201c6e149d
[user@shell]$

db31b7c9d0a3dde765936c866b6d94201c6e149d — это идентификатор первого коммита из ветки development.

Вообще ветку можно начать с произвольного коммита, например, с первого коммита из ветки master (посмотрите на вывод git log выше, этот коммит имеет идентификатор d383df155ca2481f392cde6791da3bec7aeec03d):

[user@shell]$ git branch test-branch d383df155ca2481f392cde6791da3bec7aeec03d
[user@shell]$ git checkout test-branch
[user@shell]$ git log
commit 1507c9f52ee65196a3f9e55d816fbd99df42967b
Author: Sergey Stolyarov <[email protected]>
Date:   Sat Feb 22 15:30:23 2014 +0700

    First commit
[user@shell]$ ls -l
total 8
-rw-r--r--  1 sigsergv  staff  12 19 мар 19:59 README.txt
[user@shell]$

Как видим, в истории всего один коммит, а в рабочем каталоге только созданный в нём файл. Отредактируем файл README.txt и закоммитим его в этой ветке:

[user@shell]$ echo 'Testing branches' >> README.txt
[user@shell]$ git commit -m 'New branch, new commit.' README.txt
[test-branch ad43092] New branch, new commit.
 1 file changed, 1 insertion(+)
[user@shell]$ git log
commit ad43092db56abdf1ea93ed478efa06953d6958b1
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:59:39 2014 +0700

    New branch, new commit.

commit d383df155ca2481f392cde6791da3bec7aeec03d
Author: Sergey Stolyarov <[email protected]>
Date:   Wed Mar 19 19:50:21 2014 +0700

    First commit
[user@shell]$

К этому моменту в репозитории у нас три ветки: master, development и test-branch, а ветка, напомню, представляет собой линейный упорядоченный набор коммитов, поэтому схематично структуру репозитория можно представить так:

tree

Здесь A, B, C, D, E означают коммиты, ветка master состоит из коммитов A → B, ветка development — A → B → D → E, и ветка test-branch — A → C.

Накопительный буфер

О накопительном буфере (staging area) нужно рассказать особо, поскольку понимание принципов его функционирования очень важно для правильной работы с рабочим каталогом (правильные коммиты, откат изменений, просмотр диффов и так далее).

Буфер является чем-то вроде «перевалочной базы», в которой запоминаются и накапливаются состояния файлов для последующего коммита, практически всегда файл перед коммитом проходит через буфер, иногда в скрыто-ускоренном виде, если используются команды типа git commit <FILE> или git commit -a.

По-английски процесс добавления в буфер называется stage, добавленные в него файлы называются staged, а собственно буфер называется staging area.

В ранних версиях также использовался термин индекс/index, но теперь от него стараются избавляться, поскольку слово «индекс» больше запутывает, чем проясняет.

Для добавления в накопительный буфер используется команда git add или её синоним git stage. Чтобы показать, как работают эти команды, добавим немного изменений в файлы:

[user@shell]$ cd $HOME/learning-git/first-git-repo
[user@shell]$ git checkout master
Switched to branch 'master'
[user@shell]$ echo "and a third line" >> README.txt
[user@shell]$ echo "First section" > doc/section.txt
[user@shell]$ 

Мы переключились на ветку master, добавили строчку в файл README.txt, создали новый файл doc/section.txt. Теперь внимательнее посмотрим, что нам выдаст команда git status:

[user@shell]$ git status
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   README.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   doc/section.txt
no changes added to commit (use "git add" and/or "git commit -a")

В этом листинге ещё не добавленные в буфер файлы указываются в секциях Changes not staged for commit и Untracked files. Добавим файл README.txt и посмотрим, что получилось:

[user@shell]$ git add README.txt
[user@shell]$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   README.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   doc/section.txt
[user@shell]$ 

Как мы видим, файл README.txt теперь находится в секции Changes to be committed. В буфер добавляются конкретные изменения, и если после добавления файл изменить, то буфер придётся обновить:

[user@shell]$ echo "========="  >> README.txt
[user@shell]$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   README.txt
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   README.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   doc/section.txt
[user@shell]$

Обратите внимание, что «файл» README.txt присутствует сразу в двух секциях (Changes to be committed и Changes not staged for commit), на самом деле, конечно, это не файл там присутствует, а некий конкретный набор изменений.

Если из такого состояния выполнить git commit, то в репозиторий уедут только изменения из буфера, но не изменения, сделанные после последнего добавления, поэтому обязательно смотрите на вывод git status перед каждым коммитом. Но мы пока делать коммит не будем, нам текущее состояние понадобится для демонстрации в следующем параграфе.

Наличие буфера несколько усложняет просмотр изменений. Например, в subversion команда svn diff показывает изменения в рабочем каталоге относительно репозитория, а в git команда git diff показывает изменения относительно буфера; а чтобы посмотреть изменения буфера относительно репозитория, нужно использовать команду git diff --staged (до версии 1.6.1 вместо --staged нужно было использовать --cached). Оцените разницу:

[user@shell]$ git diff
diff --git a/README.txt b/README.txt
index af3e3d6..43f1db9 100644
--- a/README.txt
+++ b/README.txt
@@ -1,3 +1,4 @@
 bla-bla-bla
 another line
 and a third line
+=========
[user@shell]$ git diff --staged
diff --git a/README.txt b/README.txt
index 6b053c2..af3e3d6 100644
--- a/README.txt
+++ b/README.txt
@@ -1,2 +1,3 @@
 bla-bla-bla
 another line
+and a third line

Обратите внимание, что у нас нигде не фигурирует в диффах содержимое файла doc/section.txt, это потому, что он ещё репозиторию не известен (находится в секции Untracked files вывода команды git status). Его можно добавить в индекс командой git add doc/section.txt, либо обозначить намерение добавить командой git add -N doc/section.txt, в таком случае в буфер попадёт пустой файл, а в секцию Changes not staged for commit собственно всё содержимое doc/section.txt. Выполним вторую команду:

[user@shell]$ git add -N doc/section.txt
[user@shell]$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   README.txt
#   new file:   doc/section.txt
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   README.txt
#   modified:   doc/section.txt
#

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

[user@shell]$ git diff HEAD
diff --git a/README.txt b/README.txt
index 6b053c2..43f1db9 100644
--- a/README.txt
+++ b/README.txt
@@ -1,2 +1,4 @@
 bla-bla-bla
 another line
+and a third line
+=========
diff --git a/doc/section.txt b/doc/section.txt
new file mode 100644
index 0000000..d9d0629
--- /dev/null
+++ b/doc/section.txt
@@ -0,0 +1 @@
+First section

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

HEAD всегда указывает на последний коммит в текущей ветке, после переключения на другую указатель автоматически меняется.

Закоммитим сделанные изменения, они нам позднее пригодятся:

[user@shell]$ git commit -m "New data" README.txt doc/section.txt
[master 76a8ff6] New data
 2 files changed, 3 insertions(+)
 create mode 100644 doc/section.txt

Работа с несколькими репозиториями

Другие репозитории (remotes)

git является распределённой системой контроля версий, то есть предназначен для использования нескольких репозиториев, связанных между собой. Разработчик обычно работает в своём локальном репозитории, периодически синхронизуясь с другим, обычно по сети. Другой репозиторий в терминах git называется remote, в русской литературе их часто называют «удалёнными репозиториями», но на мой взгляд такой термин не очень корректен, поскольку репозиторий не обязательно находится на другом сервере, поэтому везде в этой статье я называю сторонние репозитории просто другими.

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

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

Вернёмся к нашему тестовому окружению. В данный момент у нас есть всего один репозиторий — $HOME/learning-git/first-git-repo. Это каталог в локальной файловой системе, в котором содержится собственно репозиторий (в подкаталоге .git) и рабочая копия с файлами активной ветки.

Разработчик редко начинает репозиторий «с нуля», обычно он клонирует какой-нибудь другой репозиторий в свой локальный и дальше работает уже в нём. Типичная команда клонирования выглядит примерно так:

[user@shell]$ git clone https://github.com/sigsergv/blog-data.git
Cloning into 'blog-data'...
remote: Reusing existing pack: 8, done.
remote: Total 8 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (8/8), done.
Checking connectivity... done

После её выполнения вы получаете клон другого репозитория в каталоге blog-data, он содержит в себе историю коммитов стороннего репозитория и с ним дальше можно работать как с нормальным репозиторием. Базовый синтаксис команды клонирования такой: git clone <REPO_ADDRESS>, здесь <REPO_ADDRESS> — это адрес другого репозитория. Естественно, репозитории могут быть закрытыми и для доступа к ним необходимо иметь какие-то авторизационные данные (пароль или ssh-ключ).

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

[user@shell]$ git init --bare $HOME/learning-git/central-repo
Initialized empty Git repository in /Users/sigsergv/learning-git/central-repo/
[user@shell]$

Эта команда очень похожа на команду создания обычного локального репозитория, только в ней присутствует аргумент --bare, который означает, что нужно создать «голый» репозиторий, без каталога с рабочей копией. Фактически, в таком репозитории лежит то же самое, что и в каталоге .git обычного рабочего репозитория. Так как это «голый» репозиторий, в нём нет рабочей копии, а только коммиты, ветки и другие объекты.

Этот репозиторий в данный момент совсем пустой, в нём нет ни одной ветки, ни одного коммита, вообще ничего. Давайте теперь зальём туда наш пока единственный репозиторий:

[user@shell]$ cd $HOME/learning-git/first-git-repo/
[user@shell]$ git push $HOME/learning-git/central-repo master development
Counting objects: 16, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (16/16), 1.15 KiB | 0 bytes/s, done.
Total 16 (delta 1), reused 0 (delta 0)
To /Users/sigsergv/learning-git/central-repo
 * [new branch]      master -> master
 * [new branch]      development -> development
[user@shell]$

Я пока не буду в деталях объяснять, как именно работает эта команда, скажу лишь, что теперь в другом репозитории (из каталога $HOME/learning-git/central-repo) есть две ветки: development и master со всей историей изменений в них (ветку test-branch мы заливать не стали, а указали в команде только две ветки: development и master). Можно, впрочем, залить все ветки, для этого вместо названий веток нужно указать параметр --all: git push $HOME/learning-git/central-repo --all.

Итак, у нас есть теперь репозиторий, играющий роль центрального сетевого, давайте склонируем его в другой каталог — в $HOME/learning-git/first-repo-clone:

[user@shell]$ cd $HOME/learning-git
[user@shell]$ git clone $HOME/learning-git/central-repo first-repo-clone
Cloning into 'first-repo-clone'...
done.

После выполнения этой команды вы получите клон нашего «центрального» репозитория в каталоге first-repo-clone; мы указали в команде git clone сначала адрес другого репозитория, а затем путь в системе, куда его склонировать. Давайте немного поисследуем новый репозиторий:

[user@shell]$ cd $HOME/learning-git/first-repo-clone
[user@shell]$ git status
# On branch master
nothing to commit, working directory clean
[user@shell]$ git branch
* master
[user@shell]$ git branch -a 
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/development
  remotes/origin/master
[user@shell]$

Как мы видим, простая команда git clone вытащила только одну ветку — master, другие ветки, которые мы создавали, вроде бы отсутствуют, но на самом деле они есть, только скрыты. Команда git clone не просто скопировала данные из другого репозитория, но прописала в новом локальном репозитории связи со старым. В последней команде — git branch -a — показаны не только локальные ветки, но также и ветки с другого репозитория — они начинаются со строки remotes/origin, но давайте этот важный момент разберём подробнее.

Итак, командой git branch -a выводятся референсы всех доступных веток: локальные и так называемые remote-tracking5. С первыми всё понятно — это те ветки, с которыми вы можете работать непосредственно, а вот remote-tracking — это нечто вроде фиксированных образов веток с другого репозитория, их нельзя менять непосредственно, а только командой git pull.

Посмотрим на эту строчку внимательно:

remotes/origin/HEAD -> origin/master

стрелочка означает, что при клонировании рабочая копия по умолчанию будет сделана с origin/master.

Вы можете переключить рабочую копию на remote-tracking ветку:

[user@shell]$ git co remotes/origin/development
Note: checking out 'remotes/origin/development'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at 87200f3... Authors file added

После такой команды вы окажетесь в так называемой «отсоединённой» (detached) ветке, то есть все изменения, которые вы накоммитите здесь, не будут привязаны ни к какой существующей ветке. Однако эти коммиты не пропадут, они в локальном репозитории объектов сохранятся и при желании вы можете их прицепить к новой ветке: git branch new_branch_name 14188d8, здесь 14188d8 — идентификатор «зависшего» коммита, после такой команды вы получите ветку new_branch_name, со стартовым коммитом 14188d8 и всей историей из ветки remotes/origin/development.

Сейчас в списке только одна локальная ветка master, однако вы можете переключиться на ветку development, например:

[user@shell]$ % git checkout development
Branch development set up to track remote branch development from origin.
Switched to a new branch 'development'

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

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

[user@shell]$ git branch test
[user@shell]$ git branch -vv
* development 87200f3 [origin/development] Authors file added
  master      76a8ff6 [origin/master] New data
  test        87200f3 Authors file added
[user@shell]$

Как мы видим (через команду git branch -vv, опция -vv даёт ещё более детальный вывод, чем просто -v), локальные ветки development и master привязаны к соответствующим веткам другого репозитория, а ветка test — нет. Привязка означает, что при отправке изменений в другой репозиторий командой git push эти изменения попадут в правильные места.

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

[user@shell]$ git checkout -b master-2 --track origin/master
Branch master-2 set up to track remote branch master from origin.
Switched to a new branch 'master-2'
[user@shell]$ git branch -vv
  development 87200f3 [origin/development] Authors file added
  master      76a8ff6 [origin/master] New data
* master-2    76a8ff6 [origin/master] New data
  test        87200f3 Authors file added
[user@shell]$

Также можно привязать текущую ветку:

[user@shell]$ git branch --set-upstream-to=origin/branchname

Или вообще произвольную:

[user@shell]$ git branch --set-upstream-to=origin/branchname localbranchname

git remote

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

[user@shell]$ git remote
origin
[user@shell]$

Команда с аргументом -v показывает больше информации:

[user@shell]$ git remote -v
origin  /Users/sigsergv/learning-git/central-repo (fetch)
origin  /Users/sigsergv/learning-git/central-repo (push)
[user@shell]$

В каждой строчке сначала идёт название репозитория, потом его URL (в нашем случае это путь в локальной файловой системе), а затем в скобках операции, для которых это имя используется (в нашем случае один и тот же репозиторий используется для команд fetch и push, команда fetch является частью pull, а о push и pull мы поговорим дальше).

Сейчас у нас в списке только один репозиторий с именем origin6, такое имя автоматически присваивается гитом при клонировании репозитория.

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

А вот слово remote — как раз термин git, на русский его одним словом не перевести и я дальше буду это называть «другим репозиторием». Имя другого репозитория используется в разных командах, по сути является его идентификатором в локальном репозитории.

Ещё более детальную информацию о конкретном репозитории можно посмотреть командой git remote show <REMOTE> (но все детали этого листинга я объяснять не буду, об этом можете прочитать в документации, однако по мере дальнейшего чтения многие детали сами прояснятся):

[user@shell]$ git remote show origin
* remote origin
  Fetch URL: /Users/sigsergv/learning-git/central-repo
  Push  URL: /Users/sigsergv/learning-git/central-repo
  HEAD branch: master
  Remote branches:
    development tracked
    master      tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

Для добавления другого репозитория используется команда git remote add <REMOTE> <URL>, но в ней слишком много опций, плюс она для начального знакомства не особо нужна, поэтому о ней и других подкомандах git remote я здесь рассказывать не буду.

git push

Команда git push отправляет референсы и объекты в другой репозиторий. Это было формальное определение из официального мануала, неформальное же более понятное: git push обновляет другой репозиторий коммитами, сделанными в локальном.

В данный момент, если вы не делали ничего лишнего, у нас сейчас на локальной машине два «рабочих» репозитория: first-git-repo и first-repo-clone, а также один «сетевой» — central-repo. Переключимся на самый первый репозиторий и накоммитим там чего-нибудь в ветку development:

[user@shell]$ cd $HOME/learning-git/first-git-repo
[user@shell]$ git checkout development
Switched to branch 'development'
[user@shell]$ echo "BSD License" > LICENSE.txt
[user@shell]$ git commit -m "License changed to BSD" LICENSE.txt
[development ddf6df8] License changed to BSD
 1 file changed, 1 insertion(+), 1 deletion(-)
[user@shell]$ echo "John Smith <[email protected]>" > AUTHORS.txt
[user@shell]$ git add AUTHORS.txt
[user@shell]$ git commit -m "Authors file added"
[development 87200f3] Authors file added
 1 file changed, 1 insertion(+)
 create mode 100644 AUTHORS.txt

Итак, мы изменили один файл, а также один новый файл добавили, теперь у нас в ветке development два новых коммита, которые мы хотим залить в центральный репозиторий. Ранее мы туда залили две ветки командой git push $HOME/learning-git/central-repo master development, однако эта команда в оригинальном репозитории не создала ссылки на другой репозиторий, можете в этом убедиться сами:

[user@shell]$ cd $HOME/learning-git/first-git-repo
[user@shell]$ git remote
[user@shell]$ 

Если вы продолжите дальше работу и захотите снова отправить изменения в тот репозиторий, вы можете, конечно, снова выполнить команду типа git push $HOME/learning-git/central-repo development и она отправит все накопленные изменения в центральный репозиторий, однако давайте делать всё правильно, через remotes. Добавим ссылку на другой репозиторий, назовём её origin:

[user@shell]$ cd $HOME/learning-git/first-git-repo
[user@shell]$ git remote add origin $HOME/learning-git/central-repo
[user@shell]$ git remote -v
origin  /Users/sigsergv/learning-git/central-repo (fetch)
origin  /Users/sigsergv/learning-git/central-repo (push)

И теперь для отправки данных достаточно набрать просто git push:

[user@shell]$ git push
Counting objects: 9, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 579 bytes | 0 bytes/s, done.
Total 6 (delta 1), reused 0 (delta 0)
To /Users/sigsergv/learning-git/central-repo
   c1f6047..87200f3  development -> development
[user@shell]$ 

После этой команды в центральном репозитории будет лежать обновлённая ветка development.

И снова очень важный момент. Команда git push по умолчанию попытается отправить вообще все изменения из всех веток в другой репозиторий, а это далеко не всегда нужно и часто чревато отправкой того, что вы совсем не хотели пока отправлять. Чтобы отправить только текущую ветку (development), нужно выполнить команду git push origin development.

Общий синтаксис команды push такой:

git push <REMOTE> <WHAT>

Здесь <REMOTE> — это идентификатор другого репозитория (например, origin), а <WHAT> — указание, что именно нужно отправить. Обычно вместо <WHAT> пишут название ветки; если <WHAT> опустить, то, как я уже писал выше, в другой репозиторий уедут все ветки, которые мы связали с этим другим репозиторием.

Подробное описание можете найти в man git-push.

git pull

Команда git pull «вытаскивает» изменения из другого репозитория и «накатывает» их на текущую ветку в рабочем каталоге.

Продолжим играться с нашими репозиториями; напомню, что у нас сейчас на локальной машине два «рабочих» репозитория: first-git-repo и first-repo-clone, а также один «сетевой» — central-repo. Мы добавили в ветку в оригинальном репозитории несколько коммитов, «запушили» их в центральный репозиторий и теперь хотим обновить клонированный репозиторий актуальными коммитами.

Переключимся на репозиторий first-repo-clone и в нём на ветку development:

[user@shell]$ cd $HOME/learning-git/first-repo-clone
[user@shell]$ git checkout development
Branch development set up to track remote branch development from origin.
Switched to a new branch 'development'
[user@shell]$ 

Теперь, если мы выполним git pull, к нам «приедут» все нужные изменения:

[user@shell]$ git pull
remote: Counting objects: 9, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From /Users/sigsergv/learning-git/central-repo
   c1f6047..87200f3  development -> origin/development
Updating c1f6047..87200f3
Fast-forward
 AUTHORS.txt | 1 +
 LICENSE.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 AUTHORS.txt
[user@shell]$

Можете убедиться командой git log, что теперь у нас тот же набор изменений, что и в репозитории first-git-repo.

git pull по сути является сокращением для двух последовательных команд: git fetch и git merge FETCH_HEAD (FETCH_HEAD — это специальный символический референс, ссылающийся на самую последнюю «вытащенную» ветку, валиден только сразу после выполнения git fetch). git fetch вытаскивает объекты с другого репозитория, а git merge сливает их с объектами локального репозитория и локальной ветки.

Таким образом, жизненный цикл работы с репозиторием можно представить последовательностью команд: pull, add, commit, push. Но это — очень простой цикл, в жизни (а особенно в команде) всё сложнее; если репозиториями пользуется несколько человек, неизбежно возникают конфликты, которые нужно аккуратно разруливать, чтобы не потерять проделанную работу.

Code template:

[user@shell]$ 

Ссылки

Терминология и общепринятые обозначения


  1. object

    Объект — это самая главная сущность git. По сути git-репозиторий является key-value хранилищем, где value/значение как раз являются объекты, а key/ключом — SHA1-хеши. Объекты — это, например, все файлы в репозитории, коммиты и всё такое. Каждый объект идентифицируется по SHA1-хешу, например, 9da581d910c9c4ac93557ca4859e767f5caf5169.

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

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

  2. reference или ref

    Строка (далее я буду называть ЭТО референс), указывающая на какой-нибудь объект из репозитория, нечто вроде указателя. Референсом является идентификатор коммита, например.

    Бывают также символические референсы, по сути это осмысленное слово, связанное с конкретным SHA1-хешем (и, следовательно, конкретным объектом), часто слово «символические» опускают и пишут просто «референс». Значения символических референсов хранятся в каталоге .git/refs и его подкаталогах (а собственно куда указывает символический референс, зависит от контекста); например, в каталоге .git/refs/heads лежат референсы, указывающие на локальные ветки; а в каталоге .git/refs/tags — референсы, указывающие на теги. 

  3. staging area / index

    Накопительный буфер — это промежуточное состояние рабочего каталога, как бы подготовительная стадия перед коммитом. Подготовленные файлы запоминаются в буфере командой git add, который затем командой git commit записывается в репозиторий в виде коммита. Вместо git add можно использовать git stage, это синонимы. 

  4. remote

    Так называется другой репозиторий, точнее, имя такого другого репозитория. Слово «другой» здесь означает «какой-то иной репозиторий, не текущий». Чаще всего это внешний репозиторий. 

  5. remote-tracking branch

    Так называется копия ветки из другого репозитория в локальном. Эту ветку нельзя менять коммитами в локальном репозитории, она обновляется только командами типа git pull или git fetch

  6. origin

    Это общепринятое название стороннего репозитория, из которого был склонирован текущий. Это самое обычное имя и не является какой-то особой сущностью git. При клонировании git создаёт в новом локальном репозитории новый remote с адресом стороннего репозитория и присваивает ему имя origin

Комментарии

Гость: Искандер | 2017-04-20 в 10:43

В версии git для Windows давно уже есть git-bash, в нём всё прекрасно работает. Даже команды линукс есть и vim. Есть проблема с русскими именами файлов, но это чинится одной строчкой в конфиге. Родной виндовый терминал в git действительно лучше обойти стороной.

Sergey Stolyarov | 2017-04-20 в 11:13

Это https://git-for-windows.github.io или прямо в официальной сборке уже есть?

Гость: Искандер | 2017-04-20 в 18:28

Я имею в виду клиент здесь https://git-scm.com/downloads

Гость: Алексей | 2015-03-05 в 14:24

Опечатка:

Сноски 3. ... Подготовленные файлы запоминаются в буфере командой git add, который затем командой svn commit ...

Sergey Stolyarov | 2015-03-05 в 15:31

Ага, спасибо, исправил.

Текст комментария (допустимая разметка: *курсив*, **полужирная**, [ссылка](http://example.com) или <http://example.com>) Посетители-анонимы, обратите внимение, что более чем одна гиперссылка в тексте (включая оную из поля «веб-сайт») приведёт к блокировке комментария для модерации. Зайдите на сайта с использованием аккаунта на twitter, например, чтобы посылать комментарии без этого ограничения.
Имя (обязательно, 50 символов или меньше)
Опциональный email, на который получать ответы (не будет опубликован)
Веб-сайт
© 2006—2023 Sergey Stolyarov | Работает на pyrengine