Механизм Pull request

Так называемый "поток работ PR" (или "рабочий процесс PR"), применяемый в разработке Godot, является общепринятым для проектов, использующих Git, и должен быть знаком опытным пользователям и разработчикам свободного ПО. Основная идея заключается в том, что только небольшое число коммитов (commit, дословно - "совершать", "фиксировать"; подразумевается сохранение изменений в коде) записывается непосредственно в главной ветке исходного проекта. Вместо этого участники создают форки (fork, дословно - "вилка"; фактически создаётся полная копия проекта, которую участник может изменять по своему усмотрению), а затем используют интерфейс GitHub для запроса на принятие изменений (pull request, дословно - "запрос на вытягивание"; обычно используется англ. термин без перевода и транслитерации) одной из веток их форка в какую-либо ветку исходного (upstream, дословно - "восходящий поток") репозитория.

Созданный pull request (далее - PR) будет рассмотрен другими участниками проекта, которые могут одобрить его, отклонить или, чаще всего, потребовать внесения каких-либо изменений. После согласования PR принимается одним из основных разработчиков, и его коммиты становятся частью целевой ветки (обычно это ветка master).

Далее будет рассмотрен пример, демонстрирующий типичный процесс работы с Git и используемые в нём команды. Но сначала - краткий обзор организации Git-репозитория Godot.

Исходный Git-репозиторий

Репозиторий на GitHub представляет собой Git-хранилище кода со встроенной система отслеживания ошибок и PR-системой.

Примечание

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

Система управления версиями Git - это инструмент, используемый для отслеживания последовательных изменений исходного кода; для эффективного участия в разработке Godot настоятельно рекомендуется изучить основы командной строки Git. Существует несколько графических интерфейсов для Git, но они часто порождают вредные привычки в отношении работы с Git и PR, поэтому мы рекомендуем не использовать их. В частности, не желательно использовать онлайн-редактор GitHub для внесения изменений в код (это допускается только для небольших исправлений или для работы с документацией), поскольку он создаёт отдельные коммиты для каждого изменения каждого файла, что быстро приводит к PR с нечитаемой историей Git (особенно после рецензирования).

См.также

Первые разделы книги "Pro Git" содержат хорошее введение в философию инструмента и различные команды, которые вам предстоит освоить. Вы можете прочитать их онлайн на сайте Git SCM.

Ветки Git-репозитория Godot организованы следующим образом:

  • master - это ветка, в которой происходит разработка следующей основной версии. Она, как правило, нестабильна и не предназначена для использования в разработке игр. Но именно сюда следует делать PR проводить в первую очередь.

  • Стабильные ветви именуются согласно их версиям, например 3.2 или 2.1. Они используются для переноса исправлений и улучшений из ветки master в текущий поддерживаемый стабильный выпуск (например, 3.2.3 или 2.1.6). Как правило, последняя стабильная ветка поддерживается до выхода следующей версии (например, ветвь 3.0 поддерживалась до выпуска Godot 3.1). Если вы хотите сделать PR для поддерживаемой стабильной ветки, то, пожалуйста, проверьте сначала, актуальны ли ваши изменения для ветки master - и если да, то сделайте PR для неё приоритетным. После этого менеджеры релизов смогут отобрать исправления для стабильной ветви, если это необходимо.

  • Иногда создаются ветки для разработки отдельных функций - обычно они сливаются с веткой master через какое-то время.

Создание форка и клонирование

Первый шаг - это создание форка репозитория godotengine/godot на GitHub. Для этого вам потребуется создать учётную запись GitHub (если у вас её ещё нет) и войти в систему. В правом верхнем углу страницы вы увидите кнопку "Fork", как показано ниже:

../../_images/github_fork_button.png

Нажмите на неё, и через небольшой промежуток времени вас перенаправит на только что созданный форк репозитория Godot с вашим именем пользователя GitHub в качестве пространства имён:

../../_images/github_fork_url.png

Затем вы можете клонировать свой форк, то есть создать локальную копию удалённого репозитория (в терминологии Git - remote origin). Если вы ещё этого не сделали, установите Git (пользователи Windows или macOS могут скачать его с сайта Git SCM, пользователи Linux могут воспользоваться менеджером пакетов).

Примечание

Если вы используете Windows, то вам понадобится Git Bash для ввода команд. Пользователи macOS и Linux могут использовать системные терминалы.

Чтобы клонировать ваш форк из GitHub, используйте следующую команду:

$ git clone https://github.com/USERNAME/godot

Примечание

В подобных примерах символ "$" - приглашение командной строки, типичное для UNIX-оболочек. Он не является частью команды и вводить его не нужно.

Через некоторое время в вашей текущей директории должен появиться каталог godot. Переместитесь в него с помощью команды cd:

$ cd godot

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

$ git remote add upstream https://github.com/godotengine/godot
$ git fetch upstream

Это создаст ссылку с именем upstream, указывающую на исходный репозиторий godotengine/godot. Она пригодится, когда вы захотите добавить к себе новые коммиты из его ветки master, чтобы обновить свой форк. Также у вас есть ещё одна ссылка, с именем origin, которая указывает на ваш форк (USERNAME/godot).

Описанные выше шаги вам нужно выполнить только один раз и повторять их не придётся, если вы не удалите локальную папку godot (однако, если хотите, вы можете перемещать её - соответствующие метаданные скрыты в подкаталоге .git).

Примечание

Branch it, pull it, code it, stage it, commit, push it, rebase it... technologic.

Такой негативный взгляд, стилизованный под песню Technologic группы Daft Punk, показывает общее представление новичков о рабочем процессе в Git: множество странных команд, которые нужно выучить или использовать копированием/вставкой в надежде, что они будут работать так, как ожидалось. На самом деле это неплохой способ обучения, если вы любопытны и не стесняетесь задавать вопросы поисковым системам, но на всякий случай мы собрали здесь основные команды, которые вам понадобятся при работе в Git.

Далее, в качестве примера, мы предположим, что вы хотите реализовать новую функцию менеджера проектов Godot, изменив для этого файл editor/project_manager.cpp.

Ветвление

По умолчанию после git clone ваш локальный репозиторий будет переключён на ветку master. Для разработки новой функции желательно создать новую ветку:

# Create the branch based on the current branch (master)
$ git branch better-project-manager

# Change the current branch to the new one
$ git checkout better-project-manager

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

# Change the current branch to a new named one, based on the current branch
$ git checkout -b better-project-manager

Если вы захотите вернуться к ветке master, используйте:

$ git checkout master

Список веток в репозитории можно просмотреть командной git branch, при этом ветка, в которой вы сейчас находитесь, будет отмечена звёздочкой:

$ git branch
  2.1
* better-project-manager
  master

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

$ git checkout -b my-new-feature master

Обновление вашей ветки

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

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

$ git pull --rebase upstream master

Аргумент --rebase гарантирует, что любые локальные коммиты, которые вы сделали, будут повторно применены поверх принимаемых изменений, что обычно и требуется в PR-процессе. Таким образом, когда вы отправляете PR, ваши собственные коммиты будут единственным отличием от текущего состояния исходной ветки master.

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

Если во время обновления вы перестали понимать, что происходит, и запаниковали (не волнуйтесь, мы все делаем так первые несколько раз), вы можете прервать процесс с помощью команды git rebase --abort. После этого вы вернётесь в исходное состояние своей ветки (до вызова git pull --rebase).

Примечание

Если вы опустите аргумент --rebase, то у вас получится коммит слияния (merge в терминологии Git), который собирает историю из двух разных веток в одно целое. Если при этом возникнут какие-либо конфликты, они все будут разрешены в пределах этого коммита.

Хотя это допустимое действие и поведение git pull по умолчанию, оно не одобряется в нашем сообществе. Мы используем слияние только при принятии PR в исходный репозиторий.

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

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

Совет

Если в какой-то момент вы захотите откатить вашу локальную ветку до определённого коммита или до какой-либо ветки удалённого репозитория, то вы можете сделать это с помощью команд git reset --hard <commit ID> или git reset --hard <remote>/<branch> (например, git reset --hard upstream/master).

Имейте в виду, что такой сброс приведёт к удалению любых изменений в этой ветке. Если вы когда-нибудь сделаете коммит по ошибке - используйте команду git reflog, чтобы найти ID предыдущего коммита, и подставьте его в качестве аргумента в git reset --hard для возврата в это состояние.

Внесение изменений

Затем вы должны внести изменения в файл editor/project_manager.cpp нашего примера с помощью вашей обычной среды разработки (текстовый редактор, IDE и т.д.).

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

Вот некоторые команды, которые понадобятся вам, чтобы отслеживать и переключать состояния ваших изменений:

  • git diff покажет не проиндексированные изменения, то есть разницу между вашим рабочим каталогом и тем, что попадёт в следующий коммит;

  • git checkout -- <files> отменит не проиндексированные изменения в указанных файлах;

  • git add <files> проиндексирует указанные файлы, подготовив их к коммиту;

  • git diff --staged покажет проиндексированные изменения, то есть разницу между предыдущим коммитом и тем, что попадёт в следующий;

  • git reset HEAD <files> отменит индексацию указанных файлов;

  • git status покажет, какие изменения проиндексированы, а какие нет;

  • git commit зафиксирует проиндексированные изменения, создав коммит; при этом откроется текстовый редактор (вы можете задать предпочтительный редактор в переменной окружения GIT_EDITOR или в параметре core.editor в настройках Git), чтобы вы могли оставить примечания в журнале коммитов; вы также можете использовать git commit -m "Cool commit log" для записи в журнал напрямую;

  • git commit --amend позволяет вам внести текущие проиндексированные (с помощью git add) изменения в предыдущий коммит; это может пригодится, если вам нужно что-то исправить в предыдущем коммите (опечатку, стилистическую или иную мелкую ошибку);

  • git log покажет вам последние коммиты в текущей ветке; если вы делали локальные коммиты, они будут наверху списка;

  • git show покажет изменения, внесённые последним коммитом; также вы можете указать в качестве аргумента хеш коммита, изменения которого хотите увидеть.

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

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

# It's nice to know where you're starting from
$ git log

# Do changes to the project manager with the nano text editor
$ nano editor/project_manager.cpp

# Find an unrelated bug in Control and fix it
$ nano scene/gui/control.cpp

# Review changes
$ git status
$ git diff

# We'll do two commits for our unrelated changes,
# starting by the Control changes necessary for the PM enhancements
$ git add scene/gui/control.cpp
$ git commit -m "Fix handling of margins in Control"

# Check we did good
$ git log
$ git show
$ git status

# Make our second commit
$ git add editor/project_manager.cpp
$ git commit -m "Add a pretty banner to the project manager"
$ git log

Это добавит к ветке better-project-manager два новых коммита, которых нет в ветке master. Однако эти изменения всё ещё являются локальными: ни ваш удалённый форк, ни исходный репозиторий пока не знают о них.

Отправка изменений на GitHub

Тут вам пригодится команда git push. Git всегда выполняет коммит в локальном репозитории (в отличие от Subversion, где коммит изменяет удалённый репозиторий напрямую). Вам нужно вручную отправлять новые коммиты в удалённый репозиторий, если вы хотите сделать их доступными для всех. Вот синтаксис этой команды:

$ git push <remote> <local branch>[:<remote branch>]

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

$ git push origin better-project-manager

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

Запрос на принятие изменений

После того как изменения будут загружены на GitHub, на странице репозитория вы увидите строку "This branch is 2 commits ahead of godotengine:master" ("Эта ветка опережает godotengine:master на 2 коммита"). Также там может быть указано отставание на несколько коммитов, если ваша ветка не была синхронизирована с веткой master исходного репозитория.

../../_images/github_fork_make_pr.png

Правее этой строки есть кнопка "Pull request". Нажав на неё, вы откроете форму, которая позволит вам создать запрос на принятие изменений в исходный репозиторий godotengine/godot. Там должны быть указаны ваши коммиты и статус "Able to merge" ("Возможность слияния"). Если это не так (к примеру, коммитов больше, чем вы делали, или указаны конфликты слияния), лучше не создавайте PR, а зайдите в IRC и попросите поддержки.

Используйте для PR короткий и ясный заголовок, а необходимые детали укажите в поле комментариев. Также вы можете перетащить туда скриншоты, GIF-файлы или архивы, если это необходимо для пояснения того, что именно реализовано в ваших изменениях. После этого нажмите кнопку "Create a pull request" - и запрос будет отправлен.

Изменение PR

Пока ваш PR ещё не принят и рассматривается другими участниками, вам часто придётся редактировать его - либо когда вас кто-то попросит внести те или иные правки, либо когда вы сами обнаружите проблемы во время очередного тестирования.

К счастью, вы можете изменить PR просто изменяя ветку, из которой он был сделан. То есть вы можете сделать новый коммит в свою локальную ветку, отправить изменения в ту же ветку удалённого репозитория, что и раньше - и PR будет автоматически обновлён:

# Check out your branch again if you had changed in the meantime
$ git checkout better-project-manager

# Fix a mistake
$ nano editor/project_manager.cpp
$ git add editor/project_manager.cpp
$ git commit -m "Fix a typo in the banner's title"
$ git push origin better-project-manager

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

# Check out your branch again if you had changed in the meantime
$ git checkout better-project-manager

# Fix a mistake
$ nano editor/project_manager.cpp
$ git add editor/project_manager.cpp
# --amend will change the previous commit, so you will have the opportunity
# to edit its commit message if relevant.
$ git commit --amend
# As we modified the last commit, it no longer matches the one from your
# remote branch, so we need to force push to overwrite that branch.
$ git push --force origin better-project-manager

Интерактивный режим rebase

Если вы невнимательно следовали описанным выше инструкциям (или не знали о нашем рабочем процессе и советах по использованию Git) и вместо одного исправленного коммита создали несколько последовательных, рецензенты могут попросить вас перенастроить вашу ветку так, чтобы все коммиты или только часть из них была объединена в один.

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

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

Для этого применяется интерактивный режим команды rebase: git rebase -i. В качестве аргумента используется ID коммита или имя ветки, что позволяет вам редактировать все коммиты между указанной точкой и текущим состоянием вашей рабочей ветки, называемым HEAD.

Хотя вы можете передать в git rebase -i ID любого коммита и просмотреть всё, что происходило после него, наиболее распространённый и удобный рабочий процесс представляет собой редактирование изменений от ветки master исходного репозитория, то есть:

$ git rebase -i upstream/master

Примечание

Ссылаться на ветки в Git немного сложно из-за различия между удалёнными и локальными ветками. Здесь Upstream/master (с символом "/") - это локальная ветка, которая была извлечена из ветки master исходного репозитория.

Интерактивный режим rebase работает только с локальными ветками, поэтому здесь важен символ "/". Так как исходный репозиторий часто изменяется, ваша локальная ветка Upstream/master может оказаться устаревшей - во избежание этого вам необходимо обновить её с помощью git fetch Upstream master. В отличие от команды git pull --rebase Upstream master, которая будет обновлять вашу текущую ветку, fetch обновит только ссылку Upstream/master (которая отличается от вашей локальной ветки master... да, это сбивает с толку, но постепенно вы разберётесь и привыкните).

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

pick 1b4aad7 Add a pretty banner to the project manager
pick e07077e Fix a typo in the banner's title

Также редактор покажет инструкции относительно того, что вы можете делать с этими коммитами. В частности, там будет написано, что "pick" нужен, чтобы оставить коммит без изменений, а "squash" и "fixup" применяются для слияния коммита с предыдущим. Разница между "squash" и "fixup" заключается в том, что "fixup" при слиянии отбросит примечания текущего коммита. Так как в нашем примере нет необходимости сохранять в журнале коммитов фразу "Fix a typo ...", финальный текст будет следующий:

pick 1b4aad7 Add a pretty banner to the project manager
fixup e07077e Fix a typo in the banner's title

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

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

$ git push origin better-project-manager
To https://github.com/akien-mga/godot
 ! [rejected]        better-project-manager -> better-project-manager (non-fast-forward)
error: failed to push some refs to 'https://akien-mga@github.com/akien-mga/godot'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.

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

$ git push --force origin better-project-manager

Теперь Git просто заменит вашу удалённую ветку на обновлённую локальную (поэтому используйте опцию --force с осторожностью, предварительно проверяя отправляемые изменения с помощью git log). Открытый из этой ветки PR так же будет обновлён автоматически.

Удаление ветки в Git

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

Чтобы удалить локальную ветку better-project-manager, используйте следующую команду:

$ git branch -d better-project-manager

При необходимости удалить ветку до слияния вместо опции -d нужно использовать -D.

После этого используйте следующую команду, чтобы удалить эту же ветку из удалённого репозитория:

$ git push origin -d better-project-manager

Также вы можете сделать это, используя интерфейс GitHub - соответствующая кнопка должна появиться на странице PR, после того как он будет принят или закрыт.