From 6d2f2506dcb39c570b68c3365a1290565962b5d3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 17 2016 10:17:30 +0000 Subject: [PATCH 1/3] Add a Read the Doc plugin and git hook This plugin allows to trigger a build on readthedocs.org upon git push --- diff --git a/pagure/hooks/files/rtd_hook.py b/pagure/hooks/files/rtd_hook.py new file mode 100755 index 0000000..98525c6 --- /dev/null +++ b/pagure/hooks/files/rtd_hook.py @@ -0,0 +1,81 @@ +#! /usr/bin/env python2 + + +"""Pagure specific hook to trigger a build on a readthedocs.org project. +""" + +import os +import sys + +import requests + +from sqlalchemy.exc import SQLAlchemyError + + +if 'PAGURE_CONFIG' not in os.environ \ + and os.path.exists('/etc/pagure/pagure.cfg'): + os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg' + + +import pagure +import pagure.exceptions +import pagure.lib.link +import pagure.ui.plugins + + +abspath = os.path.abspath(os.environ['GIT_DIR']) + + +def run_as_post_receive_hook(): + reponame = pagure.lib.git.get_repo_name(abspath) + username = pagure.lib.git.get_username(abspath) + if pagure.APP.config.get('HOOK_DEBUG', False): + print 'repo:', reponame + print 'user:', username + + repo = pagure.lib.get_project(pagure.SESSION, reponame, user=username) + if not repo: + print 'Unknown repo %s of username: %s' % (reponame, username) + sys.exit(1) + + plugin = pagure.ui.plugins.get_plugin('Read the Doc') + dbobj = plugin.db_object() + # Get the list of branches + branches = [ + branch.strip() + for branch in repo.rtd_hook[0].branches.split(',') + if repo.rtd_hook] + + # Remove empty branches + branches = [ + branch.strip() + for branch in branches + if branch] + + url = 'http://readthedocs.org/build/%s' % ( + repo.rtd_hook[0].project_name.strip() + ) + + for line in sys.stdin: + if pagure.APP.config.get('HOOK_DEBUG', False): + print line + (oldrev, newrev, refname) = line.strip().split(' ', 2) + + refname = refname.replace('refs/heads/', '') + if branches: + if refname in branches: + print 'Starting RTD build for %s' % ( + repo.rtd_hook[0].project_name.strip()) + requests.post(url) + else: + print 'Starting RTD build for %s' % ( + repo.rtd_hook[0].project_name.strip()) + requests.post(url) + + +def main(args): + run_as_post_receive_hook() + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/pagure/hooks/rtd.py b/pagure/hooks/rtd.py new file mode 100644 index 0000000..ca287cb --- /dev/null +++ b/pagure/hooks/rtd.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2016 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import os + +import sqlalchemy as sa +import pygit2 +import wtforms +from flask.ext import wtf +from sqlalchemy.orm import relation +from sqlalchemy.orm import backref + +from pagure.hooks import BaseHook, RequiredIf +from pagure.lib.model import BASE, Project +from pagure import APP, get_repo_path + + +class RtdTable(BASE): + """ Stores information about the pagure hook deployed on a project. + + Table -- hook_rtd + """ + + __tablename__ = 'hook_rtd' + + id = sa.Column(sa.Integer, primary_key=True) + project_id = sa.Column( + sa.Integer, + sa.ForeignKey('projects.id', onupdate='CASCADE'), + nullable=False, + unique=True, + index=True) + + active = sa.Column(sa.Boolean, nullable=False, default=False) + + project_name = sa.Column(sa.Text, nullable=False) + branches = sa.Column(sa.Text, nullable=True) + + project = relation( + 'Project', remote_side=[Project.id], + backref=backref( + 'rtd_hook', cascade="delete, delete-orphan", + single_parent=True) + ) + + +class RtdForm(wtf.Form): + ''' Form to configure the pagure hook. ''' + project_name = wtforms.TextField( + 'Project name on readthedoc.org', + [RequiredIf('active')] + ) + branches = wtforms.TextField( + 'Restrict build to these branches only (coma separated)', + [wtforms.validators.Optional()] + ) + + active = wtforms.BooleanField( + 'Active', + [wtforms.validators.Optional()] + ) + + +class RtdHook(BaseHook): + ''' Read The Doc hook. ''' + + name = 'Read the Doc' + description = 'Kick off a build of the documentation on readthedocs.org.' + form = RtdForm + db_object = RtdTable + backref = 'rtd_hook' + form_fields = ['active', 'project_name', 'branches'] + + @classmethod + def install(cls, project, dbobj): + ''' Method called to install the hook for a project. + + :arg project: a ``pagure.model.Project`` object to which the hook + should be installed + + ''' + repopath = get_repo_path(project) + + hook_files = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'files') + hook_file = os.path.join(hook_files, 'rtd_hook.py') + + # Init the git repo in case + pygit2.Repository(repopath) + + # Install the hook itself + hook_path = os.path.join( + repopath, 'hooks', 'post-receive.rtd') + hook_file = os.path.join(hook_files, 'rtd_hook.py') + if not os.path.exists(hook_path): + os.symlink(hook_file, hook_path) + + @classmethod + def remove(cls, project): + ''' Method called to remove the hook of a project. + + :arg project: a ``pagure.model.Project`` object to which the hook + should be installed + + ''' + repopath = get_repo_path(project) + hook_path = os.path.join( + repopath, 'hooks', 'post-receive.rtd') + if os.path.exists(hook_path): + os.unlink(hook_path) From 46e3b70dacaf2c8bd1790cf62f555111cf846d44 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 17 2016 10:17:30 +0000 Subject: [PATCH 2/3] Adjust the unit-tests for the new plugin --- diff --git a/tests/test_pagure_flask_ui_plugins.py b/tests/test_pagure_flask_ui_plugins.py index 2c57287..ff3107b 100644 --- a/tests/test_pagure_flask_ui_plugins.py +++ b/tests/test_pagure_flask_ui_plugins.py @@ -69,8 +69,11 @@ class PagureFlaskPluginstests(tests.Modeltests): names = pagure.ui.plugins.get_plugin_names() self.assertEqual( sorted(names), - ['Block non fast-forward pushes', 'Fedmsg', 'IRC', 'Mail', - 'pagure', 'pagure requests', 'pagure tickets']) + [ + 'Block non fast-forward pushes', 'Fedmsg', 'IRC', 'Mail', + 'Read the Doc', 'pagure', 'pagure requests', 'pagure tickets' + ] + ) def test_get_plugin(self): """ Test the get_plugin function. """ From 7a838494887ef13ec0be701a3e414b7225b9a461 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 17 2016 10:17:30 +0000 Subject: [PATCH 3/3] Add unit-tests covering the new RTD plugin --- diff --git a/tests/test_pagure_flask_ui_plugins_rtd_hook.py b/tests/test_pagure_flask_ui_plugins_rtd_hook.py new file mode 100644 index 0000000..f4b5d35 --- /dev/null +++ b/tests/test_pagure_flask_ui_plugins_rtd_hook.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2016 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +__requires__ = ['SQLAlchemy >= 0.8'] +import pkg_resources + +import json +import unittest +import shutil +import sys +import os + +import pygit2 +from mock import patch + +sys.path.insert(0, os.path.join(os.path.dirname( + os.path.abspath(__file__)), '..')) + +import pagure.lib +import tests + + +class PagureFlaskPluginRtdHooktests(tests.Modeltests): + """ Tests for rtd_hook plugin of pagure """ + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskPluginRtdHooktests, self).setUp() + + pagure.APP.config['TESTING'] = True + pagure.SESSION = self.session + pagure.ui.SESSION = self.session + pagure.ui.app.SESSION = self.session + pagure.ui.plugins.SESSION = self.session + pagure.ui.repo.SESSION = self.session + pagure.ui.filters.SESSION = self.session + + pagure.APP.config['GIT_FOLDER'] = tests.HERE + pagure.APP.config['FORK_FOLDER'] = os.path.join( + tests.HERE, 'forks') + pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( + tests.HERE, 'requests') + pagure.APP.config['DOCS_FOLDER'] = os.path.join( + tests.HERE, 'docs') + self.app = pagure.APP.test_client() + + def test_plugin_pagure_request(self): + """ Test the pagure_request plugin on/off endpoint. """ + + tests.create_projects(self.session) + + user = tests.FakeUser(username='pingou') + with tests.user_set(pagure.APP, user): + output = self.app.get('/test/settings/Read the Doc') + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n' + 'test project #1
', output.data) + self.assertIn('

Read the Doc settings

', output.data) + self.assertIn( + '', + output.data) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + data = {} + + output = self.app.post('/test/settings/Read the Doc', data=data) + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n' + 'test project #1
', output.data) + self.assertIn('

Read the Doc settings

', output.data) + self.assertIn( + '', + output.data) + + data['csrf_token'] = csrf_token + # No git found + output = self.app.post('/test/settings/Read the Doc', data=data) + self.assertEqual(output.status_code, 404) + + # Create both the requests repo + tests.create_projects_git(os.path.join(tests.HERE, 'requests')) + + # With the git repo + tests.create_projects_git(tests.HERE) + output = self.app.post( + '/test/settings/Read the Doc', data=data, + follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n

Settings for test

', + output.data) + self.assertIn( + '\n Hook Read the Doc inactived', + output.data) + + output = self.app.get('/test/settings/Read the Doc') + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n' + 'test project #1
', output.data) + self.assertIn('

Read the Doc settings

', output.data) + self.assertIn( + '', + output.data) + + self.assertFalse(os.path.exists(os.path.join( + tests.HERE, 'requests', 'test.git', 'hooks', + 'post-receive.pagure'))) + + # Activate hook + data = { + 'csrf_token': csrf_token, + 'active': 'y', + 'project_name': 'foo', + } + + output = self.app.post( + '/test/settings/Read the Doc', data=data, + follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n

Settings for test

', + output.data) + self.assertIn( + '\n Hook Read the Doc activated', + output.data) + + output = self.app.get('/test/settings/Read the Doc') + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n' + 'test project #1
', output.data) + self.assertIn('

Read the Doc settings

', output.data) + self.assertIn( + '', output.data) + + self.assertTrue(os.path.exists(os.path.join( + tests.HERE, 'test.git', 'hooks', + 'post-receive.rtd'))) + + # De-Activate hook + data = {'csrf_token': csrf_token} + output = self.app.post( + '/test/settings/Read the Doc', data=data, + follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n

Settings for test

', + output.data) + self.assertIn( + '\n Hook Read the Doc inactived', + output.data) + + output = self.app.get('/test/settings/Read the Doc') + self.assertEqual(output.status_code, 200) + self.assertIn( + '
\n' + 'test project #1
', output.data) + self.assertIn('

Read the Doc settings

', output.data) + self.assertIn( + '', output.data) + + self.assertFalse(os.path.exists(os.path.join( + tests.HERE, 'test.git', 'hooks', + 'post-receive.rtd'))) + + # Try re-activate hook w/o the git repo + data = { + 'csrf_token': csrf_token, + 'active': 'y', + 'project_name': 'foo', + } + shutil.rmtree(os.path.join(tests.HERE, 'test.git')) + + output = self.app.post('/test/settings/Read the Doc', data=data) + self.assertEqual(output.status_code, 404) + + +if __name__ == '__main__': + SUITE = unittest.TestLoader().loadTestsFromTestCase( + PagureFlaskPluginRtdHooktests) + unittest.TextTestRunner(verbosity=2).run(SUITE)