Разница между `git stash show -p stash@{N}` и `git show stash@{N}`?

Я думал, что они должны быть в основном одинаковыми, но когда я попытался

$ git stash show -p stash@{N}

а также

$ git show stash@{N}

последний показывает некоторую дополнительную информацию о коммите, но фактический diff был намного короче. (Первый показывает около дюжины файлов, а второй только один.)

Итак, в чем именно разница между ними и почему они разные?

Могу ли я также полагаться на правильность таких вещей, как git diff stash@{M} stash@{N}?


person musiphil    schedule 12.02.2014    source источник


Ответы (2)


Шкатулки

Вещь, сохраненная git stash, - это то, что я назвал "сумкой для хранения". Он состоит из двух1 отдельных коммитов: коммита "index" (промежуточная область) и коммита "дерева работы". Коммит рабочего дерева — это забавный вид коммита слияния.

Позвольте мне снова нарисовать это здесь (см. Ссылочный ответ для более длинной версии), просто чтобы проиллюстрировать это должным образом. Давайте для простоты предположим, что у вас есть небольшое репо с одной веткой и тремя фиксациями в ней, с A по C. Вы находитесь в одной ветке и делаете несколько изменений, а затем запускаете git stash save (или просто git stash). Это то, что ты получаешь:

A - B - C     <-- HEAD=master
        |\
        i-w   <-- the "stash"

Теперь вы можете создать (или переключиться) на другую ветку, но для иллюстрации давайте просто скажем, что вы оставляете этот тайник там и делаете более «обычные» коммиты на master:

A - B - C - D - E    <-- HEAD=master
        |\
        i-w   <-- stash

Дело здесь в том, что «заначка», пара коммитов index и work-tree, по-прежнему висит на том же коммите, что и раньше. Коммиты не могут быть изменены, и это относится и к коммитам из заначки.

Но теперь вы создаете новый тайник, внося некоторые изменения (оставаясь на master) и снова запуская git stash save.

Что происходит со старой заначкой? "Имя ссылки"2 stash теперь указывает на новый тайник. Но старые коммиты из заначки все еще там. Только сейчас им требуется имя стиля "reflog", stash@{1}.3

В любом случае, то, что у вас есть сейчас, это:

A - B - C - D - E     <-- HEAD=master
        |\      |\
        i-w     i-w   <-- stash
          .
           -------------- stash@{1}

(Когда вы используете git stash drop, скрипт stash просто манипулирует reflog для stash ref, чтобы удалить идентификатор сброшенного stash-bag. Вот почему все «более высокие» перенумеровываются. Фактический stash-bag сам по себе является мусором, собранным на следующий git gc.)

Следующий фрагмент является ключом к пониманию того, что происходит.

Каждый раз, когда git нужно, чтобы вы назвали конкретный коммит, вы можете сделать это любым из многих способов.

У каждого коммита есть «настоящее имя», которое представляет собой большой уродливый хэш SHA-1, который вы видите, такие значения, как 676699a0e0cdfd97521f3524c763222f1c30a094. Вы можете написать это. Это всегда означает один и тот же коммит. Коммиты никогда не могут быть изменены, и это криптографический хэш всего содержимого коммита, поэтому, если этот конкретный коммит вообще существует, это значение всегда является его именем.

Однако это не очень хорошее имя для людей. Итак, у нас есть псевдонимы: такие вещи, как имена веток и тегов, и относительные имена, такие как HEAD и HEAD~2, и имена в стиле журнала ссылок, такие как HEAD@{yesterday} или master@{1}. (Есть команда git rev-parse, которая превращает подобные строки имен в хеш-значения. Попробуйте: запустите git rev-parse HEAD, git rev-parse stash и т. д. Большинство вещей в git используют либо git rev-parse, либо его старшего брата, который делает гораздо больше, git rev-list, чтобы превратить имена в значения SHA-1.)

(Полное описание того, как назвать ревизию, см. в gitrevisions Git использует SHA-1 не только для коммитов, но здесь давайте просто подумаем о коммитах.)

Git stash show, git show и git diff

Хорошо, наконец-то мы можем перейти к вашим git show против git stash show, и git diff, и так далее. Давайте сначала займемся git stash show, так как это тот, который вы должны использовать с тайниками. Более того, подкоманды git stash проверят, что указанный вами коммит — или, если вы не назвали коммит, найденный по ссылке stash — «выглядит как» тайник, т. е. является одним из этих забавных коммитов слияния.

Если вы запустите git stash show -p, git покажет вам разницу (-patch). Но что именно он показывает?

Вернитесь к схеме с заначками. Каждая заначка связана с конкретным коммитом. Выше «основной» тайник теперь висит на коммите E, а более ранний тайник stash@{1} висит на C.

Что делает git stash show -p, так это сравнивает фиксацию этого тайника в рабочем дереве, w, с фиксацией, от которой висит тайник.4

Вы, конечно, можете сделать это сами. Допустим, вы хотите сравнить w в stash, который зависает от фиксации E, с фиксацией E, которая может быть названа именем ветки master. Итак, вы можете запустить: git diff master stash. Здесь имя stash относится к (текущей) фиксации тайника w, а master относится к фиксации E, так что получается точно такой же патч, что и git stash show -p stash. (И если вы хотите сравнить w в stash@{1} с фиксацией C, вам просто нужно запустить git diff так, чтобы вы назвали эти две фиксации. Конечно, проще просто git stash show -p stash@{1}.)5

Как насчет простого git show? Это немного сложнее. git show рад показать коммит, а вы дали ему ссылку stash (либо сам stash, либо один из вариантов reflog). Это допустимый идентификатор коммита, и он разрешается в один из w коммитов рабочего дерева в одном из тайников. Но git show ведет себя по-другому, когда видит фиксацию слияния. Как говорится в документации:

Он также представляет фиксацию слияния в специальном формате, созданном git diff-tree --cc.

Таким образом, git show stash@{1} показывает вам "комбинированный diff", предполагая, что коммит w представляет собой обычное слияние коммитов C и i, производящее w. В конце концов, это не обычное слияние, хотя комбинированный diff может быть действительно полезным, если вы знаете, на что смотрите. Прочтите документацию по --cc под git diff-tree, чтобы узнать, как это работает в деталях, но я отмечу, что --cc подразумевает -c, который включает этот бит:

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

В случае stash, если вы git add отредактировали файлы до запуска git stash, так что разница i-vs-w пуста, вы не увидите эти файлы в выводе здесь.

Наконец, если вы git diff stash@{M} stash@{N}: это просто просьба git diff сравнить разные коммиты work-tree. Насколько это важно, зависит от того, что вы сравниваете, что, как правило, зависит от того, где прикреплены сумки для хранения.


1На самом деле два или три, но я нарисую их двумя. Вы получаете два коммита с git stash save (или простым git stash, что означает git stash save). Вы получите три коммита, если добавите параметры -u или -a для сохранения неотслеживаемых или всех файлов. Это влияет на восстановление тайника, но не на вывод команды git stash show.

2"Ссылочное имя" — это просто имя, похожее на имя ветви или тега. Существует много возможных форм ссылочного имени. Ветки и теги — это просто имена специального назначения. "Удаленные ветки" - это еще одна форма этих ссылок, и "тайник" также является ссылкой.

На самом деле HEAD — это просто еще одна ссылка, хотя и очень особенная. Я настолько важен, что если вы удалите файл HEAD, git решит, что ваш репозиторий в конце концов больше не является репозиторием.

За исключением некоторых особых случаев — HEAD, ORIG_HEAD, MERGE_HEAD и т. д. — все ссылки начинаются со строки refs/. Ветки начинаются с refs/heads/, теги начинаются с refs/tags/, а "удаленные ветки" начинаются с refs/remotes/. Другими словами, у ссылок есть «пространство имен», обычно начинающееся с refs/, а затем под ним следует еще одно слово, чтобы определить, где они находятся.

Ссылка на тайник пишется refs/stash (и останавливается на этом, нет refs/stash/jimmy_kimmel или чего-то подобного).

3На самом деле, это действительно действительно использует журнал ссылок. Это означает, помимо прочего, что тайники, кроме "основного", refs/stash, будут могут истечь. (К счастью, как музыкальные заметки, по умолчанию, начиная с git 1.6.0, они не истекают; вы должны настроить время истечения срока их действия, чтобы это произошло - что, вероятно, не то, что вам нужно в любом случае.)

4Умный способ сделать это с использованием нотации суффикса ^ описан в моем другой ответ.

5Что, если вы хотите посмотреть на index-коммиты в этих тайниках? А, хороший вопрос! :-) Скрипт тайника не имеет хорошего ответа. Самый простой способ увидеть это — использовать суффикс ^2 для имени второго родителя каждого тайника, который является коммитом i. И, если у вас есть тайник с третьим коммитом, содержащим неотслеживаемые или все файлы, это третий родитель: коммит w выглядит как слияние трех родителей, а stash^3 получает третий. Но опять же, w не является обычным слиянием, так что это сложно. Вероятно, лучший простой способ просмотреть все части тайника — превратить его в отдельную ветку, используя git stash branch.

person torek    schedule 13.02.2014
comment
Это невероятно подробный ответ. Спасибо, что нашли время. - person Mike Monkiewicz; 13.02.2014
comment
Что касается тайников, отличных от основного, срок действия refs/stash истекает так же, как срок действия всех записей reflog: Примечания к выпуску GIT v1.6.0 говорится: По умолчанию срок хранения записей в тайнике не ограничен. Установите reflogexpire в [gc refs/stash] на разумное значение, чтобы вернуть традиционное поведение автоматического истечения срока действия. - person musiphil; 21.03.2014
comment
@musiphil: Ах, приятно знать. В более новых версиях git есть новые элементы управления и для других ссылок, но по какой-то причине я пропустил это. - person torek; 21.03.2014
comment
Разве git diff stash master не должно быть git diff master stash для создания точно такого же патча, что и git stash show -p stash? - person musiphil; 13.03.2015
comment
@musiphil: действительно! Починю. - person torek; 13.03.2015
comment
@torek: В прошлом я был разочарован тем, что git stash pop не смог сохранить мой индекс. Прочитав подробности в вашем ответе, особенно в отношении того, что хранятся два коммита, я более внимательно изучил тайники. Я "обнаружил" git stash pop --index, который пытается восстановить не только изменения рабочего дерева, но и индекса. Хотел бы я понять это раньше. Спасибо за подсказки в правильном направлении. - person Rhubbarb; 24.09.2015
comment
@torek: кроме того, я попытался просмотреть git stash save --include-untracked (-u) с помощью gitk --all, где я установил все четыре комбинации индексированных и неиндексированных модификаций и добавленных файлов. Я вижу, что в этом случае сохранено 3 (три) фиксации: дополнительная фиксация в этом случае содержит неотслеживаемые файлы. - person Rhubbarb; 24.09.2015
comment
@Rhubbarb: см. сноску 1, в которой упоминается третья фиксация для -u/-a. - person torek; 24.09.2015
comment
@musiphil для git diff master stash у меня ошибка fatal: ambiguous argument 'stash': unknown revision or path not in the working tree. - person alper; 27.02.2021
comment
@alper: если stash - неизвестная ревизия, у вас сейчас не должно быть тайников: git stash drop удаляет последний, полностью удаляя refs/stash, после чего вы получите эту ошибку. Или, если вы никогда этого не делали, вы также получите ту же ошибку. - person torek; 28.02.2021

Я считаю, что это связано с причудой, когда git хранит рабочий каталог и индекс отдельно. git stash show -p stash@{N} покажет все изменения в тайнике, в том числе добавленные на сцену. Однако git show stash@{N} не будет включать в себя изменения, которые были подготовлены до помещения в тайник. Кажется, что команда git stash достаточно умна, чтобы объединить их вместе, тогда как git show просто показывает содержимое большого двоичного объекта stash@{0}.

И да, git diff stash@{M} stash@{N} будет работать так, как вы ожидаете.

person Mike Monkiewicz    schedule 13.02.2014
comment
git stash show -p смотрит только на версию рабочего каталога, полностью игнорируя индексную версию. В большинстве ситуаций с тайником это не имеет значения, но если вы git add кучу вещей, а затем в основном восстанавливаете копии рабочего дерева, вывод из git stash show на самом деле может ввести в заблуждение. Это одна из вещей, которая меня не очень устраивает, в скрипте тайника. (Но для этого нет очевидных исправлений, иначе, без сомнения, они уже были бы там. :-)) (Также я говорю в основном-возврат, потому что если вы получите версии рабочего дерева точно обратно синхронизированными с HEAD, вы столкнетесь с ошибкой в stash.) - person torek; 13.02.2014
comment
Я ценю подробный ответ в другом ответе, но для быстрого TL; DR этот ответ здесь дал мне то, что мне нужно было знать с точки зрения ключевых различий между постановкой / не постановкой перед эффектами тайника. Спасибо! (я предполагаю, что это точное объяснение? да...?) - person redfox05; 08.03.2019