git copy file, в отличие от `git mv`

Я понимаю, что git работает, изменяя содержимое файлов. У меня есть несколько файлов, которые я хочу скопировать. Чтобы полностью предотвратить путаницу в git, есть ли какая-нибудь команда git, которую можно использовать для копирования файлов в другой каталог (не mv, но cp), а также для размещения файлов?

2 ответа

Решение

Короткий ответ - просто "нет". Но это еще не все; это просто требует некоторого фона. (И как JDB предлагает в комментарии, я упомяну почему git mv существует как удобство.)

Чуть дольше: вы правы в том, что Git будет различать файлы, но вы можете ошибаться, когда Git выполняет эти сравнения файлов.

Модель внутреннего хранения Git предполагает, что каждый коммит является независимым снимком всех файлов в этом коммите. Версия каждого файла, который входит в новый коммит, т. Е. Данные в моментальном снимке для этого пути, соответствуют тому, что находится в индексе по этому пути во время выполнения. git commit, 1

Фактическая реализация, на первом уровне, заключается в том, что каждый моментальный снимок файла записывается в сжатом виде в виде объекта BLOB-объекта в базе данных Git. Объект BLOB-объекта совершенно независим от каждой предыдущей и последующей версии этого файла, за исключением одного особого случая: если вы делаете новый коммит, в котором данные не были изменены, вы будете повторно использовать старый BLOB-объект. Таким образом, когда вы делаете два коммита подряд, каждый из которых содержит 100 файлов, и изменяется только один файл, второй коммит повторно использует 99 предыдущих больших двоичных объектов, и ему нужно сделать снимок только одного фактического файла в новый большой двоичный объект. 2

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

Теперь все эти независимые объекты BLOB-объектов в конечном итоге занимают непомерное количество места. На этом этапе Git может "упаковать" объекты в .pack файл. Он будет сравнивать каждый объект с некоторым выбранным набором других объектов - они могут быть более ранними или более поздними в истории и иметь одно и то же имя файла или разные имена файлов, и в теории Git может даже сжимать объект фиксации против объекта BLOB-объекта или наоборот (хотя на практике это не так) - и попробуйте найти способ представить множество больших двоичных объектов, используя меньше дискового пространства. Но результатом по-прежнему, по крайней мере, логически, является серия независимых объектов, полностью восстановленных в первоначальном виде с использованием их хеш-идентификаторов. Таким образом, даже несмотря на то, что объем используемого дискового пространства уменьшается (мы надеемся!) На этом этапе, все объекты точно такие же, как и раньше.

Так когда же Git сравнивает файлы? Ответ: только тогда, когда вы попросите об этом. "Время запроса" - это когда вы бежите git diff либо напрямую:

git diff commit1 commit2

или косвенно:

git show commit  # roughly, `git diff commit^@ commmit`
git log -p       # runs `git show commit`, more or less, on each commit

В этом есть куча тонкостей, в частности, git show будет производить то, что Git называет комбинированными различиями при запуске на коммитах слияния, тогда как git log -p обычно просто пропускает правки по diff для коммитов слияния, но это, наряду с некоторыми другими важными случаями, это когда Git запускается git diff,

Это когда Git работает git diff что вы можете (иногда) попросить его найти или не найти копии. -C флаг, также пишется --find-copies=<number> просит Гита найти копии. --find-copies-harder Флаг (который в документации Git называется "вычислительно дорогим") выглядит для копий сложнее, чем обычный -C флаг. -B (нарушить неподходящие пары) влияет параметр -C, -M ака --find-renames=<number> опция также влияет -C, git merge Команде можно сказать настроить уровень обнаружения переименования, но - по крайней мере, в настоящее время - нельзя сказать, чтобы найти копии или нарушить несоответствующие пары.

(Одна команда, git blame, выполняет несколько иной поиск копий, и вышеизложенное не относится к нему полностью.)


1 Если вы бежите git commit --include <paths> или же git commit --only <paths> или же git commit <paths> или же git commit -a думайте об этом как об изменении индекса перед запуском git commit, В особом случае --only Git использует временный индекс, который немного сложнее, но он все еще фиксирует из индекса - он просто использует специальный временный индекс вместо обычного. Чтобы сделать временный индекс, Git копирует все файлы из HEAD совершать, а затем накладывает те с --only файлы, которые вы перечислили. В других случаях Git просто копирует файлы рабочего дерева в обычный индекс, а затем продолжает делать коммит из индекса как обычно.

2 Фактически, фактический снимок, сохраняющий BLOB-объект в хранилище, происходит во время git add, Это тайно делает git commit намного быстрее, так как вы обычно не замечаете дополнительное время, необходимое для запуска git add до того, как вы запустите git commit,


Зачем git mv существует

Какие git mv old new делает это очень грубо:

mv old new
git add new
git add old

Первый шаг достаточно очевиден: нам нужно переименовать рабочую версию файла. Второй шаг аналогичен: нам нужно поместить индексную версию файла на место. Третий, однако, странный: почему мы должны "добавлять" файл, который мы только что удалили? Что ж, git add не всегда добавляет файл: вместо этого, в этом случае он обнаруживает, что файл был в индексе и больше нет.

Мы могли бы также записать этот третий шаг как:

git rm --cached old

Все, что мы действительно делаем, это убираем старое имя из индекса.

Но здесь есть проблема, поэтому я сказал " очень грубо". В индексе есть копия каждого файла, который будет зафиксирован при следующем запуске git commit, Эта копия может не совпадать с копией в рабочем дереве. На самом деле, он может даже не совпадать с HEAD если есть один в HEAD совсем.

Например, после:

echo I am a foo > foo
git add foo

файл foo существует в рабочем дереве и в индексе. Содержимое рабочего дерева и содержимое индекса совпадают. Но теперь давайте изменим версию рабочего дерева:

echo I am a bar > foo

Теперь индекс и дерево работ отличаются. Предположим, мы хотим переместить основной файл из foo в bar, но - по какой-то странной причине 3 - мы хотим сохранить содержимое индекса без изменений. Если мы бежим:

mv foo bar
git add bar

мы получим I am a bar внутри нового индексного файла. Если мы затем удалим старую версию foo из индекса мы теряем I am a foo версия целиком.

Так, git mv foo bar на самом деле не перемещать-и-добавлять-дважды, или перемещать-добавлять-и-удалять. Вместо этого он переименовывает файл рабочего дерева и переименовывает копию в индексе. Если индексная копия исходного файла отличается от файла рабочего дерева, переименованная индексная копия по-прежнему отличается от переименованной копии рабочего дерева.

Это очень трудно сделать без команды интерфейса git mv, 4 Конечно, если вы планируете git add все, вам не нужны все эти вещи в первую очередь. И стоит отметить, что если git cp существует, вероятно, она также должна копировать версию индекса, а не версию рабочего дерева, при создании копии индекса. Так git cp действительно должен существовать. Там также должно быть git mv --after вариант а-ля меркуриал hg mv --after, Оба должны существовать, но в настоящее время нет. (Тем не менее, есть меньше призывов к любому из них, чем к git mv, по-моему.)


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

4 Это не невозможно: git ls-index --stage получит необходимую информацию из индекса, как сейчас, и git update-index позволяет вносить произвольные изменения в индекс. Вы можете объединить эти два, и некоторые сложные сценарии оболочки или программирование на более хорошем языке, чтобы создать что-то, что реализует git mv --after а также git cp,

Это хакерский подход , но его можно решить, обманув сам git, выполнив переименование отдельной ветки и заставив git сохранять оба файла в слиянии.

      git checkout -b rename-branch
git mv a.txt b.txt
git commit -m "Renaming file"
# if you did a git blame of b.txt, it would _follow_ a.txt history, right?
git checkout main
git merge --no-ff --no-commit rename-branch
git checkout HEAD -- a.txt # get the file back
git commit -m "Not really renaming file"

При прямой копии вы получите следующее:

      $ git log --graph --oneline --name-status
* 70f03aa (HEAD -> master) COpying file straight
| A     new_file.txt
* efc04f3 (first) First commit for file
  A     hello_world.txt
$ git blame -s new_file.txt
70f03aab 1) I am here
70f03aab 2) 
70f03aab 3) Yes I am
$ git blame -s hello_world.txt
^efc04f3 1) I am here
^efc04f3 2) 
^efc04f3 3) Yes I am

Используя переименование на стороне и вернув файл обратно, вы получите:

      $ git log --oneline --graph master2 --name-status
*   30b76ab (HEAD, master2) Not really renaming
|\  
| * 652921f Renaming file
|/  
|   R100        hello_world.txt new_file.txt
* efc04f3 (first) First commit for file
  A     hello_world.txt
$ git blame -s new_file.txt
^efc04f3 hello_world.txt 1) I am here
^efc04f3 hello_world.txt 2) 
^efc04f3 hello_world.txt 3) Yes I am
$ git blame -s hello_world.txt
^efc04f3 1) I am here
^efc04f3 2) 
^efc04f3 3) Yes I am

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

Другие вопросы по тегам