руководство пользователя
Введение
Git — это быстрая распределённая система контроля версий.
Это руководство рассчитано на читателей, обладающих базовыми навыками работы с командной строкой UNIX, но не имеющих предварительных знаний о Git.
В главах Репозитории и ветки и Изучение истории Git объясняется, как получить и изучить проект с помощью git. Прочитайте их, чтобы узнать, как собрать и протестировать конкретную версию программного проекта, найти регрессии и так далее.
Тем, кто планирует заниматься реальной разработкой, также стоит прочитать главы Разработка с Git и Совместная разработка.
В следующих главах рассматриваются более узкоспециализированные темы.
Полная справочная документация доступна на страницах руководства или с помощью команды git-help[1]. Например, для команды git clone <repo> можно использовать:
$ man git-clone
или:
$ git help clone
Во втором случае можно воспользоваться любым удобным средством просмотра руководств. Дополнительные сведения см. в git-help[1].
Краткий обзор команд Git без пояснений приведён в разделе Краткий справочник по Git.
Наконец, в разделе Замечания и список задач для этого руководства описано, как помочь сделать это руководство более полным.
Репозитории и ветки
Как получить репозиторий Git
При чтении этого руководства вам пригодится репозиторий Git для экспериментов.
Лучше всего получить его с помощью команды git-clone[1], загрузив копию существующего репозитория. Если вы ещё не выбрали проект, вот несколько интересных примеров:
# Git itself (approx. 40MB download):
$ git clone git://git.kernel.org/pub/scm/git/git.git
# the Linux kernel (approx. 640MB download):
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git Первоначальное клонирование крупного проекта может занять много времени, но выполнить его нужно будет только один раз.
Команда clone создаёт новый каталог с именем проекта (git или linux в приведённых выше примерах). Перейдя в этот каталог с помощью cd, вы увидите копию файлов проекта, называемую рабочим деревом, а также специальный каталог верхнего уровня с именем .git, в котором содержится вся информация об истории проекта.
Как переключиться на другую версию проекта
Git лучше всего рассматривать как инструмент для хранения истории набора файлов. Он хранит историю в виде сжатого набора взаимосвязанных снимков содержимого проекта. В Git каждая такая версия называется коммитом.
Эти снимки не обязательно выстроены в одну линию от самого старого к самому новому: разработка может одновременно идти по нескольким параллельным линиям, называемым ветками, которые могут сливаться и расходиться.
Один репозиторий Git может отслеживать разработку в нескольких ветках. Для этого он хранит список указателей HEAD, ссылающихся на последний коммит каждой ветки. Команда git-branch[1] показывает список указателей на вершины веток:
$ git branch * master
Только что клонированный репозиторий содержит один указатель на вершину ветки, по умолчанию называемой "master". Рабочий каталог при этом инициализируется состоянием проекта, на которое ссылается этот указатель.
В большинстве проектов также используются теги. Как и указатели HEAD, теги являются ссылками на историю проекта. Их список можно вывести командой git-tag[1]:
$ git tag -l v2.6.11 v2.6.11-tree v2.6.12 v2.6.12-rc2 v2.6.12-rc3 v2.6.12-rc4 v2.6.12-rc5 v2.6.12-rc6 v2.6.13 ...
Предполагается, что теги всегда указывают на одну и ту же версию проекта, а указатели HEAD перемещаются вперёд по мере развития проекта.
Создайте новый указатель на вершину ветки, ссылающийся на одну из этих версий, и переключитесь на неё с помощью git-switch[1]:
$ git switch -c new v2.6.13
После этого рабочий каталог будет содержать файлы проекта в том состоянии, в котором они находились при создании тега v2.6.13. Команда git-branch[1] покажет две ветки; звёздочкой будет отмечена текущая ветка:
$ git branch master * new
Если вы решите, что предпочли бы увидеть версию 2.6.17, можно переместить текущую ветку на v2.6.17 с помощью команды
$ git reset --hard v2.6.17
Обратите внимание: если указатель текущей ветки был единственной ссылкой на определённую точку истории, сброс ветки может лишить вас возможности найти историю, на которую она раньше указывала. Поэтому используйте эту команду осторожно.
Как понимать историю: коммиты
Каждое изменение в истории проекта представлено коммитом. Команда git-show[1] показывает последний коммит текущей ветки:
$ git show
commit 17cf781661e6d38f737f15f53ab552f1e95960d7
Author: Linus Torvalds <torvalds@ppc970.osdl.org.(none)>
Date: Tue Apr 19 14:11:06 2005 -0700
Remove duplicate getenv(DB_ENVIRONMENT) call
Noted by Tony Luck.
diff --git a/init-db.c b/init-db.c
index 65898fa..b002dc6 100644
--- a/init-db.c
+++ b/init-db.c
@@ -7,7 +7,7 @@
int main(int argc, char **argv)
{
- char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
+ char *sha1_dir, *path;
int len, i;
if (mkdir(".git", 0755) < 0) { Как видите, в коммите указано, кто внёс последнее изменение, что именно было сделано и почему.
У каждого коммита есть 40-значный шестнадцатеричный идентификатор, иногда называемый «именем объекта» или «идентификатором SHA-1». Он отображается в первой строке вывода git show. Обычно на коммит можно ссылаться по короткому имени, например по имени тега или ветки, но это длинное имя тоже может быть полезно. И главное: это глобально уникальное имя коммита. Поэтому, если вы сообщите кому-нибудь имя объекта (например, по электронной почте), можно быть уверенным, что в репозитории этого человека оно будет обозначать тот же коммит, что и в вашем (при условии, что этот коммит вообще есть в его репозитории). Поскольку имя объекта вычисляется как хеш содержимого коммита, коммит не может измениться, не изменив при этом своего имени.
В разделе Понятия Git мы увидим, что всё, хранящееся в истории Git, включая данные файлов и содержимое каталогов, сохраняется в объекте, имя которого является хешем его содержимого.
Как понимать историю: коммиты, родители и достижимость
У каждого коммита, кроме самого первого коммита проекта, есть родительский коммит, показывающий, что происходило до него. Если идти по цепочке родительских коммитов, в конце концов можно вернуться к началу проекта.
Однако коммиты не образуют простой список: Git позволяет линиям разработки расходиться и затем снова сходиться. Точка, в которой две линии разработки сходятся, называется «слиянием». Поэтому коммит, представляющий слияние, может иметь несколько родителей, каждый из которых представляет последний коммит одной из линий разработки, ведущих к этой точке.
Лучше всего увидеть, как это работает, с помощью команды gitk[1]. Запустите gitk в репозитории Git и найдите коммиты слияния, чтобы понять, как Git организует историю.
Далее мы будем говорить, что коммит X «достижим» из коммита Y, если X является предком Y. Иными словами, Y является потомком X, или от коммита Y к коммиту X ведёт цепочка родительских коммитов.
Как понимать историю: диаграммы истории
Иногда мы будем изображать историю Git с помощью диаграмм, подобных приведённой ниже. Коммиты обозначаются символом "o", а связи между ними — линиями, нарисованными с помощью символов - / и \. Время движется слева направо:
o--o--o <-- Branch A
/
o--o--o <-- master
\
o--o--o <-- Branch B Если нам понадобится упомянуть конкретный коммит, символ "o" может быть заменён другой буквой или цифрой.
Как понимать историю: что такое ветка?
Когда важна точность, мы будем использовать слово «ветка» для обозначения линии разработки, а выражение «указатель на вершину ветки» (или просто «указатель HEAD») — для обозначения ссылки на последний коммит ветки. В приведённом выше примере указатель на вершину ветки с именем "A" указывает на один конкретный коммит, но мы говорим, что линия из трёх коммитов, ведущая к нему, целиком относится к «ветке A».
Однако, если это не приведёт к путанице, мы часто используем термин «ветка» как для веток, так и для указателей на их вершины.
Работа с ветками
Создавать, удалять и изменять ветки быстро и просто. Вот краткое описание команд:
-
gitbranch -
вывести список всех веток.
-
gitbranch<branch> -
создать новую ветку с именем <branch>, ссылающуюся на ту же точку истории, что и текущая ветка.
-
gitbranch<branch> <start-point> -
создать новую ветку с именем <branch>, ссылающуюся на <start-point>. Указать эту точку можно любым удобным способом, в том числе с помощью имени ветки или тега.
-
gitbranch-d<branch> -
удалить ветку <branch>. Если ветка не была полностью слита с вышестоящей веткой или не входит в текущую ветку, команда завершится с предупреждением.
-
gitbranch-D<branch> -
удалить ветку <branch> независимо от того, была ли она слита.
-
gitswitch<branch> -
сделать текущей ветку <branch> и обновить рабочий каталог в соответствии с версией, на которую ссылается <branch>.
-
gitswitch-c<new> <start-point> -
создать новую ветку <new>, ссылающуюся на <start-point>, и переключиться на неё.
Специальный символ "HEAD" всегда можно использовать для обозначения текущей ветки. Фактически Git запоминает текущую ветку в файле с именем HEAD в каталоге .git:
$ cat .git/HEAD ref: refs/heads/master
Просмотр старой версии без создания новой ветки
Команда git switch обычно ожидает указатель на вершину ветки, но при вызове с параметром --detach принимает и произвольный коммит. Например, можно переключиться на коммит, на который ссылается тег:
$ git switch --detach v2.6.17 Note: checking out 'v2.6.17'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another switch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command again. Example: git switch -c new_branch_name HEAD is now at 427abfa Linux v2.6.17
После этого HEAD будет указывать на SHA-1 коммита, а не на ветку, и команда git branch покажет, что вы больше не находитесь в ветке:
$ cat .git/HEAD 427abfa28afedffadfca9dd8b067eb6d36bac53f $ git branch * (detached from v2.6.17) master
В этом случае говорят, что HEAD «отсоединён».
Это простой способ переключиться на определённую версию, не придумывая имя для новой ветки. Позже, если вы решите это сделать, для этой версии всё ещё можно создать новую ветку или тег.
Просмотр веток удалённого репозитория
Ветка "master", созданная во время клонирования, является копией HEAD репозитория, из которого вы клонировали проект. В этом репозитории могли быть и другие ветки, и локальный репозиторий хранит ветки, отслеживающие каждую из них. Они называются удалёнными отслеживаемыми ветками; их можно просмотреть с помощью параметра -r команды git-branch[1]:
$ git branch -r origin/HEAD origin/html origin/maint origin/man origin/master origin/next origin/seen origin/todo
В этом примере "origin" называется удалённым репозиторием или, сокращённо, «удалённым репозиторием». С нашей точки зрения, ветки этого репозитория называются «удалёнными ветками». Перечисленные выше удалённые отслеживаемые ветки были созданы на основе удалённых веток во время клонирования и будут обновляться командами git fetch (отсюда git pull) и git push. Подробнее см. в разделе Обновление репозитория с помощью git fetch.
Можно начать разработку на основе одной из этих удалённых отслеживаемых веток, создав собственную ветку, как и в случае с тегом:
$ git switch -c my-todo-copy origin/todo
Можно также переключиться непосредственно на origin/todo, чтобы изучить её или создать разовое исправление. См. раздел Отсоединённый HEAD.
Обратите внимание: имя "origin" — это просто имя, которое Git по умолчанию использует для ссылки на репозиторий, из которого вы клонировали проект.
Имена веток, тегов и других ссылок
Ветки, удалённые отслеживаемые ветки и теги — всё это ссылки на коммиты. Всем ссылкам присваиваются имена в виде путей, разделённых косыми чертами и начинающихся с refs. Имена, которые мы использовали до сих пор, являются сокращёнными вариантами:
-
Ветка
test— это сокращённое имя дляrefs/heads/test. -
Тег
v2.6.18— это сокращённое имя дляrefs/tags/v2.6.18. -
origin/master— это сокращённое имя дляrefs/remotes/origin/master.
Полное имя иногда бывает полезно, например если существуют тег и ветка с одинаковым именем.
(Новые ссылки фактически хранятся в каталоге .git/refs по пути, соответствующему их имени. Однако для повышения эффективности они могут быть упакованы в один файл; см. git-pack-refs[1].)
Ещё одно удобное сокращение: на HEAD репозитория можно ссылаться, просто используя имя этого репозитория. Так, например, "origin" обычно является сокращённым обозначением ветки HEAD в репозитории "origin".
Полный список путей, по которым Git ищет ссылки, и порядок выбора при наличии нескольких ссылок с одинаковым сокращённым именем приведены в разделе "SPECIFYING REVISIONS" документации gitrevisions[7].
Обновление репозитория с помощью git fetch
После клонирования репозитория и создания нескольких собственных коммитов вам может понадобиться проверить наличие обновлений в исходном репозитории.
Команда git-fetch без аргументов обновит все удалённые отслеживаемые ветки до последних версий, найденных в исходном репозитории. Она не затронет ваши собственные ветки — даже ветку "master", созданную при клонировании.
Получение веток из других репозиториев
С помощью git-remote[1] можно отслеживать ветки и из других репозиториев, а не только из того, откуда вы клонировали проект:
$ git remote add staging git://git.kernel.org/.../gregkh/staging.git $ git fetch staging ... From git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging * [new branch] master -> staging/master * [new branch] staging-linus -> staging/staging-linus * [new branch] staging-next -> staging/staging-next
Новые удалённые отслеживаемые ветки будут сохранены под сокращённым именем, заданным в команде git remote add. В данном случае это staging:
$ git branch -r origin/HEAD -> origin/master origin/master staging/master staging/staging-linus staging/staging-next
При последующем запуске команды git fetch <remote> будут обновлены удалённые отслеживаемые ветки указанного <remote>.
Если просмотреть файл .git/config, вы увидите, что Git добавил новый раздел:
$ cat .git/config
...
[remote "staging"]
url = git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging.git
fetch = +refs/heads/*:refs/remotes/staging/*
... Именно это позволяет Git отслеживать ветки удалённого репозитория. Эти параметры конфигурации можно изменить или удалить, отредактировав файл .git/config в текстовом редакторе. (Подробности см. в разделе "CONFIGURATION FILE" документации git-config[1].)
Изучение истории Git
Git лучше всего рассматривать как инструмент для хранения истории коллекции файлов. Для этого он хранит сжатые снимки содержимого иерархии файлов вместе с «коммитами», которые показывают связи между этими снимками.
Git предоставляет чрезвычайно гибкие и быстрые инструменты для изучения истории проекта.
Начнём со специализированного инструмента, который помогает найти коммит, добавивший ошибку в проект.
Как использовать bisect для поиска регрессии
Предположим, что версия 2.6.18 вашего проекта работала, а версия из ветки «master» аварийно завершается. Иногда лучший способ найти причину такой регрессии — методом полного перебора просмотреть историю проекта и найти конкретный коммит, вызвавший проблему. Команда git-bisect[1] поможет это сделать:
$ git bisect start $ git bisect good v2.6.18 $ git bisect bad master Bisecting: 3537 revisions left to test after this [65934a9a028b88e83e2b0f8b36618fe503349f8e] BLOCK: Make USB storage depend on SCSI rather than selecting it [try #6]
Если на этом этапе выполнить git branch, вы увидите, что Git временно переключил вас в состояние «(no branch)». HEAD теперь не привязан ни к одной ветке и указывает непосредственно на коммит (с идентификатором 65934), который доступен из «master», но не из v2.6.18. Соберите и протестируйте его и проверьте, происходит ли аварийное завершение. Предположим, что происходит. Тогда выполните:
$ git bisect bad Bisecting: 1769 revisions left to test after this [7eff82c8b1511017ae605f0c99ac275a7e21b867] i2c-core: Drop useless bitmaskings
чтобы перейти к более старой версии. Продолжайте в том же духе, сообщая Git на каждом этапе, хорошая или плохая предложенная им версия, и обратите внимание, что число оставшихся для проверки ревизий каждый раз уменьшается примерно вдвое.
Примерно после 13 проверок (в данном случае) будет выведен идентификатор коммита, вызвавшего проблему. Затем вы можете изучить коммит с помощью git-show[1], выяснить, кто его написал, и отправить этому человеку сообщение об ошибке с идентификатором коммита. Наконец, выполните
$ git bisect reset
чтобы вернуться в ветку, в которой вы находились ранее.
Обратите внимание, что версия, на которую git bisect переключает вас на каждом этапе, — это лишь рекомендация, и вы можете попробовать другую версию, если считаете это целесообразным. Например, иногда можно попасть на коммит, который нарушил что-то не связанное с проблемой; выполните
$ git bisect visualize
Эта команда запустит gitk и пометит выбранный коммит маркером «bisect». Выберите поблизости коммит, который кажется безопасным, запишите его идентификатор и переключитесь на него с помощью:
$ git reset --hard fb47ddb2db
Затем выполните проверку, запустите bisect good или bisect bad, в зависимости от ситуации, и продолжайте.
Вместо того чтобы выполнять git bisect visualize, а затем git reset --hard fb47ddb2db, можно просто сообщить Git, что текущий коммит нужно пропустить:
$ git bisect skip
Однако в этом случае Git может в итоге не суметь определить первый плохой коммит среди нескольких пропущенных коммитов и более позднего плохого коммита.
Если у вас есть тестовый скрипт, который может определить, является ли коммит хорошим или плохим, процесс bisect также можно автоматизировать. Дополнительные сведения об этом и других возможностях git bisect см. в git-bisect[1].
Именование коммитов
Мы уже рассмотрели несколько способов именования коммитов:
-
40-значное шестнадцатеричное имя объекта
-
имя ветки: указывает на коммит в вершине указанной ветки
-
имя тега: указывает на коммит, на который указывает данный тег (мы уже видели, что ветки и теги — это частные случаи ссылок).
-
HEAD: указывает на вершину текущей ветки
Существуют и другие способы; полный список способов именования ревизий см. в разделе «SPECIFYING REVISIONS» справочной страницы gitrevisions[7]. Вот несколько примеров:
$ git show fb47ddb2 # the first few characters of the object name
# are usually enough to specify it uniquely
$ git show HEAD^ # the parent of the HEAD commit
$ git show HEAD^^ # the grandparent
$ git show HEAD~4 # the great-great-grandparent Помните, что у коммитов слияния может быть несколько родителей; по умолчанию ^ и ~ следуют за первым указанным в коммите родителем, но можно выбрать и другого:
$ git show HEAD^1 # show the first parent of HEAD $ git show HEAD^2 # show the second parent of HEAD
Помимо HEAD, существуют и другие специальные имена коммитов:
Слияния (о них мы поговорим позже), а также такие операции, как git reset, которые меняют текущий выбранный коммит, обычно устанавливают ORIG_HEAD в значение HEAD до выполнения текущей операции.
Операция git fetch всегда сохраняет вершину последней полученной ветки в FETCH_HEAD. Например, если выполнить git fetch, не указав локальную ветку в качестве цели операции,
$ git fetch git://example.com/proj.git theirbranch
полученные коммиты всё равно будут доступны через FETCH_HEAD.
Когда мы будем обсуждать слияния, мы также рассмотрим специальное имя MERGE_HEAD, которое указывает на другую ветку, сливаемую с текущей.
Команда git-rev-parse[1] — низкоуровневая команда, которая иногда бывает полезна для преобразования имени коммита в имя объекта этого коммита:
$ git rev-parse origin e05db0fd4f31dde7005f075a84f96b360d05984b
Создание тегов
Можно также создать тег, ссылающийся на конкретный коммит; после выполнения
$ git tag stable-1 1b2e1d63ff
можно использовать stable-1, чтобы сослаться на коммит 1b2e1d63ff.
Так создаётся «лёгкий» тег. Если вы хотите добавить к тегу комментарий и, возможно, подписать его криптографической подписью, создайте вместо этого объект тега; подробности см. на справочной странице git-tag[1].
Просмотр ревизий
Команда git-log[1] может выводить списки коммитов. Без дополнительных аргументов она показывает все коммиты, доступные из родительского коммита; однако можно задавать и более конкретные запросы:
$ git log v2.5.. # commits since (not reachable from) v2.5
$ git log test..master # commits reachable from master but not test
$ git log master..test # ...reachable from test but not master
$ git log master...test # ...reachable from either test or master,
# but not both
$ git log --since="2 weeks ago" # commits from the last 2 weeks
$ git log Makefile # commits which modify Makefile
$ git log fs/ # ... which modify any file under fs/
$ git log -S'foo()' # commits which add or remove any file data
# matching the string 'foo()' Разумеется, все эти параметры можно комбинировать; следующая команда находит коммиты после v2.5, затрагивающие Makefile или любой файл в fs:
$ git log v2.5.. Makefile fs/
Можно также попросить git log показывать исправления:
$ git log -p
Дополнительные параметры отображения см. в описании параметра --pretty на справочной странице git-log[1].
Обратите внимание, что git log начинает с самого последнего коммита и движется назад по родителям; однако, поскольку история Git может содержать несколько независимых линий разработки, конкретный порядок вывода коммитов может быть в некоторой степени произвольным.
Создание различий
С помощью git-diff[1] можно создать различия между любыми двумя версиями:
$ git diff master..test
Эта команда выведет различия между вершинами двух веток. Если вместо этого нужно получить различия от их общего предка до test, используйте три точки вместо двух:
$ git diff master...test
Иногда вместо этого требуется набор исправлений; для этого можно использовать git-format-patch[1]:
$ git format-patch master..test
Команда создаст файл исправления для каждого коммита, доступного из test, но не из master.
Просмотр старых версий файлов
Чтобы просмотреть старую версию файла, можно просто сначала переключиться на нужную ревизию. Но иногда удобнее просмотреть старую версию одного файла, не переключаясь ни на какую ревизию; для этого предназначена следующая команда:
$ git show v2.5:fs/locks.c
Перед двоеточием может быть любое имя коммита, а после него — любой путь к файлу, отслеживаемому Git.
Примеры
Подсчёт числа коммитов в ветке
Предположим, вы хотите узнать, сколько коммитов было создано в mybranch с момента её отделения от origin:
$ git log --pretty=oneline origin..mybranch | wc -l
Вместо этого для подобных задач часто используют низкоуровневую команду git-rev-list[1], которая просто выводит SHA-1 всех указанных коммитов:
$ git rev-list origin..mybranch | wc -l
Проверка, указывают ли две ветки на одну и ту же историю
Предположим, вы хотите проверить, указывают ли две ветки на одну и ту же точку в истории.
$ git diff origin..master
Команда сообщит, совпадает ли содержимое проекта в обеих ветках; однако теоретически одно и то же содержимое проекта могло быть получено двумя разными путями в истории. Можно сравнить имена объектов:
$ git rev-list origin e05db0fd4f31dde7005f075a84f96b360d05984b $ git rev-list master e05db0fd4f31dde7005f075a84f96b360d05984b
Либо можно вспомнить, что оператор ... выбирает все коммиты, доступные из одной или другой ссылки, но не из обеих одновременно; поэтому
$ git log origin...master
не выведет ни одного коммита, если ветки совпадают.
Поиск первой помеченной тегом версии, содержащей указанное исправление
Предположим, вы знаете, что коммит e05db0fd исправил определённую проблему. Вы хотите найти самый ранний помеченный тегом выпуск, содержащий это исправление.
Разумеется, ответов может быть несколько: если после коммита e05db0fd история разветвилась, то может существовать несколько «самых ранних» выпусков с тегами.
Можно просто просмотреть коммиты после e05db0fd:
$ gitk e05db0fd..
либо воспользоваться git-name-rev[1], которая присвоит коммиту имя на основе любого найденного тега, указывающего на одного из его потомков:
$ git name-rev --tags e05db0fd e05db0fd tags/v1.5.0-rc1^0~23
Команда git-describe[1] делает обратное: называет ревизию тегом, на котором основан указанный коммит:
$ git describe e05db0fd v1.5.0-rc0-260-ge05db0f
но иногда это может помочь предположить, какие теги идут после указанного коммита.
Если нужно лишь проверить, содержит ли версия с указанным тегом заданный коммит, можно воспользоваться git-merge-base[1]:
$ git merge-base e05db0fd v1.5.0-rc1 e05db0fd4f31dde7005f075a84f96b360d05984b
Команда merge-base находит общего предка указанных коммитов и всегда возвращает один из них, если один является потомком другого; следовательно, приведённый выше вывод показывает, что e05db0fd действительно является предком v1.5.0-rc1.
Кроме того, обратите внимание, что
$ git log v1.5.0-rc1..e05db0fd
выведет пустой результат тогда и только тогда, когда v1.5.0-rc1 содержит e05db0fd, поскольку команда выводит только коммиты, недоступные из v1.5.0-rc1.
Ещё один вариант — команда git-show-branch[1], которая выводит коммиты, доступные из аргументов, и слева отмечает, из каких аргументов доступен каждый коммит. Поэтому, если выполнить что-то вроде
$ git show-branch e05db0fd v1.5.0-rc0 v1.5.0-rc1 v1.5.0-rc2 ! [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if available ! [v1.5.0-rc0] GIT v1.5.0 preview ! [v1.5.0-rc1] GIT v1.5.0-rc1 ! [v1.5.0-rc2] GIT v1.5.0-rc2 ...
то строка вида
+ ++ [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if available
показывает, что e05db0fd доступен из самого себя, из v1.5.0-rc1 и v1.5.0-rc2, но не из v1.5.0-rc0.
Просмотр коммитов, уникальных для указанной ветки
Предположим, вы хотите увидеть все коммиты, доступные из вершины ветки master, но не из любой другой вершины в вашем репозитории.
Список всех вершин в этом репозитории можно получить с помощью git-show-ref[1]:
$ git show-ref --heads bf62196b5e363d73353a9dcf094c59595f3153b7 refs/heads/core-tutorial db768d5504c1bb46f63ee9d6e1772bd047e05bf9 refs/heads/maint a07157ac624b2524a059a3414e99f6f44bebc1e7 refs/heads/master 24dbc180ea14dc1aebe09f14c8ecf32010690627 refs/heads/tutorial-2 1e87486ae06626c2f31eaa63d26fc0fd646c8af2 refs/heads/tutorial-fixes
С помощью стандартных утилит cut и grep можно получить только имена вершин веток и удалить master:
$ git show-ref --heads | cut -d' ' -f2 | grep -v '^refs/heads/master' refs/heads/core-tutorial refs/heads/maint refs/heads/tutorial-2 refs/heads/tutorial-fixes
Затем можно вывести все коммиты, доступные из master, но не из этих других вершин:
$ gitk master --not $( git show-ref --heads | cut -d' ' -f2 |
grep -v '^refs/heads/master' ) Возможны самые разные варианты; например, чтобы увидеть все коммиты, доступные из некоторой вершины, но не из какого-либо тега в репозитории:
$ gitk $( git show-ref --heads ) --not $( git show-ref --tags )
(Пояснения к синтаксису выбора коммитов, например --not, см. в gitrevisions[7].)
Создание журнала изменений и tarball для выпуска программы
Команда git-archive[1] может создать архив tar или zip из любой версии проекта; например:
$ git archive -o latest.tar.gz --prefix=project/ HEAD
использует HEAD для создания сжатого с помощью gzip tar-архива, в котором перед каждым именем файла стоит project/. Если возможно, формат выходного файла определяется по его расширению; подробности см. в git-archive[1].
Версии Git старше 1.7.7 не поддерживают формат tar.gz, поэтому gzip нужно использовать явно:
$ git archive --format=tar --prefix=project/ HEAD | gzip >latest.tar.gz
Если вы выпускаете новую версию программного проекта, возможно, вы захотите одновременно создать журнал изменений для включения в объявление о выпуске.
Например, Линус Торвальдс создаёт новые выпуски ядра, присваивая им теги, а затем выполняя:
$ release-script 2.6.12 2.6.13-rc6 2.6.13-rc7
где release-script — это shell-скрипт следующего вида:
#!/bin/sh stable="$1" last="$2" new="$3" echo "# git tag v$new" echo "git archive --prefix=linux-$new/ v$new | gzip -9 > ../linux-$new.tar.gz" echo "git diff v$stable v$new | gzip -9 > ../patch-$new.gz" echo "git log --no-merges v$new ^v$last > ../ChangeLog-$new" echo "git shortlog --no-merges v$new ^v$last > ../ShortLog" echo "git diff --stat --summary -M v$last v$new > ../diffstat-$new"
Затем он просто копирует и вставляет полученные команды после проверки того, что они выглядят правильно.
Поиск коммитов, ссылающихся на файл с заданным содержимым
Кто-то передаёт вам копию файла и спрашивает, какие коммиты изменили файл так, что до или после коммита он содержал заданный текст. Это можно выяснить с помощью следующей команды:
$ git log --raw --abbrev=40 --pretty=oneline |
grep -B 1 `git hash-object filename` Объяснение того, почему это работает, оставлено в качестве упражнения для продвинутого читателя. Полезными могут оказаться справочные страницы git-log[1], git-diff-tree[1] и git-hash-object[1].
Разработка с помощью git
Как сообщить Git ваше имя
Прежде чем создавать коммиты, представьтесь Git. Проще всего сделать это с помощью git-config[1]:
$ git config --global user.name 'Your Name Comes Here' $ git config --global user.email 'you@yourdomain.example.com'
Эта команда добавит в файл с именем .gitconfig в вашем домашнем каталоге следующее:
[user]
name = Your Name Comes Here
email = you@yourdomain.example.com Подробные сведения о файле конфигурации см. в разделе «ФАЙЛ КОНФИГУРАЦИИ» справки git-config[1]. Это обычный текстовый файл, поэтому его также можно редактировать в любом удобном вам редакторе.
Создание нового репозитория
Создать новый репозиторий с нуля очень просто:
$ mkdir project $ cd project $ git init
Если у вас уже есть исходное содержимое (например, tar-архив):
$ tar xzvf project.tar.gz $ cd project $ git init $ git add . # include everything below ./ in the first commit: $ git commit
Как создать коммит
Создание нового коммита состоит из трёх шагов:
-
Внесите изменения в рабочий каталог с помощью любого удобного вам редактора.
-
Сообщите Git об изменениях.
-
Создайте коммит, используя содержимое, о котором вы сообщили Git на шаге 2.
На практике шаги 1 и 2 можно чередовать и повторять сколько угодно раз: чтобы отслеживать изменения, которые вы хотите включить в коммит на шаге 3, Git хранит снимок содержимого дерева в специальной области подготовки, называемой «индексом».
Изначально содержимое индекса совпадает с содержимым HEAD. Поэтому команда git diff --cached, показывающая разницу между HEAD и индексом, на этом этапе не должна ничего выводить.
Изменить индекс просто:
Чтобы обновить индекс содержимым нового или изменённого файла, используйте
$ git add path/to/file
Чтобы удалить файл из индекса и рабочего дерева, используйте
$ git rm path/to/file
После каждого шага можно убедиться, что
$ git diff --cached
всегда показывает разницу между HEAD и файлом индекса — именно эти изменения попадут в коммит, если создать его сейчас, — а
$ git diff
показывает разницу между рабочим деревом и файлом индекса.
Обратите внимание: git add добавляет в индекс только текущее содержимое файла; дальнейшие изменения того же файла будут проигнорированы, если снова не выполнить для него git add.
Когда будете готовы, просто выполните
$ git commit
Git предложит ввести сообщение коммита, а затем создаст новый коммит. Убедитесь, что он выглядит так, как вы ожидали, выполнив
$ git show
В качестве специального сокращённого варианта
$ git commit -a
обновит индекс всеми изменёнными или удалёнными файлами и создаст коммит — всё за один шаг.
Для отслеживания того, что вы собираетесь включить в коммит, полезны несколько команд:
$ git diff --cached # difference between HEAD and the index; what
# would be committed if you ran "commit" now.
$ git diff # difference between the index file and your
# working directory; changes that would not
# be included if you ran "commit" now.
$ git diff HEAD # difference between HEAD and working tree; what
# would be committed if you ran "commit -a" now.
$ git status # a brief per-file summary of the above. Для создания коммитов, просмотра изменений в индексе и файлах рабочего дерева, а также выбора отдельных блоков diff для включения в индекс можно воспользоваться и git-gui[1] (щелкните правой кнопкой мыши по блоку diff и выберите «Добавить блок в индекс для коммита»).
Как составлять хорошие сообщения коммитов
Хотя это и не обязательно, рекомендуется начинать сообщение коммита с одной короткой строки (не более 50 символов), в которой кратко описываются изменения, затем оставлять пустую строку и приводить более подробное описание. Текст до первой пустой строки в сообщении коммита считается его заголовком, и этот заголовок используется во всём Git. Например, git-format-patch[1] преобразует коммит в электронное письмо: заголовок используется в строке темы, а остальная часть коммита — в теле письма.
Игнорирование файлов
В проекте часто создаются файлы, которые вы not хотите отслеживать с помощью Git. Обычно это файлы, созданные в процессе сборки, или временные резервные копии, создаваемые редактором. Разумеется, not отслеживать файлы с помощью Git — всего лишь вопрос not вызова для них команды git add. Но вскоре начинает раздражать присутствие этих неотслеживаемых файлов: например, они делают git add . практически бесполезной, а также постоянно появляются в выводе git status.
Можно указать Git игнорировать определённые файлы, создав в корневом каталоге рабочего дерева файл с именем .gitignore и добавив в него, например, следующее:
# Lines starting with '#' are considered comments. # Ignore any file named foo.txt. foo.txt # Ignore (generated) html files, *.html # except foo.html which is maintained by hand. !foo.html # Ignore objects and archives. *.[oa]
Подробное описание синтаксиса см. в gitignore[5]. Файлы .gitignore можно также размещать в других каталогах рабочего дерева; в этом случае они будут применяться к этим каталогам и их подкаталогам. Файлы .gitignore можно добавлять в репозиторий, как и любые другие файлы (как обычно, достаточно выполнить git add .gitignore и git commit). Это удобно, если шаблоны исключения (например, шаблоны для файлов, создаваемых при сборке) пригодятся и другим пользователям, клонирующим ваш репозиторий.
Если вы хотите, чтобы шаблоны исключения применялись только к определённым репозиториям (а не ко всем репозиториям данного проекта), можно поместить их в файл с именем .git/info/exclude в репозитории или в любой файл, указанный переменной конфигурации core.excludesFile. Некоторые команды Git также принимают шаблоны исключения непосредственно в командной строке. Подробности см. в gitignore[5].
Как выполнить слияние
С помощью git-merge[1] можно объединить две разошедшиеся ветки разработки:
$ git merge branchname
объединяет изменения из ветки branchname с текущей веткой.
При слиянии объединяются изменения, внесённые в branchname, и изменения, внесённые в текущую ветку после расхождения историй, вплоть до последнего коммита. Если объединение проходит без конфликтов, рабочее дерево перезаписывается результатом слияния; если возникают конфликты — промежуточным результатом слияния. Поэтому Git откажется продолжать, если у вас есть незакоммиченные изменения в тех же файлах, которых касается слияние. Как правило, перед слиянием нужно закоммитить свои изменения. Если этого не сделать, git-stash[1] поможет временно убрать их на время слияния и восстановить после него.
Если изменения достаточно независимы, Git автоматически завершит слияние и создаст коммит с результатом (или повторно использует существующий коммит в случае перемотки вперёд; см. ниже). Если же возникнут конфликты — например, если один и тот же файл по-разному изменён в удалённой и локальной ветках, — Git предупредит вас; вывод может выглядеть примерно так:
$ git merge next 100% (4/4) done Auto-merged file.txt CONFLICT (content): Merge conflict in file.txt Automatic merge failed; fix conflicts and then commit the result.
В проблемных файлах останутся маркеры конфликта. После того как вы разрешите конфликты вручную, можно обновить индекс содержимым файлов и выполнить Git commit, как при создании нового файла.
Если просмотреть получившийся коммит с помощью gitk, вы увидите, что у него два родительских коммита: один указывает на вершину текущей ветки, а другой — на вершину другой ветки.
Разрешение конфликтов слияния
Если Git не удаётся выполнить слияние автоматически, он оставляет индекс и рабочее дерево в особом состоянии, предоставляя всю необходимую информацию для разрешения конфликта.
Файлы с конфликтами помечаются в индексе особым образом, поэтому, пока вы не разрешите конфликт и не обновите индекс, git-commit[1] завершится ошибкой:
$ git commit file.txt: needs merge
Кроме того, git-status[1] укажет эти файлы как «необъединённые», а в файлах с конфликтами будут добавлены маркеры конфликта, например:
<<<<<<< HEAD:file.txt Hello world ======= Goodbye >>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
Вам нужно лишь отредактировать файлы, чтобы разрешить конфликты, а затем выполнить
$ git add file.txt $ git commit
Обратите внимание, что сообщение коммита уже будет заполнено некоторой информацией о слиянии. Обычно можно оставить это сообщение без изменений, но при желании добавить собственные пояснения.
Этого достаточно, чтобы разрешить простой конфликт слияния. Но Git также предоставляет дополнительную информацию, которая поможет разрешить конфликты:
Получение помощи при разрешении конфликтов во время слияния
Все изменения, которые Git удалось объединить автоматически, уже добавлены в файл индекса, поэтому git-diff[1] показывает только конфликты. При этом используется необычный синтаксис:
$ git diff diff --cc file.txt index 802992c,2b60207..0000000 --- a/file.txt +++ b/file.txt @@@ -1,1 -1,1 +1,5 @@@ ++<<<<<<< HEAD:file.txt +Hello world ++======= + Goodbye ++>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
Помните, что после разрешения этого конфликта создаваемый коммит будет иметь двух родителей вместо обычного одного: один родитель — это HEAD, вершина текущей ветки; другой — вершина другой ветки, временно сохранённая в MERGE_HEAD.
Во время слияния индекс содержит три версии каждого файла. Каждая из этих трёх «стадий файла» представляет отдельную версию файла:
$ git show :1:file.txt # the file in a common ancestor of both branches $ git show :2:file.txt # the version from HEAD. $ git show :3:file.txt # the version from MERGE_HEAD.
Когда вы просите git-diff[1] показать конфликты, команда выполняет трёхстороннее сравнение результатов слияния с конфликтами в рабочем дереве и стадий 2 и 3, чтобы показать только те блоки, содержимое которых объединяет изменения с обеих сторон (иными словами, если результат слияния блока взят только из стадии 2, эта часть не конфликтует и не показывается. То же относится к стадии 3).
В приведённом выше diff сравнивается версия file.txt из рабочего дерева с версиями из стадий 2 и 3. Поэтому вместо добавления одного символа + или - перед каждой строкой используются два столбца: первый показывает различия между первым родителем и копией в рабочем каталоге, а второй — между вторым родителем и копией в рабочем каталоге. (Описание формата см. в разделе «ФОРМАТ ОБЪЕДИНЁННОГО DIFF» справки git-diff-files[1].)
После очевидного разрешения конфликта (но до обновления индекса) diff будет выглядеть так:
$ git diff diff --cc file.txt index 802992c,2b60207..0000000 --- a/file.txt +++ b/file.txt @@@ -1,1 -1,1 +1,1 @@@ - Hello world -Goodbye ++Goodbye world
Это показывает, что в разрешённой версии мы удалили «Hello world» из первого родителя, удалили «Goodbye» из второго родителя и добавили «Goodbye world», которой не было ни в одном из них.
Некоторые специальные параметры diff позволяют сравнить рабочий каталог с любой из этих стадий:
$ git diff -1 file.txt # diff against stage 1 $ git diff --base file.txt # same as the above $ git diff -2 file.txt # diff against stage 2 $ git diff --ours file.txt # same as the above $ git diff -3 file.txt # diff against stage 3 $ git diff --theirs file.txt # same as the above.
При использовании стратегии слияния ort (по умолчанию) перед обновлением рабочего дерева результатом слияния Git записывает ссылку с именем AUTO_MERGE, отражающую состояние дерева, которое собирается записать. Пути с конфликтами в тексте, которые не удалось объединить автоматически, записываются в это дерево с маркерами конфликта, как и в рабочем дереве. Поэтому AUTO_MERGE можно использовать с git-diff[1], чтобы просмотреть уже внесённые изменения для разрешения конфликтов. Воспользуемся тем же примером, что и выше: после разрешения конфликта получим следующее:
$ git diff AUTO_MERGE diff --git a/file.txt b/file.txt index cd10406..8bf5ae7 100644 --- a/file.txt +++ b/file.txt @@ -1,5 +1 @@ -<<<<<<< HEAD:file.txt -Hello world -======= -Goodbye ->>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt +Goodbye world
Обратите внимание: diff показывает, что мы удалили маркеры конфликта и обе версии строки с содержимым и вместо них записали «Goodbye world».
Команды git-log[1] и gitk[1] также предоставляют специальные средства для работы со слияниями:
$ git log --merge $ gitk --merge
Они покажут все коммиты, присутствующие только в HEAD или только в MERGE_HEAD и затрагивающие необъединённый файл.
Можно также воспользоваться git-mergetool[1], позволяющей объединять необъединённые файлы с помощью внешних инструментов, например Emacs или kdiff3.
Каждый раз, разрешив конфликты в файле и обновив индекс, выполните:
$ git add file.txt
различные стадии этого файла будут «свёрнуты», после чего git diff (по умолчанию) перестанет показывать различия для этого файла.
Отмена слияния
Если вы зашли в тупик и решили отказаться от дальнейшей работы и отбросить все изменения, всегда можно вернуться к состоянию до слияния с помощью
$ git merge --abort
Или, если вы уже создали коммит слияния, который хотите отменить, выполните
$ git reset --hard ORIG_HEAD
Однако последняя команда в некоторых случаях может быть опасна: никогда не отменяйте уже созданный коммит, если он мог быть объединён с другой веткой, так как это может привести к путанице при дальнейших слияниях.
Слияния с перемоткой вперёд
Есть особый случай, не упомянутый выше, который обрабатывается иначе. Обычно результатом слияния становится коммит слияния с двумя родителями, каждый из которых указывает на одну из объединённых линий разработки.
Однако если текущая ветка является предком другой ветки — то есть все коммиты текущей ветки уже содержатся в другой ветке, — Git просто выполняет «перемотку вперёд»: вершина текущей ветки перемещается вперёд и начинает указывать на вершину объединяемой ветки, при этом новые коммиты не создаются.
Исправление ошибок
Если вы испортили рабочее дерево, но ещё не закоммитили ошибочные изменения, можно вернуть всё рабочее дерево к состоянию последнего коммита с помощью
$ git restore --staged --worktree :/
Если вы создали коммит, о котором впоследствии пожалели, есть два принципиально разных способа исправить ситуацию:
-
Можно создать новый коммит, отменяющий изменения, внесённые старым коммитом. Это правильный способ, если ваша ошибка уже стала общедоступной.
-
Можно вернуться назад и изменить старый коммит. Никогда не делайте этого, если история уже стала общедоступной: Git обычно предполагает, что «история» проекта не меняется, и не может правильно выполнять повторные слияния из ветки с изменённой историей.
Исправление ошибки с помощью нового коммита
Создать новый коммит, отменяющий более ранние изменения, очень просто: передайте команде git-revert[1] ссылку на ошибочный коммит. Например, чтобы отменить последний коммит:
$ git revert HEAD
Будет создан новый коммит, отменяющий изменения в HEAD. Вам предложат отредактировать сообщение нового коммита.
Можно также отменить более ранние изменения, например предпоследние:
$ git revert HEAD^
В этом случае Git попытается отменить старые изменения, сохранив все изменения, внесённые после них. Если более поздние изменения пересекаются с отменяемыми, вам придётся разрешить конфликты вручную, как и при разрешении конфликта слияния.
Исправление ошибки путём переписывания истории
Если проблемный коммит — последний, и вы ещё не сделали его общедоступным, его можно просто удалить с помощью git reset.
Кроме того, можно отредактировать рабочий каталог и обновить индекс, чтобы исправить ошибку, как если бы вы собирались создать новый коммит, а затем выполнить
$ git commit --amend
Эта команда заменит старый коммит новым, включающим ваши изменения, и сначала предложит отредактировать сообщение старого коммита.
И снова: никогда не делайте этого с коммитом, который уже мог быть объединён с другой веткой; в этом случае используйте git-revert[1].
Можно также заменить коммиты, находящиеся глубже в истории, но это более сложная тема, которая будет рассмотрена в другой главе.
Восстановление старой версии файла
При отмене предыдущих ошибочных изменений может быть полезно восстановить старую версию определённого файла с помощью git-restore[1]. Команда
$ git restore --source=HEAD^ path/to/file
заменяет содержимое path/to/file его содержимым в коммите HEAD^ и также обновляет индекс в соответствии с ним. Ветки при этом не меняются.
Если нужно просто посмотреть старую версию файла, не изменяя рабочий каталог, можно воспользоваться git-show[1]:
$ git show HEAD^:path/to/file
Команда покажет указанную версию файла.
Временное сохранение текущей работы
Представьте, что вы работаете над чем-то сложным и замечаете не связанную с этим очевидную и простую ошибку. Вы хотите исправить её, прежде чем продолжить работу. С помощью git-stash[1] можно сохранить текущее состояние работы, исправить ошибку (при желании — в другой ветке, а затем вернуться) и восстановить отложенные изменения.
$ git stash push -m "work in progress for foo feature"
Эта команда сохранит ваши изменения в stash и сбросит рабочее дерево и индекс до состояния вершины текущей ветки. После этого можно исправить ошибку обычным способом.
... edit and test ... $ git commit -a -m "blorpl: typofix"
Затем можно вернуться к работе с помощью git stash pop:
$ git stash pop
Обеспечение высокой производительности
В больших репозиториях Git использует сжатие, чтобы история не занимала слишком много места на диске и в памяти. Некоторые команды Git могут автоматически запускать git-gc[1], поэтому вручную запускать её не обязательно. Однако сжатие большого репозитория может занять некоторое время, поэтому можно явно вызвать gc, чтобы автоматическое сжатие не запустилось в неподходящий момент.
Обеспечение надёжности
Проверка репозитория на повреждения
Команда git-fsck[1] выполняет ряд проверок внутренней согласованности репозитория и сообщает о найденных проблемах. Проверка может занять некоторое время.
$ git fsck dangling commit 7281251ddd2a61e38657c827739c57015671a6b3 dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63 dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5 dangling blob 218761f9d90712d37a9c5e36f406f92202db07eb dangling commit bf093535a34a4d35731aa2bd90fe6b176302f14f dangling commit 8e4bec7f2ddaa268bef999853c25755452100f8e dangling tree d50bb86186bf27b681d25af89d3b5b68382e4085 dangling tree b24c2473f1fd3d91352a624795be026d64c8841f ...
Вы увидите информационные сообщения о висячих объектах. Это объекты, которые всё ещё существуют в репозитории, но больше не используются ни одной из ваших веток; через некоторое время их можно будет удалить с помощью gc. Чтобы скрыть эти сообщения и при этом видеть настоящие ошибки, выполните git fsck --no-dangling.
Восстановление потерянных изменений
Журналы ссылок
Предположим, вы изменили ветку с помощью git reset --hard, а затем поняли, что эта ветка была единственной ссылкой на эту часть истории.
К счастью, Git также ведёт журнал, называемый «журналом ссылок» (reflog), в котором сохраняются все предыдущие значения каждой ветки. Поэтому в этом случае старую историю всё ещё можно найти, например, так:
$ git log master@{1} Команда выводит коммиты, доступные из предыдущей версии вершины ветки master. Этот синтаксис можно использовать с любой командой Git, принимающей коммит, а не только с git log. Вот ещё несколько примеров:
$ git show master@{2} # See where the branch pointed 2,
$ git show master@{3} # 3, ... changes ago.
$ gitk master@{yesterday} # See where it pointed yesterday,
$ gitk master@{"1 week ago"} # ... or last week
$ git log --walk-reflogs master # show reflog entries for master Для HEAD ведётся отдельный журнал ссылок, поэтому команда
$ git show HEAD@{"1 week ago"} покажет, на что указывал HEAD неделю назад, а не на что указывала текущая ветка неделю назад. Это позволяет просмотреть историю переключений между версиями.
По умолчанию журналы ссылок хранятся 30 дней, после чего могут быть очищены. О том, как управлять очисткой, читайте в справках git-reflog[1] и git-gc[1]; подробности см. в разделе «УКАЗАНИЕ РЕВИЗИЙ» справки gitrevisions[7].
Обратите внимание, что история журнала ссылок сильно отличается от обычной истории Git. Обычная история является общей для всех репозиториев, работающих над одним проектом, а история журнала ссылок не является общей: она содержит сведения только об изменениях веток в вашем локальном репозитории.
Изучение висячих объектов
В некоторых ситуациях журнал ссылок может не помочь. Например, предположим, вы удалили ветку, а затем поняли, что вам нужна содержавшаяся в ней история. Журнал ссылок тоже удаляется; однако, если вы ещё не очистили репозиторий, потерянные коммиты можно найти среди висячих объектов, о которых сообщает команда git fsck. Подробности см. в разделе Висячие объекты.
$ git fsck dangling commit 7281251ddd2a61e38657c827739c57015671a6b3 dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63 dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5 ...
Например, один из этих висячих коммитов можно изучить с помощью команды
$ gitk 7281251ddd --not --all
которая делает именно то, что следует из её названия: она показывает историю коммитов, описанную висячими коммитами, но не историю, описанную всеми существующими ветками и тегами. Таким образом, вы получите именно ту историю, которая доступна из потерянного коммита. (И обратите внимание: это может быть не один коммит — мы сообщаем о «вершине линии» как о висячем объекте, но за ней может скрываться целая глубокая и сложная история коммитов.)
Если вы решите восстановить историю, всегда можно создать новую ссылку на неё, например новую ветку:
$ git branch recovered-branch 7281251ddd
Возможны и другие типы висячих объектов (blob-объекты и деревья); висячие объекты могут появляться и в других ситуациях.
Совместная разработка
Получение обновлений с помощью git pull
После клонирования репозитория и создания нескольких собственных коммитов может возникнуть необходимость проверить исходный репозиторий на наличие обновлений и объединить их со своей работой.
Мы уже видели, как поддерживать ветки отслеживания удалённых репозиториев в актуальном состоянии с помощью git-fetch[1], а также как объединить две ветки. Поэтому можно объединить изменения из главной ветки исходного репозитория с помощью команды:
$ git fetch $ git merge origin/master
Однако команда git-pull[1] позволяет сделать это за один шаг:
$ git pull origin master
На самом деле, если у вас извлечена ветка master, то эта ветка настроена командой git clone на получение изменений из ветки HEAD репозитория origin. Поэтому часто всё описанное выше можно сделать простой командой
$ git pull
Эта команда получит изменения из удалённых веток в ваши ветки отслеживания удалённых репозиториев origin/* и объединит ветку по умолчанию с текущей веткой.
В более общем случае ветка, созданная из ветки отслеживания удалённого репозитория, по умолчанию будет получать изменения именно из этой ветки. Описание параметров branch.<name>.remote и branch.<name>.merge в git-config[1], а также обсуждение параметра --track в git-checkout[1] помогут узнать, как управлять этими настройками по умолчанию.
Помимо экономии нажатий клавиш, команда git pull также помогает создать сообщение коммита по умолчанию, в котором указаны ветка и репозиторий, откуда были получены изменения.
(Обратите внимание: если происходит перемотка вперёд, такой коммит не создаётся; вместо этого ваша ветка просто обновляется, чтобы указывать на последний коммит вышестоящей ветки.)
Команде git pull также можно передать . в качестве «удалённого» репозитория; в этом случае она просто объединит ветку из текущего репозитория. Поэтому команды
$ git pull . branch $ git merge branch
примерно эквивалентны.
Отправка исправлений в проект
Если у вас всего несколько изменений, проще всего отправить их в виде исправлений по электронной почте:
Сначала воспользуйтесь git-format-patch[1], например:
$ git format-patch origin
эта команда создаст в текущем каталоге пронумерованную последовательность файлов — по одному для каждого исправления в текущей ветке, которого нет в origin/HEAD.
git format-patch может включать вступительное «сопроводительное письмо». Вы можете добавить комментарий к отдельным исправлениям после строки из трёх дефисов, которую format-patch помещает после сообщения коммита, но перед самим исправлением. Если для отслеживания материалов сопроводительного письма использовать git notes, команда git format-patch --notes аналогичным образом добавит заметки коммита.
Затем можно импортировать эти файлы в почтовый клиент и отправить их вручную. Однако, если нужно отправить сразу много писем, можно автоматизировать этот процесс с помощью скрипта git-send-email[1]. Сначала ознакомьтесь с требованиями к отправке исправлений, принятыми в списке рассылки вашего проекта.
Импорт исправлений в проект
В Git также есть инструмент git-am[1] (am означает «применить почтовый ящик»), предназначенный для импорта полученной по электронной почте последовательности исправлений. Просто сохраните все письма с исправлениями в правильном порядке в один файл почтового ящика, например patches.mbox, а затем выполните
$ git am -3 patches.mbox
Git применит каждое исправление по порядку; если возникнут конфликты, выполнение остановится, и вы сможете устранить их, как описано в разделе «Устранение конфликтов при слиянии». (Параметр -3 указывает Git выполнить слияние; если вы предпочитаете отменить операцию и оставить дерево и индекс без изменений, этот параметр можно опустить.)
После обновления индекса результатами разрешения конфликтов, вместо создания нового коммита просто выполните
$ git am --continue
Git создаст коммит и продолжит применять оставшиеся исправления из почтового ящика.
В результате получится последовательность коммитов — по одному для каждого исправления из исходного почтового ящика; автор и сообщение журнала коммитов будут взяты из письма, содержащего соответствующее исправление.
Публичные репозитории Git
Ещё один способ передать изменения в проект — попросить его сопровождающего получить изменения из вашего репозитория с помощью git-pull[1]. В разделе «Получение обновлений с помощью git pull» мы описали этот способ как вариант получения обновлений из «основного» репозитория, но он так же хорошо работает и в обратном направлении.
Если у вас и сопровождающего есть учётные записи на одном компьютере, вы можете напрямую получать изменения из репозиториев друг друга; команды, принимающие URL репозитория в качестве аргумента, также принимают имя локального каталога:
$ git clone /path/to/repository $ git pull /path/to/other/repository
или URL SSH:
$ git clone ssh://yourhost/~you/repository
Для проектов с небольшим числом разработчиков или для синхронизации нескольких частных репозиториев этого может быть достаточно.
Однако чаще для этого поддерживают отдельный публичный репозиторий (обычно на другом сервере), из которого остальные могут получать изменения. Как правило, это удобнее и позволяет чётко отделить незавершённую частную работу от работы, доступной всем.
Вы продолжите повседневную работу в личном репозитории, но время от времени будете «отправлять» изменения из личного репозитория в публичный, чтобы другие разработчики могли получать их оттуда. В ситуации, когда есть ещё один разработчик с публичным репозиторием, поток изменений выглядит так:
you push
your personal repo ------------------> your public repo
^ |
| |
| you pull | they pull
| |
| |
| they push V
their public repo <------------------- their repo В следующих разделах объясняется, как это сделать.
Настройка публичного репозитория
Предположим, ваш личный репозиторий находится в каталоге ~/proj. Сначала создадим новую копию репозитория и сообщим git daemon, что она предназначена для публичного доступа:
$ git clone --bare ~/proj proj.git $ touch proj.git/git-daemon-export-ok
В результате будет создан каталог proj.git, содержащий «голый» репозиторий Git — только содержимое каталога .git, без извлеченных файлов.
Затем скопируйте proj.git на сервер, где планируете разместить публичный репозиторий. Для этого можно использовать scp, rsync или любой другой удобный способ.
Экспорт репозитория Git по протоколу Git
Это предпочтительный способ.
Если сервером управляет кто-то другой, попросите указать каталог для размещения репозитория и URL git://, по которому он будет доступен. После этого можно перейти к разделу «Отправка изменений в публичный репозиторий» ниже.
В противном случае достаточно запустить git-daemon[1]; он будет прослушивать порт 9418. По умолчанию он разрешает доступ к любому каталогу, похожему на каталог Git и содержащему специальный файл git-daemon-export-ok. Передача путей к каталогам в качестве аргументов git daemon дополнительно ограничит экспорт указанными путями.
Также можно запустить git daemon как службу inetd; подробности см. на странице руководства git-daemon[1]. (Обратите особое внимание на раздел с примерами.)
Экспорт репозитория Git по HTTP
Протокол Git обеспечивает более высокую производительность и надёжность, но на сервере с настроенным веб-сервером экспорт по HTTP может быть проще в настройке.
Достаточно поместить только что созданный голый репозиторий Git в каталог, доступный через веб-сервер, и внести несколько изменений, чтобы предоставить веб-клиентам дополнительную необходимую информацию:
$ mv proj.git /home/you/public_html/proj.git $ cd proj.git $ git --bare update-server-info $ mv hooks/post-update.sample hooks/post-update
(Пояснения к двум последним строкам см. в git-update-server-info[1] и githooks[5].)
Сообщите URL proj.git. После этого любой желающий сможет клонировать репозиторий или получать из него изменения, например, выполнив команду:
$ git clone http://yourserver.com/~you/proj.git
(См. также настройку сервера Git по HTTP — более сложный вариант настройки с использованием WebDAV, который также позволяет отправлять изменения по HTTP.)
Отправка изменений в публичный репозиторий
Обратите внимание: два описанных выше способа (экспорт по HTTP или Git) позволяют другим сопровождающим получать ваши последние изменения, но не дают права на запись. Такое право понадобится, чтобы обновлять публичный репозиторий последними изменениями из вашего частного репозитория.
Проще всего сделать это с помощью git-push[1] и SSH. Чтобы обновить удалённую ветку с именем master до последнего состояния вашей ветки с именем master, выполните
$ git push ssh://yourserver.com/~you/proj.git master:master
или просто
$ git push ssh://yourserver.com/~you/proj.git master
Как и git fetch, команда git push выдаст сообщение об ошибке, если результатом не станет перемотка вперёд; подробности об этой ситуации приведены в следующем разделе.
Обратите внимание: целью команды push обычно является голый репозиторий. Можно также отправлять изменения в репозиторий с извлеченной рабочей копией, однако по умолчанию отправка изменений в текущую извлеченную ветку запрещена, чтобы избежать путаницы. Подробности см. в описании параметра receive.denyCurrentBranch в git-config[1].
Как и для git fetch, можно настроить параметры, чтобы сократить ввод. Например:
$ git remote add public-repo ssh://yourserver.com/~you/proj.git
добавит в .git/config следующие строки:
[remote "public-repo"]
url = yourserver.com:proj.git
fetch = +refs/heads/*:refs/remotes/example/* после чего ту же отправку можно выполнить простой командой
$ git push public-repo master
Подробности см. в описаниях параметров remote.<name>.url, branch.<name>.remote и remote.<name>.push в git-config[1].
Что делать, если отправка изменений не удалась
Если отправка изменений не приведёт к перемотке вперёд удалённой ветки, она завершится ошибкой, например:
! [rejected] master -> master (non-fast-forward) error: failed to push some refs to '...' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Такое может произойти, например, если вы:
-
используете
gitreset--hard, чтобы удалить уже опубликованные коммиты, или -
используете
gitcommit--amend, чтобы заменить уже опубликованные коммиты (как в разделе Исправление ошибки путём переписывания истории), или -
используете
gitrebase, чтобы перебазировать уже опубликованные коммиты (как в разделе Актуализация серии исправлений с помощью git rebase).
Можно принудительно заставить git push всё же выполнить обновление, поставив знак «плюс» перед именем ветки:
$ git push ssh://yourserver.com/~you/proj.git +master
Обратите внимание на добавленный знак +. В качестве альтернативы можно использовать флаг -f для принудительного обновления удалённого репозитория, например:
$ git push -f ssh://yourserver.com/~you/proj.git master
Обычно при изменении вершины ветки в публичном репозитории её перемещают на коммит, являющийся потомком коммита, на который она указывала ранее. Принудительно отправляя изменения в такой ситуации, вы нарушаете это соглашение. (См. раздел Проблемы при переписывании истории.)
Тем не менее, это распространённая практика среди тех, кому нужен простой способ опубликовать незавершённую серию исправлений; такой компромисс приемлем, если вы предупредите других разработчиков, что собираетесь работать с веткой именно так.
Отправка изменений может завершиться ошибкой и в том случае, если другие пользователи имеют право отправлять изменения в тот же репозиторий. Правильное решение — повторить отправку после обновления своей работы: либо с помощью команды pull, либо с помощью fetch с последующим rebase; подробнее см. в следующем разделе и в gitcvs-migration[7].
Настройка общего репозитория
Ещё один способ совместной работы — использовать модель, похожую на распространённую в CVS: несколько разработчиков с особыми правами отправляют изменения в один общий репозиторий и получают их из него. Инструкции по настройке см. в gitcvs-migration[7].
Однако, хотя поддержка общих репозиториев в Git вполне работоспособна, такой режим обычно не рекомендуется просто потому, что поддерживаемый Git способ совместной работы — обмен исправлениями и получение изменений из публичных репозиториев — имеет множество преимуществ перед центральным общим репозиторием:
-
Благодаря способности Git быстро импортировать и объединять исправления один сопровождающий может обрабатывать входящие изменения даже при очень высокой интенсивности. А когда это становится слишком сложно,
gitpullпозволяет легко поручить эту работу другим сопровождающим, сохранив возможность проверять поступающие изменения. -
Поскольку репозиторий каждого разработчика содержит полную копию истории проекта, ни один репозиторий не является особенным. Другому разработчику легко взять на себя сопровождение проекта — по взаимному соглашению либо в случае, если сопровождающий перестал отвечать или с ним стало трудно работать.
-
Отсутствие центральной группы «участников с правом отправки изменений» означает, что реже приходится формально решать, кто «входит» в группу, а кто — нет.
Настройка просмотра репозитория через веб-интерфейс
CGI-скрипт gitweb позволяет пользователям легко просматривать ревизии проекта, содержимое файлов и журналы, не устанавливая Git. Дополнительно можно включить такие возможности, как RSS/Atom-ленты и сведения blame/annotation.
Команда git-instaweb[1] позволяет быстро запустить просмотр репозитория с помощью gitweb. По умолчанию instaweb использует сервер lighttpd.
Инструкции по подробной настройке постоянной установки на сервере с поддержкой CGI или Perl см. в файле gitweb/INSTALL в дереве исходного кода Git и в gitweb[1].
Как получить репозиторий Git с минимальной историей
Поверхностный клон с усечённой историей полезен, если вас интересует только недавняя история проекта, а получение полной истории из вышестоящего репозитория обходится дорого.
Поверхностный клон создаётся с помощью параметра --depth команды git-clone[1]. Позже глубину можно изменить с помощью параметра --depth команды git-fetch[1] или восстановить полную историю с помощью --unshallow.
Слияние в поверхностном клоне будет работать, пока база слияния находится в недавней истории. В противном случае оно будет похоже на слияние несвязанных историй и может привести к огромному числу конфликтов. Из-за этого ограничения такой репозиторий может не подойти для рабочих процессов, основанных на слиянии.
Примеры
Поддержка тематических веток сопровождающим подсистемы Linux
В этом разделе описывается, как Тони Лак использует Git в роли сопровождающего архитектуры IA64 для ядра Linux.
Он использует две публичные ветки:
-
Дерево «test», куда первоначально помещаются исправления, чтобы они могли пройти проверку при интеграции с другой текущей разработкой. Эндрю может получать это дерево в -mm в любое время.
-
Дерево «release», куда перемещаются проверенные исправления для окончательной проверки и последующей отправки вверх по цепочке Линусу (посредством запроса «please pull»).
Кроме того, он использует набор временных веток («тематических веток»), каждая из которых содержит логически связанную группу исправлений.
Для настройки сначала создайте рабочее дерево, клонировав публичное дерево Линуса:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git work $ cd work
Дерево Линуса будет храниться в ветке отслеживания удалённого репозитория с именем origin/master; её можно обновлять с помощью git-fetch[1]. Для отслеживания других публичных деревьев используйте git-remote[1], чтобы настроить «удалённый репозиторий», и git-fetch[1], чтобы поддерживать его в актуальном состоянии; см. раздел Репозитории и ветки.
Теперь создайте ветки для работы. Изначально они указывают на текущую вершину ветки origin/master; их следует настроить (с помощью параметра --track команды git-branch[1]) на получение изменений от Линуса по умолчанию.
$ git branch --track test origin/master $ git branch --track release origin/master
Их легко поддерживать в актуальном состоянии с помощью git-pull[1].
$ git switch test && git pull $ git switch release && git pull
Важное замечание! Если в этих ветках есть локальные изменения, это слияние создаст в истории объект-коммит (если локальных изменений нет, Git просто выполнит слияние с «перемоткой вперёд»). Многие не любят «шум», который это создаёт в истории Linux, поэтому старайтесь без необходимости не делать этого в ветке release: эти шумные коммиты станут частью постоянной истории, когда вы попросите Линуса получить изменения из ветки release.
Несколько переменных конфигурации (см. git-config[1]) помогут упростить отправку обеих веток в публичное дерево. (См. раздел Настройка публичного репозитория.)
$ cat >> .git/config <<EOF
[remote "mytree"]
url = master.kernel.org:/pub/scm/linux/kernel/git/aegl/linux.git
push = release
push = test
EOF Теперь можно отправить деревья test и release с помощью git-push[1]:
$ git push mytree
или отправить только одну из веток — test или release — с помощью команды:
$ git push mytree test
или
$ git push mytree release
Теперь применим несколько исправлений от сообщества. Придумайте короткое и запоминающееся имя для ветки, в которой будет храниться это исправление (или связанная группа исправлений), и создайте новую ветку от недавнего стабильного тега ветки Линуса. Выбор стабильной основы для ветки поможет: 1) вам — не включать несвязанные и, возможно, недостаточно проверенные изменения; 2) тем, кто в будущем будет искать ошибки с помощью git bisect.
$ git switch -c speed-up-spinlocks v2.6.35
Теперь примените исправление (или исправления), выполните несколько тестов и зафиксируйте изменения. Если исправление состоит из нескольких частей, применяйте каждую часть отдельным коммитом в этой ветке.
$ ... patch ... test ... commit [ ... patch ... test ... commit ]*
Когда результат вас устроит, можно объединить изменения с веткой «test», чтобы подготовить их к публикации:
$ git switch test && git merge speed-up-spinlocks
Конфликты здесь маловероятны… но они могут возникнуть, если вы долго работали над этим шагом и за это время получили новые версии из вышестоящего репозитория.
Позже, когда пройдёт достаточно времени и исправления будут проверены, можно получить ту же ветку в дерево release, подготовив её к отправке вверх по цепочке. Здесь становится видна польза от хранения каждого исправления (или серии исправлений) в отдельной ветке: исправления можно переносить в дерево release в любом порядке.
$ git switch release && git merge speed-up-spinlocks
Со временем у вас появится несколько веток, и, несмотря на удачно подобранные имена, вы можете забыть, для чего они предназначены или в каком состоянии находятся. Чтобы вспомнить, какие изменения содержит конкретная ветка, выполните:
$ git log linux..branchname | git shortlog
Чтобы проверить, была ли она уже объединена с ветками test или release, выполните:
$ git log test..branchname
или
$ git log release..branchname
(Если эта ветка ещё не была объединена, вы увидите несколько записей журнала. Если объединение уже произошло, команда ничего не выведет.)
Когда исправление завершит полный цикл (перейдёт из test в release, будет получено Линусом, а затем вернётся в вашу локальную ветку origin/master), ветка с этим изменением больше не нужна. Это можно определить по пустому выводу команды:
$ git log origin..branchname
После этого ветку можно удалить:
$ git branch -d branchname
Некоторые изменения настолько незначительны, что для них не нужно создавать отдельную ветку, а затем объединять её с ветками test и release. Такие изменения можно сразу внести в ветку release, а затем объединить её с веткой test.
Отправив свою работу в mytree, можно воспользоваться git-request-pull[1], чтобы подготовить сообщение с запросом «please pull» для отправки Линусу:
$ git push mytree $ git request-pull origin mytree release
Ниже приведены скрипты, которые ещё больше упрощают все эти действия.
==== update script ====
# Update a branch in my Git tree. If the branch to be updated
# is origin, then pull from kernel.org. Otherwise merge
# origin/master branch into test|release branch
case "$1" in
test|release)
git checkout $1 && git pull . origin
;;
origin)
before=$(git rev-parse refs/remotes/origin/master)
git fetch origin
after=$(git rev-parse refs/remotes/origin/master)
if [ $before != $after ]
then
git log $before..$after | git shortlog
fi
;;
*)
echo "usage: $0 origin|test|release" 1>&2
exit 1
;;
esac ==== merge script ====
# Merge a branch into either the test or release branch
pname=$0
usage()
{
echo "usage: $pname branch test|release" 1>&2
exit 1
}
git show-ref -q --verify -- refs/heads/"$1" || {
echo "Can't see branch <$1>" 1>&2
usage
}
case "$2" in
test|release)
if [ $(git log $2..$1 | wc -c) -eq 0 ]
then
echo $1 already merged into $2 1>&2
exit 1
fi
git checkout $2 && git pull . $1
;;
*)
usage
;;
esac ==== status script ====
# report on status of my ia64 Git tree
gb=$(tput setab 2)
rb=$(tput setab 1)
restore=$(tput setab 9)
if [ `git rev-list test..release | wc -c` -gt 0 ]
then
echo $rb Warning: commits in release that are not in test $restore
git log test..release
fi
for branch in `git show-ref --heads | sed 's|^.*/||'`
do
if [ $branch = test -o $branch = release ]
then
continue
fi
echo -n $gb ======= $branch ====== $restore " "
status=
for ref in test release origin/master
do
if [ `git rev-list $ref..$branch | wc -c` -gt 0 ]
then
status=$status${ref:0:1}
fi
done
case $status in
trl)
echo $rb Need to pull into test $restore
;;
rl)
echo "In test"
;;
l)
echo "Waiting for linus"
;;
"")
echo $rb All done $restore
;;
*)
echo $rb "<$status>" $restore
;;
esac
git log origin/master..$branch | git shortlog
done Переписывание истории и поддержка серий исправлений
Обычно коммиты только добавляются в проект, но никогда не удаляются и не заменяются. Git разработан с таким предположением, и его нарушение приведёт к тому, что механизмы слияния Git (например) будут работать неправильно.
Однако бывают ситуации, когда нарушение этого предположения может быть полезным.
Создание идеальной серии патчей
Предположим, вы участвуете в разработке крупного проекта и хотите добавить сложную функциональность, представив её другим разработчикам так, чтобы им было удобно прочитать ваши изменения, проверить их корректность и понять причины каждого изменения.
Если представить все изменения в виде одного патча (или коммита), их может оказаться слишком много, чтобы изучить всё сразу.
Если представить полную историю своей работы, включая ошибки, исправления и тупиковые решения, это может их перегрузить.
Поэтому обычно идеальный вариант — подготовить серию патчей, в которой:
-
Каждый патч можно применить по порядку.
-
Каждый патч содержит одно логически завершённое изменение и сообщение, объясняющее его.
-
Ни один патч не вносит регрессию: после применения любой начальной части серии проект по-прежнему компилируется и работает, и в нём нет ошибок, которых не было раньше.
-
Вся серия приводит к тому же конечному результату, что и ваш собственный (вероятно, гораздо более беспорядочный!) процесс разработки.
Мы рассмотрим инструменты, которые помогут вам это сделать, объясним, как ими пользоваться, а затем расскажем о некоторых проблемах, которые могут возникнуть из-за переписывания истории.
Поддержание актуальности серии патчей с помощью git rebase
Предположим, вы создаёте ветку mywork на основе удалённой отслеживаемой ветки origin и создаёте несколько коммитов поверх неё:
$ git switch -c mywork origin $ vi file.txt $ git commit $ vi otherfile.txt $ git commit ...
Вы не выполняли слияний в mywork, поэтому это просто линейная последовательность патчей поверх origin:
o--o--O <-- origin
\
a--b--c <-- mywork В исходном проекте проделали более интересную работу, и origin продвинулась вперёд:
o--o--O--o--o--o <-- origin
\
a--b--c <-- mywork На этом этапе можно использовать pull, чтобы влить изменения обратно; в результате будет создан новый коммит слияния, например:
o--o--O--o--o--o <-- origin
\ \
a--b--c--m <-- mywork Однако, если вы предпочитаете сохранять в mywork простую последовательность коммитов без слияний, можно вместо этого воспользоваться git-rebase[1]:
$ git switch mywork $ git rebase origin
Эта команда удалит каждый ваш коммит из mywork, временно сохранив их в виде патчей (в каталоге с именем .git/rebase-apply), обновит mywork до последней версии origin, а затем применит каждый из сохранённых патчей к новой версии mywork. Результат будет выглядеть так:
o--o--O--o--o--o <-- origin
\
a'--b'--c' <-- mywork В процессе могут обнаружиться конфликты. В этом случае выполнение остановится, чтобы вы могли их устранить; после устранения конфликтов используйте git add, чтобы обновить индекс с учётом этих изменений, а затем вместо запуска git commit просто выполните
$ git rebase --continue
и Git продолжит применять остальные патчи.
В любой момент можно воспользоваться параметром --abort, чтобы прервать этот процесс и вернуть mywork в состояние, в котором она находилась до начала перебазирования:
$ git rebase --abort
Если нужно переупорядочить или отредактировать несколько коммитов в ветке, может быть удобнее использовать git rebase -i: эта команда позволяет переупорядочить коммиты и объединить их, а также пометить отдельные коммиты для редактирования во время перебазирования. Подробнее см. в разделе Использование интерактивного перебазирования, а альтернативные варианты — в разделе Переупорядочивание серии патчей или выбор патчей из неё.
Переписывание одного коммита
В разделе Исправление ошибки путём переписывания истории мы увидели, что последний коммит можно заменить с помощью команды
$ git commit --amend
Она заменит старый коммит новым, включающим ваши изменения, и позволит сначала отредактировать сообщение старого коммита. Это полезно для исправления опечаток в последнем коммите или изменения содержимого патча, который был подготовлен к коммиту неправильно.
Чтобы изменить коммиты, находящиеся глубже в истории, можно воспользоваться инструкцией edit интерактивного перебазирования.
Переупорядочивание серии патчей или выбор патчей из неё
Иногда возникает необходимость отредактировать коммит, находящийся глубже в истории. Один из вариантов — использовать git format-patch, чтобы создать серию патчей, а затем вернуть состояние к моменту до применения этих патчей:
$ git format-patch origin $ git reset --hard origin
Затем при необходимости измените, переупорядочьте или удалите патчи, прежде чем применить их снова с помощью git-am[1]:
$ git am *.patch
Использование интерактивного перебазирования
Также можно редактировать серию патчей с помощью интерактивного перебазирования. Оно позволяет переупорядочить серию патчей с помощью format-patch, поэтому используйте тот интерфейс, который вам больше нравится.
Перебазируйте текущий HEAD на последний коммит, который нужно оставить без изменений. Например, чтобы переупорядочить последние 5 коммитов, выполните:
$ git rebase -i HEAD~5
В редакторе откроется список шагов, которые будут выполнены при перебазировании.
pick deadbee The oneline of this commit pick fa1afe1 The oneline of the next commit ... # Rebase c0ffeee..deadbee onto c0ffeee # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
Как поясняется в комментариях, редактируя этот список, можно переупорядочить коммиты, объединить их, изменить сообщения коммитов и т. д. Когда список будет готов, сохраните его и закройте редактор — начнётся перебазирование.
Перебазирование остановится там, где pick заменено на edit, или если на каком-либо шаге не удастся автоматически разрешить конфликты и потребуется ваша помощь. Закончив редактирование и/или разрешение конфликтов, можно продолжить с помощью команды git rebase --continue. Если процесс становится слишком сложным, его всегда можно прервать командой git rebase --abort. Даже после завершения перебазирования исходную ветку всё ещё можно восстановить с помощью журнала ссылок (reflog).
Более подробное описание этой процедуры и дополнительные советы см. в разделе «ИНТЕРАКТИВНЫЙ РЕЖИМ» справочной страницы git-rebase[1].
Другие инструменты
Существует множество других инструментов, например StGit, предназначенных для сопровождения серии патчей. Их рассмотрение выходит за рамки этого руководства.
Проблемы переписывания истории
Основная проблема переписывания истории ветки связана со слияниями. Предположим, кто-то получает вашу ветку и сливает её со своей веткой, в результате чего получается примерно следующее:
o--o--O--o--o--o <-- origin
\ \
t--t--t--m <-- their branch: Затем предположим, что вы изменяете последние три коммита:
o--o--o <-- new head of origin
/
o--o--O--o--o--o <-- old head of origin Если рассмотреть всю эту историю вместе в одном репозитории, она будет выглядеть так:
o--o--o <-- new head of origin
/
o--o--O--o--o--o <-- old head of origin
\ \
t--t--t--m <-- their branch: Git никак не может определить, что новая вершина ветки — это обновлённая версия старой; он рассматривает эту ситуацию так же, как если бы два разработчика независимо выполнили работу на старой и новой вершинах ветки параллельно. Если на этом этапе кто-то попытается влить новую вершину в свою ветку, Git попытается объединить две линии разработки (старую и новую), а не заменить старую новой. Результат, скорее всего, будет неожиданным.
Вы всё ещё можете публиковать ветки с переписанной историей; другим может быть полезно получить такие ветки, чтобы изучить или протестировать их, но не следует пытаться влить их в свою работу.
Для полноценной распределённой разработки с корректной поддержкой слияний опубликованные ветки никогда не следует переписывать.
Почему поиск с помощью bisect по коммитам слияния может быть сложнее, чем по линейной истории
Команда git-bisect[1] корректно обрабатывает историю, содержащую коммиты слияния. Однако, если найденный ею коммит является коммитом слияния, пользователю может потребоваться приложить больше усилий, чем обычно, чтобы выяснить, почему именно этот коммит внёс проблему.
Представьте себе такую историю:
---Z---o---X---...---o---A---C---D
\ /
o---o---Y---...---o---B Предположим, что в верхней линии разработки в коммите X меняется смысл одной из функций, существующих в Z. Коммиты от Z до A изменяют как реализацию функции, так и все места её вызова, существовавшие в Z, а также новые места вызова, добавленные этими коммитами, чтобы они соответствовали друг другу. В A ошибки нет.
Предположим, что тем временем в нижней линии разработки кто-то добавляет новое место вызова этой функции в коммите Y. Коммиты от Z до B исходят из старого поведения этой функции, а вызывающий и вызываемый код согласованы друг с другом. В B тоже нет ошибки.
Кроме того, предположим, что две линии разработки сливаются в C без конфликтов, поэтому разрешать конфликты не требуется.
Тем не менее код в C содержит ошибку, поскольку места вызова, добавленные в нижней линии разработки, не были адаптированы к новому поведению, введённому в верхней линии разработки. Если вам известно только, что D содержит ошибку, Z работает правильно, а git-bisect[1] указывает на C как на причину проблемы, как выяснить, что проблема вызвана изменением поведения функции?
Если результатом git bisect является коммит, не являющийся коммитом слияния, обычно достаточно изучить только этот коммит, чтобы обнаружить проблему. Разработчики могут упростить эту задачу, разбивая изменения на небольшие, самостоятельные коммиты. Однако в описанном выше случае это не поможет, поскольку проблема не очевидна при изучении какого-либо одного коммита; вместо этого требуется рассмотреть процесс разработки в целом. Что ещё хуже, изменение поведения проблемной функции может быть лишь небольшой частью изменений в верхней линии разработки.
С другой стороны, если бы вместо слияния в C вы перебазировали историю от Z до B поверх A, получилась бы такая линейная история:
---Z---o---X--...---o---A---o---o---Y*--...---o---B*--D*
При поиске между Z и D* был бы найден один коммит-причина — Y*, и понять, почему Y* содержит ошибку, вероятно, было бы проще.
Отчасти по этой причине многие опытные пользователи Git, даже работая над проектами, где часто выполняются слияния, сохраняют линейную историю, перебазируя изменения поверх последней версии основного проекта перед публикацией.
Расширенное управление ветками
Получение отдельных веток
Вместо использования git-remote[1] можно обновлять по одной ветке за раз и сохранять её локально под произвольным именем:
$ git fetch origin todo:my-todo-work
Первый аргумент, origin, просто указывает Git получить данные из репозитория, из которого вы изначально клонировали проект. Второй аргумент указывает Git получить из удалённого репозитория ветку с именем todo и сохранить её локально под именем refs/heads/my-todo-work.
Можно также получать ветки из других репозиториев. Например, команда
$ git fetch git://example.com/proj.git master:example-master
создаст новую ветку с именем example-master и сохранит в ней ветку с именем master из репозитория по указанному URL. Если ветка example-master уже существует, Git попытается выполнить для неё перемотку вперёд до коммита, на который указывает ветка master на example.com. Подробнее:
git fetch и перемотка вперёд
В предыдущем примере при обновлении существующей ветки команда git fetch проверяет, является ли последний коммит удалённой ветки потомком последнего коммита в вашей копии этой ветки, прежде чем обновить вашу копию и переместить её указатель на новый коммит. Git называет этот процесс перемоткой вперёд.
Перемотка вперёд выглядит примерно так:
o--o--o--o <-- old head of the branch
\
o--o--o <-- new head of the branch В некоторых случаях новая вершина ветки может не являться потомком старой вершины. Например, разработчик мог обнаружить серьёзную ошибку и решить вернуться к предыдущему состоянию. Тогда ситуация будет выглядеть примерно так:
o--o--o--o--a--b <-- old head of the branch
\
o--o--o <-- new head of the branch В этом случае команда git fetch завершится ошибкой и выведет предупреждение.
В такой ситуации всё ещё можно принудительно переместить указатель Git на новую вершину ветки, как описано в следующем разделе. Однако учтите, что в приведённом выше случае это может привести к потере коммитов с метками a и b, если вы заранее не создали собственную ссылку, указывающую на них.
Принудительное выполнение git fetch без перемотки вперёд
Если выполнение git fetch завершается ошибкой, потому что новая вершина ветки не является потомком старой, обновление можно выполнить принудительно:
$ git fetch git://example.com/proj.git +master:refs/remotes/example/master
Обратите внимание на знак +. В качестве альтернативы можно использовать флаг -f, чтобы принудительно обновить все полученные ветки, например:
$ git fetch -f origin
Помните, что коммиты, на которые указывала старая версия example/master, могут быть потеряны, как описано в предыдущем разделе.
Настройка удалённых отслеживаемых веток
Выше мы увидели, что origin — это просто сокращённое обозначение репозитория, из которого вы изначально клонировали проект. Эта информация хранится в переменных конфигурации Git, которые можно просмотреть с помощью git-config[1]:
$ git config list core.repositoryformatversion=0 core.filemode=true core.logallrefupdates=true remote.origin.url=git://git.kernel.org/pub/scm/git/git.git remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* branch.master.remote=origin branch.master.merge=refs/heads/master
Если вы часто используете и другие репозитории, можно создать аналогичные параметры конфигурации, чтобы не вводить команды целиком. Например, команда
$ git remote add example git://example.com/proj.git
добавит в .git/config следующие строки:
[remote "example"]
url = git://example.com/proj.git
fetch = +refs/heads/*:refs/remotes/example/* Обратите также внимание, что приведённую выше конфигурацию можно задать, отредактировав файл .git/config напрямую, а не используя git-remote[1].
После настройки удалённого репозитория следующие три команды выполняют одно и то же:
$ git fetch git://example.com/proj.git +refs/heads/*:refs/remotes/example/* $ git fetch example +refs/heads/*:refs/remotes/example/* $ git fetch example
Подробнее о перечисленных выше параметрах конфигурации см. в git-config[1], а о синтаксисе refspec — в git-fetch[1].
Основные понятия Git
Git построен на небольшом количестве простых, но мощных идей. Хотя можно работать и не понимая их, с ними Git станет для вас гораздо интуитивнее.
Начнём с самого важного: базы объектов и индекса.
База объектов
В разделе Понимание истории: коммиты мы уже видели, что все коммиты хранятся под 40-значным «именем объекта». На самом деле вся информация, необходимая для представления истории проекта, хранится в объектах с такими именами. В каждом случае имя вычисляется как хеш SHA-1 содержимого объекта. Хеш SHA-1 — это криптографическая хеш-функция. Для нас это означает, что невозможно найти два разных объекта с одинаковым именем. У этого есть ряд преимуществ, в частности:
-
Git может быстро определить, идентичны ли два объекта, просто сравнив их имена.
-
Поскольку имена объектов вычисляются одинаково во всех репозиториях, одинаковое содержимое в двух репозиториях всегда будет храниться под одним и тем же именем.
-
Git может обнаружить ошибки при чтении объекта, проверив, что имя объекта по-прежнему является хешем SHA-1 его содержимого.
(Подробности о формате объектов и вычислении SHA-1 см. в разделе Формат хранения объектов.)
Существует четыре разных типа объектов: «blob», «tree», «commit» и «tag».
-
Объект «blob» используется для хранения данных файла.
-
Объект «tree» объединяет один или несколько объектов «blob» в структуру каталогов. Кроме того, объект tree может ссылаться на другие объекты tree, образуя иерархию каталогов.
-
Объект «commit» объединяет такие иерархии каталогов в ориентированный ациклический граф ревизий: каждый коммит содержит имя объекта ровно одного дерева, задающего иерархию каталогов на момент коммита. Кроме того, коммит ссылается на родительские объекты commit, описывающие историю того, как мы пришли к этой иерархии каталогов.
-
Объект «tag» символически идентифицирует другие объекты и может использоваться для их подписания. Он содержит имя и тип другого объекта, символическое имя (разумеется!) и, необязательно, подпись.
Подробнее о типах объектов:
Объект commit
Объект «commit» связывает физическое состояние дерева с описанием того, как и почему мы пришли к этому состоянию. Используйте параметр --pretty=raw команд git-show[1] или git-log[1], чтобы изучить интересующий вас коммит:
$ git show -s --pretty=raw 2be7fcb476
commit 2be7fcb4764f2dbcee52635b91fedb1b3dcf7ab4
tree fb3a8bdd0ceddd019615af4d57a53f43d8cee2bf
parent 257a84d9d02e90447b149af58b271c19405edb6a
author Dave Watson <dwatson@mimvista.com> 1187576872 -0400
committer Junio C Hamano <gitster@pobox.com> 1187591163 -0700
Fix misspelling of 'suppress' in docs
Signed-off-by: Junio C Hamano <gitster@pobox.com> Как видите, коммит определяется следующими данными:
-
дерево: имя SHA-1 объекта tree (описанного ниже), представляющего содержимое каталога в определённый момент времени;
-
родительские коммиты: имена SHA-1 некоторых коммитов, представляющих непосредственно предшествующие шаги в истории проекта. В примере выше один родительский коммит; у коммитов слияния их может быть больше. Коммит без родителей называется корневым коммитом и представляет начальную ревизию проекта. В каждом проекте должен быть как минимум один корневой коммит. В проекте также может быть несколько корневых коммитов, хотя это встречается нечасто (и не всегда является хорошей идеей);
-
автор: имя человека, ответственного за это изменение, и дата;
-
коммитер: имя человека, который фактически создал коммит, и дата создания. Это может быть не автор, например, если автор написал исправление и отправил его по электронной почте человеку, который использовал его для создания коммита;
-
комментарий с описанием этого коммита.
Обратите внимание: сам коммит не содержит сведений о том, что именно изменилось; все изменения вычисляются сравнением содержимого дерева, на которое ссылается этот коммит, с деревьями, связанными с его родительскими коммитами. В частности, Git не пытается явно записывать переименования файлов, хотя может определить случаи, когда наличие тех же данных файла по изменившимся путям указывает на переименование. (См., например, параметр -M команды git-diff[1].)
Обычно коммит создаётся командой git-commit[1]. Она создаёт коммит, родителем которого обычно является текущий HEAD, а дерево берётся из содержимого, сохранённого в данный момент в индексе.
Объект tree
Универсальную команду git-show[1] можно использовать и для изучения объектов tree, но команда git-ls-tree[1] предоставит больше подробностей:
$ git ls-tree fb3a8bdd0ce 100644 blob 63c918c667fa005ff12ad89437f2fdc80926e21c .gitignore 100644 blob 5529b198e8d14decbe4ad99db3f7fb632de0439d .mailmap 100644 blob 6ff87c4664981e4397625791c8ea3bbb5f2279a3 COPYING 040000 tree 2fb783e477100ce076f6bf57e4a6f026013dc745 Documentation 100755 blob 3c0032cec592a765692234f1cba47dfdcc3a9200 GIT-VERSION-GEN 100644 blob 289b046a443c0647624607d471289b2c7dcd470b INSTALL 100644 blob 4eb463797adc693dc168b926b6932ff53f17d0b1 Makefile 100644 blob 548142c327a6790ff8821d67c2ee1eff7a656b52 README ...
Как видите, объект tree содержит список записей, каждая из которых имеет режим доступа, тип объекта, имя SHA-1 и название; записи отсортированы по названию. Он представляет содержимое одного дерева каталогов.
Типом объекта может быть blob, представляющий содержимое файла, или другой tree, представляющий содержимое подкаталога. Поскольку деревья и blob-объекты, как и все остальные объекты, именуются по хешу SHA-1 их содержимого, два дерева имеют одинаковое имя SHA-1 тогда и только тогда, когда их содержимое (включая, рекурсивно, содержимое всех подкаталогов) идентично. Благодаря этому Git может быстро определять различия между двумя связанными объектами tree, игнорируя записи с одинаковыми именами объектов.
(Примечание: при наличии подмодулей записи деревьев могут также быть коммитами. Документацию см. в разделе Подмодули.)
Обратите внимание: для всех файлов установлен режим 644 или 755; на самом деле Git учитывает только бит исполняемого файла.
Объект blob
Для просмотра содержимого blob можно использовать команду git-show[1]. Например, рассмотрим blob в записи COPYING из дерева выше:
$ git show 6ff87c4664 Note that the only valid version of the GPL as far as this project is concerned is _this_ particular version of the license (ie v2, not v2.2 or v3.x or whatever), unless explicitly otherwise stated. ...
Объект «blob» — это не что иное, как двоичный блок данных. Он ни на что не ссылается и не имеет никаких атрибутов.
Поскольку blob полностью определяется своими данными, если два файла в дереве каталогов (или в нескольких разных версиях репозитория) имеют одинаковое содержимое, они будут использовать один и тот же объект blob. Объект полностью независим от своего расположения в дереве каталогов, и переименование файла не меняет объект, с которым связан этот файл.
Обратите внимание: любой объект tree или blob можно изучить с помощью git-show[1], используя синтаксис <revision>:<path>. Это может пригодиться при просмотре содержимого дерева, которое в данный момент не извлечено в рабочую копию.
Доверие
Если вы получили имя SHA-1 blob из одного источника, а его содержимое — из другого, возможно, ненадёжного источника, вы всё равно можете быть уверены в правильности содержимого, если имя SHA-1 совпадает. Это объясняется тем, что SHA-1 разработан так, чтобы найти разные данные, дающие одинаковый хеш, было практически невозможно.
Аналогично, чтобы доверять содержимому всего каталога, на который ссылается объект дерева верхнего уровня, достаточно доверять его имени SHA-1. Если вы получили имя SHA-1 коммита из надёжного источника, то можете легко проверить всю историю коммитов, достижимых через родителей этого коммита, а также всё содержимое деревьев, на которые ссылаются эти коммиты.
Таким образом, чтобы обеспечить системе реальное доверие, достаточно подписать цифровой подписью только специальное примечание one, содержащее имя коммита верхнего уровня. Ваша цифровая подпись показывает другим, что вы доверяете этому коммиту, а неизменяемость истории коммитов говорит им, что всей этой истории тоже можно доверять.
Иными словами, весь архив можно легко проверить, отправив одно электронное письмо с именем (хешем SHA-1) верхнего коммита и подписав письмо цифровой подписью с помощью, например, GPG/PGP.
Для этого в Git также предусмотрен объект tag…
Объект tag
Объект tag содержит объект, тип объекта, имя тега, имя создавшего тег человека («тегировщика») и сообщение, которое может содержать подпись, как видно из вывода команды git-cat-file[1]:
$ git cat-file tag v1.5.0 object 437b1b20df4b356c9342dac8d38849f24ef44f27 type commit tag v1.5.0 tagger Junio C Hamano <junkio@cox.net> 1171411200 +0000 GIT 1.5.0 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.6 (GNU/Linux) iD8DBQBF0lGqwMbZpPMRm5oRAuRiAJ9ohBLd7s2kqjkKlq1qqC57SbnmzQCdG4ui nLE/L9aUXdWeTFPron96DLA= =2E+0 -----END PGP SIGNATURE-----
О создании и проверке объектов tag см. команду git-tag[1]. (Обратите внимание: git-tag[1] также позволяет создавать «лёгкие теги», которые вообще не являются объектами tag, а представляют собой простые ссылки, имена которых начинаются с refs/tags/.)
Как Git эффективно хранит объекты: pack-файлы
Новые объекты изначально создаются в файле, названном по хешу SHA-1 объекта (файл хранится в .git/objects).
К сожалению, такая система становится неэффективной, когда в проекте появляется много объектов. Попробуйте выполнить это в старом проекте:
$ git count-objects 6930 objects, 47620 kilobytes
Первое число — это количество объектов, хранящихся в отдельных файлах. Второе — объём места, занимаемый этими «свободными» объектами.
Можно сэкономить место и ускорить Git, переместив эти свободные объекты в «pack-файл», который хранит группу объектов в эффективном сжатом формате. Подробности о формате pack-файлов см. в gitformat-pack[5].
Чтобы поместить свободные объекты в pack-файл, просто выполните git repack:
$ git repack Counting objects: 6020, done. Delta compression using up to 4 threads. Compressing objects: 100% (6020/6020), done. Writing objects: 100% (6020/6020), done. Total 6020 (delta 4070), reused 0 (delta 0)
Эта команда создаёт один «pack-файл» в .git/objects/pack/, содержащий все объекты, которые ещё не были упакованы. Затем можно выполнить
$ git prune
чтобы удалить все «свободные» объекты, которые теперь содержатся в pack-файле. Также будут удалены все объекты, на которые нет ссылок (например, они могут появиться при использовании git reset для удаления коммита). Убедиться, что свободных объектов больше нет, можно, проверив каталог .git/objects или выполнив
$ git count-objects 0 objects, 0 kilobytes
Хотя файлы объектов исчезли, все команды, ссылающиеся на эти объекты, будут работать как и прежде.
Команда git-gc[1] выполняет упаковку, очистку и другие операции, поэтому обычно это единственная команда высокого уровня, которая вам понадобится.
Висячие объекты
Команда git-fsck[1] иногда сообщает о висячих объектах. Это не проблема.
Чаще всего висячие объекты появляются после перебазирования ветки или получения изменений от кого-то, кто перебазировал ветку. См. раздел Переписывание истории и поддержка серий исправлений. В таком случае старое состояние вершины исходной ветки всё ещё существует, как и всё, на что оно указывало. Исчезает только указатель ветки, поскольку вы заменили его другим.
Висячие объекты могут возникать и в других ситуациях. Например, «висячий blob» может появиться, если вы выполнили git add для файла, но затем, до того как вы действительно закоммитили его и включили в общую картину, изменили что-то ещё в этом файле и закоммитили обновлённый вариант. Старое состояние, которое вы добавили изначально, в итоге не будет указано ни одним коммитом или деревом, поэтому оно станет висячим объектом blob.
Аналогично, при использовании стратегии слияния «ort», если обнаруживаются перекрёстные слияния и, следовательно, несколько оснований слияния (что бывает довольно редко, но всё же случается), создаётся одно временное промежуточное дерево (или, возможно, несколько, если перекрёстных слияний много и оснований слияния больше двух) в качестве временного внутреннего основания слияния. Это реальные объекты, но итоговый результат на них не ссылается, поэтому они остаются «висячими» в репозитории.
Как правило, о висячих объектах не стоит беспокоиться. Они даже могут оказаться очень полезными: если вы что-то испортили, висячие объекты могут помочь восстановить старое дерево (допустим, вы выполнили перебазирование, а затем поняли, что не хотели этого делать: можно посмотреть, какие висячие объекты имеются, и решить, следует ли сбросить HEAD к какому-нибудь старому висячему состоянию).
Для коммитов можно просто выполнить:
$ gitk <dangling-commit-sha-goes-here> --not --all
Эта команда выводит всю историю, достижимую из указанного коммита, но недостижимую ни из одной ветки, тега или другой ссылки. Если вы решите, что хотите сохранить этот объект, всегда можно создать на него новую ссылку, например:
$ git branch recovered-branch <dangling-commit-sha-goes-here>
Для blob-объектов и деревьев так сделать нельзя, но их всё равно можно изучить. Для этого выполните
$ git show <dangling-blob/tree-sha-goes-here>
чтобы посмотреть содержимое blob (или, в случае дерева, узнать, по сути, каким было ls этого каталога). Это может помочь понять, какая операция оставила этот висячий объект.
Обычно висячие blob-объекты и деревья не представляют особого интереса. Почти всегда они появляются либо в результате промежуточного основания слияния (blob даже часто содержит маркеры конфликта слияния, если у вас были конфликты, которые вы разрешили вручную), либо просто потому, что вы прервали git fetch нажатием ^C или чем-то подобным. В результате в базе объектов остаётся some новых объектов, которые просто висят без ссылок и бесполезны.
В любом случае, когда вы убедитесь, что висячие состояния вам не нужны, можно удалить все недостижимые объекты:
$ git prune
и они исчезнут. (Выполнять git prune следует только для неактивного репозитория — это похоже на восстановление файловой системы с помощью fsck: не следует делать это, пока файловая система смонтирована. Команда git prune спроектирована так, чтобы не наносить вреда в подобных случаях одновременного доступа к репозиторию, но вы можете получить запутанные или пугающие сообщения.)
Восстановление после повреждения репозитория
По замыслу Git осторожно обращается с доверенными ему данными. Однако даже при отсутствии ошибок в самом Git аппаратные сбои или ошибки операционной системы могут повредить данные.
Первая защита от таких проблем — резервное копирование. Создать резервную копию каталога Git можно с помощью clone, cp, tar или любого другого средства резервного копирования.
В крайнем случае можно найти повреждённые объекты и попытаться заменить их вручную. Перед этим создайте резервную копию репозитория на случай, если в процессе вы повредите его ещё сильнее.
Будем считать, что проблема заключается в одном отсутствующем или повреждённом blob — такую проблему иногда можно решить. (Восстановить отсутствующие деревья, а особенно коммиты, намного сложнее.)
Перед началом проверьте, есть ли повреждения, и выясните, где они находятся, с помощью git-fsck[1]; эта операция может занять много времени.
Предположим, что вывод выглядит так:
$ git fsck --full --no-dangling
broken link from tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
to blob 4b9458b3786228369c63936db65827de3cc06200
missing blob 4b9458b3786228369c63936db65827de3cc06200 Теперь вы знаете, что blob 4b9458b3 отсутствует, а дерево 2d9263c6 ссылается на него. Если бы вам удалось найти хотя бы одну копию этого отсутствующего объекта blob, возможно, в другом репозитории, можно было бы переместить её в .git/objects/4b/9458b3... — и на этом всё. Предположим, найти её не удалось. Вы всё равно можете изучить дерево, которое на неё ссылалось, с помощью git-ls-tree[1]. Вывод может выглядеть примерно так:
$ git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8 100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8 .gitignore 100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883 .mailmap 100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c COPYING ... 100644 blob 4b9458b3786228369c63936db65827de3cc06200 myfile ...
Теперь вы знаете, что отсутствующий blob содержал данные файла с именем myfile. Возможно, вы также сможете определить каталог — допустим, это somedirectory. Если повезёт, отсутствующая копия может совпасть с копией, извлечённой в рабочее дерево в somedirectory/myfile. Это можно проверить с помощью git-hash-object[1]:
$ git hash-object -w somedirectory/myfile
Эта команда создаст и сохранит blob-объект с содержимым somedirectory/myfile и выведет SHA-1 этого объекта. Если вам невероятно повезёт, он окажется равен 4b9458b3786228369c63936db65827de3cc06200. Значит, вы угадали, и повреждение устранено!
В противном случае понадобится больше информации. Как определить, какая версия файла была утрачена?
Проще всего сделать это с помощью:
$ git log --raw --all --full-history -- somedirectory/myfile
Поскольку вы запросили вывод в необработанном формате, теперь вы получите примерно следующее:
commit abc Author: Date: ... :100644 100644 4b9458b newsha M somedirectory/myfile commit xyz Author: Date: ... :100644 100644 oldsha 4b9458b M somedirectory/myfile
Из этого следует, что непосредственно следующая версия файла имела имя «newsha», а предыдущая — «oldsha». Также вам известны сообщения коммитов, относящиеся к изменению с oldsha на 4b9458b и с 4b9458b на newsha.
Если вы фиксировали достаточно небольшие изменения, теперь у вас может быть хороший шанс восстановить содержимое промежуточного состояния 4b9458b.
Если это удастся, можно воссоздать отсутствующий объект с помощью
$ git hash-object -w <recreated-file>
и репозиторий снова будет в порядке!
(Кстати, можно было не обращать внимания на fsck и начать с выполнения
$ git log --raw --all
а затем поискать SHA отсутствующего объекта (4b9458b) во всём этом выводе. Решать вам: у Git действительно есть много информации, просто отсутствует одна конкретная версия blob.)
Индекс
Индекс — это двоичный файл (обычно хранящийся в .git/index), содержащий отсортированный список путей, каждый из которых сопровождается правами доступа и SHA-1 blob-объекта. Содержимое индекса можно посмотреть с помощью git-ls-files[1]:
$ git ls-files --stage 100644 63c918c667fa005ff12ad89437f2fdc80926e21c 0 .gitignore 100644 5529b198e8d14decbe4ad99db3f7fb632de0439d 0 .mailmap 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0 COPYING 100644 a37b2152bd26be2c2289e1f57a292534a51a93c7 0 Documentation/.gitignore 100644 fbefe9a45b00a54b58d94d06eca48b03d40a50e0 0 Documentation/Makefile ... 100644 2511aef8d89ab52be5ec6a5e46236b4b6bcd07ea 0 xdiff/xtypes.h 100644 2ade97b2574a9f77e7ae4002a4e07a6a38e46d07 0 xdiff/xutils.c 100644 d5de8292e05e7c36c4b68857c1cf9855e3d2f70a 0 xdiff/xutils.h
Обратите внимание: в старой документации индекс может называться «кэшем текущего каталога» или просто «кэшем». У него есть три важных свойства:
-
Индекс содержит всю информацию, необходимую для создания единственного (однозначно определённого) объекта tree.
Например, команда git-commit[1] создаёт этот объект tree на основе индекса, сохраняет его в базе объектов и использует как объект tree, связанный с новым коммитом.
-
Индекс позволяет быстро сравнивать определяемый им объект tree с рабочим деревом.
Для этого в нём хранятся некоторые дополнительные данные для каждой записи (например, время последнего изменения). Эти данные не отображаются выше и не сохраняются в создаваемом объекте tree, но позволяют быстро определить, какие файлы в рабочем каталоге отличаются от сохранённых в индексе, и тем самым избавляют Git от необходимости читать все данные таких файлов для поиска изменений.
-
В нём можно эффективно представить информацию о конфликтах слияния между разными объектами tree, связав каждый путь с достаточными сведениями об участвующих деревьях, чтобы создать трёхстороннее слияние.
В разделе Получение помощи в разрешении конфликтов при слиянии мы видели, что во время слияния индекс может хранить несколько версий одного файла (так называемые «этапы»). Третий столбец в приведённом выше выводе команды git-ls-files[1] — это номер этапа. Для файлов с конфликтами слияния его значение будет отличаться от 0.
Таким образом, индекс — это своего рода временная промежуточная область, в которую помещается дерево, над которым вы работаете.
Если полностью удалить индекс, обычно вы не потеряете никакой информации, если знаете имя дерева, которое он описывал.
Подмодули
Крупные проекты часто состоят из небольших автономных модулей. Например, дерево исходного кода встроенного дистрибутива Linux будет содержать все программное обеспечение дистрибутива с некоторыми локальными изменениями; для сборки видеоплеера может потребоваться конкретная, заведомо рабочая версия библиотеки распаковки; несколько независимых программ могут использовать одни и те же сценарии сборки.
В централизованных системах управления версиями это часто реализуется включением всех модулей в один репозиторий. Разработчики могут извлечь все модули или только те, с которыми им нужно работать. Они даже могут изменять файлы в нескольких модулях в одном коммите, перемещая файлы или обновляя API и переводы.
Git не поддерживает частичное извлечение, поэтому для воспроизведения такого подхода в Git разработчикам пришлось бы хранить локальные копии модулей, которые они не собираются изменять. Коммиты в огромной рабочей копии выполнялись бы медленнее, чем можно ожидать, поскольку Git пришлось бы проверять изменения во всех каталогах. Если у модулей длинная локальная история, клонирование заняло бы вечность.
Зато распределённые системы управления версиями гораздо лучше интегрируются с внешними источниками. В централизованной модели произвольный снимок внешнего проекта экспортируется из его собственной системы управления версиями, а затем импортируется в локальную систему управления версиями в ветку поставщика. Вся история скрыта. С распределённой системой управления версиями можно клонировать всю внешнюю историю, гораздо проще следить за разработкой и повторно объединять локальные изменения.
Поддержка подмодулей в Git позволяет хранить в репозитории извлечённую копию внешнего проекта в виде подкаталога. Подмодули сохраняют собственную идентичность: поддержка подмодулей лишь хранит расположение репозитория подмодуля и идентификатор коммита, поэтому другие разработчики, клонирующие содержащий проект («суперпроект»), могут легко клонировать все подмодули на ту же ревизию. Частичное извлечение суперпроекта возможно: можно указать Git не клонировать ни одного, некоторые или все подмодули.
Команда git-submodule[1] доступна начиная с Git 1.5.3. Пользователи Git 1.5.2 могут найти коммиты подмодулей в репозитории и извлечь их вручную; более ранние версии вообще не распознают подмодули.
Чтобы посмотреть, как работает поддержка подмодулей, создайте четыре примерных репозитория, которые позже можно будет использовать как подмодули:
$ mkdir ~/git
$ cd ~/git
$ for i in a b c d
do
mkdir $i
cd $i
git init
echo "module $i" > $i.txt
git add $i.txt
git commit -m "Initial commit, submodule $i"
cd ..
done Теперь создайте суперпроект и добавьте все подмодули:
$ mkdir super
$ cd super
$ git init
$ for i in a b c d
do
git submodule add ~/git/$i $i
done | Примечание | Не используйте здесь локальные URL, если собираетесь публиковать суперпроект! |
Посмотрите, какие файлы создала команда git submodule:
$ ls -a . .. .git .gitmodules a b c d
Команда git submodule add <repo> <path> выполняет несколько действий:
-
Клонирует подмодуль из <repo> в указанный <path> относительно текущего каталога и по умолчанию извлекает ветку master.
-
Добавляет путь клона подмодуля в файл gitmodules[5] и добавляет этот файл в индекс, готовый к коммиту.
-
Добавляет текущий идентификатор коммита подмодуля в индекс, готовый к коммиту.
Закоммитьте суперпроект:
$ git commit -m "Add submodules a, b, c and d."
Теперь клонируйте суперпроект:
$ cd .. $ git clone super cloned $ cd cloned
Каталоги подмодулей на месте, но они пусты:
$ ls -a a . .. $ git submodule status -d266b9873ad50488163457f025db7cdd9683d88b a -e81d457da15309b4fef4249aba9b50187999670d b -c1536a972b9affea0f16e0680ba87332dc059146 c -d96249ff5d57de5de093e6baff9e0aafa5276a74 d
| Примечание | Показанные выше имена объектов коммитов у вас будут другими, но они должны совпадать с именами объектов коммитов HEAD ваших репозиториев. Проверить это можно, выполнив git ls-remote ../a. |
Получение подмодулей состоит из двух шагов. Сначала выполните git submodule init, чтобы добавить URL репозиториев подмодулей в .git/config:
$ git submodule init
Теперь используйте git submodule update, чтобы клонировать репозитории и извлечь коммиты, указанные в суперпроекте:
$ git submodule update $ cd a $ ls -a . .. .git a.txt
Одно из важных различий между git submodule update и git submodule add состоит в том, что git submodule update извлекает определённый коммит, а не вершину ветки. Это похоже на извлечение тега: HEAD отсоединён, поэтому вы не работаете ни в одной ветке.
$ git branch * (detached from d266b98) master
Если вы хотите внести изменение в подмодуль, а HEAD отсоединён, создайте ветку или переключитесь на существующую, внесите изменения, опубликуйте их в подмодуле, а затем обновите суперпроект, чтобы он ссылался на новый коммит:
$ git switch master
или
$ git switch -c fix-up
затем
$ echo "adding a line again" >> a.txt $ git commit -a -m "Updated the submodule from within the superproject." $ git push $ cd .. $ git diff diff --git a/a b/a index d266b98..261dfac 160000 --- a/a +++ b/a @@ -1 +1 @@ -Subproject commit d266b9873ad50488163457f025db7cdd9683d88b +Subproject commit 261dfac35cb99d380eb966e102c1197139f7fa24 $ git add a $ git commit -m "Updated submodule a." $ git push
Если вы хотите также обновить подмодули, после git pull необходимо выполнить git submodule update.
Подводные камни при работе с подмодулями
Всегда публикуйте изменение подмодуля до публикации изменения суперпроекта, который на него ссылается. Если забыть опубликовать изменение подмодуля, другие не смогут клонировать репозиторий:
$ cd ~/git/super/a $ echo i added another line to this file >> a.txt $ git commit -a -m "doing it wrong this time" $ cd .. $ git add a $ git commit -m "Updated submodule a again." $ git push $ cd ~/git/cloned $ git pull $ git submodule update error: pathspec '261dfac35cb99d380eb966e102c1197139f7fa24' did not match any file(s) known to git. Did you forget to 'git add'? Unable to checkout '261dfac35cb99d380eb966e102c1197139f7fa24' in submodule path 'a'
В старых версиях Git легко было забыть закоммитить новые или изменённые файлы в подмодуле, что незаметно приводило к проблемам, аналогичным тем, которые возникают, если не отправить изменения подмодуля. Начиная с Git 1.7.0 команды git status и git diff в суперпроекте показывают подмодули как изменённые, если они содержат новые или изменённые файлы, чтобы защитить от случайного коммита такого состояния. Команда git diff также добавит -dirty со стороны рабочего дерева при создании вывода исправления или при использовании параметра --submodule:
$ git diff diff --git a/sub b/sub --- a/sub +++ b/sub @@ -1 +1 @@ -Subproject commit 3f356705649b5d566d97ff843cf193359229a453 +Subproject commit 3f356705649b5d566d97ff843cf193359229a453-dirty $ git diff --submodule Submodule sub 3f35670..3f35670-dirty:
Не следует также откатывать ветки в подмодуле за пределы коммитов, которые когда-либо фиксировались в любом суперпроекте.
Небезопасно выполнять git submodule update, если вы внесли и закоммитили изменения в подмодуле, не переключившись предварительно на ветку. Эти изменения будут незаметно перезаписаны:
$ cat a.txt module a $ echo line added from private2 >> a.txt $ git commit -a -m "line added inside private2" $ cd .. $ git submodule update Submodule path 'a': checked out 'd266b9873ad50488163457f025db7cdd9683d88b' $ cd a $ cat a.txt module a
| Примечание | Изменения по-прежнему видны в reflog подмодуля. |
Если в рабочем дереве подмодуля есть незакоммиченные изменения, команда git submodule update не перезапишет их. Вместо этого вы получите обычное предупреждение о том, что нельзя переключиться с изменённой ветки.
Низкоуровневые операции Git
Доступ к объектам и их обработка
Команда git-cat-file[1] может показать содержимое любого объекта, хотя команда более высокого уровня git-show[1] обычно полезнее.
Команда git-commit-tree[1] позволяет создавать коммиты с произвольными родительскими коммитами и деревьями.
Дерево можно создать с помощью git-write-tree[1], а получить доступ к его данным — с помощью git-ls-tree[1]. Два дерева можно сравнить с помощью git-diff-tree[1].
Тег создаётся с помощью git-mktag[1], а подпись можно проверить с помощью git-verify-tag[1], хотя обычно проще использовать git-tag[1] для обеих операций.
Рабочий процесс
Операции высокого уровня, такие как git-commit[1] и git-restore[1], перемещают данные между рабочим деревом, индексом и базой данных объектов. Git предоставляет низкоуровневые операции, каждая из которых выполняет один из этих шагов.
Как правило, все операции Git работают с файлом индекса. Некоторые операции работают исключительно с файлом индекса (отображая текущее состояние индекса), но большинство операций перемещают данные между файлом индекса и базой данных либо рабочим каталогом. Таким образом, есть четыре основные комбинации:
рабочий каталог → индекс
Команда git-update-index[1] обновляет индекс сведениями из рабочего каталога. Обычно вы обновляете сведения в индексе, просто указав имя файла, который нужно обновить, например:
$ git update-index filename
но, чтобы избежать распространённых ошибок с подстановочными шаблонами имён файлов и т. п., команда обычно не добавляет совершенно новые записи и не удаляет старые, то есть, как правило, она лишь обновляет существующие записи кэша.
Чтобы сообщить Git, что вы действительно понимаете, что некоторые файлы больше не существуют или что нужно добавить новые файлы, используйте флаги --remove и --add соответственно.
ПРИМЕЧАНИЕ! Флаг --remove not означает, что последующие имена файлов обязательно будут удалены: если файлы по-прежнему существуют в структуре каталогов, индекс будет обновлён с учётом их нового состояния, а не удалён. Флаг --remove лишь означает, что update-index будет считать удалённый файл допустимым случаем; если файла действительно больше нет, команда соответственно обновит индекс.
В особом случае можно также выполнить git update-index --refresh, чтобы обновить сведения «stat» для каждой записи индекса в соответствии с текущими сведениями stat. Эта команда not обновляет состояние самого объекта и обновляет только те поля, которые используются для быстрой проверки того, соответствует ли объект по-прежнему своему прежнему резервному объекту в хранилище.
Упомянутая ранее команда git-add[1] — это всего лишь оболочка для git-update-index[1].
индекс → база данных объектов
Записать текущий файл индекса в объект «дерево» можно с помощью программы
$ git write-tree
у которой нет параметров — она просто запишет текущий индекс в набор объектов-деревьев, описывающих это состояние, и вернёт имя полученного дерева верхнего уровня. В любой момент можно использовать это дерево, чтобы заново создать индекс, выполнив обратную операцию:
база данных объектов → индекс
Вы считываете файл «дерева» из базы данных объектов и используете его для заполнения (и перезаписи — не делайте этого, если индекс содержит несохранённое состояние, которое может понадобиться восстановить позднее!) текущего индекса. Обычно достаточно выполнить
$ git read-tree <SHA-1 of tree>
после чего файл индекса будет эквивалентен дереву, сохранённому ранее. Однако это только ваш файл index: содержимое рабочего каталога не изменено.
индекс → рабочий каталог
Вы обновляете рабочий каталог на основе индекса, «извлекая» файлы. Это не очень распространённая операция, поскольку обычно вы просто поддерживаете свои файлы в актуальном состоянии и вместо записи в рабочий каталог сообщаете файлу индекса об изменениях в рабочем каталоге (то есть git update-index).
Однако если вы решите перейти к новой версии, извлечь чью-либо другую версию или просто восстановить предыдущее дерево, вы заполните файл индекса с помощью read-tree, а затем извлечёте полученный результат командой
$ git checkout-index filename
или, если вы хотите извлечь весь индекс, используйте -a.
ПРИМЕЧАНИЕ! Обычно git checkout-index отказывается перезаписывать старые файлы, поэтому, если у вас уже извлечена старая версия дерева, понадобится использовать флаг -f (before флаг -a или имя файла), чтобы force извлечение.
Наконец, есть несколько небольших операций, которые не сводятся к простому перемещению между представлениями:
Как всё это связать
Чтобы зафиксировать дерево, созданное с помощью git write-tree, нужно создать объект «коммит», который ссылается на это дерево и историю за ним, в первую очередь на родительские коммиты, предшествовавшие ему в истории.
Обычно у «коммита» один родитель: предыдущее состояние дерева до внесения определённого изменения. Однако иногда у него может быть два или более родительских коммита; в этом случае мы называем его «слиянием», поскольку такой коммит объединяет («сливает») два или более предыдущих состояния, представленных другими коммитами.
Иными словами, «дерево» представляет определённое состояние каталога рабочего дерева, а «коммит» представляет это состояние во времени и объясняет, как мы к нему пришли.
Чтобы создать объект-коммит, укажите дерево, описывающее состояние на момент фиксации, и список родителей:
$ git commit-tree <tree> -p <parent> [(-p <parent2>)...]
а затем укажите причину коммита в stdin (перенаправив данные из канала или файла либо просто введя их в терминале).
git commit-tree вернёт имя объекта, представляющего этот коммит; его следует сохранить для дальнейшего использования. Обычно вы фиксируете новое состояние HEAD, и хотя Git безразлично, где вы сохраните заметку об этом состоянии, на практике мы просто записываем результат в файл, на который указывает .git/HEAD, чтобы всегда можно было увидеть последнее зафиксированное состояние.
На следующей схеме показано, как сочетаются различные элементы:
commit-tree
commit obj
+----+
| |
| |
V V
+-----------+
| Object DB |
| Backing |
| Store |
+-----------+
^
write-tree | |
tree obj | |
| | read-tree
| | tree obj
V
+-----------+
| Index |
| "cache" |
+-----------+
update-index ^
blob obj | |
| |
checkout-index -u | | checkout-index
stat | | blob obj
V
+-----------+
| Working |
| Directory |
+-----------+ Изучение данных
Данные, представленные в базе данных объектов и индексе, можно изучать с помощью различных вспомогательных инструментов. Для любого объекта можно использовать git-cat-file[1], чтобы изучить сведения об объекте:
$ git cat-file -t <objectname>
показывает тип объекта; узнав тип (обычно он очевиден из места, где вы нашли объект), можно использовать
$ git cat-file blob|tree|commit|tag <objectname>
для отображения его содержимого. ПРИМЕЧАНИЕ! Деревья содержат двоичные данные, поэтому для их отображения предусмотрен специальный вспомогательный инструмент git ls-tree, который преобразует двоичные данные в более удобный для чтения вид.
Особенно полезно изучать объекты «коммит», поскольку они обычно невелики и довольно понятны. В частности, если вы придерживаетесь соглашения хранить имя верхнего коммита в .git/HEAD, можно выполнить
$ git cat-file commit HEAD
чтобы узнать, каким был верхний коммит.
Слияние нескольких деревьев
Git помогает выполнять трёхстороннее слияние, которое, в свою очередь, можно использовать для многостороннего слияния, повторяя процедуру несколько раз. Обычно выполняется только одно трёхстороннее слияние (согласование двух линий истории) с последующей фиксацией результата, но при желании можно сразу слить несколько веток.
Чтобы выполнить трёхстороннее слияние, начните с двух коммитов, которые нужно объединить, найдите их ближайшего общего родителя (третий коммит) и сравните деревья, соответствующие этим трём коммитам.
Чтобы получить «основу» для слияния, найдите общего родителя двух коммитов:
$ git merge-base <commit1> <commit2>
Эта команда выводит имя коммита, на котором основаны оба коммита. Теперь нужно найти объекты-деревья этих коммитов; это легко сделать с помощью
$ git cat-file commit <commitname> | head -1
поскольку сведения об объекте-дереве всегда находятся в первой строке объекта-коммита.
Когда вы определите три дерева для слияния (одно «исходное» дерево, то есть общее дерево, и два дерева «результата», то есть ветки, которые нужно объединить), выполните чтение «слияния» в индекс. Если для этого придётся отбросить старое содержимое индекса, команда сообщит об этом, поэтому убедитесь, что вы зафиксировали его — на практике слияние обычно всегда выполняется относительно последнего коммита (который, следовательно, должен соответствовать текущему индексу).
Чтобы выполнить слияние, запустите
$ git read-tree -m -u <origtree> <yourtree> <targettree>
эта команда выполнит все тривиальные операции слияния непосредственно в файле индекса, после чего можно просто записать результат с помощью git write-tree.
Слияние нескольких деревьев (продолжение)
К сожалению, многие слияния не бывают тривиальными. Если файлы добавлялись, перемещались или удалялись либо обе ветки изменили один и тот же файл, в дереве индекса останутся «записи слияния». Такое дерево индекса NOT можно записать в объект-дерево; прежде чем записать результат, необходимо разрешить все подобные конфликты слияния с помощью других инструментов.
Изучить такое состояние индекса можно с помощью команды git ls-files --unmerged. Например:
$ git read-tree -m $orig HEAD $target $ git ls-files --unmerged 100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1 hello.c 100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2 hello.c 100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello.c
Каждая строка вывода git ls-files --unmerged начинается с битов режима blob, SHA-1 blob, stage number и имени файла. stage number — это способ Git указать, из какого дерева взята запись: этап 1 соответствует дереву $orig, этап 2 — дереву HEAD, а этап 3 — дереву $target.
Ранее мы говорили, что тривиальные слияния выполняются внутри git read-tree -m. Например, если файл не менялся между $orig и HEAD или $target либо если файл одинаково изменился с $orig на HEAD и с $orig на $target, очевидно, что в итоге получится содержимое HEAD. В приведённом выше примере показано, что файл hello.c изменился с $orig на HEAD и с $orig на $target по-разному. Разрешить конфликт можно, запустив предпочитаемую программу трёхстороннего слияния, например diff3, merge или собственную программу Git merge-file, для объектов blob на этих трёх этапах, например так:
$ git cat-file blob 263414f >hello.c~1 $ git cat-file blob 06fa6a2 >hello.c~2 $ git cat-file blob cc44c73 >hello.c~3 $ git merge-file hello.c~2 hello.c~1 hello.c~3
Результат слияния будет записан в файл hello.c~2; при наличии конфликтов в нём также будут маркеры конфликтов. Убедившись, что результат слияния корректен, сообщите Git итоговый результат слияния для этого файла:
$ mv -f hello.c~2 hello.c $ git update-index hello.c
Если путь находится в состоянии «не слит», выполнение git update-index для этого пути сообщает Git, что конфликт разрешён.
Выше описано слияние Git на самом низком уровне, чтобы помочь понять, что происходит «под капотом». На практике никто, даже сам Git, не запускает git cat-file три раза. Для извлечения этапов во временные файлы и вызова для них скрипта «слияния» существует программа git merge-index:
$ git merge-index git-merge-one-file hello.c
Именно с помощью неё реализована команда более высокого уровня git merge -s resolve.
Внутреннее устройство Git
В этой главе рассматриваются внутренние детали реализации Git, которые, вероятно, нужно знать только разработчикам Git.
Формат хранения объектов
Тип каждого объекта определяется статически и указывает его формат (то есть то, как он используется и как может ссылаться на другие объекты). В настоящее время существует четыре типа объектов: «blob», «tree», «commit» и «tag».
Независимо от типа, у всех объектов есть следующие общие свойства: все они сжаты с помощью zlib и имеют заголовок, в котором указан не только их тип, но и размер содержащихся в объекте данных. Стоит отметить, что хеш SHA-1, используемый для имени объекта, вычисляется по исходным данным вместе с этим заголовком. Поэтому sha1sum file не совпадает с именем объекта для file (в самых ранних версиях Git хеширование выполнялось несколько иначе, но вывод остаётся тем же).
Ниже приведён краткий пример ручного вычисления таких хешей:
Предположим, у нас есть небольшой текстовый файл с простым содержимым:
$ echo "Hello world" >hello.txt
Теперь можно вручную вычислить хеш, который Git использовал бы для этого файла:
-
Объект, для которого нужно вычислить хеш, имеет тип «blob» и размер 12 байт.
-
Добавьте заголовок объекта перед содержимым файла и передайте полученное значение команде
sha1sum:
$ { printf "blob 12\0"; cat hello.txt; } | sha1sum
802992c4220de19a90767f3000a79a31b98d0df7 - Этот вычисленный вручную хеш можно проверить с помощью git hash-object, которая, разумеется, скрывает добавление заголовка:
$ git hash-object hello.txt 802992c4220de19a90767f3000a79a31b98d0df7
Таким образом, общую целостность объекта всегда можно проверить независимо от его содержимого и типа: все объекты можно проверить, убедившись, что (a) их хеши соответствуют содержимому файла и (b) объект успешно распаковывается в поток байтов, образующий последовательность <ascii-type-without-space> + <space> + <ascii-decimal-size> + <byte\0> + <binary-object-data>.
Кроме того, можно проверить структуру структурированных объектов и их связи с другими объектами. Обычно это делается с помощью программы git fsck, которая строит полный граф зависимостей всех объектов и проверяет их внутреннюю целостность (помимо поверхностной проверки целостности с помощью хеша).
Краткий обзор исходного кода Git
Новым разработчикам не всегда легко разобраться в исходном коде Git. В этом разделе приведены небольшие подсказки, которые помогут понять, с чего начать.
Хорошее начало — изучить содержимое первоначального коммита:
$ git switch --detach e83c5163
В первоначальной версии заложена основа почти всего, что есть в Git сегодня (хотя в некоторых местах детали могут отличаться), но она достаточно мала, чтобы прочитать её за один присест.
Обратите внимание, что со времени этой версии терминология изменилась. Например, в README этой версии используется слово «changeset» для описания того, что теперь мы называем коммитом.
Также мы больше не называем это «кэшем», а говорим «индекс»; однако файл по-прежнему называется read-cache.h.
Если вы усвоили идеи, изложенные в первоначальном коммите, изучите более новую версию и бегло прочитайте read-cache-ll.h, object.h и commit.h.
В первые годы Git (в традициях UNIX) представлял собой набор предельно простых программ, которые использовались в скриптах: вывод одной программы передавался другой. Для начальной разработки это оказалось удобно, поскольку упрощало тестирование новых возможностей. Однако в последнее время многие из этих частей стали встроенными командами, а часть основного кода была «перенесена в библиотеку», то есть в libgit.a, ради повышения производительности и переносимости, а также во избежание дублирования кода.
К этому моменту вы уже знаете, что такое индекс (и найдёте соответствующие структуры данных в read-cache-ll.h), и что есть лишь несколько типов объектов (блобы, деревья, коммиты и теги), которые наследуют общую структуру от struct object — своего первого члена (поэтому, например, можно привести (struct object *)commit, чтобы получить same, как в &commit->object, то есть доступ к имени объекта и флагам).
Сейчас самое время сделать перерыв и обдумать эту информацию.
Следующий шаг — разобраться с именованием объектов. Прочитайте раздел Именование коммитов. Существует довольно много способов задать имя объекта (и не только ревизии!). Все они обрабатываются в sha1_name.c. Просто бегло просмотрите функцию get_sha1(). Значительная часть специальной обработки выполняется такими функциями, как get_sha1_basic(), и им подобными.
Это поможет вам подготовиться к изучению наиболее глубоко перенесённой в библиотеку части Git — обходчика ревизий.
Изначально git log был shell-скриптом:
$ git-rev-list --pretty $(git-rev-parse --default HEAD "$@") | \
LESS=-S ${PAGER:-less} Что это значит?
git rev-list — исходная версия обходчика ревизий, которая always выводила список ревизий в stdout. Она по-прежнему работает, и так и должно быть, поскольку большинство новых команд Git сначала создаются как скрипты, использующие git rev-list.
git rev-parse больше не так важна; она использовалась только для фильтрации параметров, относящихся к разным низкоуровневым командам, которые вызывались скриптом.
Большая часть работы, выполнявшейся git rev-list, теперь находится в revision.c и revision.h. Она оборачивает параметры в структуру с именем rev_info, которая определяет, как и какие ревизии обходить, а также другие параметры.
Исходную задачу git rev-parse теперь выполняет функция setup_revisions(), которая разбирает ревизии и общие параметры командной строки для обходчика ревизий. Эти сведения сохраняются в структуре rev_info для последующего использования. После вызова setup_revisions() можно самостоятельно разбирать параметры командной строки. Затем нужно вызвать prepare_revision_walk() для инициализации, после чего получать коммиты по одному с помощью функции get_revision().
Если вас интересуют подробности процесса обхода ревизий, изучите первую реализацию cmd_log(): вызовите git show v1.3.0~155^2~4 и прокрутите до этой функции (обратите внимание, что теперь больше не нужно вызывать setup_pager() напрямую).
В наши дни git log — встроенная команда, то есть она contained в команде git. Исходный код встроенной команды включает:
-
функцию с именем
cmd_<bla>, обычно определённую в builtin/<bla.c> (обратите внимание, что в старых версиях Git она находилась вbuiltin-<bla>.c), и объявленную вbuiltin.h; -
запись в массиве
commands[] вgit.c; и -
запись в
BUILTIN_OBJECTSвMakefile.
Иногда в одном исходном файле содержится несколько встроенных команд. Например, cmd_show() и cmd_log() находятся в builtin/log.c, поскольку используют значительную часть общего кода. В этом случае команды, которые not называются так же, как файл .c, в котором они находятся, должны быть перечислены в BUILT_INS в Makefile.
git log на C выглядит сложнее, чем исходный скрипт, но зато обеспечивает гораздо большую гибкость и производительность.
И здесь самое время сделать перерыв.
Третий урок: изучайте код. В самом деле, это лучший способ узнать об устройстве Git (после того как вы усвоите основные понятия).
Подумайте о том, что вам интересно, например: «как получить доступ к блобу, зная только его имя объекта?». Сначала найдите команду Git, с помощью которой это можно сделать. В данном примере это может быть git show или git cat-file.
Для ясности остановимся на git cat-file, поскольку она
-
относится к низкоуровневым командам; и
-
существовала уже в первоначальном коммите (она прошла всего около 20 ревизий под именем
cat-file.c, была переименована вbuiltin/cat-file.c, когда стала встроенной командой, и после этого появилась менее чем в 10 версиях).
Итак, откройте builtin/cat-file.c, найдите cmd_cat_file() и посмотрите, что она делает.
repo_config(the_repository, git_default_config);
if (argc != 3)
usage("git cat-file [-t|-s|-e|-p|<type>] <sha1>");
if (get_sha1(argv[2], sha1))
die("Not a valid object name %s", argv[2]); Пропустим очевидные детали; единственная по-настоящему интересная часть здесь — вызов get_sha1(). Функция пытается интерпретировать argv[2] как имя объекта и, если оно указывает на объект, присутствующий в текущем репозитории, записывает полученный SHA-1 в переменную sha1.
Здесь интересны два момента:
-
get_sha1() возвращает 0 в случаеsuccess. Это может удивить начинающих разработчиков Git, но в UNIX существует давняя традиция возвращать разные отрицательные числа при разных ошибках и 0 в случае успеха. -
переменная
sha1в сигнатуре функцииget_sha1() имеет типunsignedchar*, но на самом деле ожидается, что это указатель наunsignedchar[20]. Эта переменная содержит 160-битный SHA-1 указанного коммита. Обратите внимание, что когда SHA-1 передаётся какunsignedchar*, передаётся его двоичное представление, а не ASCII-представление в виде шестнадцатеричных символов, передаваемое какchar*.
Вы встретите оба этих случая в коде.
А теперь — самое интересное:
case 0:
buf = odb_read_object_peeled(r->objects, sha1, argv[1], &size, NULL); Так читается блоб (на самом деле не только блоб, но и объект любого типа). Чтобы узнать, как именно работает функция odb_read_object_peeled(), найдите её исходный код (например, выполнив в репозитории Git команду вроде git grep read_object_with | grep ":[a-z]") и прочитайте его.
Чтобы узнать, как использовать результат, продолжайте читать cmd_cat_file():
write_or_die(1, buf, size);
Иногда вы не знаете, где искать нужную возможность. Во многих таких случаях помогает поиск в выводе команды git log, а затем git show соответствующего коммита.
Пример: вы знаете, что для git bundle был написан тестовый пример, но не помните, где он находился (да, можно could git grep bundle t/, но это не иллюстрирует суть!):
$ git log --no-merges t/
В программе просмотра (less) просто найдите слово «bundle», прокрутите на несколько строк вверх и увидите, что оно находится в коммите 18449ab0. Теперь скопируйте это имя объекта и вставьте его в командную строку:
$ git show 18449ab0
Вот и всё.
Ещё один пример: выясним, что нужно сделать, чтобы превратить скрипт во встроенную команду:
$ git log --no-merges --diff-filter=A builtin/*.c
Как видите, Git — лучший инструмент для изучения исходного кода самого Git!
Глоссарий Git
Объяснение Git
- альтернативная база объектов
-
С помощью механизма альтернатив репозиторий может наследовать часть своей базы объектов от другой базы объектов, которая называется «альтернативной».
- репозиторий без рабочей копии
-
Репозиторий без рабочей копии — это обычно каталог каталог с подходящим именем и суффиксом
.git, в котором нет локальной извлечённой копии файлов, находящихся под контролем версий. Иными словами, все административные и управляющие файлы Git, которые обычно находятся в скрытом подкаталоге.git, расположены непосредственно в каталогеrepository.git, а других файлов там нет и они не извлечены. Обычно издатели общедоступных репозиториев предоставляют репозитории без рабочей копии. - объект blob
-
Объект объект без типа, например содержимое файла.
- ветка
-
«Ветка» — это линия разработки. Самый последний коммит в ветке называется вершиной этой ветки. Вершина ветки указывается ссылкой head, которая перемещается вперёд по мере дальнейшей разработки в ветке. Один репозиторий Git может отслеживать произвольное количество веток, но ваше рабочее дерево связано только с одной из них (текущей, или извлечённой, веткой), и HEAD указывает на эту ветку.
- кэш
-
Устаревшее название: индекс.
- цепочка
-
Список объектов, в котором каждый объект содержит ссылку на следующий (например, следующим объектом после коммита может быть один из его родителей).
- набор изменений
-
Терминология BitKeeper/cvsps для обозначения «коммита». Поскольку Git хранит не изменения, а состояния, термин «наборы изменений» для Git в действительности не имеет смысла.
- извлечение
-
Действие по обновлению всего рабочего дерева или его части с использованием объекта дерева или объекта blob из базы объектов, а также обновлению индекса и HEAD, если всё рабочее дерево было переключено на новую ветку.
- выбор отдельных коммитов
-
В терминологии SCM «выбрать отдельный коммит» означает выбрать подмножество изменений из ряда изменений (обычно коммитов) и записать их как новый ряд изменений поверх другой кодовой базы. В Git это выполняет команда «git cherry-pick»: она извлекает изменение, внесённое существующим коммитом, и записывает его на основе вершины текущей ветки в виде нового коммита.
- чистое
-
Рабочее дерево считается чистым, если оно соответствует ревизии, на которую указывает текущая ссылка head. См. также «грязное».
- коммит
-
Как существительное: отдельная точка в истории Git; вся история проекта представлена набором взаимосвязанных коммитов. Git часто использует слово «коммит» в тех же случаях, когда другие системы контроля версий используют слова «ревизия» или «версия». Также употребляется как сокращённое название объекта коммита.
Как глагол: действие по сохранению нового снимка состояния проекта в истории Git путём создания коммита, представляющего текущее состояние индекса, и перемещения HEAD так, чтобы он указывал на новый коммит.
- понятие, представление и использование графа коммитов
-
Синоним структуры DAG, образованной коммитами в базе объектов, на которые указывают вершины веток, используя их цепочки связанных коммитов. Эта структура и есть граф коммитов. Граф может представляться и другими способами, например в виде файла «commit-graph».
- файл commit-graph
-
Файл «commit-graph» (обычно пишется через дефис) — это дополнительное представление графа коммитов, ускоряющее обходы графа. Файл «commit-graph» хранится либо в каталоге .git/objects/info, либо в каталоге info альтернативной базы объектов.
- объект коммита
-
Объект, содержащий информацию об определённой ревизии, например сведения о родителях, коммитере, авторе и дате, а также объект дерева, соответствующий верхнему каталогу сохранённой ревизии.
- commit-ish (также committish)
-
Объект коммита или объект, который можно рекурсивно разыменовать до объекта коммита. К commit-ish относятся: объект коммита; объект тега, указывающий на объект коммита; объект тега, указывающий на объект тега, который указывает на объект коммита, и так далее.
- основа Git
-
Основные структуры данных и утилиты Git. Предоставляет лишь ограниченный набор средств управления исходным кодом.
- DAG
-
Ориентированный ациклический граф. Объекты коммитов образуют ориентированный ациклический граф, поскольку у них есть родители (ориентированность), а граф объектов коммитов ацикличен (не существует цепочки, которая начинается и заканчивается одним и тем же объектом).
- висячий объект
-
Недостижимый объект, который нельзя достичь даже из других недостижимых объектов; на висячий объект не ссылается ни одна ссылка или объект в репозитории.
- разыменование
-
Применительно к символической ссылке: действие по обращению к ссылке, на которую указывает символическая ссылка. При рекурсивном разыменовании описанный процесс повторяется для полученной ссылки, пока не будет найдена нессылка-символ.
Применительно к объекту тега: действие по обращению к объекту, на который указывает тег. Теги разыменовываются рекурсивно: операция повторяется для результирующего объекта, пока его тип не станет заданным типом объекта (если он задан) или любым типом, отличным от «tag». Синоним рекурсивного разыменования в контексте тегов — «снятие оболочки».
Применительно к объекту коммита: действие по обращению к объекту дерева коммита. Коммиты нельзя разыменовывать рекурсивно.
Если не указано иное, «разыменование» в контексте команд или протоколов Git подразумевает рекурсивное разыменование.
- отсоединённый HEAD
-
Обычно в HEAD хранится имя ветки, и команды, работающие с представленной HEAD историей, оперируют историей, ведущей к вершине ветки, на которую указывает HEAD. Однако Git также позволяет извлечь произвольный коммит, который не обязательно является вершиной какой-либо ветки. В таком состоянии HEAD называется «отсоединённым».
Обратите внимание: команды, работающие с историей текущей ветки (например,
gitcommitдля создания новой истории поверх неё), продолжают работать и при отсоединённом HEAD. Они перемещают HEAD так, чтобы он указывал на вершину обновлённой истории, не затрагивая ни одну ветку. Команды, которые обновляют сведения о текущей ветке или запрашивают ихabout(например,gitbranch--set-upstream-to, задающая, с какой веткой удалённого отслеживания интегрируется текущая ветка), очевидно, не работают, поскольку в этом состоянии нет (настоящей) текущей ветки, сведения о которой можно запросить. - каталог
-
Список, который выводит команда «ls» :-)
- грязное
-
Рабочее дерево считается «грязным», если содержит изменения, которые ещё не были зафиксированы в текущей ветке.
- неправильное слияние
-
Неправильное слияние — это слияние, которое вносит изменения, отсутствующие во всех родителях.
- перемотка вперёд
-
Перемотка вперёд — особый тип слияния: у вас есть ревизия, и вы «сливаете» изменения другой ветки, которая оказывается её потомком. В этом случае новый коммит слияния не создаётся, а ваша ветка просто обновляется и указывает на ту же ревизию, что и сливаемая ветка. Такое часто происходит с веткой удалённого отслеживания удалённого репозитория.
- получение изменений
-
Получить ветку означает получить её ссылку head из удалённого репозитория, определить, каких объектов не хватает в локальной базе объектов, и получить их. См. также git-fetch[1].
- файловая система
-
Первоначально Линус Торвальдс разработал Git как файловую систему в пространстве пользователя, то есть инфраструктуру для хранения файлов и каталогов. Это обеспечило эффективность и скорость Git.
- архив Git
-
Синоним слова репозиторий (в терминологии архитекторов).
- файл gitfile
-
Обычный файл
.gitв корне рабочего дерева, указывающий на каталог, в котором находится настоящий репозиторий. Инструкции по использованию см. в git-worktree[1] или git-submodule[1]. Синтаксис описан в gitrepository-layout[5]. - подсадки
-
Подсадки позволяют объединить две в остальном различные линии разработки, записав фиктивные сведения о происхождении коммитов. Таким образом, можно заставить Git считать, что у коммита другой набор родителей, нежели тот, который был записан при создании коммита. Настройка выполняется через файл
.git/info/grafts.Обратите внимание, что механизм подсадок устарел и может вызывать проблемы при передаче объектов между репозиториями; более гибкая и надёжная система для той же задачи описана в git-replace[1].
- хеш
-
В контексте Git — синоним имени объекта.
- head
-
Именованная ссылка на коммит в вершине ветки. Ссылки head хранятся в файлах каталога
$GIT_DIR/refs/heads/, если не используются упакованные ссылки. (См. git-pack-refs[1].) - HEAD
-
Текущая ветка. Подробнее: ваше рабочее дерево обычно формируется на основе состояния дерева, на которое указывает HEAD. HEAD — это ссылка на одну из ссылок head в вашем репозитории, кроме случая с отсоединённым HEAD, когда он непосредственно указывает на произвольный коммит.
- ссылка head
-
Синоним head.
- перехватчик
-
Во время обычного выполнения нескольких команд Git вызываются необязательные скрипты, позволяющие разработчику добавить функциональность или проверки. Как правило, перехватчики позволяют предварительно проверить команду и при необходимости прервать её, а после завершения операции отправить уведомление. Скрипты перехватчиков находятся в каталоге
$GIT_DIR/hooks/; чтобы включить их, достаточно удалить из имени файла суффикс.sample. В ранних версиях Git их требовалось сделать исполняемыми. - индекс
-
Набор файлов со статистической информацией, содержимое которых хранится в виде объектов. Индекс — это сохранённая версия вашего рабочего дерева. На самом деле он может содержать также вторую и даже третью версию рабочего дерева, которые используются при слиянии.
- запись индекса
-
Информация об определённом файле, хранящаяся в индексе. Запись индекса может быть неслитой, если слияние начато, но ещё не завершено (то есть если индекс содержит несколько версий этого файла).
- master
-
Ветка разработки по умолчанию. При создании репозитория Git создаётся ветка с именем «master», которая становится активной. В большинстве случаев в ней ведётся локальная разработка, однако это лишь принятое соглашение, а не обязательное требование.
- слияние
-
Как глагол: включить содержимое другой ветки (возможно, из внешнего репозитория) в текущую ветку. Если сливаемая ветка находится в другом репозитории, сначала выполняется получение удалённой ветки, а затем результат сливается с текущей веткой. Сочетание операций получения и слияния называется извлечением изменений. Слияние выполняется автоматически: определяются изменения, внесённые с момента расхождения веток, а затем применяются все эти изменения. Если изменения конфликтуют, для завершения слияния может потребоваться вмешательство пользователя.
Как существительное: если слияние не является перемоткой вперёд, успешное слияние приводит к созданию нового коммита, представляющего результат слияния, родителями которого являются вершины слитых веток. Такой коммит называется «коммитом слияния» или просто «слиянием».
- объект
-
Единица хранения в Git. Однозначно определяется по SHA-1 своего содержимого. Поэтому объект нельзя изменить.
- база объектов
-
Хранит набор «объектов», каждый отдельный объект определяется своим именем объекта. Обычно объекты находятся в каталоге
$GIT_DIR/objects/. - идентификатор объекта, ID объекта, oid
-
Синонимы имени объекта.
- имя объекта
-
Уникальный идентификатор объекта. Имя объекта обычно представляется шестнадцатеричной строкой из 40 символов. В разговорной речи также называется SHA-1.
- тип объекта
-
Один из идентификаторов «commit», «tree», «tag» или «blob», обозначающий тип объекта.
- осьминог
- сиротская ветка
-
Действие по переходу на ещё не существующую ветку (то есть на нерождённую ветку). После такой операции первый созданный коммит становится коммитом без родителя и начинает новую историю.
- origin
-
Репозиторий репозиторий верхнего уровня по умолчанию. У большинства проектов есть хотя бы один вышестоящий проект, за которым они следят. По умолчанию для этой цели используется
origin. Новые обновления из вышестоящего проекта будут получены в ветки удалённого отслеживания с именами origin/name-of-upstream-branch; их можно просмотреть с помощьюgitbranch-r. - наложение
-
Обновлять и добавлять файлы в рабочий каталог, но не удалять их — подобно тому, как
cp -Rобновляет содержимое каталога назначения. Это режим по умолчанию при извлечении файлов из индекса или объекта tree-ish. В отличие от него, режим без наложения также удаляет отслеживаемые файлы, отсутствующие в источнике, подобноrsync --delete. - пакет
-
Набор объектов, сжатых в один файл (для экономии места или эффективной передачи).
- индекс пакета
-
Список идентификаторов и другой информации об объектах в пакете, помогающий эффективно обращаться к его содержимому.
- спецификация пути
-
Шаблон, используемый для ограничения путей в командах Git.
Спецификации путей используются в командной строке команд «git ls-files», «git ls-tree», «git add», «git grep», «git diff», «git checkout» и многих других, чтобы ограничить область операций некоторым подмножеством дерева или рабочего дерева. Узнайте из документации каждой команды, задаются ли пути относительно текущего каталога или корневого каталога. Синтаксис спецификаций путей таков:
-
любой путь совпадает сам с собой
-
часть спецификации пути до последней косой черты задаёт префикс каталога. Область действия этой спецификации ограничивается данным поддеревом.
-
оставшаяся часть спецификации пути задаёт шаблон для остатка имени пути. Пути относительно префикса каталога сопоставляются с этим шаблоном с помощью fnmatch(3); в частности,
*и?canсовпадают с разделителями каталогов.
Например, Documentation/*.jpg соответствует всем файлам .jpg в поддереве Documentation, включая Documentation/chapter_1/figure_1.jpg.
Спецификация пути, начинающаяся с двоеточия
:, имеет особое значение. В краткой форме за начальным двоеточием:следуют ноль или более букв «магической сигнатуры» (которые могут завершаться ещё одним двоеточием:), а оставшаяся часть задаёт шаблон для сопоставления с путём. «Магическая сигнатура» состоит из символов ASCII, которые не являются буквенно-цифровыми символами, символами glob, специальными символами регулярных выражений или двоеточием. Завершающее «магическую сигнатуру» необязательное двоеточие можно опустить, если шаблон начинается с символа, который не входит в набор символов «магической сигнатуры» и не является двоеточием.В полной форме за начальным двоеточием
:следует открывающая скобка (, список из нуля или более «магических слов», разделённых запятыми, закрывающая скобка ), а оставшаяся часть задаёт шаблон для сопоставления с путём.Спецификация пути, состоящая только из двоеточия, означает «спецификация пути отсутствует». Эту форму не следует сочетать с другими спецификациями путей.
- корень
-
Магическое слово
top(магическая сигнатура:/) заставляет сопоставлять шаблон начиная с корня рабочего дерева, даже если команда выполняется из подкаталога. - буквальный
-
Подстановочные символы в шаблоне, такие как
*или ?, рассматриваются как обычные символы. - без учёта регистра
-
Сопоставление без учёта регистра.
- glob
-
Git обрабатывает шаблон как оболочечный шаблон glob, подходящий для обработки функцией fnmatch(3) с флагом FNM_PATHNAME: подстановочные символы в шаблоне не соответствуют символу / в имени пути. Например, «Documentation/*.html» соответствует «Documentation/git.html», но не «Documentation/ppc/ppc.html» и не «tools/perf/Documentation/perf.html».
Две следующие подряд звёздочки («
**») в шаблонах, сопоставляемых с полным именем пути, могут иметь особое значение:-
Начальная последовательность «
**» с последующей косой чертой означает совпадение во всех каталогах. Например, «**/foo» соответствует файлу или каталогу «foo» в любом месте. «**/foo/bar» соответствует файлу или каталогу «bar» в любом месте, если он непосредственно находится в каталоге «foo». -
Конечная последовательность «
/**» соответствует всему содержимому. Например, «abc/**» соответствует всем файлам внутри каталога «abc» относительно расположения файла.gitignoreна любой глубине. -
Косая черта, за которой следуют две звёздочки подряд и ещё одна косая черта, соответствует нулю или более каталогов. Например, «
a/**/b» соответствует «a/b», «a/x/b», «a/x/y/b» и так далее. -
Другие последовательности из нескольких звёздочек считаются недопустимыми.
Магический режим glob несовместим с буквальным магическим режимом.
-
- атрибут
-
После
attr:следует список «требований к атрибутам», разделённых пробелами; все они должны выполняться, чтобы путь считался совпадающим. Это дополняет обычное сопоставление с шаблоном спецификации пути без магических символов. См. gitattributes[5].Каждое требование к атрибутам пути имеет одну из следующих форм:
-
«
ATTR» требует, чтобы атрибутATTRбыл установлен. -
«
-ATTR» требует, чтобы атрибутATTRбыл сброшен. -
«
ATTR=VALUE» требует, чтобы атрибутATTRимел значениеVALUE. -
«
!ATTR» требует, чтобы значение атрибутаATTRне было задано.
Обратите внимание: при сопоставлении с объектом дерева атрибуты всё равно берутся из рабочего дерева, а не из указанного объекта дерева.
-
- исключение
-
После того как путь совпал с любой спецификацией пути, не являющейся спецификацией исключения, он проверяется по всем спецификациям исключения (магическая сигнатура:
!или её синоним^). Если путь совпадает, он игнорируется. Если спецификаций путей, не являющихся спецификациями исключения, нет, исключение применяется к результирующему набору так, как если бы команда была вызвана без спецификаций путей.
-
- родитель
-
Объект коммита содержит (возможно, пустой) список логических предшественников в линии разработки, то есть своих родителей.
- снятие оболочки
-
Действие по рекурсивному разыменованию объекта тега.
- pickaxe
-
Термин pickaxe обозначает параметр процедур diffcore, помогающий выбирать изменения, добавляющие или удаляющие заданную текстовую строку. С помощью параметра
--pickaxe-allего можно использовать для просмотра полного набора изменений, в котором была добавлена или удалена, например, определённая строка текста. См. git-diff[1]. - низкоуровневые команды
-
Шуточное название основы Git.
- высокоуровневые команды
-
Шуточное название программ и наборов программ, использующих основу Git и предоставляющих доступ к ней на высоком уровне. Такие команды предоставляют больше возможностей интерфейса SCM, чем низкоуровневые команды.
- ссылка отдельного рабочего дерева
-
Ссылки, относящиеся к отдельному рабочему дереву, а не являющиеся глобальными. В настоящее время это только HEAD и любые ссылки, начинающиеся с
refs/bisect/, однако позднее к ним могут добавиться другие необычные ссылки. - псевдоссылка
-
Ссылка (ref), семантика которой отличается от семантики обычных ссылок. Эти ссылки можно читать с помощью обычных команд Git, но команды вроде git-update-ref[1] не могут изменять их.
Git известны следующие псевдоссылки:
-
FETCH_HEADзаписывается командами git-fetch[1] или git-pull[1]. Она может ссылаться на несколько идентификаторов объектов. К каждому идентификатору объекта добавлены метаданные, указывающие, откуда он был получен и каков статус его получения. -
MERGE_HEADзаписывается командой git-merge[1] при разрешении конфликтов слияния. Она содержит все идентификаторы коммитов, которые объединяются.
-
- извлечение (pull)
-
Извлечь ветку означает получить её и объединить. См. также git-pull[1].
- отправка (push)
-
Отправить ветку означает получить её ссылку HEAD из удалённого репозитория, выяснить, является ли она предком локальной ссылки HEAD этой ветки, и, если это так, поместить в удалённую базу данных объектов все объекты, которые достижимы из локальной ссылки HEAD и отсутствуют в удалённом репозитории, а затем обновить удалённую ссылку HEAD. Если удалённый HEAD не является предком локального HEAD, отправка завершается ошибкой.
- достижимый (reachable)
-
Все предки заданного коммита называются «достижимыми» из этого коммита. В более общем смысле один объект достижим из другого, если мы можем добраться от одного до другого по цепочке, переходя от тегов к объектам, на которые они указывают, от коммитов — к их родителям или деревьям, а от деревьев — к деревьям или блобам, которые они содержат.
- битовые карты достижимости
-
Битовые карты достижимости хранят сведения о достижимости выбранного набора коммитов в файле пакета или многопакетном индексе (MIDX), чтобы ускорить поиск объектов. Битовые карты хранятся в файле «.bitmap». В репозитории может использоваться не более одного файла битовых карт. Этот файл может относиться как к одному пакету, так и к многопакетному индексу репозитория (если он существует).
- перебазирование (rebase)
-
Повторно применить серию изменений из ветки к другой основе и переместить HEAD этой ветки на полученный результат.
- ссылка (ref)
-
Имя, указывающее на имя объекта или другую ссылку (последняя называется символической ссылкой). Для удобства ссылку иногда можно указывать в сокращённом виде в качестве аргумента команды Git; подробности см. в gitrevisions[7]. Ссылки хранятся в репозитории.
Пространство имён ссылок имеет иерархическую структуру. Имена ссылок должны начинаться с
refs/или находиться в корне иерархии. Для ссылок в корне действуют следующие правила:-
Имя состоит только из заглавных букв или символов подчёркивания.
-
Имя заканчивается на «
_HEAD» или равно «HEAD».
В корне иерархии есть несколько нестандартных ссылок, не соответствующих этим правилам. Приведённый ниже список исчерпывающий и в будущем не должен расширяться:
-
AUTO_MERGE -
BISECT_EXPECTED_REV -
NOTES_MERGE_PARTIAL -
NOTES_MERGE_REF -
MERGE_AUTOSTASH
Разные подиерархии используются для разных целей. Например, иерархия
refs/heads/используется для представления локальных веток, а иерархияrefs/tags/— для представления локальных тегов. -
- журнал ссылок (reflog)
-
Журнал ссылок показывает локальную «историю» ссылки. Иными словами, он может сообщить, какой была третья с конца ревизия в репозитории
thisи каково было текущее состояние репозиторияthisвчера в 21:14. Подробности см. в git-reflog[1]. - спецификация ссылки (refspec)
-
«Спецификация ссылки» используется командами получения и отправки для описания соответствия между удалённой ссылкой и локальной ссылкой. Подробности см. в git-fetch[1] или git-push[1].
- удалённый репозиторий
-
Репозиторий, используемый для отслеживания того же проекта, но расположенный в другом месте. Для взаимодействия с удалёнными репозиториями см. разделы о получении и отправке.
- удалённо-отслеживаемая ветка
-
Ссылка, используемая для отслеживания изменений в другом репозитории. Обычно она выглядит как
refs/remotes/foo/bar(что означает, что она отслеживает ветку с именемbarв удалённом репозитории с именемfoo) и соответствует правой части настроенной спецификации ссылки для получения. В удалённо-отслеживаемую ветку не следует вносить изменения напрямую или добавлять в неё локальные коммиты. - репозиторий
-
Набор ссылок вместе с базой данных объектов, содержащей все объекты, достижимые из этих ссылок, возможно, дополненный метаданными одной или нескольких пользовательских команд. Репозиторий может совместно использовать базу данных объектов с другими репозиториями с помощью механизма альтернативных баз данных объектов.
- разрешение (resolve)
-
Действие по ручному исправлению того, что осталось после неудачного автоматического слияния.
- ревизия
-
Синоним слова коммит (в значении существительного).
- откат (rewind)
-
Отбросить часть разработки, то есть переместить HEAD на более раннюю ревизию.
- SCM
-
Управление исходным кодом (инструмент).
- SHA-1
-
«Secure Hash Algorithm 1» — криптографическая хеш-функция. В контексте Git используется как синоним имени объекта.
- поверхностное клонирование
-
В основном синоним поверхностного репозитория, однако эта формулировка яснее указывает на то, что он был создан командой
gitclone--depth=.... - поверхностный репозиторий
-
Поверхностный репозиторий имеет неполную историю: у некоторых его коммитов удалены родители (другими словами, Git предписано считать, что у этих коммитов нет родителей, хотя они записаны в объекте коммита). Это иногда полезно, если вас интересует только недавняя история проекта, хотя настоящая история в основном репозитории гораздо обширнее. Поверхностный репозиторий создаётся с помощью параметра
--depthкоманды git-clone[1], а впоследствии его историю можно углубить с помощью git-fetch[1]. - запись stash
-
Объект, используемый для временного сохранения содержимого изменённого рабочего каталога и индекса с целью последующего повторного использования.
- подмодуль
-
Репозиторий, в котором хранится история отдельного проекта внутри другого репозитория (последний называется суперпроектом).
- суперпроект
-
Репозиторий, который ссылается на репозитории других проектов в своём рабочем дереве как на подмодули. Суперпроект знает имена объектов коммитов содержащихся в нём подмодулей, но не хранит их копии.
- символическая ссылка (symref)
-
Символическая ссылка: вместо самого идентификатора SHA-1 содержит значение формата
ref: refs/some/thingи при обращении к ней рекурсивно разыменовывается в эту ссылку.HEAD— яркий пример символической ссылки. Символическими ссылками управляют с помощью команды git-symbolic-ref[1]. - тег
-
Ссылка в пространстве имён
refs/tags/, указывающая на объект произвольного типа (обычно тег указывает на другой объект-тег или объект-коммит). В отличие от HEAD, тег не обновляется командойcommit. Тег Git не имеет отношения к тегу Lisp (который в контексте Git назывался бы типом объекта). Чаще всего тег используется для отметки определённой точки в цепочке предков коммита. - объект-тег
-
Объект, содержащий ссылку на другой объект и, подобно объекту-коммиту, способный содержать сообщение. Он также может содержать подпись (PGP); в этом случае его называют «подписанным объектом-тегом».
- тематическая ветка
-
Обычная ветка Git, которую разработчик использует для обозначения концептуального направления разработки. Поскольку ветки очень легко создавать и они не требуют больших затрат, часто удобно иметь несколько небольших веток, каждая из которых содержит чётко определённую концепцию или небольшие связанные между собой постепенные изменения.
- трейлер
-
Метаданные в формате «ключ — значение». Трейлеры могут находиться в конце сообщения коммита. В других сообществах их могут называть «нижними колонтитулами» или «тегами». См. git-interpret-trailers[1].
- дерево
-
Либо рабочее дерево, либо объект-дерево вместе с зависимыми от него объектами блобов и деревьев (то есть сохранённое представление рабочего дерева).
- объект-дерево
-
Объект, содержащий список имён файлов и режимов доступа, а также ссылки на соответствующие объекты-блобы и/или объекты-деревья. Дерево эквивалентно каталогу.
- объект-дерево (tree-ish, также treeish)
-
Объект-дерево или объект, который можно рекурсивно разыменовать до объекта-дерева. Разыменование объекта-коммита даёт объект-дерево, соответствующий корневому каталогу ревизии. К объектам tree-ish относятся: объект-коммит (commit-ish), объект-дерево, объект-тег, указывающий на объект-дерево, объект-тег, указывающий на объект-тег, который указывает на объект-дерево, и так далее.
- ещё не созданный (unborn)
-
HEAD может указывать на ветку, которая ещё не существует и пока не содержит коммитов; такая ветка называется ещё не созданной. Чаще всего пользователи сталкиваются с такой веткой, впервые создавая репозиторий, а не клонируя его из другого места. HEAD будет указывать на ветку
main(илиmaster— в зависимости от конфигурации), которая ещё не создана. Некоторые операции также могут перевести вас на ещё не созданную ветку с помощью параметра orphan. - индекс с неслитыми изменениями
-
Индекс, содержащий записи индекса с неслитыми изменениями.
- недостижимый объект
-
Объект, который не является достижимым из ветки, тега или любой другой ссылки.
- вышестоящая ветка
-
Ветка по умолчанию, которая сливается с рассматриваемой веткой (или на которую перебазируется рассматриваемая ветка). Она настраивается с помощью параметров branch.<name>.remote и branch.<name>.merge. Если вышестоящей веткой для
Aявляетсяorigin/B, иногда говорят: «Aотслеживаетorigin/B». - рабочее дерево
-
Дерево файлов, фактически извлечённых в рабочий каталог. Обычно рабочее дерево содержит файлы из дерева коммита HEAD, а также все локальные изменения, которые вы внесли, но ещё не зафиксировали.
- рабочее дерево (worktree)
-
У репозитория может не быть рабочих деревьев (то есть это голый репозиторий) или может быть одно либо несколько подключённых рабочих деревьев. Одно «рабочее дерево» состоит из «рабочей копии» и метаданных репозитория. Большинство метаданных общие для всех рабочих деревьев одного репозитория, а некоторые поддерживаются отдельно для каждого рабочего дерева (например, индекс, HEAD и псевдоссылки вроде MERGE_HEAD, ссылки и файл конфигурации, относящиеся к конкретному рабочему дереву).
Приложение а: краткий справочник по Git
Здесь приведено краткое описание основных команд; в предыдущих главах подробно объясняется, как они работают.
Создание нового репозитория
Из tar-архива:
$ tar xzf project.tar.gz $ cd project $ git init Initialized empty Git repository in .git/ $ git add . $ git commit
Из удалённого репозитория:
$ git clone git://example.com/pub/project.git $ cd project
Управление ветками
$ git branch # list all local branches in this repo $ git switch test # switch working directory to branch "test" $ git branch new # create branch "new" starting at current HEAD $ git branch -d new # delete branch "new"
Чтобы создать новую ветку не от текущего HEAD (что используется по умолчанию), выполните:
$ git branch new test # branch named "test" $ git branch new v2.6.15 # tag named v2.6.15 $ git branch new HEAD^ # commit before the most recent $ git branch new HEAD^^ # commit before that $ git branch new test~10 # ten commits before tip of branch "test"
Создать новую ветку и сразу переключиться на неё:
$ git switch -c new v2.6.15
Обновить и просмотреть ветки из репозитория, который вы клонировали:
$ git fetch # update $ git branch -r # list origin/master origin/next ... $ git switch -c masterwork origin/master
Получить ветку из другого репозитория и присвоить ей новое имя в своём репозитории:
$ git fetch git://example.com/project.git theirbranch:mybranch $ git fetch git://example.com/project.git v2.6.15:mybranch
Сохранить список репозиториев, с которыми вы регулярно работаете:
$ git remote add example git://example.com/project.git
$ git remote # list remote repositories
example
origin
$ git remote show example # get details
* remote example
URL: git://example.com/project.git
Tracked remote branches
master
next
...
$ git fetch example # update branches from example
$ git branch -r # list all remote branches Изучение истории
$ gitk # visualize and browse history $ git log # list all commits $ git log src/ # ...modifying src/ $ git log v2.6.15..v2.6.16 # ...in v2.6.16, not in v2.6.15 $ git log master..test # ...in branch test, not in branch master $ git log test..master # ...in branch master, but not in test $ git log test...master # ...in one branch, not in both $ git log -S'foo()' # ...where difference contain "foo()" $ git log --since="2 weeks ago" $ git log -p # show patches as well $ git show # most recent commit $ git diff v2.6.15..v2.6.16 # diff between two tagged versions $ git diff v2.6.15..HEAD # diff with current head $ git grep "foo()" # search working directory for "foo()" $ git grep "foo()" v2.6.15 # search old tree for "foo()" $ git show v2.6.15:a.txt # look at old version of a.txt
Поиск регрессий:
$ git bisect start
$ git bisect bad # current version is bad
$ git bisect good v2.6.13-rc2 # last known good revision
Bisecting: 675 revisions left to test after this
# test here, then:
$ git bisect good # if this revision is good, or
$ git bisect bad # if this revision is bad.
# repeat until done. Внесение изменений
Убедитесь, что Git знает, кого винить:
$ cat >>~/.gitconfig <<\EOF
[user]
name = Your Name Comes Here
email = you@yourdomain.example.com
EOF Выберите содержимое файлов, которое нужно включить в следующий коммит, а затем создайте коммит:
$ git add a.txt # updated file $ git add b.txt # new file $ git rm c.txt # old file $ git commit
Или подготовьте изменения и создайте коммит за один шаг:
$ git commit d.txt # use latest content only of d.txt $ git commit -a # use latest content of all tracked files
Слияние
$ git merge test # merge branch "test" into the current branch
$ git pull git://example.com/project.git master
# fetch and merge in remote branch
$ git pull . test # equivalent to git merge test Обмен изменениями
Импорт и экспорт исправлений:
$ git format-patch origin..HEAD # format a patch for each commit
# in HEAD but not in origin
$ git am mbox # import patches from the mailbox "mbox" Получить ветку из другого репозитория Git, а затем объединить её с текущей веткой:
$ git pull git://example.com/project.git theirbranch
Перед слиянием с текущей веткой сохранить полученную ветку в локальной ветке:
$ git pull git://example.com/project.git theirbranch:mybranch
После создания коммитов в локальной ветке обновить удалённую ветку, добавив в неё свои коммиты:
$ git push ssh://example.com/project.git mybranch:theirbranch
Если удалённая и локальная ветки называются «test»:
$ git push ssh://example.com/project.git test
Краткая команда для часто используемого удалённого репозитория:
$ git remote add example ssh://example.com/project.git $ git push example test
Обслуживание репозитория
Проверить наличие повреждений:
$ git fsck
Повторно сжать данные и удалить неиспользуемые остатки:
$ git gc
Приложение б: заметки и список задач для этого руководства
Список задач
Работа над руководством продолжается.
Основные требования:
-
Руководство должно быть удобно читать последовательно, от начала до конца, человеку, который обладает интеллектом и базовыми знаниями командной строки UNIX, но не имеет специальных знаний о Git. При необходимости любые дополнительные требования должны быть явно указаны по мере их появления.
-
По возможности заголовки разделов должны ясно описывать выполняемую задачу и быть сформулированы так, чтобы не требовать лишних знаний. Например: «импорт исправлений в проект», а не «команда
gitam».
Продумать, как создать наглядный граф зависимостей глав, который позволит читателям переходить к важным темам, не обязательно читая всё между ними.
Просмотреть Documentation/ и найти другие упущенные темы, в частности:
-
практические руководства
-
некоторые разделы из
technical/? -
хуки
-
список команд в git[1]
Просмотреть архивы электронной почты и найти другие упущенные темы.
Просмотреть справочные страницы и проверить, не предполагает ли какая-либо из них наличие знаний, которые не представлены в этом руководстве.
Добавить больше хороших примеров. Возможно, полезно создать целые разделы с примерами из практики; может быть, стоит сделать раздел «Дополнительные примеры» стандартным заключительным разделом каждой главы?
Добавить перекрёстные ссылки на глоссарий там, где это уместно.
Добавить раздел о работе с другими системами управления версиями, включая CVS, Subversion, а также импорт серий архивов выпусков.
Написать главу об использовании низкоуровневых команд и создании скриптов.
Альтернативные базы данных объектов, clone --reference и т. д.
Добавить сведения о восстановлении после повреждения репозитория. См.: https://lore.kernel.org/git/Pine.LNX.4.64.0702272039540.12485@woody.linux-foundation.org/ https://lore.kernel.org/git/Pine.LNX.4.64.0702141033400.3604@woody.linux-foundation.org/
© 2005–2026 Linus Torvalds and others
Licensed under the GNU General Public License version 2.
https://git-scm.com/docs/user-manual