В предыдущей заметке я создавал некий пустой проект и добавлял его в систему управления версиями Git. Здесь я приведу небольшую «cookbook» по работе с Git. Подробная информация про Git содержится в книге Scott Chacon and Ben Straub — Pro Git (Second Edition) — 2014.
Справка по Git
Аргументами утилиты git являются всякие команды (например git config, git diff и т. д.). Незаменимая команда для получения справки по командам утилиты git — это команда help. Например, чтобы получить справку по команде git diff, напечатайте следующее:
Конфигурирование Git
Минимально необходимое конфигурирование Git заключается в указании имени и адреса электронной почты пользователя. Эти данные будут содержаться в каждом коммите, чтобы было понятно кто сделал этот коммит. Можно установить имя пользователя и почту глобально для всех репозиториев (тогда вы используете ключ --global
):
git config --global user.email "you@example.com"
… или только для текущего репозитория (тогда вы не используете ключ --global
):
git config user.email "you@example.com"
Клонирование удаленного репозитория
У всякого удаленного репозитория есть URL. Например у моего репозитория osdevlearning URL следующий: https://dvsav@bitbucket.org/dvsav/osdevlearning.git. Клонирование репозитория — это копирование его с удаленного сервера на локальный компьютер. Часто проект начинается именно с клонирования.
По-умолчанию при клонировании репозитория git присваивает ему обозначение origin. Далее в этой заметке удаленный репозиторий будет обозначаться именно так.
Создание локального репозитория
Как было сказано выше, чтобы создать локальный репозиторий, можно клонировать удаленный. Другой вариант — создать новый локальный репозиторий командой git init. Но сначала надо перейти в папку с вашим проектом. Команда git init создаст в этой папке папку .git, где будет храниться база данных для созданного репозитория. Git автоматически создаст ветку под названием master (о ветках см. ниже).
git init
Добавление ссылки на удаленный репозиторий
Вы можете работать сразу с несколькими удаленными репозиториями. Для добавления ссылки на удаленный репозиторий надо выполнить команду:
ИмяРепозитория — это просто его локальное обозначение. Пример:
Очистка удаленного репозитория
Предположим, я хочу начать с чистого листа и очистить удаленный репозиторий osdevlearning, т. е. удалить всю историю коммитов. Можно конечно просто зайти на сайт BitBucket и удалить репозиторий osdevlearning, а затем создать новый репозиторий с таким же именем. Но есть и другой способ. Удаляю папку osdevlearning на локальном ПК и создаю ее заново:
rm -rfv osdevlearning
mkdir osdevlearning
Создаю локальный репозиторий в папке osdevlearning:
git init
Создаю первый коммит:
Ключ --allow-empty
нужен, чтобы разрешить коммиту быть пустым.
Добавляю ссылку на удаленный репозиторий:
И наконец, отправляю коммит на удаленный репозиторий. При этом в удаленном репозитории удаляются все старые коммиты:
Всё, теперь у меня есть локальный и удаленный репозитории, оба совершенно чистые с одним пустым коммитом.
Stage-Commit-Push
При работе над проектом вы производите в файлах проекта некие изменения. Какие именно изменения вы произвели с момента последнего коммита, вы можете посмотреть при помощи команды status:
Затем вы индексируете изменения некоторых файлов. Индексирование — это указание, какие из сделанных изменений войдут в коммит. Коммит — это фиксация изменений, т. е. сохранение снимка состояния проекта с учетом изменений в базе данных. Индексирование делается при помощи команды add:
Вы можете проиндексировать все изменения при помощи ключа -A:
Затем вы можете передумать и удалить изменения некоторых файлов из области индексации:
А если вы хотите отменить сами изменения, то используйте следующую команду:
Теперь вы фиксируете проиндексированные изменения, снабжая их кратким описанием. Снимок проекта с учетом этих изменений сохраняется в локальную базу данных (которая находится в папке .git):
И наконец, вы отправляете коммит в удаленный репозиторий:
Здесь origin — это обозначение для удаленного репозитория, оно как бы заменяет длинный URL этого репозитория. master — в данном случае название текущей ветки. Ветка master создается автоматически при создании репозитория.
При выполнении команды push
, в зависимости от настроенного вами метода аутентификации, вас могут спросить имя пользователя и пароль — это будут имя пользователя и пароль от аккаунта на хостинге репозиториев (например, GitHub или Bitbucket).
Просмотр состояния локального репозитория
Когда мы работаем над очередными изменениями проекта, перед их индексированием или фиксацией мы хотим получать информацию о том, какие изменения проиндексированы, а какие — нет. Эту информацию нам дает команда
Мы также хотим видеть историю изменений в целом со всеми ветвлениями. Следующая команда красиво отображает эту информацию на экране:
Просмотр удаленных репозиториев
Следующая команда отображает список удаленных репозиториев:
Можно посмотреть информацию о конкретном репозитории (о его ветках):
Просмотр изменений в файлах проекта
Следующая команда показывает, что вы изменили в файлах проекта, но пока еще не проиндексировали:
А вот эта команда показывает, что вы изменили и проиндексировали:
Ветвления
Ветки помогают синхронизировать работу членов команды (один член команды — одна ветка). Ветки помогают синхронизировать работу над отдельными задачами (одна задача — одна ветка). Ветки помогают разграничить стабильные версии проекта (обычно — ветка master) и версии, находящиеся в разработке (обычно — ветка develop).
В качестве примера достаточно рассмотреть тематическую ветку (topic branch). Предположим, я нахожусь в ветке master и мне необходимо решить некую проблему, назовем ее the_issue. Я создаю одноименную ветку и перехожу на эту ветку:
git checkout the_issue
Сделать все то же самое за одну команду можно так:
Далее я выполняю ряд изменений в проекте и делаю ряд коммитов (см. раздел Stage-Commit-Push). Когда я окончательно решил проблему the_issue, я хочу чтобы все изменения, которые я произвел, попали в ветку master. Я выполняю слияние ветки the_issue в ветку master:
git merge the_issue
Если слияние прошло успешно, то ветка the_issue мне больше не нужна, и ее можно удалить:
Если же слияние прошло не успешно, значит между какими-либо изменениями файлов в сливаемых ветках имеется конфликт, который я должен разрешить путем редактирования этих файлов. После разрешения конфликта я индексирую изменения в этих файлах при помощи команды
Затем я выполняю коммит:
После чего уже удаляю ветку the_issue:
Приведенная выше команда удаляет ветку в локальном репозитории. Удалить ветку в удаленном репозитории можно так:
Памятка
Ветка — это ссылка на коммит.
Коммит имеет обычно один родительский коммит (тот, который был перед ним).
Коммит, который образовался в результате слияния двух веток имеет два родительских коммита.
Второй способ скопировать в ветку master изменения, которые сделаны в ветке the_issue — это так называемое перемещение (rebase). Этот способ может сделать более наглядной историю изменений, но его рекомендуется использовать только в том случае, если в ветке the_issue работает только один программист. В таком случае хороший тон — это сначала сделать rebase, а затем merge:
git rebase master
# resolve any conflicts
git rebase --continue
# if you cannot resolve conflicts then do:
# git rebase --abort
git checkout master
git merge the_issue
Что здесь происходит:
- мы переходим на ветку the_issue (если мы еще не там)
- сверху к последнему коммиту master пристраиваются все коммиты из the_issue (чтобы понять, посмотрите картинки в книжке Pro Git)
- мы переходим на ветку master
- сливаем the_issue в master
Затем ветку the_issue тоже можно удалять.
Патчи (patches)
Патч — это простое средство для обмена изменениями кода в текстовом виде. Патч представляет собой текстовый файл, в котором указывается какие строки и в каком файле исходного кода были изменены. Создать патч можно при помощи команды git diff:
Здесь tag1 и tag2 — это теги, которые идентифицируют два коммита, между которыми diff вычисляет разницу, а разница перенаправляется в файл mypatch.patch. Затем вы пересылаете патч, скажем, своему коллеге, и он может применить его к своей рабочей копии репозитория, запустив утилиту patch:
Здесь предполагается, что текущей папкой в командной строке является корень репозитория (поэтому ключ -p1). Второй аргумент команды patch — это путь к файлу патча.
Предварительно можно безопасно проверить, не вызовет ли патч конфликтов при помощи ключа —dry-run:
В git также есть всякие автоматизированные команды для формирования и применения патчей: git format-patch, git am, git apply.
Reset current branch to some specific commit
А если я слил чью-то ветку в свою, еще не отправил изменения в удаленный репозиторий (не сделал push), передумал и захотел отменить слияние? Можно выполнить действие «reset current branch to commit», выбрав тот коммит, который был закоммичен до слияния:
Здесь 5ae8ad8b — это hash коммита. Помимо наиболее подходящей в данном случае опции -hard есть еще опции -mixed и -soft, которые не удаляют сами изменения, которые возникли в результате слияния.
Скачивание изменений из удаленного репозитория в локальный репозиторий
Предположим, один член команды сделал несколько коммитов и отправил их в удаленных репозиторий. У другого члена команды нет этих изменений на его локальном ПК, но он хочет их иметь. Он скачивает изменения с удаленного репозитория при помощи команды
Напоминаю, что origin в данном случае — это обозначение для удаленного репозитория.
Можно скачать изменения со всех имеющихся удаленных репозиториев:
Команда fetch однако не выполняет слияния изменений, скачанных из удаленного репозитория с вашей текущей локальной веткой — вы должны выполнить это слияние вручную (предварительно можете посмотреть изменения командой git status):
Здесь origin/master — обозначение для ветки master в удаленном репозитории.
Можно выполнить fetch и merge в один прием:
Игнорирование файлов .gitignore
Не все файлы одинаково полезны. Например, некоторые файлы генерируются автоматически средами разработки. Я в основном пишу программы в Visual Studio, которая генерирует множество служебных файлов с расширениями .pdb, .ilk, .ipch, .obj, .tlog и другими. В Git предусмотрено средство, которое заставляет Git игнорировать определенные файлы (т. е. не добавлять автоматически в репозиторий и не отображать в списке неотслеживаемых) — это текстовый файл .gitignore, который вы создаете в папке вашего проекта (например в моем проекте osdevlearning я создаю файл .gitignore в папке osdevlearning). В файле .gitignore вы указываете т. н. паттерны — выражения которые определяют группы файлов (или папок), имена которых соответствуют некоторым условиям. Например паттерн *.obj означает все файлы с расширением .obj. Ниже приведен фрагмент файла .gitignore для одного из моих проектов в Visual Studio:
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Xx]64/
[Xx]86/
[Bb]uild/
bld/
[Bb]in/
[Oo]bj/
Удаление файлов
Следующая команда удаляет файл из рабочей папки проекта:
А вот эта команда не удаляет файл из рабочей папки, но удаляет его из списка отслеживаемых:
Последняя команда пригодится вам на случай, если забыли добавить какой-либо файл в .gitignore и случайно проиндексировали его.
Кстати, чтобы просмотреть имена всех отслеживаемых файлов, нужно выполнить следующую команду:
Здесь master — имя ветки, которую вы просматриваете.
Спрятывание изменений (stashing)
Со мной часто случается следующая история. Я работаю в своей ветке, и у меня есть незакоммиченные изменения. Какой-нибудь мой коллега, который работает в своей ветке, коммитит и отправляет в удаленный репозиторий некие изменения, которые мне очень хочется слить в мою ветку прямо сейчас. Но я не могу слить его ветку в свою, поскольку у меня, как я говорил, есть незакоммиченные изменения (а коммитить недоделанную работу я не хочу). Что делать? Оказывается мои незакоммиченные изменения можно «спрятать», т. е. сохранить в базе данных git, удалив их при этом из рабочей копии. После чего я могу слить ветку моего коллеги в свою, а затем восстановить мои спрятанные изменения.
Итак, чтобы спрятать все незакоммиченные изменения в текущей ветке, я выполняю команду:
То, что я спрятал, сохраняется в некоем стеке. Содержимое этого стека можно посмотреть так:
Каждый элемент стека имеет имя stash@{i}
(здесь и далее i — это номер элемента в стеке).
Далее я что-то делаю, например, сливаю ветку своего коллеги в свою.
Затем я могу восстановить мои спрятанные изменения:
Вышеприведенная команда применяет изменения к рабочей копии, но не удаляет их их базы данных git. Чтобы удалить изменения из стека, я выполняю команду:
Теги
Коммиты идентифицируются своими 40-разрядными шестнадцатеричными контрольными суммами. Более человекопонятный способ присвоить коммиту идентификатор — это теги (tags). Присвоив коммиту тег, можно затем ссылаться на этот коммит не по контрольной сумме, а по тегу. Присвоить коммиту тег можно так:
Здесь mytag — это имя тега, а 3c10f56 — первые несколько цифр контрольной суммы соответствующего коммита.
Теперь можно ссылаться на соответствующий коммит по его тегу, например, перейти на этот коммит так:
Поиск бага методом дихотомии (bisect)
Если вы имеете цепочку коммитов, и знаете, что в начале цепочки бага нет, а в конце он есть, и если вы хотите найти, какой именно коммит привел к появлению бага, то вы можете это сделать методом дихотомии. Процесс ровно такой же, как процесс поиска корня уравнения с одним неизвестным методом дихотомии (деление отрезка пополам). А git поможет вам несколько автоматизировать процесс при помощи команды git bissect. Ниже я лишь перечислю команды которые вам понадобятся в процессе, остальное — читайте в git help bissect.
git bisect start
git bisect bad # the starting commit has a bug
git bisect good mytag # the commit with tag mytag doesn't have a bug
git bisect bad # the commit in between the staring commit and mytag commit has a bug
git bisect good
git bisect good
git bisect bad
git bisect reset # get back to the starting commit
# if you have written an automated test to check for a bug presence
git bisect run ./testbugshellscript.sh