Линукс, Vim, LaTeX, полезные скрипты, визуализация данных, численные расчёты, немного ФП

20091028

Переименование переменных и слияние изменений в Darcs

Ныне к традиционным холиварам, вроде vi против emacs, прибавился ещё hg (Mercurial) против git. И то, и другое — распределённые системы управления версиями (DVCS). В чём их преимущество перед старыми централизованными системами и как пользоваться новыми давно уже написано. Впрочем, выбор этими двумя системами не ограничивается, отдельные маньяки успешно пользуются и другими системами. А среди альтернативных систем совершенно особняком стоит darcs.

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

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

В случае обычных DVCS, каждое изменение определяется разницей двух состояний каталога. Чтобы объединить такие изменения, нужен их общий «предок», к которому изменения можно применить. В darcs изменение не обязательно должно определяться разницей между двумя состояниями каталога. Это позвляет создавать разные типы изменений, и автор может определить что именно изменение делает (семантически). В том числе есть и такой вид изменений: замена слов в файле (token replace patch).

Покажу, как это работает, а вы уж сами судите, насколько это круто :-)

Завязка



Итак, создадим вначале исходный репозиторий и поместим в него простую программку. Тут отличия между darcs и hg или git минимальны:
$ mkdir repo-0
$ cd repo-0
repo-0$ darcs init
repo-0$ cat > hello.py
def hello(what):
print "Hello %s" % what

hello("World")
^D

repo-0$ darcs add hello.py
repo-0$ darcs record -m 'initial commit' hello.py
Recording changes in "hello.py":

addfile ./hello.py
Shall I record this change? (1/2) [ynWsfvplxdaqjk], or ? for help: y
hunk ./hello.py 1
+def hello(what):
+ print "Hello %s" % what
+
+hello("World")
Shall I record this change? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'initial commit'


А теперь создадим две ветки. В каждой ветке сделаем свои изменения. В одной (A) изменим название переменной what на name, а в другой (B) переименуем и перепишем функцию hello().

Внезапно!



Клонируем исходный репозиторий:
repo-0$ cd ..
$ darcs get repo-0 repo-A
Copying patches, to get lazy repository hit ctrl-C...
Finished getting.

И переименовываем в этой ветке переменную. Только хитрость, мы хотим не просто сделать замену слов в файле, а мы хотим явно указать darcs-у, что это именно замена слов. Поэтому вместо текстового редактора выполняем такую вот команду:
$ cd repo-A
repo-A$ darcs replace what name hello.py

Убеждаемся, что программка изменилась:
repo-A$ cat hello.py
def hello(name):
print "Hello %s" % name

hello("World")

И записываем изменения в репозиторий:
repo-A$ darcs record -m 'renamed: what to name' hello.py
Recording changes in "hello.py":

replace ./hello.py [A-Za-z_0-9] what name
Shall I record this change? (1/1) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'renamed: what to name'


Тем временем...



Параллельно создаём другую ветку и как-нибудь меняем функцию hello:
repo-A$ cd ..
$ darcs get repo-0 repo-B
Copying patches, to get lazy repository hit ctrl-C...
Finished getting.
$ cd repo-B
repo-B$ cat > hello.py
def hello(what):
if len(what) > 6:
print "Hello %s" % what
else:
print "Hi %s" % what

hello("World")
^D

Изменения настолько серьёзны, что старое имя функции уже не подходит. Переименовываем её с помощью darcs replace:
repo-B$ darcs replace hello greet hello.py

И записываем изменения:
repo-B$ darcs record -m 'changed hello and renamed to greet' hello.py
Recording changes in "hello.py":

hunk ./hello.py 2
- print "Hello %s" % what
+ if len(what) > 6:
+ print "Hello %s" % what
+ else:
+ print "Hi %s" % what
Shall I record this change? (1/2) [ynWsfvplxdaqjk], or ? for help: y
replace ./hello.py [A-Za-z_0-9] hello greet
Shall I record this change? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'changed hello and renamed to greet'


Кровавый финал



А теперь возвращаемся в исходный репозиторий и объединяем изменения:
repo-B$ cd ../repo-0
repo-0$ darcs pull ../repo-A ../repo-B
Wed Oct 28 17:21:41 CET 2009 me@example.com
* changed hello and renamed to greet
Shall I pull this patch? (1/2) [ynWsfvplxdaqjk], or ? for help: y
Wed Oct 28 17:12:12 CET 2009 me@example.com
* renamed: what to name
Shall I pull this patch? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished pulling and applying.

И что же мы видим?
repo-0$ cat hello.py
def greet(name):
if len(name) > 6:
print "Hello %s" % name
else:
print "Hi %s" % name

greet("World")

Изменения объединились правильно. Система управления версиями оказалась достаточно умной, чтобы применить изменения в нужном порядке (вначале переписать функцию, а уж потом переименовать все случаи использования переменной).

Я впечатлён.

9 коммент.:

vlasovskikh комментирует...

Слишком страшно работать с умными программами :) Обычно хватает Mercurial Queues и record extension. Недавно натолкнулся на patchutils, может этот набор утилит будет полезен.

Me комментирует...

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

lockie комментирует...

Круто, блин. Аффтар, пещы ещщо про darcs vs hg/git.
Оно там, говорят, на гопнике гаскеле написано, не станет ли при установке тащить over 9000M гаскелевских либ?

Сергей комментирует...

lockie,

Сейчас проверил, бинарный пакет darcs-2.3.0 в Debian весит 1.8 МБ, зависит только от libc, libcurl3-gnutls, libffi5, libgmp3c2, libncurses5, zlib1g, рекомендует какой-нибудь mta. Для сборки из исходников — да, нужен GHC (суммарный размер пакетов всей haskell-platform в Debian unstable — 42.7 МБ).

Сергей комментирует...

Андрей,

да, я продолжаю использовать в качестве основной системы hg; просто любопытствовал, какая трава за забором. А Record extension, кстати, как раз хочу попробовать.

AlexWinner комментирует...

мм.. А SVN в холиваре не участвует?)

Сергей комментирует...

Не, SVN, мне кажется, уже не участвует. Хотя как раз именно она, говорят [1] [2], лучше всех подходит для версионирования больших двоичных файлов. Так что SVN была и останется, но просто с hg обычно всё проще.

Сергей комментирует...

хмм… а если надо переименовать локальную переменную в процедуре, а такая же переменная есть ещё и глобальная, то всё, не справится? Наверное, такой тривиальный случай учитывался и как то разрешается. Просто интересно как! кто нить в курсе? ☺

Сергей комментирует...

Сергею:

локальные переименования можно записывать, конечно, обычным патчем, как и в других VCS (и сливать их как обычно);

replace, видимо, может обрабатывать только файл целиком, причём там есть ещё несколько ограничений: 1) файл рассматривается только как байт-поток (не уникод), 2) токены для замен можно определять только диапазонами символов вроде "[A-Za-z0-9]" в опции --token-chars (более хитрого синтаксического разбора в replace нет), 3) диапазоны --token-chars не могут содержать пробел, 4) результат наложения нескольких replace-ов с разными --token-chars неопределён (так понимаю, в этом случае между ними надо ставить тэги).

В общем, darcs replace, конечно, не всемогущ и не соверешенен.

Справка по darcs replace
Обсуждение ограничений replace в списке рассылки

Отправить комментарий