Что такое область подготовленных файлов или индекс в git
Git — контентно-адресуемая файловая система. Здорово. Что это означает? А означает это, по сути, что Git — простое хранилище ключ-значение. Можно добавить туда любые данные, в ответ будет выдан ключ по которому их можно извлечь обратно.
В качестве примера, воспользуемся служебной командой git hash-object , которая берёт некоторые данные, сохраняет их в виде объекта в каталоге .git/objects (база данных объектов) и возвращает уникальный ключ, который является ссылкой на созданный объект.
Для начала создадим новый Git-репозиторий и убедимся, что каталог objects пуст:
Git проинициализировал каталог objects и создал в нём пустые подкаталоги pack и info . Теперь с помощью git hash-object создадим объект и вручную добавим его в базу Git:
В простейшем случае git hash-object берёт переданный контент и возвращает уникальный ключ, который будет использоваться для хранения данных в базе Git. Параметр -w указывает команде git hash-object не просто вернуть ключ, а ещё и сохранить объект в базе данных. Последний параметр --stdin указывает, что git hash-object должна использовать данные, переданные на стандартный потока ввода; в противном случае команда ожидает путь к файлу в качестве аргумента.
Результат выполнения команды — 40-символьная контрольная сумма. Это SHA-1 хеш — контрольная сумма содержимого и заголовка, который будет рассмотрен позднее. Теперь можно посмотреть как Git хранит ваши данные:
Мы видим новый файл в каталоге objects . Это и есть начальное внутреннее представление данных в Git — один файл на единицу хранения с именем, являющимся контрольной суммой содержимого и заголовка. Первые два символа SHA-1 определяют подкаталог файла внутри objects , остальные 38 — его имя.
Извлечь содержимое объекта можно при помощи команды cat-file . Она подобна швейцарскому ножу для анализа объектов Git. Ключ -p указывает команде cat-file автоматически определять тип объекта и выводить результат в соответствующем виде:
Теперь вы умеете добавлять данные в Git и извлекать их обратно. То же самое можно делать и с файлами. Например, можно проверсионировать один файл. Для начала, создадим новый файл и сохраним его в базе данных Git:
Теперь изменим файл и сохраним его в базе ещё раз:
Теперь в базе содержатся две версии файла, а также самый первый сохранённый объект:
Теперь можно откатить файл к его первой версии:
Однако запоминать хеш для каждой версии неудобно, к тому же теряется имя файла, сохраняется лишь содержимое. Объекты такого типа называют блобами (англ. blob — binary large object). Имея SHA-1 объекта, можно попросить Git показать нам его тип с помощью команды cat-file -t :
Деревья
Следующий тип объектов, который мы рассмотрим, — деревья — решают проблему хранения имён файлов, а также позволяют хранить группы файлов вместе. Git хранит данные сходным с файловыми системами UNIX способом, но в немного упрощённом виде. Содержимое хранится в деревьях и блобах, где дерево соответствует каталогу на файловой системе, а блоб более или менее соответствует inode или содержимому файла. Дерево может содержать одну или более записей, содержащих SHA-1 хеш, соответствующий блобу или поддереву, права доступа к файлу, тип и имя файла. Например, дерево последнего коммита в проекте может выглядеть следующим образом:
Запись master^ указывает на дерево, соответствующее последнему коммиту ветки master . Обратите внимание, что подкаталог lib — не блоб, а указатель на другое дерево:
Вы можете столкнуться с различными ошибками при использовании синтаксиса master^ в зависимости от того, какую оболочку используете.
В Windows CMD символ ^ используется для экранирования, поэтому для исключения ошибок следует использовать двойной символ: git cat-file -p master^^ . В PowerShell параметры, использующие символы <>, должны быть заключены в кавычки: git cat-file -p 'master^' .
В ZSH символ ^ используется для подстановки, поэтому выражение следует помещать в кавычки: git cat-file -p "master^" .
Концептуально, данные хранятся в Git примерно так:
Можно создать дерево самому. Обычно, Git создаёт дерево путём создания набора объектов из состояния области подготовленных файлов или индекса. Поэтому для создания дерева необходимо проиндексировать какие-нибудь файлы. Для создания индекса из одной записи — первой версии файла test.txt — воспользуемся низкоуровневой командой git update-index . Данная команда может искусственно добавить более раннюю версию test.txt в новый индекс. Необходимо передать опции --add , так как файл ещё не существует в индексе (да и самого индекса ещё нет), и --cacheinfo , так как добавляемого файла нет в рабочем каталоге, но он есть в базе данных. Также необходимо передать права доступа, хеш и имя файла:
В данном случае права доступа 100644 — означают обычный файл. Другие возможные варианты: 100755 — исполняемый файл, 120000 — символическая ссылка. Права доступа в Git сделаны по аналогии с правами доступа в UNIX, но они гораздо менее гибки: указанные три режима — единственные доступные для файлов (блобов) в Git (хотя существуют и другие режимы, используемые для каталогов и подмодулей).
Теперь можно воспользоваться командой git write-tree для сохранения индекса в объект дерева. Здесь опция -w не требуется — команда автоматически создаст дерево из индекса, если такого дерева ещё не существует:
Используя ту же команду git cat-file , можно проверить, что созданный объект действительно является деревом:
Давайте создадим новое дерево со второй версией файла test.txt и ещё одним файлом:
Теперь в области подготовленных файлов содержится новая версия файла test.txt и новый файл new.txt. Зафиксируем изменения, сохранив состояние индекса в новое дерево, и посмотрим, что из этого вышло:
Обратите внимание, что в данном дереве находятся записи для обоих файлов, а также, что хеш файла test.txt это хеш «второй версии» этого файла ( 1f7a7a ). Для интереса, добавим первое дерево как подкаталог текущего. Добавлять деревья в область подготовленных файлов можно с помощью команды git read-tree . В нашем случае, чтобы включить уже существующее дерево в индекс и сделать его поддеревом, необходимо использовать опцию --prefix :
Если бы вы сейчас добавили только что сохранённое дерево в рабочий каталог, вы бы увидели два файла в его корне и подкаталог bak с первой версией файла test.txt . В таком случае хранимые структуры данных можно представить следующим образом:
Объекты коммитов
У вас есть три дерева, соответствующих разным состояниям проекта, но предыдущая проблема с необходимостью запоминать все три значения SHA-1, чтобы иметь возможность восстановить какое-либо из этих состояний, ещё не решена. К тому же у нас нет никакой информации о том, кто, когда и почему сохранил их. Такие данные — основная информация, хранимая в объекте коммита.
Для создания коммита необходимо вызвать команду commit-tree и задать SHA-1 нужного дерева и, если необходимо, родительские коммиты. Начнём с создания коммита для самого первого дерева:
Полученный вами хеш будет отличаться, так как отличается дата создания и информация об авторе. Далее в этой главе используйте собственные хеши коммитов и тегов. Просмотреть созданный объект коммита можно командой cat-file :
Далее, создадим ещё два объекта коммита, каждый из которых будет ссылаться на предыдущий:
Каждый из созданных объектов коммитов указывает на одно из созданных ранее деревьев состояния проекта. Вы не поверите, но теперь у нас есть полноценная Git история, которую можно посмотреть командой git log , указав хеш последнего коммита:
Здорово, правда? Мы только что выполнили несколько низкоуровневых операций и получили Git репозиторий с историей без единой высокоуровневой команды. Именно так и работает Git, когда выполняются команды git add и git commit — сохраняет блобы для изменённых файлов, обновляет индекс, создаёт деревья и фиксирует изменения в объекте коммита, ссылающемся на дерево верхнего уровня и предшествующие коммиты. Эти три основных вида объектов Git — блоб, дерево и коммит — сохраняются в виде отдельных файлов в каталоге .git/objects . Вот как сейчас выглядит список объектов в этом каталоге, в комментарии указано чему соответствует каждый из них:
Если пройти по всем внутренним ссылкам, получится граф объектов, представленный на рисунке:
Хранение объектов
Ранее мы упоминали, что вместе с содержимым объекта сохраняется дополнительный заголовок. Давайте посмотрим, как Git хранит объекты на диске. Мы рассмотрим как происходит сохранение блоб объекта — в данном случае это будет строка «what is up, doc?» — в интерактивном режиме на языке Ruby.
Для запуска интерактивного интерпретатора воспользуйтесь командой irb :
Git создаёт заголовок, начинающийся с типа объекта, в данном случае это блоб. Далее идут пробел, размер содержимого в байтах и в конце нулевой байт:
Git объединяет заголовок и оригинальный контент, а затем вычисляет SHA-1 сумму от полученного результата. В Ruby значение SHA-1 для строки можно получить, подключив соответствующую библиотеку командой require и затем вызвав Digest::SHA1.hexdigest() :
Давайте сравним полученный результат с выводом команды git hash-object . Здесь используется echo -n для предотвращения автоматического добавления переноса строки.
Git сжимает новые данные при помощи zlib, в Ruby это можно сделать с помощью одноимённой библиотеки. Сперва необходимо подключить её, а затем вызвать Zlib::Deflate.deflate() :
После этого сохраним сжатую строку в объект на диске. Определим путь к файлу, который будет записан (первые два символа хеша используются в качестве названия каталога, оставшиеся 38 — в качестве имени файла в ней). В Ruby для безопасного создания нескольких вложенных каталогов можно использовать функцию FileUtils.mkdir_p() . Далее, откроем файл вызовом File.open() и запишем сжатые данные вызовом write() для полученного файлового дескриптора:
Теперь проверим содержимое объекта с помощью git cat-file :
Вот и всё, мы создали корректный блоб объект для Git.
Все другие объекты создаются аналогичным образом, меняется лишь запись о типе в заголовке: «blob», «commit» либо «tree». Стоит добавить, что блоб может иметь практически любое содержимое, однако содержимое объектов деревьев и коммитов записывается в очень строгом формате.
Индекс в Git — это специальная промежуточная область, в которой хранятся изменения файлов на пути от рабочей директории до репозитория. При выполнении коммита в него попадают только те изменения, которые были добавлены в индекс.
Понятие индекса в Git появилось не случайно. Даже когда разработчик работает над одной задачей, по пути он натыкается на разные места в коде, которые либо плохо оформлены, либо содержат ошибки, либо должны быть исправлены в соответствии с какими-то новыми требованиями. И в большинстве ситуаций совершенно нормально исправлять эти недочеты, что все и делают. В итоге в рабочей директории появляется множество разных исправлений, которые частично относятся к выполняемой задаче, а частично содержат множественные исправления, напрямую не связанные с основными изменениями. В чём здесь проблема?
Если делать ровно один коммит, включающий в себя и основную задачу, и дополнительные исправления, то появляется несколько неприятных побочных эффектов. Во-первых, сложнее смотреть историю. Коммит начинает содержать совершенно несвязанные изменения, которые отвлекают во время ревью (проверки чужого кода).
Во-вторых, что вероятно даже важнее, откат коммита по любым причинам приведет к тому, что откатятся правки, которые всё равно нужно будет делать.
Именно здесь помогает индекс. Его наличие позволяет меньше переживать на тему того, как сформируется коммит.
Стандартный способ работы с индексом — это добавление или изменение файлов и последующий коммит:
Если речь идет про один-два файла, которые нужно закоммитить прямо сейчас, то можно сделать проще. Команда git commit принимает на вход аргументы — пути до файлов. Она автоматически добавляет эти файлы в индекс и затем в коммит. Данный подход работает только с уже отслеживаемыми файлами.
Иногда бывает наоборот — мы исправили много файлов и хотим добавить их в коммит сразу все. Тогда поможет точка:
Команда выше очень опасна. С ее помощью крайне легко закоммитить много лишнего, особенно если не помнить про необходимость перед коммитом смотреть git diff --staged .
Ну и совсем страшная, но полезная команда — это коммит с одновременным добавлением всего в индекс:
С другой стороны, нередко разные изменения делаются в одних и тех же файлах. То есть изменения в этих файлах по-хорошему должны находиться в разных коммитах. И даже такое можно сделать с помощью Git. Для этого подходит команда git add -i , которая показывает измененные куски файлов и спрашивает, что с ними сделать. С помощью этой команды можно очень точно выбрать то, что должно попасть в коммит, а что нет. Ее использование обычно показывает хороший уровень владения Git.
Самостоятельная работа
Попрактикуйтесь в использовании команд из урока, меняя содержимое репозитория
Для того, чтобы начать работу с Git, вам потребуется одноименная утилита - git. Для более эффективного знакомства с Git’ом желательно, чтобы вы его себе установили. В рамках этого видео я не буду выполнять установку, сделать это довольно просто. Если вы используете в своей работе операционную систему семейства Linux или Unix - достаточно установить пакет и его зависимости при помощи вашего пакетного менеджера.
Перед тем, как мы попробуем поработать с Git, стоит обратить внимание на один очень важный факт: несмотря на то, что интерфейс взаимодействия с этой СКВ очень схож с интерфейсом других систем, внутри Git все может очень сильно отличаться. И если вы уже были знакомы с такими системами, как, например, subversion, вам стоит абстрагироваться от имеющихся знаний. Это позволит вам быстрее проникнуться философией Git и не допускать ошибок в работе с ним.
Как только вы установите Git, вам захочется попробовать поработать с ним. Но лучше, в самом начале, выполнить несколько шагов, чтобы настроить среду для работы с Git'ом под себя. Это нужно сделать только один раз — при обновлении версии Git'а настройки сохранятся. Но вы можете поменять их в любой момент, выполнив те же команды снова. Большинству операций не потребуются эти настройки, однако вы не сможете сделать коммит без них.
В состав Git'а входит утилита git config , которая позволяет просматривать и устанавливать параметры, контролирующие все аспекты работы Git'а и его внешний вид.
Эти параметры могут быть сохранены в трёх местах:
- Уровень системы: файл /etc/gitconfig содержит значения, общие для всех пользователей системы и для всех их репозиториев. Если при запуске git config указать параметр --system , то параметры будут читаться и сохраняться именно в этот файл.
- Уровень пользователя: файл ~/.gitconfig хранит настройки конкретного пользователя. Этот файл используется при указании параметра --global .
- Уровень проекта: конфигурационный файл в каталоге Git'а ( .git/config ) в том репозитории, где вы находитесь в данный момент. Эти параметры действуют только для данного конкретного репозитория. Настройки на каждом следующем уровне подменяют настройки из предыдущих уровней, то есть значения в .git/config перекрывают соответствующие значения в /etc/gitconfig .
В системах семейства Windows Git ищет файл .gitconfig в каталоге $HOME ( C:\Documents and Settings\$USER или C:\Users\$USER для большинства пользователей). Кроме того Git ищет файл /etc/gitconfig , но уже относительно корневого каталога MSys, который находится там, куда вы решили установить Git, когда запускали инсталлятор.
Первое, что вам следует сделать после установки Git'а, — указать ваше имя и адрес электронной почты. Это важно, потому что каждый коммит в Git'е содержит эту информацию, и она включена в коммиты, передаваемые вами, и не может быть далее изменена. Для установки этих параметров наберем следующие команды:
Повторюсь, что, если указана опция --global , то эти настройки достаточно сделать только один раз, поскольку в этом случае Git будет использовать эти данные для всего, что вы делаете в этой системе. Если для каких-то отдельных проектов вы хотите указать другое имя или электронную почту, можно выполнить эту же команду без параметра --global в каталоге с нужным проектом.
Я в своей работе использую Vim, поэтому верну обратно этот редактор
Другая полезная настройка, которая может понадобиться — встроенная diff-утилита, которая будет использоваться для разрешения конфликтов слияния. Например, если вы хотите использовать vimdiff:
Git умеет делать слияния при помощи kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, ecmerge и opendiff, но вы можете настроить и другую утилиту.
Если вы хотите проверить используемые настройки, можете использовать команду git config --list , чтобы показать все, которые Git найдёт:
Некоторые ключи (названия) настроек могут появиться несколько раз, потому что Git читает один и тот же ключ из разных файлов (например из /etc/gitconfig и ~/.gitconfig). В этом случае Git использует последнее значение для каждого ключа.
Также вы можете проверить значение конкретного ключа, выполнив git config :
Для создания Git-репозитория существуют два основных подхода. Первый подход — импорт в Git уже существующего проекта или каталога. Второй — клонирование уже существующего репозитория с сервера.
Если вы собираетесь начать использовать Git для существующего проекта, то вам необходимо перейти в проектный каталог и в командной строке ввести
Эта команда создаёт в текущем каталоге новый подкаталог с именем .git содержащий все необходимые файлы репозитория — основу Git-репозитория. На этом этапе ваш проект ещё не находится под версионным контролем. Это отчетливо видно, если выполнить команду
В моем случае вывод команды такой:
Это означает, что я нахожуть в ветке, под названием master (ветка по-умолчанию), был выполнен автоматический коммит пустой директории (об этом говорит фраза “Initial commit”), и перечислен спискок файлов и директорий, которые сейчас не находятся под контролем Git. А также, сказано, что мы еще не подготовили данные для коммита.
К тому же, если выпонить комманду
результат ее выполнения будет пустым, то есть изменений нет. Это потому, что Git говорит об изменениях только тех файлов, которые находятся под его контролем.
Теперь внимание. Это самое важное, что нужно помнить про Git, если вы хотите, чтобы дальше изучение шло гладко. В Git'е файлы могут находиться в одном из трёх состояний: зафиксированном, изменённом и подготовленном.
"Зафиксированный" значит, что файл уже сохранён в вашей локальной базе. К изменённым относятся файлы, которые поменялись, но ещё не были зафиксированы. Подготовленные файлы — это изменённые файлы, отмеченные для включения в следующий коммит.
Таким образом, в проектах, использующих Git, есть три части:
- каталог Git'а (Git directory),
- рабочий каталог (working directory),
- область подготовленных файлов (staging area).
Каталог Git'а — это место, где Git хранит метаданные и базу данных объектов вашего проекта. Это наиболее важная часть Git'а, и именно она копируется, когда вы клонируете репозиторий с другого компьютера.
Рабочий каталог — это извлечённая из базы копия определённой версии проекта. Эти файлы достаются из сжатой базы данных в каталоге Git'а и помещаются на диск для того, чтобы вы их просматривали и редактировали.
Область подготовленных файлов — это обычный файл, обычно хранящийся в каталоге Git'а, который содержит информацию о том, что должно войти в следующий коммит. Иногда его называют индексом (index), но в последнее время становится стандартом называть его областью подготовленных файлов (staging area).
Стандартный рабочий процесс с использованием Git'а выглядит примерно так:
- Вы вносите изменения в файлы в своём рабочем каталоге.
- Подготавливаете файлы, добавляя их слепки в область подготовленных файлов.
- Делаете коммит, который берёт подготовленные файлы из индекса и помещает их в каталог Git'а на постоянное хранение.
Если рабочая версия файла совпадает с версией в каталоге Git'а, файл считается зафиксированным. Если файл изменён, но добавлен в область подготовленных данных, он подготовлен. Если же файл изменился после выгрузки из БД, но не был подготовлен, то он считается изменённым.
Если вы хотите добавить под версионный контроль существующие файлы (в отличие от пустого каталога), вам стоит проиндексировать эти файлы и осуществить первую фиксацию изменений. Осуществить это вы можете с помощью команды git add , а затем git commit:
В результате выполнения этих комманд я получил следующий вывод:
Вывод результатов выполнения команд в Git крайне лаконичен и информативен. В данном случае было сказано что мы выполнили коммит в ветку “master”, этот коммит является первым (root-commit), хеш коммит (его краткая запись) - “f234b65”, commit-message - “Initial project version”, статистика о добавленной информации: суммарное количество измененных файлов, добавленных и удаленных строк в них, а также перечень созданных файлов.
Перед сохранением любого файла Git вычисляет контрольную сумму, и она становится индексом этого файла. Поэтому невозможно изменить содержимое файла или каталога так, чтобы Git не узнал об этом. Эта функциональность встроена в сам фундамент Git'а и является важной составляющей его философии. Если информация потеряется при передаче или повредится на диске, Git всегда это выявит.
Механизм, используемый Git'ом для вычисления контрольных сумм, называется SHA-1 хешем. Это строка из 40 шестнадцатеричных символов (0-9 и a-f), вычисляемая в Git'е на основе содержимого файла или структуры каталога. SHA-1 хеш выглядит примерно так:
Работая с Git'ом, вы будете встречать эти хеши повсюду, поскольку он их очень широко использует. Фактически, в своей базе данных Git сохраняет всё не по именам файлов, а по хешам их содержимого.
Основной инструмент, используемый для определения, какие файлы в каком состоянии находятся — это команда git status. Если вы выполните эту команду сразу после клонирования, вы увидите что-то вроде этого:
Это означает, что у вас чистый рабочий каталог, другими словами — в нём нет отслеживаемых изменённых файлов. Git также не обнаружил неотслеживаемых файлов, в противном случае они бы были перечислены здесь. И наконец, команда сообщает вам на какой ветке (branch) вы сейчас находитесь. Пока что это всегда ветка master — это ветка по умолчанию. Предположим, вы добавили в свой проект новый файл, простой файл CHANGELOG. Если этого файла раньше не было, и вы выполните git status, вы увидите свой неотслеживаемый файл вот так:
Понять, что новый файл CHANGELOG неотслеживаемый можно по тому, что он находится в секции "Untracked files" в выводе команды status. Статус "неотслеживаемый файл", по сути, означает, что Git видит файл, отсутствующий в предыдущем снимке состояния (коммите); Git не станет добавлять его в ваши коммиты, пока вы его явно об этом не попросите. Это предохранит вас от случайного добавления в репозиторий сгенерированных бинарных файлов или каких-либо других, которые вы и не думали добавлять. Мы хотели добавить CHANGELOG, так давайте сделаем это. Для того чтобы начать отслеживать (добавить под версионный контроль) новый файл, используется команда git add. Чтобы начать отслеживание файла CHANGELOG, вы можете выполнить следующее:
Если вы снова выполните команду status, то увидите, что файл CHANGELOG теперь отслеживаемый и индексированный:
Вы можете видеть, что файл проиндексирован по тому, что он находится в секции “Changes to be committed”. Если вы выполните коммит в этот момент, то версия файла, существовавшая на момент выполнения вами команды git add, будет добавлена в историю снимков состояния. Как вы помните, когда вы ранее выполнили git init, вы затем выполнили git add (файлы) — это было сделано для того, чтобы добавить файлы в вашем каталоге под версионный контроль. Команда git add принимает параметром путь к файлу или каталогу, если это каталог, команда рекурсивно добавляет (индексирует) все файлы в данном каталоге.
Давайте модифицируем файл, уже находящийся под версионным контролем. Если вы измените отслеживаемый файл README и после этого снова выполните команду git status, то результат будет примерно следующим:
Файл README находится в секции “Changes not staged for commit” — это означает, что отслеживаемый файл был изменён в рабочем каталоге, но пока не проиндексирован. Чтобы проиндексировать его, необходимо выполнить команду git add (это многофункциональная команда, она используется для добавления под версионный контроль новых файлов, для индексации изменений, а также для других целей, например для указания файлов с исправленным конфликтом слияния). Выполним git add, чтобы проиндексировать README, а затем снова выполним git status:
Теперь оба файла проиндексированы и войдут в следующий коммит. В этот момент вы, предположим, вспомнили одно небольшое изменение, которое вы хотите сделать в README до фиксации. Вы открываете файл, вносите и сохраняете необходимые изменения и вроде бы готовы к коммиту. Но давайте-ка ещё раз выполним git status:
Теперь README отображается как проиндексированный и непроиндексированный одновременно. Как такое возможно? Такая ситуация наглядно демонстрирует, что Git индексирует файл в точности в том состоянии, в котором он находился, когда вы выполнили команду git add. Если вы выполните коммит сейчас, то файл README попадёт в коммит в том состоянии, в котором он находился, когда вы последний раз выполняли команду git add, а не в том, в котором он находится в вашем рабочем каталоге в момент выполнения git commit. Если вы изменили файл после выполнения git add, вам придётся снова выполнить git add, чтобы проиндексировать последнюю версию файла:
Первая строка предписывает Git'у игнорировать любые файлы заканчивающиеся на .o или .a — объектные и архивные файлы, которые могут появиться во время сборки кода. Вторая строка предписывает игнорировать все файлы заканчивающиеся на тильду (~), которая используется во многих текстовых редакторах, например Emacs, для обозначения временных файлов. Вы можете также включить каталоги log, tmp или pid; автоматически создаваемую документацию; и т.д. и т.п. Хорошая практика заключается в настройке файла .gitignore до того, как начать серьёзно работать, это защитит вас от случайного добавления в репозиторий файлов, которых вы там видеть не хотите.
Практически все действия, которые вы совершаете в Git'е, только добавляют данные в базу. Очень сложно заставить систему удалить данные или сделать что-то неотменяемое. Можно, как и в любой другой СКВ, потерять данные, которые вы ещё не сохранили, но как только они зафиксированы, их очень сложно потерять, особенно если вы регулярно отправляете изменения в другой репозиторий. Поэтому пользоваться Git'ом — удовольствие, потому что можно экспериментировать, не боясь что-то серьёзно поломать.
Одно из ключевых отличий Git от других систем - в приоритете хранение не разницы между двумя состояниями, которые при движении по истории применяются или отменяются, а использование конечных результатов, или, в терминологии Git - снепшотов. За счет выбора подобного подхода git работает очень быстро. Возвращение к какой-то конкретной версии файла происходит моментально и не доставляет вам неудобств.
Если вам нужна помощь при использовании Git'а, есть три способа открыть страницу руководства по любой команде Git'а:
Например, так можно открыть руководство по команде config:
Эти команды хороши тем, что ими можно пользоваться всегда, даже без подключения к сети.
Перед тем как погружаться в детали, пройдём поверхностно весь путь от создания проекта в git до начала отслеживания изменений. Затем, в следующих уроках поговорим подробнее про все этапы. В процессе изучим большое количество новых терминов и команд, которые нужны для понимания работы git.
Git может отслеживать файлы проекта только в том случае, когда они помещены под контроль версий. Для этого нужно зайти в директорию проекта и выполнить команду инициализации git init . Проект может быть как новый, так и уже существующий. Процесс инициализации от этого не поменяется.
Команда git init создает репозиторий — директорию .git, которая содержит все необходимые для работы git файлы.
С помощью команды git status можно посмотреть статус репозитория:
В этом выводе указано, что репозиторий пустой (No commits yet) и в него нечего добавить, так как нет новых или изменённых файлов. Давайте попробуем добавить несколько файлов:
Теперь снова смотрим на статус:
Git увидел, что в проекте появились новые файлы, о которых ему ничего не известно. Они помечаются как неотслеживаемые (untracked files). Git не следит за изменениями в таких файлах, так как они не добавлены в репозиторий. Добавление в репозиторий происходит в два шага. Первым шагом выполняется команда подготовки файлов git add :
Смотрим что произошло:
Файл README.md теперь находится в состоянии "подготовлен к коммиту" или, другими словами, файлы попадают в индекс. Под коммитом понимается окончательное добавление в репозиторий, когда git запоминает файл навсегда и следит за всеми последующими изменениями.
Коммит — это операция, которая берёт все подготовленные изменения (они могут включать любое количество файлов) и отправляет их в репозиторий как единое целое. Вот, как он выполняется:
Флаг -m означает message, то есть описание коммита. Коммит можно выполнять и без него, но тогда откроется редактор, в котором нужно будет ввести описание коммита. Мы рекомендуем делать осмысленные описания — это хороший тон. Пример соглашения по именованию коммитов приведён в дополнительных материалах к уроку.
Может возникнуть вопрос: зачем так сложно, зачем отдельно нужен индекс (куда попадают файлы после git add ), и почему нельзя добавлять все изменённые файлы сразу в коммит? Как ни странно, такой процесс создан как раз для удобства программистов. Дело в том, что во время разработки может меняться и добавляться много файлов. Но это не значит, что мы хотим добавить все эти изменения в один коммит.
Со смысловой точки зрения, коммит — это какое-то логически завершённое изменение внутри проекта. Его размер бывает очень маленьким, например, исправлением опечатки в одном файле, а иногда и большим, например, при внедрении новой функциональности. Главное в коммите — его атомарность, то есть он должен выполнять ровно одну задачу.
Теперь файл README.md находится внутри репозитория. Убедиться в этом можно, запустив команду git status :
git status не выводит файлы, которые добавлены в репозиторий и не содержат изменений. При этом сам файл README.md находится внутри директории hexlet-git.
Самостоятельная работа
Добавьте файл PEOPLE.md в репозиторий. После добавления команда git status покажет такой вывод:
Итак, у вас имеется настоящий Git-репозиторий и рабочая копия файлов для некоторого проекта. Вам нужно делать некоторые изменения и фиксировать «снимки» состояния (snapshots) этих изменений в вашем репозитории каждый раз, когда проект достигает состояния, которое вам хотелось бы сохранить.
Запомните, каждый файл в вашем рабочем каталоге может находиться в одном из двух состояний: под версионным контролем (отслеживаемые) и нет (неотслеживаемые). Отслеживаемые файлы — это те файлы, которые были в последнем снимке состояния проекта; они могут быть неизменёнными, изменёнными или подготовленными к коммиту. Если кратко, то отслеживаемые файлы — это те файлы, о которых знает Git.
Неотслеживаемые файлы — это всё остальное, любые файлы в вашем рабочем каталоге, которые не входили в ваш последний снимок состояния и не подготовлены к коммиту. Когда вы впервые клонируете репозиторий, все файлы будут отслеживаемыми и неизменёнными, потому что Git только что их извлек и вы ничего пока не редактировали.
Как только вы отредактируете файлы, Git будет рассматривать их как изменённые, так как вы изменили их с момента последнего коммита. Вы индексируете эти изменения, затем фиксируете все проиндексированные изменения, а затем цикл повторяется.
Определение состояния файлов
Основной инструмент, используемый для определения, какие файлы в каком состоянии находятся — это команда git status . Если вы выполните эту команду сразу после клонирования, вы увидите что-то вроде этого:
Это означает, что у вас чистый рабочий каталог, другими словами — в нем нет отслеживаемых измененных файлов. Git также не обнаружил неотслеживаемых файлов, в противном случае они бы были перечислены здесь. Наконец, команда сообщает вам на какой ветке вы находитесь и сообщает вам, что она не расходится с веткой на сервере. Пока что это всегда ветка master , ветка по умолчанию; в этой главе это не важно. В главе Ветвление в Git будут рассмотрены ветки и ссылки более детально.
Предположим, вы добавили в свой проект новый файл, простой файл README . Если этого файла раньше не было, и вы выполните git status , вы увидите свой неотслеживаемый файл вот так:
Понять, что новый файл README неотслеживаемый можно по тому, что он находится в секции «Untracked files» в выводе команды status . Статус Untracked означает, что Git видит файл, которого не было в предыдущем снимке состояния (коммите); Git не станет добавлять его в ваши коммиты, пока вы его явно об этом не попросите. Это предохранит вас от случайного добавления в репозиторий сгенерированных бинарных файлов или каких-либо других, которые вы и не думали добавлять. Мы хотели добавить README, так давайте сделаем это.
Отслеживание новых файлов
Для того чтобы начать отслеживать (добавить под версионный контроль) новый файл, используется команда git add . Чтобы начать отслеживание файла README , вы можете выполнить следующее:
Если вы снова выполните команду status , то увидите, что файл README теперь отслеживаемый и добавлен в индекс:
Вы можете видеть, что файл проиндексирован, так как он находится в секции «Changes to be committed». Если вы выполните коммит в этот момент, то версия файла, существовавшая на момент выполнения вами команды git add , будет добавлена в историю снимков состояния. Как вы помните, когда вы ранее выполнили git init , затем вы выполнили git add (файлы) — это было сделано для того, чтобы добавить файлы в вашем каталоге под версионный контроль. Команда git add принимает параметром путь к файлу или каталогу, если это каталог, команда рекурсивно добавляет все файлы из указанного каталога в индекс.
Индексация изменённых файлов
Давайте модифицируем файл, уже находящийся под версионным контролем. Если вы измените отслеживаемый файл CONTRIBUTING.md и после этого снова выполните команду git status , то результат будет примерно следующим:
Файл CONTRIBUTING.md находится в секции «Changes not staged for commit» — это означает, что отслеживаемый файл был изменён в рабочем каталоге, но пока не проиндексирован. Чтобы проиндексировать его, необходимо выполнить команду git add . Это многофункциональная команда, она используется для добавления под версионный контроль новых файлов, для индексации изменений, а также для других целей, например для указания файлов с исправленным конфликтом слияния. Вам может быть понятнее, если вы будете думать об этом как «добавить этот контент в следующий коммит», а не как «добавить этот файл в проект». Выполним git add , чтобы проиндексировать CONTRIBUTING.md , а затем снова выполним git status :
Теперь оба файла проиндексированы и войдут в следующий коммит. В этот момент вы, предположим, вспомнили одно небольшое изменение, которое вы хотите сделать в CONTRIBUTING.md до коммита. Вы открываете файл, вносите и сохраняете необходимые изменения и вроде бы готовы к коммиту. Но давайте-ка ещё раз выполним git status :
Что за чёрт? Теперь CONTRIBUTING.md отображается как проиндексированный и непроиндексированный одновременно. Как такое возможно? Такая ситуация наглядно демонстрирует, что Git индексирует файл в точности в том состоянии, в котором он находился, когда вы выполнили команду git add . Если вы выполните коммит сейчас, то файл CONTRIBUTING.md попадёт в коммит в том состоянии, в котором он находился, когда вы последний раз выполняли команду git add , а не в том, в котором он находится в вашем рабочем каталоге в момент выполнения git commit . Если вы изменили файл после выполнения git add , вам придётся снова выполнить git add , чтобы проиндексировать последнюю версию файла:
Сокращенный вывод статуса
Вывод команды git status довольно всеобъемлющий и многословный. Git также имеет флаг вывода сокращенного статуса, так что вы можете увидеть изменения в более компактном виде. Если вы выполните git status -s или git status --short вы получите гораздо более упрощенный вывод:
Новые неотслеживаемые файлы помечены ?? слева от них, файлы добавленные в отслеживаемые помечены A , отредактированные файлы помечены M и так далее. В выводе содержится два столбца — в левом указывается статус файла, а в правом модифицирован ли он после этого. К примеру в нашем выводе, файл README модифицирован в рабочем каталоге, но не проиндексирован, а файл lib/simplegit.rb модифицирован и проиндексирован. Файл Rakefile модифицирован, проиндексирован и ещё раз модифицирован, таким образом на данный момент у него есть те изменения, которые попадут в коммит, и те, которые не попадут.
Игнорирование файлов
Зачастую, у вас имеется группа файлов, которые вы не только не хотите автоматически добавлять в репозиторий, но и видеть в списках неотслеживаемых. К таким файлам обычно относятся автоматически генерируемые файлы (различные логи, результаты сборки программ и т. п.). В таком случае, вы можете создать файл .gitignore . с перечислением шаблонов соответствующих таким файлам. Вот пример файла .gitignore :
Первая строка предписывает Git игнорировать любые файлы заканчивающиеся на «.o» или «.a» — объектные и архивные файлы, которые могут появиться во время сборки кода. Вторая строка предписывает игнорировать все файлы заканчивающиеся на тильду ( ~ ), которая используется во многих текстовых редакторах, например Emacs, для обозначения временных файлов. Вы можете также включить каталоги log, tmp или pid; автоматически создаваемую документацию; и т. д. и т. п. Хорошая практика заключается в настройке файла .gitignore до того, как начать серьёзно работать, это защитит вас от случайного добавления в репозиторий файлов, которых вы там видеть не хотите.
К шаблонам в файле .gitignore применяются следующие правила:
Стандартные шаблоны являются глобальными и применяются рекурсивно для всего дерева каталогов.
Чтобы избежать рекурсии используйте символ слеш (/) в начале шаблона.
Чтобы исключить каталог добавьте слеш (/) в конец шаблона.
Можно инвертировать шаблон, использовав восклицательный знак (!) в качестве первого символа.
Glob-шаблоны представляют собой упрощённые регулярные выражения, используемые командными интерпретаторами. Символ ( * ) соответствует 0 или более символам; последовательность [abc] — любому символу из указанных в скобках (в данном примере a, b или c); знак вопроса ( ? ) соответствует одному символу; и квадратные скобки, в которые заключены символы, разделённые дефисом ( 5 ), соответствуют любому символу из интервала (в данном случае от 0 до 9). Вы также можете использовать две звёздочки, чтобы указать на вложенные каталоги: a/**/z соответствует a/z , a/b/z , a/b/c/z , и так далее.
Вот ещё один пример файла .gitignore :
В простейшем случае репозиторий будет иметь один файл .gitignore в корневом каталоге, правила из которого будут рекурсивно применяться ко всем подкаталогам. Так же возможно использовать .gitignore файлы в подкаталогах. Правила из этих файлов будут применяться только к каталогам, в которых они находятся. Например, репозиторий исходного кода ядра Linux содержит 206 файлов .gitignore .
Детальное рассмотрение использования нескольких .gitignore файлов выходит за пределы этой книги; детали доступны в справке man gitignore .
Просмотр индексированных и неиндексированных изменений
Допустим, вы снова изменили и проиндексировали файл README , а затем изменили файл CONTRIBUTING.md без индексирования. Если вы выполните команду git status , вы опять увидите что-то вроде:
Чтобы увидеть, что же вы изменили, но пока не проиндексировали, наберите git diff без аргументов:
Эта команда сравнивает содержимое вашего рабочего каталога с содержимым индекса. Результат показывает ещё не проиндексированные изменения.
Если вы хотите посмотреть, что вы проиндексировали и что войдёт в следующий коммит, вы можете выполнить git diff --staged . Эта команда сравнивает ваши проиндексированные изменения с последним коммитом:
Важно отметить, что git diff сама по себе не показывает все изменения сделанные с последнего коммита — только те, что ещё не проиндексированы. Такое поведение может сбивать с толку, так как если вы проиндексируете все свои изменения, то git diff ничего не вернёт.
Другой пример: вы проиндексировали файл CONTRIBUTING.md и затем изменили его, вы можете использовать git diff для просмотра как проиндексированных изменений в этом файле, так и тех, что пока не проиндексированы. Если наше окружение выглядит вот так:
Используйте git diff для просмотра непроиндексированных изменений
а так же git diff --cached для просмотра проиндексированных изменений ( --staged и --cached синонимы):
Мы будем продолжать использовать команду git diff различными способами на протяжении всей книги. Существует еще один способ просматривать эти изменения, если вы предпочитаете графический просмотр или внешнюю программу просмотра различий, вместо консоли. Выполнив команду git difftool вместо git diff , вы сможете просмотреть изменения в файле с помощью таких программ как emerge, vimdiff и других (включая коммерческие продукты). Выполните git difftool --tool-help чтобы увидеть какие из них уже установлены в вашей системе.
Коммит изменений
Теперь, когда ваш индекс находится в таком состоянии, как вам и хотелось, вы можете зафиксировать свои изменения. Запомните, всё, что до сих пор не проиндексировано — любые файлы, созданные или изменённые вами, и для которых вы не выполнили git add после редактирования — не войдут в этот коммит. Они останутся изменёнными файлами на вашем диске. В нашем случае, когда вы в последний раз выполняли git status , вы видели что всё проиндексировано, и вот, вы готовы к коммиту. Простейший способ зафиксировать изменения — это набрать git commit :
Эта команда откроет выбранный вами текстовый редактор.
Редактор устанавливается переменной окружения EDITOR — обычно это vim или emacs, хотя вы можете установить любой другой с помощью команды git config --global core.editor , как было показано в главе Введение).
В редакторе будет отображён следующий текст (это пример окна Vim):
Для ещё более подробного напоминания, что же именно вы поменяли, можете передать аргумент -v в команду git commit . Это приведёт к тому, что в комментарий будет также помещена дельта/diff изменений, таким образом вы сможете точно увидеть все изменения которые вы совершили.
Есть и другой способ — вы можете набрать свой комментарий к коммиту в командной строке вместе с командой commit указав его после параметра -m , как в следующем примере:
Итак, вы создали свой первый коммит! Вы можете видеть, что коммит вывел вам немного информации о себе: на какую ветку вы выполнили коммит ( master ), какая контрольная сумма SHA-1 у этого коммита ( 463dc4f ), сколько файлов было изменено, а также статистику по добавленным/удалённым строкам в этом коммите.
Запомните, что коммит сохраняет снимок состояния вашего индекса. Всё, что вы не проиндексировали, так и висит в рабочем каталоге как изменённое; вы можете сделать ещё один коммит, чтобы добавить эти изменения в репозиторий. Каждый раз, когда вы делаете коммит, вы сохраняете снимок состояния вашего проекта, который позже вы можете восстановить или с которым можно сравнить текущее состояние.
Игнорирование индексации
Несмотря на то, что индекс может быть удивительно полезным для создания коммитов именно такими, как вам и хотелось, он временами несколько сложнее, чем вам нужно в процессе работы. Если у вас есть желание пропустить этап индексирования, Git предоставляет простой способ. Добавление параметра -a в команду git commit заставляет Git автоматически индексировать каждый уже отслеживаемый на момент коммита файл, позволяя вам обойтись без git add :
Обратите внимание, что в данном случае перед коммитом вам не нужно выполнять git add для файла CONTRIBUTING.md , потому что флаг -a включает все файлы. Это удобно, но будьте осторожны: флаг -a может включить в коммит нежелательные изменения.
Удаление файлов
Для того чтобы удалить файл из Git, вам необходимо удалить его из отслеживаемых файлов (точнее, удалить его из вашего индекса) а затем выполнить коммит. Это позволяет сделать команда git rm , которая также удаляет файл из вашего рабочего каталога, так что в следующий раз вы не увидите его как «неотслеживаемый».
Если вы просто удалите файл из своего рабочего каталога, он будет показан в секции «Changes not staged for commit» (измененные, но не проиндексированные) вывода команды git status :
Затем, если вы выполните команду git rm , удаление файла попадёт в индекс:
После следующего коммита файл исчезнет и больше не будет отслеживаться. Если вы изменили файл и уже проиндексировали его, вы должны использовать принудительное удаление с помощью параметра -f . Это сделано для повышения безопасности, чтобы предотвратить ошибочное удаление данных, которые ещё не были записаны в снимок состояния и которые нельзя восстановить из Git.
Другая полезная штука, которую вы можете захотеть сделать — это удалить файл из индекса, оставив его при этом в рабочем каталоге. Другими словами, вы можете захотеть оставить файл на жёстком диске, но перестать отслеживать изменения в нём. Это особенно полезно, если вы забыли добавить что-то в файл .gitignore и по ошибке проиндексировали, например, большой файл с логами, или кучу промежуточных файлов компиляции. Чтобы сделать это, используйте опцию --cached :
В команду git rm можно передавать файлы, каталоги или шаблоны. Это означает, что вы можете сделать что-то вроде:
Обратите внимание на обратный слеш ( \ ) перед * . Он необходим из-за того, что Git использует свой собственный обработчик имён файлов вдобавок к обработчику вашего командного интерпретатора. Эта команда удаляет все файлы, имеющие расширение .log и находящиеся в каталоге log/ . Или же вы можете сделать вот так:
Эта команда удаляет все файлы, имена которых заканчиваются на ~ .
Перемещение файлов
В отличие от многих других систем контроля версий, Git не отслеживает перемещение файлов явно. Когда вы переименовываете файл в Git, в нём не сохраняется никаких метаданных, говорящих о том, что файл был переименован. Однако, Git довольно умён в плане обнаружения перемещений постфактум — мы рассмотрим обнаружение перемещения файлов чуть позже.
Таким образом, наличие в Git команды mv выглядит несколько странным. Если вам хочется переименовать файл в Git, вы можете сделать что-то вроде:
и это отлично сработает. На самом деле, если вы выполните что-то вроде этого и посмотрите на статус, вы увидите, что Git считает, что произошло переименование файла:
Однако, это эквивалентно выполнению следующих команд:
Git неявно определяет, что произошло переименование, поэтому неважно, переименуете вы файл так или используя команду mv . Единственное отличие состоит лишь в том, что mv — одна команда вместо трёх — это функция для удобства. Важнее другое — вы можете использовать любой удобный способ для переименования файла, а затем воспользоваться командами add/rm перед коммитом.
Читайте также: