Browse Source

[git] Git hosting init

v12_initial_fix
Maciej Wawro 6 years ago
parent
commit
c84bdc5120
  1. 1
      galicea_git/README.md
  2. 5
      galicea_git/__init__.py
  3. 48
      galicea_git/__manifest__.py
  4. 3
      galicea_git/controllers/__init__.py
  5. 83
      galicea_git/controllers/main.py
  6. 9
      galicea_git/data/config.xml
  7. 63
      galicea_git/http_chunked_fix.py
  8. 4
      galicea_git/models/__init__.py
  9. 42
      galicea_git/models/config_settings.py
  10. 100
      galicea_git/models/repository.py
  11. 3
      galicea_git/security/ir.model.access.csv
  12. 47
      galicea_git/security/security.xml
  13. BIN
      galicea_git/static/description/icon.png
  14. BIN
      galicea_git/static/description/images/config_screenshot.png
  15. BIN
      galicea_git/static/description/images/console_screenshot.png
  16. BIN
      galicea_git/static/description/images/create_screenshot.png
  17. 19
      galicea_git/static/description/index.html
  18. 37
      galicea_git/system_checks.py
  19. 66
      galicea_git/views/views.xml

1
galicea_git/README.md

@ -0,0 +1 @@
[See add-on page on odoo.com](https://apps.odoo.com/apps/modules/10.0/galicea_git/)

5
galicea_git/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import system_checks

48
galicea_git/__manifest__.py

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
{
'name': "Galicea Git hosting",
'summary': """Git repository hosting and per-user access checking""",
'author': "Maciej Wawro",
'maintainer': "Galicea",
'website': "http://galicea.pl",
'category': 'Technical Settings',
'version': '10.0.1.0',
'depends': ['web', 'galicea_environment_checkup'],
'external_dependencies': {
'bin': ['git']
},
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/config.xml',
'views/views.xml',
],
'images': [
'static/description/images/create_screenshot.png',
'static/description/images/config_screenshot.png',
'static/description/images/console_screenshot.png',
],
'application': True,
'installable': True,
'environment_checkup': {
'dependencies': {
'external': [
{
'name': 'git',
'version': '>=2.1.4',
'install': "apt install git"
}
]
}
}
}

3
galicea_git/controllers/__init__.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import main

83
galicea_git/controllers/main.py

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
import subprocess, os, io
from odoo import http
from odoo.tools import config
import werkzeug
from ..http_chunked_fix import http_input_stream
class Main(http.Controller):
@http.route(
[
'/git/<repo>',
'/git/<repo>/<path:path>',
],
auth='public',
csrf=False
)
def git(self, request, repo, **kw):
auth = request.httprequest.authorization
if auth:
request.session.authenticate(request.session.db, auth.username, auth.password)
if not request.env.uid or request.env.user.login == 'public':
return werkzeug.Response(
headers=[('WWW-Authenticate', 'Basic')],
status=401
)
try:
repository = request.env['galicea_git.repository'].search(
[('system_name', '=', repo)]
)
except AccessError:
return werkzeug.Response(
status=403
)
if not repository.exists():
return werkzeug.Response(
status=404
)
http_environment = request.httprequest.environ
git_env = {
'REQUEST_METHOD': http_environment['REQUEST_METHOD'],
'QUERY_STRING': http_environment['QUERY_STRING'],
'CONTENT_TYPE': request.httprequest.headers.get('Content-Type'),
'REMOTE_ADDR': http_environment['REMOTE_ADDR'],
'GIT_PROJECT_ROOT': os.path.join(config['data_dir'], 'git'),
'GIT_HTTP_EXPORT_ALL': '1',
'PATH_INFO': http_environment['PATH_INFO'][4:],
'REMOTE_USER': request.env.user.login
}
command_env = os.environ.copy()
for var in git_env:
command_env[var] = git_env[var]
git = subprocess.Popen(
['/usr/lib/git-core/git-http-backend'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=command_env,
shell=True
)
stdout, stderr = git.communicate(http_input_stream(request).read())
headers_str, body = stdout.split("\r\n\r\n", 2)
http_status_code = 200
headers = []
for header in headers_str.split("\r\n"):
name, value = header.split(': ', 2)
if name == 'Status':
http_code = int(value.split(' ')[0])
else:
headers.append((name, value))
return werkzeug.Response(
body,
status = http_status_code,
headers = headers
)

9
galicea_git/data/config.xml

@ -0,0 +1,9 @@
<odoo>
<data noupdate="1">
<record id="config_git_backend_path" model="ir.config_parameter">
<field name="key">galicea_git.git_http_backend</field>
<field name="value">/usr/lib/git-core/git-http-backend</field>
<field name="group_ids" eval="[(4, ref('galicea_git.group_admin'))]" />
</record>
</data>
</odoo>

63
galicea_git/http_chunked_fix.py

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
import io
# Werkzeug version bundled into odoo doesn't handle this kind of Transfer-Encoding
# correctly. We copy the fix from https://github.com/pallets/werkzeug/pull/1198/files
class DechunkedInput(io.RawIOBase):
"""An input stream that handles Transfer-Encoding 'chunked'"""
def __init__(self, rfile):
self._rfile = rfile
self._done = False
self._len = 0
def readable(self):
return True
def read_chunk_len(self):
try:
line = self._rfile.readline().decode('latin1')
_len = int(line.strip(), 16)
except ValueError:
raise IOError('Invalid chunk header')
if _len < 0:
raise IOError('Negative chunk length not allowed')
return _len
def readinto(self, buf):
read = 0
while not self._done and read < len(buf):
if self._len == 0:
# This is the first chunk or we fully consumed the previous
# one. Read the next length of the next chunk
self._len = self.read_chunk_len()
if self._len == 0:
# Found the final chunk of size 0. The stream is now exhausted,
# but there is still a final newline that should be consumed
self._done = True
if self._len > 0:
# There is data (left) in this chunk, so append it to the
# buffer. If this operation fully consumes the chunk, this will
# reset self._len to 0.
n = min(len(buf), self._len)
buf[read:read + n] = self._rfile.read(n)
self._len -= n
read += n
if self._len == 0:
# Skip the terminating newline of a chunk that has been fully
# consumed. This also applies to the 0-sized final chunk
terminator = self._rfile.readline()
if terminator not in (b'\n', b'\r\n', b'\r'):
raise IOError('Missing chunk terminating newline')
return read
def http_input_stream(request):
if request.httprequest.headers.get('Transfer-Encoding') == 'chunked' \
and not request.httprequest.environ.get('wsgi.input_terminated'):
return DechunkedInput(request.httprequest.environ['wsgi.input'])
return request.httprequest.stream

4
galicea_git/models/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import repository
from . import config_settings

42
galicea_git/models/config_settings.py

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import os
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class ConfigSettings(models.TransientModel):
_name = 'galicea_git.config.settings'
_inherit = 'res.config.settings'
git_http_backend = fields.Char(
'Absolute path to Git HTTP backend',
required=True
)
git_http_backend_valid = fields.Boolean(
compute='_compute_git_http_backend_valid'
)
@api.one
@api.depends('git_http_backend')
def _compute_git_http_backend_valid(self):
self.git_http_backend_valid = self.git_http_backend and os.access(self.git_http_backend, os.X_OK)
@api.one
def set_params(self):
self.env['ir.config_parameter'].set_param('galicea_git.git_http_backend', self.git_http_backend)
@api.model
def get_default_values(self, fields):
return {
'git_http_backend': self.env['ir.config_parameter'].get_param('galicea_git.git_http_backend')
}
@api.multi
def execute(self):
self.ensure_one()
if not self.env.user.has_group('galicea_git.group_admin'):
raise AccessError("Only Git administrators can change those settings")
super(ConfigSettings, self.sudo()).execute()
act_window = self.env.ref('galicea_git.config_settings_action')
return act_window.read()[0]

100
galicea_git/models/repository.py

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
import os
import random
import shutil
import string
import subprocess
from odoo import models, fields, api, http
from odoo.exceptions import ValidationError
from odoo.tools import config, which
class Repository(models.Model):
_name = 'galicea_git.repository'
state = fields.Selection(
[('draft', 'Draft'), ('created', 'Created')],
default='draft'
)
name = fields.Char('User-friendly name', required=True)
system_name = fields.Char(
'Directory name',
required=True,
readonly=True,
index=True,
states={'draft': [('readonly', False)]}
)
collaborator_ids = fields.Many2many(
'res.users',
string='Collaborators'
)
local_directory = fields.Char(
'Local directory on server',
compute='_compute_local_directory',
groups='galicea_git.group_admin'
)
url = fields.Char(
'Clone',
compute='_compute_url'
)
@api.one
@api.depends('system_name')
def _compute_url(self):
base_url = http.request.httprequest.host_url if http.request \
else env['ir.config_parameter'].get_param('web.base.url') + '/'
self.url = u'{}git/{}'.format(base_url, self.system_name)
@api.one
@api.depends('system_name')
def _compute_local_directory(self):
if self.system_name:
self.local_directory = os.path.join(config['data_dir'], 'git', self.system_name)
@api.constrains('system_name')
def _validate_system_name(self):
allowed_characters = string.ascii_lowercase + string.digits + '-'
if not all(c in allowed_characters for c in self.system_name):
raise ValidationError(
'Only lowercase, digits and hyphens (-) are allowed in directory name'
)
@api.constrains('collaborator_ids')
def _validate_collaborator_ids(self):
invalid_collaborators = self.collaborator_ids.filtered(lambda c: not c.has_group('galicea_git.group_collaborator'))
if invalid_collaborators:
raise ValidationError(
'User {} does not have the {} role. Contact your Administrator'.format(
invalid_collaborators[0].name,
self.env.ref('galicea_git.group_collaborator').full_name
)
)
@api.model
def create(self, values):
values['state'] = 'created'
ret = super(Repository, self).create(values)
ret.__initialize_repo()
return ret
@api.multi
def unlink(selfs):
directories_to_move = selfs.mapped(lambda r: r.local_directory)
ret = super(Repository, selfs).unlink()
for directory in directories_to_move:
if os.path.exists(directory):
suffix = ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
new_directory = directory + '-deleted-' + suffix
shutil.move(directory, new_directory)
@api.multi
def __initialize_repo(self):
self.ensure_one()
if os.path.exists(self.local_directory):
raise ValidationError(
'Repository {} already exists, choose a different name!'.format(self.system_name)
)
subprocess.check_call([which('git'), 'init', '--bare', self.local_directory])

3
galicea_git/security/ir.model.access.csv

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_repository_collaborator,repository_collaborator,model_galicea_git_repository,galicea_git.group_collaborator,1,0,0,0
access_repository_admin,repository_admin,model_galicea_git_repository,galicea_git.group_admin,1,1,1,1

47
galicea_git/security/security.xml

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="module_category_git" model="ir.module.category">
<field name="name">Git</field>
</record>
<record id="group_collaborator" model="res.groups">
<field name="name">Collaborator</field>
<field name="category_id" ref="module_category_git" />
</record>
<record id="group_admin" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="module_category_git" />
<field name="implied_ids" eval="[(4,ref('group_collaborator'))]" />
</record>
<record id="base.group_erp_manager" model="res.groups">
<field name="implied_ids" eval="[(4,ref('group_admin'))]" />
</record>
<record id="repository_collaborator_access_rule" model="ir.rule">
<field name="name">Collaborators can only access repositories they are assigned to</field>
<field name="model_id" ref="model_galicea_git_repository" />
<field name="groups" eval="[(4, ref('group_collaborator'))]"/>
<field name="domain_force">
[('collaborator_ids', 'in', user.id)]
</field>
<field eval="1" name="perm_read" />
<field eval="0" name="perm_write" />
<field eval="0" name="perm_create" />
<field eval="0" name="perm_unlink" />
</record>
<record id="repository_admin_access_rule" model="ir.rule">
<field name="name">Administrators can access any repositories</field>
<field name="model_id" ref="model_galicea_git_repository" />
<field name="groups" eval="[(4, ref('group_admin'))]"/>
<field name="domain_force">
[(1, '=', 1)]
</field>
<field eval="1" name="perm_read" />
<field eval="0" name="perm_write" />
<field eval="0" name="perm_create" />
<field eval="0" name="perm_unlink" />
</record>
</odoo>

BIN
galicea_git/static/description/icon.png

After

Width: 80  |  Height: 80  |  Size: 3.8 KiB

BIN
galicea_git/static/description/images/config_screenshot.png

After

Width: 800  |  Height: 354  |  Size: 42 KiB

BIN
galicea_git/static/description/images/console_screenshot.png

After

Width: 738  |  Height: 359  |  Size: 78 KiB

BIN
galicea_git/static/description/images/create_screenshot.png

After

Width: 805  |  Height: 273  |  Size: 31 KiB

19
galicea_git/static/description/index.html

@ -0,0 +1,19 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Galicea Git hosting</h2>
<h3 class="oe_slogan">
Simple Odoo-based HTTP interface for Git repository hosting
</h3>
<p>
This add-on allows you to create Git repositories hosted by Odoo, and add specific Odoo users as collaborators. Only those users will have access to the repository. It requires <tt>git</tt> package, including <tt>git-http-backend</tt>, installed in the system. For Ubuntu/Debian it's enough to call
<pre>sudo apt install git</pre>
</p>
<h3>Creating repositories</h3>
<img class="oe_picture oe_screenshot" src="images/create_screenshot.png" />
<img class="oe_picture oe_screenshot" src="images/config_screenshot.png" />
<h3>Interacting with the repository</h3>
<img class="oe_picture oe_screenshot" src="images/console_screenshot.png" />
</div>
</div>
</section>

37
galicea_git/system_checks.py

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import os
from odoo.addons.galicea_environment_checkup import \
custom_check, CheckWarning, CheckSuccess, CheckFail
from odoo import http
@custom_check
def check_single_db(env):
if not http.request:
raise CheckWarning('Could not detect DB settings.')
dbs = http.db_list(True, http.request.httprequest)
if len(dbs) == 1:
return CheckSuccess('Odoo runs in a single-DB mode.')
details = (
'<p>Odoo runs in a multi-DB mode, which will cause Git HTTP requests to fail.</p>'
'<p>Run Odoo with <tt>--dbfilter</tt> or <tt>--database</tt> flag.</p>'
)
return CheckFail(
'Odoo runs in a multi-DB mode.',
details=details
)
@custom_check
def check_http_backend(env):
backend_path = env['ir.config_parameter'].sudo().get_param(
'galicea_git.git_http_backend'
)
if not os.access(backend_path, os.X_OK):
raise CheckFail(
'Git HTTP backend not found',
details='<a href="http://galicea.mw-odoo:8080/web#action=galicea_git.config_settings_action">Check the configuration here</a>'
)
return CheckSuccess('Git HTTP backend was found')

66
galicea_git/views/views.xml

@ -0,0 +1,66 @@
<odoo>
<record id="repository_view_form" model="ir.ui.view">
<field name="model">galicea_git.repository</field>
<field name="arch" type="xml">
<form>
<group>
<field name="state" invisible="1" />
<field name="name" />
<field name="system_name" groups="galicea_git.group_admin" />
<field name="collaborator_ids" widget="many2many_tags" options="{'no_create': True}" />
</group>
<group class="oe_read_only">
<label for="url" />
<span style="font-family: monospace">git clone <field name="url" nolabel="True" /></span>
<field name="local_directory" style="font-family: monospace" />
</group>
</form>
</field>
</record>
<record id="repository_view_tree" model="ir.ui.view">
<field name="model">galicea_git.repository</field>
<field name="arch" type="xml">
<tree>
<field name="state" invisible="1" />
<field name="name" />
<field name="system_name" groups="galicea_git.group_admin" />
</tree>
</field>
</record>
<act_window id="repository_action"
name="Git repositories"
res_model="galicea_git.repository" />
<record id="config_settings_view_form" model="ir.ui.view">
<field name="model">galicea_git.config.settings</field>
<field name="arch" type="xml">
<form string="Git hosting settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header>
<field name="git_http_backend_valid" invisible="1" />
<group>
<label for="git_http_backend" />
<span>
<field name="git_http_backend" nolabel="True" class="oe_inline" style="min-width:300px; margin-right:5px" />
<i class="fa fa-check" aria-hidden="true" style="color: green"
attrs="{'invisible': [('git_http_backend_valid', '=', False)]}" />
<i class="fa fa-times" aria-hidden="true" style="color: red"
attrs="{'invisible': [('git_http_backend_valid', '=', True)]}" />
</span>
</group>
</form>
</field>
</record>
<act_window id="config_settings_action" name="Settings" res_model="galicea_git.config.settings"
view_mode="form" target="inline" />
<menuitem name="Git hosting" id="root_menu" sequence="20" />
<menuitem name="Repositories" id="repo_menu" parent="galicea_git.root_menu" action="repository_action" sequence="1" />
<menuitem name="Settings" id="settings_menu" parent="galicea_git.root_menu" action="config_settings_action" sequence="99"
groups="galicea_git.group_admin" />
</odoo>
Loading…
Cancel
Save