Tuesday, October 21, 2008

hook pre-commit в svn. Реализация на питоне.

Задача : при коммите в svn, хочется чего то делать, например закрывать тикет в trac.

Для этого в svn есть механизм хуков. Далее по шагам как я это делал я.

Создание репозитория, начальный checkout

директория для тестов
cd@cd-laptop:~$ mkdir tests
cd@cd-laptop:~$ cd tests
директория для репозитория
cd@cd-laptop:~/tests$ mkdir repo
создание репозитория
cd@cd-laptop:~/tests$ svnadmin create repo
директория для чекаутов
cd@cd-laptop:~/tests$ mkdir checkout-dir
первый чекаут
cd@cd-laptop:~/tests$ svn checkout file:///home/cd/tests/repo/ checkout-dir/
Checked out revision 0.
создаем, добавляем, коммитим файл.
cd@cd-laptop:~/tests$ touch checkout-dir/x.txt
cd@cd-laptop:~/tests$ svn add checkout-dir/x.txt
checkout-dir/x.txt
cd@cd-laptop:~/tests$ svn ci checkout-dir/x.txt -m'first commit'
Adding checkout-dir/x.txt
Transmitting file data .
Committed revision 1.
cd@cd-laptop:~/tests$




Репозиторий создан, для теста сделал один коммит.

Теперь собственно создаем хук.


Заходим в папку репозитоия
cd@cd-laptop:~/tests$ cd repo/
смотрим чего есть. Интересует папка hooks
cd@cd-laptop:~/tests/repo$ ls
conf dav db format hooks locks README.txt
cd@cd-laptop:~/tests/repo$ cd hooks/
Заходим в нее.
cd@cd-laptop:~/tests/repo/hooks$ ls
post-commit.tmpl post-revprop-change.tmpl pre-commit.tmpl pre-revprop-change.tmpl start-commit.tmpl
post-lock.tmpl post-unlock.tmpl pre-lock.tmpl pre-unlock.tmpl
Создаем, делаем права на запуск, заполняем
cd@cd-laptop:~/tests/repo/hooks$ touch pre-commit
cd@cd-laptop:~/tests/repo/hooks$ chmod +x pre-commit
cd@cd-laptop:~/tests/repo/hooks$ cat > pre-commit
#!/usr/bin/python

import sys

sys.stderr.write('Test error')
sys.exit(1)

cd@cd-laptop:~/tests/repo/hooks$ cd ../..
cd@cd-laptop:~/tests$ echo '1' >> checkout-dir/x.txt
cd@cd-laptop:~/tests$ svn ci checkout-dir/x.txt -m'test'
Sending checkout-dir/x.txt
Transmitting file data .svn: Commit failed (details follow):
svn: 'pre-commit' hook failed with error output:
Test error
cd@cd-laptop:~/tests$



В папке репозитория в подпапке hoosk был создан файл pre-commit и ему поставлены права на запуск.
Теперь когда свн его увидит, будет запускать после получения данных непосредственно перед коммитом. При коде возврата не 0, commit не пройдет. Собственно сам скрипт можно писать на любом скриптовом языке, главное чтобы была шибанг строка.

Подробнее о коде pre-commit

#!/usr/bin/python

import sys

#пишем в STDERR
sys.stderr.write('Test error')
#выходим с кодом возврата 1
sys.exit(1)



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


Здесь я маленько не разобрался, в основном попробовал методом тыка, поэтому напишу как я понял. Не факт что будет совсем правильно, но пользуясь тем как я это понял, у меня чего то получается делать.


Для получения информации о предстоящем коммите, полезна утилита

svnlook


Формат запуска следующий

svnlook SUBCOMMAND REPOS_PATH [ARGS & OPTIONS ...]

Где subcommand одна из следующего списка

author
cat
changed
date
diff
dirs-changed
help (?, h)
history
info
lock
log
propget (pget, pg)
proplist (plist, pl)
tree
uuid
youngest



REPOS_PATH путь до репозитория, аргументы и опции могут зависеть от subcommand нас интересуют только две

any subcommand which takes the '--revision' and '--transaction'


--revision понятно, выполняет subcommand по определенной ревизии
--transaction интереснее, выполняет subcommand по транзакции, с этим я не разобрался, но понял так.

Если взглянуть на pre-commit.tpl (файл созданные в репозитории по умолчанию) увидим там следующее


REPOS="$1"
TXN="$2"

# Make sure that the log message contains some text.
SVNLOOK=/usr/bin/svnlook
$SVNLOOK log -t "$TXN" "$REPOS" | grep "[a-zA-Z0-9]" > /dev/null || exit 1


Где видно что параметр --transaction он же -t используется с переменной TXN, которая является вторым аргументом передаваемым в pre-commit. Т.е. эта штука у нас на руках, и svnlook умеет с ней работать.

Итак при обработке pre-commit у нас нет ревизии (мы ее создаем ) зато есть транзакция. Теперь что мы можем из этого получить.

Пример pre-commit который выводит автора и не дает закоммититься.


#!/usr/bin/python

import sys
import os

#обертка над svnlook для запуска любой команды
def cmd_out(subcommand, transaction, repo) :
cmd = '/usr/local/bin/svnlook %s -t "%s" "%s"' % (subcommand, transaction, repo)
str = os.popen(cmd, 'r').read()
return str.strip('\n')

#вызвает нашу оберку, для подкомманды author
def look_author(transaction, repo) :
return cmd_out('author', transaction, repo )

#читаем репозиторий, транзакцию
repos = sys.argv[1]
txn = sys.argv[2]

#получаем автора
author = look_author(txn, repos)

#пишем автора в STDERR
sys.stderr.write('Author :%s:' % author )
#выходим с ошибкой, запрещая коммит
sys.exit(1)


Единственное нужно указать правильный путь до svnlook

Ну теперь собственно все. Т.е. при помощи svnlook можно получить очень много информации о предстоящем коммите :)

Например получения текста лога для данного коммита.


def look_log (transaction, repo) :
return cmd_out('log', transaction, repo)


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


#!/usr/bin/python

import sys, os, string, re

# return true or false if this passed string is a valid comment

php_devs = ('xx', 'xxx', 'yy', 'yyy', 'zz', 'zzz') #only for this dev's rules allowed



def cmd_out(subcommand, transaction, repo) :
cmd = '/usr/local/bin/svnlook %s -t "%s" "%s"' % (subcommand, transaction, repo)
str = os.popen(cmd, 'r').read()
return str.strip('\n')

def look_info(trans, repo) :
return cmd_out('tree --full-paths', trans, repo)

def look_author(transaction, repo) :
return cmd_out('author', transaction, repo )

def look_log (transaction, repo) :
return cmd_out('log', transaction, repo)

def check_comments(comment ):#return ticket id if find in comment
p = re.compile('\s*ticket:(\d+)(:close)?\s+(.*)$', re.MULTILINE)
res = p.match(comment)
try:
ticket_id = res.group(1)
except:
ticket_id = None
return ticket_id

def need_close(comment) : #i know it's copy paste, but i had choice, write into blog, or rewrite it ...
p = re.compile('\s*ticket:(\d+)(:(close))?\s+(.*)$', re.MULTILINE)
res = p.match(comment)
try:
is_closed = res.group(3)
except:
is_closed = ''
return is_closed == 'close'


def update_ticket_in_trac(ticket, author, comment, data ) :
import xmlrpclib
s = xmlrpclib.ServerProxy('http://tracuser:tracpast@domain.com:port/path_to_Trac/login/xmlrpc')#create service
try:
ticketInfo = s.ticket.get(ticket)#try get ticket
status = ticketInfo[3]['status']
if status == 'closed' :
sys.stderr.write("Ticket already closed, need reopen it first !!!")
return 1
except: #can catch here protocol err, it mean no rights
sys.stderr.write("No ticket found in trac for id %s" % ticket )
return 1
comment = comment.replace(':close','')
s.ticket.update(ticket,comment.replace("ticket:%s" % ticket, '%s killed himself with message : \n' % author , data) , data)
return 0


def main(repos, txn):
comment = look_log(txn, repos)
author = look_author(txn,repos)
if not (author in php_devs) :
return 0
ticket = check_comments(comment)
if ticket == None :
sys.stderr.write("\nticket:num must be at the comment begin where num is ticket id, if not exists create it please!")
return 1
data = dict()
if ( need_close(comment) ) :
data['status'] = 'closed'
return update_ticket_in_truck(ticket, author,comment, data )

if __name__ == '__main__':
if len(sys.argv) < 3 :
sys.stderr.write("Usage: %s REPOS TXN\n" % (sys.argv[0]))
else:
sys.exit(main(sys.argv[1], sys.argv[2]))


No comments:

 
Каталог сайтов, Добавить сайт