1. Git: прелюдия

Мы поговорим о системе контроля версий и логике её работы, с самых азов. С Git можно работать с помощью разных клиентов, потому в статье не пойдет речь об интерфейсе пользователя. Это может показаться непривычным, но это сделано намеренно. Вместо этого мы сфокусируемся на рабочем каталоге, коммитах, ветках, командах pull, push и прочих. Когда вы разберетесь в этих понятиях, вам останется выбрать один из Git-клиентов и освоить его интерфейс.

1.1. Первое знакомство

Представьте, что у вас есть каталог Work, в котором вы будете работать. Мы будем называть его рабочим каталогом. В нём лежит текстовый файл document.txt. Вы изо дня в день работаете с этим файлом, добавляете в него новый текст, ненужный удаляете, что-то изменяете. Внезапно вам потребовался текст, который вы пару дней назад удалили. Что делать? Если вы не используете систему контроля версий (СКВ), то этот текст не восстановить. А если вы пользуетесь СКВ – это не будет проблемой.

Как это работает? Вы всегда можете отправить свой файл в СКВ, и она его запомнит. Это называется «закоммитить файл» (команда commit). Если вы делаете такие коммиты после всех важных изменений файла, то внутри СКВ образуется множество копий разных его версий. Вы не увидите эти копии в вашем каталоге Work – там, по-прежнему будет лежать лишь один файл. Но все его копии, которые вы коммитили, можно при необходимости достать из СКВ. Там вы сможете посмотреть список всех версий вашего файла и проверить те строки, которые меняли в каждой версии. Удобно, не правда ли?

Предположим, вы меняете свой файл и даже сделали несколько его коммитов. СКВ запомнила закоммиченные версии файла (для краткости будем называть закоммиченную версию файла просто «коммитом»). Теперь выполним команду push – она отсылает ваши коммиты на сервер. В этом есть две главные цели:

  • Сохранность (если у вас испортится компьютер, вся история изменений файла останется на сервере);

  • Возможность нескольким людям одновременно работать над одним файлом.

Как выглядит одновременная работа? У других сотрудников на компьютере тоже есть папка Work и в ней тоже есть файл document.txt. Если вы отредактировали файл, сделали коммит и пуш, то ваши изменения попадут на сервер. Если другой человек выполнит команду pull, с сервера скачаются ваши изменения и человек увидит их в своём файле.

Так над одним и тем же файлом могут работать сразу несколько сотрудников. Они периодически забирают себе изменения других людей (pull), а также вносят свои изменения, коммитят их (commit), и отправляют на сервер (push). Оттуда их смогут забрать другие сотрудники. Интересно, что команда pull скачивает с сервера не только последнюю версию файла, но и все предыдущие, которые кто-либо коммитил. То есть, все коммиты вашего коллеги будут храниться не только на его компьютере, но теперь и на вашем тоже. Благодаря этому можно просмотреть всю историю файла: кто, когда и какие строчки в нём менял.

Для контроля версий мы используем программу Git. Для просмотра истории изменения файла Git не обращается к серверу. Он просто показывает все версии файла, которые уже хранятся на вашем компьютере. Версии файла, которые отредактировали другие, попадают на ваш компьютер после команды pull.

Итоги:

  • Мы доверили Git следить за нашей рабочей папкой;

  • В папке есть текстовый файл, в котором работают наши коллеги;

  • Периодически мы забираем изменения других сотрудников в этом файле (команда pull);

  • А также мы сами меняем этот файл и делаем коммиты, позволяя Git запомнить новую версию файла. Потом мы отправляем наши коммиты на сервер (команда push). Отправлять можно как один коммит, так и сразу несколько. Но лучше пушить почаще, чтобы сотрудники видели актуальную версию файла. Другое дело, если работа не закончена — такую «рабочую» версию файла пушить, конечно, не стоит. Ведь вы создадите неудобства другим сотрудникам.

Убедитесь, что вы чётко поняли разницу между изменением файла, его коммитом и «пушем». Поздравляем! Вы познакомились с основными возможностями системы контроля версий Git. Обратите внимание, мы специально не описываем, как вызывать команды. Потому что есть несколько Git-клиентов с разным интерфейсом. Чуть позже вы выберите один и изучите его. А пока мы просто расскажем, какие есть команды и что они делают.

1.2. Удалённый репозиторий, который никто не удалял

Место, куда Git сохраняет ваш файл после каждого коммита, называют локальным репозиторием. Он находится на вашем компьютере – потому и «локальный». Выше мы говорили, что команда push посылает ваши коммиты на сервер. Так вот, в Git не совсем верно говорить «сервер» – правильнее называть это «удалённым репозиторием». «Удалённый» – не потому, что его кто-то удалил, а потому, что он далеко (не у вас на компьютере). В нашем случае удалённый репозиторий находится на сайте GitHub. По своей структуре это такой же репозиторий, как у вас на компьютере. Он так же хранит коммиты. Команда push посылает коммиты с вашего локального репозитория в удалённый, а pull наоборот – забирает новые из удалённого репозитория в ваш локальный. Откуда там взялись свежие коммиты? Их туда отправили другие сотрудники.

Писать каждый раз «удалённый репозиторий» слишком долго. В Git его называют origin. Такова традиция. Дальше мы для краткости будем использовать термин origin вместо «удалённый репозиторий».

1.3. Углубляемся в детали

Пора углубиться в важные технические детали. Их довольно много, но понимание логики работы Git пригодится вам в работе. Так что придется набраться терпения.

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

Каждый коммит может запоминать изменения сразу в нескольких файлах. Хорошая практика – в один коммит включать только те изменения, которые логически связаны между собой. Не коммитьте одновременно множество разных изменений – в их истории будет трудно разобраться и вам, и другим сотрудникам.

Если вы добавили в ваш рабочий каталог новый файл и хотите, чтобы его тоже можно было коммитить (и чтобы его увидели другие сотрудники), вам нужно самому добавить файл в Git. Это делает команда add. Пока вы этого не сделали, Git не будет включать в коммиты ваш новый файл. Также можно создавать подкаталоги в рабочем каталоге. Файлы в них нужно добавить в Git тоже с помощью команды add.

Важно понять, что часть файлов в вашем рабочем каталоге может находиться под контролем Git, а часть – нет (например, логи). Следить за этим и добавлять новые файлы в Git – ваша задача.

Кстати, а откуда ваш рабочий каталог вообще возьмётся на вашем компьютере? Он там появится после того, как вы заберёте его с сервера (команда clone).

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

Git может вам показать список файлов, которые вы изменили с момента предыдущего коммита. Вы не обязаны коммитить их все сразу. Можете выбрать группу файлов, изменения в которых логически связаны, и закоммитить их. Потом выбрать другую – сделать ещё один коммит и так далее. Важно разбивать коммиты на логические.

Перед тем, как сделать push (отправить коммиты в удалённый репозиторий origin), нужно обязательно сначала сделать pull (забрать последние изменения других людей, которые они отправили в origin). Если этого не сделать, система не даст вам выполнить push (появится сообщение об ошибке). Когда узнаете больше об особенностях работы Git, вы поймёте почему так происходит, а пока просто запомните это правило.

1.4. Незакоммиченные изменения

Если вы поменяли какие-то файлы, но ещё не закоммитили их, то говорят, что в вашем рабочем каталоге есть незакоммиченные изменения. Ряд команд Git нельзя выполнить в таком состоянии. Например, pull или checkout (переключение файлов рабочей копии на другой коммит). Какие варианты выхода из этой ситуации?

  • Если эти изменения не нужны, то можно отменить незакоммиченные изменения. При этом файл вернётся в состояние последнего коммита;

  • Если изменения полезные, то можно их закоммитить;

  • Ещё можно временно спрятать эти изменения (команда stash). При этом в рабочем каталоге ваши изменения откатятся. Позже вы сможете вернуть спрятанные изменения в рабочий каталог.

1.5. Конфликты и их разрешение

Когда вы забираете изменения из origin могут возникнуть конфликты. Это происходит, если:

  • Вы изменили некоторые строчки в текстовом файле, закоммитили изменения, но ещё не сделали push.

  • В это же время кто-то изменил эти же строчки в файле и успел отправить изменения в origin.

  • Вы делаете pull, чтобы забрать чужие изменения. Git пытается объединить чужие изменения с вашими и обнаруживает, что изменены одни и те же строчки. Это называется конфликт (conflict).

Если это произошло, вам придётся разрешить конфликты вручную во всех файлах, где они возникли. Для каждой группы конфликтных строчек вы увидите два варианта – который написали вы и который написал кто-то другой.

Вам нужно выбрать, какой из этих вариантов оставить в результате слияния ваших изменений. Или можно вручную написать какой-то третий вариант – если нужно более тонкое объединение, чем просто копия одного из вариантов.

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

Если конфликт не в текстовом файле, а в бинарном (например, картинка), то там никаких строчек, конечно, нет. Тогда нужно целиком выбрать какой из файлов оставить после слияния – вашу версию файла или чужую. Если вы не уверены, как правильно – свяжитесь с тем, кто менял этот файл последним (это можно узнать в истории изменений файла) и обсудите с ним чья версия файла должна остаться. Не стоит всегда брать свои версии не разобравшись – вдруг изменения другого человека правильнее. А вы их затрёте своими изменениями, если разрешите конфликт в свою пользу. Это явно будет нехорошо.

Когда все конфликты разрешены, можно продолжить операцию, которая была прервана из-за конфликта – обычно это операции pull, merge, rebase.

Если вас напугали конфликты и вы не готовы их разрешать прямо сейчас, можно выполнить команду abort. Она откатит состояние рабочего каталога – сделает его таким, каким он был до начала операции, которая вызвала конфликт.

1.6. Слепки

Представьте текущее состояние одновременно всех файлов в вашем рабочем каталоге.

Представили? Назовём это состояние рабочего каталога «слепком». Каждый раз, когда вы делаете коммит очередных изменённых файлов, Git запоминает новый слепок (состояние всех файлов рабочего каталога). Понятие «слепка» мы будем использовать в следующей главе.

Примечания:

  • Слепок – это НЕ состояние только лишь изменённых файлов. Это состояние ВСЕХ файлов рабочего каталога._

  • Когда мы говорим «все файлы рабочего каталога», то, конечно, имеем в виду файлы под контролем Git. О всех остальных файлах Git ничего не знает.

1.7. История изменений и её визуальное представление

Историю изменений файлов рабочего каталога можно изобразить в таком виде:

Git History

Каждый кружок – это один коммит. На картинке показаны коммиты, которые сделаны один за другим. Это графическое представление истории коммитов.

Кстати, коммит можно рассматривать одним из двух способов:

  • Коммит, как изменение файлов с предыдущего коммита;

  • Коммит, как слепок – новое состояние всех файлов рабочего каталога, которое возникло после закоммиченных изменений.

В каких-то случаях удобно рассматривать историю, как цепочку изменений, а в каких-то – как цепочку слепков. Так что научитесь мысленно оперировать обоими вариантами.

Вот пример, когда удобно рассматривать «слепки». Представьте, что вы хотите посмотреть, как выглядел ваш рабочий каталог три коммита назад. Это легко устроить. Берём историю (см. картинку), отсчитываем 3 кружка (коммита) назад и говорим Git: «Хочу посмотреть, как выглядел рабочий каталог вот после этого коммита». Git изменит файлы в рабочем каталоге соответственно. Другими словами, мы переключили рабочий каталог на слепок этого коммита (или просто «на этот коммит»).

1.8. Ветки

Концепция веток не так проста. Представьте, что вам нужно внести множество изменений в файлы вашего рабочего каталога, но эта работа экспериментальная – не факт, что всё получится хорошо. Вы бы не хотели, чтобы ваши изменения увидели другие сотрудники до тех пор, пока работа не будет закончена. Может просто ничего не коммитить до тех пор? Это плохой вариант. Мы уже знаем, что частые коммиты и пуши – залог сохранности вашей работы, а также возможность посмотреть историю изменений. К счастью, в Git есть механизм веток, который позволит нам коммитить и пушить, но не мешать другим сотрудникам.

Перед началом экспериментальных изменений вы должны создать ветку. У ветки есть имя. Пусть она будет называться my test work. Теперь все ваши коммиты будут идти именно туда. До этого они шли в основную ветку разработки – будем называть её master. Другими словами, раньше вы были в ветке master (хоть и не знали этого), а сейчас переключились на ветку my test work. Это выглядит так:

Создание ветки my test work

После коммита «3» создана ветка и ваши новые коммиты «4»и «5» пошли в неё. А ваши коллеги остались в ветке master, поэтому их новые коммиты «6», «7», «8» добавляются в ветку master. История перестала быть линейной.

На что это повлияло? Сотрудники теперь не видят изменений файлов, которые вы делаете. А вы не видите их изменений в своих рабочих файлах. Хотя историю изменений в ветке master вы все-таки посмотреть можете.

Итак, теперь вы сможете никому не мешая сделать свою экспериментальную работу. Если её результаты вас не устроит, вы просто переключитесь на ветку master (на её последний коммит – на рисунке это коммит «8»). В момент переключения файлы в вашей рабочей папке станут такими же, как у ваших коллег, а ваши изменения исчезнут. Теперь ваша рабочая копия стала слепком из коммита «8». По картинке видно, что в нём нет ваших изменений, сделанных в ветке my test work.

1.9. Слияние веток

Теперь мы знаем, что каждый может создать ветки и работать независимо. Можно по очереди работать то в одной ветке, то в другой – переключаясь между ними. Ветки переключает команда checkout.

Ветки используются не только для временной независимой работы. Часто мы одновременную готовим несколько версий игры. Например, одна версия уже почти готова к публикации и программисты вносят в неё последние исправления. В то же время гейм-дизайнеры уже занимаются следующим обновлением. Им нельзя работать в предыдущей версии потому, что:

  • Их изменения не должны появиться в текущей версии;

  • Любые изменения могут что-то сломать, поэтому перед публикацией версии нужно вносить в неё как можно меньше изменений.

Словом, от веток много пользы. Но вернёмся к примеру с вашей экспериментальной работой. В предыдущей главе мы решили, что она не удалась. Вы вернулись в ветку master и потеряли изменения, сделанные в ветке my test work. А если все получилось? Вы хотите перенести свои изменения в ветку master, чтобы их увидели сотрудники, которые с ней работают. Git может помочь – выполним команду merge ветки my test work в ветку master:

Мерж  ветки my test work и ветки master

Здесь коммит «8» – это специальный коммит, который называется merge-commit. Когда мы выполняем команду merge, система сама создает этот коммит. В нём объединены изменения ваших коллег из коммитов «5», «6», «7», а также ваша работа из коммитов «3», «4».

Изменения из коммитов «1» и «2» объединять не нужно, ведь они были сделаны до создания ветки. А значит изначально были и в ветке master, и в ветке my test work.

Команда merge ничего не посылает в origin. Единственный ее результат – это merge-commit (на рисунке кружок с номером 8), который появится у вас на компьютере. Его нужно запушить, как и ваши обычные коммиты. Только после этого merge-commit отправится на origin – тогда коллеги увидят результат вашей работы, сделав pull.

1.10. Несколько мержей из ветки А в ветку В

В предыдущей главе мы узнали, как сделать новую ветку, поработать в ней и залить изменения в главную ветку. На картинке после объединения ветки слились вместе. Означает ли это, что в ветке my test work теперь работать нельзя – она ведь уже объединилась с master? Нет, вы можете продолжать коммитить в ветку my test work и периодически мержить в главную ветку. Как это выглядит:

Несколько мержей из ветки my test work в ветку master

Обратите внимание, что отрезки соединяющие ветки не горизонтальные – так показано, из какой ветки в какую был мерж. В этой ситуации было два мержа и оба из правой ветки в левую. Результатом первого объединения стал merge-commit «7», а второго – merge-commit «10». Поскольку мерж происходит из правой ветки в левую, то, например, в слепке «8» есть изменения, которые были сделаны в коммите «3». А вот в слепке «11» нет изменений, которые были сделаны в коммите «5». Убедитесь, что вы понимаете причину этого. Если нет, перечитайте главы о ветках ещё раз.

1.11. Мерж между ветками в обе стороны

В предыдущем примере мы всё время мержили из ветки my test work в ветку master. Можно ли мержить в обратную сторону и есть ли в этом смысл? Можно. Есть.

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

Мерж как из ветки my test work в ветку master

Здесь два мержа из ветки my test work в ветку master и один мерж в обратную сторону. Результатом обратного объединения стал merge-commit «8». Благодаря ему, например, слепок коммита «11» содержит изменения из коммита «7». А вот изменений из коммита «9» в слепке «11» уже нет, ведь этот коммит был сделан после мержа.

1.12. Коммиты и их хеши

Как Git различает коммиты? На картинках мы для простоты помечали их порядковыми номерами. На самом деле каждый коммит в Git обозначается вот такой строкой:

e09844739f6f355e169f701a5b7ae02c214d5fb0

Это «названия» коммитов, которые Git автоматически даёт им при создании. Вообще, такие строки принято называть «хеш». У каждого коммита хеш разный. Если вы хотите кому-то сообщить об определённом коммите, можно отправить человеку хеш этого коммита. Зная хеш, он сможет найти этот коммит (если это ваш коммит, то, конечно, его надо сначала запушить).

1.13. Ветки и указатели

Сейчас мы немного углубимся в то, как Git хранит информацию о ветках. Вроде бы внутреннее устройство Git нас не должно волновать, но это позволит намного лучше понимать, что происходит при выполнении операций в Git. А вы, в свою очередь, сможете избежать ряда ошибок.

Познакомимся с концепцией «указателя». В упрощённом виде указатель состоит из своего названия и хеша. Вот пример указателя:

master – e09844739f6f355e169f701a5b7ae02c214d5fb0

Тут вы скажете: «master – знакомое имя! У нас так называлась главная рабочая ветка». И это совпадение не случайно. Git использует указатели для обозначения веток. Идея простая: если нужна новая ветка, Git создаёт новый указатель, даёт ему имя ветки и записывает в него хеш последнего (самого свежего) коммита ветки. Ветка создана! Благодаря хешу в указателе можно сказать, что указатель ссылается или «указывает» на последний коммит ветки. Этого достаточно Git’у, чтобы выполнять все операции над ветками. То есть, никакой другой информации о том, какие коммиты принадлежат какой ветке Git не хранит. Вот так всё минималистично.

На каждую ветку есть свой указатель. Когда в ветку добавляется очередной коммит, хеш в указателе меняется, чтобы снова «указывать» на последний коммит. Это можно представить, как сдвигание указателя ветки на последний коммит с предпоследнего.

Если вы просите Git переключиться на другую ветку (команда checkout), ему достаточно найти указатель с именем этой ветки и взять из него хеш последнего коммита. Теперь Git знает, как должны выглядеть файлы вашего рабочего каталога (как слепок этого коммита). Git приводит файлы к такому виду – и переключение на ветку произошло.

Если вы не совсем поняли идею указателей и то, как они связаны с ветками, перечитайте главу ещё раз. В Git многое завязано на указатели, поэтому важно чётко понимать механику их работы. К счастью, она совсем не сложная, просто немного необычная. Нужно лишь привыкнуть.

1.14. Указатель head

Итак, мы знаем, что указатели – это такие штуки, у которых есть имя, и они ссылаются на определенный коммит (хранят его хеш). Мы знаем, что при необходимости новой ветки, Git создаёт указатель на ее последний коммит и двигает его вперед при каждом новом коммите.

Указатели используются не только для веток. Есть особый указатель head. Он указывает на коммит, который выступает состоянием вашего рабочего каталога. Поняли идею? Вот пример:

Пример указателя head

Здесь мы видим две ветки, которые представлены двумя указателями: master и test. Мы находимся в ветке master и файлы нашего рабочего каталога соответствуют слепку коммита «4». Откуда мы это знаем? Из того, что указатель head указывает на коммит «4». Точнее, он указывает на указатель master, который указывает на коммит «4». Почему бы не указывать напрямую на коммит «4»? Зачем такой финт с указанием на указатель? Так Git обозначает, что сейчас мы находимся в ветке master.

Мы можем поставить указатель head на любой коммит – для этого есть команда checkout. Вспомним, что на какой коммит показывает head, в таком состоянии и будут файлы в рабочем каталоге (это свойство указателя head). Поэтому переставляя указатель head на другой коммит, мы тем самым заставим Git поменять файлы нашего рабочего каталога. Это может потребоваться, например, чтобы откатиться на старую версию рабочих файлов и посмотреть, как там всё было. А потом можно вернуться назад к последнему коммиту ветки master (checkout master). Если же сделаем checkout test (см. картинку), то head будет указывать на указатель test, который указывает на последний коммит ветки test. Файлы в рабочем каталоге поменяются на слепок «6». Так мы переключились на ветку test.

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

А что происходит, если head указывает на какой-то коммит напрямую (хранит его хеш)? Это состояние называется detached head. В него можно переключиться на время, чтобы посмотреть, как выглядели файлы рабочего каталога на одном из коммитов в прошлом.

Переключение (как между ветками, так и между обычными коммитами) выполняется командой checkout.

1.15. Указатель origin/master

Раз удалённый репозиторий (origin) такой же, как наш, значит там тоже есть свои указатели веток? Верно. Например, есть свой указатель master, который ссылается на самый свежий коммит в этой ветке.

Интересно, что когда мы забираем свежие коммиты из origin командой pull, то вместе с коммитами скачиваются и копии указателей оттуда. Чтобы не путать наш указатель master и тот, который скачался с origin, второй из них отображается у нас, как origin/master. Нужно понимать, что origin/master не показывает текущее состояние указателя master в удаленном репозитории, это лишь его копия на момент выполнения команд fetch или pull.

master и origin/master могут указывать на разные коммиты. Станет понятнее, если посмотреть на картинку:

Указатели head и origin/master отображают разные коммиты: head - коммит номер 5

Здесь показана ситуация, когда мы забрали свежие коммиты (командой pull), сделали два новых коммита, но ещё не сделали push. В итоге наш локальный master показывает на последний коммит. А origin/master – это последнее известное нам состояние указателя из удалённого репозитория. Поэтому он и «отстал».

После команды push два верхних коммита уйдут в origin и логично, что origin/master подвинется вверх и тоже будет указывать на наш последний коммит, как и master.

А может ли быть так, что origin/master будет наоборот выше, а master ниже? Может. Вот как это получается. Команда pull забирает свежие коммиты и сразу же помещает их в рабочий каталог. Сразу после команды pull оба указателя origin/master и master будут указывать на один и тот же последний коммит. Но есть ещё команда fetch. Она, как и pull, скачивает последние коммиты из origin, но не торопится обновлять рабочий каталог. Графически это выглядит так (если у вас нет незапушенных коммитов):

Указатели head и origin/master отображают разные коммиты: head - коммит номер 3

До команды fetch указатель master показывал на коммит «3» и это был последний коммит в нашем репозитории. После fetch скачались два новых коммита «4» и «5». В удалённом репозитории указатель master, очевидно, указывал на коммит «5». Этот указатель скачался нам вместе с коммитами и теперь мы его видим как origin/master, указывающий на «5». Всё логично.

Зачем может потребоваться fetch? Например, вы не готовы менять состояние рабочего каталога, а просто хотите поглядеть, чего там накоммитили ваши коллеги? Вы делаете fetch и изучаете их коммиты. Когда будете готовы, делаете команду merge. Она применит скачанные ранее коммиты к вашему рабочему каталогу.

Поскольку в этом простом примере у вас не было незапушенных коммитов, то команде merge объединять ничего не придётся. Она просто подвинет указатели master и head – теперь они будут показывать на коммит «5». Как и origin/master.

Вы можете заметить, что ничего по-настоящему сложного в описанных механиках нет. Есть лишь множество деталей, в которых приходится кропотливо разбираться. Но Git – он такой.

1.16. Откуда взялась ветка?

Набираемся терпения и продолжаем рассматривать разные рабочие ситуации. Если мы сделаем несколько коммитов, а потом выполним команду fetch (скачаем свежие коммиты, но пока не применим их в рабочий каталог), то увидим немного сбивающую с толку картину:

Схема коммитов

Что это ещё за ветка получилась? Мы ведь не создавали никакой ветки. Может её создал кто-то из сотрудников? Нет, никто её не создавал. Восстановим хронологию событий:

  • Сначала мы скачали свежие коммиты. Тогда последним был коммит «2».

  • Затем мы сделали коммиты «3» и «4» (но пока не пушили их).

  • В это время другие сотрудники запушили в удалённый репозиторий коммиты «5», «6» и «7». Тогда мы ничего не знали об этом.

  • Наконец, мы сделали fetch и увидели то, что на картинке.

В Git каждый коммит хранит ссылку на предыдущий (это и позволяет нам соединять кружки на рисунках; каждый отрезок – это ссылка на предыдущий коммит). Когда мы сделали коммит «3», для нас последним коммитом был «2» поэтому они соединены. Но когда на origin кто-то запушил коммит «5», там последним был тоже коммит «2» – ведь мы свои коммиты «3» и «4» ещё не запушили, и на origin их не было. А раз так, то для коммита «5» предыдущим тоже выступает коммит «2», именно эту связь Git и запомнил.

Итого, разные люди независимо друг от друга поменяли результат коммита «2» – вот и возникла ветка. Кстати, эта ветка сейчас есть только в нашем локальном репозитории. В origin её пока нет, поскольку коммиты «3» и «4» мы до сих пор не запушили.

Что дальше? Поскольку мы сделали fetch, а не pull, то скачанные коммиты ещё не применились к нашему рабочему каталогу. Давайте применим их – для этого выполним merge. Результат представлен на картинке:

Схема пулов

Произошедшее уже знакомо нам. Образовался автоматический merge-commit «8» – master и head теперь указывают на него. В рабочей копии появились изменения из коммитов «5», «6» и «7», которые объединились с нашими изменениями из коммитов «3» и «4». origin/master по-прежнему указывает на «7», поскольку последние наши операции проходили на локальном компьютере. А origin/master может сдвинуться только после общения нашего репозитория с origin.

Наконец, делаем push, и вот теперь origin/master тоже указывает на «8», ведь: * Наш merge-commit «8» отправлен в origin. * Там он стал последним, а значит удалённый указатель master теперь показывает на него. * Нам скачалась информация об удалённом указателе master и мы её видим как origin/master.

Вот он и показывает на «8». Логично.

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

1.17. Почему push выдаёт ошибку?

Вы обязательно столкнетесь с тем, что Git выдаёт ошибку при команде push. В чём проблема? Почему он не принимает наши коммиты? Push успешно завершится, только если для каждого отправляемого в origin коммита Git сможет найти предшественника. Пример:

Схема пушей

Здесь слева изображены коммиты в вашем локальном репозитории, а справа – коммиты в удалённом репозитории (origin).

Хронология этих коммитов следующая:

  • Сначала в origin были коммиты «1» и «2».

  • Мы сделали pull (в локальном репозитории тоже оказались лишь эти два коммита).

  • Потом мы закоммитили «3» и «4» в локальный репозиторий (но не пушили).

  • Кто-то запушил коммит «5» в origin.

И получилось то, что сейчас на картинке. Разобрались?

Теперь наша попытка запушить «3» и «4» в origin завершится ошибкой. Git откажется пристыковать наши коммиты к последнему коммиту «5» в origin, поскольку в local предшественником для коммита «3» является коммит «2» – а вовсе не «5», как в origin! Для Git важно, чтобы предшественник был тот же.

Проблема решается легко. Перед тем, как сделать push, мы сделаем pull (забираем коммит «5» себе). Тут вы можете просить: «Секунду! А почему это забрать коммит «5» Git может, а послать коммиты «3» и «4» он не может? Вроде же ситуация симметричная в обе стороны». Правильный вопрос! А ответ на него простой. Если бы Git позволил отправить коммиты «3» и «4» в такой ситуации, то пришлось бы делать merge на стороне origin – а кто там будет разрешать конфликты? Некому. Поэтому Git заставляет вас сначала забрать свежие коммиты себе, сделать merge на своем компьютере (если будут конфликты, то разрешить их), а уже готовый результат он позволит вам отправить в origin командой push. При этом, никаких конфликтов в origin уже быть не может.

Давайте посмотрим, как будет выглядеть локальная история, после того, как вы заберете коммит «5» командой pull.

Схема локальная

Здесь у «3» и «5» предок «2», как и на предыдущей картинке. А новый коммит «6» – это уже давно известный нам merge-commit.

В таком состоянии локальные коммиты уже можно запушить. Пусть тут и появилось разветвление истории, но обе ветки при мерже объединились. А значит голова у ветки снова одна. То есть, ничего не мешает сделать push. После этого в origin коммиты будут выглядеть такой же точно «петелькой».

Теперь, когда push выдаст вам ошибку, вы уже знаете почему и что с этим делать.

1.18. Rebase

В предыдущей главе мы сделали несколько локальных коммитов, а потом командой pull забрали коммиты других сотрудников из удалённого репозитория. У нас в локальном репозитории образовалась как бы «ветка», которая потом обратно объединилась с основной. После push это временное раздвоение ветки попало в origin, откуда его скачают сотрудники и увидят в своей истории. Часто такие «петли» считаются нежелательными. Поскольку вместо красивой линейной истории получается куча петель, которые затрудняют просмотр.

Git предлагает альтернативу. Выше мы делали fetch+merge. Первая команда забирает свежие коммиты, вторая объединяет их с нашими незапушенными коммитами (если они есть) и создаёт merge-commit с результатом объединения.

Так вот, оказывается можно вместо fetch+merge делать fetch+rebase. Что за rebase и чем он отличается от merge? Вспомним ещё раз, как проходил merge в предыдущем примере:

Схема фетч-мердж

Rebase действует по-другому – он отсоединяет вашу цепочку незапушенных коммитов от своего предка. Напомним, это были коммиты «3» и «4». Они отсоединяются от своего предка «2» и rebase ставит их «сверху» на только что скачанный коммит «5». То есть, «3» и «4» будут прицеплены сверху к «5» (а мерж-коммит «6» вообще не появится). Итог будет таким:

Схема фетч-ребэйз

Никакой петли больше нет, история линейная и красивая! Да здравствует rebase! Теперь мы знаем, что при скачивании коммитов из origin лучше объединять их со своими локальными коммитами при помощи rebase, а не merge.

Хорошо, а если речь не о паре-тройке ваших коммитов, а о большой ветке с разработкой новой фичи. Когда настанет время влить эту фичу в главную ветку, как это лучше сделать – через rebase или merge? У обоих способов есть преимущества:

  • rebase позволит сохранить историю простой и линейной – он добавит цепочку ваших коммитов из ветки в конец основной ветки.

  • merge сделает петлю, но зато в истории более наглядно будет прослеживаться история разработки вашей фичи.

Вопрос предпочтения rebase или merge в таких случаях обсудите с ведущим программистом вашего проекта.

1.19. Эпилог

Мы с вами разобрались в множестве команд Git для работы с репозиториями:

  • pull

  • commit

  • push

  • add

  • clone

  • checkout

  • stash

  • merge

  • rebase

  • abort

  • fetch

Это не все команды, которые бывают нужны в работе – только самые частые. Будьте готовы, что потребуется освоить и другие. Работать с Git можно при помощи разных git-клиентов. Мы в основном используем эти три:

  • Консольный

  • SourceTree

  • TortoiseGit

Выбор клиента – дело вкуса.

Консольный – работает на всех платформах, но у него крайне аскетичный интерфейс. Если вы не привыкли работать в консоли, то скорее всего вам будет в нем некомфортно.

SourceTree — графический клиент с довольно простым интерфейсом. Есть версии для наших основных платформ: Win и Mac. Однако, сотрудники часто жалуются на его медленную работу и глюки.

TortoiseGit — еще один графический клиент. Есть версия для Win, для Mac`а нет. Интерфейс несколько непривычный, но многим нравится. Жалоб на глюки и тормоза существенно меньше, чем в случае с SourceTree.

Интересно, что и SourceTree, и TortoiseGit не работают с репозиторием Git напрямую. Внутри себя они используют консольный Git. Когда вы нажимаете на красивые кнопки, вызываются консольные команды Git с разными хитрыми параметрами, а результат вызова снова показывают в красивом виде. Использование всеми клиентами консольного Git означает, что все они работают со стандартной файловой структурой Git-хранилища на вашем жёстком диске. А значит можно использовать смешанный стиль работы: одни операции выполнять в одном клиенте, а другие – в другом.

Итак, вы узнали основные концепции, используемые системой контроля версий Git. А также, как работают основные команды. Наверняка при чтении статьи вам не хватало описания «какие кнопки нажимать». Однако, в каждом Git-клиенте это выглядит по-разному, поэтому нам пришлось отделить описание логики от описания интерфейса. Настало время выбрать один из клиентов и изучить его интерфейс пользователя.

2. Что такое системы контроля версий?

Система контроля версий (Version Control System, VCS) представляет собой программное обеспечение, которое позволяет отслеживать изменения в документах, при необходимости производить их откат, определять, кто и когда внес исправления и т.п.

2.1. Что такое система контроля версий?

Наверное, всем знакома ситуация, когда при работе над проектом, возникает необходимость внести изменения, но при этом нужно сохранить работоспособный вариант, в таком случае, как правило, создается новая папка, название которой скорее всего будет Новая папка с дополнением в виде даты или небольшой пометки, в нее копируется рабочая версия проекта и уже с ним производится работа. Со временем количество таких папок может значительно возрасти, что создает трудности в вопросе отката на предыдущие версии, отслеживании изменений и т.п. Эта ситуация значительно ухудшается, когда над проектом работает несколько человек.

Для решения таких проблем как раз и используется система контроля версий, она позволяет комфортно работать над проектом как индивидуально, так в коллективе. VCS:

  • отслеживает изменения в файлах

  • предоставляет возможности для создания новых и слияние существующих ветвей проекта

  • производит контроль доступа пользователей к проекту

  • позволяет откатывать исправления

  • определять кто, когда и какие изменения вносил в проект.

Основным понятием VCS является репозиторий (repository) – специальное хранилище файлов и директорий проекта, изменения в которых отслеживаются. В распоряжении разработчика имеется так называемая рабочая копия (working copy) проекта, с которой он непосредственно работает. Рабочую копию необходимо периодически синхронизировать с репозиторием, эта операция предполагает отправку в него изменений, которые пользователь внес в свою рабочую копию (такая операция называется commit). Так же она предполагает, актуализацию рабочей копии, в процессе которой к пользователю загружается последняя версия из репозитория (этот процесс носит название update).

2.2. Централизованные и распределенные системы контроля версий

Системы контроля версий можно разделить на две группы:

  • централизованные

  • распределенные

2.2.1. Централизованные системы контроля версий

Централизованные системы контроля версий представляют собой приложения типа клиент-сервер, когда репозиторий проекта существует в единственном экземпляре и хранится на сервере. Доступ к нему осуществлялся через специальное клиентское приложение. В качестве примеров таких программных продуктов можно привести CVS, Subversion.

Схема системы контроля версий с центральным репозиторием
CVS
Система контроля версий CVS

CVS (Concurrent Versions System, Система одновременных версий) одна из первых систем получивших широкое распространение среди разработчиков, она возникла в конце 80-х годов прошлого века. В настоящее время этот продукт не развивается, это в первую очередь связано с рядом ключевых недостатков, таких как невозможность переименования файлов, неэффективное их хранение, практически полное отсутствие контроля целостности.

Subversion
Система контроля версий Subversion

Subversion (SVN) – система контроля версий, созданная на замену CVS. SVN была разработана в 2004 году и до сих пор используется. Несмотря на многие преимущества по сравнению с CVS у SVN все-таки есть недостатки, такие как проблемы с переименованием, невозможность удаления данных из хранилища, проблемы в операции слияния ветвей и т.д. В целом SVN был (и остается) значительном шагом вперед по сравнению с CVS.

2.2.2. Распределенные системы контроля версий

Распределенные системы контроля версий (Distributed Version Control System, DVCS) позволяют хранить репозиторий (его копию) у каждого разработчика, работающего с данной системой. При этом можно выделить центральный репозиторий (условно), в который будут отправляться изменения из локальных и, с ним же эти локальные репозитории будут синхронизироваться. При работе с такой системой, пользователи периодически синхронизируют свои локальные репозитории с центральным и работают непосредственно со своей локальной копией. После внесения достаточного количества изменений в локальную копию они (изменения) отправляются на сервер. При этом сервер, чаще всего, выбирается условно, т.к. в большинстве DVCS нет такого понятия как выделенный сервер с центральным репозиторием.

Схема распределенной системы контроля версий

Большое преимущество такого подхода заключается в автономии разработчика при работе над проектом, гибкости общей системы и повышение надежности, благодаря тому, что каждый разработчик имеет локальную копию центрального репозитория. Две наиболее известные DVCS – это Git и Mercurial.

Mercurial
Система контроля версий Mercurial

Начнем с Mercurial, эта система представляет собой свободную DVCS, которая построена таким образом, что в ней отсутствует понятие центрального репозитория, для работы с этой VCS используется (как правило) консольная утилита hg. Mercurial обладает всеми возможностями системы контроля версий, такими как ветвление, слияние, синхронизация с другими репозиториями. Данный проект используют и поддерживают большое количество крупных разработчиков, среди них Mozilla, OpenOffice, OpenJDK и многие другие. Сам продукт написан на языке Python и доступен на большинстве современных операционных систем (Linux, Windows, Mac OS), также существует значительное количество утилит с графическим интерфейсом для работы с Mercurial. Основным конкурентом Mercurial на рынке распределенных систем контроля версий является Git, который, на сегодняшний день, выиграл гонку за лидерство.

Git
Система контроля версий Git

Git – распределенная система контроля версий, разработанная Линусом Торвальдсем для работы над ядром операционной системы Linux. Среди крупных проектов, в рамках которых используется git, можно выделить ядро Linux, Qt, Android. Git свободен и распространяется под лицензией GNU GPL 2 и, так же как Mercurial, доступен практически на всех операционных системах. По своим базовым возможностям git схож с Mercurial (и другими DVCS), но благодаря ряду достоинств (высокая скорость работы, возможность интеграции с другими VCS, удобный интерфейс) и очень активному сообществу, сформировавшемуся вокруг этой системы, git вышел в лидеры рынка распределенных систем контроля версий. Необходимо отметить, что несмотря на большую популярность таких систем как git, крупные корпорации, подобные Google, используют свои VCS.

3. Установка Git

Для того, что бы начать работать с системой контроля версий Git ее необходимо предварительно установить. Рассмотрим варианты установки этой VCS под Linux и Windows.

3.1. Установка Git под Linux

Для установки Git под Linux, необходимо зайти на сайт Git и перейти в раздел Downloads. В зависимости от используемой вами версии операционной системы Linux необходимо выбрать тот или иной способ установки Git.

3.1.1. Debian/Ubuntu

apt-get install git

3.1.2. Fedora

Fedora 21
yum install git
Fedora 22
dnf install git

3.1.3. Gentoo

emerge --ask --verbose dev-vcs/git

3.1.4. Arch Linux

pacman -S git

3.1.5. openSUSE

zypper install git

3.1.6. Mageia

urpmi git

3.1.7. FreeBSD

pkg install git

3.1.8. Solaris 9/10/11 (OpenCSW)

pkgutil -i git

3.1.9. Solaris 11 Express

pkg install developer/versioning/git

3.1.10. OpenBSD

pkg_add git

3.1.11. Alpine

apk add git

3.2. Установка Git под Windows

Для установки Git под Windows необходимо предварительно скачать дистрибутив. Для этого перейдите на страницу Git

Если вы зашли из под операционной системы (ОС) Windows, главная страница сайта будет выглядеть примерно так, как показано на рисунке ниже. Для других ОС отличие будет заключаться в том, что изменится область для скачивания дистрибутива (см. правый нижний угол).

first page

Для того чтобы скачать Git, нужно нажать на кнопку Downloads for Windows, расположенную в правой части окна.

Процесс дальнейшей установки Git выглядит так.

3.2.1. Начало

Запустить установочный файл.

3.2.2. Лицензионное соглашение

Ознакомиться, если есть желание, с лицензионным соглашением и нажать на кнопку Next:

license terms

3.2.3. Выбор устанавливаемых компонентов

Выбрать компоненты, которые следует установить:

selection of components

3.2.4. Выбор способа использования Git

Указать способ использования Git:

way to use

В этом окне доступны три возможных варианта:

  • Use Git from Git Bash only

Переменная PATH не модифицируется и работа с Git возможна только через специализированную оболочку, которая называется Git Bash.

  • Use Git from the Windows Command Prompt

В этом случае происходит минимальная модификация переменной окружения PATH, которая позволит работать с Git через командную стоку Windows. Работа через Git Bash также возможна.

  • Use Git and optional Unix tools from the Windows Command Prompt

В переменную PATH вносится значительное количество модификаций, которые позволят, в рамках командной строки Windows, использовать как Git так и утилиты Unix, которые поставляются вместе с дистрибутивом Git.

Рекомендуется: опция Use Git from the Windows Command Prompt.

3.2.5. Настройка правил окончания строки

setting line ending rules

Существует два варианта формирования конца строки в текстовых файлах – это Windows стиль и Unix стиль. Данное окно позволяет выбрать одну из опций, определяющих правило формирования окончания строки:

  • Checkout Windows-style, commit Unix-style line endings

Checkout (операция извлечения документа из хранилища и создания рабочей копии) производится в Windows стиле, а commit (операция отправки изменений в репозиторий) в Unix стиле.

  • Checkout as-is, commit Unix-style line endigns

Checkout производится в том формате, в котором данные хранятся в репозитории, а commit осуществляется в Unix стиле.

  • Checkout as-is, commit as-is

Checkout и commit производятся без дополнительных преобразований.

Рекомендуется: опция Checkout Windows-style, commit Unix-style line endings.

3.2.6. Выбор эмулятора терминала

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

choosing a terminal emulator

Возможен выбор из двух вариантов:

  • Use MinTTY (the defaul terminal of MSYS2)

Git Bash будет использовать в качестве эмулятора терминала MinTTY.

  • Use Windows’ default console window

Git будет использовать Windows консоль (cmd.exe).

Рекомендуется: опция Use MinTTY (the defaul terminal of MSYS2).

3.2.7. Настройка дополнительных параметров

setting additional parameters

Доступны следующие параметры:

  • Enable file system caching

Включение операции кэширования при работе с файлами. Эта опция позволит значительно повысить производительность.

  • Enable Git Credential Manager

Предоставляет возможность работы с защищенным хранилищем.

  • Enable symbolic links

Активирует работу с символьными ссылками.

Рекомендуется: опции Enable file system caching и Enable Git Credential Manager.

3.2.8. Завершение установки

После нажатия на кнопку Install будет произведена установка Git на Windows, по окончании установки пользователь получит соответствующее сообщение.

completing the installation

4. Настройка Git

Настройка системы Git предполагает, в первую очередь, указание имени пользователя и e-mail, которые используются для подписи коммитов и отправки изменений в удаленный репозиторий.

В Git существует три места, где хранятся настройки:

  • на уровне системы;

  • на уровне пользователя;

  • на уровне проекта (репозитория).

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

4.1. Расположение конфигурационных файлов Git

4.1.1. Windows

  • Уровень системы: C:\Program Files\Git\mingw64\etc\gitconfig

    • Имейте в виду, что для его изменения могут понадобиться права администратора!

  • Уровень пользователя: %HOMEPATH%\.gitconfig

  • Уровень репозитория: директория_с_проектом\.git\config

4.1.2. Linux

  • Уровень системы: /etc/gitconfig

  • Уровень пользователя: ~/.gitconfig

  • Уровень репозитория: директория_с_проектом/.git/config

4.2. Конфигурирование Git с помощью утилиты командной строки

Конфигурирование Git с помощью утилиты командной строки – это наиболее удобный и безопасный способ. Независимо от того, на каком уровне вы хотите менять настройки, команда будет начинаться так:

git config

Для уровня системы, необходимо написать:

git config --system

уровня пользователя:

git config --global

уровня приложения:

git config --local

После этой команды указывается параметр и его значение.

Например, зададим имя и e-mail разработчика для уровня пользователя.

git config --global user.name "User"
git config --global user.email "user@company.com"

Для просмотра введенных изменений воспользуйтесь командой:

git config --list

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

вариант для Linux:

git config --global core.editor "nano"

вариант для Windows:

git config --global core.editor "notepad.exe"

5. Архитектура Git

Для того чтобы двигаться дальше – создавать репозитории, отправлять коммиты, делать новые бранчи (ветви) и т.п. необходимо предварительно ознакомиться с архитектурой Git. Архитектура будет рассмотрена на достаточном уровне, чтобы понимать суть действий, которые будут совершаться в дальнейшем.

5.1. Архитектура трех деревьев

Система контроля версий Git имеет архитектуру трех деревьев. Перед тем как перейти к ее описанию, для начала, рассмотрим архитектуру двух деревьев. Схематично она выглядит так, как представлено на рисунке ниже.

Схема архитектуры двух деревьев

Для начала введем используемую в системах контроля версий терминологию. Набор файлов, с которым идет работа в данный момент, называется рабочая копия (working copy). После того как решено, что все нужные изменения на данный момент внесены, и об этом можно сообщить системе контроля версий, разработчик производит отправку изменений в репозиторий (repository). Репозиторий – это хранилище для проекта, которое обслуживает система контроля версий. Сама операция отправки изменений называется commit. Если необходимо взять данные из репозитория, то мы осуществляем операцию checkout. Для названий операций commit и checkout не используют прямой перевод, предпочитают транскрипцию. В дальнейшем будет использоваться как английский, так и русский эквивалентом терминов.

Для архитектуры двух деревьев регламент работы с репозиторием может выглядеть следующим образом:

  1. Перед началом работы разработчик делает checkout, для того чтобы быть уверенным, что он будет работать с актуальной рабочей копией.

  2. Разработчик вносит необходимые изменения в исходный код.

  3. Разработчик отправляет изменения в репозиторий (коммитит их).

  4. Повторить необходимое количество раз пункты 2 и 3.

Система контроля версий Git использует архитектуру трех деревьев. Схематично она выглядит так как показано на рисунке ниже.

Схема архитектуры трёх деревьев

Суть ее заключается в том, что дополнительно добавляется ещё одно место, которое можно назвать кэшем или stage в английской терминологии. Рабочая копия и репозиторий идейно не отличается от их аналогов в архитектуре двух деревьев. Наличие дополнительного элемента меняет регламент работы, которой в этом случае выглядит так:

  1. Перед началом работы разработчик делает checkout, для того чтобы быть уверенным, что он будет работать с актуальной рабочей копией.

  2. Разработчик вносит необходимые изменения в исходный код.

  3. Разработчик отправляет необходимый набор файлов, изменения в которые внесены, в stage, для того, чтобы потом построить из них коммит. До того как изменения будут отправлены в репозиторий, разработчик может добавлять и удалять файлы из stage. Набор файлов в stage, как правило, идеологически связан между собой.

  4. Разработчик отправляет изменения в репозиторий (коммитит их).

  5. Повторить необходимое количество раз пункты 2–4.

Наличие stage добавляет гибкости в процесс разработки, вы можете внести изменения в довольно большое количество файлов, но отправить их в репозиторий в разных коммитах со своими специфическими комментариями.

6. Создание репозитория и первый коммит

6.1. Создание репозитория

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

mkdir repo

Теперь перейдем в эту директорию.

cd repo

Создадим в нем пустой git-репозиторий.

git init

6.2. Создание первого коммита

Если посмотреть на список коммитов, которые были отправлены в репозиторий, то увидим, что он пустой – это правильно, т.к. пока только создан репозиторий и ничего ещё туда не отправлено.

git log
fatal: your current branch 'master' does not have any commits yet

Для просмотра состояния рабочей директории воспользуемся командой git status.

git status
On branch master

Initial commit

nothing to commit (create/copy files and use "git add" to track)

Создадим в директории пустой файл.

touch README.adoc

Теперь, если выполнить команду git status, то увидим, что в директории появился один не отслеживаемый файл: README.adoc.

git status
On branch master

Initial commit

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

        README.adoc

nothing added to commit but untracked files present (use "git add" to track)

Добавим, созданный файл в stage. Stage (или cache) – это хранилище для файлов с изменениями, информация о которых попадет в единый коммит. Stage является элементом архитектуры трех деревьев, на базе которой построен Git. Для добавления файла README.adoc в stage необходимо воспользоваться командой git add.

git add README.adoc

Если изменение было произведено в нескольких файлах, и мы хотим их все отправить в stage, то вместо имени файла необходимо поставить точку: git add ..

Выполним git status для того, чтобы посмотреть на то, что сейчас происходит в директории.

git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   README.adoc

Как видно, в stage был добавлен один файл с именем README.adoc и теперь представленный набор изменений готов к отправке в репозиторий – т.е. к коммиту. Сделаем коммит.

git commit -m "[create repository]"
[master (root-commit) 500067c] [create repository]
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README.adoc

Проверим статус директории.

git status
On branch master

nothing to commit, working tree clean

Как видно с момента последнего коммита никаких изменений в рабочей директории не производилось.

Теперь взглянем на список коммитов.

git log
commit 500067cc0b80643d38e2a24e9e0699031ada6be3
Author: Writer <writer@someserver.com>
Date:   Mon Feb 12 22:51:14 2018 +0500

    [create repository]

Из приведенной информации видно, что был отправлен один коммит, который имеет ID: 500067cc0b80643d38e2a24e9e0699031ada6be3. Автор данного коммита Writer, он (коммит) был создан Mon Feb 12 22:51:14 2018 +0500, с сообщением: [create repository]. Это довольно подробная информация, когда коммитов станет много, такой формат вывода будет не очень удобным, сокращенный вариант выглядит так.

git log --oneline
500067c [create repository]

6.3. Резюме

Подведем небольшое резюме вышесказанному.

Создание пустого репозитория.
git init
Добавление файлов в stage.
git add filename
Создание коммита.
git commit -m “message”
Просмотр статуса директории.
git status
Просмотр коммитов в репозитории.
git log
Просмотр коммитов в репозитории с сокращенным выводом информации.
git log --oneline

7. Просмотр информации по коммитам

Рассмотрим инструмент системы контроля версий Git, который позволяет делать выборку и представлять пользователю коммиты, отправленные в репозиторий, в соответствии с заданными параметрами.

Рабочий процесс с использованием Git, в упрощенном виде, выглядит следующим образом (пока не рассматривается работа с удаленным репозиторием):

  • Внесение изменений в рабочую директорию.

  • Отправка изменений в stage.

  • Формирование и отправка коммита на базе того, что лежит в stage, в репозиторий.

В процессе работы, в репозитории накопится больше количество коммитов и довольно часто будет возникать необходимость их просматривать. Git предоставляет удобный способ просмотра информации по коммитам. Для демонстрации возможностей Git, создадим репозиторий и добавим в него один файл – README.adoc.

Для просмотра информации по сделанным коммитам используется команда git log.

git log
commit a98cce47b59256d00a853c421af4f7b9f0dc0a29
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:10:51 2018 +0500

    [create repository]

Как видно из полученной информации, в репозиторий был отправлен один коммит с сообщением [create repository], этот коммит сделал пользователь с именем Writer, его email: writer@somecompany.com, уникальный идентификатор коммита a98cce47b59256d00a853c421af4f7b9f0dc0a29, и дата и время отправки коммита: Mon Mar 5 23:10:51 2018 +0500.

Внесем еще несколько изменений в репозитории. Добавим текст в файл README.adoc.

echo "Project 51" > README.adoc

Зафиксируем эти изменения в репозитории.

git add .
git commit -m "[add]: caption into README file"

Создадим файл main.c и добавим его в репозиторий.

touch main.c
git add .
git commit -m "[create]: main file of program"

Таким образом в репозитории уже должно быть три коммита, проверим это.

git log
commit 2b826bb4929fb1c8166ef05b540ce2cc68f3ebb2
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:17:08 2018 +0500

    [create]: main file of program

commit bc067c88c427dbedbb02817f9ae25241dcae4d07
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:15:12 2018 +0500

    [add]: caption into README file

commit a98cce47b59256d00a853c421af4f7b9f0dc0a29
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:10:51 2018 +0500

    [create repository]

Коммиты располагаются от новых к старым. Сделаем ещё несколько изменений.

touch main.h
git add .
git commit -m "[create]: header for main"
touch .gitignore
git add .
git commit -m "[create]: git ignore file"
echo "*.tmp" > .gitignore
git add .
git commit -m "[add] ignore .tmp files">

Снова получим список всех коммитов.

git log
commit cf3d9d8f7b283267a085986e85cc8f152cca420d
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:21:59 2018 +0500

    [add] ignore .tmp files

commit a7b88eed6110b6ebb1fc4d96f4399e4cbb8339e7
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:21:11 2018 +0500

    [create]: git ignore file

commit c185b80ca916af7d6f068450f6cafb073d955c40
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:20:26 2018 +0500

    [create]: header for main

commit 2b826bb4929fb1c8166ef05b540ce2cc68f3ebb2
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:17:08 2018 +0500

    [create]: main file of program

commit bc067c88c427dbedbb02817f9ae25241dcae4d07
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:15:12 2018 +0500

    [add]: caption into README file

commit a98cce47b59256d00a853c421af4f7b9f0dc0a29
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:10:51 2018 +0500

    [create repository]

Количество коммитов в репозитории уже такое, что просматривать информацию о них в том виде, в котором выдает git log уже неудобно. Для того, что бы сократить количество показываемой информации можно воспользоваться ключом -–oneline, при этом будет выведена часть идентификатора и сообщение коммита.

git log --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file
c185b80 [create]: header for main
2b826bb [create]: main file of program
bc067c8 [add]: caption into README file
a98cce4 [create repository]

В таком виде работать с коммитами уже намного удобнее. Если вы хотите просмотреть n последних коммитов, то укажите количество коммитов после ключа -n. Выведем три последних коммита.

git log -n 3 --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file
c185b80 [create]: header for main

Для вывода списка коммитов, начиная с какой-то временной метки, используйте ключ –since="<date> <time>". Например, получим все коммиты, сделанные после 5-го марта 2018 года 23:21.

git log --since="2018-03-05 23:21:00" --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file

Для вывода списка коммитов до какой-то даты используется ключ --until. Получим список коммитов, сделанных до 5-го марта 2018 года 23:21.

git log --until="2018-03-05 23:21:00" --oneline
c185b80 [create]: header for main
2b826bb [create]: main file of program
bc067c8 [add]: caption into README file
a98cce4 [create repository]

Еще одним полезным ключом является –-author, который позволяет вывести список коммитов, сделанных конкретным автором.

git log --author="Writer" --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file
c185b80 [create]: header for main
2b826bb [create]: main file of program
bc067c8 [add]: caption into README file
a98cce4 [create repository]

В приведенном выше примере, вывели все коммиты сделанные пользователем с именем Writer. Т.к. в репозитории все коммиты сделаны от имени данного автора, то при любых других именах, передаваемых параметру –author, мы будем получать пустой список.

7.1. Программа grep

В Linux есть программа grep – это утилита командной строки, которая, в переданном ей тексте, находит вхождения, соответствующие заданному регулярному выражению.

Выведем все коммиты, в которых встречается слово create.

git log --grep="create" --oneline
a7b88ee [create]: git ignore file
c185b80 [create]: header for main
2b826bb [create]: main file of program
a98cce4 [create repository]

Теперь коммиты со словом add.

git log --grep="add" --oneline
cf3d9d8 [add] ignore .tmp files
bc067c8 [add]: caption into README file

Для более продуктивного использования данной команды рекомендуем ознакомиться с возможностями утилиты grep.

8. HEAD и tree-ish

8.1. HEAD

HEAD – это указатель, задача которого ссылаться на определенный коммит в репозитории. Суть данного указателя можно попытаться объяснить с разных сторон.

Во-первых, HEAD – это указатель на коммит в репозитории, который станет родителем следующего коммита. Для того, что бы лучше понять это, обратимся к репозиторию, в котором сделано шесть коммитов, посмотрим на них.

git log --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file
c185b80 [create]: header for main
2b826bb [create]: main file of programm
bc067c8 [add]: caption into README file
a98cce4 [create repository]

Эти коммиты создавались в порядке от самого нижнего (a98cce4) к самому верхнему (cf3d9d8). Каждый раз, когда отправлялся новый коммит в репозиторий, HEAD смещался и указывал на него. Посмотрим на изображение ниже: на нем показана ситуация, когда были отправлены три первых коммита.

HEAD in commits

После того как был отправлен коммит с id 2b826bb, указатель HEAD стал показывать на него, т.е. данный коммит будет родителем для следующего, и когда будет сделан еще один коммит, HEAD сместится.

HEAD offset in commits

Во-вторых, HEAD указывает на коммит, относительного которого будет создана рабочая копия во-время операции checkout. Другими словами, когда идет переключение с ветки на ветку, используя операцию checkout, то в репозитории указатель HEAD будет переключаться между последними коммитами выбираемых ветвей.

В репозитории пока только одна ветвь – master, но и этого будет достаточно, чтобы показать зависимость между положением указатель HEAD и операцией checkout.

Текущее состояние репозитория выглядит так, как показано на рисунке ниже.

HEAD in repository

Для того, что бы скопировать снимок репозитория относительно последнего коммита ветки master, т.е. того на который указывает HEAD, необходимо выполнить следующую команду.

git checkout master
Switched to branch 'master'

Содержимое репозитория, в данном случае, выглядит так.

git log --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file
c185b80 [create]: header for main
2b826bb [create]: main file of programm
bc067c8 [add]: caption into README file
a98cce4 [create repository]

Теперь передвинем указатель HEAD на коммит с id 2b826bb.

Move HEAD back

Для этого передадим команде checkout идентификатор коммита.

git checkout 2b826bb
Note: checking out '2b826bb'.

You are in 'detached HEAD' state. You can look around, make experimentalchanges and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 2b826bb... [create]: main file of programm

Обратим внимание на текст, который напечатал Git, после того, как была выполнена эта команда. Интересна самая последняя строка HEAD is now at 2b826bb…, теперь HEAD указывает на коммит с id 2b826bb – именно то, что нужно.

Посмотрим на текущий список коммитов.

git log --oneline
2b826bb [create]: main file of programm
bc067c8 [add]: caption into README file
a98cce4 [create repository]

Git выводит коммиты сделанные до того коммита, на который ссылается HEAD.

Вернем HEAD на прежнее место.

git checkout cf3d9d8
Previous HEAD position was 2b826bb... [create]: main file of programm
HEAD is now at cf3d9d8... [add] ignore .tmp files
git log --oneline
cf3d9d8 [add] ignore .tmp files
a7b88ee [create]: git ignore file
c185b80 [create]: header for main
2b826bb [create]: main file of programm
bc067c8 [add]: caption into README file
a98cce4 [create repository]

Все вернулось на прежнее место. Таким образом, можно получать в виде рабочей копии содержимое репозитория на момент отправки того или иного коммита. Перейдем в директорию .git, в которой находится репозиторий, он расположен в корневой директории нашего проекта, и посмотрим его содержимое.

cd .git
ls -la
total 21
drwxr-xr-x 1 User 197121   0 мар 18 17:10 ./
drwxr-xr-x 1 User 197121   0 мар 18 17:10 ../
-rw-r--r-- 1 User 197121  24 мар  5 23:21 COMMIT_EDITMSG
-rw-r--r-- 1 User 197121 184 мар  5 23:10 config
-rw-r--r-- 1 User 197121  73 мар  5 23:10 description
-rw-r--r-- 1 User 197121  41 мар 18 17:10 HEAD
drwxr-xr-x 1 User 197121   0 мар  5 23:10 hooks/
-rw-r--r-- 1 User 197121 441 мар 18 17:10 index
drwxr-xr-x 1 User 197121   0 мар  5 23:10 info/
drwxr-xr-x 1 User 197121   0 мар  5 23:10 logs/
drwxr-xr-x 1 User 197121   0 мар  5 23:21 objects/
drwxr-xr-x 1 User 197121   0 мар  5 23:10 refs/

В данном каталоге содержится файл HEAD, в нем находится идентификатор, на который ссылается данный указатель. Посмотрим содержимое файла HEAD.

cat HEAD
cf3d9d8f7b283267a085986e85cc8f152cca420d

HEAD указывает на коммит cf3d9d8.

8.2. Tree-ish

Понятие tree-ish часто используется в документации по Git. Tree-ish – это то, что указывает на коммит, эту сущность мы можем передавать в качестве аргумента для команд Git. Вот список того, чем может являться tree-ish.

Table 1. Tree-ish examples
Tree-ish Examples

<sha1>

dae86e1950b1277e545cee180551750029cfe735

<describeOutput>

v1.7.4.2-679-g3bee7fb

<refname>

master, heads/master, refs/heads/master

<refname>@{<date>}

master@{yesterday}, HEAD@{5 minutes ago}

<refname>@{<n>}

master@{1}

@{<n>}

@{1}

@{-<n>}

@{-1}

<refname>@{upstream}

master@{upstream}, @{u}

<rev>^

HEAD^, v1.5.1^0

<rev>~<n>

master~3

<rev>^{<type>}

v0.99.8^{commit}

<rev>^{}

v0.99.8^{}

<rev>^{/<text>}

HEAD^{/fix nasty bug}

:/<text>

:/fix nasty bug

<rev>:<path>

HEAD:README.txt, master:sub-directory/

Рассмотрим работу с tree-ish на примере команды git show.

git show cf3d9d8f -q
commit cf3d9d8f7b283267a085986e85cc8f152cca420d
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:21:59 2018 +0500

    [add] ignore .tmp files
git show -q HEAD
commit cf3d9d8f7b283267a085986e85cc8f152cca420d
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:21:59 2018 +0500

    [add] ignore .tmp files
git show -q master
commit cf3d9d8f7b283267a085986e85cc8f152cca420d
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:21:59 2018 +0500

    [add] ignore .tmp files
git show -q @{5}
commit cf3d9d8f7b283267a085986e85cc8f152cca420d
Author: Writer <writer@somecompany.com>
Date:   Mon Mar 5 23:21:59 2018 +0500

    [add] ignore .tmp files

Во всех примерах, представленных выше, команде git show передаются различные tree-ish, которые на самом деле указывают на одно и то же место – последний коммит.

9. Добавление, удаление и переименование файлов в репозитории

9.1. Добавление файлов в git-репозиторий

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

git init

Создадим в директории файл README.adoc любым удобным способом, например с помощью команды touch.

touch README.adoc

Теперь проверим состояние отслеживаемой директории.

git status
On branch master

Initial commit

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

        README.adoc

nothing added to commit but untracked files present (use "git add" to track)

В рабочей директории есть один не отслеживаемый файл README.adoc. Git подсказывает, что нужно сделать для того, чтобы начать отслеживать изменения в файле README.adoc: необходимо выполнить команду git add, сделаем это.

git add README.adoc

Посмотрим ещё раз на состояние.

git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   README.adoc

Видно, что информация о появлении нового файла попала в stage. Для того чтобы это изменение зафиксировалось в репозитории необходимо выполнить команду git commit.

git commit -m "add README.adoc file"
[master (root-commit) 0bb6c94] add README.adoc file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.adoc

Теперь в рабочей директории и в stage нет объектов, информацию об изменении которых необходимо внести в репозиторий.

git status
On branch master
nothing to commit, working tree clean

В репозиторий был сделан один коммит.

git log --oneline
0bb6c94 add README.adoc file

9.2. Удаление файлов из git репозитория и stage

9.2.1. Удаление файла из stage

Вначале разберемся со stage. Создадим ещё один файл.

touch main.c

Отправим файл main.c в stage.

git add main.c

Внесем изменения в README.adoc.

echo "# README" > README.adoc

Информацию об этом также отправим в stage.

git add README.adoc

Посмотрим на состояние stage.

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

        modified:   README.adoc
        new file:   main.c

Если необходимо убрать из stage, какой-то из этих файлов (main.c или README.adoc), то для этого можно воспользоваться командой git –rm cashed scripted <filename>, сделаем это для файла main.c.

git rm --cached main.c
rm 'main.c'

Теперь посмотрим на состояние рабочей директории и stage.

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

        modified:   README.adoc

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

        main.c

Видно, что изменения в файле README.adoc готовы для коммита, а вот файл main.c перешел в состояние – не отслеживаемый. Отправим main.c в stage и, после этого, сделаем коммит в репозиторий.

git add main.c
git commit -m "add main.c and do some changes in README.adoc"
[master 49049bc] add main.c and do some changes in README.adoc
 2 files changed, 1 insertion(+)
 create mode 100644 main.c

9.2.2. Удаление файлов из git репозитория

Удалить файл из репозитория можно двумя способами:

  • удалить его из рабочей директории и уведомить об этом Git

  • воспользоваться средствами Git

Первый способ

Для начала посмотрим, какие файлы у нас хранятся в репозитории.

git ls-tree master
100644 blob 7e59600739c96546163833214c36459e324bad0a    README.adoc
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    main.c

Удалим файл main.c из рабочей директории.

rm main.c
ls
README.adoc

Уведомим об этом систему Git.

git rm main.c
rm 'main.c'

Вместо команды git rm можно использовать git add, но само слово add в данном случае будет звучать несколько неоднозначно, поэтому лучше использовать rm. На данном этапе еще можно вернуть все назад с помощью команды git checkout — <filename>, в результате, в рабочую директорию будет скопирован файл из репозитория. Создадим коммит, фиксирующий удаление файла.

git commit -m "remove main.c"
[master d4e22ae] remove main.c
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 main.c

Теперь в репозитории остался только один файл README.adoc.

git ls-tree master
100644 blob 7e59600739c96546163833214c36459e324bad0a    README.adoc
Второй способ

Сразу использовать команду git rm без предварительного удаления файла из директории. Вновь создадим файл main.c и добавим его в репозиторий.

touch main.c
git add main.c
git commit -m "add main.c file"
[master 6d93049] add main.c file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 main.c
git ls-tree master
100644 blob 7e59600739c96546163833214c36459e324bad0a    README.adoc
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    main.c

Удалим файл из репозитория.

git rm main.c
rm 'main.c'
git commit -m "deleted: main.c file"
[master ba7d027] deleted: main.c file
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 main.c

Файла main.c больше нет в репозитории.

git ls-tree master
100644 blob 7e59600739c96546163833214c36459e324bad0a    README.adoc

Его также нет и в рабочем каталоге.

ls
README.adoc

9.3. Переименование файлов в git репозитории

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

9.3.1. Первый способ

Создадим файл test_main_file.c и добавим его в репозиторий.

touch test_main_file.c
git add test_main_file.c
git commit -m "add test_main_file.c"
[master 6cf53ac] add test_main_file.c
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test_main_file.c

Содержимое репозитория после этого будет выглядеть так.

git ls-tree master
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    test_main_file.c

Переименуем его на test_main.c.

Сделаем это в рабочей директории.

mv test_main_file.c test_main.c

Теперь отправим изменение в репозиторий.

git add .
git commit -m "Rename test_main_file.c"
[master 79528c4] Rename test_main_file.c
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename test_main_file.c => test_main.c (100%)

В репозитории и в рабочей директории будет находиться только файл test_main.c.

git ls-tree master
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    test_main.c
ls
test_main.c

9.3.2. Второй способ

В рамках второго способа рассмотрим работу с командой git mv. Переименуем файл test_main.c в main.c. Текущее содержимое репозитория и рабочего каталога.

git ls-tree master
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    test_main.c
ls
test_main.c

Переименуем файл test_main.c на main.c средствами Git.

git mv test_main.c main.c
git commit -m "Rename test_main.c file"
[master c566f0e] Rename test_main.c file
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename test_main.c => main.c (100%)

Имя файла изменилось как в репозитории, так и в рабочем каталоге.

git ls-tree master
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    main.c
ls
main.c

10. Как удалить коммит в git?

10.1. Отмена изменений в файлах в рабочей директории

Если вы сделали какие-то изменения в файле и хотите вернуть предыдущий вариант, то для этого следует обратиться к репозиторию и взять из него файл, с которым вы работаете. Таким образом, в вашу рабочую директорию будет скопирован файл из репозитория с заменой. Например, вы работаете с файлом main.c и внесли в него какие-то изменения. Для того чтобы вернуться к предыдущей версии (последней отправленной в репозиторий) воспользуйтесь командой git checkout.

git checkout -- main.c

Ключ -- означает, что нас интересует файл в текущей ветке.

10.2. Отмена коммитов в git

10.2.1. Работа с последним коммитом

Для демонстрации возможностей Git создадим новую директорию и инициализируем в ней репозиторий.

git init

Добавим в каталог файл main.c.

touch main.c

Отправим изменения в репозиторий.

git add main.c
git commit -m "first commit"
[master (root-commit) 86f1495] first commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 main.c

Внесем изменения в файл.

echo "// main.c file"
main.c

И сделаем еще один коммит.

git add main.c
git commit -m "second commit"
[master d142679] second commit
1 file changed, 1 insertion(+)

В репозиторий, на данный момент, было сделано два коммита.

git log --oneline
d142679 second commit
86f1495 first commit

Теперь удалим последний коммит и вместо него отправим другой. Предварительно изменим содержимое файла main.c.

echo "// author: Writer" > main.c

Отправим изменения в репозиторий с заметой последнего коммита.

git add main.c
git commit --amend -m "third commit"
git log --oneline
18411fd third commit
86f1495 first commit

Из репозитория пропал коммит с id d142679, вместо него теперь коммит с id 18411fd.

10.2.2. Отмена изменений в файле в выбранном коммите

Сделаем ещё несколько изменений в нашем файле main.c, каждое из которых будет фиксироваться коммитом в репозиторий.

echo "// Some text 1" > main.c
----
git add main.c
git commit -m "fourth commit"
[master dcf7253 ] fourth commit
1 file changed, 1 insertion(+), 1 deletion(-)
echo "// Some text 2" > main.c
git add main.c
git commit -m "fifth commit"
[master 7f2eb3a ] fifth commit
1 file changed, 1 insertion(+), 1 deletion(-)
git log --oneline
7f2eb3a fifth commit
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Представим ситуацию, что два последних коммита были неправильными, и нам нужно вернуться к версии 18411fd и внести изменения именно в нее. В примере, идет работа только с одним файлом, но в реальном проекте файлов будет много, и после коммитов, в рамках которых вносились изменения в какой-то файл, может быть ещё довольно много коммитов, фиксирующих изменения в других файлах. Просто так взять и удалить коммиты из середины ветки не получится – это нарушит связность, что идет в разрез с идеологией Git. Одни из возможных вариантов – это получить версию файла из нужного коммита, внести в него изменения и сделать новый коммит. Для начала посмотрим на содержимое файла main.c из последнего, на текущий момент, коммита.

git checkout main.c
cat main.c
// Some text 2

Для просмотра содержимого файла в коммите с id 18411fd воспользуемся правилами работы с tree-ish:

git show 18411fd:main.c
// author: Writer

Переместим в рабочую директорию файл main.c из репозитория с коммитом id 18411fd.

git checkout 18411fd -- main.c
cat main.c
// author: Writer

Теперь содержимое файла main.c соответствует тому, что было на момент создания коммита с id 18411fd. Сделаем коммит в репозиторий и в сообщении укажем, что он отменяет два предыдущих коммита.

git add main.c
git commit -m "return main.c from third commit"
[master cffc5ad] return main.c from third commit
1 file changed, 1 insertion(+), 1 deletion(-)
git log --oneline
cffc5ad return main.c from third commit
7f2eb3a fifth commit
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Таким образом вернулись к предыдущей версии файла main.c и при этом сохранили всю историю изменений.

10.2.3. Использование git revert для быстрой отмены изменений

Рассмотрим ещё одни способ отмены коммитов, на этот раз воспользуемся командой git revert.

Например, отменим коммит с id cffc5ad. После того будет введена команда git revert, система Git выдаст сообщение в текстовом редакторе, если содержимое открытого файла корректно, то просто сохраняем его и закрываем. В результате изменения будут применены, и автоматически сформируется и отправится в репозиторий коммит.

git revert cffc5ad
[master 81499da] Revert "return main.c from third commit"
1 file changed, 1 insertion(+), 1 deletion(-)

Если есть желание поменять редактор, то можно воспользоваться командой.

git config core.editor "notepad.exe"

Обратите внимание, что в этом случае будут изменены настройки для текущего репозитория.

Проверим, применялась ли настройка.

git config core.editor notepad.exe

Посмотрим на список коммитов в репозитории.

git log --oneline
81499da Revert "return main.c from third commit"
cffc5ad return main.c from third commit
7f2eb3a fifth commit
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Содержимое файла вернулось к тому, что было сделано в рамках коммита с id 7f2eb3a.

cat main.c
// Some text 2
git show 7f2eb3a:main.c
// Some text 2

10.3. Отмена группы коммитов

Warning
Используйте эту команду очень аккуратно!

Существует три опции, которые можно использовать с командой git reset для изменения положения HEAD и управления состоянием stage и рабочей директории.

10.3.1. Удаление коммитов из репозитория (без изменения рабочей директории) (ключ --soft)

Для изменения положения указателя HEAD в репозитории, без оказания влияния рабочую директорию (в stage, при этом, будет зафиксировано отличие рабочей директории от репозитория), используется ключ --soft. Посмотрим на репозиторий.

git log --oneline
81499da Revert "return main.c from third commit"
cffc5ad return main.c from third commit
7f2eb3a fifth commit
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Содержимое файла main.с в рабочей директории.

cat main.c
// Some text 2

Содержимое файла main.с в репозитории.

git show HEAD:main.c
// Some text 2

Теперь переместим HEAD в репозитории на коммит с id dcf7253.

git reset --soft dcf7253

Получим следующий список коммитов.

git log --oneline
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Содержимое файла main.c в репозитории выглядит так.

git show HEAD:main.c
// Some text 1

В рабочей директории файл main.c остался прежним (эти изменения отправлены в stage).

cat main.c
// Some text 2

Для того, что бы зафиксировать в репозитории последнее состояние файла main.c сделаем коммит.

git commit -m "soft reset example"
[master db1a8b0] soft reset example
1 file changed, 1 insertion(+), 1 deletion(-)

Посмотрим на список коммитов.

git log --oneline
db1a8b0 soft reset example
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Как видите из репозитория пропали следующие коммиты:

81499da Revert "return main.c from third commit"
cffc5ad return main.c from third commit
7f2eb3a fifth commit

10.3.2. Удаление коммитов из репозитория и очистка stage (без изменения рабочей директории) (ключ --mixed)

Если использовать команду git reset с аргументом --mixed, то в репозитории указатель HEAD переместится на нужный коммит, а также будет сброшено содержимое stage. Отменим последний коммит.

git reset --mixed dcf7253
Unstaged changes after reset:
M       main.c

В результате изменилось содержимое репозитория.

git log --oneline
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Содержимое файла main.c в последнем коммите выглядит так.

git show HEAD:main.c
// Some text 1

Файл main.c в рабочей директории не изменился.

cat main.c
// Some text 2

Отправим изменения вначале в stage, а потом в репозиторий.

git add main.c
git commit -m "mixed reset example"
[master ab4ef00] mixed reset example
1 file changed, 1 insertion(+), 1 deletion(-)

10.3.3. Удаление коммитов из репозитория, очистка stage и внесение изменений в рабочую директорию (ключ --hard)

Если вы воспользуетесь ключом --hard, то обратного пути уже не будет. Вы не сможете восстановить данные из рабочей директории. Все компоненты Git (репозиторий, stage и рабочая директория) будут приведены к одному виду в соответствии с коммитом, на который будет перенесен указатель HEAD.

Текущее содержимое репозитория выглядит так.

git log --oneline
ab4ef00 mixed reset example
dcf7253 fourth commit
18411fd third commit
86f1495 first commit

Посмотрим на содержимое файла main.c в каталоге и репозитории.

cat main.c
// Some text 2
git show HEAD:main.c
// Some text 2

Содержимое файлов идентично.

Удалим все коммиты до самого первого с id 86f1495.

git reset --hard 86f1495
HEAD is now at 86f1495 first commit

Содержимое репозитория.

git log --oneline
86f1495 first commit

Состояние рабочей директории и stage.

git status
On branch master
nothing to commit, working tree clean

Содержимое файла main.c в репозитории и в рабочей директории.

cat main.c
git show HEAD:main.c

Файл main.c пуст.

Т.к. использовалась команда git reset с ключом --hard, то восстановить прежнее состояние не получится.

11. Лучшие практики

11.1. Работа с remote repository

Git Diagram

Git basic operation

11.1.1. GitHub process

Git and GitHub Version Control

11.2. Именование git-репозиториев.

На Git-hosting каждому репозиторию нужно имя, поэтому, естественно, он должен соответствовать следующим критериям:

  • Описательный, т.е. однозначно описывать содержимое репозитория

  • Удобочитаемый

Для многих проектов, которые используют Git, по их названию нельзя ничего сказать о них. Так может есть стандарт, которому нужно следовать? Ответ: нет. Поэтому следует придерживаться здравого смысла, а если быть точнее, название репозитория зависит от проекта и языка программирование, который используется для него.

Если проект предназначен для веб-сайтов, то можно использовать domain name как название проекта. Например:

Если проект разрабатывается на Java, то лучше использовать имя конечного artifact (.war, .jar).

По умолчанию лучше использовать:

  • строчные буквы

  • дефисы (как разделители)

the-fellowship-of-the-ring
the-two-towers
the-return-of-the-king

11.3. Клонирование git-repositories

  • Клонирование repository в директорию с таким же названием как Git project:

git clone git@github.com:username/project-name.git
  • Клонирование repository в директорию с названием project:

git clone git@github.com:username/project-name.git project
  • Клонирование repository в текущую директорию (она должна быть пуста):

git clone git@github.com:username/project-name.git .

11.4. Диаграмма процесса работы с Git

11.4.1. Precondition

  • Все команды кроме git clone выполняются в terminal/cmd в directory с проектом

  • Команда git clone выполняется в directory dev, которая должна быть создана в пользовательской directory

  • Команды для работы в cmd:

    • cd .. - изменить directory на directory выше

    • cd dir_name - изменить directory на directory с именем dir_name - directory в которую необходимо перейти и которая находится в текущей directory

    • c: - перейти на диск c

11.4.2. Диаграмма

Git basic operation

11.4.3. Q&A

  1. Как копировать remote repository когда его нет на компьютере?

    Необходимо делать единожды: git clone https://github.com/GITHUB_USERNAME/GITHUB_REPOSITORY.git, где https://github.com/GITHUB_USERNAME/GITHUB_REPOSITORY.git - ссылка на НУЖНЫЙ репозиторий

  2. Как сконфигурировать user.name в git?

    Необходимо делать единожды: git config --global user.name 'YOUR FULLNAME', где YOUR FULLNAME - ваше имя и фамилия

  3. Как сконфигурировать user.email в git?

    Необходимо делать единожды: git config --global user.email 'YOUR_EMAIL', где YOUR_EMAIL - ваш email

  4. Как посмотреть состояние working directory?

    git status

  5. Как посмотреть commits для local repository?

    git log

  6. Как переключать branch?

    git checkout BRANCH_NAME, где BRANCH_NAME - имя branch в который необходимо переключиться

  7. Как добавить файлы из working directory в staging area для предстоящего commit?

    git add ., где . - все файлы, можно также использовать имя файла, для добавления только файла

  8. Как добавить файлы из staging area в local repository с определенным заголовком?

    git commit -m "YOUR_COMMIT_MEASSAGE", где YOUR_COMMIT_MEASSAGE - залоговок который НУЖНО использовать

  9. Как отправить изменения из local repository в remote repository?

    git push GITHUB_REPOSITORY, где GITHUB_REPOSITORY - краткое имя remote repository

  10. Как узнать о произошедших изменениях в remote repository?

    git fetch GITHUB_REPOSITORY, где GITHUB_REPOSITORY - краткое имя remote repository

  11. Как обновить текущий local repository, если произошли изменения в remote repository?

    git pull GITHUB_REPOSITORY, где GITHUB_REPOSITORY - краткое имя remote repository

  12. Как добавить дополнительный remote repository?

    git remote add GITHUB_USERNAME https://github.com/GITHUB_USERNAME/GITHUB_REPOSITORY.git, где https://github.com/GITHUB_USERNAME/GITHUB_REPOSITORY.git - ссылка на remote repository