mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-06-24 11:20:40 +00:00
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:
parent
ff3c6adac8
commit
1b9dea4033
225
actions/gitweb
Executable file
225
actions/gitweb
Executable 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='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()
|
||||
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -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/
|
||||
|
||||
262
plinth/modules/gitweb/__init__.py
Normal file
262
plinth/modules/gitweb/__init__.py
Normal 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])
|
||||
@ -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>
|
||||
@ -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>
|
||||
52
plinth/modules/gitweb/data/etc/gitweb-freedombox.conf
Normal file
52
plinth/modules/gitweb/data/etc/gitweb-freedombox.conf
Normal 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"; };
|
||||
}
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.gitweb
|
||||
70
plinth/modules/gitweb/forms.py
Normal file
70
plinth/modules/gitweb/forms.py
Normal 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
|
||||
64
plinth/modules/gitweb/manifest.py
Normal file
64
plinth/modules/gitweb/manifest.py
Normal 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]
|
||||
}
|
||||
})
|
||||
94
plinth/modules/gitweb/templates/gitweb_configure.html
Normal file
94
plinth/modules/gitweb/templates/gitweb_configure.html
Normal 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 %}
|
||||
38
plinth/modules/gitweb/templates/gitweb_create_edit.html
Normal file
38
plinth/modules/gitweb/templates/gitweb_create_edit.html
Normal 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 %}
|
||||
48
plinth/modules/gitweb/templates/gitweb_delete.html
Normal file
48
plinth/modules/gitweb/templates/gitweb_delete.html
Normal 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 %}
|
||||
0
plinth/modules/gitweb/tests/__init__.py
Normal file
0
plinth/modules/gitweb/tests/__init__.py
Normal file
38
plinth/modules/gitweb/urls.py
Normal file
38
plinth/modules/gitweb/urls.py
Normal 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',
|
||||
),
|
||||
]
|
||||
158
plinth/modules/gitweb/views.py
Normal file
158
plinth/modules/gitweb/views.py
Normal 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
|
||||
})
|
||||
BIN
static/themes/default/icons/gitweb.png
Normal file
BIN
static/themes/default/icons/gitweb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
73
static/themes/default/icons/gitweb.svg
Normal file
73
static/themes/default/icons/gitweb.svg
Normal 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 |
Loading…
x
Reference in New Issue
Block a user