Итак, у вас имеется настоящий репозиторий Git и рабочая копия файлов для некоторого проекта. Вам нужно делать некоторые изменения и фиксировать “снимки” состояния (snapshots) этих изменений в вашем репозитории каждый раз, когда проект достигает состояния, которое вам хотелось бы сохранить.
Запомните, каждый файл в вашем рабочем каталоге может находиться в одном из двух состояний: под версионным контролем (отслеживаемые) и нет (неотслеживаемые). Отслеживаемые файлы — это те файлы, которые были в последнем слепке состояния проекта (snapshot); они могут быть неизмененными, измененными или подготовленными к коммиту (staged). Неотслеживаемые файлы — это всё остальное, любые файлы в вашем рабочем каталоге, которые не входили в ваш последний слепок состояния и не подготовлены к коммиту. Когда вы впервые клонируете репозиторий, все файлы будут отслеживаемыми и неизмененные, потому что вы только взяли их из хранилища (checked them out) и ничего пока не редактировали.
Как только вы отредактируете файлы, Git будет рассматривать их как измененные, т.к. вы изменили их с момента последнего коммита. Вы индексируете (stage) эти изменения и затем фиксируете все индексированные изменения, а затем цикл повторяется. Этот жизненный цикл изображен на Рисунке 2-1.

Рисунок 2-1. Жизненный цикл состояния ваших файлов
Определение состояния файлов
Основной инструмент, используемый для определения какие файлы в каком состоянии находятся — это командаgit status. Если вы выполните эту команду сразу после клонирования, вы увидите что-то вроде этого:
$ git status
# On branch master
nothing to commit (working directory clean)
Это означает, что у вас чистый рабочий каталог, другими словами — в нем нет отслеживаемых измененных файлов. Git также не обнаружил неотслеживаемых файлов, в противном случае они бы были перечислены здесь. И наконец, команда сообщает вам на какой ветке (branch) вы сейчас находитесь. Пока что это всегда ветка master — это ветка по умолчанию; в этой главе это не важно. В следующей главе будет подробно рассказано про ветки и ссылки.
Предположим, вы добавили новый файл в ваш проект, простой README файл. Если этого файла раньше не было, и вы выполните git status, вы увидите неотслеживаемый файл как-то так:
$ vim README
$ git status
# On branch master
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# README
nothing added to commit but untracked files present (use "git add" to track)
Вы можете видеть, что новый файл README неотслеживаемый, т.к. он находится в секции “Untracked files” в выводе команды status. Неотслеживаемый файл обычно означает, что Git нашел файл, отсутствующий в предыдущем снимке состояния (коммите); Git не станет добавлять его в ваши коммиты, пока вы явно ему это не укажете. Это предохраняет вас от случайного добавления в репозиторий сгенерированных двоичных файлов или каких-либо других, которые вы и не думали добавлять. Вы хотите добавить README, так что давайте сделаем это.
Отслеживание новых файлов
Для того чтобы начать отслеживать (добавить под версионный контроль) новый файл, используется командаgit add. Чтобы начать отслеживание файла README, вы можете выполнить следующее:
$ git add README
Если вы снова выполните команду status, то увидите, что файл README теперь отслеживаемый и индексированный:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
#
Вы можете видеть, что файл проиндексирован по тому, что он находится в секции “Changes to be committed”. Если вы выполните коммит в этот момент, то версия файла, существовавшая на момент выполнения вами команды git add, будет добавлена в историю снимков состояния. Как вы помните, когда вы ранее выполнили git init, вы затем выполнили git add (files) — это было сделано для того, чтобы добавить файлы в вашем каталоге под версионный контроль. Команда git add принимает параметром путь к файлу или каталогу, если это каталог, команда рекурсивно добавляет (индексирует) все файлы в данном каталоге.
Индексация изменённых файлов
Давайте модифицируем файл уже находящийся под версионным контролем. Если вы измените отслеживаемый файлbenchmarks.rb, и после этого снова выполните команду status, то результат будет примерно следующим:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
#
# modified: benchmarks.rb
#
Файл benchmarks.rb находится в секции “Changed but not updated” — это означает, что отслеживаемый файл был изменен в рабочем каталоге, но пока не проиндексирован. Чтобы проиндексировать его, необходимо выполнить команду git add (это многофункциональная команда, она используется для добавления под версионный контроль новых файлов, для индексации изменений, а также для других целей, например для указания файлов с исправленным конфликтом слияния). Выполним git add, чтобы проиндексировать benchmarks.rb, а затем снова выполним git status:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
Теперь оба файла проиндексированы и войдут в следующий коммит. В этот момент, вы, предположим, вспомнили одно небольшое изменение, которое вы хотите сделать в benchmarks.rb до фиксации. Вы открываете файл, вносите и сохраняете необходимые изменения и вроде бы готовы к коммиту. Но давайте-ка еще раз выполним git status:
$ vim benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
#
# modified: benchmarks.rb
#
Что за черт? Теперь benchmarks.rb отображается как проиндексированный и непроиндексированный одновременно. Как такое возможно? Такая ситуация наглядно демонстрирует, что Git индексирует файл в точности в том состоянии, в котором он находился, когда вы выполнили команду git add. Если вы выполните коммит сейчас, то файл benchmarks.rb попадет в коммит в том состоянии, в котором он находился, когда вы последний раз выполняли команду git add, а не в том, в котором он находится в вашем рабочем каталоге в момент выполнения git commit. Если вы изменили файл после выполнения git add, вам придется снова выполнить git add, чтобы проиндексировать последнюю версию файла:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
Игнорирование файлов
Зачастую, у вас имеется группа файлов, которые вы не только не хотите автоматически добавлять в репозиторий, но и видеть в списках неотслеживаемых. К таким файлам обычно относятся автоматически генерируемые файлы (различные логи, результаты сборки программ и т.п.). В таком случае, вы можете создать файл.gitignore с перечислением шаблонов соответствующих таким файлам. Вот пример файла .gitignore:
$ cat .gitignore
*.[oa]
*~
Первая строка предписывает Git-у игнорировать любые файлы заканчивающиеся на .o или .a — объектные и архивные файлы, которые могут появиться во время сборки кода. Вторая строка предписывает игнорировать все файлы заканчивающиеся на тильду (~), которая используется во многих текстовых редакторах, например Emacs, для обозначения временных файлов. Вы можете также включить каталоги log, tmp или pid; автоматически создаваемую документацию; и т.д. и т.п. Хорошая практика заключается в настройке файла .gitignore до того, как начать серьезно работать, это защитит вас от случайного добавления в репозиторий файлов, которых вы там видеть не хотите.
К шаблонам в файле .gitignore применяются следующие правила:
- Пустые строки, а также строки начинающиеся с
#игнорируются. - Можно использовать стандартные
globшаблоны. - Можно заканчивать шаблон символом слэша (
/) для указания каталога. - Можно инвертировать шаблон, использовав восклицательный знак (
!) в качестве первого символа.
* соответствует 0 или более символам; последовательность [abc] — любому символу из указанных в скобках (в данном примере a, b или c); знак вопроса (?) соответствует одному символу; [0-9] соответствует любому символу из интервала (в данном случае от 0 до 9).
Вот еще один пример файла .gitignore:
# комментарий – эта строка игнорируется
*.a # не обрабатывать файлы, имя которых заканчивается на .a
!lib.a # НО отслеживать файл lib.a, несмотря на то, что мы игнорируем все .a файлы с помощью предыдущего правила
/TODO # игнорировать только файл TODO находящийся в корневом каталоге, не относится к файлам вида subdir/TODO
build/ # игнорировать все файлы в каталоге build/
doc/*.txt # игнорировать doc/notes.txt, но не doc/server/arch.txt
Просмотр индексированных и неиндексированных изменений
Если результат работы командыgit status недостаточно информативен для вас — вам хочется знать, что конкретно поменялось, а не только какие файлы были изменены — вы можете использовать команду git diff. Позже мы рассмотрим команду git diff подробнее; вы, скорее всего, будете использовать эту команду для получения ответов на два вопроса: что вы изменили но еще не проиндексировали, и что вы проиндексировали и собираетесь фиксировать. Если git status отвечает на эти вопросы слишком обобщенно, то git diff показывает вам непосредственно добавленные и удаленные строки — собственно заплатку (patch).
Допустим, вы снова изменили и проиндексировали файл README, а затем изменили файл benchmarks.rb без индексирования. Если вы выполните команду status, вы опять увидите что-то вроде:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
#
# modified: benchmarks.rb
#
Чтобы увидеть, что же вы изменили, но пока не проиндексировали, наберите git diff без аргументов:
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size
Эта команда сравнивает содержимое вашего рабочего каталога с содержимым индекса. Результат показывает еще не проиндексированные изменения.
Если вы хотите посмотреть, что вы проиндексировали и что войдет в следующий коммит, вы можете выполнить git diff --cached. (В Git версии 1.6.1 и выше, вы также можете использовать git diff --staged, которая легче запоминается.) Эта команда сравнивает ваши индексированные изменения с последним коммитом:
$ git diff --cached
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README2
@@ -0,0 +1,5 @@
+grit
+ by Tom Preston-Werner, Chris Wanstrath
+ http://github.com/mojombo/grit
+
+Grit is a Ruby library for extracting information from a Git repository
Важно отметить, что git diff сама по себе не показывает все изменения сделанные с последнего коммита — только те, что еще не проиндексированы. Такое поведение может сбивать с толку, так как если вы проиндексируете все свои изменения, то git diff ничего не вернет.
Другой пример, вы проиндексировали файл benchmarks.rb и затем изменили его, вы можете использовать git diff для просмотра как индексированных изменений в этом файле, так и тех, что пока не проиндексированны:
$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
# On branch master
#
# Changes to be committed:
#
# modified: benchmarks.rb
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
Теперь вы можете используя git diff посмотреть непроиндексированные изменения
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
main()
##pp Grit::GitRuby.cache_client.stats
+# test line
а также уже проиндексированные, используя git diff --cached:
$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size
Фиксация изменений
Теперь, когда ваш индекс настроен так как вам и хотелось, вы можете зафиксировать ваши изменения. Запомните, всё, что до сих пор не проиндексировано — любые файлы, созданные или измененные вами, и для которых вы не выполнилиgit add после момента редактирования — не войдут в этот коммит. Они останутся измененными файлами на вашем диске. В нашем случае, когда вы в последний раз выполняли git status, вы видели что все проиндексировано, и вот, вы готовы к коммиту. Простейший способ зафиксировать ваши изменения — это набрать git commit:
$ git commit
Эта команда откроет выбранный вами текстовый редактор. (Редактор устанавливается системной переменной $EDITOR — обычно это vim или emacs, хотя вы можете установить ваш любимый с помощью команды git config --global core.editor как было показано в Главе Введение).
В редакторе будет отображен следующий текст (это пример окна Vim-а):
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: README
# modified: benchmarks.rb
~
~
~
".git/COMMIT_EDITMSG" 10L, 283C
Вы можете видеть, что комментарий по умолчанию для коммита содержит закомментированный результат работы (“выхлоп”) команды git status и ещё одну пустую строку сверху. Вы можете удалить эти комментарии и набрать ваше сообщение или же оставить их для напоминания того, что вы фиксируете. (Для еще более подробного напоминания, что же вы именно меняли, вы можете передать аргумент -v в команду git commit. Это приведет к тому, что в комментарий будет помещена также разница/diff ваших изменений, таким образом вы сможете точно увидеть всё что сделано.) Когда вы выходите из редактора, Git создает ваш коммит с этим сообщением (удаляя комментарии и вывод diff-а).
Другой способ — вы можете набрать ваш комментарий к коммиту в командной строке вместе с командой commit указав его после параметра -m, как в следующем примере:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master]: created 463dc4f: "Fix benchmarks for speed"
2 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 README
Итак, вы создали свой первый коммит! Вы можете видеть, что коммит вывел вам немного информации о себе: на какую ветку вы выполнили коммит (master), какая контрольная сумма SHA-1 у этого коммита (463dc4f), сколько файлов было изменено, а также статистику по добавленным/удаленным строкам в этом коммите.
Запомните, что коммит сохраняет снимок состояния вашего индекса. Все, что вы не проиндексировали, так и торчит в рабочем каталоге как измененное; вы можете сделать еще один коммит, чтобы добавить эти изменения в репозиторий. Каждый раз, когда вы делаете коммит, вы сохраняете снимок состояния вашего проекта, который позже вы можете восстановить, или, с которым можно сравнить текущее состояние.
Игнорирование индексации
Несмотря на то, что индекс может быть удивительно полезным для создания коммитов именно такими вам и хотелось, он временами несколько сложнее, чем вам нужно в процессе работы. Если у вас есть желание пропустить этап индексирования, Git предоставляет простой способ. Добавление параметра-a в команду git commit заставляет Git автоматически индексировать каждый уже отслеживаемый на момент коммита файл, позволяя вам обойтись без git add:
$ git status
# On branch master
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
1 files changed, 5 insertions(+), 0 deletions(-)
Обратите внимание на то, что в данном случае перед коммитом вам не нужно выполнять git add для файла benchmarks.rb.
Удаление файлов
Для того чтобы удалить файл из Git, вам необходимо удалить его из отслеживаемых файлов (точнее, удалить его из вашего индекса) а затем выполнить коммит. Это позволяет сделать командаgit rm, которая также удаляет файл из вашего рабочего каталога, так что вы в следующий раз не увидите его как “неотслеживаемый”.
Если вы просто удалите файл из вашего рабочего каталога, он будет показан в секции “Changed but not updated” (“Измененные но не обновленные” — читай не проиндексированные) вывода команды git status:
$ rm grit.gemspec
$ git status
# On branch master
#
# Changed but not updated:
# (use "git add/rm ..." to update what will be committed)
#
# deleted: grit.gemspec
#
Затем, если вы выполните команду git rm, удаление файла попадёт в индекс:
$ git rm grit.gemspec
rm 'grit.gemspec'
$ git status
# On branch master
#
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# deleted: grit.gemspec
#
После следующего коммита, файл исчезнет и больше не будет отслеживаться. Если вы изменили файл и уже проиндексировали его, вы должны использовать принудительное удаление с помощью параметра -f. Это сделано для повышения безопасности, чтобы предотвратить ошибочное удаление данных, которые ещё не были записаны в снимок состояния и которые нельзя восстановить из Git.
Другая полезная штука, которую вы можете захотеть сделать — это удалить файл из индекса, оставив его при этом в вашем рабочем каталоге. Другими словами, вы можете захотеть оставить файл на вашем винчестере, и убрать его из-под бдительного ока Git-а. Это особенно полезно, если вы забыли добавить что-то в ваш файл .gitignore и по ошибке проиндексировали, например, большой файл с логами, или кучу промежуточных файлов компилляции. Чтобы сделать это, используйте опцию --cached:
$ git rm --cached readme.txt
В команду git rm вы можете передавать файлы, каталоги или glob-шаблоны. Это означает, что вы можете вытворять что-то вроде:
$ git rm log/\*.log
Обратите внимание на обратный слэш (\) перед *. Это обязательно, т.к. Git использует свой собственный обработчик имён файлов в добавок к обработчику вашего командного интерпретатора. Эта команда удаляет все файлы которые имеют расширение .log в каталоге log/. Или же вы можете сделать вот так:
$ git rm \*~
Эта команда удаляет все файлы чьи именя заканчиваются на ~.
Перемещение файлов
В отличие от многих других систем версионного контроля, Git не отслеживает непосредственно перемещение файла. Если вы переименуете файл в Git, то в Git не сохранится никаких метаданных о том, что вы переименовали файл. Однако, Git довольно умён в плане обнаружения перемещений постфактум — мы рассмотрим обнаружение перемещения файлов чуть позже. Таким образом наличие в Git командыmv выглядит несколько странным. Если вам хочется переименовать файл в Git, вы можете сделать что-то вроде:
$ git mv file_from file_to
и это отлично сработает. На самом деле, если вы выполните что-то вроде этого и посмотрите на статус, вы увидите, что Git считает, что произошло переименование файла:
$ git mv README.txt README
$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# renamed: README.txt -> README
#
Однако, это эквивалентно выполнению следующих команд:
$ mv README.txt README
$ git rm README.txt
$ git add README
Git неявно определяет, что было переименование, поэтому не важно переименуете вы файл так или используя команду mv. Единственное отличие состоит лишь в том, что mv это одна команда вместо трёх — это функция для удобства. Важнее другое — вы можете использовать любой удобный способ, чтобы переименовать файл, и затем воспользоваться add/rm перед коммитом.
Pro Git