Работа со сторонними исходными текстами
Рассмотрим ситуацию, когда сторонние исходники используются, но нужно их немного поправить:
- Потому что это правка, необходимая только вашему проекту
- Потому что вы — майнтейнер пакета в Linux, а исходники не соответствуют принятой в вашем сообществе дисциплине, или апстрим не считает происходящее ошибкой, а вы считаете ☺
- …
В любом случае при обновлении апстрима исправления придётся вносить заново
Работа с патчами
Цикл обновления / внесения изменений:
- Первоначальный импорт исходников
- Адаптация к вашим нуждам
Оформление серии патчей (см. ниже)
- Релиз
- Отныне и навеки
- Обновление апстримной версии
- Применение патчей к обновлённой версии
- Адаптация отвалившихся патчей
- Релиз
diff
Утилита diff — построчное сравнение и демонстрация изменений двух текстов
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <ctype.h>
6
7 /* open()->fopen() wrapper */
8 static FILE *ffopen(const char *pathname, const char *mode, int flags)
9 {
10 int m;
11
12 switch(tolower(mode[0])+(mode[1]=='+')) {
13 case 'b': /* "a+" */
14 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break;
15 case 'r': m = O_RDONLY; break;
16 case 's': m = O_RDWR; break; /* "r+" */
17 case 'x': /* "w+" */
18 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break;
19 default: m = O_RDONLY; break;
20 }
21
22 int fd = open(pathname, flags, m);
23 return fd<0? NULL: fdopen(fd, mode);
24 }
25
26 int main(int argc, char *argv[]) {
27 FILE *fp;
28
29 fp = ffopen(argv[1], "r", O_NOFOLLOW);
30 if(fp == NULL) {
31 perror(argv[1]);
32 return 1;
33 }
34
35 return 0;
36 }
|
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <ctype.h>
6
7 /* open()->fopen() wrapper */
8 static FILE *ffopen(const char *pathname, const char *mode, int flags)
9 {
10 int m;
11
12 switch(tolower(mode[0])+(mode[1]=='+')) {
13 case 'b': /* "a+" */
14 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break;
15 case 'r': m = O_RDONLY; break;
16 case 's': m = O_RDWR; break; /* "r+" */
17 case 'x': m = O_RDWR | O_CREAT | O_TRUNC; break; /* w+ */
18 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break;
19 default: m = O_RDONLY; break;
20 }
21
22 int fd = open(pathname, flags, m);
23 return fd<0? NULL: fdopen(fd, mode);
24 }
25
26 int main(int argc, char *argv[]) {
27 FILE *fp;
28
29 fp = ffopen(argv[1], "r", O_NOFOLLOW);
30 if(fp == NULL) {
31 perror(argv[1]);
32 return 1;
33 }
34
35 return 0;
36 }
|
Просто diff: как из одного файла сделать другой: diff ffopen.c ffopen_new.c
diff -e diff ffopen.c ffopen_new.c — команды для текстового редактора ed
diff -c — разница вместе с окружающим её контекстом
1 *** ffopen.c 2024-11-12 15:00:55.592882805 +0300 2 --- ffopen_new.c 2024-11-12 15:00:55.619883265 +0300 3 *************** 4 *** 14,20 **** 5 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 6 case 'r': m = O_RDONLY; break; 7 case 's': m = O_RDWR; break; /* "r+" */ 8 ! case 'x': /* "w+" */ 9 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 10 default: m = O_RDONLY; break; 11 } 12 --- 14,20 ---- 13 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 14 case 'r': m = O_RDONLY; break; 15 case 's': m = O_RDWR; break; /* "r+" */ 16 ! case 'x': m = O_RDWR | O_CREAT | O_TRUNC; break; /* w+ */ 17 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 18 default: m = O_RDONLY; break; 19 }
diff -u — наиболее удобный формат
1 --- ffopen.c 2024-11-12 15:00:55.592882805 +0300 2 +++ ffopen_new.c 2024-11-12 15:00:55.619883265 +0300 3 @@ -14,7 +14,7 @@ 4 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 5 case 'r': m = O_RDONLY; break; 6 case 's': m = O_RDWR; break; /* "r+" */ 7 - case 'x': /* "w+" */ 8 + case 'x': m = O_RDWR | O_CREAT | O_TRUNC; break; /* w+ */ 9 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 10 default: m = O_RDONLY; break; 11 }
@@ -9,9 +9,10 @@ означает: «в строчке 9 исходного файла взять 9 строк и превратить их в 10 строк целевого файла»
Есть варианты -C размер контекста и -U размер контекста
- Вариант с несколькими изменениями (chunks):
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <ctype.h> 6 7 /* open()->fopen() wrapper */ 8 static FILE *ffopen(const char *pathname, const char *mode, int flags) 9 { 10 int m; 11 12 switch(tolower(mode[0])+(mode[1]=='+')) { 13 case 'b': /* "a+" */ 14 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 15 case 'r': m = O_RDONLY; break; 16 case 's': m = O_RDWR; break; /* "r+" */ 17 case 'x': /* "w+" */ 18 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 19 default: m = O_RDONLY; break; 20 } 21 22 int fd = open(pathname, flags, m); 23 return fd<0? NULL: fdopen(fd, mode); 24 } 25 26 int main(int argc, char *argv[]) { 27 FILE *fp; 28 29 fp = ffopen(argv[1], "r", O_NOFOLLOW); 30 if(fp == NULL) { 31 perror(argv[1]); 32 return 1; 33 } 34 35 return 0; 36 }
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <ctype.h> 6 7 /* open()->fopen() wrapper */ 8 static FILE *ffopen(const char *pathname, const char *mode, int flags) 9 { 10 int m; 11 12 switch(tolower(mode[0])+(mode[1]=='+')) { 13 case 'b': /* "a+" */ 14 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 15 case 'r': m = O_RDONLY; break; 16 case 's': m = O_RDWR; break; /* "r+" */ 17 case 'x': m = O_RDWR | O_CREAT | O_TRUNC; break; /* w+ */ 18 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 19 default: m = O_RDONLY; break; 20 } 21 22 int fd = open(pathname, flags, m); 23 return fd<0? NULL: fdopen(fd, mode); 24 } 25 26 int main(int argc, char *argv[]) { 27 FILE *fp; 28 29 fp = ffopen(argv[1], "r", O_NOFOLLOW); 30 if(fp == NULL) { 31 perror(argv[1]); 32 return 1; 33 } 34 35 return 0; 36 }
⇒ diff -u ffopen.c ffopen_new2.c
1 --- ffopen.c 2024-11-12 15:00:55.592882805 +0300 2 +++ ffopen_new2.c 2024-11-12 15:00:55.639883606 +0300 3 @@ -14,7 +14,7 @@ 4 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 5 case 'r': m = O_RDONLY; break; 6 case 's': m = O_RDWR; break; /* "r+" */ 7 - case 'x': /* "w+" */ 8 + case 'x': m = O_RDWR | O_CREAT | O_TRUNC; break; /* w+ */ 9 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 10 default: m = O_RDONLY; break; 11 } 12 @@ -31,6 +31,8 @@ 13 perror(argv[1]); 14 return 1; 15 } 16 + else 17 + fclose(fp); 18 19 return 0; 20 }
Утилиту diff можно запустить рекурсивно (-r), в выводе будет несколько файлов (отмеченных --- и +++).
patch
Утилита patch умеет применять результат diff к исходному файлу, при этом получается целевой файл
Этот-то «результат diff» и называется патчем
Файл ffopen.c стал таким же, как ffopen_new.c
- Несколько патчей в правильном порядке называются патчсетом
Файл ffopen.c стал таким же, как ffopen_new2.c
Неточный контекст в patch
Что гораздо важнее, patch умеет находить перемещение контекста и даже определять приблизительное совпадение контекста:
Возьмём слегка изменённый файл ffopen2.c (в нём добавлено два комментария)
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <ctype.h> 6 7 /* open()->fopen() wrapper */ 8 static FILE *ffopen(const char *pathname, const char *mode, int flags) 9 { 10 int m; 11 12 switch(tolower(mode[0])+(mode[1]=='+')) { 13 case 'b': /* "a+" */ 14 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 15 case 'r': m = O_RDONLY; break; 16 // TODO "b" modifier 17 case 's': m = O_RDWR; break; /* "r+" */ 18 case 'x': /* "w+" */ 19 case 'w': m = O_WRONLY | O_CREAT | O_TRUNC; break; 20 default: m = O_RDONLY; break; 21 } 22 23 int fd = open(pathname, flags, m); 24 return fd<0? NULL: fdopen(fd, mode); 25 } 26 27 /* just for test purpose */ 28 int main(int argc, char *argv[]) { 29 FILE *fp; 30 31 fp = ffopen(argv[1], "r", O_NOFOLLOW); 32 if(fp == NULL) { 33 perror(argv[1]); 34 return 1; 35 } 36 37 return 0; 38 }
и применим к нему патч, предназначенный для исходного файла:
Контекст первого блока неточен (в патче одна строка контекста слегка другая); тем не менее это очень похожий контекст (с разницей 1, что бы это ни значило), так что блок применён
- Контекст второго блока точен, но найден со смещением в одну строку, блок применён
Получившийся файл имеет свойства как общего предка (ffopen.c), так и обоих родителей (ffopen2.c и ffopen_changed.c)
В случае, когда какие-то блоки применяются, а какие-то — нет, patch создаёт reject-файл — патч, содержащий только неприложившиеся блоки.
Например, переставим в ffopen2.c комментарий относительно "b" и case 's' местами
И перестановка, и модификация строк делает контекст слишком различным, такой блок не приложится; второй блок приложится нормально:
1 $ patch --verbose ffopen2.c < change.patch 2 Hmm... Looks like a unified diff to me... 3 The text leading up to this was: 4 -------------------------- 5 |--- ffopen.c 2024-11-12 15:34:19.116050449 +0300 6 |+++ ffopen_new2.c 2024-11-12 15:33:32.052246203 +0300 7 -------------------------- 8 patching file ffopen2.c 9 Using Plan A... 10 Hunk #1 FAILED at 14. 11 Hunk #2 succeeded at 33 (offset 2 lines). 12 1 out of 2 hunks FAILED -- saving rejects to file ffopen2.c.rej 13 done 14
Строчка Hmm... означает, что помимо собственно патча утилита patch нашла какой-то неопределённый текст (заголовок, выдаваемый diff)
- Этот reject файл предполагается открыть редактором, посмотреть в него, и ручками применить описанные там различия
Ручное преобразование патчей
Что-то вроде примера:
1 ## combinediff
2 $ rm -rf *.old* p?.patch
3 $ cal -y 2024 > calend
4 $ sed -i.old1 's/21 13/21...13/' calend
5 $ diff -u calend.old1 calend > p1.patch
6 $ sed -i.old2 's/ябрь/ёр../g' calend
7 $ diff -u calend.old2 calend > p2.patch
8 $ sed -i.old3 's/[.]/ /g' calend
9 $ diff -u calend.old3 calend > p3.patch
10 $ combinediff p1.patch p2.patch | combinediff - p3.patch
11
12 ## interdiff
13 $ interdiff p1.patch p2.patch
14
15 ## splitdiff
16 $ cp calend.old1 calend # restore
17 $ cat p?.patch > pall.patch
18 $ patch < pall.patch
19 $ cp calend.old1 calend # restore
20 $ splitdiff pall.patch
21
22 $ { echo "QQ"; cat calend.old1; } > calend
23 $ patch < pall.patch
combinediff — слияние двух патчей (инкрементальные в один кумулятивный)
interdiff — превращение кумулятивного патча2 в инкрементальный относительно патча1
splitdiff — несколько патчей по отдельным файлам
lsdiff — какие файлы модифицируются в патче
grepdiff — поиск РВ в патче — какие файлы модифицируются строкой с этим РВ
filterdiff — подпатч только по указанным файлам
recountdiff — пересчитать годный патч со смещением и/или изменившимся размером контекста
rediff — пересчитать патч, отредактированный вручную (со всякими ещё проблемами)
Использование git
Команда git diff порождает не что иное, как diff -u. Предположим, мы отредактировали файл ffopen.c в git-репозитории. Вот что покажет git diff:
1 diff --git a/ffopen.c b/ffopen.c 2 index 7ac8822..bf6ba89 100644 3 --- a/ffopen.c 4 +++ b/ffopen.c 5 @@ -9,9 +9,10 @@ static FILE *ffopen(const char *pathname, const char *mode, int flags) 6 { 7 int m; 8 9 + /* Simulate fopen flags */ 10 switch(tolower(mode[0])+(mode[1]=='+')) { 11 - case 'b': /* "a+" */ 12 case 'a': m = O_WRONLY | O_CREAT | O_APPEND; break; 13 + case 'b': /* "a+" */ 14 case 'r': m = O_RDONLY; break; 15 case 's': m = O_RDWR; break; /* "r+" */ 16 case 'x': /* "w+" */ 17 @@ -31,6 +32,8 @@ int main(int argc, char *argv[]) { 18 perror(argv[1]); 19 return 1; 20 } 21 + else 22 + fclose(fp); 23 24 return 0; 25 }
Поскольку git сравнивает две версии одного и того же файла, для большей понятности исходный файл считается лежащим в воображаемом каталоге a/, а целевой — в воображаемом каталоге b/.
В заголовке патча присутствуют (урезанные) идентификаторы сравниваемых объектов (7ac8822 и bf6ba89)
Умный git догадался, что это файл на Си (?) и в описании контекста добавил сигнатуру функции, в которой он встретился (это комментарий)
Если сейчас сделать commit, тот же формат будет у git log -p (+загловок коммита)
- Из последовательности коммитов можно сделать патчсет (в примере патч один)
1 $ git format-patch HEAD^ 2 0001-FFopen-updated.patch 3 $ head -20 0001-FFopen-updated.patch 4 From e3522f55a58d7f3ea6d85d518ef55a9d36ce887e Mon Sep 17 00:00:00 2001 5 From: "George V. Kouryachy (Fr. Br. George)" <george@altlinux.ru> 6 Date: Tue, 8 Dec 2020 21:11:51 +0300 7 Subject: [PATCH] FFopen updated 8 9 --- 10 ffopen.c | 5 ++++- 11 1 file changed, 4 insertions(+), 1 deletion(-) 12 13 diff --git a/ffopen.c b/ffopen.c 14 index 7ac8822..bf6ba89 100644 15 --- a/ffopen.c 16 +++ b/ffopen.c 17 @@ -9,9 +9,10 @@ static FILE *ffopen(const char *pathname, const char *mode, int flags) 18 { 19 int m; 20 21 + /* Simulate fopen flags */ 22 switch(tolower(mode[0])+(mode[1]=='+')) { 23 - case 'b': /* "a+" */ 24
Это патч можно применить к старой версии файла!
С помощью patch -p1 < 0001-FFopen-updated.patch
Если не указывать, какой файл патчим, patch возьмёт её из заголовка
Поэтому надо с помощью -p удалить один уровень воображаемых каталогов a/ или b/
С помощью git apply 0001-FFopen-updated.patch
С помощью git am 0001-FFopen-updated.patch (при этом произведётся также и commit с данными из заголовка)
Важно: правила наложения патчей в git очень строгие: fuzz не допускается. Возможно, накладывать старый патч на обновлённые исходники лучше всё-таки patch-ем
Сочетание git и patch
Если никакого git-репозитория нет, всё равно удобно использовать git:
- Создаём git-репозиторий
Помечаем тегом, например, base
- Накатываем патч
- Часть блоков прикладывается, част отваливается
- Коммитим то, что приложилось
- Глядя в reject-файлы применяем вручную то, что не приложилось, проверяем сборку, и тоже коммитим, возможно, несколько раз
Выкатываем преобразованный (и унифицированный под patch -p1) патчсет:
git format-patch base (патчи будут пронумерованы, а их названия — начало первой строки соответствующих commit mnessages)
Можно слить всё в один коммит с помощью git rebase -i base и последующего fixup
Д/З
- Написать на Си программу, которая генерирует случайный лабиринт размера 6×6
- Минимальные требования:
Лабиринт — это последовательность строк из «.» и «#».
- Длина строки — 13 символов, количество строк также 13. Таким образом, он состоит из «комнат» 3×3 с общими «стенами». Пример полностью непроходимого лабиринта 6×6:
############# #.#.#.#.#.#.# ############# #.#.#.#.#.#.# ############# #.#.#.#.#.#.# ############# #.#.#.#.#.#.# ############# #.#.#.#.#.#.# ############# #.#.#.#.#.#.# #############
- Блоки по сторонам от точек — это «стены», а по диагоналям — «углы». Чтобы пройти из комнаты в комнату, надо разрушить стену. Пример лабиринта 6×6 без стен:
############# #...........# #.#.#.#.#.#.# #...........# #.#.#.#.#.#.# #...........# #.#.#.#.#.#.# #...........# #.#.#.#.#.#.# #...........# #.#.#.#.#.#.# #...........# #############
Лабиринт, генерируемый программой, должен быть проходимым из любой комнаты в любую
(необязательно) Путь между комнатами должен быть единственным. Пример:
############# #.......#...# #######.#.#.# #...#...#.#.# #.###.###.#.# #...#.....#.# ###.#######.# #.......#...# #.#######.### #...#.....#.# #.#.#.#####.# #.#.........# #############
- Приложить к файлу набор из минимум трёх unified-патчей, которые модифицируют программу следующим образом:
- Размер лабиринта задаётся из командной строки первым параметром
- Первый параметр — это строка из двух символов, «проход» и «стена» выводимого лабиринта, а второй — его размер
- Первый параметр — начальное значение генератора псевдослучайных чисел (для воспроизведения лабиринта), второй и третий — «проход»-«стена» и размер соответственно.
Написать Makefile (или аналог), который будет
- генерировать три дополнительных исходника, соответствующх добавлению очередного патча
- генерировать, соответственно, четыре бинарника
иметь цель run, которая запускает все четыре бинарника с заданными параметрами
иметь цель clean, уничтожающую все генераты
- Минимальные требования:
Создать в репозитории с домашними заданиями подкаталог 09_PatchDiff и поместить туда решение.
Получилось примерно так: