Пишем хук для Atlassian Stash
Date August 27th, 2013 Author Vitaly Agapov
Раньше я тесно взаимодействовал с системой контроля версий Subversion и с системой для багтрекинга (точнее issue-трекинга) Atlassian Jira. Они были тесно переплетены друг с другом. Jira показывала привязанные к задачам коммиты в SVN, SVN спрашивал у Jira разрешения на коммит с учётом главного правила – сообщение к коммиту должно содержать валидный ключ задачи в Jira. Последняя фича реализовывалась с помощью плагина JIRA Commit Acceptance и pre-commit хука для SVN, написанного на Perl'е.
Когда же SVN отошёл и уступил место git'у под управлением Atlassian Stash, возникла необходимость реализовать ту же самую тёплую ламповую функциональность, которая существовала и раньше. Каково же было моё удивление, когда я узнал, что плагина для Stash с аналогичным хуком не существует (да-да, хуки здесь существуют в виде плагинов). А это значило, что следует засучить рукава и разобраться, как написать свой собственный плагин под Stash и научить его делать то, что нам надо.
Плагины для продуктов Atlassian пишутся на Java, и лично для меня основной сложностью и преградой было именно это обстоятельство, ведь я джавой головного мозга не страдаю и за всю жизнь не написал на Java ни строчки кода, если не считать кое-каких HelloWorld'ов по разным туториалам. Но всё бывает в первый раз. Так что этот текст может пригодиться (а может и не пригодиться) людям совершенно далёким от java-разработки, плагиностроения для Atlassian'овсих продуктов и вообще от быдлокодинга.
Atlassian SDK
Atlassian удивляет качеством софта, который они пишут. Но ещё сильнее они удивляют своей техподдержкой, своим комьюнити и своим SDK. Всё сделано для удобства и быстроты вовлечения. В общем, мы начнём с установки этого самого SDK. Все необходимые инструкции есть здесь. А так как в моём случае всё происходит под Ubuntu, то я делаю следующее:
1.
sudo
sh -c '
echo
"deb https://sdkrepo.atlassian.com/debian/ stable contrib"
>> /etc/apt/sources.list'
2.
sudo
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B07804338C015B73
3.
sudo
apt-get
install
apt-transport-https
4.
sudo
apt-get update
5.
sudo
apt-get
install
atlassian-plugin-sdk
Всё, Atlassian SDK стоит и его можно использовать. Конечно, остаётся вопрос с тем, какой IDE мы хотим использовать. Можно ставить Eclipse или что-то вроде того, но это отдельная тема. Я для своего маленького эксперимента использовал vim, который при должной сноровке может превратиться в почти полноценный IDE.
В общем, теперь с помощью набора команд, начинающихся с atlas-, можно производить разные манипуляции вроде такой:
1.
$ atlas-version
2.
ATLAS Version: 4.2.7
3.
...
Создание шаблона модуля
Но мы лучше создадим новый проект модуля:
1.
mkdir
stash
2.
cd
stash
3.
atlas-create-stash-plugin
Maven скачает нужные исходники и задаст разные вопросы по проекту:
1.
[INFO] using latest stable data version: 2.7.0
2.
Define value for groupId: : com.example.stash
3.
Define value for artifactId: : jira-acceptance
4.
Define value for version: [1.0-SNAPSHOT]:
5.
Define value for package: [com.example.stash]:
6.
...
7.
[INFO] BUILD SUCCESSFUL
Готово. Утилита создала готовый проект, который хоть сейчас можно билдить и использовать в качестве плагина. Правда, он ничего не будет делать. Но это уже детали. Проект лежит в отдельной директории с названием jira-acceptance (в соответствии с artifactId), в которой лежит готовый maven'овский pom.xml, а также пути с ресурсами и исходниками. Уже на этом этапе можно погрузиться в документацию для разработчиков и начать пилить собственный хук. Я изначально так и сделал, и даже добился рабочего состояния. Но SDK даёт ещё одну интересную возможность – создать отдельный модуль в проекте под нужную нам функциональность.
01.
$ atlas-create-stash-plugin-module
02.
...
03.
Choose Plugin Module:
04.
...
05.
7: REST Plugin Module
06.
8: Repository Hook
07.
9: SCM Request Check
08.
...
09.
Choose a number (1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20): 8
10.
...
11.
Enter Hook Type (pre/post/merge) [post]: pre
12.
Enter New Classname [MyPreReceiveRepositoryHook]: JiraPreReceiveHook
13.
Enter Package Name [com.example.stash.hook]:
14.
Show Advanced Setup? (Y/y/N/n) [N]:
15.
...
16.
Add Another Plugin Module? (Y/y/N/n) [N]:
Доработка логики плагина
Посомтрим, что изменилось в проекте. Во-первых в дескрипторе плагина по пути jira-acceptance/src/main/resources/atlassian-plugin.xml появился новый блок:
1.
<
repository-hook
name
=
"Jira Pre Receive Hook"
i18n-name-key
=
"jira-pre-receive-hook.name"
key
=
"jira-pre-receive-hook"
class
=
"com.example.stash.hook.JiraPreReceiveHook"
>
2.
<
description
key
=
"jira-pre-receive-hook.description"
>The Jira Pre Receive Hook Plugin</
description
>
3.
<
icon
>icon-example.png</
icon
>
4.
</
repository-hook
>
Мы в ней изменим description, логотип и добавим форму конфигурации:
1.
<
repository-hook
name
=
"Jira Pre Receive Hook"
i18n-name-key
=
"jira-pre-receive-hook.name"
key
=
"jira-pre-receive-hook"
class
=
"com.example.stash.hook.JiraPreReceiveHook"
>
2.
<
description
key
=
"jira-pre-receive-hook.description"
>Jira Commit Acceptance</
description
>
3.
<
icon
>images/stash.png</
icon
>
4.
<
config-form
name
=
"Simple Hook Config"
key
=
"simpleHook-config"
>
5.
<
view
>stash.config.example.hook.simple.formContents</
view
>
6.
<
directory
location
=
"/static/"
/>
7.
</
config-form
>
8.
</
repository-hook
>
Естественно, здесь же в директорию images надо подложить заготовленный логотип размером 96×96 точек. А в директорию static подложить файл simple.soy с шаблоном формы конфигурации:
01.
{namespace stash.config.example.hook.simple}
02.
03.
/**
04.
* @param config
05.
* @param? errors
06.
*/
07.
{template .formContents}
08.
{call aui.form.textField}
09.
{param id: 'jiraBaseURL' /}
10.
{param value: $config['jiraBaseURL'] /}
11.
{param labelContent}
12.
{stash_i18n('stash.web.test.hook.config.label', 'Jira Base URL')}
13.
{/param}
14.
{param descriptionText: stash_i18n('stash.web.test.hook.config.description', 'Example: http://jira.example.com') /}
15.
{param extraClasses: 'long' /}
16.
{param errorTexts: $errors ? $errors['jiraBaseURL'] : null /}
17.
{/call}
18.
{call aui.form.textField}
19.
{param id: 'projectKey' /}
20.
{param value: $config['projectKey'] ? $config['projectKey'] : '*' /}
21.
{param labelContent}
22.
{stash_i18n('stash.web.test.hook.config.label', 'projectKey')}
23.
{/param}
24.
{param descriptionText: stash_i18n('stash.web.test.hook.config.description', 'Can contain multiple comma-separated JIRA project keys. * forces using the global commit acceptance settings') /}
25.
{param extraClasses: 'long' /}
26.
{param errorTexts: $errors ? $errors['projectKey'] : null /}
27.
{/call}
28.
{/template}
В этой форме у нас будет поле для пути к Jira и шаблон имени проекта, в котором должна быть искомая задача.
В java у нас появился новый путь к файлу JiraPreReceiveHook.java с классом JiraPreReceiveHook и методом onReceive, служащим в качестве хука.
Тут стоит сделать небольшое отступление. Так как git следует парадигме распределённой системы контроля версий (в отличие от централизованной SVN), здесь неприменимы понятия pre-commit и post-commit, так как с каждым push'ем в репозиторий может отправляться не один коммит, а целый набор (changeset). В связи с этим здесь применяются понятия pre-receive (или on-receive) и post-receive. Причём в нашем случае на каждый pre-receive придётся проверять скорее всего не один коммит с одним комментарием и одним jira-issue-key, а целый набор, каждый из которых должен удовлетворять нашим условиям.
В общем, используя заготовку, подготовленную нам командой atlas-create-stash-plugin-module, преобразим её в такой вот вид:
01.
package
com.example.stash.hook;
02.
03.
04.
import
com.atlassian.stash.content.Changeset;
05.
import
com.atlassian.stash.history.HistoryService;
06.
import
com.atlassian.stash.hook.HookResponse;
07.
import
com.atlassian.stash.util.PageRequestImpl;
08.
import
com.atlassian.stash.setting.*;
09.
import
java.util.List;
10.
import
com.google.common.collect.Lists;
11.
import
java.net.URL;
12.
13.
import
org.apache.xmlrpc.client.*;
14.
import
org.apache.xmlrpc.util.*;
15.
import
java.util.Vector;
16.
17.
import
com.atlassian.stash.hook.*;
18.
import
com.atlassian.stash.hook.repository.*;
19.
import
com.atlassian.stash.repository.*;
20.
import
java.util.Collection;
21.
22.
public
class
JiraPreReceiveHook
implements
PreReceiveRepositoryHook, RepositorySettingsValidator
23.
{
24.
25.
private
static
final
PageRequestImpl PAGE_REQUEST =
new
PageRequestImpl(
0
,
100
);
26.
private
final
HistoryService historyService;
27.
public
JiraPreReceiveHook(HistoryService historyService) {
28.
this
.historyService = historyService;
29.
}
30.
31.
@Override
32.
public
boolean
onReceive(RepositoryHookContext context, Collection<RefChange> refChanges, HookResponse hookResponse) {
33.
List<Changeset> badChangesets = Lists.newArrayList();
34.
String url = context.getSettings().getString(
"jiraBaseURL"
);
35.
String RPC_PATH =
"/rpc/xmlrpc"
;
36.
String jiraLogin =
"somelogin"
;
37.
String jiraPassword =
"somepass"
;
38.
String projectKey = context.getSettings().getString(
"projectKey"
);
39.
40.
if
(url !=
null
) {
41.
for
(RefChange refChange : refChanges) {
42.
for
(Changeset changeset : historyService.getChangesetsBetween(context.getRepository(), refChange.getFromHash(), refChange.getToHash(), PAGE_REQUEST).getValues()) {
43.
try
{
44.
XmlRpcClientConfigImpl config =
new
XmlRpcClientConfigImpl();
45.
config.setServerURL(
new
URL(url + RPC_PATH));
46.
XmlRpcClient rpcClient =
new
XmlRpcClient();
47.
rpcClient.setConfig(config);
48.
Vector jiraParams =
new
Vector(
5
);
49.
jiraParams.add(jiraLogin);
50.
jiraParams.add(jiraPassword);
51.
jiraParams.add(
"admin"
);
52.
jiraParams.add(projectKey);
53.
jiraParams.add(changeset.getMessage());
54.
String result = (String) rpcClient.execute(
"commitacc.acceptCommit"
, jiraParams);
55.
56.
hookResponse.out().println(
"Result: "
+ result);
57.
if
(!result.matches(
"true(.*)"
)) {
58.
badChangesets.add(changeset);
59.
}
60.
}
catch
(Exception e) {
61.
e.printStackTrace();
62.
}
63.
}
64.
}
65.
if
(badChangesets.isEmpty()) {
66.
hookResponse.out().println(
"Jira Commit Acceptance Passed"
);
67.
return
true
;
68.
}
69.
for
(Changeset changeset : badChangesets) {
70.
hookResponse.err().println(String.format(
"Bad changeset '%s' with message '%s'"
, changeset.getId(), changeset.getMessage()));
71.
}
72.
return
false
;
73.
}
74.
hookResponse.out().println(
"Jira URL is empty"
);
75.
return
false
;
76.
}
77.
78.
@Override
79.
public
void
validate(Settings settings, SettingsValidationErrors errors, Repository repository) {
80.
if
(settings.getString(
"jiraBaseURL"
,
""
).isEmpty()) {
81.
errors.addFieldError(
"jiraBaseURL"
,
"jiraBaseURL field is blank, please supply one"
);
82.
}
83.
if
(settings.getString(
"projectKey"
,
""
).isEmpty()) {
84.
errors.addFieldError(
"projectKey"
,
"projectKey field is blank, please supply one"
);
85.
}
86.
}
87.
}
Функциональная часть плагина готова, но надо ещё поправить maven-зависимости, чтобы добавить в проект пакет org.apache.xmlrpc. Для этого в pom.xml добавляем:
01.
<
dependency
>
02.
<
groupId
>org.apache.xmlrpc</
groupId
>
03.
<
artifactId
>xmlrpc-client</
artifactId
>
04.
<
version
>3.1.3</
version
>
05.
</
dependency
>
06.
<
dependency
>
07.
<
groupId
>org.apache.xmlrpc</
groupId
>
08.
<
artifactId
>xmlrpc-common</
artifactId
>
09.
<
version
>3.1.3</
version
>
10.
</
dependency
>
Делаем atlas-run из корня проекта, и… чудеса – всё собирается. Более того – SDK запустил нам локальную копию Stash с установленным нашим свеженаписанным плагином. Достучаться до неё можно по пути http://127.0.0.1:7990/stash. Логин и пароль: admin/admin.
В списке плагинов мы видим наш плагин, а в списке хуков для репозитория мы видим наш хук. Включаем его, вводим в конфиге путь к Jira и, скажем, знак "*" в шаблон проекта. После этого можно попробовать поэкспериментировать.
01.
$ git clone http://admin@127.0.0.1:7990/stash/scm/project_1/rep_1.git
02.
$
cd
rep_1/
03.
$
touch
test
04.
$ git add
test
05.
$ git commit -a -m &
#39;BadMessage'
06.
$ git push
07.
Password
for
'http://admin@127.0.0.1:7990':
08.
remote: Communication breakdown with Stash.
09.
To http://admin@127.0.0.1:7990/stash/scm/project_1/rep_1.git
10.
! [remote rejected] master -> master (pre-receive hook declined)
11.
error: failed to push some refs to 'http://admin@127.0.0.1:7990/stash/scm/project_1/rep_1.git'
Проблема с org.apache.xerces.jaxp.SAXParserFactoryImpl cannot be cast to javax.xml.parsers.SAXParserFactory
Оп-па. Вместо нормального сообщения от нашего хука (метод hookResponse) мы получили какой-то Communication breakdown with Stash. А в логе самого Stash вылез стектрейс:
1.
[INFO] [talledLocalContainer] Caused by: java.lang.ClassCastException: org.apache.xerces.jaxp.SAXParserFactoryImpl cannot be cast to javax.xml.parsers.SAXParserFactory
2.
[INFO] [talledLocalContainer] at javax.xml.parsers.SAXParserFactory.newInstance(Unknown Source) ~[na:1.6.0_27]
3.
[INFO] [talledLocalContainer] at org.apache.xmlrpc.util.SAXParsers.<clinit>(SAXParsers.java:34) ~[na:na]
4.
[INFO] [talledLocalContainer] ... 57 common frames omitted
Это из-за того, что XML-RPC имеет свои зависимости, конфликтующие с теми, что уже есть в Stash. В частности – SAX XML Parser из пакета xml-apis. На стадии билда эта проблема себя никак не проявляет из-за динамической модульной шины OSGi для Java. Из-за неё модуль xml-apis загружается только при обращении к соответствующему методу, в нашем случае – при попытке распарсить XML-ответ от XML-RPC интерфеса джировского плагина.
Посмотреть зависимости можно так:
atlas-mvn dependency:tree
…
[INFO] +- org.apache.xmlrpc:xmlrpc-client:jar:3.1.3:compile
[INFO] \- org.apache.xmlrpc:xmlrpc-common:jar:3.1.3:compile
[INFO] \- org.apache.ws.commons.util:ws-commons-util:jar:1.0.2:compile
[INFO] \- xml-apis:xml-apis:jar:1.0.b2:compile
Пофиксить проблему можно, добавив исключения в зависимости в pom.xml:
01.
<
dependency
>
02.
<
groupId
>org.apache.xmlrpc</
groupId
>
03.
<
artifactId
>xmlrpc-client</
artifactId
>
04.
<
version
>3.1.3</
version
>
05.
<
exclusions
>
06.
<
exclusion
>
07.
<
artifactId
>xml-apis</
artifactId
>
08.
<
groupId
>xml-apis</
groupId
>
09.
</
exclusion
>
10.
</
exclusions
>
11.
</
dependency
>
12.
<
dependency
>
13.
<
groupId
>org.apache.xmlrpc</
groupId
>
14.
<
artifactId
>xmlrpc-common</
artifactId
>
15.
<
version
>3.1.3</
version
>
16.
<
exclusions
>
17.
<
exclusion
>
18.
<
artifactId
>xml-apis</
artifactId
>
19.
<
groupId
>xml-apis</
groupId
>
20.
</
exclusion
>
21.
</
exclusions
>
22.
</
dependency
>
После внесения изменений надо выполнить команду atlas-clean, а потом запускать сборку снова: atlas-run. После пересборки всё работает как по маслу.
Установка плагина
В директории target в проекте у нас лежит собранный jar-ник jira-acceptance-1.0-SNAPSHOT.jar. Мы качаем его на сервер с боевым Stash, подкладываем в plugins/installed-plugins внутри директории STASH_HOME и перезапускаем Stash.
Готово.
Tags: Atlassian, Java
Category:
Java |
No comments »