Распределённая система контроля версий Git

Список литературы

Краткое введение

Что делать?

Ничего не понимаю, какие-то remotes, heads, refs, что все это значит??? В svn всего этого не было!

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

Подготовка к работе

Отключите фильтрацию трафика в антивирусе. Даже если он в винде, а у вас виртуальный Linux. Причина: http-трафик, генерируемый Git, он считает вредительским.

Укажите своё имя и адрес почты.

$ git config --global user.name "Your Name"
$ git config --global user.email you@yourdomain.example.com

Укажите свои пароли для доступа к git в файле ~/.netrc (рекомендованный способ):

machine dev.iu7.bmstu.ru
login your_login
password your_password

Начало работы

  • Создание копии удалённого репозитория. В git это называется клонированием.
    $ git clone http://dev.iu7.bmstu.ru/git/your_project_name
    $cd your_project_name
    
  • Альтернатива: cоздание пустого локального репозитория (в т.ч. для сугубо локальной работы).
$ git init

Основные операции

  • Вносим изменения в текущие файлы/создаём новые файлы/папки и т.д.
  • Используя команду $ git add имя-файла, добавляем изменённые/созданные файлы в список для коммита. Примеры:
    $ git add *.c # При нормальном проекте включает все Си-файлы.
    $ git add . # Включает всё.
    $ git src/*.h
    
  • Используя команду $ git commit (обычно с параметрами -a и -m), заносим изменения в текущую ветку в локальный репозиторий. Пример использования:
    $ git commit -am "Компилируется, запускается, но сразу падает."
    
  • Используя команду $ git push, отправляем данные изменения на удалённый сервер. При первом использовании вам обычно нужно указать удалённый репозиторий и ветку (главная ветка --- master):
    $ git push origin master # Пушим туда, откуда клонировали, см. .git/config
    $ git push http://dev.iu7.bmstu.ru/git/your_project_name master # Указываем удалёную ветку.
    $ git push # При указании [чего-то?] хватает и формы без параметров.
    
  • Синхронизация с удалённым (или другим локальным) репозиторием : git pull.
    $ git pull origin # Синхронизируем с тем, откуда клонировали.
    

Можно так же указать репозиторий и ветку. Если на git pull без параметров он ругается, то (видимо) нужно добавить в .git/config следующее

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

Откат изменений

  • Возврат к последнему коммиту (всё незакоммиченное -- убивается):
    $ git reset --hard HEAD
    

  • Возврат к предпоследнему коммиту:
    $ git reset --hard HEAD^
    

Работа с ветками (branches)

  • Команда git branch: удаление, отслеживание веток.
  • Команда git checkout: переход в нужную ветку и создание веток.
  • Типичное создания удалённой ветки и работа с ней локально с push-ем всех изменений.
    • git branch # просмотр локальных бранчей, просмотр удаленных: git branch -r
    • git checkout -b new_branch # создание ветки с именем new_branch
    • git push origin new_branch # пропихивание локального бранча в удаленный
  • git checkout -new_branch # переход в new_branch
  • git merge --- сливание бранчей (надо дописать);
  • Чтобы создать локальный бранч, соответствующий удаленному (и обновляющийся при git pull), следует выполнить команду:
    $ git fetch # обновляем информацию о бранчах
    $ git checkout -t origin/branch_name
    
    • git branch -t new_branch origin/new_branch # создание бранча и связывание с удалённым
  • при клонировани можно указать конкретный бранч

Именование коммитов

А что, ревизии тут 20-значными номерами обозначаются?

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

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

Хорошо, но как все-таки сослаться на ревизиюкоммит??

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

Важные коммиты (как и в svn) можно тэгить (командой git-tag) и далее вместо хэша указывать имя тэга.

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

Родитель коммита X обозначается X^. Например, последний сделанный коммит можно обозначть HEAD^ (или HEAD~1), предпоследний -- HEAD^^ (или HEAD~2) и т.д.

Чтобы составить более наглядное имя коммита, чем его хэш, можно использовать git-describe.

$ git describe
v1.7.0-rc0-15-gd539de9

Приведенный выше пример означает, что текущий коммит -- это 15-й по счету коммит после тэга v1.7.0-rc0, с хэшом d539de9...

Зачастую, именовать коммиты не нужно вообще. Например, если вам нужно сформировать диффы по последним 5 коммитам, это можно сделать одной командой git format-patch -n5.

Также, настоятельно рекомендуется прочитать  man git-rev-parse, где содержится полное и детальное описание различных способов указания коммита.

Способы пуллинга

Внимание: этот раздел будет малопонятен тем, кто считает, что git -- это такой продвинутый svn, но с той же идеологией.

Когда вы делаете git-pull, в реальности сначала делается git-fetch, который получает изменения, а дальше эти изменения применяются к репозиторию. Тут есть 2 варианта:

  • У вас отсутствуют новые коммиты с момента последнего пулла (т.е., говоря языком авторов git, в графе коммитов верхушка вашего бранча является предком верхушки удаленного бранча -- ответвления не произошло). В этом случае ничего сложного не происходит -- в ваш репозиторий добавляются новые коммиты, верхушка (tip) вашего текущего бранча продвигается до последнего коммита. Это называется fast-forward.
  • У вас имеются локальные коммиты, сделанные после предыдущего пулла. В этом случае напрямую добавить новые коммиты в вашу историю невозможно -- у вас будет 2 верхушки, текущая и взятая из удаленного репозитория, не связанные друг с другом. В этом случае есть опять-таки две возможности: можно их слить, можно "переписать историю" ваших коммитов. Проще показать то и другое на примерах.

Пусть Алиса и Боб имеют 2 репозитория. Боб пуллит себе репозиторий Алисы (последний коммит в котором -- Х). После этого Алиса делает у себя коммит Y (потомок Х), а Боб делает у себя коммит Z (тоже потомок X). После этого Боб решает снова пуллить репозиторий Алисы. Теперь ему надо как-то добавить коммит Y в свою историю. Но вот беда -- если он просто добавит его в конец лога (и сделает верхушкой), то он теряет коммит Z (потому что Y является потомком X). Встроить его "между" X и Z Боб тоже не может -- потому что Z является потомком Х, а перед ним будет Y. Два выхода:

  • слить (merge). Это то, что делает git-pull в такой ситуации по умолчанию. Создается новый коммит U, который будет являться потомком Y и Z и который будет содержать объединение усилий Боба и Алисы. После чего верхушкой становится этот коммит U. Преимущество: абсолютно безопасно (см. далее); недостаток: возникают "левые" коммиты, тысячи их, которые засоряют timeline. Неудобно при использовании трекера с публичным репозиторием.
  • "переписать" коммит Z (rebase). Выполняется, если сделать git-pull --rebase. В этом случае коммит Z удаляется из репозитория Боба (но его дифф сохраняется), далее в репозиторий добавляется коммит Y (это обычный fast-forward). Затем дифф коммита Z применяется заново (но уже поверх Y, а не поверх X, к которому он был применен изначально), и заново коммитится с тем сообщением и временем. Т.е. теперь репозиторий Боба выглядит так, как будто в нем всегда был коммит Y Алисы (привет, 1984!). Преимущество: нет "левых" коммитов; недостаток: нельзя делать не публичных репозиториях; если из репозитория Боба кто-то далее пуллит, то после "переписывания" истории у него возникнут проблемы (читай: все поломается). Поэтому использовать этот вариант можно только в приватном репозитории, который никто не пуллит.

Если хотите всегда использовать второй вариант, можно добавить алиас (алиасить существующие команды, похоже, нельзя) (или это только в последней версии git сломали?)

pullb = pull --rebase

Рефлоги

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

Например, у вас есть бранч master, указывающий на коммит X. Вы делаете коммит Y. master теперь указывает на Y, а в рефлог записывается, что раньше master указывал на X. Затем вы делаете коммит Z, master указывает на него, а в его рефлоге -- коммиты X и Y.

Обратиться к рефлогу можно так: бранч@{номер}. Например, master@{1} (или, если master текущий, просто @{1}) будет означать "коммит, на который master ссылался на один коммит раньше", т.е. Y, а master@{2} -- X. Можно также использовать даты, например, @{yesterday} -- покажет, какой коммит был на верхушке master сутки назад.

Есть еще специальный рефлог HEAD -- оно отслеживает коммит в вашей рабочей копии. Например, если у вас в рабочей копии был коммит X, после чего вы сделали checkout Y или переключились на другой бранч, HEAD@{1} будет указывать на X, и вы можете быстро вернуться к нему командой

git checkout HEAD@{1}

ВАЖНО: рефлоги хранятся исключительно локально. Т.е., в каждом репозитории они свои и не копируются командами push и pull.

Помимо HEAD, есть еще ряд специальных ссылок на коммиты, которые существуют после определенных операций:

  • MERGE_HEAD -- указывает на верхушку бранча, с которым только что сделано слияние
  • FETCH_HEAD -- указывает на верхушку новых коммитов, полученных командой git fetch
  • ORIG_HEAD -- указывает на верхушку текущего бранча до операции слияния/пулла (т.е. сразу после git pull|merge совпадает с HEAD@{1}).

Если мы хотим быстро узнать, что интересного мы только что запуллили из репозитария (или что собираемся запушить), можно заюзать такие алиасы (wn = whats new, wp = whats to push):

wn = log ORIG_HEAD..HEAD
wp = log origin..master

Переименования файлов

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

Однако, по умолчанию такие переименования не показываются в выводе status и log (хотя показываются при коммите). Чтобы включить их показ, необходимо добавить в конфиг опцию diff.renames со значением true либо copy (чтобы отслеживал и копирование)

$ git config --global diff.renames copy

Псевдонимы команд (aliases)

Псевдонимы команд задаются в конфиге git (~/.gitconfig) в секции alias. Вот мои:

[alias]
    ci  = commit
    st  = status
    co  = checkout
    br  = branch
    # Long log of last 5 commits
    ll  = log -n5
    # Short log of last 10
    lll = log -n10 --pretty=oneline
    # See at which commit I am
    tip = log -n1
    # Last change
    lc  = log -n1 --stat
    # What new
    wn  = log ORIG_HEAD..HEAD
    # Whats to push
    wp  = log origin..master
    # Pull with rebase
    pb  = pull -v --rebase

Цветной вывод

Для любителей цветного вывода в командах:

git config --global ui.color auto

Прочее

Непонятное сообщение при первом push

warning: You did not specify any refspecs to push, and the current remote
warning: has not configured any push refspecs. The default action in this
warning: case is to push all matching refspecs, that is, all branches
warning: that exist both locally and remotely will be updated. This may
warning: not necessarily be what you want to happen.

Ну да. Если вы не поняли, что оно хочет, то скажите следующее.

$ git config --global push.default matching

Git и Windows

Расшифровка названия

"git" can mean anything, depending on your mood.

  • random three-letter combination that is pronounceable, and not actually used by any common UNIX command. The fact that it is a mispronunciation of "get" may or may not be relevant.
  • stupid. contemptible and despicable. simple. Take your pick from the dictionary of slang.
  • "global information tracker": you're in a good mood, and it actually works for you. Angels sing, and a light suddenly fills the room.
  • "goddamn idiotic truckload of sh*t": when it breaks