TL; DR заключается в том, что предположение Лассе В. Карлсена в комментарии :
Возможно, кто-то принудительно отправил на пульт и удалил эти коммиты после того, как вы получили их в ваш локальный репозиторий?
, вероятно, ответ. Запустите:
git reflog origin/master
и найдите строку forced-update
в выводе; если вы видите это, то вот что произошло.
Длинный: что все это означает
Git, по сути, действительно все о коммитит . Коммиты - это единица хранения, с которой мы работаем. У каждого коммита есть своего рода «истинное имя», одинаковое для всех в каждом Git, которое является его необработанным ha sh ID - большой уродливой строкой букв и цифр, например b34789c0b0d3b137f0bb516b417bd8d75e0cb306
. Этот ha sh ID означает этот коммит, и если у вас есть клон репозитория Git для Git, у вас либо есть этот коммит (часть предстоящего выпуска Git 2.27), либо вы этого не сделаете - ну, пока.
Однако мы обычно не ссылаемся на коммиты по этим идентификаторам ha sh. Они слишком большие и некрасивые, их невозможно запомнить или перепечатать. Нам нравится использовать имена: имена веток, например master
, имена тегов, например v2.26.0
, 1 или имена удаленного отслеживания например origin/master
.
Чтобы теги работали правильно во всех случаях, мы должны пообещать никогда не изменять идентификатор ha sh, на который ссылается какой-либо тег. Некоторое современное программное обеспечение, в частности Go модули и Go кэш версий - полагается на этот для бесперебойной работы (хотя вы можете ограничить его в целях безопасности, если / при необходимости).
Особенность имен веток - по сравнению с именами тегов - заключается в том, что sh ID коммита, который мы получаем, обычно просматривая имя ветки , меняет . Имя ветки фактически определено в Git как хранящее идентификатор ha sh последней фиксации, которая должна считаться частью ветки.
Коммиты неизменяемы , потому что их настоящее имя ha sh ID - это криптографическая c контрольная сумма их содержимого. 2 Между тем каждая фиксация содержит ha sh ID каждого своего непосредственного предшественника или родителя коммиты - обычно всего один, а слияния обычно два. Эта форма фиксируется в узлах в пределах ориентированного ациклического c графа, заставляя каждую фиксацию действовать как вершина, названная ha sh ID, и соединяющие односторонние ребра или дуги, названные как родительские.
Этот график может быть - и добавлен - просто добавлением новых коммитов в набор всех коммитов в репозитории. То есть мы используем git checkout
или git switch
, чтобы выбрать имя ветки в качестве текущей ветки и выбрать его последнюю фиксацию - ha sh ID, сохраненный в имя ветки - как текущая фиксация . Затем мы делаем свою работу как обычно, и Git упаковывает новый снимок и новый набор метаданных, чтобы сделать новую фиксацию. Родителем новой фиксации является текущая фиксация. 3 Теперь, когда новая фиксация существует и безопасно хранится в репозитории, 4 Git записывает новую уникальную ha * 1365 новой фиксации. * ID в текущей имя ветки , чтобы ветвь автоматически указывала на (новую) последнюю фиксацию.
Вот как ветви растут, по одной фиксации за раз, и это обеспечивает «нормальное направление» для перемещения имен веток:
... <-F <-G <-H <-- master
становится:
... <-F <-G <-H <-I <-- master
, когда мы добавляем новую фиксацию I
, затем становится:
... <-F <-G <-H <-I <-J <-- master
когда мы добавляем фиксацию J
и т. д.
Фактически мы не можем удалить коммитов из этого графика, но мы можем принудительно переместить любое имя ветки назад как бы. Например, после добавления коммитов I
и J
мы можем «удалить» их для большинства практических целей, заставив имя master
содержать ha sh ID фиксации H
снова:
I--J ???
/
...--F--G--H <-- master
Поскольку Git обычно находит коммиты, используя имя вроде master
, а затем работая в обратном направлении через родительские ссылки фиксации-фиксации, которые указывают в обратном направлении, Git больше не может найти зафиксировать J
, по крайней мере, не начиная с имени master
. В конце концов, наш Git отбросит I-J
по-настоящему - не сразу, и есть скрытые имена, по которым мы можем их найти, как мы увидим, но в конечном итоге .
Ваши собственные Git имена удаленного отслеживания , например origin/master
, являются вашей Git памятью некоторых других Git имен веток. То есть, когда вы впервые клонируете репозиторий или используете git fetch
для обновления клона, ваш Git вызывает какой-нибудь другой Git через URL-адрес, который Git сохраняет под именем. Мы называем имя - в данном случае origin
—a remote и указываем имя как сокращение для URL:
git fetch origin
Наш Git вызывает их Git , получает от них список имен их веток и last-commit-ha sh -ID, а также получает от них любые коммиты, которые у них есть, которых у нас нет и которые нам нужны. Например, если у нас есть, в это время:
...--F--G--H <-- master, origin/master
мы можем вызвать их Git и, возможно, их master
теперь указывает на новую фиксацию J
:
...--F--G--H--I--J <-- master [in the Git at origin]
Наш Git проверяет, замечает, что у нас нет фиксации с ha sh ID J
, и получает эту фиксацию из их Git. Это также приводит к фиксации I
, потому что мы всегда получаем все, что у нас нет, 5 , а затем обновляем нашу origin/master
- нашу память их master
. Итак, теперь у нас есть в нашем собственном локальном репозитории:
...--F--G--H <-- master
\
I--J <-- origin/master
На данный момент их хозяином является 2 ahead
- на два коммита впереди - наш master
. Теперь мы можем предпринять действия, чтобы добавить эти коммиты, I-J
, в наш собственный master
. Мы можем сделать это Git, сдвинув name master
«вперед», чтобы указать на фиксацию J
:
...--F--G--H
\
I--J <-- master, origin/master
Если мы сделаем это с помощью git merge
, Git называет это слиянием с перемоткой вперед . Фактического слияния не происходит: Git на самом деле просто проверяет фиксацию J
напрямую и обновляет имя master
, чтобы указать на фиксацию J
. (Есть и другие способы перемотки вперед любого имени ветки: если фиксация не извлечена, а это не наша текущая ветка, имя просто «скользит вперед» ".)
Но, как мы отметили выше, мы можем заставить имя ветки« двигаться назад », чтобы« удалить »- ну, вроде как-удалить - фиксацию. Предположим, кто-то делает это на другом Git после того, как мы добавили I-J
к нашему master
. Их репозиторий теперь имеет master
, указывающий на фиксацию H
. 6 Когда наш Git вызывает их Git - запустив git fetch origin
- мы видим, что их master
имена фиксируют H
, а не фиксируют J
. У них нет новых коммитов, поэтому наш Git просто обновляет наш origin/master
сейчас, давая нам следующее:
...--F--G--H <-- origin/master
\
I--J <-- master
Мы теперь 2 ahead
из них, поскольку if мы сделали коммиты I-J
.
Если нам разрешено git push origin master
, мы можем легко вернуть эти два коммита, просто запустив git push origin master
сейчас. Если их Git действительно отклонил коммиты I-J
, что ж, теперь они вернулись. Если нет, у них уже есть I-J
, а наш git push
просто заставляет их перемотать вперед их имя master
, чтобы указать на фиксацию J
.
Кто бы ни "удалил" коммит Для I-J
из репозитория Git по адресу origin
была причина, по которой они это сделали. Это было намеренно? Это была ошибка? У нас нет способа узнать - ну, у нас есть один способ: спросите их! Они знают; мы можем только догадываться, поэтому спросите их.
Однако мы можем сказать, что это произошло, если наш Git забрал I-J
от них в какой-то момент и обновил наши собственный origin/master
соответственно. Здесь и появляются те скрытые имена, которые я упомянул.
1 В репозитории Git для Git, v2.26.0
означает фиксацию 274b9cc25322d9ee79aa8e6d4e86f0ffe5ced925
, хотя имя фактически преобразуется в объект тега ha sh ID, adf6396efeb4e8c12fb07174b4074c4031b2c460
. Однако весь смысл тега в том, что нам не нужно , чтобы знать что-либо из этого. Достаточно простого, удобочитаемого и понятного имени v2.26.0
.
2 На самом деле это верно для всех Git объектов - оно не указывает c на фиксацию объектов. См. Также Как вновь обнаруженная коллизия SHA-1 влияет на Git?
3 Если новая фиксация - слияние фиксация, его первый родитель - это текущая фиксация. Остальные родители - вот что делает коммит слияния; обычно здесь есть только один другой коммит - это могут быть идентификаторы ha sh любого существующего коммита, но каждый должен быть идентификатором ha sh некоторого существующего коммита.
4 Обратите внимание, что дерево работ не в репозитории - это отдельная сущность, которая находится рядом с репозиторием, в смысл - и ваши файлы не безопасны до фиксации.
5 За исключением того, что Git называет мелкими клонами. Давайте проигнорируем их здесь.
6 Их репозиторий может или не может все еще содержать коммиты I-J
, в зависимости от того, как быстро их Git успеет отбросить недоступен совершает. Доступность - ключевое понятие здесь; для (гораздо) более подробной информации см. Think Like (a) Git.
Reflogs сохраняют предыдущие значения имен
Каждый раз, когда наш Git обновляет любое из наших имен - master
или origin/master
или даже имя тега - наш Git будет (необязательно, но он включен по умолчанию для нас) сохранит старое значение имени в журнале. Этот журнал представляет собой reflog для данного имени. 7 Это означает, что наша origin/master
- наша память о том, где был master
, обновляется каждый раз, когда мы запускаем git fetch
- отслеживает, где их master
был на предыдущем git fetch
.
Предположим, кто-то намеренно или случайно заставляет origin
имя master
переместиться назад - на go от фиксации J
до H
в приведенном выше примере. Предположим далее, что мы запустили git fetch
, который ранее записал это имя, указывающее на фиксацию J
, а теперь мы запускаем git fetch
, а имя указывает на H
. Наш Git принудительно обновит наш origin/master
без перемотки вперед. 8
Теперь мы можем, постфактум, найти это в рефлоге для origin/master
:
$ git reflog origin/master
В моем случае я могу сделать это с моим Git репозиторием для Git, потому что ветвь pu
все время перемещается так:
$ git reflog origin/pu
dfeb8fbf42 (origin/pu) refs/remotes/origin/pu@{0}: fetch: forced-update
fc307aa377 refs/remotes/origin/pu@{1}: fetch: forced-update
5525884f08 refs/remotes/origin/pu@{2}: fetch: forced-update
...
Часть «принудительного обновления» здесь означает, что у них были некоторые коммиты в своей ветке, которые мы смогли получить и запомнить с нашим собственным именем для удаленного отслеживания, но затем они решили отбросить эти коммиты в сторону. Наш Git подобрал свое новое имя pu
и соответственно обновил наше origin/pu
.
7 Существует также рефлог для специального имени HEAD
, но у него нет важной функции в данном конкретном случае, потому что мы не можем быть "на" имени удаленного отслеживания, например origin/master
.
8 Команда git fetch
сообщает об этом в своем выводе, хотя многие не обращают на это внимания. В каждой строке вы увидите три индикатора: ведущий знак плюс, три точки вместо двух и добавленные слова «принудительное обновление»:
b34789c0b0..07d8ea56f2 master -> origin/master
232c24e857..5cccb0e1a8 next -> origin/next
+ fc307aa377...dfeb8fbf42 pu -> origin/pu (forced update)
139372b246..6b33381979 todo -> origin/todo
Обратите внимание, как ветвь pu
переместилась в без перемотки вперед, и git fetch
сказал нам это три раза.