Я работаю в операционной системе «Windows 10». Использую программу «Git» (дистрибутив «Git for Windows») версии 2.35.1 (и программу-оболочку «Git Bash» из ее дистрибутива). С программой «Git», которая является системой управления версиями, обычно работаю из командной строки с помощью программы-оболочки «PowerShell» версии 7, запускаемой из программы-«эмулятора терминала» «Windows Terminal» версии 1.16.
Разные способы организации работы над проектом
Погружаясь в мир совместной разработки программ, я узнал, как выполнять вклад в проект, работая с удалёнными репозиториями (по-английски «remote repository» или просто «remote»), которые могут находиться на других компьютерах в компьютерной сети, в том числе — в интернете. При этом для отправки вклада в проект, файлы которого хранятся в удалённом репозитории (хранилище), в программе «Git» используется команда «git push», для применения которой требуется обладать правом записи в удалённый репозиторий.
На сайте «GitHub» я познакомился с другим способом отправки вклада в проект: с помощью создания «форка» (копии исходного репозитория проекта; это слово является калькой с английского «fork») под своей учетной записью, внесения в форк изменений и последующей отправки запроса на принятие изменений (по-английски «pull request», сокращенно «PR») в исходный репозиторий. При этом не требуется обладать правом записи в исходный репозиторий. Получается, что вы передаете предлагаемые вами изменения в проект (вклад) на рассмотрение людям, которые имеют право записи в исходный репозиторий. Они рассмотрят ваш запрос на принятие изменений и, возможно, примут его (выполнят слияние ваших изменений с исходным проектом), если изменение будет признано ими полезным и не будет содержать ошибок. То есть в данном случае мы действуем через посредника.
Только после этого я узнал про существование «патчей» (файлов с вашими изменениями в проект). Сразу мне было непонятно, зачем нужны эти патчи, если существуют вышеописанные способы внесения вклада в проект. Как оказалось, существует еще множество других способов организации работы над проектом, в том числе способ с помощью патчей. При этом способе вы получаете копию файлов проекта из исходного репозитория, вносите в них свои изменения, формируете файл (патч) с изменениями и передаете этот файл каким-либо способом (загрузив патч на сайт проекта, переслав его по электронной почте или еще как-либо) людям, у которых есть право записи в исходный репозиторий.
Таким образом, способ работы над проектом с помощью патчей немного похож на вышеописанный способ работы с помощью запросов на принятие изменений на сайте «GitHub»: здесь тоже работа идет через посредника, который рассмотрит ваш патч и, возможно, сделает его слияние с файлами проекта в исходном репозитории.
Откуда взялось слово «патч», инструменты «diff» и «patch»
Вообще, способ работы над проектом с помощью патчей возник намного раньше, чем появилась программа «Git» (2005 год). Да, в общем-то, и намного раньше, чем появились системы управления версиями (начало 1960-х). Слово «патч» — это калька с английского слова «patch», которое дословно означает «заплатка» или «наложить заплатку». Уже в 1940-х годах «патчи» реально могли представлять собой физически существующие заплатки в виде кусочков бумаги, которыми заклеивали некоторые места на бумажных перфолентах или перфокартах, таким образом исправляя ошибки в программах, хранящихся на этих перфолентах и перфокартах.
В Unix-подобных операционных системах существуют команды (программы-инструменты) «diff» и «patch». С помощью инструмента «diff» можно найти разницу между файлами, которую после этого можно выгрузить в отдельный файл. Полученный файл с изменениями тоже называют «diff», а еще этот же файл могут называть «patch», так как его можно передать другим разработчикам, которые применят этот файл с изменениями к своей копии файлов проекта с помощью инструмента «patch», таким образом внеся предлагаемые изменения в проект. Название инструмента «diff» получено сокращением от английского слова «difference» (по-русски «разница»).
Следует иметь в виду, что программа-инструмент «diff» может выдавать разницу между файлами в разных форматах, например, в «контекстном формате» или в «унифицированном формате» (по-английски «unified format» или «unidiff»).
В программе «Git» для получения разницы между файлами, коммитами, версиями проекта используется команда «git diff», после чего полученная разница может быть выгружена в отдельный файл (патч). После передачи файла-патча человеку, имеющему право записи в исходный проект, этот человек может применить полученный файл-патч к файлам исходного проекта с помощью команды «git apply». Насколько я понимаю, при этом используется формат вывода разниц «unidiff» (унифицированный формат).
Работа с патчем в программе «Git» на тестовом проекте
Подготовка тестового проекта
Сначала подготовим тестовый проект, он у меня находится в папке «C:\Users\Илья\source\repos\test\». Проект состоит из одного текстового файла «shalandy.txt», в который записан текст в кодировке UTF-8 с окончаниями строк вида CRLF, как принято в операционных системах «Windows» (окончания строк я буду показывать, хотя в редакторах кода они обычно скрыты):
shalandy.txt (185 байтов, кодировка UTF-8)
Шаланды, полные кефали,CRLF
В Одессу Костя приводил.CRLF
И все биндюжники вставали,CRLF
Когда в пивную он входил.
Шаг 1. Создание пустого Git-репозитория для проекта:
PS C:\Users\Илья\source\repos\test> git init
Initialized empty Git repository in C:/Users/Илья/source/repos/test/.git/
Шаг 2. Проверка настройки режима работы с окончаниями строк для проекта:
PS C:\Users\Илья\source\repos\test> git config core.autocrlf
true
Про эту настройку у меня есть отдельная статья, там же показано на примере, как ее можно изменить. У меня для этой настройки прописано значение true
на уровне текущего пользователя (global) операционной системы, поэтому нет необходимости задавать ей значение отдельно на уровне проекта. Напомню, при этой настройке со значением true
предполагается, что в рабочей папке в файлах мы имеем дело с окончаниями строк вида CRLF, а в Git-репозитории версии файлов хранятся с окончаниями строк вида LF.
Шаг 3. Добавление исходной версии файла «shalandy.txt» в индекс (stage):
PS C:\Users\Илья\source\repos\test> git add "shalandy.txt"
Для демонстрации создания файла-патча нам достаточно помещения версии файла «shalandy.txt» в индекс (далее в статье будет показано и помещение версии файла в коммит). Команда «git diff» может быть использована как для получения разницы между версией файла в индексе и версией файла в рабочей папке, так и для получения разницы между версией файла в определенном коммите и версией файла в рабочей папке (а также для многих других разных сравнений).
Шаг 4. Внесение изменений в файл «shalandy.txt» (то есть создание его новой версии) в рабочей папке:
shalandy.txt (217 байтов, кодировка UTF-8)
Шаланды, полные кефали,CRLF
В Одессу Гена приводил.CRLF
И все биндюжники вставали,CRLF
Когда в пивную он входил.CRLF
(поёт Марк Бернес)
Создание файла разниц (файла-патча)
У нас есть две версии файла «shalandy.txt» из тестового проекта. Исходная версия содержится в индексе Git-репозитория, новая версия — в рабочей папке проекта. Разницы между ними можно просмотреть в окне консоли (терминала) с помощью следующей команды:
PS C:\Users\Илья\source\repos\test> git diff
Вот как выглядит результат работы этой команды у меня в программе-оболочке «PowerShell» версии 7, запущенной в программе-«эмуляторе терминала» «Windows Terminal» версии 1.16:
Далее я опишу два сценария получения файла-патча и некоторые тонкости работы программ, на которые при этом стоит обратить внимание. Начнем с более простого сценария, а потом перейдем к более сложному.
Сценарий 1. Получение файла-патча «my.patch» из программы-оболочки «Git Bash», которую я получил в составе дистрибутива «Git for Windows». По умолчанию программа-оболочка «Git Bash» у меня настроена для работы в традиционной для операционных систем «Windows» программе-«эмуляторе терминала» «Windows Console»:
Илья@IlyaComp MINGW64 ~/source/repos/test (master)
$ git diff > my.patch
my.patch (493 байта, кодировка UTF-8)
diff --git a/shalandy.txt b/shalandy.txtLF
index 8591ff8..fd3ff04 100644LF
--- a/shalandy.txtLF
+++ b/shalandy.txtLF
@@ -1,4 +1,5 @@LF
Шаланды, полные кефали,LF
-В Одессу Костя приводил.LF
+В Одессу Гена приводил.LF
И все биндюжники вставали,LF
-Когда в пивную он входил.LF
\ No newline at end of fileLF
+Когда в пивную он входил.LF
+(поёт Марк Бернес)LF
\ No newline at end of fileLF
Следует иметь в виду, что в команде git diff > my.patch
только часть git diff
относится к программе «Git», а часть > my.patch
обрабатывается программой-оболочкой (в данном случае «Git Bash»), из которой запущена эта команда. Символ >
означает перенаправление вывода (в данном случае вывод команды git diff
вместо окна консоли отправляется в указанный файл). В разных программах-оболочках часть > my.patch
команды может быть обработана по-разному, из-за чего файл «my.patch» может быть сформирован с ошибками (это будет показано далее).
В вышеприведенном блоке кода видно, что в полученном файле-патче созданы окончания строк вида LF. Следует иметь в виду, что программа «Git» в данном случае возвращает строки исходной версии файла «shalandy.txt» в том виде, в котором они хранятся в Git-репозитории. Если в Git-репозиторий случайно попадут строки с окончаниями вида, к примеру, CRLF, то в файл «my.patch» они будут выгружены тоже с окончаниями вида CRLF.
Сценарий 2. Получение файла-патча «my.patch» из программы-оболочки «PowerShell» версии 7, запущенной в программе-«эмуляторе терминала» «Windows Terminal» версии 1.16.
Сначала следует проверить значения некоторых настроек:
PS C:\Users\Илья\source\repos\test> (Get-WinSystemLocale).Name
ru-RU
PS C:\Users\Илья\source\repos\test> ([System.Console]::OutputEncoding).CodePage
866
PS C:\Users\Илья\source\repos\test> $OutputEncoding.CodePage
65001
Выше, на иллюстрации 1, было показано, что вывод команды git diff
, запущенной из программы-оболочки «PowerShell», в окно терминала у меня происходит без проблем с настройками по умолчанию. Перенаправление вывода этой команды в файл происходит по-другому, русские буквы трансформируются в кракозябры (по-английски «mojibake»).
Это происходит потому, что при перенаправлении вывода в файл в вышеописанном сценарии в процесс вмешивается объект класса System.Console
, в свойстве которого OutputEncoding
у меня по умолчанию записан объект, представляющий кодировку «CP866» (устаревшая 8-битная кодировка, входящая в группу устаревших кодировок «OEM» операционных систем «Windows»). Значение по умолчанию для свойства OutputEncoding
объекта класса System.Console
зависит от языка системы (по-английски «system locale»; не путать с «языком интерфейса», по-английски «display language» или «interface language»). Для языка системы «ru-RU» («Русский (Россия)») кодировкой по умолчанию в группе кодировок «OEM» является кодировка (кодовая страница) «CP866», так как в нее включен русский алфавит.
Текущее значение языка системы я получил в блоке кода выше с помощью командлета Get-WinSystemLocale
.
Свойство OutputEncoding
объекта класса System.Console
не следует путать с предопределенной переменной $OutputEncoding
, это разные вещи. Как видно из блока кода выше, по умолчанию в программе-оболочке «PowerShell» версии 7 эта переменная содержит объект, представляющий кодировку UTF-8 (кодовая страница 65001).
В результате настроек по умолчанию, показанных в блоке кода выше, при применении команды git diff > my.patch
байты текста в кодировке UTF-8 сначала интерпретируются побайтно по кодовой странице «CP866», а затем полученные символы конвертируются в байты по правилам кодировки UTF-8. Вот как это происходит на примере буквы «Ш» слова «Шаланды» (кому интересно, у меня есть более подробный разбор):
D0 A8 -----------------> D0 A8 --------------> ╨ и
Ш интерпретация ╨ и конвертация E2 95 A8 D0 B8
UTF-8 как CP866 в UTF-8
Как видно из блока кода выше, мало того, что в результате получаются кракозябры, так еще размер полученного текста в байтах может вырасти в 2-3 раза (в примере выше из двух изначальных байт D0 A8
получено пять байт E2 95 A8 D0 B8
). Понятно, что всё это не касается символов из таблицы ASCII (в том числе символов латиницы), так как эти символы в кодировках «CP866» и «UTF-8» отображаются одинаково (одними и теми же кодами, и занимают по одному байту).
Конечно, результат можно конвертировать обратно. Например, в редакторе «VS Code» есть нужные для этого инструменты. Но правильнее будет перед выгрузкой файла-патча просто изменить соответствующую настройку:
PS C:\> [System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8
PS C:\> ([System.Console]::OutputEncoding).CodePage
65001
PS C:\Users\Илья\source\repos\test> git diff > my.patch
Теперь команда git diff > my.patch
создаст файл «my.patch» в корректном, то есть читаемом, виде, в кодировке UTF-8, размером 507 байтов. От файла, полученного при сценарии 1, который имел размер в 493 байта, файл, полученный при сценарии 2, отличается только видом окончаний строк. При первом сценарии окончания строк были вида LF, при втором сценарии получились окончания строк вида CRLF (отсюда разница в 14 байтов, так как в файле-патче содержится 14 строк).
Следует иметь в виду, что в программе-оболочке «PowerShell» перенаправление вывода >
делает то же самое, что и передача вывода по конвейеру |
командлету Out-File
(с указанием пути к файлу-результату, но без дополнительных параметров).
В отличие от команды git diff > my.patch
в первом сценарии, в данном случае строки от команды git diff
передаются на конвейер |
в виде 14 объектов (как уже было сказано выше, в файле-патче получилось 14 строк) класса System.String
. Однако, эти строки-объекты не содержат окончаний строк, это касается и тех строк, которые получены из Git-репозитория (это поведение отличается от описанного в сценарии 1). Программа-оболочка «PowerShell» версии 7 записывает полученные строки в файл с окончаниями вида CRLF.
Что делать, если мы хотим получить в этом сценарии файл-патч с окончаниями строк вида LF? Способ попроще: открыть полученный файл в редакторе кода и преобразовать окончания строк в окончания нужного вида. Инструменты для этого есть, например, в редакторе «Notepad++». Способ посложнее, из командной строки: вместо команды git diff > my.patch
в блоке кода выше можно ввести, к примеру, такую команду:
PS C:\Users\Илья\source\repos\test> git diff |
>> Join-String -Separator "`n" -OutputSuffix "`n" |
>> Out-File -FilePath "my.patch" -NoNewline
Суть тут в том, что мы получаем от команды git diff
14 объектов-строк, склеиваем их в один объект-строку с помощью командлета Join-String
, вставляя между ними (параметр -Separator
) символ LF и в конце (параметр -OutputSuffix
) символ LF. После этого передаем полученный один объект-строку командлету Out-File
, запрещая ему с помощью параметра -NoNewline
вставку умолчательного окончания строки CRLF после нашего одного итогового объекта-строки (у меня есть подробный разбор этой команды).
Применение файла-патча с помощью команды «git apply»
Думаю, понятно, что применять созданный нами файл-патч будут другие люди на их копии нашего проекта (или на оригинале проекта, если наш проект является копией оригинала). Чтобы смоделировать эту ситуацию, создадим копию нашего проекта, на которой будем пытаться применить файл-патч. Вернее, мы будем создавать копию Git-репозитория с помощью команды «git clone».
Ранее, в рамках подготовки тестового проекта, я создал Git-репозиторий, но ничего в него не поместил, поэтому Git-репозиторий пока что остался пустым. Я поместил только исходную версию файла «shalandy.txt» в индекс, а в рабочей папке создал измененную версию исходного файла «shalandy.txt», после чего из разниц между этими версиями с помощью команды «git diff» создал файл-патч «my.patch». Напомню, индекс — это отдельный служебный файл, он не входит в состав базы данных «Git», поэтому Git-репозиторий всё еще считается пустым.
Шаг 5. Поместим в базу данных «Git» первый коммит, сформировав его из содержимого индекса:
PS C:\Users\Илья\source\repos\test> git commit -m "Первый коммит"
[master (root-commit) 3834f65] Первый коммит
1 file changed, 4 insertions(+)
create mode 100644 shalandy.txt
Шаг 6. Создание копии (клона) исходного Git-репозитория:
PS C:\Users\Илья\source\repos\test> cd ..
PS C:\Users\Илья\source\repos> git clone test test2
Cloning into 'test2'...
done.
Команда cd
в программе-оболочке «PowerShell» — это псевдоним (alias) командлета «Set-Location». А вообще это известная команда в разных операционных системах. Параметр ..
означает родительскую папку для текущей папки. То есть команда cd ..
выполняет переход на ближайший верхний уровень от текущего местоположения в дереве папок. В результате мы перешли в местоположение «C:\Users\Илья\source\repos\».
Этот переход не обязателен для применения команды «git clone», но он делает ввод параметров для этой команды более удобным: после перехода нам достаточно набрать название исходной папки (наш проект) и название новой папки (новый проект, копия нашего), без набора полного пути к этим папкам. (Вообще, команду «git clone» можно применить несколькими способами, об этом можно прочитать в документации.)
Итак, первый параметр test
команды «git clone» в данном случае — название исходной папки нашего проекта. Папки test2
до ввода вышеприведенной команды «git clone» в местоположении «C:\Users\Илья\source\repos\» у меня не существовало. Команда «git clone» у меня создала папку с таким названием, после чего создала внутри вложенную папку «.git» (скрытую) и скопировала в нее базу данных «Git» моего оригинального проекта «test».
Если бы в данном случае папка «test2» (папка назначения) существовала бы в указанном местоположении, команда «git clone» выполнила бы клонирование, если б папка «test2» была бы пуста. Иначе, если бы в папке «test2» были бы какие-то файлы, то команда «git clone» не выполнила бы клонирование и выдала бы соответствующее предупреждающее сообщение.
После того, как команда «git clone» у меня успешно отработала и создала новую папку «test2» с вложенной в нее скрытой папкой «.git» (скопированная из оригинального проекта база данных «Git»), в рабочей папке «test2» появился и файл «shalandy.txt». Тут следует понимать, что этот файл не был скопирован из рабочей папки оригинального проекта «test», а был восстановлен из коммита в скопированной базе данных «Git». Таким образом, эта версия файла «shalandy.txt» — это первоначальная версия этого файла из оригинального проекта. То есть на данный момент файлы «shalandy.txt» в папке «test» и в папке «test2» отличаются друг от друга на разницу в файле-патче «my.patch», полученном в предыдущих постах. Всё готово для тестирования применения файла-патча с помощью команды «git apply».
Шаг 7. Применение файла-патча «my.patch» к файлу «shalandy.txt» в папке «test2»:
PS C:\Users\Илья\source\repos> cd test2
PS C:\Users\Илья\source\repos\test2> git apply "..\test\my.patch"
Первая команда из двух в блоке кода выше была уже объяснена ранее. Здесь мы с помощью этой команды cd test2
переходим в только что созданную папку с копией (клоном) нашего исходного проекта (вернее, с копией Git-репозитория нашего исходного проекта). Это нужно потому, что команда «git apply» применяет указанный файл-патч к текущей рабочей папке, так что в эту папку сначала следует перейти.
Далее запускаем команду «git apply», которая и применяет файл-патч к файлам в текущей рабочей папке текущего проекта. В данном случае я указал относительный путь "..\test\my.patch"
, который означает, что файл-патч «my.patch» находится в соседней для папки «test2» папке «test» (в рабочей папке исходного проекта).
В итоге у меня команда «git apply» выполнилась успешно, то есть исходная версия файла «shalandy.txt» в рабочей папке проекта «test2» преобразовалась в измененную версию (217 байтов, кодировка UTF-8, окончания строк вида CRLF).
Важно отметить, что всё работает успешно, если в файле-патче «my.patch» окончания строк вида LF. Если там окончания строк вида CRLF, то у меня выдаются сообщения об ошибке следующего вида:
PS C:\Users\Илья\source\repos\test2> git apply "..\test\my.patch"
error: patch failed: shalandy.txt:1
error: shalandy.txt: patch does not apply
В итоге всё это работает не слишком интуитивно: в файле-патче окончания строк вида LF, а в файле, к которому применяется файл-патч, окончания строк оказываются вида CRLF.
Работа с патчем на примере веб-приложения «WordPress»
Существует такое довольно известное веб-приложение «WordPress», которое представляет собой систему управления содержимым сайта. Это проект с открытым исходным кодом. Вы можете внести вклад в код этого проекта разными способами, в частности, предложив файл-патч. Вообще, исходный код веб-приложения «WordPress» находится под управлением программы (системы управления версиями) «Subversion» или сокращенно «SVN», но эта программа принимает патчи, сформированные и из программы «Git».
В принципе, когда разработчик хочет поработать с исходным кодом какого-либо проекта, находящегося под управлением программы «Git», он клонирует Git-репозиторий этого проекта к себе на компьютер и дальше уже работает с этим клоном (существуют зеркала репозитория с исходным кодом для программы «Git»). Но мне удобнее показать создание файла-патча на уже развернутом у меня локально экземпляре веб-приложения «WordPress».
Я установил это веб-приложение (дистрибутив можно загрузить с сайта проекта) к себе на компьютер в папку «C:\inetpub\wwwroot\wp\» (эта папка будет корневой папкой нашего проекта). Я решил предложить небольшое изменение в код одного из файлов проекта: «wp-includes\class-requests.php» (путь к файлу указан относительно корневой папки проекта).
Шаг 1. Создание пустого Git-репозитория для проекта:
PS C:\inetpub\wwwroot\wp> git init
Initialized empty Git repository in C:/inetpub/wwwroot/wp/.git/
Шаг 2. Настройка режима работы с окончаниями строк для проекта:
PS C:\inetpub\wwwroot\wp> git config core.autocrlf
true
PS C:\inetpub\wwwroot\wp> git config --local core.autocrlf input
PS C:\inetpub\wwwroot\wp> git config core.autocrlf
input
Из блока кода выше видно, что я переключил настройку «core.autocrlf» для проекта со значения «true» на значение «input». Этот шаг делать не обязательно. Дело в том, что файлы веб-приложения «WordPress» из его дистрибутива написаны с окончаниями строк вида LF. Настройкой «core.autocrlf=input» я хочу сохранить окончания строк в файлах проекта (в рабочей папке) в исходном виде при извлечении (checkout) версий файлов из Git-репозитория. При настройке «core.autocrlf=true» исходные окончания строк в файлах проекта (в рабочей папке) будут в такой ситуации затерты окончаниями строк вида CRLF.
Шаг 3. Запишем оригинальную версию файла «wp-includes\class-requests.php» в индекс:
PS C:\inetpub\wwwroot\wp> git add "wp-includes\class-requests.php"
Шаг 4. Внесем изменение в файл «wp-includes\class-requests.php» в рабочей папке проекта. Суть изменения в рамках данной статьи неважна, но кому интересно, можно прочитать подробный разбор по следующей ссылке.
Шаг 5. Создание файла-патча из программы-оболочки «Git Bash» и помещение этого файла на рабочий стол (кавычки, обособляющие путь к файлу-результату, в данном случае обязательны):
Илья@IlyaComp MINGW64 /c/inetpub/wwwroot/wp (master)
$ git diff > "C:\Users\Илья\Desktop\57325.diff"
После этого на моем рабочем столе появился файл-патч «57325.diff» размером 444 байта в кодировке UTF-8 с окончаниями строк вида LF. В проекте «WordPress» принимают файлы-патчи с расширением либо «.diff», либо «.patch».
Предварительно вы должны создать в системе управления проектом (это можно сделать через сайт wordpress.org проекта, для этого требуется регистрация на сайте) сообщение об ошибке (по-английски его называют «ticket») с подробным описанием ошибки. После чего к этому сообщению об ошибке можно приложить файл-патч с предлагаемым исправлением ошибки. Название файла-патча должно совпадать с номером сообщения об ошибке. То есть в моем случае я приложу полученный выше файл-патч «57325.diff» к предварительно созданному сообщению об ошибке с номером 57325.
Вот что у меня получилось в результате в системе управления проектом «WordPress» на сайте wordpress.org:
Файл-патч, приложенный к сообщению об ошибке номер 57325.