gitweb: New app for simple git hosting

Closes: #1658

Signed-off-by: Veiko Aasa veiko17@disroot.org
[sunil@medhas.org Minor styling and cosmetic changes]
[sunil@medhas.org Write comments for Apache configurations]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Veiko Aasa 2019-10-10 06:53:08 +00:00 committed by Sunil Mohan Adapa
parent ff3c6adac8
commit 1b9dea4033
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
17 changed files with 1215 additions and 0 deletions

225
actions/gitweb Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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='Repositorys 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='Repositorys 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()

6
debian/copyright vendored
View File

@ -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/

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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 '
'<a href="https://git-scm.com/docs/gittutorial">Git tutorial</a>.')
]
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])

View File

@ -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.
##
<Directory /usr/share/gitweb>
Include includes/freedombox-single-sign-on.conf
<IfModule mod_auth_pubtkt.c>
TKTAuthToken "git-access" "admin"
</IfModule>
</Directory>

View File

@ -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
<Directory /usr/share/gitweb>
<If "%{HTTP_COOKIE} =~ /auth_pubtkt=.*tokens.*(admin|git-access)/">
Include includes/freedombox-single-sign-on.conf
<IfModule mod_auth_pubtkt.c>
TKTAuthToken "git-access" "admin"
</IfModule>
</If>
# 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]
</Directory>
<Directory /usr/lib/git-core/>
# 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.
<If "%{REQUEST_URI} =~ m#^/gitweb/([^/]+)# && -f '/var/lib/git/$1/private'">
SetEnvIfExpr true AUTHREQUIRED
</If>
# Either authentication is not required for this operation and repository
# combination, or...
<RequireAll>
Require all granted
Require not env AUTHREQUIRED
</RequireAll>
# ...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
</Directory>

View File

@ -0,0 +1,52 @@
# path to git projects (<project>.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"; };
}
};

View File

@ -0,0 +1 @@
plinth.modules.gitweb

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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]
}
})

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block page_head %}
<style type="text/css">
.repo-label {
display: inline-block;
width: 40%;
}
.repo-private-icon {
margin: 4px 10px;
}
.list-group-item .btn {
margin: -5px 2px;
}
</style>
{% endblock %}
{% block status %}
<a href="{% url 'gitweb:create' %}" class="btn btn-primary"
role="button" title="{% trans 'Create repository' %}">
<span class="fa fa-plus" aria-hidden="true"></span>
{% trans 'Create repository' %}
</a>
{% endblock %}
<h2>{{ title }}</h2>
{% block configuration %}
<h3>{% trans "Configuration" %}</h3>
<form class="form form-configuration" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Update setup" %}"/>
</form>
<h3>{% trans "Manage Repositories" %}</h3>
<div class="row">
<div class="col-sm-6">
{% if not repos %}
<p>{% trans 'No repositories available.' %}</p>
{% else %}
{% for repo in repos %}
<div class="list-group-item clearfix">
<a href="{% url 'gitweb:delete' repo.name %}"
class="btn btn-default btn-sm pull-right"
role="button"
title="{% blocktrans %}Delete repository {{ repo.name }}{% endblocktrans %}">
<span class="fa fa-trash-o" aria-hidden="true"></span>
</a>
<a class="repo-edit btn btn-sm btn-default pull-right"
href="{% url 'gitweb:edit' repo.name %}">
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
</a>
{% if repo.access == 'private' %}
<span class="repo-private-icon fa fa-lock pull-right" aria-label="private"></span></span>
{% endif %}
<a class="repo-label" href="/gitweb/{{ repo.name }}.git"
title="{% blocktrans %}Go to repository {{ repo.name }}{% endblocktrans %}">
{{ repo.name }}
</a>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block content %}
<h3>{{ title }}</h3>
<div class="col-sm-6">
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Submit" %}"/>
</form>
</div>
{% endblock %}

View File

@ -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 <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h3>
{% blocktrans trimmed %}
Delete Git Repository <em>{{ name }}</em>
{% endblocktrans %}
</h3>
<p>
{% blocktrans trimmed %}
Delete this repository permanently?
{% endblocktrans %}
</p>
<form class="form" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-md btn-danger"
value="{% blocktrans %}Delete {{ name }}{% endblocktrans %}"/>
<a href="{% url 'gitweb:index' %}" role="button"
class="btn btn-md btn-primary">{% trans "Cancel" %}</a>
</form>
{% endblock %}

View File

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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<name>[a-zA-Z0-9-._]+)/edit/$',
EditRepoView.as_view(),
name='edit',
),
url(
r'^apps/gitweb/(?P<name>[a-zA-Z0-9-._]+)/delete/$',
delete,
name='delete',
),
]

View File

@ -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 <http://www.gnu.org/licenses/>.
#
"""
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
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
width="512"
height="512"
viewBox="0 0 512 512"
sodipodi:docname="gitweb.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6" />
<sodipodi:namedview
pagecolor="#575757"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview4"
showgrid="true"
inkscape:zoom="2"
inkscape:cx="195.3288"
inkscape:cy="254.07096"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
showguides="true">
<inkscape:grid
type="xygrid"
id="grid7767" />
</sodipodi:namedview>
<g
id="g7765"
transform="matrix(1.7663401,0,0,1.7663401,-4.5673475,9.5668645)">
<path
sodipodi:nodetypes="cccccccccccccccccccscscccsc"
style="display:inline;fill:#bf0303;stroke-width:0.31999999"
d="M 11.7124,83.16759 V 70.847587 h 36.96 36.96 v 12.320003 12.32 h -36.96 -36.96 z m 98.88,0 V 70.847587 h 36.95999 36.96 v 12.320003 12.32 h -36.96 -36.95999 z m 98.75423,11.81382 c -0.10683,-0.27839 -0.19424,-5.59461 -0.19424,-11.81382 0,-6.219213 0.0874,-11.535437 0.19424,-11.81383 0.17556,-0.457517 3.74373,-0.506173 37.12,-0.506173 h 36.92576 v 12.320003 12.32 h -36.92576 c -33.37627,0 -36.94444,-0.0487 -37.12,-0.50618 z"
id="path7107"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:#007f00;stroke-width:0.31999999"
d="m 36.52795,207.93332 c -0.30094,-0.19113 -0.41625,-3.16879 -0.48,-12.3951 l -0.0839,-12.14353 -12.16,-0.16 -12.16,-0.16 v -12.32 -12.32 l 12.16,-0.16 12.16,-0.16 0.16,-12.16 0.16,-12.16 h 12.16 12.16 l 0.16,12.16 0.16,12.16 12.16,0.16 12.16,0.16 v 12.32 12.32 l -12.16,0.16 -12.16,0.16 -0.0839,12.14353 c -0.0637,9.22631 -0.17906,12.20397 -0.48,12.3951 -0.21785,0.13837 -5.58009,0.25157 -11.91609,0.25157 -6.336,0 -11.69824,-0.1132 -11.91609,-0.25157 z m 98.88,0 c -0.30094,-0.19113 -0.41625,-3.16879 -0.48,-12.3951 l -0.0839,-12.14353 -12.16,-0.16 -12.16,-0.16 v -12.32 -12.32 l 12.16,-0.16 12.16,-0.16 0.16,-12.16 0.16,-12.16 h 12.15999 12.16 l 0.16,12.16 0.16,12.16 12.16,0.16 12.16,0.16 v 12.32 12.32 l -12.16,0.16 -12.16,0.16 -0.0839,12.14353 c -0.0637,9.22631 -0.17906,12.20397 -0.48,12.3951 -0.21785,0.13837 -5.58009,0.25157 -11.91609,0.25157 -6.33599,0 -11.69823,-0.1132 -11.91608,-0.25157 z m 98.87999,0 c -0.30094,-0.19113 -0.41625,-3.16879 -0.48,-12.3951 l -0.0839,-12.14353 -12.16,-0.16 -12.16,-0.16 v -12.32 -12.32 l 12.16,-0.16 12.16,-0.16 0.0838,-12.24379 0.0838,-12.2438 12.2362,0.0838 12.23619,0.0838 0.16,12.16 0.16,12.16 12.16,0.16966 c 6.688,0.0933 12.19771,0.2013 12.2438,0.24 0.0461,0.0387 0.0461,5.57834 0,12.31034 l -0.0838,12.24 -12.16,0.16 -12.16,0.16 -0.0839,12.14353 c -0.0638,9.22631 -0.17906,12.20397 -0.48,12.3951 -0.21785,0.13837 -5.58009,0.25157 -11.91609,0.25157 -6.336,0 -11.69824,-0.1132 -11.91609,-0.25157 z"
id="path7105"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB