10.7 Git изнутри — Обслуживание репозитория и восстановление данных
Изредка вам потребуется делать «уборку» — сделать репозиторий более компактным, очистить импортированный репозиторий от лишних файлов или восстановить потерянные данные. Данный раздел охватывает некоторые из этих сценариев.
Обслуживание репозитория
Время от времени Git выполняет автоматическую сборку мусора. Чаще всего эта команда ничего не делает. Однако, если у вас накопилось слишком много «рыхлых» объектов (не в pack-файлах), или, наоборот, отдельных pack-файлов, Git запускает полноценный сборщик — git gc (здесь «gc» это сокращение от «garbage collect», что означает «сборка мусора»). Эта команда выполняет несколько действий: собирает все «рыхлые» объекты и упаковывает их в pack-файлы; объединяет несколько упакованных файлов в один большой; удаляет недостижимые объекты, хранящиеся дольше нескольких месяцев.
Сборку мусора можно запустить вручную следующим образом:
$ git gc --auto
Опять же, как правило, эта команда ничего не делает. Нужно иметь примерно 7000 несжатых объектов или более 50 pack-файлов, чтобы запустился настоящий gc . Эти значения можно изменить с помощью параметров gc.auto и gc.autopacklimit соответственно.
Ещё одно действие, выполняемое gc — упаковка ссылок в единый файл. Предположим, репозиторий содержит следующие ветки и теги:
$ find .git/refs -type f .git/refs/heads/experiment .git/refs/heads/master .git/refs/tags/v1.0 .git/refs/tags/v1.1
Если выполнить git gc , эти файлы будут удалены из каталога refs . Git перенесёт их в файл .git/packed-refs в угоду эффективности:
$ cat .git/packed-refs # pack-refs with: peeled fully-peeled cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0 9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1 ^1a410efbd13591db07496601ebc7a059dd55cfe9
При обновлении ссылки Git не будет редактировать этот файл, а добавит новый файл в refs/heads . Для получения хеша, соответствующего нужной ссылке, Git сначала проверит наличие файла ссылки в каталоге refs , а к файлу packed-refs обратится только в случае отсутствия оного. Так что, если вы не можете найти ссылку в каталоге refs , скорее всего она упакована в файле packed-refs .
Обратите внимание, последняя строка файла начинается с ^ . Это означает, что предыдущая строка является аннотированным тегом, а текущая строка — это коммит, на который указывает аннотированный тег.
Восстановление данных
В какой-то момент при работе с Git вы можете нечаянно потерять коммит. Как правило, такое случается, когда вы удаляете ветку, в которой находились некоторые наработки, а потом оказывается, что они всё-таки были нужными; либо вы выполнили git reset —hard , тем самым отказавшись от коммитов, которые затем понадобились. Как же в таком случае вернуть свои коммиты обратно?
Ниже приведён пример, в котором мы сбрасываем ветку master с потерей данных до более раннего состояния, а затем восстанавливаем потерянные коммиты. Для начала, давайте посмотрим, как сейчас выглядит история изменений:
$ git log --pretty=oneline ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit 484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb 1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit cac0cab538b970a37ea1e769cbbde608743bc96d Second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
Теперь сбросим ветку master на третий коммит:
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9 HEAD is now at 1a410ef Third commit $ git log --pretty=oneline 1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit cac0cab538b970a37ea1e769cbbde608743bc96d Second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
Итак, теперь два последних коммита по-настоящему потеряны — они не достижимы ни из одной ветки. Необходимо найти SHA-1 хеш последнего коммита и создать ветку, указывающую на него. Сложность в том, чтобы узнать этот самый SHA-1, ведь вряд ли вы его запомнили, да?
Зачастую самый быстрый способ — использование команды git reflog . Дело в том, что во время вашей работы Git записывает все изменения HEAD. Каждый раз при переключении веток и коммитов изменений, добавляется запись в reflog. reflog также обновляется командой git update-ref — это, кстати, хорошая причина использовать именно эту команду, а не вручную записывать SHA-1 в ref-файлы, как было показано в Ссылки в Git. Вы можете посмотреть где находился указатель HEAD в любой момент времени, запустив git reflog :
$ git reflog 1a410ef HEAD@: reset: moving to 1a410ef ab1afef HEAD@: commit: Modify repo.rb a bit 484a592 HEAD@: commit: Create repo.rb
Здесь мы видим два коммита, на которые когда-то указывал HEAD, однако информации не так уж и много. Для получения информации в более удобном виде, можно воспользоваться командой git log -g , которая выведет лог записей из reflog в привычном формате:
$ git log -g commit 1a410efbd13591db07496601ebc7a059dd55cfe9 Reflog: HEAD@ (Scott Chacon ) Reflog message: updating HEAD Author: Scott Chacon Date: Fri May 22 18:22:37 2009 -0700 Third commit commit ab1afef80fac8e34258ff41fc1b867c702daa24b Reflog: HEAD@ (Scott Chacon ) Reflog message: updating HEAD Author: Scott Chacon Date: Fri May 22 18:15:24 2009 -0700 Modify repo.rb a bit
Похоже, что последний коммит — это и есть тот, который мы потеряли; его можно восстановить, создав новую ветку, указывающую на него. Например, создадим новую ветку с именем recover-branch , указывающую на этот коммит ( ab1afef ):
$ git branch recover-branch ab1afef $ git log --pretty=oneline recover-branch ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit 484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb 1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit cac0cab538b970a37ea1e769cbbde608743bc96d Second commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
Отлично — теперь у нас есть ветка recover-branch , указывающая туда, куда ранее указывала ветка master , тем самым делая потерянные коммиты вновь доступными. Теперь предположим, что потерянные изменения отсутствуют в reflog — для симуляции такого состояния удалим восстановленную ветку и очистим reflog.
$ git branch -D recover-branch $ rm -Rf .git/logs/
В этом случае два первых коммита недоступны ниоткуда. Так как данные reflog хранятся в каталоге .git/logs/ , которую мы только что удалили, то теперь у нас нет reflog. Как теперь восстановить коммиты? Один из вариантов — использование утилиты git fsck , проверяющую внутреннюю базу данных на целостность. Если выполнить её с ключом —full , будут показаны все объекты, недостижимые из других объектов:
$ git fsck --full Checking object directories: 100% (256/256), done. Checking objects: 100% (18/18), done. dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9 dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
В нашем случае потерянный коммит указан после слов «dangling commit» («висячий коммит»). Его можно восстановить аналогичным образом, создав новую ветку, указывающую на этот SHA-1.
Удаление объектов
Git — замечательный инструмент с кучей классных возможностей, но некоторые из них способны стать источником проблем; например, команда git clone загружает проект вместе со всей историей, включая все версии всех файлов. Это нормально, если в репозитории хранится только исходный код, так как Git хорошо оптимизирован под такой тип данных и может эффективно сжимать их. Однако, если когда-либо в проект был добавлен большой файл, каждый, кто потом захочет клонировать проект, будет вынужден скачивать этот файл, даже если он был удалён в следующем коммите. Он будет в базе всегда, просто потому, что он доступен в истории.
Это может стать большой проблемой при конвертации Subversion или Perforce репозиториев в Git. В этих системах вам не нужно загружать всю историю, поэтому добавление больших файлов не имеет там особых последствий. Если при импорте из другой системы или при каких-либо других обстоятельствах стало ясно, что ваш репозиторий намного больше, чем он должен быть, то как раз сейчас мы расскажем, как можно найти и удалить большие объекты.
Предупреждаем: дальнейшие действия переписывают историю изменений. Каждый коммит, начиная с самого раннего, из которого нужно удалить большой файл, будет переписан. Если сделать это непосредственно после импорта, пока никто ещё не работал с репозиторием, то всё окей, иначе придётся сообщить всем участникам о необходимости перебазирования их правок относительно ваших новых коммитов.
Для примера добавим большой файл в тестовый репозиторий, удалим его в следующем коммите, а потом найдём и удалим его полностью из базы. Сначала добавим большой файл в нашу историю:
$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz $ git add git.tgz $ git commit -m 'Add git tarball' [master 7b30847] Add git tarball 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 git.tgz
Упс, мы нечаянно. Нам лучше избавиться от этого файла:
$ git rm git.tgz rm 'git.tgz' $ git commit -m 'Oops - remove large tarball' [master dadf725] Oops - remove large tarball 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 git.tgz
Теперь запустим сборщик мусора и посмотрим, сколько места сейчас используется:
$ git gc Counting objects: 17, done. Delta compression using up to 8 threads. Compressing objects: 100% (13/13), done. Writing objects: 100% (17/17), done. Total 17 (delta 1), reused 10 (delta 0)
Чтобы быстро узнать, сколько места занято, можно воспользоваться командой count-objects :
$ git count-objects -v count: 7 size: 32 in-pack: 17 packs: 1 size-pack: 4868 prune-packable: 0 garbage: 0 size-garbage: 0
Строка size-pack — это размер pack-файлов в килобайтах, то есть всего занято почти 5 MБ. Перед последним коммитом использовалось около 2 КБ — очевидно, удаление файла не удалило его из истории. Всякий раз, когда кто-либо захочет клонировать этот репозиторий, ему придётся скачивать все 5 МБ для того, чтобы заполучить этот крошечный проект, просто потому, что однажды вы имели неосторожность добавить большой файл. Давайте же исправим это!
Для начала найдём проблемный файл. В данном случае, мы уже знаем, что это за файл. Но если бы не знали, как можно было бы определить, какие файлы занимают много места? При вызове git gc все объекты упаковываются в один pack-файл, но, несмотря на это, определить самые крупные файлы можно, запустив служебную команду git verify-pack и отсортировав её вывод по третьей колонке, в которой записан размер файла. Так как нас интересуют самые крупный файлы, оставим три последние строки с помощью tail :
$ git verify-pack -v .git/objects/pack/pack-29…69.idx \ | sort -k 3 -n \ | tail -3 dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 22044 5792 4977696 82c99a3e86bb1267b236a4b6eff7868d97489af1 blob 4975916 4976258 1438
Большой объект внизу списка, его размер — 5 МБ. Для того чтобы узнать, что это за файл, воспользуемся командой rev-list , которая уже упоминалась в разделе Проверка формата сообщения коммита главы 8. Если передать ей ключ —objects , она выдаст хеши всех коммитов, а также хеши объектов и соответствующие им имена файлов. Воспользуемся этим для определения имени выбранного объекта:
$ git rev-list --objects --all | grep 82c99a3 82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
Теперь необходимо удалить данный файл из всех деревьев в прошлом. Легко получить все коммиты, которые изменяли данный файл:
$ git log --oneline --branches -- git.tgz dadf725 Oops - remove large tarball 7b30847 Add git tarball
Для полного удаления этого файла из истории необходимо переписать все коммиты, начиная с 7b30847 . Воспользуемся командой filter-branch , о которой мы писали в разделе Перезапись истории главы 7:
$ git filter-branch --index-filter \ 'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^.. Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz' Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2) Ref 'refs/heads/master' was rewritten
Опция —index-filter похожа на —tree-filter , использовавшуюся в разделе Перезапись истории главы 7, за исключением того, что вместо передачи команды, модифицирующей файлы на диске, мы используем команду, изменяющую файлы в индексе.
Вместо удаления файла чем-то вроде rm file , мы используем git rm —cached , так как нам надо удалить файл из индекса, а не с диска. Причина, по которой мы делаем именно так — скорость: нет необходимости извлекать каждую ревизию на диск, чтобы применить фильтр, а это может очень сильно ускорить процесс. Если хотите, можете использовать и tree-filter для получения аналогичного результата. Опция —ignore-unmatch команды git rm отключает вывод сообщения об ошибке в случае отсутствия файлов, соответствующих шаблону. Ещё один момент: мы указали команде filter-branch переписывать историю, начиная с коммита 7b30847 , потому что мы знаем, что именно в нём впервые появилась проблема. По умолчанию перезапись начинается с самого первого коммита, что потребовало бы гораздо больше времени.
Теперь история не содержит ссылок на данный файл. Однако, в reflog и в новом наборе ссылок, добавленном Git в .git/refs/original после выполнения filter-branch , ссылки на него всё ещё присутствуют, поэтому необходимо их удалить, а затем переупаковать базу. Необходимо избавиться от всех возможных ссылок на старые коммиты перед переупаковкой:
$ rm -Rf .git/refs/original $ rm -Rf .git/logs/ $ git gc Counting objects: 15, done. Delta compression using up to 8 threads. Compressing objects: 100% (11/11), done. Writing objects: 100% (15/15), done. Total 15 (delta 1), reused 12 (delta 0)
Проверим, сколько места удалось освободить:
$ git count-objects -v count: 11 size: 4904 in-pack: 15 packs: 1 size-pack: 8 prune-packable: 0 garbage: 0 size-garbage: 0
Размер упакованного репозитория сократился до 8 КБ, что намного лучше, чем 5 МБ. Из значения поля size видно, что большой объект всё ещё хранится в одном из ваших «рыхлых» объектов, но, что самое главное, при любой последующей отправке данных наружу (а значит и при последующих клонированиях репозитория) он передаваться не будет. Если очень хочется, можно удалить его навсегда локально, выполнив git prune —expire :
$ git prune --expire now $ git count-objects -v count: 0 size: 0 in-pack: 15 packs: 1 size-pack: 8 prune-packable: 0 garbage: 0 size-garbage: 0
Полезные команды Git: безопасная отмена коммитов, добавление файла из другой ветки и другие
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Git — это мощный, хотя и незамысловато выглядящий, инструмент, который при неосторожном использовании может устроить большой беспорядок. Поэтому, прежде чем пробовать выполнять различные фокусы с Git, я рекомендую ознакомиться с его основными командами (init, clone, push, pull, status, log и т.д.) и концептами (репозиторий, ветвь, коммит, и т.д.).
Итак, если вы уверенно чувствуете себя при работе с Git, вот несколько полезных трюков, о которых я знаю.
Reflog. Отмена операций
Я помню, что в начале знакомства с Git я боялся запускать команды. Я думал, что всё испорчу. Например, первое использование git rebase вызвало у меня несколько восклицаний наподобие «Вот *****! Что я только что сделал?!» из-за дублировавшихся или вообще исчезнувших коммитов. Вскоре после этого я понял, что нужно идти дальше.
Лично я не люблю использовать инструмент, не зная, что именно он делает для достижения результата. Это как вождение машины без руля: всё прекрасно до тех пор, пока не доедешь до поворота. А что потом? Ну, вы меня поняли.
Итак, в нашем деле очень важно понимать концепты. Я много читал про Git, прошёл несколько обучающих курсов, и моё видение мира Git кардинально изменилось. Используя его, я стал чувствовать себя гораздо спокойнее, а что более важно, я познал его истинную мощь. И одной из самых мощных команд является git reflog .
Используя reflog , вы можете вернуться в прошлое, отменяя почти любые действия, сделанные в Git. Даже если это были rebase или reset . Кроме того, её легко использовать. Чтобы доказать это, приведу маленький пример.
$ git reflog 1234571 HEAD@: checkout: moving from one-branch to another-branch 1234570 HEAD@: merge origin/feature-branch: Fast-forward 1234569 HEAD@: commit: The commit message goes here. 1234568 HEAD@: reset: moving to 2234567 1234567 HEAD@: rebase finished: returning to refs/heads/one-branch
Чтобы изменить нужное состояние, нужно запустить checkout , используя абсолютную ссылку:
$ git checkout 1234568
или относительную ссылку:
$ git checkout HEAD@
и вы окажетесь в независимом (detached) состоянии HEAD, из которого можно создать новую ветвь.
Вот и всё. Все операции после вышеуказанного состояния отменены.
Revert. Отмена изменений коммита
Вы, должно быть, сталкивались с ситуацией, когда вам нужно было отменить некоторые изменения. На помощь придёт команда revert :
$ git revert
Что она делает? Она просто отменяет действия прошлых коммитов, создавая новый, содержащий все отменённые изменения. Зачем использовать её вместо других решений? Это — единственный безопасный способ, так как он не изменяет историю коммитов. Он обычно используется в публичных ветвях, где изменение истории нежелательно.
Давайте взглянем на другие команды, способные отменять действия.
Rebase
$ git rebase --interactive
Вы можете выполнить эту команду и просто убрать строки, относящиеся к ненужным коммитам. Это самое очевидное и простое решение, но у него есть недостаток, из-за которого его нельзя использовать в публичных ветвях: он изменяет историю коммитов. Поэтому после rebase’а у вас может возникнуть проблема с push’ем.
Reset
$ git reset --hard
Эта команда удаляет все коммиты выше указанного. Она лучше предыдущей тем, что не добавляет новую информацию в историю, но тем не менее тоже ее меняет.
Checkout
$ git checkout --
Эта команда откатывает выбранный файл к состоянию в более ранней версии указанного коммита. Отменённые изменения будут в рабочей директории, и их можно будет закоммитить как угодно.
Вывод таков, что git revert — самая безопасная операция для отмены действий. Кроме того, нужно заметить, что эти команды не взаимозаменяемы в 100% случаев.
Log. Более приятное форматирование
С git log всё просто. Вы вводите команду и видите историю коммитов в хронологическом порядке, и каждый пункт включает в себя хэш коммита, автора, дату и прикреплённое сообщение.
Давайте взглянем на вид лога по умолчанию:
$ git log commit 01ba45f789abcdef9876543210fedcba01234567 Author: First Last Date: Wed Apr 13 17:00:01 2016 +0300 The last commit message goes here. commit 01cf4a6789abcdef9876543210fedcba01234568 Author: First Last Date: Wed Apr 12 16:24:03 2016 +0300 Another commit message goes here. commit 01bf456789abcdef9876543210fedcba01234568 Author: First Last Date: Wed Apr 11 15:30:33 2016 +0300 Another commit message goes here.
Я думаю, что могло бы быть и лучше.
Я предпочитаю простые вещи. Поэтому я использую две опции:
- —oneline – показывает каждый коммит в одной строке. Кроме того, она показывает лишь префикс ID коммита.
- —decorate – печатает все относительные имена показанных коммитов.
Вот как это выглядит:
$ git log --oneline --decorate 01ba45f (HEAD -> origin/feature-branch, feature-branch) The last commit message goes here. 01cf4a6 Another commit message goes here. 01bf456 (origin/dev, dev) Another commit message goes here.
Опция —oneline сокращает количество занятых строк, а опция —decorate добавляет относительные имена (локальные/удалённые ветви + HEAD). Используя эти две опции, мы получаем более компактный и осмысленный git log . Да, мы теряем некоторые вещи, но они не особо важны. Так гораздо лучше, да?
Тем не менее, иногда нам нужно знать дату и автора коммита. В таких случаях я использую следующее:
$ git log --color --pretty=format:"%C(yellow)%h%C(reset) %s%C(bold red)%d%C(reset) %C(green)%ad%C(reset) %C(blue)[%an]%C(reset)" --relative-date --decorate
Ничего себе! Давайте посмотрим, как это выглядит:
$ git log --color --pretty=format:"%C(yellow)%h%C(reset) %s%C(bold red)%d%C(reset) %C(green)%ad%C(reset) %C(blue)[%an]%C(reset)" --relative-date --decorate 01ba45f The last commit message goes here. (HEAD -> origin/feature-branch, feature-branch) 3 seconds ago [First Last] 01cf4a6 Another commit message goes here. 1 day ago [First Last] 01bf456 Another commit message goes here. (origin/dev, dev) 2 days ago [First Last]
И всё-таки вводить эти команды каждый раз достаточно неудобно, так? Давайте создадим алиасы. Я считаю, что использование алиасов очень сильно повышает продуктивность. Чтобы зарегистрировать их, добавим следующие строки в «~/.bash_profile» (конечно, если вы используете *nix OS):
alias gl='git log --oneline --decorate' alias gh='git log --color --pretty=format:"%C(yellow)%h%C(reset) %s%C(bold red)%d%C(reset) %C(green)%ad%C(reset) %C(blue)[%an]%C(reset)" --relative-date --decorate'
Таким образом, мы получили не только хорошо выглядящую историю коммитов, но и упрощённый способ доступа к ней, используя команды gl и gh .
Еще один вариант украшения вывода лога приведен в этой статье.
Diff. Разница между коммитами
Простая команда, показывающая изменения между объектами в проекте — git diff . Один из основных сценариев её использования — сравнение двух коммитов:
$ git diff ..
Другой — вывод всех изменений, не внесённых в индекс. Для этого нужно выполнить следующее:
$ git diff --cached
Branch. Проверка ветвей на слияние
Я думаю, вы уже знаете, что делает базовая команда git branch (без опций выводит список локальных веток — прим. перев.), поэтому перейдём к делу. Время от времени вам понадобится удалять старые или ненужные ветви из вашего «дерева» git.
$ git branch --merged
вы получите список всех веток, соединённых с текущей.
Можно и наоборот:
$ git branch --no-merged
Так вы получите список всех не соединённых с текущей веток.
Таким образом, вы всегда узнаете, безопасно ли удаление той или иной ветви.
Checkout. Получение файла из другой ветви
Вам наверняка приходилось искать конкретную версию файла, которая была в другой, ещё не присоединённой ветви. Помните команду checkout , приведённую выше? Она выглядит так:
$ git checkout --
Указанный файл будет приведен к версии, указанной в заданном коммите. Раньше мы использовали это для отмены изменений файла. Удобно то, что этот коммит необязательно должен находиться в той же ветви, что и наш файл. Поэтому мы можем задать любому файлу любую версию из любого коммита или ветви.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Надеюсь, вы нашли здесь для себя что-то полезное. Прошу заметить, что все вышеуказанные моменты субъективны, поскольку я подгонял их под свои нужды.

Следите за новыми постами по любимым темам
Подпишитесь на интересующие вас теги, чтобы следить за новыми постами и быть в курсе событий.
Git. Коротко о главном

Привет, Хабр! Меня зовут Егор, я занимаюсь разработкой мобильных приложений на Flutter. Это моя первая работа в сфере IT, и как подобает начинающим, я столкнулся с проблемой изучения систем контроля версий. В данной публикации я хочу поделиться приобретенными знаниями и подробно рассмотреть одну из таких систем, а именно Git. Итак, начнем.
“Whoah, I’ve just read this quick tuto about git and oh my god it is cool. I feel now super comfortable using it, and I’m not afraid at all to break something.” — said no one ever.
Не так страшен чёрт, как его малюют. Хотя, как мне кажется, это не касается Git. Так или иначе многие сталкиваются с необходимостью обучиться грамотной работе с этим инструментом. Несмотря на обилие информации и туториалов, это задача является не самой тривиальной. Исходя из своего опыта, могу сделать вывод: необходимо изучить самые разные ресурсы, прежде чем наступит понимание.
Полагаю, все дело в том, что изучение Git — комплексная тема, и охватить ее в одной статье, чтобы быстренько освоить азы и тут же применить знания на практике, несколько сложно.
Тем не менее, я бы хотел дополнить просторы интернета очередной статьей о Git. Постараюсь изложить все таким образом, как если бы у меня была возможность объяснить все самому себе из прошлого. Как следует из названия, я расскажу о Git очень коротко; а точнее о возможностях, которые он нам предоставляет.
Перед тем, как говорить про какую-либо конкретную систему контроля версий, необходимо понимать, что это такое и какими они бывают.
Система контроля версий — это программное обеспечение, помогающее разработчикам управлять состоянием исходного кода на протяжение всей разработки. Другими словами, это система, которая записывает ваши изменения в файл и позже позволяет откатиться к более ранней версии проекта.
Системы контроля версий можно разделить на две группы:
1. Централизованные системы контроля версий;
2. Распределенные системы контроля версий.
Централизованная система контроля версий — это система, при которой репозиторий проекта хранится на сервере и вносить изменения вы можете непосредственно только в этот репозиторий при помощи специальных клиентских приложений. Среди таких систем можно выделить: ClearCase, TFVC, SVN.
Распределенная система контроля версий — это система, при которой копия репозитория может храниться на машине у каждого разработчика, что значительно снижает риск потерять результат работы над проектом. Примером таких систем могут быть: Git, Mercurial, Bazaar.
Git является распределенной системой контроля версий, разработанной Линусом Торвальдсом для управления разработкой ядра Linux. На данный момент Git завоевал огромную популярность в IT сообществе и, как следствие, его часто можно встретить в стеке технологий различных компаний.
Далее я расскажу про структуру Git репозитория и как его завести. Познакомлю вас с основными, наиболее популярными командами в Git. Также вы узнаете о том, как инспектировать историю своего проекта и как откатить его до определенной точки. И, в заключение, я слегка затрону тему ветвления.
Установка Git
Прежде чем мы продолжим, вам необходимо установить Git.
Ниже я представлю краткую инструкцию к установке, но вы также можете пройти по этой ссылке на официальный источник и разобраться в этом самостоятельно.
# Если вы используете менеджер пакетов HomeBrew, # Вы можете выполнить следующую команду: brew install git # В противном случае вам достаточно ввести: git --version # После чего вам будет предложено установить Git
- Windows. Перейдите по ссылке и скачайте Git соответствующий архитектуре вашего процессора (32 или 64-bit) и установите его.
- Linux. Перейдите по ссылке для более подробной инструкции.
# Установка на Linux зависит от дистрибутива который вы используете # Debian/Ubuntu apt-get install git # Fedora yum install git
Структура директории .git/
Как правило, ваша работа с Git будет начинаться с того, что вам потребуется проинициализировать Git директорию в своем проекте. Это делается с помощью команды:
Ее необходимо ввести в корне вашего проекта. Это создаст в текущем каталоге новый подкаталог .git со следующим содержанием:

В данной директории будет содержаться вся конфигурация Git и история проекта. При желании можно править эти файлы вручную, внося необходимые изменения в историю проекта.
В данном файле содержатся настройки Git репозитория. Например, здесь можно хранить email и имя пользователя.
Данный файл предназначен для GitWeb и содержит в себе информацию о проекте (название проекта и его описание). GitWeb — это веб интерфейс, написанный для просмотра Git репозитория используя веб-браузер. Если вы не пользуетесь GitWeb, то это не столь важно.
В этом каталоге Git предоставляет набор скриптов, которые могут автоматически запускаться во время выполнения git команд. В некоторых случаях это значительно упрощает разработку. Например, вы можете написать скрипт, который будет редактировать сообщение коммита согласно вашим требованиям.
4. info — exclude
Каталог info содержит файл exclude, в котором можно указывать любые файлы, и Git не станет добавлять их в свою историю. Это почти то же самое что и .gitingnore (возможно вы сталкивались с ним. Его можно найти в корневом каталоге вашего проекта), за тем исключением, что exclude не сохраняется в истории проекта, и вы не сможете им поделиться с другими.
Каталог refs хранит в себе копию ссылок на объекты коммитов в локальных и удаленных ветках.
Каталог logs хранит в себе историю проекта для всех веток в вашем проекте.
Каталог objects хранит в себе BLOB объекты, каждый из которых проиндексирован уникальным SHA.
Промежуточная область с метаданными, такими как временные метки, имена файлов, а также SHA файлов, которые уже упакованы Git. В эту область попадают файлы, над которыми вы работали, при выполнение команды git add .
Файл содержит ссылку на текущую ветку, в которой вы работаете
Каждый раз во время слияния в этот файл попадает SHA ветки, с которой проводилось слияние
Файл хранит в себе ссылки в виде SHA на ветки, которые участвовали в git fetch
Файл хранит в себе ссылки в виде SHA на ветки, которые участвовали в git merge
Файл содержит в себе последнее введенное вами сообщение коммита
Самые распространенные команды в Git.

При работе с системами контроля версий разработчики сталкиваются с определенной, повторяющейся последовательностью действий. Оно и понятно, ведь, по сути, если не брать в расчет возможности Git для управления состоянием проекта и прочие плюшки, то как правило ваша работа ограничена рядом действий:
1. Внести изменения в проект;
2. Добавить изменения в индекс(staging area) — git add (таким образом вы сообщаете Git какие именно изменения должны быть занесены в историю.)
2. Закоммитить изменения — git commit (сохранить изменения в историю проекта)
3. Запушить — git push (отправить результаты работы на удаленный сервер, чтобы другие разработчики тоже имели к ним доступ)
Итак, разберемся в этом подробнее. Проинициализировав Git репозиторий, вы начинаете вносить какие-то изменения в проект. Предположим, что вы создали файл `hello_world.txt` и работаете над его редактированием.
Введем git status и увидим следующее:
On branch master No commits yet Untracked files: (use "git add . " to include in what will be committed) hello_world.txt
Команда git status отображает состояние директории и индекса(staging area). Это позволяет определить, какие файлы в проекте отслеживаются Git, а также какие изменения будут включены в следующий коммит.
Так как вы создали новый файл, Git определяет его как неотслеживаемый, и тут же подсказывает, что делать дальше:
use “git add . ” to include in what will be committed
git add hello_world.txt
On branch master No commits yet Changes to be commited: (use "git rm --cached . " to unstage) new file: hello_world.txt
Файл добавлен в индекс. Теперь можно закоммитить внесенные изменения и оставить небольшое описание. Делается это командой:
git commit -m ‘first commit
Готово! Мы сделали наш первый коммит! Далее добавим в наш файл строку “Hello, World!”, и снова проверим git status:
On branch master Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) no changes added to commit (use "git add" and/or "git commit -a")
Теперь Git сообщает, что у нас есть измененный файл hello_world.txt . И теперь нам сново нужно добавить его в индекс и затем закоммитить.
Что ж, мы научились записывать и хранить изменения на своей машине, теперь нам нужно отправить версию нашей истории на удаленный сервер. В данном примере я воспользуюсь репозиторием на GitHub.
Для начала вам нужно создать удаленный репозиторий. Как это реализовать в случае с GitHub подробно описано тут.
Далее необходимо добавить удаленный репозиторий в Git:
Эта команда сопоставит удаленное хранилище с ссылкой на локальный репозиторий. С этого момента можно обращаться к удаленному репозиторию через эту ссылку. Например:
git remote add origin https://github.com/user/hello_world.git
В данном случае “origin” является коротким именем для удаленного репозитория, на которое он будет ссылаться. Вы можете выбрать совершенно любое имя — это не важно. “origin” это просто стандартное соглашение.
Осталось дело за малым — отправить результат нашей работы в репозиторий. Делается это следующим образом:
git push origin
Готово! Теперь история изменений вашего проекта будет храниться в удаленном репозитории.
Работа с историей

Итак, как записывать, сохранять и отправлять изменения в удаленное хранилище мы разобрались.
Настало время поговорить о том, как управлять историей проекта. А именно как просматривать изменения и как откатить проект до определенной точки.
Для инспектирования истории в Git предусмотрен определенный ряд команд, рассмотрим несколько из них:
- git log
- git show
- git reflog
- git reset
- git log
Данная команда предназначена для отображения всей вашей истории. Она может быть весьма удобна, если вам понадобилось узнать, какие изменения вы вносили ранее. Или если вам нужно откатиться до определенного места в истории, либо если есть нужда её отредактировать.
Если ввести git log без каких либо параметров, выглядит это примерно так:
commit 957e1132f57d83§dbd402faf3c858cf5ba8b335f (HEAD -> master) Author: egor Date: Fri Jul 16 13:25:21 1021 +0300 fourth commit commit ekd53dkcld4dkf334r3r3sefio5dk6kfl54dkf53 Author: egor Date: Fri Jul 16 13:22:25 2021 +0300 third commit commit dslf4453lk34jk34k3h5g34u6m5n75j7kj3l345k Date: Fri Jul 16 13:22:27 2021 +0300 second commit commit h4k4o5jk2lhkl234jkl6nkg6j4lh4gjbh6ll45k4 Author: egor Date: Fri Jul 16 13:21:32 2021 +0300 first commit
git log имеет огромное множество дополнительных параметров, которые будут влиять на вывод в консоль. Вам предоставляется выбор на любой вкус.
Хотите просмотреть последние три коммита? Пожалуйста:
commit ekd53dkcld4dkf334r3r3sefio5dk6kfl54dkf53 Author: egor Date: Fri Jul 16 13:22:25 2021 +0300 third commit commit dslf4453lk34jk34k3h5g34u6m5n75j7kj3l345k Date: Fri Jul 16 13:22:27 2021 +0300 second commit commit h4k4o5jk2lhkl234jkl6nkg6j4lh4gjbh6ll45k4 Author: egor Date: Fri Jul 16 13:21:32 2021 +0300 first commit
Есть необходимость вывести все в одну линию? Запросто:
git log —oneline
957e113 (HEAD -> main) fourth commit ekd53dk third commit dslf445 second commit h4k4o5j first commit
Так можно продолжать до бесконечности, поэтому я оставлю ссылочку, перейдя по которой, вы сможете с ними подробней ознакомиться.
Команда git show используется для отображения полной информации о любом объекте в Git, будь то коммит или ветка. По умолчанию git show отображает информацию коммита, на который в данный момент времени указывает HEAD.
Для удобства работы git show оснащен рядом дополнительных параметров, некоторые из них мы рассмотрим ниже.
Итак, если ввести git show , мы получим следующий результат:
commit 957e1132f57d83§dbd402faf3c858cf5ba8b335f (HEAD -> master) Author: egor Date: Fri Jul 16 13:25:21 1021 +0300 fourth commit diff --git a/hello_world.txt b/hello_world.txt index b402110..d49b5d7 10044 --- a/hello_world.txt +++ b/hello_world.txt @@ -1,2 +1,3 @@ Hello world! Bye, bye! +See you soon!
Здесь представлена полная информация о последнем коммите, а также какие именно изменения он в себя включает.
Мы также можем вывести диапазон из указанных коммитов. Диапазон указывается полуоткрытым интервалом, содержащим id коммитов, не включая первый элемент. Выглядит это следующим образом:
git show 349de9d..957e113
commit 957e1132f57d83§dbd402faf3c858cf5ba8b335f (HEAD -> master) Author: egor Date: Fri Jul 16 13:25:21 1021 +0300 fourth commit diff --git a/hello_world.txt b/hello_world.txt index b402110..d49b5d7 10044 --- a/hello_world.txt +++ b/hello_world.txt @@ -1,2 +1,3 @@ Hello world! Bye, bye! +See you soon! commit ekd53dkcld4dkf334r3r3sefio5dk6kfl54dkf53 Author: egor Date: Fri Jul 16 13:22:25 2021 +0300 third commit diff --git a/hello_world.txt b/hello_world.txt index cd08755..b402110 100644 --- a/hello_world.txt +++ b/hello_world.txt @@ -1 +1,2 @@ Hello world! +Bye, bye!
Для более лаконичного вывода, можно воспользоваться командой:
git show —oneline
957e113 (HEAD -> master) fourth commit diff --git a/hello_world.txt b/hello_world.txt index b402110..d49b5d7 10044 --- a/hello_world.txt +++ b/hello_world.txt @@ -1,2 +1,3 @@ Hello world! Bye, bye! +See you soon!
Таким образом, мы сократим id коммита, а также исключим авторство и дату коммита.
Подробнее с командой `git show` и с её параметрами можно ознакомиться перейдя по ссылке.
Эта команда выводит упорядоченный список коммитов, на которые указывал HEAD. Грубо говоря, она отображает историю всех ваших перемещений по проекту.
Основное преимущество этой команды заключается в том, что если вы вдруг случайно удалили часть истории или откатились назад, вы сможете проинспектировать момент утраты нужной вам информации и откатиться обратно.
Это возможно за счет того, что git reflog хранит свою информацию на вашей машине отдельно от коммитов, поэтому при удалении чего-либо в истории, в сможете это найти в git reflog .
Вывод этой команды выглядит следующим образом:
957e113 (HEAD -> master) HEAD@: commit: fourth commit ekd53dk HEAD@: commit: third commt dslf445 HEAD@: commit: second commit h4k4o5j HEAD@: commit (intial): first commit

Теперь давайте рассмотрим очень полезную команду `git reset`. Она позволяет откатить проект до определенной точки.
Эту команду можно использовать с тремя параметрами:
- git reset —soft
- git reset —mixed
- git reset —hard
Рассмотрим их по порядку. Для этого сначала давайте вспомним, что такое index. Как я упоминал ранее, index — это временный файл, который фиксирует структуру Git проекта в определенный момент времени. Это важная деталь сейчас, поскольку команда git reset в зависимости от параметра прокидывает нас в истории проекта с соответствующими состояниями индекса.
1. В случае с —soft , содержимое вашего индекса, а также рабочей директории, остается неизменным. Это значит, что если мы откатимся назад на пару коммитов, мы изменим ссылку указателя HEAD на указанный коммит и все изменения, которые были до этого внесены, окажутся в индексе.
2. При использовании параметра —mixed , мы опять-таки изменим ссылку указателя HEAD, но все предыдущие изменения в индекс не попадут, а будут отслеживаться как не занесенные в индекс. Это дает возможность внести в индекс только те изменения, которые нам необходимы, что довольно удобно!
3. Если использовать команду git reset с параметром —hard , мы снова изменим ссылку указателя HEAD, но все предыдущие изменения не попадут ни в индекс, ни в зону отслеживаемых файлов. Это значит, что мы полностью сотрем все изменения, которые вносили ранее. Это также удобно, если вы знаете, что вам больше не пригодится ваша предыдущая работа над проектом.
Давайте вернемся к нашему репозиторию и рассмотрим следующий пример:
git log —oneline
957e113 (HEAD -> master) fourth commit ekd53dk third commit dslf445 second commit h4k4o5j first commit
Итак, в ходе нашей работы, мы сделали четыре коммита. Предположим, что мы хотим откатить проект до второго коммита. Давайте посмотрим, как это будет происходить, используя разные параметры:
git reset —soft 349de9d
Далее, проверим индекс:
On branch master Changes to be committed: (use "git restore --staged . " to unstage) modified: hello_world.txt
И снова git log —oneline :
dslf445 (HEAD -> master) second commit h4k4o5j first commit
Как и ожидалось, указатель HEAD переместился на второй коммит, а состояние индекса осталось неизменным.
Теперь повторим наши действия, но уже с параметром —mixed :
git reset —mixed 349de9d
Проверим git status:
On branch master Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) modified: hello_world.txt
А также git log —oneline :
dslf445 (HEAD -> master) second commit h4k4o5j first commit
В данном случае, указатель снова переместился на второй коммит, но предыдущие изменения попали в зону отслеживаемых файлов. Это значит, что теперь мы можем решить — оставить эти изменения, добавив их в индекс, либо избавиться от них.
И в заключении повторим ту же последовательность действий с параметром —hard .
git reset —hard 349de9d
Снова проверяем git status :
On branch master nothing to commit, working tree clean
И git log —oneline :
dslf445 (HEAD -> master) second commit h4k4o5j first commit
Здесь указатель HEAD переместился на второй коммит, а все предыдущие изменения были стерты, что видно по пустому индексу и зоне отслеживаемых файлов.
И если посмотреть сейчас содержимое файла, то мы увидим единственную строку “Hello, world!”, которую мы с вами добавляли в файл во втором коммите.
Ветвление в Git
Почти каждая система контроля версий имеет поддержку ветвления. Ветвление означает, что у вас есть возможность работать над разными версиями проекта. То есть, если раньше история вашей разработки являла собой прямую последовательность коммитов, то теперь она может расходиться в определенных точках.
Это очень полезная функция по многим причинам, например для взаимодействия нескольких разработчиков. Представьте, вы с коллегой корпите над одним проектом. Каждый из вас работает над разными фичами, и для того чтобы не мешать друг другу, вы можете работать в разных ветках, а по окончанию работы слить эти ветки в одну.
Давайте попробуем с этим поработать на нашем примере. У нас имеется следующая последовательность коммитов.
957e113 (HEAD -> master) fourth commit ekd53dk third commit dslf445 second commit h4k4o5j first commit
Git по умолчанию во время инициализации создает ветку master и уже ведет свою работу в ней. Мы можем в этом убедиться введя команду:
* master
Предположим, что нам по какой-либо причине понадобилось создать новую ветку и вести работу в ней. Для этого сначала необходимо её создать.
Делается это при помощи команды git branch . Давайте создадим ветку “dev”:
Теперь введя команду git branch мы увидим следующее:
dev * master
Звёздочкой Git указывает на текущую ветку, в которой мы работаем.
Для того чтобы переключиться на другую ветку используют команду git checkout . Давайте переключимся на ветку “dev”.
git checkout dev
Switched to branch 'dev'
Теперь внесем любые изменения в файл hello_world.txt и сделаем коммит, после чего посмотрим, как выглядят наши ветки после редактирования.
Взглянем на git log —oneline :
dece9c9 (HEAD -> dev) fifth commit 957e113 (master) fourth commit ekd53dk third commit dslf445 second commit h4k4o5j first commit
Как и следовало ожидать, в нашей истории появился еще один — пятый коммит.
Теперь перейдем на ветку master
git checkout master
И просмотрим git log —oneline , и убедимся в том что все осталось без изменений.
957e113 (HEAD -> master) fourth commit ekd53dk third commit dslf445 second commit h4k4o5j first commit
Помимо разделения истории в Git мы также можем соединять воедино два потока разработки. Это значит, что нашу проделанную работу в новой ветке мы можем слить обратно в master. Такой процесс слияния можно выполнить при помощи команды git merge . То есть, если мы хотим слить изменения из ветки “dev” в ветку “master”, нам необходимо перейти на ветку “master” и в ней выполнить:
Updating 957e113..dece9c9 Fast-forward hello_world.txt | 1 + 1 file chaged, 1 insertion(+)
Теперь если мы проверим git log —oneline , то убедимся в том, что новый коммит из ветки “dev” переместился в ветку “master”.
dece9c9 (HEAD -> master, dev) fifth commit 957e113 fourth commit ekd53dk third commit dslf445 second commit h4k4o5j first commit
Теперь от ненужной ветки можно избавиться и удалить её с помощью команды git branch -d .
В данных примерах я не учитывал того факта, что при слиянии веток иногда возможно возникновение конфликтов, которые приходится разрешать вручную. Как правило это происходит при слиянии двух веток, в которых одновременно велась какая-то работа. Или же когда в новой ветке вы задели изменения старой. Я не буду рассматривать такие примеры, т.к. разрешение конфликтов — тема не маленькая, и многие вещи в ней можно разобрать самостоятельно в ходе практики.
Примеры ведения истории проекта
Моя статья подходит к концу, но перед завершением хочу отметить, что во многих командах существуют определенные соглашения по поводу ведения истории в Git.
Например вас могут попросить соблюдать несколько правил, при написании сообщения коммита. Или перед работой вас могут ознакомить с определенной стратегией ветвления в проектах компании. Вас также могут ограничить в количестве отправляемых коммитов в удаленный репозиторий.
В качестве примера, я расскажу какие соглашения действуют в компании, в которой я работаю.
1. Сообщение коммита:
Ниже представлен шаблон наших сообщений коммита:

Мы указываем дату совершения коммита и версию приложения для удобства поиска работы в истории.
Модификаторы формата коммита предоставляют информацию о том какой фронт работы был выполнен в этом коммите.
Мы используем следующие модификаторы:
- Dev: — указывает на то, что в коммите велась разработка нового функционала.
- Refactoring: — данный модификатор сообщает о рефакторинге проведенном в коде.
- Fix: — в данном коммите фиксили баги.
- Release: — данный коммит отправлен в ветку «release» и хранит состояние релизной версии приложения.
Также в конце сообщения мы оставляем короткое описание — над чем мы работали в этом коммите.
2. Стратегия ветвления:
В действительности можно насчитать достаточно много стратегий ветвления. Все они могут незначительно различаться, но выполнять совершенно разные задачи.
В нашем случае, мы выделяем две основные ветки master и «release». Master используется для подготовки к выкладке новых версий приложения. Код попавший в «master» проходит автоматические тесты, после которых выполняется сборка проекта, которую необходимо вручную протестировать перед дальнейшими действиями. Далее если замечаний к работе нет, мы сливаем ветку «master» в ветку «release». Там снова запускаются автоматические тесты, и собираются сборки к выкладке в маркеты.
Для ведения разработки мы создаем feature векти. Это означает, что каждая ветка отвечает за разработку какой-нибудь функциональности. Например, если мы хотим внедрить в приложение хранение данных в облаке, то программист создаст ветку «feature-cloud» и будет вести работу в ней.
Заключение
Мы рассмотрели самые основные приемы работы с Git. Моей задачей было сформировать в вас некоторое понимание — что есть система контроля версий и познакомить с одной из них. Мы разобрали структуру Git проекта, и теперь у вас есть представление о том, как он работает. Мы познакомились с самыми важными командами в Git, рассмотрели некоторые команды для инспектирования истории проекта и даже овладели несколькими приемами для перемещения HEAD указателя. Мы немного затронули тему ветвления, попробовали создать свою новую ветку и слить её с базовой.
Надеюсь, вам было интересно. Хочу снова заметить, что Git — тема комплексная, и, собирая информацию по крупицам, вы в итоге сможете работать уверенно с этим инструментом. Главное здесь — практика и желание во всем разобраться.
В качестве тренировки и закрепления имеющихся навыков, оставлю ссылку на удобный тренажер для Git.
А также на книги, которыми я пользовался и интернет ресурсы:
1. “Version control with Git” — Jon Loeliger;
2. “Pro Git” — Scott Chacon, Ben Straub.
Также было бы интересно узнать какие практики по Git есть у вас в компаниях и какие интересные ресурсы вы можете подсказать.
Спасибо за ваше внимание!
5 способов исправить ошибки с помощью Git
Независимо от того, насколько вы опытны, практика разработки невозможна без ошибок. Но что отличает программистов от лучших программистов, так это то, что они умеют исправлять свои ошибки!
Если вы используете Git в качестве системы контроля версий, у вас под рукой уже есть множество «инструментов исправления». В этом посте я расскажу вам о пяти мощных способах устранения ошибок в Git!
Отмена некоторых локальных изменений
Программирование — это часто беспорядочный процесс. Временами кажется, что вы делаете два шага вперед и один назад. Другими словами: часть кода, который вы написали, прекрасна. а часть — не очень. Вот где Git может помочь вам: он позволяет сохранить то, что хорошо, и отбросить те изменения, которые вам больше не нужны.
Давайте рассмотрим пример сценария с некоторыми «локальными» изменениями (они же изменения, которые мы еще не коммитили).
У нас есть проблемный файл general.css . Давайте сначала разберемся с ним. Изменения, которые мы внесли, пошли в совершенно неправильном направлении. Давайте отменим все эти действия и воссоздадим последнее закоммиченное состояние этого файла:
$ git restore css/general.css
Обратите внимание, что в качестве альтернативы я мог бы использовать команду git checkout для достижения того же результата. Но поскольку у git checkout так много разных задач и значений, я предпочитаю более новую команду git restore (которая ориентирована исключительно на такие задачи).
Наша вторая проблема в файле index.html немного сложнее. Некоторые из изменений, которые мы внесли в этот файл, на самом деле замечательные, и только некоторые из них нужно отменить.
И снова на помощь приходит git restore — но на этот раз с помощью -p , потому что мы хотим спуститься на уровень «patch»:
$ git restore -p index.html
Затем Git для каждого фрагмента изменений в этом файле будет опрос — хотите ли вы отбросить данное изменение или нет.
Вы заметите, что я ввел «n» для первого фрагмента (чтобы сохранить его) и «y» для второго фрагмента (чтобы отбросить его). После завершения процесса видно, что сохранился только первый, необходимый фрагмент изменений — как мы и хотели!
Возврат определенного файла в предыдущее состояние
Иногда требуется восстановить определенный файл до определенной версии. Например, вы знаете, что index.html работал нормально в какой-то более ранний момент времени, но теперь это не так. Тогда вы хотите вернуть время назад, но только для этого конкретного файла, а не для всего проекта!
Прежде всего, необходимо выяснить, какую именно часть проекта мы хотим восстановить. При правильном наборе параметров вы можете заставить команду git log показать вам историю только нашего единственного файла:
$ git log -- index.html
Оно показывает нам только те коммиты, в которых был изменен index.html , что довольно полезно для поиска ошибки.
Если вам нужно больше информации и вы хотите посмотреть на содержимое этих коммитов, вы можете попросить Git показать вам фактические изменения в этих коммитах с помощью флага -p :
$ git log -p -- index.html
После того как мы нашли неправильный коммит, в котором мы испортили наш чудесный маленький файл, мы можем исправить ошибку. Мы сделаем это, восстановив файл на версии, предшествующей неверному коммиту! Это важно: мы хотим восстанавливать файл не в том коммите, в котором была допущена ошибка, а в последнем хорошем состоянии — то есть за один коммит до этого!
$ git checkout ~1 -- index.html
Добавление ~1 к хэшу плохого коммита даст указание Git’у сделать именно это: перейти на одну версию раньше ссылающегося коммита.
Выполнив эту команду, вы увидите, что index.html в вашей локальной рабочей версии изменен: Git восстановил для нас последнюю правильную версию файла!
Восстановление потерянной версии с помощью Reflog
Еще одним замечательным инструментом исправления в арсенале Git’а является «Reflog». Его можно рассматривать как журнал, в котором Git протоколирует все движения указателя HEAD, происходящие в вашем локальном репозитории — такие вещи, как коммиты, чек-ауты, слияния и ребейзы, cherry-pick’и и сбросы. Все наиболее интересные действия прекрасно отображаются в этом журнале!
Такой журнал идеально подходит для тех случаев, когда дела идут неважно. Итак, давайте для начала устроим небольшую поломку, которую потом можно будет устранить с помощью Reflog .
Допустим, вы были уверены, что ваши последние коммиты никуда не годятся: вы хотели от них избавиться и поэтому использовали git reset для возврата к предыдущей версии. В результате «плохие» коммиты исчезли из вашей истории коммитов — как вы и хотели.
Дальше вы замечаете, что это была плохая идея: на самом деле коммиты были не так уж плохи! Но плохая новость, конечно, в том, что вы только что стёрли их из истории коммитов вашего репозитория!
Это классический случай для инструмента Git’а Reflog! Давайте посмотрим, как он может спасти вас:
$ git reflog
- Во-первых, Reflog очень легко открыть: для этого достаточно выполнить простой git reflog .
- Во-вторых, вы заметите, что записанные состояния отсортированы в хронологическом порядке, причем самое новое находится вверху.
- Если вы присмотритесь, то увидите, что самый верхний (то есть самый новый) элемент — это действие «сброс». Что мы и сделали 20 секунд назад. Очевидно, журнал работает.
- Если теперь мы захотим отменить наш непроизвольный «сброс», мы можем просто вернуться к прежнему состоянию — что также аккуратно показано здесь! Мы можем просто скопировать хэш коммита этого предыдущего состояния в буфер обмена и продолжить работу оттуда.
Чтобы восстановить прежнее состояние, мы можем либо снова использовать git reset , либо просто создать новую ветку:
$ git branch happy-ending e5b19e4
Как мы можем с радостью убедиться, наша новая ветка содержит коммиты, которые, как мы думали, потеряли из-за случайной ошибки при git reset !
Восстановление удаленной ветки
Рефлог может быть полезен и в других ситуациях. Например, когда вы по неосторожности удалили ветку, которую действительно (!) не следовало удалять. Давайте посмотрим на наш пример:
$ git branch * feature/analytics master
Представим, что наш заказчик проекта говорит нам, что прекрасная функция analytics , над которой мы работали, больше не нужна. Конечно же, мы удалим соответствующую ветку feature/analytics !
feature/analytics — это наша текущая ветка HEAD. Чтобы иметь возможность удалить ее, мы должны сначала переключиться на другую ветку:
$ git checkout master $ git branch -d feature/analytics error: The branch 'feature/analytics' is not fully merged. If you are sure you want to delete it, run 'git branch -D feature/analytics'.
Git говорит нам, что мы собираемся сделать что-то действительно значимое: поскольку feature/analytics содержит уникальные коммиты, которые больше нигде не присутствуют, удаление этой функции уничтожит некоторые (потенциально ценные) данные. Что ж. поскольку функция больше не нужна, мы можем продолжать:
$ git branch -D feature/analytics Deleted branch feature/analytics (was b1c249b).
Вы, наверное, уже догадались, что сейчас произойдет: наш заказчик проекта радостно сообщает нам, что функцию нужно вернуть в проект! Они все-таки хотят ее реализовать!
И снова мы сталкиваемся с неприятной ситуацией, когда мы могли потерять ценные данные! Так что давайте посмотрим, сможет ли Reflog спасти нас еще раз:
$ git reflog
Прежде чем мы решили удалить нашу ветку, нам пришлось выполнить git checkout, чтобы от нее переключиться (поскольку Git не позволяет удалять текущую ветку). Конечно, эта процедура также была зарегистрирована в Reflog. Чтобы восстановить удалённую ветку, мы можем теперь просто взять предыдущее состояние в качестве отправной точки для новой ветки:
$ git branch feature/analytics b1c249b
И вуаля: наша ветка восстановилась!
Перенос коммита в другую ветку
В заключение приведем еще одну классику из серии «О нет!»: коммит в не той ветке.
Сегодня во многих командах действует правило, запрещающее коммиты непосредственно в давно работающие ветки, такие как «main», «master» или «develop». Как правило, новые коммиты должны попадать в эти ветки только через слияния/ребейзы. И все же мы иногда забываем и делаем коммит напрямую.
Давайте рассмотрим следующий сценарий в качестве примера.
Мы должны были сделать коммит на feature/newsletter , но по неосторожности нажали на master. Давайте рассмотрим решение проблемы шаг за шагом.
Во-первых, мы должны убедиться, что на этот раз мы находимся в правильной ветке, поэтому мы проверяем feature/newsletter :
$ git checkout feature/newsletter
Теперь мы можем безопасно перенести этот коммит с помощью команды cherry-pick :
$ git cherry-pick 26bf1b48
Мы можем взглянуть на feature/newsletter и увидим, что теперь коммит существует и здесь.
Пока всё хорошо. Но cherry-pick не удалил коммит из master ветки, где его никогда не должно было быть. Поэтому нам придётся навести порядок и там:
$ git checkout master $ git reset --hard HEAD~1
Мы переключаемся обратно на master и затем используем git reset , чтобы удалить ненужный коммит из истории. Наконец, все вновь исправлено.
Мы не можем избежать ошибок! Каким бы лучшим программистом мы ни были, время от времени мы будем ошибаться. Поэтому вопрос не в том, совершаем ли мы ошибки, а в том, насколько хорошо мы умеем с ними справляться и устранять проблемы.