diff --git a/actions/gitweb b/actions/gitweb new file mode 100755 index 000000000..b00926b93 --- /dev/null +++ b/actions/gitweb @@ -0,0 +1,225 @@ +#!/usr/bin/python3 +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Configuration helper for Gitweb. +""" + +import argparse +import configparser +import json +import os +import shutil +import subprocess + +from plinth import action_utils +from plinth.modules.gitweb.manifest import GIT_REPO_PATH + + +def parse_arguments(): + """Return parsed command line arguments as dictionary.""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') + + subparsers.add_parser( + 'setup', help='Perform post-installation operations for Gitweb') + + subparser = subparsers.add_parser('create-repo', + help='Create a new repository') + subparser.add_argument('--name', required=True, + help='Name of the repository') + subparser.add_argument('--description', required=True, + help='Description of the repository') + subparser.add_argument('--owner', required=True, + help='Repository’s owner name') + subparser.add_argument( + '--is-private', required=False, default=False, action='store_true', + help='Allow only authorized users to access this repository') + + subparser = subparsers.add_parser( + 'repo-info', help='Get information about the repository') + subparser.add_argument('--name', required=True, + help='Name of the repository') + + subparser = subparsers.add_parser('rename-repo', + help='Rename an repository') + subparser.add_argument('--oldname', required=True, + help='Old name of the repository') + subparser.add_argument('--newname', required=True, + help='New name of the repository') + + subparser = subparsers.add_parser('set-repo-description', + help='Set description of the repository') + subparser.add_argument('--name', required=True, + help='Name of the repository') + subparser.add_argument('--description', required=True, + help='Description of the repository') + + subparser = subparsers.add_parser('set-repo-owner', + help='Set repository\'s owner name') + subparser.add_argument('--name', required=True, + help='Name of the repository') + subparser.add_argument('--owner', required=True, + help='Repository’s owner name') + + subparser = subparsers.add_parser( + 'set-repo-access', help='Set repository as private or public') + subparser.add_argument('--name', required=True, + help='Name of the repository') + subparser.add_argument('--access', required=True, + choices=['public', 'private'], help='Access status') + + subparser = subparsers.add_parser('delete-repo', + help='Delete an existing repository') + subparser.add_argument('--name', required=True, + help='Name of the repository to remove') + + subparsers.required = True + return parser.parse_args() + + +def subcommand_setup(_): + """Disable default Apache2 Gitweb configuration""" + action_utils.webserver_disable('gitweb') + + +def _get_repo_description(repo): + """Set description of the repository.""" + description_file = os.path.join(GIT_REPO_PATH, repo + '.git', + 'description') + if os.path.exists(description_file): + with open(description_file, 'r') as file_handle: + description = file_handle.read() + else: + description = '' + + return description + + +def _set_repo_description(repo, description): + """Set description of the repository.""" + description_file = os.path.join(GIT_REPO_PATH, repo + '.git', + 'description') + with open(description_file, 'w') as file_handle: + file_handle.write(description) + + +def _get_repo_owner(repo): + """Set repository's owner name.""" + repo_config = os.path.join(GIT_REPO_PATH, repo + '.git', 'config') + config = configparser.ConfigParser() + config.read(repo_config) + try: + owner = config['gitweb']['owner'] + except KeyError: + owner = '' + + return owner + + +def _set_repo_owner(repo, owner): + """Set repository's owner name.""" + repo_config = os.path.join(GIT_REPO_PATH, repo + '.git', 'config') + config = configparser.ConfigParser() + config.read(repo_config) + if not config.has_section('gitweb'): + config.add_section('gitweb') + + config['gitweb']['owner'] = owner + with open(repo_config, 'w') as file_handle: + config.write(file_handle) + + +def _get_access_status(repo): + """Get repository's access status""" + private_file = os.path.join(GIT_REPO_PATH, repo + '.git', 'private') + if os.path.exists(private_file): + return 'private' + + return 'public' + + +def _set_access_status(repo, status): + """Set repository as private or public""" + private_file = os.path.join(GIT_REPO_PATH, repo + '.git', 'private') + if status == 'private': + open(private_file, 'a') + elif status == 'public': + if os.path.exists(private_file): + os.remove(private_file) + + +def subcommand_rename_repo(arguments): + """Rename a repository.""" + oldpath = os.path.join(GIT_REPO_PATH, arguments.oldname + '.git') + newpath = os.path.join(GIT_REPO_PATH, arguments.newname + '.git') + os.rename(oldpath, newpath) + + +def subcommand_set_repo_description(arguments): + """Set description of the repository.""" + _set_repo_description(arguments.name, arguments.description) + + +def subcommand_set_repo_owner(arguments): + """Set repository's owner name.""" + _set_repo_owner(arguments.name, arguments.owner) + + +def subcommand_set_repo_access(arguments): + """Set repository's access status.""" + _set_access_status(arguments.name, arguments.access) + + +def subcommand_repo_info(arguments): + """Get information about repository.""" + print( + json.dumps( + dict(name=arguments.name, description=_get_repo_description( + arguments.name), owner=_get_repo_owner(arguments.name), + access=_get_access_status(arguments.name)))) + + +def subcommand_create_repo(arguments): + """Create a new git repository.""" + os.chdir(GIT_REPO_PATH) + repo_name = arguments.name + '.git' + subprocess.check_call(['git', 'init', '--bare', repo_name]) + subprocess.check_call(['chown', '-R', 'www-data:www-data', repo_name]) + _set_repo_description(arguments.name, arguments.description) + _set_repo_owner(arguments.name, arguments.owner) + if arguments.is_private: + _set_access_status(arguments.name, 'private') + + +def subcommand_delete_repo(arguments): + """Delete a git repository.""" + repo_path = os.path.join(GIT_REPO_PATH, arguments.name + '.git') + shutil.rmtree(repo_path) + + +def main(): + """Parse arguments and perform all duties.""" + arguments = parse_arguments() + + subcommand = arguments.subcommand.replace('-', '_') + subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) + + +if __name__ == '__main__': + main() diff --git a/debian/copyright b/debian/copyright index d86e1fb3a..d0118625d 100644 --- a/debian/copyright +++ b/debian/copyright @@ -74,6 +74,12 @@ Copyright: 2012 William Theaker Comment: https://gitlab.com/fdroid/artwork/blob/master/fdroid-logo-2015/fdroid-logo.svg License: CC-BY-SA-3.0 or GPL-3+ +Files: static/themes/default/icons/gitweb.png + static/themes/default/icons/gitweb.svg +Copyright: Git +Comment: https://commons.wikimedia.org/wiki/File:Git_icon_2007.svg +License: GPL-2+ + Files: static/themes/default/icons/google-play.png Copyright: Chameleon Design (https://thenounproject.com/Chamedesign/) Comment: https://thenounproject.com/icon/887917/ diff --git a/plinth/modules/gitweb/__init__.py b/plinth/modules/gitweb/__init__.py new file mode 100644 index 000000000..82911bc07 --- /dev/null +++ b/plinth/modules/gitweb/__init__.py @@ -0,0 +1,262 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +FreedomBox app to configure Gitweb. +""" + +import json +import os + +from django.utils.translation import ugettext_lazy as _ + +from plinth import action_utils, actions +from plinth import app as app_module +from plinth import frontpage, menu +from plinth.modules.apache.components import Webserver +from plinth.modules.firewall.components import Firewall +from plinth.modules.users import register_group + +from .manifest import GIT_REPO_PATH, backup, clients # noqa, pylint: disable=unused-import + +clients = clients + +version = 1 + +managed_packages = ['gitweb', 'highlight'] + +name = _('Gitweb') + +short_description = _('Simple Git Hosting') + +description = [ + _('Git is a distributed version-control system for tracking changes in ' + 'source code during software development. Gitweb provides a web ' + 'interface to Git repositories. You can browse history and content of ' + 'source code, use search to find relevant commits and code. ' + 'You can also clone repositories and upload code changes with a ' + 'command-line Git client or with multiple available graphical clients. ' + 'And you can share your code with people around the world.'), + _('To learn more on how to use Git visit ' + 'Git tutorial.') +] + +group = ('git-access', _('Read-write access to Git repositories')) + +app = None + + +class GitwebApp(app_module.App): + """FreedomBox app for Gitweb.""" + + app_id = 'gitweb' + + def __init__(self): + """Create components for the app.""" + super().__init__() + + self.repos = [] + + menu_item = menu.Menu('menu-gitweb', name, short_description, 'gitweb', + 'gitweb:index', parent_url_name='apps') + self.add(menu_item) + + shortcut = frontpage.Shortcut( + 'shortcut-gitweb', name, short_description=short_description, + icon='gitweb', url='/gitweb/', clients=clients, + login_required=True, allowed_groups=[group[0]]) + self.add(shortcut) + + firewall = Firewall('firewall-gitweb', name, ports=['http', 'https'], + is_external=True) + self.add(firewall) + + webserver = Webserver('webserver-gitweb', 'gitweb-freedombox') + self.add(webserver) + + self.auth_webserver = GitwebWebserverAuth('webserver-gitweb-auth', + 'gitweb-freedombox-auth') + self.add(self.auth_webserver) + + def have_public_repos(self): + """If Gitweb have public repos.""" + return any((repo['access'] == 'public' for repo in self.repos)) + + def set_shortcut_login_required(self, login_required): + """Change the login_required property of shortcut.""" + shortcut = self.remove('shortcut-gitweb') + shortcut.login_required = login_required + self.add(shortcut) + + def update_repo_list(self): + """List all Git repositories and set Gitweb as public or private.""" + repos = [] + if os.path.exists(GIT_REPO_PATH): + for repo in os.listdir(GIT_REPO_PATH): + if not repo.endswith('.git'): + continue + + private_file = os.path.join(GIT_REPO_PATH, repo, 'private') + access = 'public' + if os.path.exists(private_file): + access = 'private' + + repos.append({'name': repo[:-4], 'access': access}) + + repos = sorted(repos, key=lambda repo: repo['name']) + + self.repos = repos + + if self.have_public_repos(): + self._enable_public_access() + else: + self._disable_public_access() + + def _enable_public_access(self): + """Allow Gitweb app to be accessed by anyone with access.""" + if self.auth_webserver.is_running(): + self.auth_webserver.disable() + + self.set_shortcut_login_required(False) + + def _disable_public_access(self): + """Allow Gitweb app to be accessed by logged-in users only.""" + if not self.auth_webserver.is_running(): + self.auth_webserver.enable() + + self.set_shortcut_login_required(True) + + +class GitwebWebserverAuth(Webserver): + """Component to handle Gitweb authentication webserver configuration.""" + + def is_running(self): + """Check whether Gitweb authentication webserver is running""" + return super().is_enabled() + + def is_enabled(self): + """Return if configuration is enabled or public access is enabled.""" + return app.have_public_repos or super().is_enabled() + + +def init(): + """Initialize the module.""" + global app + app = GitwebApp() + register_group(group) + + setup_helper = globals()['setup_helper'] + if setup_helper.get_state() != 'needs-setup' and app.is_enabled(): + app.update_repo_list() + app.set_enabled(True) + + +def setup(helper, old_version=None): + """Install and configure the module.""" + helper.install(managed_packages) + helper.call('post', actions.superuser_run, 'gitweb', ['setup']) + app.update_repo_list() + helper.call('post', app.enable) + + +def diagnose(): + """Run diagnostics and return the results.""" + results = [] + + results.extend( + action_utils.diagnose_url_on_all('https://{host}/gitweb/', + check_certificate=False)) + return results + + +def create_repo(repo, repo_description, owner, is_private): + """Create a new repository by calling the action script.""" + args = [ + 'create-repo', '--name', repo, '--description', repo_description, + '--owner', owner + ] + if is_private: + args.append('--is-private') + + actions.superuser_run('gitweb', args) + + +def repo_info(repo): + """Get information about repository.""" + output = actions.run('gitweb', ['repo-info', '--name', repo]) + info = json.loads(output) + if info['access'] == 'private': + info['is_private'] = True + else: + info['is_private'] = False + + return info + + +def _rename_repo(oldname, newname): + """Rename a repository.""" + args = ['rename-repo', '--oldname', oldname, '--newname', newname] + actions.superuser_run('gitweb', args) + + +def _set_repo_description(repo, repo_description): + """Set description of the repository.""" + args = [ + 'set-repo-description', + '--name', + repo, + '--description', + repo_description, + ] + actions.superuser_run('gitweb', args) + + +def _set_repo_owner(repo, owner): + """Set repository's owner name.""" + args = ['set-repo-owner', '--name', repo, '--owner', owner] + actions.superuser_run('gitweb', args) + + +def _set_repo_access(repo, access): + """Set repository's owner name.""" + args = ['set-repo-access', '--name', repo, '--access', access] + actions.superuser_run('gitweb', args) + + +def edit_repo(form_initial, form_cleaned): + """Edit repository data.""" + repo = form_initial['name'] + + if form_cleaned['name'] != repo: + _rename_repo(repo, form_cleaned['name']) + repo = form_cleaned['name'] + + if form_cleaned['description'] != form_initial['description']: + _set_repo_description(repo, form_cleaned['description']) + + if form_cleaned['owner'] != form_initial['owner']: + _set_repo_owner(repo, form_cleaned['owner']) + + if form_cleaned['is_private'] != form_initial['is_private']: + if form_cleaned['is_private']: + _set_repo_access(repo, 'private') + else: + _set_repo_access(repo, 'public') + + +def delete_repo(repo): + """Delete a repository.""" + actions.superuser_run('gitweb', ['delete-repo', '--name', repo]) diff --git a/plinth/modules/gitweb/data/etc/apache2/conf-available/gitweb-freedombox-auth.conf b/plinth/modules/gitweb/data/etc/apache2/conf-available/gitweb-freedombox-auth.conf new file mode 100644 index 000000000..9f5d0b62f --- /dev/null +++ b/plinth/modules/gitweb/data/etc/apache2/conf-available/gitweb-freedombox-auth.conf @@ -0,0 +1,11 @@ +## +## Limit access to gitweb web interface. Only users belonging to 'admin' or +## 'git-access' groups are allowed to view the web interface. This configuration +## is to be enabled when there is at least one public git project. +## + + Include includes/freedombox-single-sign-on.conf + + TKTAuthToken "git-access" "admin" + + diff --git a/plinth/modules/gitweb/data/etc/apache2/conf-available/gitweb-freedombox.conf b/plinth/modules/gitweb/data/etc/apache2/conf-available/gitweb-freedombox.conf new file mode 100644 index 000000000..e67340bcb --- /dev/null +++ b/plinth/modules/gitweb/data/etc/apache2/conf-available/gitweb-freedombox.conf @@ -0,0 +1,75 @@ +## +## On all sites, enable gitweb web interface. Also enable git-http-backend on +## when performing upload/receive operations on the URL. +## +## Requires the following Apache modules to be enabled: +## mod_cgi or mod_cgid +## mod_rewrite +## + +# Make gitweb work with custom FreedomBox configuration. +SetEnv GITWEB_CONFIG /etc/gitweb-freedombox.conf + +# Configure git-http-backend to work with our repository path. +SetEnv GIT_PROJECT_ROOT /var/lib/git + +# Tell git-http-backend to work with all the projects even when they don't have +# the file 'git-daemon-export-ok'. +SetEnv GIT_HTTP_EXPORT_ALL + +# All git operations are handled by git-http-backend CGI script. Rest of the +# HTTP requests (say sent by the browser) are handled by gitweb. +ScriptAliasMatch \ + "(?x)^/gitweb/(.*/(HEAD | \ + info/refs | \ + objects/(info/[^/]+ | \ + [0-9a-f]{2}/[0-9a-f]{38} | \ + pack/pack-[0-9a-f]{40}\.(pack|idx)) | \ + git-(upload|receive)-pack))$" \ + /usr/lib/git-core/git-http-backend/$1 + +Alias /gitweb /usr/share/gitweb + + + + Include includes/freedombox-single-sign-on.conf + + TKTAuthToken "git-access" "admin" + + + + # Allow index.cgi symlink to gitweb.cgi to work. Treat gitweb.cgi as CGI + # script and execute it. + Options +FollowSymLinks +ExecCGI + AddHandler cgi-script .cgi + + # Allow files in /usr/share/gitweb/static/ to be served directly by Apache. + # Pass every other URL as argument to gitweb.cgi to enable short and clean + # URLs. + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^.* /gitweb/gitweb.cgi/$0 [L,PT] + + + + # Authentication is required when performing git push (git send-pack). + SetEnvIfExpr "%{QUERY_STRING} =~ /service=git-receive-pack/" AUTHREQUIRED + SetEnvIfExpr "%{REQUEST_URI} =~ /git-receive-pack$/" AUTHREQUIRED + # Authentication is required for any operation if repository is private. + + SetEnvIfExpr true AUTHREQUIRED + + + # Either authentication is not required for this operation and repository + # combination, or... + + Require all granted + Require not env AUTHREQUIRED + + + # ...user belongs to admin or git-access groups, with basic auth via LDAP. + Include includes/freedombox-auth-ldap.conf + Require ldap-group cn=admin,ou=groups,dc=thisbox + Require ldap-group cn=git-access,ou=groups,dc=thisbox + diff --git a/plinth/modules/gitweb/data/etc/gitweb-freedombox.conf b/plinth/modules/gitweb/data/etc/gitweb-freedombox.conf new file mode 100644 index 000000000..108de9159 --- /dev/null +++ b/plinth/modules/gitweb/data/etc/gitweb-freedombox.conf @@ -0,0 +1,52 @@ +# path to git projects (.git) +$projectroot = "/var/lib/git"; + +# directory to use for temp files +$git_temp = "/tmp"; + +# target of the home link on top of all pages +#$home_link = $my_uri || "/"; + +# html text to include at home page +#$home_text = "indextext.html"; + +# file with project list; by default, simply scan the projectroot dir. +#$projects_list = $projectroot; + +# stylesheet to use +@stylesheets = ("/gitweb/static/gitweb.css"); + +# javascript code for gitweb +$javascript = "/gitweb/static/gitweb.js"; + +# logo to use +$logo = "/gitweb/static/git-logo.png"; + +# the 'favicon' +$favicon = "/gitweb/static/git-favicon.png"; + +# git-diff-tree(1) options to use for generated patches +#@diff_opts = ("-M"); +@diff_opts = (); + +# enable short urls +$feature{'pathinfo'}{'default'} = [1]; + +# enable git blame +$feature{'blame'}{'default'} = [1]; + +# enable pickaxe search +$feature{'pickaxe'}{'default'} = [1]; + +# enable syntax highlighting +$feature{'highlight'}{'default'} = [1]; + +# export private repos only if authorized +our $per_request_config = sub { + if(defined $ENV{'REMOTE_USER_TOKENS'}){ + our $export_auth_hook = sub { return 1; }; + } + else { + our $export_auth_hook = sub { return ! -e "$_[0]/private"; }; + } +}; diff --git a/plinth/modules/gitweb/data/etc/plinth/modules-enabled/gitweb b/plinth/modules/gitweb/data/etc/plinth/modules-enabled/gitweb new file mode 100644 index 000000000..640d8a836 --- /dev/null +++ b/plinth/modules/gitweb/data/etc/plinth/modules-enabled/gitweb @@ -0,0 +1 @@ +plinth.modules.gitweb diff --git a/plinth/modules/gitweb/forms.py b/plinth/modules/gitweb/forms.py new file mode 100644 index 000000000..902493e6e --- /dev/null +++ b/plinth/modules/gitweb/forms.py @@ -0,0 +1,70 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Django form for configuring Gitweb. +""" + +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ +from plinth.modules import gitweb + + +class EditRepoForm(forms.Form): + """Form to create and edit a new repository.""" + + name = forms.RegexField( + label=_('Name of the repository'), + strip=True, + regex=r'^[a-zA-Z0-9-._]+$', + help_text=_( + 'An alpha-numeric string that uniquely identifies a repository.'), + ) + + description = forms.CharField( + label=_('Description of the repository'), strip=True, required=False) + + owner = forms.CharField( + label=_('Repository\'s owner name'), strip=True, required=False) + + is_private = forms.BooleanField( + label=_('Private repository'), required=False, + help_text=_('Allow only authorized users to access this repository.')) + + def __init__(self, *args, **kwargs): + """Initialize the form with extra request argument.""" + super().__init__(*args, **kwargs) + self.fields['name'].widget.attrs.update({'autofocus': 'autofocus'}) + + def clean_name(self): + """Check if the name is valid.""" + name = self.cleaned_data['name'] + if 'name' in self.initial and name == self.initial['name']: + return name + + if name.endswith('.git'): + name = name[:-4] + + if (not name) or name.startswith(('-', '.')): + raise ValidationError(_('Invalid repository name.')) + + for repo in gitweb.app.repos: + if name == repo['name']: + raise ValidationError( + _('A repository with this name already exists.')) + + return name diff --git a/plinth/modules/gitweb/manifest.py b/plinth/modules/gitweb/manifest.py new file mode 100644 index 000000000..0743c3c1f --- /dev/null +++ b/plinth/modules/gitweb/manifest.py @@ -0,0 +1,64 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from django.utils.translation import ugettext_lazy as _ + +from plinth.clients import validate +from plinth.modules.backups.api import validate as validate_backup + +CONFIG_FILE = '/etc/gitweb-freedombox.conf' +GIT_REPO_PATH = '/var/lib/git' + +clients = validate([ + { + 'name': _('Gitweb'), + 'platforms': [{ + 'type': 'web', + 'url': '/gitweb/' + }] + }, + { + 'name': + _('Git'), + 'platforms': [ + { + 'type': 'download', + 'os': 'gnu-linux', + 'url': 'https://git-scm.com/download/linux' + }, + { + 'type': 'download', + 'os': 'macos', + 'url': 'https://git-scm.com/download/mac' + }, + { + 'type': 'download', + 'os': 'windows', + 'url': 'https://git-scm.com/download/mac' + }, + ] + }, +]) + +backup = validate_backup({ + 'config': { + 'files': [CONFIG_FILE] + }, + 'data': { + 'directories': [GIT_REPO_PATH] + } +}) diff --git a/plinth/modules/gitweb/templates/gitweb_configure.html b/plinth/modules/gitweb/templates/gitweb_configure.html new file mode 100644 index 000000000..489565699 --- /dev/null +++ b/plinth/modules/gitweb/templates/gitweb_configure.html @@ -0,0 +1,94 @@ +{% extends "app.html" %} +{% comment %} +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block page_head %} + +{% endblock %} + +{% block status %} + + + {% trans 'Create repository' %} + +{% endblock %} + +

{{ title }}

+ +{% block configuration %} +

{% trans "Configuration" %}

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ +

{% trans "Manage Repositories" %}

+ +
+
+ {% if not repos %} +

{% trans 'No repositories available.' %}

+ {% else %} + {% for repo in repos %} +
+ + + + + + + {% if repo.access == 'private' %} + + {% endif %} + + {{ repo.name }} + +
+ {% endfor %} + {% endif %} +
+
+ + +{% endblock %} diff --git a/plinth/modules/gitweb/templates/gitweb_create_edit.html b/plinth/modules/gitweb/templates/gitweb_create_edit.html new file mode 100644 index 000000000..b251e8ab3 --- /dev/null +++ b/plinth/modules/gitweb/templates/gitweb_create_edit.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} +{% load static %} + +{% block content %} + +

{{ title }}

+
+
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+
+{% endblock %} diff --git a/plinth/modules/gitweb/templates/gitweb_delete.html b/plinth/modules/gitweb/templates/gitweb_delete.html new file mode 100644 index 000000000..c6d138e8b --- /dev/null +++ b/plinth/modules/gitweb/templates/gitweb_delete.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% comment %} +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + +

+ {% blocktrans trimmed %} + Delete Git Repository {{ name }} + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + Delete this repository permanently? + {% endblocktrans %} +

+ +
+ {% csrf_token %} + + + + {% trans "Cancel" %} +
+ +{% endblock %} diff --git a/plinth/modules/gitweb/tests/__init__.py b/plinth/modules/gitweb/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plinth/modules/gitweb/urls.py b/plinth/modules/gitweb/urls.py new file mode 100644 index 000000000..7b1763038 --- /dev/null +++ b/plinth/modules/gitweb/urls.py @@ -0,0 +1,38 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +URLs for the Gitweb module. +""" + +from django.conf.urls import url + +from .views import CreateRepoView, EditRepoView, GitwebAppView, delete + +urlpatterns = [ + url(r'^apps/gitweb/$', GitwebAppView.as_view(), name='index'), + url(r'^apps/gitweb/create/$', CreateRepoView.as_view(), name='create'), + url( + r'^apps/gitweb/(?P[a-zA-Z0-9-._]+)/edit/$', + EditRepoView.as_view(), + name='edit', + ), + url( + r'^apps/gitweb/(?P[a-zA-Z0-9-._]+)/delete/$', + delete, + name='delete', + ), +] diff --git a/plinth/modules/gitweb/views.py b/plinth/modules/gitweb/views.py new file mode 100644 index 000000000..3e52b2f98 --- /dev/null +++ b/plinth/modules/gitweb/views.py @@ -0,0 +1,158 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Django views for Gitweb. +""" + +from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin +from django.http import Http404 +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.urls import reverse_lazy +from django.utils.translation import ugettext as _ +from django.views.generic import FormView + +from plinth import actions, views +from plinth.errors import ActionError +from plinth.modules import gitweb + +from .forms import EditRepoForm + + +class GitwebAppView(views.AppView): + """Serve configuration page.""" + + clients = gitweb.clients + name = gitweb.name + description = gitweb.description + diagnostics_module_name = 'gitweb' + app_id = 'gitweb' + show_status_block = False + template_name = 'gitweb_configure.html' + + def get_initial(self): + """Return the status of the service to fill in the form.""" + initial = super().get_initial() + gitweb.app.update_repo_list() + initial['repos'] = gitweb.app.repos + return initial + + +class CreateRepoView(SuccessMessageMixin, FormView): + """View to create a new repository.""" + + form_class = EditRepoForm + prefix = 'gitweb' + template_name = 'gitweb_create_edit.html' + success_url = reverse_lazy('gitweb:index') + success_message = _('Repository created.') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Create Repository') + return context + + def form_valid(self, form): + """Create the repository on valid form submission.""" + form_data = {} + for key, value in form.cleaned_data.items(): + if value is None: + form_data[key] = '' + else: + form_data[key] = value + + gitweb.create_repo(form_data['name'], form_data['description'], + form_data['owner'], form_data['is_private']) + return super().form_valid(form) + + +class EditRepoView(SuccessMessageMixin, FormView): + """View to edit an existing repository.""" + + form_class = EditRepoForm + prefix = 'gitweb' + template_name = 'gitweb_create_edit.html' + success_url = reverse_lazy('gitweb:index') + success_message = _('Repository edited.') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Edit repository') + return context + + def get_initial(self): + """Load information about repository being edited.""" + name = self.kwargs['name'] + for repo in gitweb.app.repos: + if repo['name'] == name: + break + else: + raise Http404 + + return gitweb.repo_info(name) + + def form_valid(self, form): + """Edit the repo on valid form submission.""" + if form.initial != form.cleaned_data: + form_data = {} + for key, value in form.cleaned_data.items(): + if value is None: + form_data[key] = '' + else: + form_data[key] = value + + try: + gitweb.edit_repo(form.initial, form_data) + except ActionError: + messages.error(self.request, + _('An error occurred during configuration.')) + + return super().form_valid(form) + + +def delete(request, name): + """Handle deleting repositories, showing a confirmation dialog first. + + On GET, display a confirmation page. + On POST, delete the repository. + """ + for repo in gitweb.app.repos: + if repo['name'] == name: + break + else: + raise Http404 + + if request.method == 'POST': + try: + gitweb.delete_repo(name) + messages.success(request, _('{name} deleted.').format(name=name)) + except actions.ActionError as error: + messages.error( + request, + _('Could not delete {name}: {error}').format( + name=name, error=error), + ) + + return redirect(reverse_lazy('gitweb:index')) + + return TemplateResponse(request, 'gitweb_delete.html', { + 'title': gitweb.name, + 'name': name + }) diff --git a/static/themes/default/icons/gitweb.png b/static/themes/default/icons/gitweb.png new file mode 100644 index 000000000..9d1a00d36 Binary files /dev/null and b/static/themes/default/icons/gitweb.png differ diff --git a/static/themes/default/icons/gitweb.svg b/static/themes/default/icons/gitweb.svg new file mode 100644 index 000000000..7b74e37ee --- /dev/null +++ b/static/themes/default/icons/gitweb.svg @@ -0,0 +1,73 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + +