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 будет следовать за отдельной веткой, где происходит переименование, и тогда он сможет перейти к исходному файлу после копии только потому, что это сделано в этой ветке.