Синхронизируем данные с yarsync
===============================

Привет, Хабр!

*yarsync* - Yet Another Rsync - предназначен для синхронизации данных между несколькими устройствами, более точно - между файловыми системами в Unix-подобных средах. *yarsync* обладает интерфейсом, похожим на *git*, и является Python-обёрткой вокруг программы *rsync*. Программа доступна под свободной лицензией GPL v3.0 на [github](https://github.com/ynikitenko/yarsync) (я автор).

*yarsync* работает там, где есть Питон и *rsync*.
Данные могут синхронизироваться локально или между разными компьютерами (в таком случае на удалённой машине также должен быть установлен *rsync*). Кроме того, файловые системы должны поддерживать жёсткие ссылки (hard links). Популярные системы, [поддерживающие жёсткие ссылки](https://github.com/ynikitenko/yarsync#hard-links) - ext2-ext4, HFS+, а также NTFS. Не поддерживают жёсткие ссылки FAT, exFAT (часто используемые на флеш-накопителях).

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

Прежде чем говорить о дизайне, стоит обозначить цели *yarsync*, а ими являются:

- удобство пользователя. Оно не только включает в себя привычный интерфейс, но и снижает риск ошибок.
- производительность. *rsync* использует эффективный алгоритм для передачи данных (передавая только различия). Уже переданные файлы не передаются вновь (даже если были перемещены или переименованы). Программа вызывается когда необходимо и не занимает постоянно процессор и память.
- надёжность. Первый выпуск *rsync* был в [1996 году](https://ru.wikipedia.org/wiki/Rsync), и с тех пор она является практически стандартной программой для синхронизации, то есть проверена множеством пользователей (к сожалению, мне не удалось найти даже примерное их число), и поддерживается и развивается по сегодняшний день (последняя версия вышла пять дней назад).
- прозрачность для системы. Служебная информация (коммиты) является обычными файлами, не требующими упаковки и распаковки.

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

Начало работы
-------------

Скопируйте программу:

    $ git clone https://github.com/ynikitenko/yarsync

Внутри репозитория находится подпапка *yarsync*, в ней питоновский модуль *yarsync.py* и исполняемый файл *yarsync* (вызывающий первый). Нужно добавить путь к этим файлам, например, в *~/.bashrc*:

    export PATH=$PATH:~/yarsync/yarsync
    export PYTHONPATH=$PYTHONPATH:~/yarsync

Если программа находится и работает, то для изучения простых команд создадим новую директорию

    me@myhost$ mkdir ~/tmp
    me@myhost$ cd ~/tmp

Инициализируем репозиторий:

    me@myhost$ yarsync init
    # init configuration for myhost
    mkdir .ys
    create configuration file .ys/config.ini

Как видно из выхода программы, при инициализации создаётся скрытая директория *.ys* с конфигурационным файлом *config.ini* (он пока пуст, и подробнее мы обсудим его ниже).
Все служебные данные будут находиться только внутри директории *.ys* (также при частом использовании я использую для *yarsync* псевдоним (alias) *ys*).
Для инициализации репозитория с существующими данными можно вызвать *yarsync init* внутри нужной директории. Если репозиторий уже был инициализирован, то эта команда остаётся безопасной (то есть ничего не делает).

Различные состояния репозитория фиксируются в коммитах, которые создаются с помощью команды *commit*:

    me@myhost$ yarsync commit
    rsync -a --link-dest=../../.. --exclude=/.ys --exclude=/.ys/* /home/me/tmp/ /home/me/tmp/.ys/commits/1650462990_tmp
    mv /home/me/tmp/.ys/commits/1650462990_tmp /home/me/tmp/.ys/commits/1650462990
    mkdir /home/me/tmp/.ys/logs
    commit 1650462990 created
    
    When: Wed, 20 Apr 2022 16:56:30 MSK
    Where: me@myhost

Как можно видеть, вывод программы в данный момент довольно подробный (часто публикуются полные команды *rsync*).
Сначала создаётся временный коммит в директории *.ys/commits*. С помощью команды *rsync* в директории коммита создаются жёсткие ссылки файлов из рабочей директории (то есть всех файлов кроме служебной директории). В данный момент, поскольку файлов нет, то коммит будет пустым.
Если всё прошло удачно, то коммит перемещается в директорию без суффикса *_tmp*. Кроме того, создаётся директория *.ys/logs*, куда записывается описание коммита (в нашем случае *.ys/logs/1650462990.txt* содержит время, пользователя и машину, где был создан коммит).

Название коммита - это число секунд с начала эпохи (Unix-время начинается 1 января 1970, 00:00:00 UTC), получаемое с помощью функции Python [time.time](https://docs.python.org/3/library/time.html#time.time). Это универсальное время, то есть названия коммитов будут упорядочены вне зависимости от часового пояса на различных машинах.

Также можно создать описание коммита с помощью опции *-m*:

    $ yarsync commit -m 'second commit'
    ...

Давайте добавим в репозиторий новый файл:

    $ touch example.txt
    $ yarsync status
    rsync -aun --delete -i --exclude=/.ys --exclude=/.ys/* --outbuf=L /home/me/tmp/ /home/me/tmp/.ys/commits/1650463725
    Changed since head commit:
    .d..t...... ./
    >f+++++++++ example.txt
    No syncronization information found.

Вывод программы даётся в формате опции *rsync -i* ([--itemize-changes](https://linux.die.net/man/1/rsync)). Первая строка обозначает, что корневая директория ('d') не изменилась ('.'), а точнее изменилась только её временная метка ('t'). На следующей строке мы видим, что со времени последнего ("головного") коммита (более по-русски будет сказать "снимка") появился наш новый файл. Как и ранее, директория *.ys* не принимается во внимание (*--exclude*), и никаких изменений при запросе статуса не происходит (*-n, --dry-run*).

Также в директории *.ys* можно создать файл *rsync-filter* с синтаксисом фильтров *rsync* (он очень богатый, смотрите его руководство). Пример его содержания (комментарии разрешены):

    # data can be copied separately
    - /data

В данном случае мы исключаем папку tmp/data из репозитория и игнорируем или синхронизируем её отдельно. Выделение отдельных подрепозиториев удобно в организации работы, но на носителях резервных копий удобнее линейная структура, чтобы можно было не искать вложения при синхронизации (хотя это можно решить, когда мы будем обсуждать работу с несколькими репозиториями одновременно).

Существующие коммиты можно посмотреть командой

    $ yarsync log

Полный список команд можно получить с помощью *yarsync --help*. Кроме того, директория *.ys* может находиться вне синхронизируемого каталога (с помощью опций *--config-dir* и *--root-dir*). [Моя первая публикация на Хабре](https://habr.com/ru/post/425259/) была о статических страницах сайта, которые поддерживали контроль версий с помощью чистого (bare) git-репозитория в отдельной папке.

Содержимое коммитов - это файлы и папки корневого репозитория (на момент их создания). Их можно просматривать с помощью обычного менеджера файлов или вызывать в них из терминала стандартные команды вроде *find*. Если вы удалили файл в рабочей директории, но потом решили его восстановить, то можете скопировать его (создать жёсткую ссылку) из коммита, где он был. Если же, напротив, вам не нужны старые файлы, то вы можете свободно удалить старые коммиты *rm -rf .ys/commits/<commit-number>*, при этом ни рабочая директория, ни инфраструктура *yarsync* не пострадают.

Синхронизация
-------------

Информация о репозиториях находится в файле *.ys/config.ini*. Создадим простую конфигурацию для копии наших данных в папке *~/tmp2*:

    [tmp2]
    # empty host means local host
    host =
    path = /home/me/tmp2

Если мы попробуем скопировать репозиторий туда с помощью *yarsync push tmp2*, то получим ошибку. Программа проверяет, что назначение (destination) действительно является корректным репозиторием (что не так, поскольку мы его ещё не создали). Также перед отправкой данных необходимо сохранить (commit) локальные изменения. Если мы уверены, что папка *~/tmp2* пуста или не существует, то мы можем клонировать туда наш репозиторий с помощью ключа *-f, --force*:

    $ yarsync push -f tmp2
    # rsync -avHP --delete-after --include=/.ys/commits --include=/.ys/logs --exclude=/.ys/* /home/me/tmp/ /home/me/tmp2/

При переходе в ту папку, мы увидим, что она идентична нашему первому репозиторию, как и коммиты и их история (проверьте с помощью *yarsync log*). Хотя коммиты и логи копируются полностью, конфигурационные файлы (*config.ini, rsync-filter* и другие в папке *.ys*) не копируются, то есть независимы друг от друга.

Ключ *rsync -H* означает связывание жёстких ссылок в *назначении* (в нашем случае *tmp2*) таким же образом, как и в *источнике*. Если мы посмотрим индексные дескрипторы (inodes) файлов *ls -i example.txt* в *tmp* и *tmp2*, то мы увидим, что они отличаются - при этом внутри одного клона они совпадают в коммитах и рабочей директории.

Ключ *--delete-after* требует, чтобы перед реальными изменениями *rsync* просканировал все файлы, то есть собрал все существующие жёсткие ссылки. Если между двумя репозиториями есть синхронизированный коммит, и в одном из них файл в последующем коммите был перемещён, то *rsync* увидит совпадающий дескриптор и не будет вновь пересылать существующий файл.

Если мы в процессе работы создали новый коммит в *tmp2*, то мы можем также перенести изменения обратно в *tmp*:

    $ cd ~/tmp
    $ yarsync pull tmp2

Разумеется, в конфигурационном файле в нескольких секциях могут быть настройки для большего числа репозиториев (*tmp3* и пр.). Полный синтаксис файла указан в модуле [configparser](https://docs.python.org/3/library/configparser.html#supported-ini-file-structure) стандартной библиотеки.

Часто бывает, что путь к репозиторию меняется. Например, если мы копируем данные по сети, то DHCP может выдать новый ip-адрес компьютера, а при подключении жёсткого диска на лету может быть сгенерирован уникальный путь в */run/media*. В таком случае можно использовать в конфигурации переменную окружения:

    [my_drive]
    path = $MYDRIVE/programming

и если мы зададим переменную *MYDRIVE*, то путь будет определён корректно:

    $ export MYDRIVE=/run/media/my_drive
    $ yarsync push my_drive

Кроме того, при синхронизации с другим репозиторием об этом сохраняется информация в *.ys/sync.txt*. В нашем случае в этом файле будет

    1650468609,tmp2

то есть номер коммита и название другого репозитория. Также информация о синхронизации будет отображаться в командах *status* и *log*:

    $ yarsync log
    commit 1650468609 <-> tmp2
    When: Wed, 20 Apr 2022 18:30:09 MSK
    ...
    
Слияние версий
--------------
--------------

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

Допустим, наши репозитории в *tmp* и *tmp2* синхронизированы. Создадим новые коммиты в каждой из реплик:

    $ cd ~/tmp2
    $ touch B
    $ yarsync commit -m 'Add B'
    ...
    commit 1650480050 created

и аналогично добавим файл 'A' в *tmp*. Теперь, когда мы попытаемся отправить данные из *tmp* в *tmp2*, то программа зафиксирует различающиеся коммиты и выдаст ошибку:

    $ yarsync push tmp2
    Nothing to commit, working directory clean.
    # local repository is 1 commits ahead of tmp2
    # rsync -avHP --delete-after --include=/.ys/commits --include=/.ys/logs --exclude=/.ys/* /home/me/tmp/ /home/me/tmp2/
    rsync --list-only /home/me/tmp2/.ys/commits/
    ! 
    destination has commits missing on source: 1650480050, synchronize these commits first:
    1) pull missing commits with 'pull --new',
    2) push if these commits were successfully merged, or
    2') optionally checkout,
    3') manually update the working directory to the desired state, commit and push,
    2'') --force local state to remote (removing all commits and logs missing on the destination).

Как мы видим, предлагается несколько вариантов действий. Самое простое, если мы уверены, что в *tmp2* не актуальные данные - записать туда состояние репозитория *tmp*, удалив все новые файлы с помощью опции *push -f*. Команда *push* проверяет, что в источнике все изменения были сохранены в коммит. В общем случае это невозможно сделать в удалённом репозитории (*rsync* не может копировать данные между двумя удалёнными машинами), поэтому изменения на другой машине могут быть не сохранены в её локальный коммит, и поэтому у команды *pull* опция *-f* отсутствует. Мы требуем, чтобы синхронизировались только сохранённые состояния - за исключением, о котором ниже.

У команды *pull* есть опция *--new* (которой нет у *push*), которая переносит только новые файлы с удалённого репозитория (не уничтожая локальные файлы, которые там отсутствуют):

    $ yarsync pull --new tmp2
    ...
    rsync --list-only /home/me/tmp2/.ys/commits/
    # rsync -avHP --include=/.ys/commits --include=/.ys/logs --exclude=/.ys/* /home/me/tmp2/ /home/me/tmp/
    merge 1650480038 and 1650480050 manually and commit (most recent common commit is 1650468609)

Как мы видим, в команде *rsync* здесь отсутствует флаг *delete*. Программа изучает коммиты в удалённом источнике, находит последний общий коммит (если он есть) и указывает последние коммиты в локальном и удалённом репозиториях, которые нужно синхронизировать.

В данный момент нам могут помочь несколько других команд:

    $ yarsync diff 1650480038 1650468609
    ...
    >f+++++++++ A

показывает, что чтобы перейти от общего коммита 1650468609 к 38-му (надеюсь, сокращение понятно), нужно добавить файл А. Так же мы можем посмотреть, что изменилось на другом репозитории с общего коммита (поскольку последний удалённый коммит уже скопирован локально). В данном случае это тривиально, но если вы в последний раз синхронизировали другую машину год назад, то эта информация будет очень полезна. Кроме того, поскольку наши коммиты хранятся в файловой системе, мы можем просто сравнить их с помощью *diff -r* (хотя придётся писать к ним пути), поэтому эта команда скорее для удобства.

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

    $ yarsync status
    ...
    Changed since head commit:
    >f+++++++++ A
    Merging 1650480038 and 1650480050 (most recent common commit 1650468609).
    # local repository is 2 commits ahead of tmp2
    $ ls
    A  B  example.txt

новым файлом считается *A*, поскольку последний коммит (головной) был сделан в *tmp2* (при этом в рабочей директории находятся оба файла).

Информация об объединении находится в файле *.ys/MERGE.txt*, который создаётся автоматически. Если мы сейчас сделаем *commit*, то этот файл будет удалён, а информация о слиянии добавится в log.

Но представим ситуацию, когда мы действительно пять минут назад создали новый небольшой коммит в *tmp2*, однако до этого очень долго работали в *tmp*, удаляли и переименовывали многие файлы в рабочей директории, и теперь вместе с состоянием *tmp2* мы вернули все эти файлы обратно (вместе с уже переименованными). В таком случае мы можем восстановить более актуальный коммит с помощью команды *checkout*:

    $ yarsync checkout 1650480038
    rsync -au --delete -i --exclude=/.ys --exclude=/.ys/* --outbuf=L /home/me/tmp/.ys/commits/1650480038/ /home/me/tmp
    *deleting   B
    .d..t...... ./

Когда мы выполняем *checkout*, то головным (head) коммитом становится не самый последний, а тот, который мы загрузили. В частности, если мы ничего не меняли, то команда *status* будет показывать разницу с загруженным коммитом (а не последним), с добавлением строки

    Detached HEAD (see 'yarsync log' for more recent commits)

Информация о головном коммите (если он не самый последний) сохраняется в *.ys/HEAD.txt*.

Если мы решим, что файл *B* нам больше не нужен, то мы можем прямо сейчас сделать коммит и отправить итоговую версию в *tmp2*:

    $ yarsync commit -m 'Merge.'
    $ yarsync push tmp2

Во время коммита и MERGE.txt, и HEAD.txt будут автоматически удалены, и новый коммит будет считаться корректным состоянием репозитория. Поскольку все коммиты с *tmp2* уже были скопированы локально, то сложностей с *push* уже не возникнет.

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

Реализация
----------

Написание исполняемой программы на Питоне не похоже на создание обычного модуля, а работа с командами не похожа на создание объектов с состояниями.

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

В одной из ранних версий я пытался зафиксировать опции *rsync* в отдельном объекте, но в итоге у меня остался только класс *YARsync*. Большинство его методов приватные. Более того, в отличие от привычных объектов, большинство его методов могут быть недоступны: если мы вызываем *yarsync status*, то происходит инициализация с данными аргументами командной строки, и метод *_pull_push* (они объединены, поскольку отличаются для *rsync* только порядком последних аргументов) мы вызвать просто не сможем, потому что неизвестен удалённый репозиторий. Огромную работу с всевозможными аргументами командной строки делает стандартный модуль *argparse*, а *rsync* вызывается с помощью *subprocess*.

Безопасность
------------

Обычно парсинг конфигурационных файлов (тем более с заменой переменных окружения) может быть небезопасным. Если директория *.ys* отсутствует в текущем каталоге, то она ищется в его родительских каталогах (как в git). Я вызываю *yarsync* в проверенных директориях, однако в *configparser* ничего не говорится о том, чтобы его использование было небезопасным, поэтому не могу быть уверен, есть ли здесь уязвимость или нет. Возможно, читатели подскажут на этот счёт.

Также к безопасности я отношу возможность удалить личные данные из репозитория (напомню, что у нас очень много коммитов, то есть недостаточно удалить файл из рабочей директории). Жёсткие ссылки здесь скорее в плюс, поскольку мы можем просто вызвать *shred* для нашего файла, и все его дубликаты будут одновременно стёрты. Затем мы можем удалить файлы с одним путём из всех коммитов, а если сомневаемся, не был ли он в какой-то момент перемещён, то можем найти его по иноду *find -inum*. Поскольку удалённые коммиты всё равно будут содержать этот файл, то нужно будет также стереть его там с помощью *push -f*.

Если вы синхронизируете личные файлы, то может быть важным их шифрование. Я пользуюсь стандартными зашифрованными разделами [LUKS](https://ru.wikipedia.org/wiki/LUKS), которые после открытия прозрачны для любой синхронизации. Также можно шифровать отдельные файлы с помощью [EncFS](https://github.com/vgough/encfs), основанной на FUSE, и она поддерживает жёсткие ссылки (кроме режима *paranoia*), то есть в принципе может использоваться с *yarsync*.

Главным аспектом безопасности я считаю сохранность данных и всегда вызываю *--dry-run* перед настоящими *push* и *pull*.

    $ yarsync push -n dest

покажет, что именно будет перенесено на *dest*, не делая физических изменений. Если существующий файл был изменён (в результате ошибки или сбоя файловой системы), то это тоже скорее всего будет отражено. Для более надёжной проверки у *rsync* есть опция *checksum* (гораздо более длительная, и мне она пока не потребовалась).

Альтернативы
------------

Синхронизация данных, видимо, является важной задачей для программистов и системных администраторов, потому что только перечисление всех известных инструментов потребовало бы отдельной статьи. Однако дать небольшой обзор альтернатив я считаю уместным (здесь [чуть более полный список](https://github.com/ynikitenko/yarsync#alternatives) инструментов, на которые я обратил внимание), в том числе чтобы показать их различия с *yarsync*.

Прежде всего, для синхронизации ценных изменяемых текстовых файлов и программ используются системы контроля версий. Я многие годы пользуюсь *git*, к которому у меня нет ни малейших претензий. Системы контроля версий предлагают значительно больше, чем просто синхронизацию, но положа руку на сердце, почти всегда я набираю *git push* в первую очередь для того, чтобы моя последняя работа не пропала. В свою очередь, если я скачал статью из интернета, то мне удобно, что она у меня сохранена, но её ценность для меня не критична и её версии мне не нужны (*yarsync* их не поддерживает).

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

К инструментам **непрерывной синхронизации** относятся Dropbox, Яндекс.Диск и многие другие облачные сервисы. Ранее я ими пользовался, но лично мне не очень нравится, что какой-то сервис постоянно занимает память и процессор. Такие программы, если и доступны под Linux, могут быть с закрытым кодом, что тоже минус (хотя и побудили меня лучше изучить изоляцию процессов и SELinux). Одним из главных минусов для меня явилось то, что бесплатный объём памяти в них гораздо меньше, чем возможный объём жёсткого диска, который я могу купить. Платный гугл-диск был мне удобен (хотя я уже не синхронизировал его с компьютером), но, по иzвестным причинам, в последнее время я уже физически не мог за него заплатить с российской карты. Кроме того, поскольку я не постоянно нахожусь в сети, то у меня не было уверенности, что при подключении синхронизация произойдёт корректно и файлы не будут утеряны. Понятными плюсами облачных сервисов является то, что не обязательно иметь с собой носитель информации (или поддерживать свой сервер), а также то, что синхронизация может происходить с разными устройствами (например, планшетом на Android). Из интересного отмечу, что, например, Dropbox [использует алгоритм rsync](https://ru.wikipedia.org/wiki/Rsync) через библиотеку *librsync*.

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

Существует множество программ для **бэкапа** и **архивирования**. Сама тема [резервного копирования](https://ru.wikipedia.org/wiki/Резервное_копирование) очень широка. Помимо сохранения, в ней также важен аспект *восстановления* данных из бэкапа. К сожалению, при изучении таких программ я периодически видел посты (issues) о том, как архив был повреждён, и в этом смысле прозрачные коммиты с жёсткими ссылками не требуют никакого восстановления (если *yarsync* недоступна, то его можно сделать вручную). Разумеется, это не значит, что такие программы всегда хуже и не будут подходить для вашего случая.

Также я длительное время пользовался программой [git-annex](https://git-annex.branchable.com/). Это огромная сложная программа на Haskell, использующая *git*, со множеством возможностей, про которую автор пишет, что она не является ни бэкапом, ни архивированием (но *git-annex assistant* позволяет синхронизацию файлов между компьютерами на OSX и Linux). К сожалению, мне не удалось хорошо в ней разобраться, и в какой-то момент одна из копий моих данных оказалась в непригодном состоянии (но автор программы оперативно помог на её форуме). Постепенно разбираясь в *git-annex*, я обнаружил, что он не сохраняет временные метки файлов, что для меня было неприемлемо, поскольку информация о том, создал я файл год назад или 10 лет назад, для меня важна. После этого я перешёл к классическому *rsync* и созданию удобной конфигурации для него.

Итоговые замечания
------------------

Для одновременной синхронизации нескольких репозиториев очень удобна программа [myrepos](https://myrepos.branchable.com/) (от создателя *git-annex*). Например, если вы хотите отправить новые коммиты в нескольких репозиториях на удалённую машину, то можете это сделать одной командой *mr push <remote>*. Она работает с *git*, но поскольку интерфейс *yarsync* очень близок к нему, то конфигурацию *.mrconfig* очень легко перенастроить.

Если представить, что *yarsync* предстоит ещё долгий путь, то программа находится ближе к его началу. На данный момент она используется только одним человеком, и хотя я публикую её в открытом доступе, мне известно множество её недостатков: вывод осуществляется простым текстом, без цветов и выделения, описание коммита можно передать только через опцию *-m* (текстовый редактор не вызывается), сам вывод явно будет улучшен, а самое страшное, что я до сих пор не понял, должны ли команды rsync, когда они есть, печататься с начала строки или после символов "# ". Для полного использования функционала нужны жёсткие ссылки, но возможно, что иногда это требование можно ослабить. Известно, что хотя *rsync* относится в первую очередь к Linux и близким системам, но он может использоваться и в Windows, что я также не пробовал, поскольку не пользуюсь этой ОС.

Несмотря на обозначенные недостатки, лично для меня оказалась удобна и понятна работа *yarsync*. Главные свои задачи: *commit, push* и *pull* она выполняет надёжно. При этом я не могу сказать, что ей доверяю, потому что всегда сначала запускаю *status* и пробую синхронизацию с ключом *-n*. В общем же случае я не доверяю ни одной программе, и единственным (достаточно) надёжным способом для меня является иметь не меньше трёх копий данных (это даже формализовано в виде [правила 3-2-1](https://habr.com/ru/company/veeam/blog/188544/)). Копирование данных на несколько машин / носителей я использую очень давно, однако если их изменения возможны в нескольких репозиториях, то без коммитов это превращается в хаос (либо запрещается обновление некоторых копий). Сейчас почти все данные, которыми я дорожу (будь то музыка, фотографии, книги, статьи), я добавил в репозитории *yarsync*. Я думаю, что *yarsync* близка к философии Unix, потому что использует простые существующие инструменты (*rsync* и файловую систему). Если сообщество разделяет предложенные идеи, то программа быстро преодолеет имеющиеся недостатки.

Данная статья написана к конкурсу Хабра [Технотекст 2021](https://contenting.io/2021.html). Девиз конкурса в этом году - слова Станислава Лема «ничто не стареет так быстро, как будущее». Я думаю, что *rsync* подходит под этот девиз, потому что несмотря на появление множества программ с той же целью, они рождаются, стареют и исчезают, а *rsync* так и продолжает существовать и использоваться вот уже больше 25 лет, потому что прошлое стареет гораздо медленнее, чем будущее.
