Что произойдет если вы добавили файлы в индекс затем изменили их и выполнили команду git commit
Перейти к содержимому

Что произойдет если вы добавили файлы в индекс затем изменили их и выполнили команду git commit

  • автор:

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

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

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

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

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

Жизненный цикл состояний файлов

Рисунок 8. Жизненный цикл состояний файлов

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

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

$ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working tree clean

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

Примечание

В 2020 году GitHub изменил имя ветки по умолчанию с master на main , другие же git-хостинг платформы последовали этому примеру. Поэтому, вы можете обнаружить, что ветка по умолчанию для новых репозиториев — main , а не master . Более того, имя ветки по умолчанию можно изменить (как вы видели в Настройка ветки по умолчанию), поэтому вам может встретиться и другое имя. При этом Git продолжает использовать имя master , поэтому далее в книге мы используем именно его.

Предположим, вы добавили в свой проект новый файл, простой файл README . Если этого файла раньше не было, и вы выполните git status , вы увидите свой неотслеживаемый файл вот так:

$ echo 'My Project' > README $ git status On branch master Your branch is up-to-date with 'origin/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 . Статус Untracked означает, что Git видит файл, которого не было в предыдущем снимке состояния (коммите); Git не станет добавлять его в ваши коммиты, пока вы его явно об этом не попросите. Это предохранит вас от случайного добавления в репозиторий сгенерированных бинарных файлов или каких-либо других, которые вы и не думали добавлять. Мы хотели добавить README, так давайте сделаем это.

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

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

$ git add README

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

$ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git restore --staged . " to unstage) new file: README

Вы можете видеть, что файл проиндексирован, так как он находится в секции «Changes to be committed». Если вы выполните коммит в этот момент, то версия файла, существовавшая на момент выполнения вами команды git add , будет добавлена в историю снимков состояния. Как вы помните, когда вы ранее выполнили git init , затем вы выполнили git add (файлы) — это было сделано для того, чтобы добавить файлы в вашем каталоге под версионный контроль. Команда git add принимает параметром путь к файлу или каталогу, если это каталог, команда рекурсивно добавляет все файлы из указанного каталога в индекс.

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

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

$ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) new file: README Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md

Файл CONTRIBUTING.md находится в секции «Changes not staged for commit» — это означает, что отслеживаемый файл был изменён в рабочем каталоге, но пока не проиндексирован. Чтобы проиндексировать его, необходимо выполнить команду git add . Это многофункциональная команда, она используется для добавления под версионный контроль новых файлов, для индексации изменений, а также для других целей, например для указания файлов с исправленным конфликтом слияния. Вам может быть понятнее, если вы будете думать об этом как «добавить этот контент в следующий коммит», а не как «добавить этот файл в проект». Выполним git add , чтобы проиндексировать CONTRIBUTING.md , а затем снова выполним git status :

$ git add CONTRIBUTING.md $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) new file: README modified: CONTRIBUTING.md

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

$ vim CONTRIBUTING.md $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) new file: README modified: CONTRIBUTING.md Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md

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

$ git add CONTRIBUTING.md $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) new file: README modified: CONTRIBUTING.md

Сокращённый вывод статуса

Вывод команды git status довольно всеобъемлющий и многословный. Git также имеет флаг вывода сокращённого статуса, так что вы можете увидеть изменения в более компактном виде. Если вы выполните git status -s или git status —short вы получите гораздо более упрощённый вывод:

$ git status -s M README MM Rakefile A lib/git.rb M lib/simplegit.rb ?? LICENSE.txt

Новые неотслеживаемые файлы помечены ?? слева от них, файлы добавленные в отслеживаемые помечены A , отредактированные файлы помечены M и так далее. В выводе содержится два столбца — в левом указывается статус файла, а в правом модифицирован ли он после этого. К примеру в нашем выводе, файл README модифицирован в рабочем каталоге, но не проиндексирован, а файл lib/simplegit.rb модифицирован и проиндексирован. Файл Rakefile модифицирован, проиндексирован и ещё раз модифицирован, таким образом на данный момент у него есть те изменения, которые попадут в коммит, и те, которые не попадут.

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

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

$ cat .gitignore *.[oa] *~

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

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

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

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

Вот ещё один пример файла .gitignore :

# Исключить все файлы с расширением .a *.a # Но отслеживать файл lib.a даже если он подпадает под исключение выше !lib.a # Исключить файл TODO в корневом каталоге, но не файл в subdir/TODO /TODO # Игнорировать все файлы в каталоге build/ build/ # Игнорировать файл doc/notes.txt, но не файл doc/server/arch.txt doc/*.txt # Игнорировать все .txt файлы в каталоге doc/ doc/**/*.txt

GitHub поддерживает довольно полный список примеров .gitignore файлов для множества проектов и языков https://github.com/github/gitignore это может стать отправной точкой для .gitignore в вашем проекте.

Примечание

В простейшем случае репозиторий будет иметь один файл .gitignore в корневом каталоге, правила из которого будут рекурсивно применяться ко всем подкаталогам. Так же возможно использовать .gitignore файлы в подкаталогах. Правила из этих файлов будут применяться только к каталогам, в которых они находятся. Например, репозиторий исходного кода ядра Linux содержит 206 файлов .gitignore .

Детальное рассмотрение использования нескольких .gitignore файлов выходит за пределы этой книги; детали доступны в справке man gitignore .

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

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

Допустим, вы снова изменили и проиндексировали файл README , а затем изменили файл CONTRIBUTING.md без индексирования. Если вы выполните команду git status , вы опять увидите что-то вроде:

$ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) modified: README Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md

Чтобы увидеть, что же вы изменили, но пока не проиндексировали, наберите git diff без аргументов:

$ git diff diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ebb991..643e24f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,8 @@ branch directly, things can get messy. Please include a nice description of your changes when you submit your PR; if we have to read the whole diff to figure out why you're contributing in the first place, you're less likely to get feedback and have your change -merged in. +merged in. Also, split your changes into comprehensive chunks if you patch is +longer than a dozen lines. If you are starting to work on a particular area, feel free to submit a PR that highlights your work in progress (and note in the PR title that it's

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

Если вы хотите посмотреть, что вы проиндексировали и что войдёт в следующий коммит, вы можете выполнить git diff —staged . Эта команда сравнивает ваши проиндексированные изменения с последним коммитом:

$ git diff --staged diff --git a/README b/README new file mode 100644 index 0000000..03902a1 --- /dev/null +++ b/README @@ -0,0 +1 @@ +My Project

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

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

$ git add CONTRIBUTING.md $ echo '# test line' >> CONTRIBUTING.md $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) modified: CONTRIBUTING.md Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md

Используйте git diff для просмотра непроиндексированных изменений

$ git diff diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 643e24f..87f08c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,3 +119,4 @@ at the ## Starter Projects See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md). +# test line

а так же git diff —cached для просмотра проиндексированных изменений ( —staged и —cached синонимы):

$ git diff --cached diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ebb991..643e24f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,8 @@ branch directly, things can get messy. Please include a nice description of your changes when you submit your PR; if we have to read the whole diff to figure out why you're contributing in the first place, you're less likely to get feedback and have your change -merged in. +merged in. Also, split your changes into comprehensive chunks if you patch is +longer than a dozen lines. If you are starting to work on a particular area, feel free to submit a PR that highlights your work in progress (and note in the PR title that it's

Примечание
Git Diff во внешних инструментах

Мы будем продолжать использовать команду git diff различными способами на протяжении всей книги. Существует ещё один способ просматривать эти изменения, если вы предпочитаете графический просмотр или внешнюю программу просмотра различий, вместо консоли. Выполнив команду git difftool вместо git diff , вы сможете просмотреть изменения в файле с помощью таких программ как emerge, vimdiff и других (включая коммерческие продукты). Выполните git difftool —tool-help чтобы увидеть какие из них уже установлены в вашей системе.

Коммит изменений

Теперь, когда ваш индекс находится в таком состоянии, как вам и хотелось, вы можете зафиксировать свои изменения. Запомните, всё, что до сих пор не проиндексировано — любые файлы, созданные или изменённые вами, и для которых вы не выполнили 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 # Your branch is up-to-date with 'origin/master'. # # Changes to be committed: # new file: README # modified: CONTRIBUTING.md # ~ ~ ~ ".git/COMMIT_EDITMSG" 9L, 283C

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

Примечание

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

Когда вы выходите из редактора, Git создаёт для вас коммит с этим сообщением, удаляя комментарии и вывод команды diff .

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

$ git commit -m "Story 182: fix benchmarks for speed" [master 463dc4f] Story 182: fix benchmarks for speed 2 files changed, 2 insertions(+) create mode 100644 README

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

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

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

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

$ git status On branch master Your branch is up-to-date with 'origin/master'. Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md no changes added to commit (use "git add" and/or "git commit -a") $ git commit -a -m 'Add new benchmarks' [master 83e38c7] Add new benchmarks 1 file changed, 5 insertions(+), 0 deletions(-)

Обратите внимание, что в данном случае перед коммитом вам не нужно выполнять git add для файла CONTRIBUTING.md , потому что флаг -a включает все файлы. Это удобно, но будьте осторожны: флаг -a может включить в коммит нежелательные изменения.

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

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

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

$ rm PROJECTS.md $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes not staged for commit: (use "git add/rm . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) deleted: PROJECTS.md no changes added to commit (use "git add" and/or "git commit -a")

Затем, если вы выполните команду git rm , удаление файла попадёт в индекс:

$ git rm PROJECTS.md rm 'PROJECTS.md' $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) deleted: PROJECTS.md

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

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

$ git rm --cached README

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

$ git rm log/\*.log

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

$ git rm \*~

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

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

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

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

$ git mv file_from file_to

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

$ git mv README.md README $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README

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

$ mv README.md README $ git rm README.md $ git add README

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

git reset

Команда git reset — это сложный универсальный инструмент для отмены изменений. Она имеет три основные формы вызова, соответствующие аргументам командной строки —soft, —mixed, —hard . Каждый из этих трех аргументов соответствует трем внутренним механизмам управления состоянием Git: дереву коммитов ( HEAD ), разделу проиндексированных файлов и рабочему каталогу.

Git reset и три дерева Git

Чтобы понять, как используется команда git reset , необходимо разобраться с внутренними системами управления состоянием в Git. Иногда эти механизмы называют «тремя деревьями» Git. «Деревья» — возможно, не самое точное название, поскольку это не традиционные древовидные структуры данных в строгом смысле слова. Тем не менее это структуры данных на основе узлов и указателей, которые Git использует для отслеживания истории внесения правок. Лучший способ продемонстрировать эти механизмы — создать набор изменений в репозитории и проследить его по трем деревьям.

Начнем работу с создания нового репозитория с помощью приведенных ниже команд.

$ mkdir git_reset_test
$ cd git_reset_test/
$ git init .
Initialized empty Git repository in /git_reset_test/.git/
$ touch reset_lifecycle_file
$ git add reset_lifecycle_file
$ git commit -m"initial commit"
[main (root-commit) d386d86] initial commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 reset_lifecycle_file

В приведенном выше примере кода создается новый репозиторий Git с одним пустым файлом reset_lifecycle_file . На этом этапе репозиторий имеет один коммит ( d386d86 ), в котором отражено добавление файла reset_lifecycle_file .

Связанные материалы
Шпаргалка по Git
СМ. РЕШЕНИЕ
Изучите Git с помощью Bitbucket Cloud

Рабочий каталог

Первое дерево, которое мы рассмотрим, — рабочий каталог. Это дерево синхронизировано с локальной файловой системой и отображает непосредственные изменения, внесенные в содержимое файлов и каталогов.

$ echo 'hello git reset' > reset_lifecycle_file 
$ git status
On branch main
Changes not staged for commit:
(use "git add . " to update what will be committed)
(use "git checkout -- . " to discard changes in working directory)
modified: reset_lifecycle_file

В нашем демонстрационном репозитории изменим и добавим содержимое в файл reset_lifecycle_file . Вызов команды git status показывает, что Git знает об изменениях в этом файле. В данный момент эти изменения являются частью первого дерева — рабочего каталога. Для отображения изменений в рабочем каталоге можно использовать команду git status . Измененные файлы будут отображаться красным цветом с префиксом «modified»

Раздел проиндексированных файлов

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

Для полного просмотра состояния раздела проиндексированных файлов необходимо использовать менее известную команду Git — git ls-files . Команда git ls-files по сути является утилитой отладки для проверки состояния дерева раздела проиндексированных файлов.

git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 reset_lifecycle_file

Здесь мы выполнили команду git ls-files с параметром -s (или —stage ). Без параметра -s вывод команды git ls-files представляет собой просто список путей и имен файлов, которые в данный момент являются частью индекса. Параметр -s отображает дополнительные метаданные файлов, находящихся в разделе проиндексированных файлов. Эти метаданные — биты режима проиндексированного контента, имя объекта и номер в индексе. Здесь нас интересует второе значение, имя объекта ( d7d77c1b04b5edd5acfc85de0b592449e5303770 ). Это стандартный хеш SHA-1 объекта Git, представляющий собой хеш содержимого файлов. В истории коммитов хранятся собственные SHA объектов для идентификации указателей на коммиты и ссылки, а в разделе проиндексированных файлов есть свои SHA объектов для отслеживания версий файлов в индексе.

Далее мы добавим измененный файл reset_lifecycle_file в раздел проиндексированных файлов.

$ git add reset_lifecycle_file 


$ git status


On branch main Changes to be committed:


(use "git reset HEAD . " to unstage)


modified: reset_lifecycle_file

Здесь мы вызываем команду git add reset_lifecycle_file , которая добавляет файл в раздел проиндексированных файлов. Теперь при вызове команды git status файл reset_lifecycle_file отображается зеленым цветом, как изменение, подлежащее коммиту («Changes to be committed»). Важно отметить, что команда git status не отображает истинное представление раздела проиндексированных файлов. Вывод git status отображает различия между историей коммитов и разделом проиндексированных файлов. Давайте рассмотрим содержимое раздела проиндексированных файлов на данный момент.

$ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

Видно, что SHA объекта для файла reset_lifecycle_file изменился с e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 на d7d77c1b04b5edd5acfc85de0b592449e5303770 .

История коммитов

Последнее дерево — история коммитов. Команда git commit добавляет изменения в постоянный снимок, который находится в истории коммитов. Этот снимок также включает состояние раздела проиндексированных файлов на момент выполнения коммита.

$ git commit -am"update content of reset_lifecycle_file"
[main dc67808] update content of reset_lifecycle_file
1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean

Здесь мы создали новый коммит с комментарием update content of resetlifecyclefile . В историю коммитов был добавлен набор изменений. Вызов команды git status в этой точке показывает, что ни в одном дереве нет ожидающих изменений. Выполнение команды git log отобразит дерево коммитов. Теперь, когда мы проследили за этим набором изменений во всех трех деревьях, можно приступать к использованию команды git reset .

Порядок действий

На первый взгляд, поведение команды git reset схоже с поведением команды git checkout . Но команда git checkout работает исключительно с указателем HEAD , а git reset перемещает указатель HEAD и указатель текущей ветки. Чтобы лучше продемонстрировать это поведение, рассмотрим следующий пример.

4 узла, из которых «главный узел» является последним

В этом примере показана последовательность коммитов в ветке main . Сейчас и указатель HEAD , и указатель на главную ветку main указывают на коммит d. Теперь давайте выполним обе команды, git checkout b и git reset b , и сравним результат.

git checkout b

4 узла: указатель Main указывает на последний узел, а указатель HEAD — на 2-й узел

После выполнения команды git checkout указатель main по-прежнему ссылается на коммит d . Указатель HEAD переместился и теперь ссылается на коммит b . В данный момент репозиторий находится в состоянии открепленного указателя HEAD .

git reset b

2 набора из 2 узлов: указатели HEAD и Main указывают на 2-й узел 1-го набора

Команда git reset перемещает и указатель HEAD , и указатель ветки на заданный коммит.

Помимо обновления указателей на коммит команда git reset изменяет состояние трех деревьев. Указатели меняются всегда, то есть происходит обновление третьего дерева, дерева коммитов. Аргументы командной строки —soft, —mixed и —hard определяют, каким образом необходимо изменить деревья раздела проиндексированных файлов и рабочего каталога.

Основные параметры

По умолчанию при вызове команды git reset используются неявные аргументы —mixed и HEAD . Таким образом, выполнение команды git reset эквивалентно выполнению команды git reset —mixed HEAD . В этом случае HEAD является указателем на конкретный коммит. Вместо HEAD можно использовать любой хеш SHA-1 коммита Git.

‘—hard

Это самый прямой, ОПАСНЫЙ и часто используемый вариант. При использовании аргумента —hard указатели в истории коммитов обновляются на указанный коммит. Затем происходит сброс раздела проиндексированных файлов и рабочего каталога до указанного коммита. Все предыдущие ожидающие изменения в разделе проиндексированных файлов и рабочем каталоге сбрасываются в соответствии с состоянием дерева коммитов. Это значит, что любая работа, находившаяся в состоянии ожидания в разделе проиндексированных файлов и рабочем каталоге, будет потеряна.

Чтобы продемонстрировать это, продолжим работать в репозитории, созданном ранее для примера с тремя деревьями. Сначала внесем в репозиторий некоторые изменения. Выполните в нем следующие команды:

$ echo 'new file content' > new_file
$ git add new_file
$ echo 'changed content' >> reset_lifecycle_file

С помощью этих команд мы создали новый файл с именем new_file и добавили его в репозиторий. Кроме того, было изменено содержимое файла reset_lifecycle_file . А теперь давайте выполним команду git status и посмотрим, как эти изменения повлияли на состояние репозитория.

$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)

new file: new_file

Changes not staged for commit:
(use "git add . " to update what will be committed)
(use "git checkout -- . " to discard changes in working directory)

modified: reset_lifecycle_file

Здесь мы вызываем команду git add reset_lifecycle_file , которая добавляет файл в раздел проиндексированных файлов. Теперь при вызове команды git status файл reset_lifecycle_file отображается зеленым цветом, как изменение, подлежащее коммиту («Changes to be committed»). Важно отметить, что команда git status не отображает истинное представление раздела проиндексированных файлов. Вывод git status отображает различия между историей коммитов и разделом проиндексированных файлов. Давайте рассмотрим содержимое раздела проиндексированных файлов на данный момент.

$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

Мы видим, что в индекс добавлен файл new_file . Мы внесли изменения в файл reset_lifecycle_file , но его SHA в разделе проиндексированных файлов ( d7d77c1b04b5edd5acfc85de0b592449e5303770 ) остался прежним. Это ожидаемый результат, поскольку мы не использовали команду git add для добавления этих изменений в раздел проиндексированных файлов. Эти изменения присутствуют и в рабочем каталоге.

Теперь давайте выполним команду git reset —hard и изучим новое состояние репозитория.

$ git reset --hard
HEAD is now at dc67808 update content of reset_lifecycle_file
$ git status
On branch main
nothing to commit, working tree clean
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

Здесь мы выполнили «жесткий сброс» с помощью параметра —hard . Вывод Git сообщает, что указатель HEAD показывает на последний коммит, dc67808 . Далее проверяем состояние репозитория с помощью команды git status . Git сообщает, что ожидающих изменений нет. Проверяем также состояние раздела проиндексированных файлов и видим, что он был сброшен к состоянию, предшествовавшему добавлению файла new_file . Изменения, которые мы внесли в файл reset_lifecycle_file , а также добавление файла new_file уничтожены. Важно понимать: восстановить эти потерянные данные невозможно.

‘—mixed

Это режим работы по умолчанию. Указатели ссылок обновляются. Раздел проиндексированных файлов сбрасывается до состояния указанного коммита. Любые изменения, которые были отменены в разделе проиндексированных файлов, перемещаются в рабочий каталог. Давайте продолжим.

$ echo 'new file content' > new_file
$ git add new_file
$ echo 'append content' >> reset_lifecycle_file
$ git add reset_lifecycle_file
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)

new file: new_file
modified: reset_lifecycle_file


$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 7ab362db063f9e9426901092c00a3394b4bec53d 0 reset_lifecycle_file

В приведенном выше примере мы внесли в репозиторий некоторые изменения: добавили файл new_file и изменили содержимое файла reset_lifecycle_file . Затем эти изменения были добавлены в раздел проиндексированных файлов с помощью команды git add . Теперь выполним команду reset по отношению к репозиторию в данном состоянии.

$ git reset --mixed
$ git status
On branch main
Changes not staged for commit:
(use "git add . " to update what will be committed)
(use "git checkout -- . " to discard changes in working directory)

modified: reset_lifecycle_file

Untracked files:
(use "git add . " to include in what will be committed)

new_file


no changes added to commit (use "git add" and/or "git commit -a")
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file

В данном случае мы выполнили «смешанный сброс». Напоминаем, что —mixed является режимом по умолчанию и выполнение команды git reset приведет к тому же результату. Изучение вывода команд git status и git ls-files показывает, что раздел проиндексированных файлов был сброшен до состояния, когда в этом разделе находился только файл reset_lifecycle_file . SHA объекта для файла reset_lifecycle_file был сброшен к предыдущей версии.

Обратите внимание: команда git status показывает нам, что появились изменения в файле reset_lifecycle_file и существует неотслеживаемый файл new_file . Это явный результат действия параметра —mixed . Раздел проиндексированных файлов был сброшен, а ожидающие изменения перемещены в рабочий каталог. Для сравнения, при использовании параметра —hard были сброшены и раздел проиндексированных файлов, и рабочий каталог, что привело к потере этих обновлений.

‘—soft

При передаче аргумента —soft выполняется обновление указателей, и на этом операция сброса останавливается. Раздел проиндексированных файлов и рабочий каталог остаются неизменными. Четко продемонстрировать такое поведение довольно сложно. Давайте продолжим работать с нашим демонстрационным репозиторием и подготовим его к мягкому сбросу.

$ git add reset_lifecycle_file 


$ git ls-files -s


100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file


$ git status


On branch main


Changes to be committed:


(use "git reset HEAD . " to unstage)


modified: reset_lifecycle_file


Untracked files:


(use "git add . " to include in what will be committed)


new_file

Здесь мы снова воспользовались командой git add , чтобы добавить измененный файл reset_lifecycle_file в раздел проиндексированных файлов. Чтобы убедиться, что индекс обновлен, посмотрим на вывод команды git ls-files . Теперь в выводе команды git status строка «Changes to be committed» (Изменения, подлежащие коммиту) окрашена в зеленый цвет. Файл new_file из предыдущего примера находится в рабочем каталоге как неотслеживаемый. Удалим его с помощью простой команды rm new_file , поскольку в следующих примерах он нам больше не понадобится.

Теперь давайте мягко сбросим текущее состояние репозитория.

$ git reset --soft
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)

modified: reset_lifecycle_file
$ git ls-files -s
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

Мы выполнили «мягкий сброс». Изучение состояния репозитория с помощью команд git status и git ls-files показывает, что ничего не изменилось. Это ожидаемый результат. Мягкий сброс влияет только на историю коммитов. По умолчанию при вызове команды git reset в качестве целевого коммита используется HEAD . Поскольку HEAD уже указывал на нашу историю коммитов и мы выполнили неявный сброс до HEAD , в реальности ничего не произошло.

Для получения более полного представления о параметре —soft и правильного его использования нам потребуется целевой коммит, отличный от HEAD . У нас уже есть файл reset_lifecycle_file , находящийся в разделе проиндексированных файлов. Давайте создадим новый коммит.

$ git commit -m"prepend content to reset_lifecycle_file"

На данный момент в нашем репозитории находится три коммита. Мы выполним возврат к первому коммиту. Для этого нам потребуется идентификатор первого коммита. Его можно найти, просмотрев вывод команды git log .

$ git log
commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df
Author: bitbucket
Date: Fri Dec 1 15:03:07 2017 -0800
prepend content to reset_lifecycle_file

commit dc67808a6da9f0dec51ed16d3d8823f28e1a72a
Author: bitbucket
Date: Fri Dec 1 10:21:57 2017 -0800

update content of reset_lifecycle_file

commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4

Author: bitbucket
Date: Thu Nov 30 16:50:39 2017 -0800

initial commit

Помните, что идентификаторы в истории коммитов уникальны для каждой системы. Это означает, что идентификатор коммита в этом примере будет отличаться от идентификатора, который вы увидите на своей машине. Идентификатор интересующего нас коммита для этого примера — 780411da3b47117270c0e3a8d5dcfd11d28d04a4 . Это идентификатор, соответствующий первичному коммиту «initial commit». Теперь укажем его в качестве целевого для нашей операции мягкого сброса.

Прежде чем вернуться назад во времени, проверим текущее состояние репозитория.

$ git status && git ls-files -s
On branch main
nothing to commit, working tree clean
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

Здесь мы выполнили составную команду, состоящую из команд git status и git ls-files -s . Она показывает, что в репозитории есть ожидающие изменения, а файл reset_lifecycle_file в разделе проиндексированных файлов находится в версии 67cc52710639e5da6b515416fd779d0741e3762e . Держа в уме эти сведения, выполним мягкий сброс до нашего первого коммита.

$git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4
$ git status && git ls-files -s
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)

modified: reset_lifecycle_file
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file

Приведенный выше код выполняет «мягкий сброс», а также вызывает составную команду, которая включает git status и git ls-files и выводит информацию о состоянии репозитория. Мы можем изучить эту информацию и сделать несколько интересных наблюдений. Во-первых, git status указывает на наличие изменений в файле reset_lifecycle_file и сообщает, что эти изменения проиндексированы для выполнения коммита. Во-вторых, входные данные git ls-files указывают, что раздел проиндексированных файлов не изменился и значение SHA осталось прежним: 67cc52710639e5da6b515416fd779d0741e3762e.

Чтобы выяснить, что произошло при этом сбросе, выполним команду git log:

$ git log commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket Date: Thu Nov 30 16:50:39 2017 -0800 initial commit

Теперь вывод команды log показывает, что в истории коммитов находится единственный коммит. Это четко иллюстрирует, что делает параметр —soft . Как и при всех вызовах команды git reset , сначала происходит сброс дерева коммитов. Наши предыдущие примеры с параметрами —hard и —mixed воздействовали на указатель HEAD и не возвращали дерево коммитов в предыдущее состояние. Во время мягкого сброса происходит только сброс дерева коммитов.

Почему же команда git status сообщает о наличии измененных файлов? Это может сбивать с толку. Параметр —soft не затрагивает раздел проиндексированных файлов, поэтому изменения в этом разделе сохраняются в истории коммитов. В этом можно убедиться, просмотрев вывод команды git ls-files -s , показывающий, что SHA для файла reset_lifecycle_file остается неизменным. Напомним, что команда git status показывает не состояние «трех деревьев», а различия между ними. В данном случае она показывает, что изменения в разделе проиндексированных файлов опережают изменения в истории коммитов, как если бы мы уже добавили их в индекс.

Разница между командами git reset и git revert

Команда git revert — это «безопасный» способ отмены изменений, а вот git reset — наоборот. При выполнении git reset есть реальный риск потерять наработки. Команда git reset никогда не удаляет коммиты, однако может оставить их без родителя, т. е. указатель потеряет прямой путь для доступа к ним. Такие коммиты обычно можно найти и восстановить с помощью команды git reflog. После запуска внутреннего «сборщика мусора» система Git навсегда удалит все коммиты без родителя. По умолчанию запуск происходит каждые 30 дней. История коммитов — одно из «трех деревьев Git»; два других — раздел проиндексированных файлов и рабочий каталог — не отличаются таким же постоянством, как коммиты. При использовании данного инструмента необходимо соблюдать осторожность, поскольку это одна из немногих команд Git, которые могут привести к потере наработок.

Команда revert предназначена для безопасной отмены публичных коммитов, а git reset — для отмены локальных изменений в разделе проиндексированных файлов и рабочем каталоге. Поскольку они предназначены для разных целей, их реализация также различается: команда reset полностью удаляет набор изменений, тогда как команда revert оставляет исходный набор изменений и использует новый коммит для применения отмены.

Не используйте reset в публичной истории

Никогда не используйте команду git reset , если после этого в публичный репозиторий были отправлены какие-либо снимки состояния. После того как коммит опубликован, нужно учитывать, что на него могут полагаться другие разработчики.

При удалении коммита, после которого другие участники команды начали работу, могут возникнуть серьезные проблемы. Когда коллеги попытаются синхронизироваться с вашим репозиторием, часть истории проекта будет просто отсутствовать. На следующей схеме показано, что происходит при использовании команды reset для публичного коммита. Ветка origin/main является версией вашей локальной главной ветки main в центральном репозитории.

Как только вы добавите новые коммиты после выполнения команды reset, система Git будет считать, что локальная история отклонилась от ветки origin/main , а коммит слияния, необходимый для синхронизации репозиториев, скорее всего, собьет с толку вашу команду и помешает ей.

Команду git reset <коммит> можно использовать только для локальных экспериментов, в которых что-то пошло не так, а не для публичных изменений. Если необходимо исправить публичный коммит, воспользуйтесь специальной командой git revert .

Примеры

git reset <file>

Удаляет указанный файл из раздела проиндексированных файлов, но оставляет рабочий каталог без изменений. Эта команда удаляет из индекса подготовленный файл, не перезаписывая все изменения.

git reset

Сбрасывает раздел проиндексированных файлов до состояния последнего коммита, но оставляет рабочий каталог без изменений. Эта команда удаляет из индекса все подготовленные файлы, не перезаписывая все изменения, что позволяет повторно собрать снимок состояния с нуля.

git reset --hard

Сбрасывает раздел проиндексированных файлов и рабочий каталог до состояния последнего коммита. Флаг —hard говорит Git, что нужно не только отменить изменения в разделе проиндексированных файлов, но и перезаписать все изменения в рабочем каталоге. Другими словами, эта команда уничтожит все неотправленные изменения, поэтому перед ее использованием убедитесь, что вы действительно хотите удалить ваши локальные наработки.

git reset 

Перемещает указатель текущей ветки на более ранний коммит , сбрасывает раздел проиндексированных файлов до состояния этого коммита, но не затрагивает рабочий каталог. Все изменения, внесенные после <коммита> , останутся в рабочем каталоге, чтобы вы могли повторно сделать коммит истории проекта с использованием более мелких и упорядоченных снимков состояния.

git reset --hard 

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

Удаление файла из раздела проиндексированных файлов

Команда git reset часто встречается при подготовке проиндексированного снимка состояния. В следующем примере предполагается, что у вас есть два файла с именами hello.py и main.py , которые вы уже добавили в репозиторий.

# Edit both hello.py and main.py

# Stage everything in the current directory
git add .

# Realize that the changes in hello.py and main.py
# should be committed in different snapshots

# Unstage main.py
git reset main.py

# Commit only hello.py
git commit -m "Make some changes to hello.py"

# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"

Как видите, команда git reset помогает соблюдать согласованность коммитов, позволяя не вносить изменения, которые не связаны со следующим коммитом.

Удаление локальных коммитов

В следующем примере показан более продвинутый сценарий использования. Он демонстрирует, что происходит, когда вы некоторое время работали над новой экспериментальной функцией, но после добавления нескольких коммитов состояния решили полностью все удалить.

# Create a new file called `foo.py` and add some code to it

# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"

# Edit `foo.py` again and change some other tracked files, too

# Commit another snapshot
git commit -a -m "Continue my crazy feature"

# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2

Команда git reset HEAD~2 перемещает указатель текущей ветки на два коммита назад, по сути удаляя из истории проекта оба снимка состояния, которые мы только что создали. Помните, что этот вид команды reset можно использовать только для неопубликованных коммитов. Никогда не выполняйте эту операцию, если вы уже отправили свои коммиты в общий репозиторий.

Резюме

Итак, git reset — это мощная команда, используемая для отмены локальных изменений в репозитории Git. Команда git reset работает с «тремя деревьями Git»: историей коммитов ( HEAD ), разделом проиндексированных файлов и рабочим каталогом. Существует три параметра командной строки, соответствующие этим трем деревьям. Эти параметры — —soft, —mixed и —hard — можно передавать команде git reset .

В этой статье мы воспользовались несколькими другими командами Git для демонстрации reset. Подробнее об этих командах см. на соответствующих страницах: git status, git log, git add, git checkout, git reflog и git revert.

Запись изменений в репозиторий

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

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

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

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

Если результат работы команды 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

Pro 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:

Первая строка предписывает 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\\.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 увидел изменения которые мы внесли, но чтобы их зафиксировать надо дать команду

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 — …

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

Git00039

Git00040

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

Git00041

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *