Как стать автором
Обновить

Достаточно Git-а, чтобы быть (менее) опасным

Время на прочтение23 мин
Количество просмотров131K
imageТы просто-напросто ненавидишь Git? Ты абсолютно счастлив с Mercurial (или, фу, с Subversion), но раз в месяц тебе приходится отважно сталкиваться с Git, потому что каждый, даже его чертова собака, теперь использует GitHub? Тебя терзают смутные подозрения, что половина всех команд Git на самом деле удалят всю твою работу навсегда, но ты не знаешь какие именно и не хочешь проводить три недели, углубляясь в документацию?

Хорошие новости! Я написал тебе этот изумительный Интернет-пост. Я надеюсь, что смогу размазать достаточно Git-а по твоему лицу, чтобы понизить вероятность сделать что-то непоправимое, а так же уменьшить твой страх что-то сломать. Этого должно быть также достаточно, чтобы сделать документацию Git немного более понятной; она крайне тщательно и глубоко проработана и очень глупо, если ты все еще не прочитал половину.

Я постараюсь излагать коротко, но также, чтобы это было потенциально полезно тем людям, кто вообще никогда не сталкивался с контролем версий, поэтому повсюду будет разбросан 101 совет. Не бойся! Я не думаю, что пользователи Mercurial понятия не имеют, что такое патч.

Что здесь вообще происходит?


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

Git — это DVCS, или «распределенная система контроля версий», где «система контроля версий» означает «запоминает историю твоих файлов», а «распределенная» означает «ты можешь делать это оффлайн». (Раньше фиксирование изменений вызывало немедленную загрузку твоих изменений на центральный сервер. Ты не мог сохранить код, если не мог подключиться к серверу. Верно?)

Git был изобретен Линусом Торвальдсом, злобным мужиком, который также принес нам ядро Linux. Linux — это огромный проект с очень длинной историей и он перерос VCS, которую использовал, поэтому Линус решил написать новую, т.к. он программист, а именно этим мы и занимаемся.

Процесс разработки Linux — это такой процесс, который ты бы вполне мог ожидать от группы людей, работающих над ядром Linux: они рассылают патчи по почте. (Патч — это всего лишь текстовый файл, который перечисляет изменения между двумя версиями каких-то файлов. Ты можешь применить патч к старой версии, чтобы получить новую.)

Итак, у Линуса есть «каноничная» копия кодовой базы, которую ты можешь называть «репозиторий», потому что это место для хранения разных вещей. Время от времени ты можешь скачивать свежую копию и приступать к написанию каких-нибудь сломанных Wi-Fi-драйверов или еще чего-нибудь, и это уже будет отличаться от того, с чего ты начал. Поэтому ты генерируешь патч с изменениями и отправляешь его в почтовую рассылку, а кто-то говорит «по-моему неплохо» и Линус применяет этот патч к его копии кодовой базы. Теперь каждый, кто собирается работать с этим кодом, увидит там и твою работу тоже.

Великий секрет к пониманию Git, который, я надеюсь, заставит широко раскрыться твои глаза и прозвучать «ааа» из твоего рта, заключается в следующем:

Git — это просто набор инструментов для рассылки патчей по почте.

Нет, серьезно. Есть всего где-то пять команд внутри поставки Git для этих определенных целей. Есть даже подразделы в документации: am, apply, format-patch, send-email, request-pull. Ты можешь прямо сейчас пойти в почтовую рассылку ядра Linux и увидеть, что до сих пор все так и делается, Git просто делает большую часть скучной работы. Даже man-страница Git описывает Git, как «тупой трекер содержимого».

Git — это коллекция специализированных инструментов для решения конкретных проблем, на самом деле не являющихся теми проблемами, которые тебе нужно решать.

Давай будем рассматривать модель Git, держа это в голове.

Коммиты


Коммит — это патч. Все. Он перечисляет некоторые изменения в некоторых файлах в формате «единого diff-а».

В нем также есть некоторые заголовки, которые подозрительно напоминают почтовые. Там есть автор, отметка времени и прочее.

Здесь и происходит вся магия. Запомни, патч выражает различия между двумя наборами файлов. (Давай назовем их «деревьями» — по аналогии с деревьями каталогов.) Итак, если ты отправишь мне патч по почте, я не многое смогу с ним сделать, пока мы не согласуем к чему я должен применить патч. Может будет полезно указать, скажем, «примени этот патч к ядру Linux». Может будет даже более полезно указать «примени этот патч к релизу 3.0.5 ядра Linux».

Git-коммит кодирует это в заголовке «parent», указывая, поверх какого коммита его нужно применить.

Эй, подожди-ка, ведь коммит — это всего лишь патч. Как применить патч к другому патчу? Ты можешь применить патч только к полному набору файлов (дереву). Но после этого ты получаешь новый набор файлов (дерево). Поэтому «коммит» также означает «состояние репозитория после применения этого патча».

Но небольшая рекурсивная проблема все еще остается. Если у тебя есть коммит C и он говорит, что его родитель — B… что ж, ты не знаешь как выглядит состояние репозитория в B, пока не применишь его, и поэтому тебе нужно смотреть на его родителя, верно?

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

  • Коммит C: Мой родитель — B. Добавь «three» в конец файла «numbers.txt».
  • Коммит B: Мой родитель — A. Добавь «two» в конец файла «numbers.txt».
  • Коммит A: Создай файл «numbers.txt», содержащий «one».

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

Итак, ты начинаешь с чистого листа. Затем ты применяешь патч A, который дает тебе one. Затем ты можешь применить патч B, который дает тебе one two. И наконец, ты можешь применить патч C, который дает тебе one two three — состояние кодовой базы для коммита C. (Git не проделывает это буквально каждый раз, конечно; там достаточно хитрое кэширование и всякое-такое. Но модель действует достаточно схожим образом.)

Документация Git стремится изображать историю слева направо, поэтому описанное выше выглядело бы так:

A---B---C

Та же идея, только написанная по-другому. Имеет немного больше смысла, если ты представишь стрелки: A → B → C.

В реальности коммиты обозначаются не буквами, а хэшами, которые выглядят как 8edc525cd8dc9e81b5fbeb297c44dd513ba5518e, но обычно сокращаются до 8edc52. Ты можешь подумать, что они называются «хэшами», потому что это длинные шестнадцатеричные строки, которые выглядят как хэши SHA-1. В общем, да, но также они буквально являются SHA-1-хэшами патча, включая заголовки. (И т.к. родитель — это один из заголовков, то хэш включает хэш родителя, который включает хэш его родителя и т.д. Это длинная цепочка хэшей до самого начала. Прямо как в Bitcoin!)

Замечательное свойство такого хэширования в том, что отдельный коммит не может быть изменен. Ты не можешь просто вернуться назад и по тихому вбить строчку в патч A, потому что это изменит его хэш и B не будет указывать на измененный A. Если ты захочешь обновить родителя B, то это изменит его хэш и он потеряется. Как только у тебя будет хэш коммита, можешь быть абсолютно уверен, что его история неизменна.

Деревья


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

Тебе очень редко придется беспокоиться о деревьях — после многих лет использования Git я думал о деревьях примерно дважды. Это всего лишь деталь реализации. Я заметил их только потому, что документация обратила на них мое внимание, и это приятно, когда ты понимаешь о чем, черт возьми, тебе рассказывает документация. Они появляются в двух (практических) местах документации.

  1. Некоторые команды описаны, как принимающие «древовидный» аргумент, например, использование git checkout для работы с отдельными файлами. Это всего лишь означает «что-то, из чего Git может извлечь дерево». Т.к. у каждого коммита есть дерево, ты можешь просто использовать коммит в качестве аргумента.
  2. Существует множество ссылок на «рабочее дерево». Это просто дерево, в котором ты работаешь, т.е. актуальная копия кодовой базы, которая расположена у тебя на винте.

И это все, что тебе нужно знать о деревьях!

Ветки


Если ты использовал Mercurial, забудь о ветках Mercurial. Я не знаю как они работают, но пользователи Mercurial рассказывали мне, что это такая боль в заднице, что никто на самом деле их больше не использует.

Вместо этого представь себе ситуацию от лица разработчиков нашего Wi-Fi-драйвера. Они хотят изменить код ядра, чтобы добавить свой драйвер, но чтобы создать патч после того, как они закончат, им также необходима первоначальная копия кода. Патч перечисляет различия, поэтому необходимы две вещи, чтобы сделать патч.

Что ж, нет проблем. Когда они впервые скачивают код, они могут привязать его к директории под названием master (потому что это мастер-копия). Затем, когда они приступают к работе над своим драйвером, они могут все это полностью скопировать в директорию под названием ужасный-драйвер-broadcom. Чтобы сгенерировать патч, им нужно просто получить разницу между этими двумя директориями.

Это и есть ветки Git в двух словах.

Заметь, что при таком подходе, никто не знает и никого не волнуют имена твоих веток. В конце концов, ты не отправляешь кому-то другому целую директорию; ты просто отправляешь им некоторые патчи. Патчи не содержат имена веток; они только знают своих родителей.

Более технически, ветка — это только имя, которое указывает на какой-то коммит. (Буквально, ничего более. Ветка foo — это 41-байтный текстовый файл, содержащий хэш коммита.) Однако, ветка имеет особое свойство, при котором, если ты делаешь новый коммит, пока находишься в данной ветке, имя ветки начнет указывать на этот новый коммит. Еще раз, это работает как учебный пример: если ты делаешь какую-то работу или применяешь патч в своей директории ужасный-драйвер-broadcom, очевидно, что новое содержимое директории будет отражать новые изменения.

Вот почему о Git говорят, что у него «дешевое локальное ветвление». Оно дешевое, потому что ветка — это не более, чем имя; оно локальное, потому что тебя не заставляют синхронизировать имена твоих веток с кем-то еще.

Ветки добавляют новую возможность в нашу модель: теперь истории не обязательно быть линейной. Два разных патча могут иметь одного и того же родителя! Разумеется, тебе в действительности не нужны для этого ветки — если два человека работают с ядром Linux и оба делают изменения, они оба производят патчи с одинаковым родителем. Говоря о котором…

Удаленные репозитории


«Удаленный» означает «где-то еще», поэтому естественно, что удаленный репозиторий Git — это просто репозиторий, который находится где-то еще. Обычно это центральный сервер, но не обязательно; можно вообще работать без центрального сервера, когда разные разработчики просто перечисляют друг-друга в качестве удаленных репозиториев.

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

Ты также получишь все ветки оригинала. Ну. Вроде того. Имена веток — локальны, запомни. Если у твоего оригинала есть ветка с именем foo, Git создаст для тебя ветку с именем origin/foo (называемую «удаленно-отслеживаемой» веткой). А т.к. ты ее не создавал, то по-умолчанию она не отображается в git branch.

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

Слияние


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

Так что, теперь у тебя: A → B → C

А у Линуса: A → B → D → E → F

Или, если рисовать в духе документации Git, где время течет слева направо:

      C            ужасный-драйвер-broadcom
     /
A---B---D---E---F  origin/master

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

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

Вместо этого, Git может просто слить эти два разных отображения истории вместе, создав новый коммит с двумя родителями: C и F.

      C-----------.    ужасный-драйвер-broadcom
     /             \
A---B---D---E---F---G  origin/master

Если никакие изменения с любой стороны не противоречат друг-другу, то это «простое» слияние. Т.к. ничего нового на самом деле не изменилось, то патч в G — пустой; он присутствует только для склейки C и F вместе.

Если обе стороны изменили одинаковые части одинаковых файлов разным образом, ты получаешь конфликт слияния и должен указать, какая сторона побеждает. Любое изменение, которое ты делаешь, затем становится частью патча в G.

Тэги


Тэги — это имена для коммитов, довольно похожие на ветки. Однако, тэги предназначены быть постоянными: они в основном используются для отмечания версий релизов. Ты можешь переходить по тэгу, но тэг не может быть твоей «текущей веткой», и тэг никогда не появится автоматически, если ты делаешь новый коммит.

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



Понятненько. Это круто. Но как мне сделать хоть что-то?


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

Добудь немного кода


git clone github.com/funny_guy/just_for_lulz_code вывалит смешной код этого весельчака в новую директорию just_for_lulz_code.

Когда ты захочешь обновить его, ты можешь вызвать команду git pull origin master, которая получит все изменения и попытается слить их в твою текущую ветку. Если ты ничего не менял, то твое рабочее состояние просто перейдет в актуальное.

Если у тебя устаревшее рабочее состояние репозитория и ты не помнишь делал ли ты что-либо, ты можешь выполнить команду git pull --ff-only origin master, которая сделает что-либо только в том случае, если обновление будет «прямой перемоткой». Это всего лишь означает, что твоя сторона не делала никаких коммитов и никаких слияний не требуется. Другими словами, если у тебя состояние репозитория A, а у оригинала A → B → C, то это будет прямой перемоткой, потому что Git необходимо просто нарастить еще больше коммитов прямо поверх тех, что у тебя уже есть.

Посмотри содержимое


git log покажет тебе лог. Формат немного многословен и не очень подходит для беглого просмотра истории.

git log --oneline --graph --decorate намного приятнее для просмотра. Ты также можешь установить tig, который делает в основном то же самое, но ты сможешь использовать Enter на коммите, чтобы увидеть различия на месте.

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

git show показывает тебе патч, внесенный коммитом. git show : показывает тебе состояние файла для конкретного коммита.


Просто используй эту чертову штуку, чтобы сделать этот чертов патч для этого чертова проекта


git status
рассказывает тебе о текущем состоянии твоей кодовой базы: в какой ветке ты находишься, какие изменения ты сделал и т.д.

git branch создает новую ветку, основанную на коммите, в котором ты работаешь, но не переключается на нее. Вместо этого тебе может понадобиться команда наподобие git checkout -b origin/master, которая создает новую ветку, основанную на origin/master, а также переключается на нее.

git checkout устанавливает текущую ветку и переключается в соответствующее состояние кодовой базы. Ты также можешь перейти в удаленную ветку, в тэг или в конкретный коммит, но текущая ветка будет покинута и ты будешь получать предупреждения о наличии "оторванной HEAD". Это буквально означает, что HEAD (специальное имя, которое всегда указывает на то, с чем ты работаешь) не указывает на ветку, и если ты делаешь новые коммиты, у них не будет ничего, указывающее на них и они могут леко потеряться.

git add говорит Git о новых файлах, созданных тобой, которые нужны тебе в следующем коммите.

git rm говорит Git, что ты собираешься удалить файл, а так же удаляет его физически. (Это всегда обратимо. Git отклонит операцию, если файл был изменен. Также ты можешь просто удалить файл командой rm, а git commit -a зафиксирует это.)

git mv говорит Git, что ты переименовываешь файл. (Заметь, что Git в действительности не хранит переименования; он догадывается на лету, был ли файл переименован.)

git commit -a откроет текстовый редактор, для запроса описания коммита, затем создаст коммит из всех сделанных изменений всех файлов, известных Git.

Кое-что в модели Git я еще не затронул: там есть одна вещь, называемая "index", или "staging area", или иногда "cache". (Я не знаю зачем ей нужно столько имен.) Это те изменения, которые ты собираешься зафиксировать. Когда ты используешь git add и компанию, любые изменения файла (или все содержимое целиком, если это новый файл) формируются и отображаются в своих собственных секциях в git status. Несформированные изменения перечисляются под ними. Если ты используешь простой git commit без -a, то только сформированные изменения станут частью коммита. Иногда это бывает довольно полезно, потому что позволяет тебе проводить кучу исследовательской работы, а затем упаковывать ее в различные коммиты для будущих археологов. (Если у тебя разыгралось воображение, то рассмотри git add -p.) Но ты можешь просто использовать git commit -a, когда захочешь. Черт, да тебе даже не нужно git add; ты можешь просто передавать список файлов в git commit.

Понятненько. Теперь, как мне работать где-угодно?


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

git push отправит твою ветку в ветку с тем же именем в удаленном репозитории. Если ты используешь форк с GitHub, тогда у тебя вероятно есть единственный удаленный репозиторий под названием "origin", который и является твоим форком, и ты, вероятно, просто работаешь в master ветке. Тогда ты можешь сделать git push origin master и все будет в порядке.

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

Конфликты слияния


Если ты делаешь слияние, или отправку изменений, или (упаси Господи) перемещение, возможно твои изменения будут конфликтовать с чьими-то чужими. Git остановит слияние с сообщением "Автоматическое слияние не удалось; ошибка бла-бла-бла". Если ты посмотришь в git status, ты увидишь новую секцию для конфликтующих фалов. Тебе необходимо это исправить для завершения слияния или выполнения множества других реальных задач.

Открой конфликтующий файл и ты увидишь что-то вроде этого:

<<<<<<< HEAD
что-то, что ты изменил
=======
что-то, что изменил кто-то другой
>>>>>>> origin/master

(Стиль отображения конфликтов diff3 может немного улучшить ситуацию; смотри секцию настроек ниже.)

Это говорит тебе о том, что двое людей изменили те же самые строки в том же самом файле разным способом, а Git не знает как должен выглядеть конечный результат. Первая часть, отмеченная HEAD - это то, как выглядит твоя копия файла (HEAD - это просто специальный указатель на коммит или ветку, в которой ты находишься); вторая часть - это то, как выглядит копия файла другой ветки.

Если тебе повезло, то "конфликт" - это просто исправление каким-то засранцем ошибок расстановки пробельных символов, или вы оба добавляете секцию импорта в том же самом месте, или какие-то другие простые вещи. Отредактируй файл как тебе нужно и выполни git add, чтобы сообщить Git, что он готов к отправке. Как только все конфликты исправлены и все файлы добавлены через git add, сделай простой git commit, чтобы завершить слияние.

Если тебе не повезло, то кто-то провел большой рефакторинг, пока ты исправлял маленький баг, и теперь конфликтует весь файл, а ты окончательно попал. Ты можешь выполнить git merge --abort, чтобы отменить слияние, создать новую ветку, основанную на текущей ветке master, и повторить свои изменения вручную.

Несколько примечаний:

  • Дважды проверяй, что ты действительно исправил все конфликты. Git НЕ БУДЕТ препятствовать тебе фиксировать отметки о конфликте!
  • Иногда, конфликт - это когда одна сторона отредактировала файл, а другая сторона удалила этот файл. Когда это происходит, Git расскажет тебе кто произвел удаление. Я чаще всего сталкиваюсь с этим, когда использую автоматическое форматирование, или рефакторинг, или еще что-то, в этом случае мне на самом деле плевать на файл, который был удален; если это тот случай, ты можешь просто удалить его через git rm.
  • Есть полу-интерактивная команда git mergetool, которую ты можешь использовать в ходе конфликта, и которая откроет твою программу разрешения слияний для каждого конфликтующего файла. В моем случае это vimdiff, использование которой у меня никогда не входило в привычку, поэтому я не использую ее слишком часто. В твоем случае это может отличаться.


Боже-божечки мои! Что я наделал?!


Ты скопипастил вызов git какого-то придурка на Stack Overflow и теперь все сломано. Не паникуй! И тем более не копипасть проверенное решение от какого-то другого придурка.

Если твоя рабочая копия или индекс окончательно навернулись, ты можешь использовать команду git reset --hard, чтобы отменить все свои незафиксированные изменения. Но не используй ее необдуманно, поскольку это, естественно, деструктивная операция.

Если ты делал какую-то интерактивную многоступенчатую вещь, вроде git rebase или git cherry-pick и все пошло ужасно неверно, git status укажет тебе на это, а, например, git rebase --abort гарантированно вернет тебя туда, откуда ты начал.

Если ты думаешь, что каким-то образом потерял коммиты, ты можешь найти их в git reflog.

В самом худшем случае ты можешь вытащить свои наработки в виде патчей с помощью git show и начать заново со свежим клоном.

И еще, немного синтаксического барахла


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

HEAD - это что-то вроде специального имени ветки, которое просто ссылается на то, с чем ты работаешь прямо сейчас.

Есть целая куча синтаксисов для указания коммитов и диапазонов коммитов. Ты можешь просмотреть man gitrevisions на досуге. Наиболее полезные это:

  • foo^ - это (первый) родитель foo. Чаще всего используется как HEAD^. Заметь, что ^ - это специальный символ во многих оболочках и может понадобиться экранирование.
  • foo..bar - это диапазон и обозначает все, что после foo, вплоть до bar включительно.

Есть еще больше в man gitrevisions, но 80% из этого я никогда не использовал, если честно.

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


Полезные настройки


У меня немного в моем .gitconfig, но там есть несколько моих любимых вещей, может тебе они тоже понравятся. Если ты очень активно используешь Git, то может быть полезным пролистать man git-config, какой-нибудь из множества представленных вариантов его настройки может относиться к твоей проблеме.

Ты можешь запросить свою конфигурацию Git с помощью git config foo.bar.baz. Ты также можешь редактировать ее с помощью git config --global foo.bar.baz value, где параметр --global изменит твой ~/.gitconfig файл (который применяется к любому репозиторию, с которым ты работаешь), а его пропуск изменит .git/config (который применяется только к текущему репозиторию).

Или ты можешь крякнуть ~/.gitconfig, открыв его в текстовом редакторе, потому что это чертов INI-файл, в общем, не бином Ньютона. Давай представим, что делаем это вместо команд.

Прежде, чем ты сделаешь ЧТО-ЛИБО, настрой свои имя и почту


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

Если ты не укажешь Git свое имя, то ему придется гадать, а гадает он плохо. Он возьмет твое имя из поля "настоящее имя" в /etc/passwd (что может быть верным), а твою почту он возьмет из твоего логина плюс имени хоста твоего компьютера (что, конечно, полная бессмыслица, если только ты не на университетском сервере и это не 1983 год). И ты не сможешь исправить их задним числом, потому что они являются частью коммитов, а коммиты - неизменны.

Поэтому первые три строчки твоего .gitconfig должны исправить эту проблему:

[user]
    name = Eevee (Alex Munroe)
    email = eevee.git@veekun.com

Легкотня.

Стандартные цвета - это мусор, вселяющий ужас


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

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

Что меня встревожило и показалось ужасающим. Пожалуйста, просто доверься мне, когда я говорю, что тебе абсолютно необходимо тупо вставить этот блок определения цветов в свой .gitconfig.

[color "branch"]
    current = yellow reverse
    local = yellow
    remote = green
[color "diff"]
    meta = yellow bold
    frag = magenta bold
    old = red bold
    new = green bold
[color "status"]
    added = yellow
    changed = green
    untracked = cyan

Стиль отображения конфликтов


Единственная действительно стоящая вещь в моем .gitconfig вот эта:

[merge]
    conflictstyle = diff3

Обычно, конфликт слияния выглядит так:

<<<<<<< HEAD
то, на что ты поменял
=======
то, на что они поменяли
>>>>>>> master

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

Введи diff3, который меняет отображение конфликтов слияний так:

<<<<<<< HEAD
то, на что ты поменял
|||||||
то, что было изначально
=======
то, на что они поменяли
>>>>>>> master

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


Некоторые допущения, которые ты можешь, но не должен допускать


Git - это не дружелюбный инструмент управления проектом. Git - это тупой трекер содержимого.

Скорее, Git - это странная файловая система и у нее есть набор инструментов, типа rm и ls. Чем на более низкий уровень ты спускаешься, тем меньше Git будет предполагать о том, что ты пытаешься сделать, и тем меньше будет пытаться тебя остановить от проделывания чего-то странного. Если ты почерпнул только одну вещь из этой статьи, пусть это будет следующее: Git был спроектирован для тех людей, которые уже поняли его на 100% - для людей, которые его написали. В этом плане сейчас становится лучше, но это причина множества его острых углов.

Ради наставления тебя на путь истинный, вот несколько допущений, которые ты уже мог, но не должен был делать:

  • Коммит не обязан иметь одного родителя. У него их может быть двое (если это слияние). Или трое, или больше (если это "осьминожное" слияние). Или ноль (если это первоначальный коммит).
  • У тебя может быть удаленный репозиторий, у которого ноль общих коммитов с твоим репозиторием. Нет ничего строго предписывающего двум репозиториям содержать "одинаковую" кодовую базу или заставляющего их никогда не взаимодействовать. Просто это обычно не так полезно. (Один возможный способ использования: я слил два проекта в один репозиторий без потери какой-либо истории, через добавление одного, как удаленного репозитория другого и просто слияния их историй вместе.)
  • Похожим образом у тебя может быть две ветки в том же самом репозитории, у которых ноль общих коммитов. (Что означает, что у тебя может быть более одного первоначального коммита!) Это то, как GitHub хранит "страницы GitHub": они находятся на отдельной ветке gh-pages внутри твоего репозитория, ведя совершенно независимую историю.
  • Коммиты не знают на какой ветке они были созданы. Ветка указывает на отдельный коммит; коммит никогда не указывает на ветку. Хотя, в большинстве практических случаев ты можешь достаточно верно это предположить.
  • Git отслеживает файлы, а не директории. Ты не можешь хранить пустую директорию в Git. Обычной практикой является хранения файла нулевого размера с имененм .keep или что-то еще в директории и фиксирование этого файла.
  • Документация не обязательно перечисляет опции, или формы команд, или огромное множество всего остального в порядке полезности. Например, наиболее фундаментальная команда это, вероятно, git commit, а третья опция в документации - это -C, выполняющая некую странную форму слияния, которую я сомневаюсь, что когда-либо использовал. Опция -m, которая позволяет тебе создавать описание коммита, появляется лишь на шестнадцатом месте.




Револьвер в сапоге, на всякий случай


Git - это, в основном, странная файловая система, а команды Git, по сути, странные команды файловой системы. Прямо как ls и rm одинаково непрозрачны, если ты еще не знаешь, что они делают, так и команды Git не обладают очевидными признаками того, что они опасны или нет.

Поэтому, вот несколько опасных вещей и то, как их безопасно использовать, или, по крайней мере, как их использовать с наименьшим риском.

git rm


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

git checkout


git checkout переключает ветки, но на более фундаментальном уровне то, что она делает - это вытаскивает файлы. Ты можешь использовать ее как git checkout [commit] -- <files...>, чтобы вытащить некоторые файлы конкретного коммита. По-умолчанию это относится к твоей текущей ветке, поэтому способом отменить изменения, которые ты сделал в файле (но еще не зафиксировал), является git checkout -- .

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

Ты можешь захотеть передать опцию -p
, которая интерактивно покажет тебе откат каждой отдельной части каждого файла. (Различные команды принимают опцию -p, включая git add, которая дает возможность делать различные изменения в отдельный файл и фиксировать только некоторые из них. Довольно удобно.)

git reset


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

Опасная часть это git reset --hard <files...>, которая отменит твою работу без предупреждений, прямо как git checkout. Здесь нет какой-либо "проверочной" опции. Будь очень осторожен с этим и трижды проверь, что у тебя нет ничего, что ты хотел бы сначала сохранить.

Более безопасный вариант - это git stash, которая запихнет все твои незафиксированные изменения в некий временный псевдо-коммит, не привязанный к твоей ветке. Ты можешь увидеть их, используя git stash list, и если ты поймешь, что хочешь оставить что-то из этой работы, ты можешь заново применить спрятанный патч с помощью git stash apply.

git rebase


Мне плевать, что говорят другие. Не используй ничего, что содержит "rebase", пока ты не понимаешь, что именно ты делаешь.

"Rebase" - для редактирования истории. Но ты не можешь редактировать историю, по причине ее полного хэширования. Вместо этого git rebase создает новую историю.

Скажем, у тебя есть A → B → C, где C - это твой собственный коммит, а B - это самый последний коммит в origin/master. Ты отправляешь изменения и... О, нет! Там уже есть новый коммит D на сервере. Поэтому ты получаешь следующее:

      .---C  master
     /
A---B---D    origin/master

Ты бы мог сделать слияние здесь... или ты бы мог сделать перемещение. Фундаментально, "перемещение" означает пересоздание коммита с другим родителем. Git возьмет патч в C, применит его поверх D, исправит все номера строк и попросит тебя разрешить все конфликты (прямо как в слиянии), а потом создаст новый коммит из результата. Это не может быть до сих пор коммит C, потому что родитель является частью хэша коммита, а родитель изменился. Вместо этого ты получишь коммит C'. (Новый хэш не обязательно похож как-либо на старый; апостроф, произносимый как "штрих", это соглашение, заимствованное из математики.)

Поэтому теперь у тебя:

      .---C
     /
A---B---D         origin/master
         \
          .---C'  master

Твой коммит должен был быть основан на B, но ты переписал его, чтобы он был основан на D. Следовательно, он перемещен. Я полагаю.

В любом случае, теперь ты можешь делать обычную перематывающую вперед отправку изменений в origin.

Заметь, что C до сих пор присутствует, но у него больше нет имени. Git сохраняет висящие коммиты вроде этого около 30 дней (видно в git reflog), просто на случай, если ты совершил ошибку, и удаляет их в ходе сборки мусора.

Перемещение может быть очень разрушительным, и не должно легко выполняться. Определенно, никогда не перемещай коммиты, которые ты уже так или иначе опубликовал - если у кого-то еще работа основана на твоем оригинальном коммите C, то обновление их работы так, чтобы она основывалась вместо этого на C', становится огромной болью в заднице. А если они этого не делают, то ты можешь остаться с обоими C и C' в своей истории, или они могут конфликтовать друг с другом, или кто его знает что еще. Я повидал злоупотребление git rebase, превратившее линейную ветку с четырьмя коммитами в запутанное месиво из порядка пятнадцати коммитов, все слитые вперемешку с копиями друг друга.

А также существуют дальнейшие осложнения, наподобие: C может быть более чем один коммит, C может включать коммиты слияния, ты можешь редактировать C, пока ты в нем находишься и т.д. Это может доставить неудобства и довольно скоро, и не обязательно очевидно, как разрешить дурацкую проблему, если ты не абсолютно уверен в том, что делает Git.

Если ты решил поэкспериментировать с перемещением, одно последнее предупреждение. В слиянии твоя ветка "наша", а чужая ветка - "их". Но в перемещении все наоборот - ты начинаешь с чужой ветки и заново добавляешь свои собственные коммиты поверх нее, даже если ты думал, что перемещал свою текущую ветку. Поэтому с точки зрения Git, твоя ветка "их", а чужая ветка - "наша"! Это влияет на принудительное разрешение с помощью команды git checkout --ours, она обходит все патчи в отметках о конфликте и инвертирует "их" в "нас", когда описывает конфликты в git status. Еще одна причина не производить перемещение до тех пор, пока ты абсолютно не уверен, что ты понимаешь, что происходит!

Если ты делаешь лажу во время перемещения, ты всегда можешь выполнить git rebase --abort. Или, если перемещение уже закончено, ты можешь сослаться на старую версию ветки с помощью специального синтаксиса имяветки@{1}, который означает "куда указывала имяветки, перед тем, как была изменена в последний раз". Тебе следует использовать git reset --hard чтобы заставить ветку вернуться, хотя, ой, фу.

--force


Обычно появляется как аргумет для git push после перемещения. Будь супер-пупер осторожен, т.к. он вслепую переписывает что бы то ни было на удаленном репозитории. Если ты читаешь эту статью, у тебя, вероятно, нет хорошего повода для принудительной отправки изменений. А если ты думаешь, что есть, то, вероятно, все еще нет, потому что в Git 2.0 есть аргумент --force-with-lease, который, по крайней мере, защищает от ситуации гонки.

Сохранение паролей, больших файлов и т.д.


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

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

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

Забывая, что ты в середине чего-то


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

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

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


Вот и все, что у меня есть


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

Несмотря на это, я надеюсь, что что-то из этого будет полезным или, по крайней мере, сделает другие ресурсы по Git более понятными!

Если ты просто умираешь от нетерпения узнать больше о Git, интернет переполнен другими людьми, пытающимися рассказать о нем. Отсылаю тебя к списку статей от GitHub.


P.S.


Автор оригинальной статьи - Алекс Манро (Alex Munroe aka Eevee).
Автор перевода - Indexator.

Материал распространяется под лицензией CC-BY.

Теги:
Хабы:
Всего голосов 131: ↑103 и ↓28+75
Комментарии365

Публикации

Истории

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань