{"action":"created","comment":{"url":"https://api.github.com/repos/OCA/server-tools/pulls/comments/58196202","id":58196202,"diff_hunk":"@@ -0,0 +1,157 @@\n+# -*- coding: utf-8 -*-\n+# © 2016 Therp BV <http://therp.nl>\n+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).\n+import os\n+import logging\n+import urllib2\n+import urlparse\n+import subprocess\n+import tempfile\n+from openerp import _, api, models, exceptions\n+from openerp.tools.config import _get_default_datadir\n+from openerp.tools.misc import file_open\n+\n+\n+DEFAULT_KEY_LENGTH = 4096\n+_logger = logging.getLogger(__name__)\n+\n+\n+class Letsencrypt(models.AbstractModel):\n+    _name = 'letsencrypt'\n+    _description = 'Abstract model providing functions for letsencrypt'\n+\n+    @api.model\n+    def get_data_dir(self):\n+        return os.path.join(_get_default_datadir(), 'letsencrypt')\n+\n+    @api.model\n+    def call_cmdline(self, cmdline, loglevel=logging.INFO,\n+                     raise_on_result=True):\n+        process = subprocess.Popen(\n+            cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n+        stdout, stderr = process.communicate()\n+        if stderr:\n+            _logger.log(loglevel, stderr)\n+        if stdout:\n+            _logger.log(loglevel, stdout)\n+\n+        if process.returncode:\n+            raise exceptions.Warning(\n+                _('Error calling %s: %d') % (cmdline[0], process.returncode),\n+                ' '.join(cmdline),\n+            )\n+\n+        return process.returncode\n+\n+    @api.model\n+    def generate_account_key(self):\n+        data_dir = self.get_data_dir()\n+        if not os.path.isdir(data_dir):\n+            os.makedirs(data_dir)\n+        account_key = os.path.join(data_dir, 'account.key')\n+        if not os.path.isfile(account_key):\n+            _logger.info('generating rsa account key')\n+            self.call_cmdline([\n+                'openssl', 'genrsa', '-out', account_key,\n+                str(DEFAULT_KEY_LENGTH),\n+            ])\n+        assert os.path.isfile(account_key), 'failed to create rsa key'\n+        return account_key\n+\n+    @api.model\n+    def generate_domain_key(self, domain):\n+        domain_key = os.path.join(self.get_data_dir(), '%s.key' % domain)\n+        if not os.path.isfile(domain_key):\n+            _logger.info('generating rsa domain key for %s', domain)\n+            self.call_cmdline([\n+                'openssl', 'genrsa', '-out', domain_key,\n+                str(DEFAULT_KEY_LENGTH),\n+            ])\n+        return domain_key\n+\n+    @api.model\n+    def validate_domain(self, domain):\n+        if domain in ['localhost', '127.0.0.1'] or\\\n+                domain.startswith('10.') or\\\n+                domain.startswith('172.16.') or\\\n+                domain.startswith('192.168.'):\n+            raise exceptions.Warning(\n+                _(\"Let's encrypt doesn't work with private addresses!\"))\n+\n+    @api.model\n+    def generate_csr(self, domain):\n+        domains = [domain]\n+        i = 0\n+        while self.env['ir.config_parameter'].get_param(\n+                'letsencrypt.altname.%d' % i):\n+            domains.append(\n+                self.env['ir.config_parameter']\n+                .get_param('letsencrypt.altname.%d' % i)\n+            )\n+            i += 1\n+        _logger.info('generating csr for %s', domain)\n+        if len(domains) > 1:\n+            _logger.info('with alternative subjects %s', ','.join(domains[1:]))\n+        config = self.env['ir.config_parameter'].get_param(\n+            'letsencrypt.openssl.cnf', '/etc/ssl/openssl.cnf')\n+        csr = os.path.join(self.get_data_dir(), '%s.csr' % domain)\n+        with tempfile.NamedTemporaryFile() as cfg:\n+            cfg.write(open(config).read())\n+            if len(domains) > 1:\n+                cfg.write(\n+                    '\\n[SAN]\\nsubjectAltName=' +\n+                    ','.join(map(lambda x: 'DNS:%s' % x, domains)) + '\\n')\n+            cfg.file.flush()\n+            cmdline = [\n+                'openssl', 'req', '-new',\n+                self.env['ir.config_parameter'].get_param(\n+                    'letsencrypt.openssl.digest', '-sha256'),\n+                '-key', self.generate_domain_key(domain),\n+                '-subj', '/CN=%s' % domain, '-config', cfg.name,\n+                '-out', csr,\n+            ]\n+            if len(domains) > 1:\n+                cmdline.extend([\n+                    '-reqexts', 'SAN',\n+                ])\n+            self.call_cmdline(cmdline)\n+        return csr\n+\n+    @api.model\n+    def cron(self):\n+        domain = urlparse.urlparse(\n+            self.env['ir.config_parameter'].get_param(\n+                'web.base.url', 'localhost')).netloc\n+        self.validate_domain(domain)\n+        account_key = self.generate_account_key()\n+        csr = self.generate_csr(domain)\n+        manifest, manifest_path = file_open(\n+            'letsencrypt/__openerp__.py', pathinfo=True)\n+        manifest.close()\n+        acme_challenge = os.path.join(\n+            os.path.dirname(manifest_path),\n+            'static', 'acme-challenge')\n+        if not os.path.isdir(acme_challenge):\n+            os.makedirs(acme_challenge)\n+        if self.env.context.get('letsencrypt_dry_run'):\n+            crt_text = 'I\\'m a test text'\n+        else:  # pragma: no cover\n+            from .acme_tiny import get_crt, DEFAULT_CA\n+            crt_text = get_crt(\n+                account_key, csr, acme_challenge, log=_logger, CA=DEFAULT_CA)\n+        with open(os.path.join(self.get_data_dir(), '%s.crt' % domain), 'w')\\\n+                as crt:\n+            crt.write(crt_text)\n+            chain_cert = urllib2.urlopen(\n+                self.env['ir.config_parameter'].get_param(\n+                    'letsencrypt.chain_certificate_address',\n+                    'https://letsencrypt.org/certs/'\n+                    'lets-encrypt-x1-cross-signed.pem')\n+            )\n+            crt.write(chain_cert.read())\n+            chain_cert.close()\n+            _logger.info('wrote %s', crt.name)\n+        self.call_cmdline([\n+            'sh', '-c', self.env['ir.config_parameter'].get_param(\n+                'letsencrypt.reload_command', ''),","path":"letsencrypt/models/letsencrypt.py","position":156,"original_position":156,"commit_id":"c97b0400ac36bec3c4fcd354976add4d10f14384","original_commit_id":"c97b0400ac36bec3c4fcd354976add4d10f14384","user":{"login":"Yajo","id":973709,"avatar_url":"https://avatars.githubusercontent.com/u/973709?v=3","gravatar_id":"","url":"https://api.github.com/users/Yajo","html_url":"https://github.com/Yajo","followers_url":"https://api.github.com/users/Yajo/followers","following_url":"https://api.github.com/users/Yajo/following{/other_user}","gists_url":"https://api.github.com/users/Yajo/gists{/gist_id}","starred_url":"https://api.github.com/users/Yajo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Yajo/subscriptions","organizations_url":"https://api.github.com/users/Yajo/orgs","repos_url":"https://api.github.com/users/Yajo/repos","events_url":"https://api.github.com/users/Yajo/events{/privacy}","received_events_url":"https://api.github.com/users/Yajo/received_events","type":"User","site_admin":false},"body":"Why the last `''`?","created_at":"2016-04-01T12:04:15Z","updated_at":"2016-04-01T12:04:15Z","html_url":"https://github.com/OCA/server-tools/pull/347#discussion_r58196202","pull_request_url":"https://api.github.com/repos/OCA/server-tools/pulls/347","_links":{"self":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/comments/58196202"},"html":{"href":"https://github.com/OCA/server-tools/pull/347#discussion_r58196202"},"pull_request":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/347"}}},"pull_request":{"url":"https://api.github.com/repos/OCA/server-tools/pulls/347","id":57852562,"html_url":"https://github.com/OCA/server-tools/pull/347","diff_url":"https://github.com/OCA/server-tools/pull/347.diff","patch_url":"https://github.com/OCA/server-tools/pull/347.patch","issue_url":"https://api.github.com/repos/OCA/server-tools/issues/347","number":347,"state":"open","locked":false,"title":"[ADD] letsencrypt","user":{"login":"hbrunn","id":2563186,"avatar_url":"https://avatars.githubusercontent.com/u/2563186?v=3","gravatar_id":"","url":"https://api.github.com/users/hbrunn","html_url":"https://github.com/hbrunn","followers_url":"https://api.github.com/users/hbrunn/followers","following_url":"https://api.github.com/users/hbrunn/following{/other_user}","gists_url":"https://api.github.com/users/hbrunn/gists{/gist_id}","starred_url":"https://api.github.com/users/hbrunn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hbrunn/subscriptions","organizations_url":"https://api.github.com/users/hbrunn/orgs","repos_url":"https://api.github.com/users/hbrunn/repos","events_url":"https://api.github.com/users/hbrunn/events{/privacy}","received_events_url":"https://api.github.com/users/hbrunn/received_events","type":"User","site_admin":false},"body":"=============================================\r\nRequest SSL certificates from letsencrypt.org\r\n=============================================\r\n\r\nThis module was written to have your Odoo installation request SSL certificates\r\nfrom https://letsencrypt.org automatically.\r\n\r\nInstallation\r\n============\r\n\r\nAfter installation, this module generates a private key for your account at\r\nletsencrypt.org automatically in ``$data_dir/letsencrypt/account.key``. If you\r\nwant or need to use your own account key, replace the file.\r\n\r\nFor certificate requests to work, your site needs to be accessible via plain\r\nHTTP, see below for configuration examples in case you force your clients to\r\nthe SSL version.\r\n\r\nAfter installation, trigger the cronjob `Update letsencrypt certificates` and\r\nwatch your log for messages.\r\n\r\nConfiguration\r\n=============\r\n\r\nThis addons requests a certificate for the domain named in the configuration\r\nparameter ``web.base.url`` - if this comes back as ``localhost`` or the like,\r\nthe module doesn't request anything.\r\n\r\nIf you want your certificate to contain multiple alternative names, just add\r\nthem as configuration parameters ``letsencrypt.altname.N`` with ``N`` starting\r\nfrom ``0``. The amount of domains that can be added are subject to `rate\r\nlimiting <https://community.letsencrypt.org/t/rate-limits-for-lets-encrypt/6769>`_.\r\n\r\nNote that all those domains must be publicly reachable on port 80 via HTTP, and\r\nthey must have an entry for ``.well-known/acme-challenge`` pointing to your odoo\r\ninstance.\r\n\r\nUsage\r\n=====\r\n\r\nThe module sets up a cronjob that requests and renews certificates automatically.\r\n\r\nAfter the first run, you'll find a file called ``domain.crt`` in\r\n``$datadir/letsencrypt``, configure your SSL proxy to use this file as certificate.\r\n\r\nIn depth configuration\r\n======================\r\n\r\nThis module uses ``openssl`` to generate CSRs suitable to be submitted to\r\nletsencrypt.org. In order to do this, it copies ``/etc/ssl/openssl.cnf`` to a\r\ntemporary and adapts it according to its needs (currently, that's just adding a\r\n``[SAN]`` section if necessary). If you want the module to use another configuration\r\ntemplate, set config parameter ``letsencrypt.openssl.cnf``.\r\n\r\nAfter refreshing the certificate, the module attempts to run the content of\r\n``letsencrypt.reload_command``, which is by default ``sudo service nginx reload``.\r\nChange this to match your server's configuration.\r\n\r\nYou'll also need a matching sudo configuration, like::\r\n\r\n    your_odoo_user ALL = NOPASSWD: /usr/sbin/service nginx reload\r\n\r\nFurther, if you force users to https, you'll need something like::\r\n\r\n    if ($scheme = \"http\") {\r\n        set $redirect_https 1;\r\n    }\r\n    if ($request_uri ~ ^/.well-known/acme-challenge/) {\r\n        set $redirect_https 0;\r\n    }\r\n    if ($redirect_https) {\r\n        rewrite ^   https://$server_name$request_uri? permanent;\r\n    }\r\n","created_at":"2016-02-01T15:16:40Z","updated_at":"2016-04-01T12:04:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c08142b2a26e695a88de8b572c76609c38d507cb","assignee":null,"milestone":{"url":"https://api.github.com/repos/OCA/server-tools/milestones/3","html_url":"https://github.com/OCA/server-tools/milestones/8.0","labels_url":"https://api.github.com/repos/OCA/server-tools/milestones/3/labels","id":983662,"number":3,"title":"8.0","description":"","creator":{"login":"max3903","id":939952,"avatar_url":"https://avatars.githubusercontent.com/u/939952?v=3","gravatar_id":"","url":"https://api.github.com/users/max3903","html_url":"https://github.com/max3903","followers_url":"https://api.github.com/users/max3903/followers","following_url":"https://api.github.com/users/max3903/following{/other_user}","gists_url":"https://api.github.com/users/max3903/gists{/gist_id}","starred_url":"https://api.github.com/users/max3903/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/max3903/subscriptions","organizations_url":"https://api.github.com/users/max3903/orgs","repos_url":"https://api.github.com/users/max3903/repos","events_url":"https://api.github.com/users/max3903/events{/privacy}","received_events_url":"https://api.github.com/users/max3903/received_events","type":"User","site_admin":false},"open_issues":35,"closed_issues":56,"state":"open","created_at":"2015-02-17T21:52:00Z","updated_at":"2016-03-31T06:20:31Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/OCA/server-tools/pulls/347/commits","review_comments_url":"https://api.github.com/repos/OCA/server-tools/pulls/347/comments","review_comment_url":"https://api.github.com/repos/OCA/server-tools/pulls/comments{/number}","comments_url":"https://api.github.com/repos/OCA/server-tools/issues/347/comments","statuses_url":"https://api.github.com/repos/OCA/server-tools/statuses/c97b0400ac36bec3c4fcd354976add4d10f14384","head":{"label":"hbrunn:8.0-letsencrypt","ref":"8.0-letsencrypt","sha":"c97b0400ac36bec3c4fcd354976add4d10f14384","user":{"login":"hbrunn","id":2563186,"avatar_url":"https://avatars.githubusercontent.com/u/2563186?v=3","gravatar_id":"","url":"https://api.github.com/users/hbrunn","html_url":"https://github.com/hbrunn","followers_url":"https://api.github.com/users/hbrunn/followers","following_url":"https://api.github.com/users/hbrunn/following{/other_user}","gists_url":"https://api.github.com/users/hbrunn/gists{/gist_id}","starred_url":"https://api.github.com/users/hbrunn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hbrunn/subscriptions","organizations_url":"https://api.github.com/users/hbrunn/orgs","repos_url":"https://api.github.com/users/hbrunn/repos","events_url":"https://api.github.com/users/hbrunn/events{/privacy}","received_events_url":"https://api.github.com/users/hbrunn/received_events","type":"User","site_admin":false},"repo":{"id":22118758,"name":"server-tools","full_name":"hbrunn/server-tools","owner":{"login":"hbrunn","id":2563186,"avatar_url":"https://avatars.githubusercontent.com/u/2563186?v=3","gravatar_id":"","url":"https://api.github.com/users/hbrunn","html_url":"https://github.com/hbrunn","followers_url":"https://api.github.com/users/hbrunn/followers","following_url":"https://api.github.com/users/hbrunn/following{/other_user}","gists_url":"https://api.github.com/users/hbrunn/gists{/gist_id}","starred_url":"https://api.github.com/users/hbrunn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hbrunn/subscriptions","organizations_url":"https://api.github.com/users/hbrunn/orgs","repos_url":"https://api.github.com/users/hbrunn/repos","events_url":"https://api.github.com/users/hbrunn/events{/privacy}","received_events_url":"https://api.github.com/users/hbrunn/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/hbrunn/server-tools","description":"","fork":true,"url":"https://api.github.com/repos/hbrunn/server-tools","forks_url":"https://api.github.com/repos/hbrunn/server-tools/forks","keys_url":"https://api.github.com/repos/hbrunn/server-tools/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hbrunn/server-tools/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hbrunn/server-tools/teams","hooks_url":"https://api.github.com/repos/hbrunn/server-tools/hooks","issue_events_url":"https://api.github.com/repos/hbrunn/server-tools/issues/events{/number}","events_url":"https://api.github.com/repos/hbrunn/server-tools/events","assignees_url":"https://api.github.com/repos/hbrunn/server-tools/assignees{/user}","branches_url":"https://api.github.com/repos/hbrunn/server-tools/branches{/branch}","tags_url":"https://api.github.com/repos/hbrunn/server-tools/tags","blobs_url":"https://api.github.com/repos/hbrunn/server-tools/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hbrunn/server-tools/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hbrunn/server-tools/git/refs{/sha}","trees_url":"https://api.github.com/repos/hbrunn/server-tools/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hbrunn/server-tools/statuses/{sha}","languages_url":"https://api.github.com/repos/hbrunn/server-tools/languages","stargazers_url":"https://api.github.com/repos/hbrunn/server-tools/stargazers","contributors_url":"https://api.github.com/repos/hbrunn/server-tools/contributors","subscribers_url":"https://api.github.com/repos/hbrunn/server-tools/subscribers","subscription_url":"https://api.github.com/repos/hbrunn/server-tools/subscription","commits_url":"https://api.github.com/repos/hbrunn/server-tools/commits{/sha}","git_commits_url":"https://api.github.com/repos/hbrunn/server-tools/git/commits{/sha}","comments_url":"https://api.github.com/repos/hbrunn/server-tools/comments{/number}","issue_comment_url":"https://api.github.com/repos/hbrunn/server-tools/issues/comments{/number}","contents_url":"https://api.github.com/repos/hbrunn/server-tools/contents/{+path}","compare_url":"https://api.github.com/repos/hbrunn/server-tools/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hbrunn/server-tools/merges","archive_url":"https://api.github.com/repos/hbrunn/server-tools/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hbrunn/server-tools/downloads","issues_url":"https://api.github.com/repos/hbrunn/server-tools/issues{/number}","pulls_url":"https://api.github.com/repos/hbrunn/server-tools/pulls{/number}","milestones_url":"https://api.github.com/repos/hbrunn/server-tools/milestones{/number}","notifications_url":"https://api.github.com/repos/hbrunn/server-tools/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hbrunn/server-tools/labels{/name}","releases_url":"https://api.github.com/repos/hbrunn/server-tools/releases{/id}","deployments_url":"https://api.github.com/repos/hbrunn/server-tools/deployments","created_at":"2014-07-22T20:04:24Z","updated_at":"2014-07-07T03:06:56Z","pushed_at":"2016-03-23T16:03:25Z","git_url":"git://github.com/hbrunn/server-tools.git","ssh_url":"git@github.com:hbrunn/server-tools.git","clone_url":"https://github.com/hbrunn/server-tools.git","svn_url":"https://github.com/hbrunn/server-tools","homepage":null,"size":3128,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"OCA:8.0","ref":"8.0","sha":"22c1c7efdc15f27a2597d59f2509899702491424","user":{"login":"OCA","id":7600578,"avatar_url":"https://avatars.githubusercontent.com/u/7600578?v=3","gravatar_id":"","url":"https://api.github.com/users/OCA","html_url":"https://github.com/OCA","followers_url":"https://api.github.com/users/OCA/followers","following_url":"https://api.github.com/users/OCA/following{/other_user}","gists_url":"https://api.github.com/users/OCA/gists{/gist_id}","starred_url":"https://api.github.com/users/OCA/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OCA/subscriptions","organizations_url":"https://api.github.com/users/OCA/orgs","repos_url":"https://api.github.com/users/OCA/repos","events_url":"https://api.github.com/users/OCA/events{/privacy}","received_events_url":"https://api.github.com/users/OCA/received_events","type":"Organization","site_admin":false},"repo":{"id":20896532,"name":"server-tools","full_name":"OCA/server-tools","owner":{"login":"OCA","id":7600578,"avatar_url":"https://avatars.githubusercontent.com/u/7600578?v=3","gravatar_id":"","url":"https://api.github.com/users/OCA","html_url":"https://github.com/OCA","followers_url":"https://api.github.com/users/OCA/followers","following_url":"https://api.github.com/users/OCA/following{/other_user}","gists_url":"https://api.github.com/users/OCA/gists{/gist_id}","starred_url":"https://api.github.com/users/OCA/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OCA/subscriptions","organizations_url":"https://api.github.com/users/OCA/orgs","repos_url":"https://api.github.com/users/OCA/repos","events_url":"https://api.github.com/users/OCA/events{/privacy}","received_events_url":"https://api.github.com/users/OCA/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/OCA/server-tools","description":"Tools for Odoo Administrators to improve some technical features on Odoo.","fork":false,"url":"https://api.github.com/repos/OCA/server-tools","forks_url":"https://api.github.com/repos/OCA/server-tools/forks","keys_url":"https://api.github.com/repos/OCA/server-tools/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OCA/server-tools/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OCA/server-tools/teams","hooks_url":"https://api.github.com/repos/OCA/server-tools/hooks","issue_events_url":"https://api.github.com/repos/OCA/server-tools/issues/events{/number}","events_url":"https://api.github.com/repos/OCA/server-tools/events","assignees_url":"https://api.github.com/repos/OCA/server-tools/assignees{/user}","branches_url":"https://api.github.com/repos/OCA/server-tools/branches{/branch}","tags_url":"https://api.github.com/repos/OCA/server-tools/tags","blobs_url":"https://api.github.com/repos/OCA/server-tools/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OCA/server-tools/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OCA/server-tools/git/refs{/sha}","trees_url":"https://api.github.com/repos/OCA/server-tools/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OCA/server-tools/statuses/{sha}","languages_url":"https://api.github.com/repos/OCA/server-tools/languages","stargazers_url":"https://api.github.com/repos/OCA/server-tools/stargazers","contributors_url":"https://api.github.com/repos/OCA/server-tools/contributors","subscribers_url":"https://api.github.com/repos/OCA/server-tools/subscribers","subscription_url":"https://api.github.com/repos/OCA/server-tools/subscription","commits_url":"https://api.github.com/repos/OCA/server-tools/commits{/sha}","git_commits_url":"https://api.github.com/repos/OCA/server-tools/git/commits{/sha}","comments_url":"https://api.github.com/repos/OCA/server-tools/comments{/number}","issue_comment_url":"https://api.github.com/repos/OCA/server-tools/issues/comments{/number}","contents_url":"https://api.github.com/repos/OCA/server-tools/contents/{+path}","compare_url":"https://api.github.com/repos/OCA/server-tools/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OCA/server-tools/merges","archive_url":"https://api.github.com/repos/OCA/server-tools/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OCA/server-tools/downloads","issues_url":"https://api.github.com/repos/OCA/server-tools/issues{/number}","pulls_url":"https://api.github.com/repos/OCA/server-tools/pulls{/number}","milestones_url":"https://api.github.com/repos/OCA/server-tools/milestones{/number}","notifications_url":"https://api.github.com/repos/OCA/server-tools/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OCA/server-tools/labels{/name}","releases_url":"https://api.github.com/repos/OCA/server-tools/releases{/id}","deployments_url":"https://api.github.com/repos/OCA/server-tools/deployments","created_at":"2014-06-16T18:52:44Z","updated_at":"2016-03-17T15:03:53Z","pushed_at":"2016-04-01T09:09:52Z","git_url":"git://github.com/OCA/server-tools.git","ssh_url":"git@github.com:OCA/server-tools.git","clone_url":"https://github.com/OCA/server-tools.git","svn_url":"https://github.com/OCA/server-tools","homepage":"https://odoo-community.org/project/tools-maintainers-30","size":9649,"stargazers_count":98,"watchers_count":98,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":275,"mirror_url":null,"open_issues_count":105,"forks":275,"open_issues":105,"watchers":98,"default_branch":"9.0"}},"_links":{"self":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/347"},"html":{"href":"https://github.com/OCA/server-tools/pull/347"},"issue":{"href":"https://api.github.com/repos/OCA/server-tools/issues/347"},"comments":{"href":"https://api.github.com/repos/OCA/server-tools/issues/347/comments"},"review_comments":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/347/comments"},"review_comment":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/347/commits"},"statuses":{"href":"https://api.github.com/repos/OCA/server-tools/statuses/c97b0400ac36bec3c4fcd354976add4d10f14384"}}}}