Как выполнить переименование без последующих изменений в git?

У меня есть файл, который я переименовал, а затем отредактировал. Я хотел бы рассказать Git о переименовании, но не о модификациях содержимого. То есть, я хочу сместить удаление старого имени файла и добавление старого содержимого файла с новым именем файла.

Итак, у меня есть это:

Changes not staged for commit:

        deleted:    old-name.txt

Untracked files:

        new-name.txt

но хотите либо это:

Changes to be committed:

        new file:   new-name.txt
        deleted:    old-name.txt

Changes not staged for commit:

        modified:   new-name.txt

или это:

Changes to be committed:

        renamed:    old-name.txt -> new-name.txt

Changes not staged for commit:

        modified:   new-name.txt

(где мера подобия должна быть 100%).

Я не могу придумать простой способ сделать это.

Есть ли синтаксис для получения содержимого конкретной ревизии конкретного файла и добавление его в область размещения Git по указанному пути?

Удалить часть с помощью git rm в порядке:

$ git rm old-name.txt

Это добавляет часть переименования, с которой я борюсь. (Я могу сохранить новое содержимое, проверить новую копию (для старого содержимого), mv в оболочке, git add, а затем восстановить новое содержимое, но это похоже на очень длинный путь!)

Спасибо!

Ответ 1

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


Когда вы спрашиваете git (через git show или git log -p или git diff HEAD^ HEAD) "что произошло в последнем коммите", он выполняет разницу с предыдущим фиксацией (HEAD^ или HEAD~1 или фактический необработанный SHA-1 для предыдущего фиксации - любой из них сделает для его идентификации) и текущий фиксатор (HEAD). При создании этого diff он может обнаружить, что раньше был old.txt и больше не существует; и не было new.txt, но теперь есть.

Эти имена файлов, которые раньше были там, но не были, а файлы, которые есть сейчас, которые не были помещены в кучу, помечены как "кандидаты на переименование". Затем для каждого имени в куче git сравнивается "старое содержимое" и "новое содержимое". Сравнение для точного соответствия супер-легко из-за способа git сводит содержимое к SHA-1; если сбой точного совпадения, git переключается на необязательный параметр "- это содержимое, по крайней мере, похожее" diff для проверки переименований. С git diff этот необязательный шаг управляется флагом -M. С другими командами он либо устанавливается вашими значениями git config, либо жестко закодирован в команду.

Теперь вернемся к промежуточной области и git status: что git хранит в области index/staging в основном "прототип следующего коммита". Когда вы git add что-то, git сохраняет содержимое файла прямо в этой точке, вычисляя SHA-1 в этом процессе и затем сохраняя SHA-1 в индексе. Когда вы git rm что-то, git хранит примечание в индексе, говорящее, что "это имя пути намеренно удаляется при следующем коммите".

Затем команда git status просто выполняет diff-or really, two diffs: HEAD vs index, для того, что будет сделано; и index vs work-tree, для того, что может быть (но еще не завершено).

В этом первом diff, git использует тот же механизм, что и всегда, для обнаружения переименований. Если в тесте HEAD, который ушел в индекс, есть путь, а путь в индексе, который новый, а не в HEAD, совершает, он является кандидатом на переименование. Команда git status hardwires переименовывает обнаружение на "on" (и ограничение количества файлов до 200; только с одним кандидатом для обнаружения переименования этот предел достаточно).


Что все это значит для вашего дела? Ну, вы переименовали файл (без использования git mv, но это не имеет особого значения, поскольку git status находит переименование или не находит его в git status время) и теперь имеет более новую, другую версию новый файл.

Если вы git add новой версии, эта более новая версия переходит в репо, а ее SHA-1 находится в индексе, а когда git status выполняет diff, он будет сравнивать новый и старый. Если они по крайней мере "похожи на 50%" (жесткое значение для git status), git сообщит вам, что файл переименован.

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

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

Самый простой способ - так же, как вы предлагаете: переместите измененное содержимое где-то в сторону, используйте git checkout -- old-name.txt, затем git mv old-name.txt new-name.txt, а затем зафиксируйте. git mv переименует файл в области index/staging и переименует рабочую версию.

Если git mv имел параметр --cached, например git rm, вы могли бы просто git mv --cached old-name.txt new-name.txt, а затем git commit. Первый шаг переименовал бы файл в индекс, не касаясь дерева. Но это не так: он настаивает на перезаписывании версии рабочего дерева и настаивает на том, чтобы старое имя должно существовать в дереве рабочих данных.

Одноэтапный метод для этого, не касаясь рабочего дерева, заключается в использовании git update-index --index-info, но это тоже несколько беспорядочно (я покажу его в любой момент). К счастью, мы можем сделать последнее. Я установил ту же ситуацию, что и вы, переименовав старое имя в новое и изменив файл:

$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    deleted:    old-name.txt

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

    new-name.txt

Теперь мы делаем , вручную ставим файл под своим старым именем, а затем используем git mv для переключения на новое имя:

$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt

На этот раз git mv обновляет имя в индексе, но сохраняет исходное содержимое как индекс SHA-1, но перемещает версию рабочего дерева (новое содержимое) на место в дереве:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    old-name.txt -> new-name.txt

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

    modified:   new-name.txt

Теперь просто git commit сделать фиксацию с переименованием на месте, но не новое содержимое.

(Обратите внимание, что это зависит от того, не существует ли новый файл со старым именем!)


Как насчет использования git update-index? Сначала сначала вернемся к состоянию "изменено в дереве дерева, соответствует индексу соответствия HEAD":

$ git reset --mixed HEAD  # set index=HEAD, leave work-tree alone

Теперь посмотрим, что в индексе для old-name.txt:

$ git ls-files --stage -- old-name.txt
100644 2b27f2df63a3419da26984b5f7bafa29bdf5b3e3 0   old-name.txt

Итак, нам нужно git update-index --index-info сделать, чтобы стереть запись для old-name.txt, но сделать иначе идентичную запись для new-name.txt:

$ (git ls-files --stage -- old-name.txt;
   git ls-files --stage -- old-name.txt) |
  sed -e \
'1s/^[0-9]* [0-9a-f]*/000000 0000000000000000000000000000000000000000/' \
      -e '2s/old-name.txt$/new-name.txt/' | 
  git update-index --index-info

(примечание: я сломал выше для целей публикации, это была вся одна строка, когда я набрал ее, в sh/ bash, он должен работать разбитым, как это, учитывая обратную косую черту, которую я добавил, чтобы продолжить команда "sed" ).

Есть и другие способы сделать это, но просто дважды извлечь запись индекса и перенести первую в удаление, а вторая с новым именем показалась самой легкой здесь, поэтому команда sed. Первая замена изменяет режим файла (100644, но любой режим будет преобразован в нулевые) и SHA-1 (соответствует любому SHA-1, заменяется на git специальные все нули SHA-1), а второй покидает режим и только SHA-1 при замене имени.

Когда индекс обновления заканчивается, индекс записывает удаление старого пути и добавление нового пути (с тем же режимом и SHA-1, как и в предыдущем пути).

Обратите внимание, что это может сильно потерпеть неудачу, если индекс имел несвязанные записи для old-name.txt, поскольку для файла могут быть другие этапы (от 1 до 3).

Ответ 2

@torek дал очень четкий и полный ответ. Там много очень полезной детали; это хорошо стоит прочитать.

Но, ради тех, кто спешил, суть самого простого решения:

Что мы делаем сейчас, сначала вручную помещаем файл обратно под его старый имя, затем используйте git mv, чтобы снова переключиться на новое имя:

$ mv new-name.txt old-name.txt
$ git mv old-name.txt new-name.txt

(Это был только mv назад, который я отсутствовал, чтобы сделать git mv возможным.)

Пожалуйста, поддержите @torek ответ, если найдете это полезным.