четверг, 5 февраля 2015 г.

Основы Git - Запись изменений в репозиторий

Итак, у вас имеется настоящий Git-репозиторий и рабочая копия файлов для некоторого проекта. Вам нужно делать некоторые изменения и фиксировать “снимки” состояния (snapshots) этих изменений в вашем репозитории каждый раз, когда проект достигает состояния, которое вам хотелось бы сохранить.

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

Отслеживаемые файлы — это те файлы, которые были в последнем слепке состояния проекта (snapshot); они могут быть неизменёнными, изменёнными или подготовленными к коммиту (staged).

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

Когда вы впервые клонируете репозиторий, все файлы будут отслеживаемыми и неизменёнными, потому что вы только взяли их из хранилища (checked them out) и ничего пока не редактировали.

Как только вы отредактируете файлы, Git будет рассматривать их как изменённые, т.к. вы изменили их с момента последнего коммита. Вы индексируете (stage) эти изменения и затем фиксируете все индексированные изменения, а затем цикл повторяется. Этот жизненный цикл изображён на рисунке

lifecycle

 

Определение состояния файлов

Основной инструмент, используемый для определения, какие файлы в каком состоянии находятся — это команда git status. Если вы выполните эту команду сразу после клонирования, вы увидите что-то вроде этого:

$ git status
# On branch master
nothing to commit, working directory clean

ggg0001

Это означает, что у вас чистый рабочий каталог, другими словами — в нём нет отслеживаемых изменённых файлов. Git также не обнаружил неотслеживаемых файлов, в противном случае они бы были перечислены здесь. И наконец, команда сообщает вам на какой ветке (branch) вы сейчас находитесь. Пока что это всегда ветка master — это ветка по умолчанию.

Создадим в каталоге TxT2 файлик MyNewTextFile.txt и снова дадим команду git status

ggg0002

Понять, что новый файл MyNewTextFile.txt неотслеживаемый можно по тому, что он находится в секции "Untracked files" в выводе команды status. Статус "неотслеживаемый файл", по сути, означает, что Git видит файл, отсутствующий в предыдущем снимке состояния (коммите); Git не станет добавлять его в ваши коммиты, пока вы его явно об этом не попросите. Это предохранит вас от случайного добавления в репозиторий сгенерированных бинарных файлов или каких-либо других, которые вы и не думали добавлять.

Мы хотим добавить MyNewTextFile.txt в отслеживаемые файлы. Так давайте сделаем это.

Отслеживание новых файлов

Для того чтобы начать отслеживать (добавить под версионный контроль) новый файл, используется команда git add. Чтобы начать отслеживание файла MyNewTextFile.txt , вы можете выполнить следующее:

git add MyNewTextFile.txt

Если вы снова выполните команду status, то увидите, что файл MyNewTextFile.txt теперь отслеживаемый и индексированный:

ggg0003

Вы можете видеть, что файл проиндексирован по тому, что он находится в секции “Changes to be committed”. Если вы выполните коммит в этот момент, то версия файла, существовавшая на момент выполнения вами команды git add, будет добавлена в историю снимков состояния.

Команда git add принимает параметром путь к файлу или каталогу, если это каталог, команда рекурсивно добавляет (индексирует) все файлы в данном каталоге.

Индексация изменённых файлов

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

ggg0004

Git отследил, что в новом файле который мы добавили под версионный контроль произошли изменения.

Файл MyNewTextFile.txt находится в секции “Changes not staged for commit” — это означает, что отслеживаемый файл был изменён в рабочем каталоге, но пока не проиндексирован. Чтобы проиндексировать его, необходимо выполнить команду git add (это многофункциональная команда, она используется для добавления под версионный контроль новых файлов, для индексации изменений, а также для других целей, например для указания файлов с исправленным конфликтом слияния). Выполним git add, чтобы проиндексировать MyNewTextFile.txt, а затем снова выполним git status:

ggg0005

Теперь файл проиндексирован и войдет в следующий коммит. В этот момент вы, предположим, вспомнили одно небольшое изменение, которое вы хотите сделать в MyNewTextFile.txt до фиксации. Вы открываете файл, вносите и сохраняете необходимые изменения и вроде бы готовы к коммиту. Но давайте-ка ещё раз выполним git status:

ggg0006

Что за чёрт? Теперь MyNewTextFile.txt отображается как проиндексированный и непроиндексированный одновременно. Как такое возможно? Такая ситуация наглядно демонстрирует, что Git индексирует файл в точности в том состоянии, в котором он находился, когда вы выполнили команду git add. Если вы выполните коммит сейчас, то файл MyNewTextFile.txt попадёт в коммит в том состоянии, в котором он находился, когда вы последний раз выполняли команду git add, а не в том, в котором он находится в вашем рабочем каталоге в момент выполнения git commit. Если вы изменили файл после выполнения git add, вам придётся снова выполнить git add, чтобы проиндексировать последнюю версию файла:

ggg0007

Краткий статус

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

ggg0008

Давайте внесем какие-нибудь изменения в MyNewTextFile.txt и добавим в каталог проекта новый файл LICENSE.txt и дадим команду git status –s

ggg0009

Новый файл который не находится под версионным контролем, то есть является (untracked) не отслеживаемым помечен двумя вопросительными знаками ?? До этого мы видели что файл MyNewTextFile.txt был помечен буквой A поскольку тогда он находился в staging area, то есть изменения в нем были проиндексированы, сейчас же он имеет статус AM, то есть он был модифицирован и проиндексирован, но этот индекс не был сохранен командой git add.

В индикаторах состояния мы видим две буквы (верней две колонки), левая колонка обозначает что файл был проиндексирован, а правая что он был модифицирован. В нашем примере мы видим, что файл MyNewTextFile.txt был модифицирован в рабочей директории, но еще не был снова проиндексирован.

Игнорирование файлов

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

$ cat .gitignore
*.[oa]
*~

Первая строка предписывает Git'у игнорировать любые файлы заканчивающиеся на .o или .a — объектные и архивные файлы, которые могут появиться во время сборки кода. Вторая строка предписывает игнорировать все файлы заканчивающиеся на тильду (~), которая используется во многих текстовых редакторах, например Emacs, для обозначения временных файлов. Вы можете также включить каталоги log, tmp или pid; автоматически создаваемую документацию; и т.д. и т.п. Хорошая практика заключается в настройке файла .gitignore до того, как начать серьёзно работать, это защитит вас от случайного добавления в репозиторий файлов, которых вы там видеть не хотите.

К шаблонам в файле .gitignore применяются следующие правила:

  • Пустые строки, а также строки, начинающиеся с #, игнорируются.
  • Можно использовать стандартные glob шаблоны.
  • Можно заканчивать шаблон символом слэша (/) для указания каталога.
  • Можно инвертировать шаблон, использовав восклицательный знак (!) в качестве первого символа.

Glob-шаблоны представляют собой упрощённые регулярные выражения используемые командными интерпретаторами. Символ * соответствует 0 или более символам; последовательность [abc] — любому символу из указанных в скобках (в данном примере a, b или c); знак вопроса (?) соответствует одному символу; [0-9] соответствует любому символу из интервала (в данном случае от 0 до 9).

# комментарий — эта строка игнорируется
# не обрабатывать файлы, имя которых заканчивается на .a
*.a
# НО отслеживать файл lib.a, несмотря на то, что мы игнорируем все .a файлы с помощью предыдущего правила
!lib.a
# игнорировать только файл TODO находящийся в корневом каталоге, не относится к файлам вида subdir/TODO
/TODO
# игнорировать все файлы в каталоге build/
build/
# игнорировать doc/notes.txt, но не doc/server/arch.txt
doc/*.txt
# игнорировать все .txt файлы в каталоге doc/
doc/**/*.txt

Шаблон **/ доступен в Git, начиная с версии 1.8.2.

Теперь посмотрим все это на примере. Создадим файл .gitignore в нашем проекте

$ touch .gitignore

И посмотрим статус проекта

Git00001

Мы увидели файл .gitignore в не отслеживаемых файлах. Теперь добавим сам файл .gitignore в игнорируемые. Для этого добавлем строчку .gitignore в файл .gitignore – это магия, не иначе. И смотрим статус.

Git00002

Видим, что файл .gitignore теперь не отображается, хотя он продолжает физически присутствовать в каталоге проекта. То есть он игнорируется Git’ом и не будет включаться ни какие коммиты и даже более того, не будет отображаться в выводе команд git status  и ls.

Теперь добавим в файл .gitignore файловую маску LIC*.* , которая так же добавит файл LICENSE.txt в игнорируемые и посмотрим статус.

Git00003

Теперь файл LICENSE.txt тоже у нас в игнорируемых.

Посмотрим содержимое нашего файла .gitignore

Git00004

Использование файла .gitignore это один из способов добавить файлы в игнорируемые Git’ом для конкретного проекта, то есть проекта в текущем каталоге Git. Это можно сделать так же используя файл .git/info/exclude. Он по умолчанию создается при создании репозитария Git (команда git init). Правила добавления в него масок для файлов точно такие же как были описаны выше.

Давайте удалим файл .gitignore и дадим команду git status

Git00017

Git снова увиделл файл LICENSE.txt. Теперь добавим строчку LIC*.* в файл .git/info/exclude. И снова посмотрим статус.

Git00018

Git снова перестал видеть файл LICENSE.txt. Еще раз повторюсь, что данный способ работает для только для текущего проекта (папки) git.

Есть так же способ задать игнорирование файлов более глобально. То есть для всех проектов Git’a. Чтобы посмотреть этот способ сперва уберем строчку LIC*.* в файле .git/info/exclude. И снова посмотрим статус.

Git00019

Теперь Git снова показывает LICENSE.txt в не отслеживаемых файлах.

Далее даем команду

$ touch ~/.gitignore

Эта команда создаст файл .gitignore в домашнем каталоге пользователя (например C:\Users\<USER_NAME>\.gitignore)

Далее добавляем в этот файл нашу строчку LIC*.* для игнорирования LICENSE.txt

И прописываем этот файл (.gitignore) в глобальный config (.gitconfig), чтобы git его видел и использовал.

$ git config --global core.excludesfile ~/.gitignore

Данная команда добавляет в файл ~/.gitconfig в раздел [core] строчку

excludesfile = c:/Users/Pilgrim/.gitignore

Как вы понимаете, название файла списка игнорируемых гитом файлов, вы можете выбрать любое, какое вам нравится. Главное чтобы этот файл был прописан в файле глобального конфига ~/.gitconfig. Я выбрал .gitignore, но вы можете назвать его по другому, например gitexclude и т.д и т.п.

В принципе все эти операции можно проделать в ручную: создать самим файл .gitignore в домашнем каталоге пользователя и самим отредактировать файл .gitconfig там же.

Смотрим статус и видим что Git опять не видит наш файл LICENSE.txt

Git00020

После данной операции все файлы попадающие под маску LIC*.* во всех проектах Git текущего пользователя Windows будут игнорироваться. Это удобно если надо игнорировать временные или бинарные файлы в различных проектах. Например файлы *.apk, *.dex и т.д и т.п.

Просмотр индексированных и неиндексированных изменений

Если результат работы команды git status недостаточно информативен для вас — вам хочется знать, что конкретно поменялось, а не только какие файлы были изменены — вы можете использовать команду git diff. Позже мы рассмотрим команду git diff подробнее; вы, скорее всего, будете использовать эту команду для получения ответов на два вопроса: что вы изменили, но ещё не проиндексировали, и что вы проиндексировали и собираетесь фиксировать. Если git status отвечает на эти вопросы слишком обобщённо, то git diff показывает вам непосредственно добавленные и удалённые строки — собственно заплатку (patch).

Дадим команду git diff

Git00005

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

Если вы хотите посмотреть, что вы проиндексировали и что войдёт в следующий коммит, вы можете выполнить git diff --cached. (В Git'е версии 1.6.1 и выше, вы также можете использовать git diff --staged, которая легче запоминается.) Эта команда сравнивает ваши индексированные изменения с последним коммитом:

Git00006

Важно отметить, что git diff сама по себе не показывает все изменения сделанные с последнего коммита — а только те, что ещё не проиндексированы. Такое поведение может сбивать с толку, так как если вы проиндексируете все свои изменения, то git diff ничего не вернёт.

Git00007

Другой пример: мы уже проиндексировали файл MyNewTextFile.txt и затем изменили его. Теперь мы можем использовать git diff для просмотра как индексированных изменений в этом файле, так и тех, что пока не проиндексированы:

Git00008

Смотрим не индексированные изменения командой git diff

Git00009

а также уже проиндексированные, используя git diff --cached:

Git00010

Фиксация изменений

Теперь, когда ваш индекс настроен так, как вам и хотелось, вы можете зафиксировать свои изменения. Запомните, всё, что до сих пор не проиндексировано — любые файлы, созданные или изменённые вами, и для которых вы не выполнили git add после момента редактирования — не войдут в этот коммит. Они останутся изменёнными файлами на вашем диске.

Дадим команду git status

Git00011

И добавим изменения в индекс и снова посмотрим статус

Git00012

Видим что всё проиндексировано, и вот, мы готовы к коммиту. Простейший способ зафиксировать изменения — это набрать git commit

Git00013

Эта команда откроет выбранный вами текстовый редактор. (Редактор устанавливается системной переменной $EDITOR (или переменной окружения Windows  GIT_EDITOR) — обычно это vim или emacs, хотя вы можете установить ваш любимый с помощью команды git config --global core.editor, как я писал раньше я установил Notepad++).

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

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

Git00014

После сохранения комментария и закрытия окна редактора мы увидим что мы сделали коммит и наш комментарий к нему. Ну и комментарий Git’a в данном случае про CRLF.

Для ещё более подробного напоминания, что же именно вы поменяли, можете передать аргумент -v в команду git commit. Это приведёт к тому, что в комментарий будет также помещена дельта diff изменений, таким образом вы сможете точно увидеть всё, что сделано.

Git00022

Вот такой комментарий был сгенерирован при использовании ключа –v

Git00021

Есть и другой способ — вы можете набрать свой комментарий к коммиту в командной строке вместе с командой commit, указав его после параметра -m, как в следующем примере:

Git00023

И так мы уже сделали несколько коммитов. Вы можете видеть, что коммит выводит вам немного информации о себе: на какую ветку вы выполнили коммит (master), какая контрольная сумма SHA-1 у этого коммита (bd0b39e), сколько файлов было изменено, а также статистику по добавленным/удалённым строкам в этом коммите.

Запомните, что коммит сохраняет снимок состояния вашего индекса. Всё, что вы не проиндексировали, так и торчит в рабочем каталоге как изменённое; вы можете сделать ещё один коммит, чтобы добавить эти изменения в репозиторий. Каждый раз, когда вы делаете коммит, вы сохраняете снимок состояния вашего проекта, который позже вы можете восстановить или с которым можно сравнить текущее состояние.

Игнорирование индексации

Несмотря на то, что индекс может быть удивительно полезным для создания коммитов именно такими, как вам и хотелось, он временами несколько сложнее, чем вам нужно в процессе работы. Если у вас есть желание пропустить этап индексирования, Git предоставляет простой способ. Добавление параметра -a в команду git commit заставляет Git автоматически индексировать каждый уже отслеживаемый на момент коммита файл, позволяя вам обойтись без git add:

Git00024

Обратите внимание на то, что в данном случае перед коммитом не пришлось выполнять git add.

Удаление файлов

Чтобы показать примеры данного раздела я создал файл FileForDelete.txt. Добавил его в индекс и закоммитил.

Примечание: Для демонстрации примеров в этом разделе я несколько раз восстанавливал удаляемый файл (FileForDelete.txt) из базы Git, но не приводил скриншотов дабы сэкономить время. Учитывайте это при чтении материала.

Для того чтобы удалить файл из Git'а, вам необходимо удалить его из отслеживаемых файлов (точнее, удалить его из вашего индекса) а затем выполнить коммит. Это позволяет сделать команда git rm, которая также удаляет файл из вашего рабочего каталога, так что вы в следующий раз не увидите его как “неотслеживаемый”.

Если вы просто удалите файл из своего рабочего каталога, он будет показан в секции “Changes not staged for commit” (“Изменённые но не обновлённые” — читай не проиндексированные) вывода команды git status:

Git00025

В данном выводе я просто удалил файл FileForDelete.txt из каталога проекта. Теперь верну его обратно и посмотрим статус

Git00026

Теперь все хорошо и можно будет использовать команду git rm:

Git00027

При выполнение команды git rm, удаление файла попадёт в индекс. Что и видно из скриншота.

После следующего коммита файл исчезнет из каталога проекта и больше не будет отслеживаться.

Git00028

Но это не означает что его не будет в базе Git’a. Все предыдущие версии удаленного файла будут продолжать хранится в базе Git. То есть все версии этого файла, до момента его удаления можно будет достать и посмотреть из хранилища Git.

Команда git rm удаляет файл из текущего каталога проекта и Git его больше не отслеживает, но хранит все предыдущие версии.

Если вы изменили уже проиндексированный файл, но не добавили его командой git add снова в индекс и хотите его удалить, то вы должны использовать принудительное удаление с помощью параметра -f. Это сделано для повышения безопасности, чтобы предотвратить ошибочное удаление данных, которые ещё не были записаны в снимок состояния (индекс) и которые нельзя восстановить из Git'а.

Например, наш файл FileForDelete.txt уже был закоммичен и мы его изменили, посмотрели статус и захотели удалить командой git rm, но git не позволит этого сделать и выведет предупреждение.

Git00029

Теперь используем ключ –f

Git00030

После этого файл будет удален из каталога и не будет отслеживаться. Дадим команду git commit и git status

Git00031

Наши изменения были зафиксированы.

Другая полезная штука, которую вы можете захотеть сделать — это удалить файл из индекса, оставив его при этом в рабочем каталоге. Другими словами, вы можете захотеть оставить файл на винчестере, и убрать его из-под бдительного ока Git'а. Это особенно полезно, если вы забыли добавить что-то в файл .gitignore и по ошибке проиндексировали, например, большой файл с логами, или кучу промежуточных файлов компиляции. Чтобы сделать это, используйте опцию --cached:

$ git rm --cached FileForDelete.txt

Дадим эту команду и посмотрим статус

Git00032

Как видим Git увидел изменения которые мы внесли, но чтобы их зафиксировать надо дать команду

$ git commit

Git00033

Посмотрим теперь статус

Git00034

Файл FileForDelete.txt теперь у нас находится в не отслеживаемых. Если мы теперь его удалим средствами ОС или перенесем в другой каталог и опять посмотрим статус, то увидим что теперь у нас нечего коммитить и рабочая директория “чиста”, в смысле что там есть файлы но изменений в них нет.

Git00035

Но даже при таком раскладе, файл FileForDelete.txt все равно можно вытащить из базы Git. Другое дело что он больше туда не будет записываться.

Поэтому очень важно перед началом работы с Git правильно настроить игнорирование файлов, которые вы не хотите чтобы включались в базу коммитов Git.

В команду git rm можно передавать файлы, каталоги или glob-шаблоны. Это означает, что вы можете вытворять что-то вроде:

$ git rm log/\*.log

Обратите внимание на обратный слэш (\) перед *. Он необходим из-за того, что Git использует свой собственный обработчик имён файлов вдобавок к обработчику вашего командного интерпретатора. Эта команда удаляет все файлы, которые имеют расширение .log в каталоге log/. Или же вы можете сделать вот так:

$ git rm \*~

Эта команда удаляет все файлы, чьи имена заканчиваются на ~.

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

В отличие от многих других систем версионного контроля, Git не отслеживает перемещение файлов явно. Когда вы переименовываете файл в Git'е, в нём не сохраняется никаких метаданных, говорящих о том, что файл был переименован. Однако, Git довольно умён в плане обнаружения перемещений постфактум — мы рассмотрим обнаружение перемещения файлов чуть позже.

Таким образом, наличие в Git'е команды mv выглядит несколько странным. Если вам хочется переименовать файл в Git'е, вы можете сделать что-то вроде:

$ git mv FileForDelete.txt FileForMove.txt

и это отлично сработает. На самом деле, если вы выполните что-то вроде этого и посмотрите на статус, вы увидите, что Git считает, что произошло переименование файла:

Git00036

Однако, это эквивалентно выполнению следующих команд:

$ mv FileForDelete.txt FileForMove.txt
$ git rm FileForDelete.txt
$ git add FileForMove.txt

Если мы закоммитим данные изменения то увидим следующее

Git00037

Git неявно определяет, что произошло переименование, поэтому неважно, переименуете вы файл так или используя команду mv. Единственное отличие состоит лишь в том, что mv — это одна команда вместо трёх — это функция для удобства. Важнее другое — вы можете использовать любой удобный способ, чтобы переименовать файл, и затем воспользоваться add/rm перед коммитом.

Теперь попробуем переименовать файлик FileForMove.txt в FileForRename.txt  средствами ОС и посмотрим, что нам на это скажет Git.

Git00038

Git увидел, что тот файл который он отслеживал FileForMove.txt был удален, и что появился файл FileForRename.txt, который он не отслеживает.

Кроме того Git как бы вроде догадался, что мы переименовали файл и предлагает нам использовать команды git add/rm и git checout -- <file>…

Если мы сейчас дадим команду git add . то увидим как Git ругнется на такое поведение но все же поймет что произошло.

Git00039

Посмотрим статус

Git00040

И так Git нам говорит что FileForRename находится в индексе и будет закоммичен при следующем коммите. Однако изменения с файлом FileForMove.txt не находятся в индексе и нам рекомендуется дать команды git add/rm или git checout. Используем git rm и посмотрим статус

Git00041

После этой команды Git таки догадался что мы переименовали файл FileForMove.txt в файл FileForRename.txt и запихал эти изменения в индекс, подготовив их к коммиту. Закоммитем же эти изменения!

Git00042

Комментариев нет:

Отправка комментария