Ответ 1
[слегка изменено и отформатировано]. С учетом клонирования, созданного с помощью
git clone --single-branch --depth 1 url directory
, как я могу его обновить для достижения того же результата, что иrm -rf directory; git clone --single-branch --depth 1 url directory
?
Обратите внимание, что --single-branch
является значением по умолчанию при использовании --depth 1
. Единственная ветвь - это та, которую вы даете с помощью -b
. Там длинная сторона, которая идет здесь об использовании -b
с тегами, но я оставлю это позже. Если вы не используете -b
, ваш Git запрашивает "вверх по течению" Git -the Git в URL-адресе, в какой ветке он выгружен, и притворяется, что вы использовали -b thatbranch
. Это означает, что важно соблюдать осторожность при использовании --single-branch
без -b
, чтобы убедиться, что текущая ветка репозитория выше, и, конечно, когда вы используете -b
, чтобы убедиться, что аргумент ветки вам дать действительно называет ветку, а не тег.
Простой ответ в основном таков: с двумя небольшими изменениями:
После fooobar.com/questions/8319/... я попробовал
git fetch --depth 1; git reset --hard origin/master
, но две вещи: сначала я не понимаю, зачем нуженgit reset
, во-вторых, хотя файлы похоже, обновлен, некоторые старые файлы остаются, аgit clean -df
не удаляет эти файлы.
Два небольших изменения: убедитесь, что вы используете origin/branchname
вместо этого, и добавьте -x
(git clean -d -f -x
или git clean -dfx
) на шаг git clean
. Что касается того, почему это становится немного сложнее.
Что происходит
Без --depth 1
шаг git fetch
вызывает другой Git и получает из него список имен ветвей и соответствующих идентификаторов хеширования. То есть, он находит список всех ветвей вверх и их текущих коммитов. Затем, поскольку у вас есть репозиторий --single-branch
, ваш Git выкидывает все, кроме единственной ветки, и передает все Git, чтобы связать текущую фиксацию с данными, которые у вас уже есть в вашем репозитории.
С --depth 1
ваш Git не беспокоится о подключении нового фиксации к старым историческим фиксациям вообще. Вместо этого он получает только одно коммит и другие Git объекты, необходимые для завершения этого коммита. Затем он записывает дополнительную запись "мелкого трансплантата", чтобы отметить, что один совершает в качестве нового псевдокорневого коммита.
Обычный (неглубокий) клон и выборка
Все это связано с тем, как Git ведет себя, когда вы используете обычный (не-мелкий, неединичный) клон: git fetch
вызывает восходящий Git, получает список всего, а затем приносит все, что у вас еще нет. Вот почему начальный клон настолько медленный, и выборка для обновления обычно такая быстрая: как только вы получаете полный клон, обновления редко бывают очень полезными: может быть, несколько коммитов, может быть, несколько сотен, и большинству этих коммитов больше не нужно.
История репозитория формируется из коммитов. Каждая фиксация называет ее родительскую фиксацию (или для слияний, родительских коммитов, множественное число), в цепочке, которая возвращается назад от "последней фиксации", к предыдущей фиксации, к еще более-предческому фиксации и т.д. Цепочка в конечном итоге останавливается, когда она достигает фиксации, у которой нет родителя, такого как первая фиксация, когда-либо сделанная в репозитории. Этот тип фиксации является фиксацией root.
То есть мы можем нарисовать график коммитов. В действительно простом репозитории граф - это просто прямая линия со всеми стрелками, направленными назад:
o <- o <- o <- o <-- master
Имя master
указывает на четвертый и последний фиксации, который указывает на третью, которая указывает на вторую, которая указывает на первую.
Каждая фиксация несет с собой полный снимок всех файлов, которые входят в это commit. Файлы, которые вообще не изменяются, разделяются между этими коммитами: четвертая фиксация просто "заимствует" неизмененную версию из третьей фиксации, которая "заимствует" ее со второго и т.д. Следовательно, каждая фиксация называет все "Git объекты", которые ей нужны, и Git либо находит эти объекты локально, потому что она уже имеет их, либо использует протокол fetch
, чтобы перевести их из другого, вверх по течению Git. Там есть формат сжатия, называемый "упаковка", и специальный вариант для сетевого переноса, называемый "тонкие пакеты", который позволяет Git сделать это еще лучше /fancier, но принцип прост: Git требуется все и только, те объекты, которые идут с новыми, фиксируют его. Ваш Git решает, имеет ли он эти объекты, а если нет, получает их из своих Git.
Более сложный, более полный граф обычно имеет несколько точек, где он разветвляется, где-то, где он сливается, и несколько имен ветвей, указывающих на разные подсказки ветки:
o--o <-- feature/tall
/
o--o--o---o <-- master
\ /
o--o <-- bug/short
Здесь ветвь bug/short
снова объединяется в master
, а ветка feature/tall
все еще находится в процессе разработки. Имя bug/short
может (возможно) теперь быть полностью удалено: мы больше не нуждаемся в этом, если мы закончили делать коммиты на нем. Конец на вершине master
называет две предыдущие фиксации, включая фиксацию на кончике bug/short
, поэтому, извлекая master
, мы получим комманду bug/short
.
Обратите внимание, что как простой, так и немного более сложный график имеет только один коммит root. Это довольно типично: все репозитории, у которых есть коммиты, имеют по крайней мере один коммит root, так как первая фиксация всегда является фиксацией root; но в большинстве репозиториев есть только один корневой фиксатор. Однако у вас могут быть разные корневые коммиты, как с этим графиком:
o--o
\
o--o--o <-- master
или этот:
o--o <-- orphan
o--o <-- master
На самом деле тот, у кого только один master
, вероятно, был сделан путем слияния orphan
в master
, а затем с удалением имени orphan
.
Прививки и замены
Git долгое время имела (возможно, шаткую) поддержку для трансплантатов, которая была заменена (намного лучше, фактически-твердой) поддержкой для общих замен. Чтобы понять их конкретно, нам нужно добавить к вышесказанному, что каждая фиксация имеет свой собственный уникальный идентификатор. Эти идентификаторы представляют собой большие уродливые 40-символьные хэши SHA-1, face0ff...
и так далее. Фактически, каждый объект Git имеет уникальный идентификатор, хотя для целей графика все, о чем мы заботимся, это коммиты.
Для рисования графов эти большие идентификаторы хэшей слишком болезненны для использования, поэтому мы можем использовать однобуквенные имена A
через Z
. Позвольте снова использовать этот график, но введите однобуквенные имена:
E--H <-- feature/tall
/
A--B--D---G <-- master
\ /
C--F <-- bug/short
Commit H
ссылается на commit E
(E
is H
parent). Commit G
, который является фиксацией слияния, то есть имеет по крайней мере два родителя, возвращает значения как D
, так и F
и т.д.
Обратите внимание, что имена ветвей, feature/tall
, master
и bug/short
, каждая точка указывает на одну фиксацию. Имя bug/short
указывает на фиксацию F
. Вот почему commit F
находится на ветке bug/short
... но так же commit C
. Commit C
находится на bug/short
, потому что он доступен из имени. Имя возвращает нас к F
, а F
возвращает нас к C
, поэтому C
находится на ветке bug/short
.
Обратите внимание, однако, что commit G
, кончик master
, заставит нас зафиксировать F
. Это означает, что commit F
также находится на ветке master
. Это ключевая концепция в Git: commits может быть на одной, много или даже без ветвей. Имя ветки - это всего лишь способ начать работу в графе фиксации. Существуют и другие способы, такие как имена тегов, refs/stash
(который приводит вас к текущему типу: каждый тайник на самом деле представляет собой пару коммитов) и reflogs (которые обычно скрыты от представления, поскольку они обычно просто беспорядочны).
Это также, однако, заставляет нас пересаживаться и заменять. 1 Я не буду описывать замену полностью здесь, поскольку они немного сложнее, но в целом, что Git для всех из них - использовать трансплантат или замену как "вместо". Для конкретного случая коммитов мы хотим, чтобы мы могли изменить или, по крайней мере, притвориться, что нужно изменить - родительский идентификатор или идентификаторы любого фиксации... и для мелких хранилищ, мы хотим иметь возможность притворяться, что в рассматриваемом деле нет родителей.
1 То, как мелкие хранилища используют код трансплантата, не шатко. Для более общего случая я рекомендовал вместо этого использовать git replace
, поскольку это также было и не трясло. Единственное рекомендуемое использование для трансплантатов - или, по крайней мере, было несколько лет назад - разместить их на месте достаточно долго, чтобы запустить git filter-branch
, чтобы скопировать измененную историю прививки, после чего вы должны просто полностью отказаться от привитой истории. Вы можете использовать git replace
для этой цели, но, в отличие от трансплантатов, вы можете использовать git replace
постоянно или полу-постоянно, без необходимости git filter-branch
.
Создание мелкого клона
Чтобы сделать неглубокий клон глубины-1 текущего состояния восходящего репозитория, мы выберем одно из трех названий ветвей - feature/tall
, master
или bug/short
- и переведем его в идентификатор фиксации, Затем мы напишем специальную запись трансплантата, в которой говорится: "Когда вы видите это коммит, притворитесь, что у него нет родительских коммитов, т.е. Является фиксацией корня".
Скажем, мы выбираем master
. Имя master
указывает на фиксацию G
, поэтому для создания мелкого клонирования commit G
мы получаем commit G
из восходящего Git, как обычно, но затем записываем специальную запись трансплантата, которая утверждает commit G
не имеет родителей. Мы помещаем это в наш репозиторий, и теперь наш график выглядит следующим образом:
G <-- master, origin/master
Те родительские идентификаторы по-прежнему находятся внутри G
; это просто, что каждый раз, когда мы Git используем или показываем нам историю, он сразу же "трансформирует" ничего-на-все, так что G
кажется корневой фиксацией для целей отслеживания истории.
Обновление неглубокого клона, который мы сделали ранее
Но что, если у нас уже есть (глубина-1 мелкий) клон, и мы хотим его обновить? Ну, это не проблема. Скажем, мы сделали мелкий клон обратной линии, когда master
указал на commit B
, перед новыми ветвями и исправлением ошибки. Это означает, что у нас есть это:
B <-- master, origin/master
В то время как B
реальный родитель A
, у нас есть запись с коротким клоном, в которой говорится: "Притворяться B
является фиксацией корня". Теперь мы git fetch --depth 1
, который смотрит вверх по течению master
- вещь, которую мы называем origin/master
, и видит commit G
. Мы берем commit G
из восходящего потока вместе с его объектами, но намеренно не захватываем фиксации D
и F
. Затем мы обновляем наши записи в блоке с мелким клоном, чтобы сказать: "Притворяться G
тоже является фиксацией корня":
B <-- master
G <-- origin/master
В нашем репозитории теперь есть две корневые коммиты: имя master
(по-прежнему) указывает на commit B
, родители которого мы (все еще) притворяемся несуществующими, а имя origin/master
указывает на G
, родители которых мы притворяемся несуществующими.
Вот почему вам нужно git reset
В обычном репозитории вы можете использовать git pull
, который действительно является git fetch
, а затем git merge
. Но git merge
требует истории, и у нас ее нет: мы подделали Git с притворяющимся корнем, и у них нет истории за ними. Поэтому мы должны использовать git reset
.
Что git reset
делает, это немного сложно, потому что оно может влиять на три разных вещи: имя ветки, индекс и дерево работы. Мы уже видели, что такое имена ветвей: они просто указывают на одно (конкретное) коммит, которое мы называем концом ветки. Это оставляет индекс и рабочее дерево.
Рабочее дерево легко объяснить: это где все ваши файлы. Что это: не больше и не меньше. Это там, так что вы действительно можете использовать Git: Git - это все, что нужно хранить на каждом из когда-либо сделанных коммитов, чтобы все они могли быть восстановлены. Но они в формате, бесполезном для простых смертных. Чтобы быть использованным, файл, или, что более типично, целую стоимость фиксации файлов, должен быть извлечен в его нормальный формат. Рабочее дерево - это то, где это происходит, и затем вы можете работать над ним и делать с ним новые коммиты.
Индекс немного сложнее объяснить. Это что-то особенное для Git: другие системы управления версиями не имеют одного, или если у них что-то похожее, они не раскрывают его. Git. Git index - это то место, где вы сохраняете следующую фиксацию, но это означает, что она начинает удерживать текущую фиксацию, которую вы извлекли в дерево работы, а Git использует это, чтобы сделать Git быстро. Мы расскажем об этом немного.
То, что git reset --hard
делает, влияет на все три: имя ветки, индекс и дерево работы. Он перемещает имя ветки так, чтобы оно указывало на (возможно, другое) commit. Затем он обновляет индекс в соответствии с этим фиксацией и обновляет дерево работы в соответствии с новым индексом.
Следовательно:
git reset --hard origin/master
сообщает Git искать origin/master
. Поскольку мы запустили наш git fetch
, который теперь указывает на commit G
. Git затем делает наш мастер-текущую (и только) ветку - также указывать на фиксацию G
, а затем обновляет наш индекс и рабочее дерево. Теперь наш график выглядит следующим образом:
B [abandoned - but see below]
G <-- master, origin/master
Теперь master
и origin/master
и имя commit G
, и commit G
- это тот, который выгружен в дерево работы.
Зачем вам нужен git clean -dfx
Ответ здесь немного сложный, но обычно он "вы не делаете" (нужно git clean
).
Когда вам понадобится git clean
, это потому, что вы или что-то запустили добавленные файлы в свое дерево работы, о котором вы не сказали Git. Это незатребованные и/или проигнорированные файлы. Использование git clean -df
приведет к удалению неиспользуемых файлов (и пустых каталогов); добавление -x
также приведет к удалению игнорируемых файлов.
Подробнее о различии между "untracked" и "ignored" см. этот ответ.
Почему вам не нужен git clean
: индекс
Я упомянул выше, что вам обычно не нужно запускать git clean
. Это из-за индекса. Как я сказал ранее, индекс Git - это, в основном, "следующая фиксация". Если вы никогда не добавляете свои собственные файлы - если вы просто используете git checkout
, чтобы проверить различные существующие коммиты, которые у вас были все время, или что вы добавили с помощью git fetch
; или если вы используете git reset --hard
для перемещения имени ветки, а также для переключения индекса и рабочего дерева на другой фиксатор - тогда все, что находится в индексе прямо сейчас, существует, потому что ранний git checkout
(или git reset
) помещает его в индекс, а также в дерево работы.
Другими словами, индекс имеет короткий и быстрый для Git доступ к сводке или manifest, описывающий текущую работу -tree. Git использует это, чтобы узнать, что сейчас находится в дереве. Когда вы попросите Git переключиться на другую фиксацию, через git checkout
или git reset --hard
, Git может быстро сравнить существующий индекс с новым фиксатором. Любые файлы, которые были изменены, Git должны извлечь из новой фиксации (и обновить индекс). Любые добавленные файлы, Git также должны извлекать (и обновлять индекс). Любые файлы, которые ушли, которые находятся в существующем индексе, но не в новом commit-Git, должны удалить... и то, что делает Git. Git обновляет, добавляет и удаляет эти файлы в дереве работы, как показано в сравнении между текущим индексом и новым фиксатором.
Это означает, что если вам действительно нужно git clean
, вы должны сделать что-то вне Git, что добавил файлы. Эти добавленные файлы не находятся в индексе, поэтому по определению, они не отслеживаются и/или игнорируются. Если они просто не отслеживаются, git clean -f
удалит их, но если они будут проигнорированы, только git clean -fx
удалит их. (Вы хотите, чтобы -d
просто удалял каталоги, которые являются или становятся пустыми во время очистки.)
Заброшенные коммиты и сбор мусора
Я упомянул и нарисовал обновленный мелкий график, что, когда мы git fetch --depth 1
, а затем git reset --hard
, мы завершаем отказ от предыдущей фиксации мелкого графа глубины-1. (На графике, нарисованном мной, это было commit B
.) Однако в Git заброшенные коммиты редко прекращаются - по крайней мере, не сразу. Вместо этого некоторые специальные имена, такие как ORIG_HEAD
, на какое-то время зависают на них, и каждая ссылка-ветки и теги являются формами reference-carry с ним журнал "предыдущих значений".
Вы можете отобразить каждый рефлок с помощью git reflog refname
. Например, git reflog master
показывает вам не только те, которые фиксируют имена master
, но также и то, что они объявили в прошлом. Существует также reflog для HEAD
, что показывает git reflog
по умолчанию.
Записи Reflog в конце концов истекают. Их точная продолжительность варьируется, но по умолчанию они имеют право на истечение через 30 дней в некоторых случаях и 90 дней в других. Как только они истекают, эти записи reflog больше не защищают заброшенные коммиты (или, для ссылок на аннотированные теги, теги-объекты аннотированных тегов не должны перемещаться, поэтому этот случай не должен возникать, но если это произойдет - если вы принудительно Git для перемещения тега - он просто обрабатывается так же, как и все другие ссылки).
Как только любой объект Git object-commit, аннотированный тег, "дерево" или "blob" (файл) - действительно не найден, Git разрешено удалить его для реального. 2 Только в этот момент данные базового репозитория для коммитов и файлов уходят. Даже тогда это происходит только тогда, когда что-то выполняется git gc
. Таким образом, неглубокий репозиторий, обновленный с помощью git fetch --depth 1
, не совсем такой же, как новый клон с --depth 1
: у мелкого репозитория, вероятно, есть несколько затяжных имен для первоначальных коммитов и не будет удалять дополнительные объекты репозитория до тех пор, пока эти имена не истекут или иначе очищены.
2 Помимо контрольной проверки, объекты получают минимальное время до истечения срока их действия. По умолчанию используется две недели. Это предотвращает удаление git gc
временных объектов, создаваемых Git, но пока не установит ссылку. Например, при создании нового коммита Git сначала превращает индекс в ряд объектов tree
, которые ссылаются друг на друга, но не имеют ссылки верхнего уровня. Затем он создает новый объект commit
, который ссылается на дерево верхнего уровня, но еще не относится к фиксации. Наконец, он обновляет текущее название ветки. Пока этот последний шаг не завершится, деревья и новый коммит недоступны!
Особые соображения для --single-branch
и/или неглубоких клонов
Я отметил выше, что имя, которое вы даете git clone -b
, может относиться к тегу. Для обычных (не-мелких или неединичных) клонов это работает так же, как и следовало ожидать: вы получаете обычный клон, а затем Git выполняет git checkout
по имени тега. В результате получается обычная отдельная головка в совершенно обычном клоне.
Однако с неглубокими или одновитковыми клонами существует несколько необычных последствий. Все это, в какой-то мере, результат Git, позволяющий реализовать реализацию.
Во-первых, , если вы используете --single-branch
, Git изменяет обычную конфигурацию fetch
в новом репозитории. Обычная конфигурация fetch
зависит от имени, которое вы выбрали для пульта дистанционного управления, но по умолчанию это origin
, поэтому я просто использую origin
здесь. Он гласит:
fetch = +refs/heads/*:refs/remotes/origin/*
Опять же, это нормальная конфигурация для обычного (не односегментного) клона. Эта конфигурация сообщает git fetch
, что выбрать, что означает "все ветки". Однако, когда вы используете --single-branch
, вместо этого вы получаете строку извлечения, которая ссылается только на одну ветвь:
fetch = +refs/heads/zorg:refs/remotes/origin/zorg
если вы клонируете ветвь zorg
.
Какую ветвь вы клонируете, ту, которая входит в строку fetch
. Каждое будущее git fetch
будет подчиняться этой строке 3 поэтому вы не будете получать какие-либо другие ветки. Если вы хотите получить другие ветки позже, вам придется изменить эту строку или добавить дополнительные строки.
Во-вторых, , если вы используете --single-branch
, и то, что вы клонируете, является тегом, Git будет помещать довольно нечетную строку fetch
. Например, с git clone --single-branch -b v2.1 ...
я получаю:
fetch = +refs/tags/v2.1:refs/tags/v2.1
Это означает, что вы не получите никаких ветвей, и если кто-то не переместил тег, 4git fetch
ничего не сделает!
В-третьих, поведение тега по умолчанию немного странно из-за того, что теги git clone
и git fetch
получают. Помните, что теги - это просто ссылка на одну конкретную фиксацию, точно так же как ветки и все другие ссылки. Между ветвями и тегами существуют два ключевых отличия: ветки, как ожидается, будут перемещаться (а теги - нет), а ветки получаются переименованными (а теги - нет).
Помните, что во всем вышеизложенном мы продолжаем находить, что другой (вверх по течению) Git master
становится нашим origin/master
и т.д. Это пример процесса переименования. Мы также кратко заметили, как именно это переименование работает через строку fetch =
: наш Git принимает их refs/heads/master
и изменяет его на наш refs/remotes/origin/master
. Это имя не только разное (origin/master
), но буквально не может быть таким же, как у любой из наших ветвей. Если мы создадим ветвь с именем origin/master
, 5 эта ветка "полное имя" на самом деле refs/heads/origin/master
, которая отличается от другого полного имени refs/remotes/origin/master
. Это только тогда, когда Git использует более короткое имя, у которого есть одна (регулярная, локальная) ветвь с именем origin/master
и другая другая (удаленное отслеживание) ветвь с именем origin/master
. (Это очень похоже на группу, где каждого называют Брюсом.)
Теги не проходят через все это. Тег v2.1
называется только refs/tags/v2.1
. Это означает, что нет возможности отделить "свой" тег от "вашего" тега. У вас может быть либо тэг, либо тег. Пока никто не перемещает тег, это не имеет значения: если у вас есть тег, он должен указывать на один и тот же объект. (Если кто-то начинает перемещать теги, все становится некрасиво.)
В любом случае Git реализует "нормальную" выборку тегов по простому правилу: 6, когда Git уже имеет фиксацию, если некоторые имена тегов, которые совершают, Git также копирует тег. С обычными клонами первый клон получает все теги, а затем последующие операции git fetch
получают новые теги. Однако мелкий клон по определению пропускает некоторые фиксации (а), а именно все, что ниже любой точки трансплантата на графике. Эти коммиты не будут подбирать теги. Они не могут: иметь теги, вам нужно будет совершить коммиты. Git не допускается (кроме как через мелкие трансплантаты) иметь идентификатор фиксации без фактического фиксации.
3 Вы можете дать git fetch
некоторые refspec в командной строке, и они переопределяют значение по умолчанию. Это относится только к выборке по умолчанию. Вы также можете использовать несколько строк fetch =
в конфигурации, например, для получения только определенного набора ветвей, хотя обычный способ "де-ограничивать" изначально однократный клон состоит в том, чтобы вернуть обычный +refs/heads/*:refs/remotes/origin/*
выборка.
4 Так как теги не должны двигаться, мы могли бы просто сказать "это ничего не делает". Однако, если они перемещаются, +
в refspec представляет флаг силы, поэтому тег завершается.
5Не делай этого. Это сбивает с толку. Git будет обрабатывать его просто отлично: локальная ветвь находится в локальном пространстве имен, а ветвь удаленного отслеживания находится в пространстве имен удаленных отслеживаний, но это действительно запутывает.
6 Это правило не соответствует документации. Я тестировал версию Git версии 2.10.1; более старые Gits могут использовать другой метод.