У вас неверная ментальная модель работы Git. (Не волнуйтесь, что вы делаете - я делал, когда начал с Git, более десяти лет go.) Чтобы исправить свою ментальную модель, вам нужно знать следующие вещи:
Git магазины коммиты . Он не хранит файлы - не на том уровне, на котором вы будете его использовать, во всяком случае, - скорее, целые коммиты.
Коммиты сами делают файлы для хранения, так что вот как вы получить файлы, но они находятся на уровне коммита: у вас либо есть коммит (и все его файлы), либо нет (у вас нет ни одного из его файлов). Каждый коммит хранит полный и полный снимок из всех файлов (ну, все из его файлов; см. Ниже).
Коммиты также хранят некоторые метаданные: информацию о коммите, например, кто его сделал, когда и почему (сообщение журнала). Важнейшим элементом метаданных в каждом коммите является «номер» коммита, который приходит до этого коммита.
коммит "числа" большие и некрасивые и случайный вид ha sh ID. Каждый коммит получает уникальный идентификатор ha sh. Вот как вы (или ваш Git) узнаете, есть ли у вас коммит. Каждый Git повсюду соглашается, что этот конкретный коммит получает этот конкретный га sh ID , и никакой другой коммит, прошлый или будущий, никогда не сможет иметь этот ID. Для этого идентификатор ha sh представляет собой криптографическую c контрольную сумму содержимого коммита - это означает, что ни одна из частей любого существующего коммита не может когда-либо измениться.
Ни один человек не может на самом деле запомнить эти га sh идентификаторы. К счастью, нам не нужно: у нас есть компьютер, чтобы запомнить их для нас.
A название филиала , что большинство людей (включая меня) часто будет сокращенно "ветвь", содержит только один идентификатор ha sh. Идентификатор ha sh в имени , например, это идентификатор last commit в ветви. Вот почему каждый коммит ссылается на свой родительский или предыдущий коммит: так что Git может начинаться с конца и работать в обратном направлении.
A коллекция коммитов то, что вы получаете, начиная с конца и работая в обратном направлении, также называется «ветвью». Поэтому, когда кто-то говорит, например, branch master
, важно подумать о том, означает ли это последний коммит в master
, сохраненный в имени master
или серия коммитов, заканчивающаяся последним коммитом в master
.
Теперь тот факт, что каждый когда-либо сделанный коммит является только для чтения , означает что то, что мы делаем с репозиторием, обычно просто добавляет новые коммиты . Но чтобы сделать новый коммит, мы должны иметь возможность изменять файлов: открывать их в наших редакторах, вносить в них изменения и сохранять их обратно. Файлы внутри коммитов не могут быть изменены . Поэтому мы не работаем и не можем работать с совершенными файлами. Сами коммиты, которые хранят моментальные снимки всех ваших файлов, являются всего лишь архивами.
Чтобы архивы не становились очень толстыми очень быстро, Git хранит зафиксированные файлы в специальном, только для чтения, Git -только сжатый формат. Только Git действительно может использовать их. (Конечно, вы могли бы написать свои собственные программы для их чтения, но существует более одного формата, и уже есть Git сантехническая команда , то есть то, что пользователям не нужно использовать, чтобы читать необработанный объект, используя git cat-file -p
. Это может читать больше, чем просто файлы, но он может читать файлы внутри коммита.) Новые коммиты могут делиться файлами из существующих коммитов - это, очевидно, безопасно все доступны только для чтения - и на самом деле все это происходит автоматически.
В любом случае, чтобы выполнить любую работу new в каком-либо существующем репозитории, вы должны сначала выбрать существующий коммит и Git извлеките его куда-нибудь. Это «где-то» - ваше рабочее дерево (или рабочее дерево или какой-либо вариант с этим именем). Извлеченная область рабочего дерева содержит обычные файлы в обычных повседневных форматах.
Вы и ваш компьютер можете работать с этими файлами рабочего дерева. Это то, что вы делаете, например, в шагах 2 и 6.
Git не не использует эти файлы рабочего дерева вообще. Он создает их для вас (извлекая их из коммитов), и он будет смотреть на них, когда вы скажете это, но он не использует их для создания коммитов . Они существуют для того, чтобы вы могли использовать для выполнения своей работы. Вы должны скопировать их в файлы, которые использует Git, что и было шагом 3. Здесь все становится немного сложнее.
Индекс
На шаге 1 вы создали новый пустой Git репозиторий. В этом хранилище еще нет коммитов. Он имеет пустое рабочее дерево, в котором вы можете работать со своими файлами. И он имеет пустой индекс . Эта вещь - этот индекс - довольно сложная, но вы можете думать о ней как о , где вы строите следующий коммит, который вы сделаете . Вы можете думать об этом как о хранении копий каждого из ваших файлов.
Ваш шаг 2:
touch file1.txt file2.txt
, который создал два (пустых) файла в вашей работе -tree. Эти файлы еще не включены в ваш индекс. Ваш шаг 3, однако, был:
git add file1.txt file2.txt
Это приводит к копированию содержимого файлов в индекс. 1 Git теперь говорит, что эти файлы постановка для коммита . Это приводит к другому, альтернативному имени индекса: он также называется промежуточной областью . Это просто синонимы: указатель или промежуточная область - это только одно. 2
Наконец, на шаге 4 вы запустили git commit
. Это сделало новый коммит из файлов, которые были в индексе , а не в рабочем дереве. Эти два индексных файла были копиями из рабочего дерева.
Теперь у вас есть коммит. Этот коммит является самым первым коммитом в репозитории, поэтому он немного особенный: он не записывает ни одного предыдущего коммита. (Конечно, не может; никаких предыдущих коммитов нет.) Я понятия не имею, что у вас есть sh ID вашего коммита: он зависит не только от файлов в коммите (которые я знаю) и вашего сообщение в журнале (которое я видел в вашей команде), но также на ваше имя и адрес электронной почты и в ту самую секунду, когда ваш Git создал коммит (а я этого не знаю). Однако я знаю, что он имеет уникальный идентификатор ha sh, отличный от всех других идентификаторов ha sh в вашем репозитории или любом другом репозитории Git, с которым вы будете разговаривать в будущем. 3
1 Технически, индекс содержит режимы файлов, их имена и - для каждого файла - ссылку на внутренний Git объект, который содержит содержимое. Этот объект BLOB-объекта имеет идентификатор ha sh, как и коммит (хотя в отличие от коммита, объект BLOB-объекта можно использовать повторно). Идентификатор пустого файла ha sh: e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
, который можно найти, запустив git hash-object -t blob --stdin </dev/null
. Если и когда Git перейдет к SHA-2 вместо SHA-1, идентификаторы каждого объекта изменятся, что будет очень интересным временем для Git. Мы можем надеяться, что Git скрывает здесь все болезненные части для нас.
2 Технически, индекс в основном представляет собой просто файл в .git
с именем .git/index
. «В основном» здесь только потому, что Git имеет режим, называемый индекс разделения . Все это, однако, внутренние детали, которые могут измениться. Единственное внешнее обещание состоит в том, что вы можете установить переменную среды с именем GIT_INDEX_FILE
, чтобы Git использовал другой индекс. Некоторые Git программы делают это для специальных целей: например, git stash
, когда это был скрипт оболочки, делал это при выполнении некоторых коммитов sta sh, чтобы избежать перезаписи нормального индекса.
3 Это зависит от уникальности идентификаторов ha sh. В присутствии злоумышленников это, в свою очередь, зависит от силы криптографии. См. Как влияет недавно обнаруженное столкновение SHA-1 на Git?
Подробнее об именах ветвей
Мы уже упоминали, что имена ветвей, например, master
, держите га sh ID коммита. Пока у вас нет идентификаторов ha sh, у вас не может быть названий ветвей. Таким образом, создание этого начального коммита - это то, что создало name master
. Это имя содержит фактический идентификатор ha sh, что бы это ни было. Когда что-то содержит идентификатор sh, мы говорим, что это указывает на коммит. Итак, в настоящее время - после того, как шаг 4 создает первый коммит - у вас есть коммит с каким-то большим уродливым идентификатором ha sh, но давайте просто назовем его «commit A
» и нарисуем его так: *
Имя master
указывает на (содержит га sh ID) commit A
.
Теперь мы go переходим к шагу 5:
git checkout -b myBranch
Это создает новое имя , myBranch
, которое также содержит идентификатор ha sh существующего коммита A
. Давайте обновим наш чертеж:
A <-- master, myBranch
Git также необходимо знать какое название ветви мы используем , поэтому давайте прикрепим имя HEAD
, написанное заглавными буквами, к одно из этих двух названий веток. Имя ветви, которое мы хотим использовать - созданное этим git checkout -b
- является новым, так что:
A <-- master, myBranch (HEAD)
Оба имени указывают на один и тот же коммит. Это совершенно нормально в Git: коммит A
теперь в обе ветви . Текущее имя равно myBranch
, а текущее значение - это значение A
.
Теперь давайте посмотрим, что происходит в шагах 6, 7 и 8:
rm file1.txt
Удаляет файл из вашего рабочего дерева . Git index , который по-прежнему соответствует фиксации A
- Git made commit A
из индекса - в нем по-прежнему есть два файла.
git status
Это выполняет два отдельных сравнения. Один сравнивает текущий коммит, commit A
, с индексом. Они имеют одинаковые файлы с одинаковым содержимым, поэтому эта часть git status
ничего не говорит. Второе сравнение - index-vs-work-tree. Здесь индекс имеет file1.txt
, а рабочее дерево - нет, поэтому это сравнение говорит о том, что file1.txt
удаляется из рабочего дерева, но не из индекса, говоря, что это удаление not staged for commit
.
git checkout master
Это сообщает Git, что вы хотите изменить текущий коммит и / или ветвь . Текущая ветвь myBranch
, а текущая фиксация A
. Имя выбранной ветви master
и его фиксация A
. Таким образом, Git может пропустить изменение коммитов , при этом прикрепляя специальное имя HEAD
к имени master
сейчас: 4
A <-- master (HEAD), myBranch
Ничего больше не происходило: в индексе по-прежнему есть два файла, в текущем коммите все еще сохраняется A
, а в рабочем дереве по-прежнему отсутствует один файл. Шаг 9 - еще один git status
- скажет вам, что ваша текущая ветвь теперь master
, но будет выполнять те же сравнения: зафиксировать A
против индекса и индекс против рабочего дерева. Результат здесь будет таким же. Шаг 10 просто смотрит на рабочее дерево, которое, как мы знаем, отсутствует file1.txt
.
Шаг 11 просит Git снова присоединить HEAD
к master
. Больше ничего не меняется: индекс не затрагивается, а рабочее дерево не затрагивается.
На шаге 12, однако, вы запускаете:
git rm file1.txt
Это меняет index . Команда git rm
удаляет файл как из индекса, так и из рабочего дерева. Оно уже ушло из рабочего дерева, так что ничего не изменилось, но теперь в index больше нет file1.txt
.
На шаге 13 вы запускаете git commit
снова. Это делает новый коммит из того, что находится в индексе: то есть коммит, в котором есть только пустой file2.txt
. Вы также получаете все обычные метаданные: ваше имя и адрес электронной почты, а также сообщение в журнале, почему вы сделали этот коммит. родитель этого нового коммита, который мы будем называть B
, а не пытаться угадать идентификатор ha sh, - это существующий коммит A
: новый коммит B
указывает на существующий коммит A
.
Последний шаг git commit
предназначен для Git, чтобы записать идентификатор ha sh нового коммита в имя, к которому прикреплен HEAD
. Поскольку шаг 11 привязан HEAD
к myBranch
, результат будет следующим:
A <-- master
\
B <-- myBranch (HEAD)
Существующее имя master
не изменилось вообще. HEAD
все еще прикреплен к myBranch
, но имя myBranch
теперь указывает на новый коммит B
. Индекс по-прежнему имеет то, что имел до того, как вы запустили git commit
: то есть он содержит только пустой file2.txt
. Коммит B
имеет стрелку, указывающую назад, или содержит идентификатор ha sh - коммит A
, поэтому, если вы сейчас запустите git log
, ваш Git начнется с HEAD
, find myBranch
, find B
, show commit B
, следуйте стрелке, чтобы зафиксировать A
, и show commit A
.
4 Технически Git выполняет это, записывая имя ветви master
в файл в .git
с именем .git/HEAD
. Вы можете посмотреть на этот файл, но когда вы хотите обновить его, вы должны использовать различные Git инструменты, потому что при различных условиях Git может использовать некоторые другие файл. В частности, поскольку Git 2.5, Git теперь имеет git worktree add
, что добавляет новую пару индекс-и-дерево-работа. Каждое добавленное рабочее дерево также должно иметь свое собственное отдельное HEAD
, поэтому, как только вы добавите несколько рабочих деревьев, индекс уже не всегда будет .git/index
, а HEAD
- не всегда .git/HEAD
. .
Сводка
Всегда помните о следующих пунктах:
Git это все о совершает . Имена ветвей - и другие имена, как только вы доберетесь до этой точки - просто служат для find коммитов.
Каждый коммит имеет уникальный идентификатор ha sh, и за исключением некоторых новых незавершенных функций («частичных клонов»), у вас всегда либо полный коммит, либо ни одного из коммитов.
Каждый коммит ссылается на один или несколько предшественников или parent коммиты, за исключением особых случаев, таких как самый первый коммит в каком-либо хранилище. Эти связи - или цепочки коммитов - образуют то, что люди называют ветвями (одно из нескольких значений слова «ветвь»).
Чтобы сделать новый коммит, вам необходимо обновить Git индекс . Когда вы впервые git checkout
делаете какой-то коммит, которого у вас еще нет, Git заполняет индекс - и, конечно, ваше рабочее дерево - из этого коммита. Вы работаете с файлами в вашем рабочем дереве, а Git работает с его индексом.
Индекс и ваше рабочее дерево не копируются: когда вы git clone
, или git fetch
, или git push
, вы передадите коммитов . Индекс и дерево работы здесь не имеют значения (ну, есть некоторые условия для git push
, в other Git, который получает git push
).
Коммиты навсегда заморожены (и в основном постоянные - от них немного сложно избавиться, даже если вы захотите, иногда). Копии файлов в вашем индексе и рабочем дереве являются временными.
Добавление новых коммитов обновляет названия вашей ветви. Обновленное имя ветви - это имя, к которому вы прикрепили HEAD
.
В Git 2.23 или более поздней версии вы можете использовать git switch
, чтобы выбрать, где HEAD
идет и / или создает новые имена веток и git restore
для извлечения указанных c файлов из указанных c коммитов; в более ранних версиях Git оба задания объединялись в одну команду git checkout
.
Когда вы приступите к использованию второго репозитория Git, помните, что до git push
эти коммиты в этого другого хранилища, ваш Git - единственный, который имеет ваш новый коммит. Это позволяет легко (и хорошо) «переписывать историю», заменяя некоторые коммиты некоторыми новыми и улучшенными версиями (например, git rebase -i
или git commit --amend
). Как только вы отправили коммитов в другое место, вы все равно можете заменить коммиты на новые и улучшенные версии, просто у other Git теперь есть коммиты, которые вы отправили ранее, так что эти все становится сложнее - иногда намного сложнее.