Maciej Wawro
6 years ago
19 changed files with 530 additions and 0 deletions
-
1galicea_git/README.md
-
5galicea_git/__init__.py
-
48galicea_git/__manifest__.py
-
3galicea_git/controllers/__init__.py
-
83galicea_git/controllers/main.py
-
9galicea_git/data/config.xml
-
63galicea_git/http_chunked_fix.py
-
4galicea_git/models/__init__.py
-
42galicea_git/models/config_settings.py
-
100galicea_git/models/repository.py
-
3galicea_git/security/ir.model.access.csv
-
47galicea_git/security/security.xml
-
BINgalicea_git/static/description/icon.png
-
BINgalicea_git/static/description/images/config_screenshot.png
-
BINgalicea_git/static/description/images/console_screenshot.png
-
BINgalicea_git/static/description/images/create_screenshot.png
-
19galicea_git/static/description/index.html
-
37galicea_git/system_checks.py
-
66galicea_git/views/views.xml
@ -0,0 +1 @@ |
|||||
|
[See add-on page on odoo.com](https://apps.odoo.com/apps/modules/10.0/galicea_git/) |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
from . import controllers |
||||
|
from . import models |
||||
|
from . import system_checks |
@ -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" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
from . import main |
@ -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 |
||||
|
) |
@ -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> |
@ -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 |
@ -0,0 +1,4 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
from . import repository |
||||
|
from . import config_settings |
@ -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] |
@ -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]) |
@ -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 |
@ -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> |
After Width: 80 | Height: 80 | Size: 3.8 KiB |
After Width: 800 | Height: 354 | Size: 42 KiB |
After Width: 738 | Height: 359 | Size: 78 KiB |
After Width: 805 | Height: 273 | Size: 31 KiB |
@ -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> |
@ -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') |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue