Глава 5. Чудеса ветвления

Возможности мгновенного ветвления и слияния — самые замечательный особенности Git.

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

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

Однако клонирование все же предполагает копирование всего рабочего каталога, как и всей истории изменений до настоящего момента. Хотя Git и снижает затратность этого действия за счет возможности совместного использования файлов и жестких ссылок, но все файлы проекта придется полностью воссоздать в новом рабочем каталоге.

Решение: у Git есть более удобный инструмент для таких случаев, который сэкономит и время, и дисковое пространство по сравнению с клонированием — это git branch (branch — ветка, прим. пер.).

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

Кнопка босса

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

В каком-нибудь каталоге:

$ echo "Я хитрее моего босса" > myfile.txt
$ git init
$ git add .
$ git commit -m "Начальный коммит"

Мы создали хранилище Git, содержащее один текстовый файл с определенным сообщением. Теперь выполните

$ git checkout -b boss # вероятно, это последнее изменение
$ echo "Мой босс умнее меня" > myfile.txt
$ git commit -a -m "Другой коммит"

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

$ git checkout master # переключиться на оригинальную версию файла

Вуаля! Текстовый файл восстановлен. А если босс решит сунуть нос в этот каталог, запустите

$ git checkout boss # перейти на версию, подходящую для глаз босса

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

Грязная работа

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

$ git commit -a
$ git checkout HEAD~3

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

$ git checkout master

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

А что, если вы все-таки хотели сохранить временные изменения? Запросто:

$ git checkout -b dirty

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

$ git checkout dirty

Мы говорили об этой команде в одной из предыдущих глав, когда обсуждали загрузку старых состояний. Теперь у нас перед глазами полная картина: файлы изменились к нужному состоянию, но мы должны покинуть главную ветку. Любые коммиты, сделанные с этого момента, направят файлы по другому пути, к которому можно будет вернуться позже.

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

Быстрые исправления

Ваша работа в самом разгаре, когда вдруг выясняется, что нужно все бросить и исправить только что обнаруженную ошибку в коммите «1b6d…»:

$ git commit -a
$ git checkout -b fixes 1b6d

После исправления ошибки сделайте

$ git commit -a -m "Ошибка исправлена"
$ git checkout master

и вернитесь к работе над вашими исходными задачами.

Вы можете даже «влить» только что сделанное исправление ошибки в основную ветку:

$ git merge fixes

Слияния

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

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

Обычно у коммита есть один «родитель», а именно предыдущий коммит. Слияние веток приводит к коммиту как минимум с двумя родителями. Отсюда возникает вопрос: к какому коммиту на самом деле отсылает HEAD~10? Коммит может иметь несколько родителей, так за которым из них следовать далее?

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

Вы можете обращаться к конкретному родителю с помощью символа «^». Например, чтобы показать запись в журнале от второго родителя, наберите

$ git log HEAD^2

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

$ git diff HEAD^

Вы можете сочетать такую запись с другими. Например,

$ git checkout 1b6d^^2~10 -b ancient

создаст новую ветку «ancient» («древняя», прим. пер.), отражающую состояние на десять коммитов назад от второго родителя первого родителя коммита, начинающегося с 1b6d.

Непрерывный рабочий процесс

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

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

Благодаря безболезненным ветвлению и слиянию, мы можем изменить правила и работать над второй частью до того, как первая официально будет готова. Допустим, вы закоммитили первую часть и выслали ее на проверку. Скажем, вы в ветке master. Теперь смените ветку:

$ git checkout -b part2 # часть2

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

$ git checkout master  # Возвращаемся к первой части.
$ вносим_исправления
$ git commit -a        # Фиксируем изменения
$ git checkout part2   # Возвращаемся ко второй части.
$ git merge master     # Вливаем сделанные исправления.

В конечном счете, первая часть утверждена:

$ git checkout master  # Возвращаемся к первой части.
$ отправка файлов        # Выпускаем в мир!
$ git merge part2      # Вливаем вторую часть.
$ git branch -d part2  # Удаляем ветку part2.

Теперь вы снова в ветке master, а вторая часть — в вашем рабочем каталоге.

Этот прием легко расширить на любое количество частей. Столь же легко сменить ветку задним числом. Предположим, вы слишком поздно обнаружили, что должны были создать ветку семь коммитов назад. Тогда введите:

$ git branch -m master part2 # Переименовываем ветку master в part2.
$ git branch master HEAD~7   # Создаем новую ветку master семью коммитами выше.

Теперь ветка master содержит только первую часть, а ветка part2 — всё остальное. В последней мы и находимся. Мы создали ветку master, не переключаясь на нее, потому что хотим продолжить работу над part2. Это непривычно: до сих пор мы переключались на ветки сразу же после их создания, вот так:

$ git checkout HEAD~7 -b master  # Создаем ветку и переключаемся на нее.

Изменяем состав смеси

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

$ git branch sanitized    # Создаем ветку для очищенных коммитов.
$ git checkout -b medley  # Создаем ветку для работы и переключаемся на нее.

Далее делайте всё что нужно: исправляйте ошибки, добавляйте новые функции, добавляйте временный код и так далее, при этом почаще выполняя коммиты. После этого

$ git checkout sanitized
$ git cherry-pick medley^^

применит коммит «пра-родителя» головы ветки «medley» к ветке «sanitized». Правильно подбирая элементы, вы сможете создать ветку, в которой будет лишь окончательный код, а связанные между собой коммиты будут собраны вместе.

Управление Ветками

Для просмотра списка всех веток наберите

$ git branch

По умолчанию вы начинаете с ветки под названием «master». Кому-то нравится оставлять ветку «master» нетронутой и создавать новые ветки со своими изменениями.

Опции -d и -m позволяют удалять и перемещать (переименовывать) ветки. Смотрите git help branch.

Ветка «master» — это удобная традиция. Другие могут предполагать, что в вашем хранилище есть ветка с таким именем и что она содержит официальную версию проекта. Хотя вы можете переименовать или уничтожить ветку «master», лучше соблюсти общее соглашение.

Временные Ветки

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

Это похоже на то, как вы переключаете телевизионные каналы, чтобы посмотреть что показывают по другим. Но вместо того, чтобы нажать на пару кнопок, вам нужно создавать, выбирать (checkout), сливать (merge) а затем удалять временные ветки. К счастью, в Git есть сокращенная команда, столь же удобная, как пульт дистанционного управления.

$ git stash

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

$ git stash apply # Возможно, понадобится устранить возникшие конфликты.

Можно создавать несколько тайников, используя их по-разному. Смотрите git help stash. Как вы могли догадаться, Git оставляет ветки «за кадром» при выполнении этого чудесного приема.

Работайте как вам нравится

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

Посмотрим на веб-браузеры. Зачем нужна поддержка вкладок вдобавок к окнам? Поддержка и тех, и других позволяет приспособиться к широкому разнообразию стилей работы. Некоторым пользователям нравится держать открытым единственное окно и использовать вкладки для множества веб-страниц. Другие могут впасть в другую крайность: множество окон без вкладок вообще. Третьи предпочтут нечто среднее.

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