mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +00:00
gitweb: Allow to import from a remote repository
- Allow to enter either name or URL when creating repository - Validate a repository URL, allow only http and https schemes - Set autocomplete off on the name/URL field because URL may contain username:password - Check whether the repository actually exists before cloning - Show progress info while cloning - Actions script: new subcommand check-repo-exists and new arguments for the create-repo: --url, --prepare-only and --skip-prepare - Add test for invalid URLs Closes #1670 Signed-off-by: Veiko Aasa <veiko17@disroot.org> [sunil: Fix validating repo name in edit form] [sunil: Don't pipe stdin of clone process, it may lead to a hang] [sunil: Always run clone process with 'C' locale since we are parsing output] [sunil: Cosmetic changes] Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
ca0c991562
commit
c99b33b40e
249
actions/gitweb
249
actions/gitweb
@ -22,14 +22,37 @@ Configuration helper for Gitweb.
|
|||||||
import argparse
|
import argparse
|
||||||
import configparser
|
import configparser
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils
|
||||||
from plinth.modules.gitweb.forms import validate_repository
|
from plinth.modules.gitweb.forms import RepositoryValidator, get_name_from_url
|
||||||
from plinth.modules.gitweb.manifest import GIT_REPO_PATH
|
from plinth.modules.gitweb.manifest import GIT_REPO_PATH
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateRepoName(argparse.Action):
|
||||||
|
"""Validate a repository name and add .git extension if necessary."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
RepositoryValidator()(values)
|
||||||
|
if not values.endswith('.git'):
|
||||||
|
values = values + '.git'
|
||||||
|
setattr(namespace, self.dest, values)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateRepoUrl(argparse.Action):
|
||||||
|
"""Validate a repository URL."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
RepositoryValidator(input_should_be='url')(values)
|
||||||
|
setattr(namespace, self.dest, values)
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""Return parsed command line arguments as dictionary."""
|
"""Return parsed command line arguments as dictionary."""
|
||||||
@ -41,8 +64,11 @@ def parse_arguments():
|
|||||||
|
|
||||||
subparser = subparsers.add_parser('create-repo',
|
subparser = subparsers.add_parser('create-repo',
|
||||||
help='Create a new repository')
|
help='Create a new repository')
|
||||||
subparser.add_argument('--name', required=True,
|
group = subparser.add_mutually_exclusive_group(required=True)
|
||||||
help='Name of the repository')
|
group.add_argument('--name', action=ValidateRepoName,
|
||||||
|
help='Name of the repository')
|
||||||
|
group.add_argument('--url', action=ValidateRepoUrl,
|
||||||
|
help='URL of the remote repository')
|
||||||
subparser.add_argument('--description', required=True,
|
subparser.add_argument('--description', required=True,
|
||||||
help='Description of the repository')
|
help='Description of the repository')
|
||||||
subparser.add_argument('--owner', required=True,
|
subparser.add_argument('--owner', required=True,
|
||||||
@ -53,58 +79,195 @@ def parse_arguments():
|
|||||||
subparser.add_argument(
|
subparser.add_argument(
|
||||||
'--keep-ownership', required=False, default=False, action="store_true",
|
'--keep-ownership', required=False, default=False, action="store_true",
|
||||||
help='Do not chanege ownership of the repository directory')
|
help='Do not chanege ownership of the repository directory')
|
||||||
|
subparser.add_argument('--prepare-only', required=False, default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Run preparation tasks for cloning.')
|
||||||
|
subparser.add_argument('--skip-prepare', required=False, default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Skip preparation tasks for cloning.')
|
||||||
|
|
||||||
subparser = subparsers.add_parser(
|
subparser = subparsers.add_parser(
|
||||||
'repo-info', help='Get information about the repository')
|
'repo-info', help='Get information about the repository')
|
||||||
subparser.add_argument('--name', required=True,
|
subparser.add_argument('--name', required=True, action=ValidateRepoName,
|
||||||
help='Name of the repository')
|
help='Name of the repository')
|
||||||
|
|
||||||
|
subparser = subparsers.add_parser(
|
||||||
|
'check-repo-exists', help='Check whether the remote repository exists')
|
||||||
|
subparser.add_argument('--url', required=True, action=ValidateRepoUrl,
|
||||||
|
help='URL of the remote repository')
|
||||||
|
|
||||||
subparser = subparsers.add_parser('rename-repo',
|
subparser = subparsers.add_parser('rename-repo',
|
||||||
help='Rename an repository')
|
help='Rename an repository')
|
||||||
subparser.add_argument('--oldname', required=True,
|
subparser.add_argument('--oldname', required=True, action=ValidateRepoName,
|
||||||
help='Old name of the repository')
|
help='Old name of the repository')
|
||||||
subparser.add_argument('--newname', required=True,
|
subparser.add_argument('--newname', required=True, action=ValidateRepoName,
|
||||||
help='New name of the repository')
|
help='New name of the repository')
|
||||||
|
|
||||||
subparser = subparsers.add_parser('set-repo-description',
|
subparser = subparsers.add_parser('set-repo-description',
|
||||||
help='Set description of the repository')
|
help='Set description of the repository')
|
||||||
subparser.add_argument('--name', required=True,
|
subparser.add_argument('--name', required=True, action=ValidateRepoName,
|
||||||
help='Name of the repository')
|
help='Name of the repository')
|
||||||
subparser.add_argument('--description', required=True,
|
subparser.add_argument('--description', required=True,
|
||||||
help='Description of the repository')
|
help='Description of the repository')
|
||||||
|
|
||||||
subparser = subparsers.add_parser('set-repo-owner',
|
subparser = subparsers.add_parser('set-repo-owner',
|
||||||
help='Set repository\'s owner name')
|
help='Set repository\'s owner name')
|
||||||
subparser.add_argument('--name', required=True,
|
subparser.add_argument('--name', required=True, action=ValidateRepoName,
|
||||||
help='Name of the repository')
|
help='Name of the repository')
|
||||||
subparser.add_argument('--owner', required=True,
|
subparser.add_argument('--owner', required=True,
|
||||||
help='Repository’s owner name')
|
help='Repository’s owner name')
|
||||||
|
|
||||||
subparser = subparsers.add_parser(
|
subparser = subparsers.add_parser(
|
||||||
'set-repo-access', help='Set repository as private or public')
|
'set-repo-access', help='Set repository as private or public')
|
||||||
subparser.add_argument('--name', required=True,
|
subparser.add_argument('--name', required=True, action=ValidateRepoName,
|
||||||
help='Name of the repository')
|
help='Name of the repository')
|
||||||
subparser.add_argument('--access', required=True,
|
subparser.add_argument('--access', required=True,
|
||||||
choices=['public', 'private'], help='Access status')
|
choices=['public', 'private'], help='Access status')
|
||||||
|
|
||||||
subparser = subparsers.add_parser('delete-repo',
|
subparser = subparsers.add_parser('delete-repo',
|
||||||
help='Delete an existing repository')
|
help='Delete an existing repository')
|
||||||
subparser.add_argument('--name', required=True,
|
subparser.add_argument('--name', required=True, action=ValidateRepoName,
|
||||||
help='Name of the repository to remove')
|
help='Name of the repository to remove')
|
||||||
|
|
||||||
subparsers.required = True
|
subparsers.required = True
|
||||||
return parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.subcommand == 'create-repo' and args.name:
|
||||||
|
if args.prepare_only:
|
||||||
|
parser.error('--prepare-only can be set when using --url')
|
||||||
|
|
||||||
|
if args.skip_prepare:
|
||||||
|
parser.error('--skip-prepare can be set when using --url')
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
def subcommand_setup(_):
|
def subcommand_setup(_):
|
||||||
"""Disable default Apache2 Gitweb configuration"""
|
"""Disable default Apache2 Gitweb configuration."""
|
||||||
action_utils.webserver_disable('gitweb')
|
action_utils.webserver_disable('gitweb')
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_with_progress_report(url, repo_dir):
|
||||||
|
"""Clone a repository and write progress info to the file."""
|
||||||
|
starttime = time.time()
|
||||||
|
status_file = os.path.join(repo_dir, 'clone_progress')
|
||||||
|
repo_temp_dir = os.path.join(repo_dir, '.temp')
|
||||||
|
# do not ask for credidentials and set low speed timeout
|
||||||
|
env = dict(os.environ, GIT_TERMINAL_PROMPT='0', LC_ALL='C',
|
||||||
|
GIT_HTTP_LOW_SPEED_LIMIT='100', GIT_HTTP_LOW_SPEED_TIME='60')
|
||||||
|
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
['git', 'clone', '--bare', '--progress', url, repo_temp_dir],
|
||||||
|
stderr=subprocess.PIPE, text=True, env=env)
|
||||||
|
|
||||||
|
# write clone progress to the file
|
||||||
|
errors = []
|
||||||
|
while True:
|
||||||
|
line = proc.stderr.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
if 'error:' in line or 'fatal:' in line:
|
||||||
|
errors.append(line.strip())
|
||||||
|
|
||||||
|
currenttime = time.time()
|
||||||
|
if currenttime - starttime > 1:
|
||||||
|
elapsed = _clone_status_line_to_percent(line)
|
||||||
|
if elapsed is not None:
|
||||||
|
with open(status_file, 'w') as file_handle:
|
||||||
|
file_handle.write(elapsed)
|
||||||
|
|
||||||
|
starttime = currenttime
|
||||||
|
|
||||||
|
# make sure process is ended
|
||||||
|
try:
|
||||||
|
proc.communicate(timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
|
||||||
|
os.remove(status_file)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
shutil.rmtree(repo_dir)
|
||||||
|
logger.error('Git repository cloning failed: %s', '\n'.join(errors))
|
||||||
|
raise RuntimeError('Git repository cloning failed.', errors)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_clone_repo(arguments):
|
||||||
|
"""Prepare cloning a repository."""
|
||||||
|
repo_name = get_name_from_url(arguments.url)
|
||||||
|
if not repo_name.endswith('.git'):
|
||||||
|
repo_name = repo_name + '.git'
|
||||||
|
|
||||||
|
repo_dir = os.path.join(GIT_REPO_PATH, repo_name)
|
||||||
|
os.mkdir(repo_dir)
|
||||||
|
|
||||||
|
if arguments.is_private:
|
||||||
|
_set_access_status(repo_name, 'private')
|
||||||
|
|
||||||
|
status_file = os.path.join(repo_dir, 'clone_progress')
|
||||||
|
with open(status_file, 'w') as file_handle:
|
||||||
|
file_handle.write('0')
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_status_line_to_percent(line):
|
||||||
|
"""Parse Git clone command output."""
|
||||||
|
result = re.match(r'.* ([0-9]+)% ', line)
|
||||||
|
if result is not None:
|
||||||
|
text = result.group(0)
|
||||||
|
progress = int(result.group(1))
|
||||||
|
if 'Counting objects' in text:
|
||||||
|
total_progress = 0.05 * progress
|
||||||
|
elif 'Compressing objects' in text:
|
||||||
|
total_progress = 5 + 0.05 * progress
|
||||||
|
elif 'Receiving objects' in text:
|
||||||
|
total_progress = 10 + 0.6 * progress
|
||||||
|
elif 'Resolving deltas' in text:
|
||||||
|
total_progress = 70 + 0.3 * progress
|
||||||
|
|
||||||
|
return str(int(total_progress))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_repo(arguments):
|
||||||
|
"""Clone a repository."""
|
||||||
|
url = arguments.url
|
||||||
|
repo = get_name_from_url(url)
|
||||||
|
if not repo.endswith('.git'):
|
||||||
|
repo = repo + '.git'
|
||||||
|
|
||||||
|
repo_path = os.path.join(GIT_REPO_PATH, repo)
|
||||||
|
repo_temp_path = os.path.join(repo_path, '.temp')
|
||||||
|
|
||||||
|
_clone_with_progress_report(url, repo_path)
|
||||||
|
|
||||||
|
for item in os.listdir(repo_temp_path):
|
||||||
|
shutil.move(os.path.join(repo_temp_path, item), repo_path)
|
||||||
|
|
||||||
|
shutil.rmtree(repo_temp_path)
|
||||||
|
if not arguments.keep_ownership:
|
||||||
|
subprocess.check_call(['chown', '-R', 'www-data:www-data', repo],
|
||||||
|
cwd=GIT_REPO_PATH)
|
||||||
|
|
||||||
|
_set_repo_description(repo, arguments.description)
|
||||||
|
_set_repo_owner(repo, arguments.owner)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_repo(arguments):
|
||||||
|
"""Create an empty repository."""
|
||||||
|
repo = arguments.name
|
||||||
|
subprocess.check_call(['git', 'init', '--bare', repo], cwd=GIT_REPO_PATH)
|
||||||
|
if not arguments.keep_ownership:
|
||||||
|
subprocess.check_call(['chown', '-R', 'www-data:www-data', repo],
|
||||||
|
cwd=GIT_REPO_PATH)
|
||||||
|
_set_repo_description(repo, arguments.description)
|
||||||
|
_set_repo_owner(repo, arguments.owner)
|
||||||
|
if arguments.is_private:
|
||||||
|
_set_access_status(repo, 'private')
|
||||||
|
|
||||||
|
|
||||||
def _get_repo_description(repo):
|
def _get_repo_description(repo):
|
||||||
"""Set description of the repository."""
|
"""Set description of the repository."""
|
||||||
description_file = os.path.join(GIT_REPO_PATH, repo + '.git',
|
description_file = os.path.join(GIT_REPO_PATH, repo, 'description')
|
||||||
'description')
|
|
||||||
if os.path.exists(description_file):
|
if os.path.exists(description_file):
|
||||||
with open(description_file, 'r') as file_handle:
|
with open(description_file, 'r') as file_handle:
|
||||||
description = file_handle.read()
|
description = file_handle.read()
|
||||||
@ -116,15 +279,14 @@ def _get_repo_description(repo):
|
|||||||
|
|
||||||
def _set_repo_description(repo, description):
|
def _set_repo_description(repo, description):
|
||||||
"""Set description of the repository."""
|
"""Set description of the repository."""
|
||||||
description_file = os.path.join(GIT_REPO_PATH, repo + '.git',
|
description_file = os.path.join(GIT_REPO_PATH, repo, 'description')
|
||||||
'description')
|
|
||||||
with open(description_file, 'w') as file_handle:
|
with open(description_file, 'w') as file_handle:
|
||||||
file_handle.write(description)
|
file_handle.write(description)
|
||||||
|
|
||||||
|
|
||||||
def _get_repo_owner(repo):
|
def _get_repo_owner(repo):
|
||||||
"""Set repository's owner name."""
|
"""Set repository's owner name."""
|
||||||
repo_config = os.path.join(GIT_REPO_PATH, repo + '.git', 'config')
|
repo_config = os.path.join(GIT_REPO_PATH, repo, 'config')
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(repo_config)
|
config.read(repo_config)
|
||||||
try:
|
try:
|
||||||
@ -137,7 +299,7 @@ def _get_repo_owner(repo):
|
|||||||
|
|
||||||
def _set_repo_owner(repo, owner):
|
def _set_repo_owner(repo, owner):
|
||||||
"""Set repository's owner name."""
|
"""Set repository's owner name."""
|
||||||
repo_config = os.path.join(GIT_REPO_PATH, repo + '.git', 'config')
|
repo_config = os.path.join(GIT_REPO_PATH, repo, 'config')
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(repo_config)
|
config.read(repo_config)
|
||||||
if not config.has_section('gitweb'):
|
if not config.has_section('gitweb'):
|
||||||
@ -149,8 +311,8 @@ def _set_repo_owner(repo, owner):
|
|||||||
|
|
||||||
|
|
||||||
def _get_access_status(repo):
|
def _get_access_status(repo):
|
||||||
"""Get repository's access status"""
|
"""Get repository's access status."""
|
||||||
private_file = os.path.join(GIT_REPO_PATH, repo + '.git', 'private')
|
private_file = os.path.join(GIT_REPO_PATH, repo, 'private')
|
||||||
if os.path.exists(private_file):
|
if os.path.exists(private_file):
|
||||||
return 'private'
|
return 'private'
|
||||||
|
|
||||||
@ -159,7 +321,7 @@ def _get_access_status(repo):
|
|||||||
|
|
||||||
def _set_access_status(repo, status):
|
def _set_access_status(repo, status):
|
||||||
"""Set repository as private or public"""
|
"""Set repository as private or public"""
|
||||||
private_file = os.path.join(GIT_REPO_PATH, repo + '.git', 'private')
|
private_file = os.path.join(GIT_REPO_PATH, repo, 'private')
|
||||||
if status == 'private':
|
if status == 'private':
|
||||||
open(private_file, 'a')
|
open(private_file, 'a')
|
||||||
elif status == 'public':
|
elif status == 'public':
|
||||||
@ -169,64 +331,61 @@ def _set_access_status(repo, status):
|
|||||||
|
|
||||||
def subcommand_rename_repo(arguments):
|
def subcommand_rename_repo(arguments):
|
||||||
"""Rename a repository."""
|
"""Rename a repository."""
|
||||||
validate_repository(arguments.oldname)
|
oldpath = os.path.join(GIT_REPO_PATH, arguments.oldname)
|
||||||
validate_repository(arguments.newname)
|
newpath = os.path.join(GIT_REPO_PATH, arguments.newname)
|
||||||
oldpath = os.path.join(GIT_REPO_PATH, arguments.oldname + '.git')
|
|
||||||
newpath = os.path.join(GIT_REPO_PATH, arguments.newname + '.git')
|
|
||||||
os.rename(oldpath, newpath)
|
os.rename(oldpath, newpath)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_set_repo_description(arguments):
|
def subcommand_set_repo_description(arguments):
|
||||||
"""Set description of the repository."""
|
"""Set description of the repository."""
|
||||||
validate_repository(arguments.name)
|
|
||||||
_set_repo_description(arguments.name, arguments.description)
|
_set_repo_description(arguments.name, arguments.description)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_set_repo_owner(arguments):
|
def subcommand_set_repo_owner(arguments):
|
||||||
"""Set repository's owner name."""
|
"""Set repository's owner name."""
|
||||||
validate_repository(arguments.name)
|
|
||||||
_set_repo_owner(arguments.name, arguments.owner)
|
_set_repo_owner(arguments.name, arguments.owner)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_set_repo_access(arguments):
|
def subcommand_set_repo_access(arguments):
|
||||||
"""Set repository's access status."""
|
"""Set repository's access status."""
|
||||||
validate_repository(arguments.name)
|
|
||||||
_set_access_status(arguments.name, arguments.access)
|
_set_access_status(arguments.name, arguments.access)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_repo_info(arguments):
|
def subcommand_repo_info(arguments):
|
||||||
"""Get information about repository."""
|
"""Get information about repository."""
|
||||||
validate_repository(arguments.name)
|
repo_path = os.path.join(GIT_REPO_PATH, arguments.name)
|
||||||
repo_path = os.path.join(GIT_REPO_PATH, arguments.name + '.git')
|
|
||||||
if not os.path.exists(repo_path):
|
if not os.path.exists(repo_path):
|
||||||
raise RuntimeError('Repository not found')
|
raise RuntimeError('Repository not found')
|
||||||
|
|
||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
dict(name=arguments.name, description=_get_repo_description(
|
dict(name=arguments.name[:-4], description=_get_repo_description(
|
||||||
arguments.name), owner=_get_repo_owner(arguments.name),
|
arguments.name), owner=_get_repo_owner(arguments.name),
|
||||||
access=_get_access_status(arguments.name))))
|
access=_get_access_status(arguments.name))))
|
||||||
|
|
||||||
|
|
||||||
def subcommand_create_repo(arguments):
|
def subcommand_create_repo(arguments):
|
||||||
"""Create a new git repository."""
|
"""Create a new or clone a remote repository."""
|
||||||
validate_repository(arguments.name)
|
if arguments.url:
|
||||||
repo_name = arguments.name + '.git'
|
if not arguments.skip_prepare:
|
||||||
subprocess.check_call(['git', 'init', '--bare', repo_name],
|
_prepare_clone_repo(arguments)
|
||||||
cwd=GIT_REPO_PATH)
|
|
||||||
if not arguments.keep_ownership:
|
if not arguments.prepare_only:
|
||||||
subprocess.check_call(['chown', '-R', 'www-data:www-data', repo_name],
|
_clone_repo(arguments)
|
||||||
cwd=GIT_REPO_PATH)
|
else:
|
||||||
_set_repo_description(arguments.name, arguments.description)
|
_create_repo(arguments)
|
||||||
_set_repo_owner(arguments.name, arguments.owner)
|
|
||||||
if arguments.is_private:
|
|
||||||
_set_access_status(arguments.name, 'private')
|
def subcommand_check_repo_exists(arguments):
|
||||||
|
"""Check whether remote repository exists."""
|
||||||
|
env = dict(os.environ, GIT_TERMINAL_PROMPT='0')
|
||||||
|
subprocess.check_call(['git', 'ls-remote', arguments.url, 'HEAD'],
|
||||||
|
timeout=10, env=env)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_delete_repo(arguments):
|
def subcommand_delete_repo(arguments):
|
||||||
"""Delete a git repository."""
|
"""Delete a git repository."""
|
||||||
validate_repository(arguments.name)
|
repo_path = os.path.join(GIT_REPO_PATH, arguments.name)
|
||||||
repo_path = os.path.join(GIT_REPO_PATH, arguments.name + '.git')
|
|
||||||
shutil.rmtree(repo_path)
|
shutil.rmtree(repo_path)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -26,10 +26,12 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from plinth import action_utils, actions
|
from plinth import action_utils, actions
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
from plinth import frontpage, menu
|
from plinth import frontpage, menu
|
||||||
|
from plinth.errors import ActionError
|
||||||
from plinth.modules.apache.components import Webserver
|
from plinth.modules.apache.components import Webserver
|
||||||
from plinth.modules.firewall.components import Firewall
|
from plinth.modules.firewall.components import Firewall
|
||||||
from plinth.modules.users import register_group
|
from plinth.modules.users import register_group
|
||||||
|
|
||||||
|
from .forms import is_repo_url
|
||||||
from .manifest import GIT_REPO_PATH, backup, clients # noqa, pylint: disable=unused-import
|
from .manifest import GIT_REPO_PATH, backup, clients # noqa, pylint: disable=unused-import
|
||||||
|
|
||||||
clients = clients
|
clients = clients
|
||||||
@ -102,13 +104,26 @@ class GitwebApp(app_module.App):
|
|||||||
repos = []
|
repos = []
|
||||||
if os.path.exists(GIT_REPO_PATH):
|
if os.path.exists(GIT_REPO_PATH):
|
||||||
for repo in os.listdir(GIT_REPO_PATH):
|
for repo in os.listdir(GIT_REPO_PATH):
|
||||||
if not repo.endswith('.git'):
|
if not repo.endswith('.git') or repo.startswith('.'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
repo_info = {}
|
||||||
|
repo_info['name'] = repo[:-4]
|
||||||
|
|
||||||
private_file = os.path.join(GIT_REPO_PATH, repo, 'private')
|
private_file = os.path.join(GIT_REPO_PATH, repo, 'private')
|
||||||
access = 'public'
|
|
||||||
if os.path.exists(private_file):
|
if os.path.exists(private_file):
|
||||||
access = 'private'
|
repo_info['access'] = 'private'
|
||||||
repos.append({'name': repo[:-4], 'access': access})
|
else:
|
||||||
|
repo_info['access'] = 'public'
|
||||||
|
|
||||||
|
progress_file = os.path.join(GIT_REPO_PATH, repo,
|
||||||
|
'clone_progress')
|
||||||
|
if os.path.exists(progress_file):
|
||||||
|
with open(progress_file) as file_handle:
|
||||||
|
clone_progress = file_handle.read()
|
||||||
|
repo_info['clone_progress'] = clone_progress
|
||||||
|
|
||||||
|
repos.append(repo_info)
|
||||||
|
|
||||||
return sorted(repos, key=lambda repo: repo['name'])
|
return sorted(repos, key=lambda repo: repo['name'])
|
||||||
|
|
||||||
@ -189,21 +204,36 @@ def restore_post(packet):
|
|||||||
app.update_service_access()
|
app.update_service_access()
|
||||||
|
|
||||||
|
|
||||||
|
def repo_exists(name):
|
||||||
|
"""Check whether a remote repository exists."""
|
||||||
|
try:
|
||||||
|
actions.run('gitweb', ['check-repo-exists', '--url', name])
|
||||||
|
except ActionError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def have_public_repos(repos):
|
def have_public_repos(repos):
|
||||||
"""Check for public repositories"""
|
"""Check for public repositories"""
|
||||||
return any((repo['access'] == 'public' for repo in repos))
|
return any((repo['access'] == 'public' for repo in repos))
|
||||||
|
|
||||||
|
|
||||||
def create_repo(repo, repo_description, owner, is_private):
|
def create_repo(repo, repo_description, owner, is_private):
|
||||||
"""Create a new repository by calling the action script."""
|
"""Create a new repository or clone a remote repository."""
|
||||||
args = [
|
args = ['--description', repo_description, '--owner', owner]
|
||||||
'create-repo', '--name', repo, '--description', repo_description,
|
|
||||||
'--owner', owner
|
|
||||||
]
|
|
||||||
if is_private:
|
if is_private:
|
||||||
args.append('--is-private')
|
args.append('--is-private')
|
||||||
|
if is_repo_url(repo):
|
||||||
actions.superuser_run('gitweb', args)
|
args = ['create-repo', '--url', repo] + args
|
||||||
|
# create a repo directory and set correct access rights
|
||||||
|
actions.superuser_run('gitweb', args + ['--prepare-only'])
|
||||||
|
# start cloning in background
|
||||||
|
actions.superuser_run('gitweb', args + ['--skip-prepare'],
|
||||||
|
run_in_background=True)
|
||||||
|
else:
|
||||||
|
args = ['create-repo', '--name', repo] + args
|
||||||
|
actions.superuser_run('gitweb', args)
|
||||||
|
|
||||||
|
|
||||||
def repo_info(repo):
|
def repo_info(repo):
|
||||||
|
|||||||
@ -19,37 +19,65 @@ Django form for configuring Gitweb.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import URLValidator
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from plinth.modules import gitweb
|
from plinth.modules import gitweb
|
||||||
|
|
||||||
|
|
||||||
def validate_repository(name):
|
def get_name_from_url(url):
|
||||||
"""Validate a Git repository name."""
|
"""Get a repository name from URL"""
|
||||||
|
return urlparse(url).path.split('/')[-1]
|
||||||
if not re.match(r'^[a-zA-Z0-9-._]+$', name):
|
|
||||||
raise ValidationError(_('Invalid repository name.'))
|
|
||||||
|
|
||||||
if name.startswith(('-', '.')):
|
|
||||||
raise ValidationError(_('Invalid repository name.'))
|
|
||||||
|
|
||||||
if name.endswith('.git.git'):
|
|
||||||
raise ValidationError(_('Invalid repository name.'))
|
|
||||||
|
|
||||||
|
|
||||||
class EditRepoForm(forms.Form):
|
def is_repo_url(url):
|
||||||
|
"""Check if URL is valid."""
|
||||||
|
try:
|
||||||
|
RepositoryValidator(input_should_be='url')(url)
|
||||||
|
except ValidationError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryValidator:
|
||||||
|
input_should_be = 'name'
|
||||||
|
|
||||||
|
def __init__(self, input_should_be=None):
|
||||||
|
if input_should_be is not None:
|
||||||
|
self.input_should_be = input_should_be
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""Validate that the input is a correct repository name or URL"""
|
||||||
|
if self.input_should_be in ('url', 'url_or_name'):
|
||||||
|
try:
|
||||||
|
URLValidator(schemes=['http', 'https'],
|
||||||
|
message=_('Invalid repository URL.'))(value)
|
||||||
|
except ValidationError:
|
||||||
|
if self.input_should_be == 'url':
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
value = get_name_from_url(value)
|
||||||
|
|
||||||
|
if (not re.match(r'^[a-zA-Z0-9-._]+$', value)) \
|
||||||
|
or value.startswith(('-', '.')) \
|
||||||
|
or value.endswith('.git.git'):
|
||||||
|
raise ValidationError(_('Invalid repository name.'), 'invalid')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRepoForm(forms.Form):
|
||||||
"""Form to create and edit a new repository."""
|
"""Form to create and edit a new repository."""
|
||||||
|
|
||||||
name = forms.CharField(
|
name = forms.CharField(
|
||||||
label=_('Name of the repository'),
|
label=_(
|
||||||
strip=True,
|
'Name of a new repository or URL to import an existing repository.'
|
||||||
validators=[validate_repository],
|
), strip=True,
|
||||||
help_text=_(
|
validators=[RepositoryValidator(input_should_be='url_or_name')],
|
||||||
'An alpha-numeric string that uniquely identifies a repository.'),
|
widget=forms.TextInput(attrs={'autocomplete': 'off'}))
|
||||||
)
|
|
||||||
|
|
||||||
description = forms.CharField(
|
description = forms.CharField(
|
||||||
label=_('Description of the repository'), strip=True, required=False,
|
label=_('Description of the repository'), strip=True, required=False,
|
||||||
@ -68,6 +96,40 @@ class EditRepoForm(forms.Form):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['name'].widget.attrs.update({'autofocus': 'autofocus'})
|
self.fields['name'].widget.attrs.update({'autofocus': 'autofocus'})
|
||||||
|
|
||||||
|
def clean_name(self):
|
||||||
|
"""Check if the name is valid."""
|
||||||
|
name = self.cleaned_data['name']
|
||||||
|
|
||||||
|
repo_name = name
|
||||||
|
if is_repo_url(name):
|
||||||
|
repo_name = get_name_from_url(name)
|
||||||
|
|
||||||
|
if repo_name.endswith('.git'):
|
||||||
|
repo_name = repo_name[:-4]
|
||||||
|
|
||||||
|
for repo in gitweb.app.get_repo_list():
|
||||||
|
if repo_name == repo['name']:
|
||||||
|
raise ValidationError(
|
||||||
|
_('A repository with this name already exists.'))
|
||||||
|
|
||||||
|
if is_repo_url(name):
|
||||||
|
if not gitweb.repo_exists(name):
|
||||||
|
raise ValidationError('Remote repository is not available.')
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class EditRepoForm(CreateRepoForm):
|
||||||
|
"""Form to create and edit a new repository."""
|
||||||
|
|
||||||
|
name = forms.CharField(
|
||||||
|
label=_('Name of the repository'),
|
||||||
|
strip=True,
|
||||||
|
validators=[RepositoryValidator()],
|
||||||
|
help_text=_(
|
||||||
|
'An alpha-numeric string that uniquely identifies a repository.'),
|
||||||
|
)
|
||||||
|
|
||||||
def clean_name(self):
|
def clean_name(self):
|
||||||
"""Check if the name is valid."""
|
"""Check if the name is valid."""
|
||||||
name = self.cleaned_data['name']
|
name = self.cleaned_data['name']
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block page_head %}
|
{% block page_head %}
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@ -33,6 +34,9 @@
|
|||||||
.list-group-item .btn {
|
.list-group-item .btn {
|
||||||
margin: -5px 2px;
|
margin: -5px 2px;
|
||||||
}
|
}
|
||||||
|
.repo-cloning {
|
||||||
|
margin: 0px 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -58,13 +62,13 @@
|
|||||||
{% for repo in repos %}
|
{% for repo in repos %}
|
||||||
<div class="list-group-item clearfix">
|
<div class="list-group-item clearfix">
|
||||||
<a href="{% url 'gitweb:delete' repo.name %}"
|
<a href="{% url 'gitweb:delete' repo.name %}"
|
||||||
class="btn btn-default btn-sm pull-right"
|
class="btn btn-default btn-sm pull-right {% if 'clone_progress' in repo %} disabled {% endif %}"
|
||||||
role="button"
|
role="button"
|
||||||
title="{% blocktrans %}Delete repository {{ repo.name }}{% endblocktrans %}">
|
title="{% blocktrans %}Delete repository {{ repo.name }}{% endblocktrans %}">
|
||||||
<span class="fa fa-trash-o" aria-hidden="true"></span>
|
<span class="fa fa-trash-o" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="repo-edit btn btn-sm btn-default pull-right"
|
<a class="repo-edit btn btn-sm btn-default pull-right {% if 'clone_progress' in repo %} disabled {% endif %}"
|
||||||
href="{% url 'gitweb:edit' repo.name %}">
|
href="{% url 'gitweb:edit' repo.name %}">
|
||||||
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
|
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
@ -74,10 +78,17 @@
|
|||||||
aria-label="private"></span>
|
aria-label="private"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="repo-label" href="/gitweb/{{ repo.name }}.git"
|
{% if 'clone_progress' in repo %}
|
||||||
title="{% blocktrans %}Go to repository {{ repo.name }}{% endblocktrans %}">
|
<span class="repo-cloning pull-right">
|
||||||
{{ repo.name }}
|
{% trans 'Cloning...' %} {{ repo.clone_progress }}%
|
||||||
</a>
|
</span>
|
||||||
|
<span class="repo-label">{{ repo.name }}<span>
|
||||||
|
{% else %}
|
||||||
|
<a class="repo-label" href="/gitweb/{{ repo.name }}.git"
|
||||||
|
title="{% blocktrans %}Go to repository {{ repo.name }}{% endblocktrans %}">
|
||||||
|
{{ repo.name }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -86,3 +97,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block page_js %}
|
||||||
|
|
||||||
|
{% if cloning %}
|
||||||
|
<script type="text/javascript" src="{% static 'theme/js/refresh.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -104,3 +104,16 @@ def test_action_create_repo_with_invalid_names(call_action, name):
|
|||||||
'create-repo', '--name', name, '--description', '', '--owner', '',
|
'create-repo', '--name', name, '--description', '', '--owner', '',
|
||||||
'--keep-ownership'
|
'--keep-ownership'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('url', [
|
||||||
|
'Test-repo', 'file://root/Test-repo', 'localhost/Test-repo',
|
||||||
|
'ssh://localhost/Test-repo', 'https://localhost/.Test-repo'
|
||||||
|
])
|
||||||
|
def test_action_create_repo_with_invalid_urls(call_action, url):
|
||||||
|
"""Test that cloning repository with invalid URL fails."""
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
call_action([
|
||||||
|
'create-repo', '--url', url, '--description', '', '--owner', '',
|
||||||
|
'--keep-ownership'
|
||||||
|
])
|
||||||
|
|||||||
@ -31,7 +31,7 @@ from plinth import actions, views
|
|||||||
from plinth.errors import ActionError
|
from plinth.errors import ActionError
|
||||||
from plinth.modules import gitweb
|
from plinth.modules import gitweb
|
||||||
|
|
||||||
from .forms import EditRepoForm
|
from .forms import CreateRepoForm, EditRepoForm
|
||||||
|
|
||||||
|
|
||||||
class GitwebAppView(views.AppView):
|
class GitwebAppView(views.AppView):
|
||||||
@ -48,14 +48,16 @@ class GitwebAppView(views.AppView):
|
|||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
"""Add repositories to the context data."""
|
"""Add repositories to the context data."""
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
context['repos'] = gitweb.app.get_repo_list()
|
repos = gitweb.app.get_repo_list()
|
||||||
|
context['repos'] = repos
|
||||||
|
context['cloning'] = any('clone_progress' in repo for repo in repos)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CreateRepoView(SuccessMessageMixin, FormView):
|
class CreateRepoView(SuccessMessageMixin, FormView):
|
||||||
"""View to create a new repository."""
|
"""View to create a new repository."""
|
||||||
|
|
||||||
form_class = EditRepoForm
|
form_class = CreateRepoForm
|
||||||
prefix = 'gitweb'
|
prefix = 'gitweb'
|
||||||
template_name = 'gitweb_create_edit.html'
|
template_name = 'gitweb_create_edit.html'
|
||||||
success_url = reverse_lazy('gitweb:index')
|
success_url = reverse_lazy('gitweb:index')
|
||||||
@ -75,9 +77,13 @@ class CreateRepoView(SuccessMessageMixin, FormView):
|
|||||||
form_data[key] = ''
|
form_data[key] = ''
|
||||||
else:
|
else:
|
||||||
form_data[key] = value
|
form_data[key] = value
|
||||||
|
try:
|
||||||
gitweb.create_repo(form_data['name'], form_data['description'],
|
gitweb.create_repo(form_data['name'], form_data['description'],
|
||||||
form_data['owner'], form_data['is_private'])
|
form_data['owner'], form_data['is_private'])
|
||||||
|
except ActionError:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_('An error occurred while creating the repository.'))
|
||||||
gitweb.app.update_service_access()
|
gitweb.app.update_service_access()
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
@ -102,7 +108,7 @@ class EditRepoView(SuccessMessageMixin, FormView):
|
|||||||
"""Load information about repository being edited."""
|
"""Load information about repository being edited."""
|
||||||
name = self.kwargs['name']
|
name = self.kwargs['name']
|
||||||
for repo in gitweb.app.get_repo_list():
|
for repo in gitweb.app.get_repo_list():
|
||||||
if repo['name'] == name:
|
if repo['name'] == name and 'clone_progress' not in repo:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise Http404
|
raise Http404
|
||||||
@ -136,7 +142,7 @@ def delete(request, name):
|
|||||||
On POST, delete the repository.
|
On POST, delete the repository.
|
||||||
"""
|
"""
|
||||||
for repo in gitweb.app.get_repo_list():
|
for repo in gitweb.app.get_repo_list():
|
||||||
if repo['name'] == name:
|
if repo['name'] == name and 'clone_progress' not in repo:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user