Итак, у вас имеется настоящий репозиторий 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