From f3eed153ec09752600c6807e5d261242eaa05df0 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Apr 19 2016 09:32:46 +0000
Subject: [PATCH 1/3] Add a new unsigned commit plugin


This plugin installs a git hook to block push with commit missing
Signed-Off-By.

---

diff --git a/pagure/hooks/files/pagure_block_unsigned.py b/pagure/hooks/files/pagure_block_unsigned.py
new file mode 100755
index 0000000..bd366f3
--- /dev/null
+++ b/pagure/hooks/files/pagure_block_unsigned.py
@@ -0,0 +1,72 @@
+#! /usr/bin/env python2
+
+
+"""Pagure specific hook to block commit not having a 'Signed-off-by'
+statement.
+"""
+
+import os
+import sys
+
+from sqlalchemy.exc import SQLAlchemyError
+
+import sys
+sys.path.insert(0, '/home/pierrey/repos/gitrepo/pagure/')
+
+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_pre_receive_hook():
+
+    for line in sys.stdin:
+        if pagure.APP.config.get('HOOK_DEBUG', False):
+            print line
+        (oldrev, newrev, refname) = line.strip().split(' ', 2)
+
+        if pagure.APP.config.get('HOOK_DEBUG', False):
+            print '  -- Old rev'
+            print oldrev
+            print '  -- New rev'
+            print newrev
+            print '  -- Ref name'
+            print refname
+
+        if set(newrev) == set(['0']):
+            print "Deleting a reference/branch, so we won't run the "\
+                "hook to block unsigned commits"
+            return
+
+        commits = pagure.lib.git.get_revs_between(oldrev, newrev, abspath)
+        for commit in commits:
+            if pagure.APP.config.get('HOOK_DEBUG', False):
+                print 'Processing commit: %s' % commit
+            signed = False
+            for line in pagure.lib.git.read_git_lines(
+                ['log', '--no-walk', commit], abspath):
+                if 'signed-off-by' in line.lower():
+                    signed = True
+                    break
+            if pagure.APP.config.get('HOOK_DEBUG', False):
+                print ' - Commit: %s is signed: %s' % (commit, signed)
+            if not signed:
+                print "Commit %s is not signed" % commit
+                sys.exit(1)
+
+
+def main(args):
+    run_as_pre_receive_hook()
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
diff --git a/pagure/hooks/pagure_unsigned_commits.py b/pagure/hooks/pagure_unsigned_commits.py
new file mode 100644
index 0000000..00fb76a
--- /dev/null
+++ b/pagure/hooks/pagure_unsigned_commits.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2016 - Copyright Red Hat Inc
+
+ Authors:
+   Pierre-Yves Chibon <pingou@pingoured.fr>
+
+"""
+
+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 PagureUnsignedCommitTable(BASE):
+    """ Stores information about the pagure hook deployed on a project.
+
+    Table -- hook_pagure_unsigned_commit
+    """
+
+    __tablename__ = 'hook_pagure_unsigned_commit'
+
+    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 = relation(
+        'Project', foreign_keys=[project_id], remote_side=[Project.id],
+        backref=backref(
+            'pagure_unsigned_commit_hook', cascade="delete, delete-orphan",
+            single_parent=True)
+    )
+
+
+class PagureUnsignedCommitForm(wtf.Form):
+    ''' Form to configure the pagure hook. '''
+
+    active = wtforms.BooleanField(
+        'Active',
+        [wtforms.validators.Optional()]
+    )
+
+
+class PagureUnsignedCommitHook(BaseHook):
+    ''' PagurPagureUnsignedCommit hook. '''
+
+    name = 'Block Un-Signed commits'
+    description = 'Using this hook you can block any push with commits '\
+        'missing a "Signed-Off-By"'
+    form = PagureUnsignedCommitForm
+    db_object = PagureUnsignedCommitTable
+    backref = 'pagure_unsigned_commit_hook'
+    form_fields = ['active']
+    hook_type = 'pre-receive'
+
+    @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, 'pagure_block_unsigned.py')
+
+        # Init the git repo in case
+        pygit2.Repository(repopath)
+
+        # Install the hook itself
+        hook_path = os.path.join(
+            repopath, 'hooks', 'pre-receive.pagureunsignedcommit')
+        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', 'pre-receive.pagureunsignedcommit')
+        if os.path.exists(hook_path):
+            os.unlink(hook_path)

From 0ce05f915ebdbcc81422b0ffb56f03edb212ea46 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Apr 19 2016 09:32:46 +0000
Subject: [PATCH 2/3] Add unit-tests for the new plugin/hook blocking push with unsigned commits


---

diff --git a/tests/test_pagure_flask_ui_plugins.py b/tests/test_pagure_flask_ui_plugins.py
index a0aed8a..90d80be 100644
--- a/tests/test_pagure_flask_ui_plugins.py
+++ b/tests/test_pagure_flask_ui_plugins.py
@@ -70,8 +70,9 @@ class PagureFlaskPluginstests(tests.Modeltests):
         self.assertEqual(
             sorted(names),
             [
-                'Block non fast-forward pushes', 'Fedmsg', 'IRC', 'Mail',
-                'Pagure', 'Pagure requests', 'Pagure tickets', 'Read the Doc',
+                'Block Un-Signed commits',  'Block non fast-forward pushes',
+                'Fedmsg', 'IRC', 'Mail', 'Pagure', 'Pagure requests',
+                'Pagure tickets', 'Read the Doc',
             ]
         )
 
diff --git a/tests/test_pagure_flask_ui_plugins_unsigned.py b/tests/test_pagure_flask_ui_plugins_unsigned.py
new file mode 100644
index 0000000..04a63e2
--- /dev/null
+++ b/tests/test_pagure_flask_ui_plugins_unsigned.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2016 - Copyright Red Hat Inc
+
+ Authors:
+   Pierre-Yves Chibon <pingou@pingoured.fr>
+
+"""
+
+__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 PagureFlaskPluginUnsignedtests(tests.Modeltests):
+    """ Tests for Block pushes with unsigned commit plugin of pagure """
+
+    def setUp(self):
+        """ Set up the environnment, ran before every tests. """
+        super(PagureFlaskPluginUnsignedtests, 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['TICKETS_FOLDER'] = os.path.join(
+            tests.HERE, 'tickets')
+        pagure.APP.config['DOCS_FOLDER'] = os.path.join(
+            tests.HERE, 'docs')
+        self.app = pagure.APP.test_client()
+
+    def test_plugin_noff(self):
+        """ Test the noff 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/Block Un-Signed commits')
+            self.assertEqual(output.status_code, 200)
+            self.assertIn(
+                '<div class="projectinfo m-t-1 m-b-1">\n'
+                'test project #1        </div>', output.data)
+            self.assertIn(
+                '<h3>Block Un-Signed commits settings</h3>',
+                output.data)
+            self.assertTrue(
+                '<input id="active" name="active" type="checkbox" value="y">'
+                in output.data)
+
+            csrf_token = output.data.split(
+                'name="csrf_token" type="hidden" value="')[1].split('">')[0]
+
+            data = {}
+
+            output = self.app.post(
+                '/test/settings/Block Un-Signed commits', data=data)
+            self.assertEqual(output.status_code, 200)
+            self.assertIn(
+                '<div class="projectinfo m-t-1 m-b-1">\n'
+                'test project #1        </div>', output.data)
+            self.assertIn(
+                '<h3>Block Un-Signed commits settings</h3>',
+                output.data)
+            self.assertTrue(
+                '<input id="active" name="active" type="checkbox" value="y">'
+                in output.data)
+
+            data['csrf_token'] = csrf_token
+            # No git found
+            output = self.app.post(
+                '/test/settings/Block Un-Signed commits', data=data)
+            self.assertEqual(output.status_code, 404)
+
+            tests.create_projects_git(tests.HERE)
+
+            # With the git repo
+            output = self.app.post(
+                '/test/settings/Block Un-Signed commits',
+                data=data, follow_redirects=True)
+            self.assertEqual(output.status_code, 200)
+            self.assertIn(
+                '<section class="settings">\n  <h3>Settings for test</h3>',
+                output.data)
+            self.assertTrue(
+                '</button>\n                      Hook Block Un-Signed '
+                'commits inactived' in output.data)
+
+            output = self.app.get(
+                '/test/settings/Block Un-Signed commits')
+            self.assertEqual(output.status_code, 200)
+            self.assertIn(
+                '<div class="projectinfo m-t-1 m-b-1">\n'
+                'test project #1        </div>', output.data)
+            self.assertIn(
+                '<h3>Block Un-Signed commits settings</h3>',
+                output.data)
+            self.assertTrue(
+                '<input id="active" name="active" type="checkbox" value="y">'
+                in output.data)
+
+            self.assertFalse(os.path.exists(os.path.join(
+                tests.HERE, 'test.git', 'hooks',
+                'pre-receive.pagureunsignedcommit')))
+
+            # Activate the hook
+            data = {'csrf_token': csrf_token, 'active': 'y'}
+
+            output = self.app.post(
+                '/test/settings/Block Un-Signed commits',
+                data=data, follow_redirects=True)
+            self.assertEqual(output.status_code, 200)
+            self.assertIn(
+                '<div class="projectinfo m-t-1 m-b-1">\n'
+                'test project #1        </div>', output.data)
+            self.assertIn(
+                '<section class="settings">\n  <h3>Settings for test</h3>',
+                output.data)
+            self.assertNotIn(
+                '</button>\n                      Hook activated',
+                output.data)
+
+            self.assertTrue(os.path.exists(os.path.join(
+                tests.HERE, 'test.git', 'hooks',
+                'pre-receive.pagureunsignedcommit')))
+
+            # De-Activate hook
+            data = {'csrf_token': csrf_token}
+            output = self.app.post(
+                '/test/settings/Block Un-Signed commits',
+                data=data, follow_redirects=True)
+            self.assertEqual(output.status_code, 200)
+            self.assertIn(
+                '<section class="settings">\n  <h3>Settings for test</h3>',
+                output.data)
+            self.assertTrue(
+                '</button>\n                      Hook Block Un-Signed '
+                'commits inactived' in output.data)
+
+            output = self.app.get(
+                '/test/settings/Block Un-Signed commits')
+            self.assertIn(
+                '<div class="projectinfo m-t-1 m-b-1">\n'
+                'test project #1        </div>', output.data)
+            self.assertIn(
+                '<h3>Block Un-Signed commits settings</h3>',
+                output.data)
+            self.assertIn(
+                '<input id="active" name="active" type="checkbox" '
+                'value="y">', output.data)
+
+            self.assertFalse(os.path.exists(os.path.join(
+                tests.HERE, 'test.git', 'hooks',
+                'pre-receive.pagureunsignedcommit')))
+
+
+if __name__ == '__main__':
+    SUITE = unittest.TestLoader().loadTestsFromTestCase(
+        PagureFlaskPluginUnsignedtests)
+    unittest.TextTestRunner(verbosity=2).run(SUITE)

From ae069a9fabf245439702f6fd69a8d4581f524463 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Apr 20 2016 07:20:11 +0000
Subject: [PATCH 3/3] Ask for the Signed-Off-By to be at the front of the line


---

diff --git a/pagure/hooks/files/pagure_block_unsigned.py b/pagure/hooks/files/pagure_block_unsigned.py
index bd366f3..ecc1ffe 100755
--- a/pagure/hooks/files/pagure_block_unsigned.py
+++ b/pagure/hooks/files/pagure_block_unsigned.py
@@ -54,7 +54,7 @@ def run_as_pre_receive_hook():
             signed = False
             for line in pagure.lib.git.read_git_lines(
                 ['log', '--no-walk', commit], abspath):
-                if 'signed-off-by' in line.lower():
+                if line.lower().strip().startswith('signed-off-by'):
                     signed = True
                     break
             if pagure.APP.config.get('HOOK_DEBUG', False):

