Как можно проверить, есть ли в stash неотслеживаемые / игнорируемые файлы, которые больше не существуют локально?(мерзавец, Linux) - PullRequest
1 голос
/ 26 мая 2019

Я был сбит с толку, я звонил git stash --all и git stash apply stash@{...} несколько раз, а также удалял некоторые неотслеживаемые / игнорируемые файлы.

Как можно проверить, существуют ли файлы, которые существуютв одном из тайников, но не локально?

Ответы [ 2 ]

0 голосов
/ 26 мая 2019

TL; DR

Вы можете использовать git diff --name-only --diff-filter=D stash@{<em>number</em>}^3 для каждого действительного stash@{<em>number</em>}.(Чтобы получить список тайников, используйте git stash list.)

Вы можете использовать git show --name-only stash@{<em>number</em>}^3 на каждом действительном stash@{<em>number</em>}.Обратите внимание, что это git show stash@..., а не git stash show.

Чтобы понять, что и почему, читайте дальше.

Long

То, что делает git stash, немного сложно, но это можно суммировать довольно просто:

  • git stash push (или старое написание, git stash save) делает два или три коммита , причем ни один изсовершает это делает пребывание на любой ветке.Затем он запускает git reset или git clean или какую-то их комбинацию, в зависимости от используемых флагов.

  • git stash apply объединяет некоторые или все из двух илитри коммита в некотором тайнике с вашим существующим индексом и рабочим деревом.

  • git stash pop означает run git stash apply, а затем, если это претендует на успех, запустите git stash drop.

К сожалению, вышесказанное на самом деле довольно сложно - требуется, чтобы вы поняли использование Git индекса , с одной стороны, - и все же не является полным,В нем говорится, что git stash push делает два коммита (или иногда три), но в нем не говорится, что такое в этих коммитах, и какую форму они имеют в вашем хранилище.Для простейшего использования git stash, ни один из них не имеет большого значения, но для вашего случая они имеют решающее значение.

Commits

Я просто кратко упомяну это, поскольку, кромете, которые git stash делает, мы не слишком озабочены ими.Каждый коммит содержит полный снимок файлов.Точно , какие файлы, мы увидим через минуту.Наряду со снимком, фиксация содержит некоторые метаданные, включая информацию о том, кто совершил фиксацию, когда (отметка даты и времени) и почему (сообщение журнала).Каждый коммит имеет уникальный хэш-идентификатор, и как часть метаданных, каждый коммит включает хэш-идентификатор своего родителя - ссылку на предыдущий коммит.

A merge commit содержит ссылкивернуться к двум или более родителям.Коммиты, которые связаны таким образом, как правило, тесно связаны - в конце концов, именно поэтому связь является родительской / дочерней - но в отличие от части «снимок-плюс-метаданные», нет строгого требования, чтобы файлы в одном коммите были в значительной степени связаны с файлами влюбой другой.Вскоре мы увидим, что и с коммитами stash.

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

Индекс и рабочее дерево

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

Для того, чтобы вы на самом деле использовали или изменили свои файлы, Git должен повторно их увлажнить, превратив их в обычные файлы, которые вы можете читать и записывать,Область, в которой коммиты регидратируются для вашего использования, - ваше рабочее дерево .Git может остановиться здесь - с замороженными коммитами, содержащими обезвоженные файлы, такие же постоянные, как и сами коммиты, плюс временные, эфемерные, но полезные файлы рабочего дерева.Другие системы контроля версий do останавливаются на этом: у вас есть в любое время лиофилизированная копия файла в текущем коммите, плюс полезная копия в рабочем дереве.Но по разным причинам Git добавляет третью копию файла.Эта дополнительная копия находится между коммитом и рабочим деревом, в том, что Git по-разному называет index или область подготовки .

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

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

Когда вы запускаете git commit, Git смотрит не на ваше рабочее дерево , а на ваш индекс.Что бы ни было в вашем индексе прямо тогда , Git упаковывает эти (уже высушенные) файлы в новый коммит, и этот новый коммит становится вашим текущим коммитом.Теперь ваш индекс и совпадение фиксации, потому что новый коммит был сделан из индекса.

Это также определяет, является ли файл отслеженным .Если в индексе есть копия, файл отслеживается.Отслеживаемая копия - та, что в индексе - будет в следующем коммите, если вы сделаете это прямо сейчас.Если в вашем рабочем дереве есть файл, который не в вашем индексе, то этот файл не отслеживается .Этот файл не будет в следующем коммите, если вы сделаете это прямо сейчас.Следовательно, индекс, в некотором смысле, предлагает следующий коммит .Каждый раз, когда вы обновляете его с помощью git add, вы предлагаете зафиксировать что-то немного другое.

Файлы, которые не отслеживаются, обычно выполняют различные команды Git, особенно git status - жалуются .Вы можете закрыть эти жалобы, а также заставить git add --all не копировать файлы в указатель, перечислив некоторые или все эти файлы в .gitignore.Обратите внимание, что перечисление отслеживаемого файла не имеет никакого эффекта: он уже есть в индексе, поэтому нет никаких сомнений в его игнорировании: он не игнорируется.Наличие в списке .gitignore влияет только на неотслеживаемых файлов и, как правило, затрудняет их случайное отслеживание, а также отключает git status для них.

Вы можете поставить new файлы в индекс в любое время, используя git add.Если файла там не было раньше, git add создает его в индексе, а не смещает предыдущую копию.Вы также можете удалить файлы из индекса в любое время, используя либо git rm - это удалит файл из индекса и рабочего дерева, либоgit rm --cached, который удаляет файл только из индекса.В git commit время, не имеет значения , как файл находится или нет в индексе, только , есть ли , есть или нет, и если он есть,с каким лиофилизированным содержимым.

Стоит кратко взглянуть на то, как git commit делает новый коммит сейчас.Когда вы запускаете git commit, как мы уже упоминали, Git вставляет все отслеживаемые файлы в новый коммит.Однако сначала Git собирает метаданные : ваше имя (с user.name), ваш адрес электронной почты (с user.email), текущую дату и время и ваше сообщение журнала.Git также знает, какой коммит является текущим коммитом.Хеш-идентификатор этого коммита входит в родительский хеш-код нового коммита.Затем Git сохраняет индекс и делает коммит, который автоматически получает новый уникальный идентификатор хэша.В качестве последнего шага git commit, Git затем записывает новый идентификатор хеша коммита в имя текущей ветви .

Следовательно, если раньше вы имели:

...--F--G--H   <-- master (HEAD)

с коммитом H в качестве текущего коммита, и вы только что сделали новый коммит I, новый коммит I указывает на H, и Git вставил хеш-код I в имя ветви master, так что теперь у вас есть:

...--F--G--H--I   <-- master

Теперь мы можем посмотреть, что git stash push делает

Когда git stash строит новый тайник без --all, он:

  1. Записывает индекс как коммит. Это действительно легко, так как это то, что git commit уже делает. Все, что Git должен сделать, это , а не обновить имя master (и предоставить вам сообщение журнала, что он и делает). Давайте выпишем коммит i (в нижнем регистре), а не поместим его на master. Вместо этого мы запомним это с помощью временной переменной:

    ...--F--G--H   <-- master
               |
               i   <-- $tempvar
    
  2. Записывает рабочее дерево как коммит. Это сложно сделать эффективно, а также требует еще одного специального трюка в конце. Не вдаваясь в подробности как git stash управляет записью рабочего дерева, стоит сказать, что это только запись отслеженных файлов. Хитрость в конце состоит в том, что git stash настраивает вещи так, чтобы у этого нового коммита, который мы назовем w, было двух родителей вместо одного. Первый родитель w будет H, а второй родитель w будет i:

    ...--F--G--H   <-- master
               |\
               i-w   <-- stash
    

С этими двумя записанными коммитами Git обновляет специальное имя refs/stash, чтобы запомнить хэш-идентификатор коммита w.

В этом тайнике нет неотслеживаемых файлов, независимо от того, игнорируются ли они Фиксация i была сделана из индекса, поэтому по определению она не имеет неотслеживаемых файлов. Процесс, который Git использует для создания w, сохраняет только файлы, которые есть в индексе, поэтому он также не имеет неотслеживаемых файлов.

Если вы используете git stash push --all, git stash push --include-untracked или git stash save разновидностей этих же команд, Git немного модифицирует процесс сохранения. Он делает коммит i как обычно, но затем делает коммит, который я называю u, для хранения неотслеживаемых файлов. Этот дополнительный коммит либо содержит всего неотслеживаемых файлов, исключая неотслеживаемые и игнорируемые файлы, либо содержит всех неотслеживаемых файлов, включая игнорируемые. Для этого коммита в списке указан no parent (что является хорошим трюком, но его легко выполнить, когда вы используете команды Git plumbing , как это делал git stash до того, как он был недавно преобразован в код C). ); он просто плавает сам по себе:

...--F--G--H   <-- master
           |
           i   <-- $i_commit

           u   <-- $u_commit

Теперь git stash save возвращается к своему основному пути и делает коммит w, но на этот раз он дает w трем родителям: текущий коммит H, индекс фиксации i и неотслеживаемые файлы фиксируют u:

...--F--G--H   <-- master
           |\
           i-w   <-- stash
            /
           u

Краткое резюме: что в i, w и u

Commit i содержит состояние индекса. В i нет неотслеживаемых файлов по определению. Commit w содержит состояние рабочего дерева, опять же без неотслеживаемых файлов. Если фиксация u существует - в конце концов, она необязательна - она ​​содержит неотслеживаемые файлы, , но не отслеживаемые файлы: код тайника экономно сохраняет их только в i и w.

Сейчас git stash push убирает

После сохранения файлов за два-три коммита последний шаг git stash push - сброс индекса и рабочего дерева. Если вы сказали git stash создать коммит u, он также удаляет из рабочего дерева любой файл, сохраненный в коммите u.

Сброс индекса и рабочего дерева обычно выполняется простым git reset --hard. Это оставляет индекс и рабочее дерево в состоянии, которое соответствует текущему коммиту H. Если вы сделали коммит u, его файлы теперь удалены из рабочего дерева, в противном случае эти файлы не затрагиваются в рабочем дереве.

Однако git stash push (в отличие от git stash save) может сбрасывать меньше, чем все рабочее дерево. В этом случае все это делается с помощью более сложного кода. Вы также можете (ну вместо этого) добавить опцию --keep-index, и в этом случае вместо git reset --hard или аналогичной, Git проверяет, что находится в коммите i, так что рабочее дерево соответствует i. (Он оставляет индекс сам по себе, поэтому i и индекс продолжают совпадать.) Ничто из этого не влияет на вашу непосредственную задачу, но все это влияет на способность восстановить один из этих тайников.

Предыдущие тайники "складываются"

Когда git stash push будет сделано, новый тайник будет идентифицирован как refs/stash, или просто stash для краткости. Вы также можете описать это как stash@{0}, если хотите. Все существующие тайники перемещаются на одну позицию вверх до stash@{1}, stash@{2} и т. Д.: То, что было stash@{1}, становится stash@{2} и т. Д.

Механизм, лежащий в основе этого, - reflogs * в Git, которые применяются ко всем ссылкам: ветви имеют master@{1}, master@{2} и т. Д., Тоже. Код stash просто (ab?) Использует их для реализации стека. Другие reflogs только для вставки: нет команды "pop the n'th master".

Восстановление тайника

Когда вы решите применить тайник - с помощью git stash apply или git stash pop; помните, что последнее просто применить-затем-отбросить - вы сообщаете Git , какой stash использовать, например, stash@{<em>number</em>}. Это напрямую указывает на коммит w, но коммит w позволяет вам достичь его коммит i и, если он существует, его коммит u. Самый простой способ сделать это - использовать синтаксис gitrevisions для обхода графа . Например, чтобы сослаться на commit i, который является вторым родителем w, вы можете написать:

stash^2

потому что stash указывает на фиксацию w, а вторым родителем w является i. Если в этом тайнике существует коммит u, stash^3 назовет его.

Следовательно, git stash apply сначала проверяет, существует ли коммит u. Если это так, git stash настаивает на его восстановлении. Для восстановления u commit требуется , чтобы ни один из файлов в u не существовал в рабочем дереве прямо сейчас.

Это означает, что если у вас есть несколько неотслеживаемых файлов, и вы не уверены, какие из них находятся в u, вы можете просто удалить (или убрать) все неотслеживаемые файлы. Это, безусловно, самая простая вещь. Если вы хотите быть тщательно отобранным, вам нужно будет перечислить имена файлов, которые находятся в коммите u, и для этого нет внешней интерфейсной команды для этого. Вы можете сделать это, хотя, несколькими способами, как мы увидим через мгновение.

В любом случае, Git определенно имеет коммиты i и w. (Код git stash обеспечивает наличие двух таких коммитов плюс необязательный третий u коммит и отклоняет ваш аргумент командной строки, если нет.) Поэтому git stash apply необходимо восстановить i и w тоже. Вот как это происходит:

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

  2. Если вы не использовали --index, полностью игнорировать commit i. В противном случае, сравните i с первым родителем w - фиксацией, что был текущим на момент сохранения тайника - используя git diff. Отправьте diff на git apply --cached. Технически, настоящая строка кода в старом сценарии git-stash:

    git diff-tree --binary $s^2^..$s^2 | git apply --cached
    

    ($s - это коммит w, поэтому он использует i^, а не w^, но i^ и w^ одинаковы; diff использует diff-tree --binary, так что он всегда работает правильно, как обычный diff не будет анализировать двоичные файлы и будет использовать вашу индивидуальную конфигурацию, что является плохой идеей).

    Шаг применения может завершиться неудачно. Если это так, git stash apply --index не работает и ничего не делает. Если шаг применения выполнен успешно, сохраните полученный индекс на потом, а затем используйте git reset, чтобы сбросить его в соответствии с фиксацией HEAD.

    ThЗдесь также есть хитрость: Git проверяет, соответствует ли сохраненный индекс на шаге 1 сохраненному индексу в тайнике. Если это так, индекс уже имеет правильное содержимое, и нет смысла делать git apply --cached. Это не просто оптимизация; это полезно с git stash --keep-index: оно заставляет git stash apply --index работать в этом случае. (Конечно, вы могли бы запустить git stash apply без --index, если ваш индекс уже совпадает с i тайника, но я думаю, что кто-то подумал, что это было слишком недружелюбно.)

  3. Используйте механизм слияния, чтобы объединить коммит w с вашим текущим рабочим деревом, используя в качестве основы слияния первого родителя w. Я не буду вдаваться в детали, но эта часть может быть довольно грязной. Если здесь есть конфликты слияния, и текущее рабочее дерево не не соответствует коммиту HEAD, когда вы начали, может быть очень трудно вернуться в состояние, в котором вы были.

    (Это одна из нескольких причин, по которой я рекомендую избегать git stash в целом. безопасно использовать git stash во многих случаях, и если вы действительно знаете, что делаете, вы Я знаю, как сделать вещи безопасными для себя во всех случаях. Но git stash рекламируется как быстрое и простое решение для новичков Git, и на самом деле, это совсем не быстро и не легко, в этих угловых случаях!)

Ваш случай (наконец-то!)

В вашем случае вы выполнили несколько операций git stash push --all, поэтому у вас есть от нескольких до многих тайников - скажем, от stash@{0} до stash@{9}, или, возможно, даже больше - некоторые или все из которых имеют u коммитов, к которому вы можете получить доступ через stash@{<em>number</em>}^3.

Эти u коммиты не имеют родителя, поэтому если вы запустите:

git show stash@{1}^3
Например,

, Git будет сравнивать, т.е. git diff, пустое дерево с коммитом u для stash@{1}. Это покажет файлы (и содержимое файлов - добавьте --name-only, чтобы получить только имена) в этом u коммите.

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

... если есть файлы, которые существуют в одном из хранилищ, но не локально

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

Если мы запустим:

git diff <commit-specifier>

без дополнительных параметров, Git будет сравнивать содержимое указанного коммита с содержимым рабочего дерева. Текущий индекс не играет никакой роли в этой разнице, хотя содержание .gitignore делает. Интересующие файлы:

  • тех, кто в данном коммите
  • тех, кто находится в рабочем дереве, независимо от того, существуют ли они в данном коммите, за исключением любых файлов, которые (а) существуют в рабочем дереве и (b) перечислены в .gitignore.

То есть предположим, что мы называем коммит - например, один из этих u коммитов - который содержит файлы a.ext, b.ext и c.ext. Ваше текущее рабочее дерево либо имеет a.ext, либо его нет; То же самое касается b.ext и c.ext. Ваше текущее рабочее дерево также имеет d.ext и e.bin, ни один из них не входит в этот u коммит.

Если файл a.ext не существует в вашем текущем рабочем дереве, git diff будет утверждать, что для преобразования коммита u в соответствие вашему рабочему дереву вы должны удалить a.ext , Если файл b.ext существует в вашем рабочем дереве и соответствует этому в u, Git ничего не скажет об этом. Если файл c.ext действительно существует, но не совпадает с копией в u, Git скажет, что c.ext изменен, и для c.ext в коммите u make c.ext в вашем рабочем дереве необходимо добавить и / или удалить определенные строки: это вывод команды diff.

Since d.ext существует ли в вашем рабочем дереве, git diff скажет, что для преобразования u в соответствие с вашим рабочим деревом, вы должны добавить d.ext, с его текущее содержание. Если *.bin игнорируется, то git diff не скажет вам, как добавить его для фиксации u: здесь предполагается, что вы не хотите делать новый коммит, который похож на u, но имеет e.bin добавлено, поскольку e.bin следует игнорировать. Это верно , даже если e.bin находится в вашем индексе прямо сейчас. В то время как git commit заботится, этот конкретный git diff нет.

Поскольку commit u в каждом тайнике перечисляет все (и только) файлы, сохраненные в нем, любая инструкция из Git, которая говорит вам, что вы должны удалить некоторый файл из u, чтобы сделать оно соответствует вашему рабочему дереву, говорит, что файл существует в u, а не в вашем рабочем дереве. Поэтому мы используем --diff-filter=D, чтобы сделать git diff файл упоминания a.ext. Этот фильтр исключит c.ext, так как он существует: у него просто неправильное содержимое. Поэтому набор файлов, о которых будет сообщать git diff, состоит исключительно из a.ext, который находится в u, но не в вашем рабочем дереве. Опция --name-only заставляет git diff печатать только имя файла, а не фактические инструкции по преобразованию файла.


Есть и другие способы решения этой проблемы, но эти два - git show или git diff с --name-only и дополнительными опциями, если / при необходимости, а также именем коммита u для хранилища - выглядят как Простейшие.

0 голосов
/ 26 мая 2019

Полагаю, вы можете запустить diff:

git diff --name-status stash@{10}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...