gitweb: Add ability to change default branch

Now it is possible to change default branch when editing a repository.

Gitweb site shows default branch as a main branch and the 'git clone'
command checks out to default branch.

Added unit and functional tests. Splitted one large 'test_actions'
into multiple tests.

Tests performed:
- All gitweb unit and functional tests pass.
- Created a repository from a remote repository which has default
  branch other than master. Confirmed that the 'Edit repository'
  page shows correct branch and gitweb site shows this branch as
  a default branch

Closes #1925

Signed-off-by: Veiko Aasa <veiko17@disroot.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Veiko Aasa 2020-09-01 13:34:59 +03:00 committed by Sunil Mohan Adapa
parent 95c99bf3ce
commit c136f27707
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
7 changed files with 294 additions and 65 deletions

View File

@ -12,6 +12,7 @@ import os
import re
import shutil
import subprocess
import sys
import time
from plinth import action_utils
@ -23,6 +24,7 @@ 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'):
@ -32,6 +34,7 @@ class ValidateRepoName(argparse.Action):
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)
@ -86,6 +89,18 @@ def parse_arguments():
subparser.add_argument('--newname', required=True, action=ValidateRepoName,
help='New name of the repository')
subparser = subparsers.add_parser(
'set-default-branch', help='Set default branch of the repository')
subparser.add_argument('--name', required=True, action=ValidateRepoName,
help='Name of the repository')
subparser.add_argument('--branch', required=True,
help='Name of the branch')
subparser = subparsers.add_parser(
'get-branches', help='Get all the branches of the repository')
subparser.add_argument('--name', required=True, action=ValidateRepoName,
help='Name of the repository')
subparser = subparsers.add_parser('set-repo-description',
help='Set description of the repository')
subparser.add_argument('--name', required=True, action=ValidateRepoName,
@ -245,7 +260,7 @@ def _create_repo(arguments):
"""Create an empty repository."""
repo = arguments.name
try:
subprocess.check_call(['git', 'init', '--bare', repo],
subprocess.check_call(['git', 'init', '-q', '--bare', repo],
cwd=GIT_REPO_PATH)
if not arguments.keep_ownership:
subprocess.check_call(['chown', '-R', 'www-data:www-data', repo],
@ -261,6 +276,15 @@ def _create_repo(arguments):
raise
def _get_default_branch(repo):
"""Get default branch of the repository."""
repo_path = os.path.join(GIT_REPO_PATH, repo)
return subprocess.check_output(
['git', '-C', repo_path, 'symbolic-ref', '--short',
'HEAD']).decode().strip()
def _get_repo_description(repo):
"""Set description of the repository."""
description_file = os.path.join(GIT_REPO_PATH, repo, 'description')
@ -325,6 +349,25 @@ def _set_access_status(repo, status):
os.remove(private_file)
def _get_branches(repo):
"""Return list of the branches in the repository."""
output = subprocess.check_output(
['git', '-C', repo, 'branch', '--format=%(refname:short)'],
cwd=GIT_REPO_PATH)
return output.decode().strip().split()
def subcommand_get_branches(arguments):
"""Check whether a branch exists in the repository."""
repo = arguments.name
print(
json.dumps(
dict(default_branch=_get_default_branch(repo),
branches=_get_branches(repo))))
def subcommand_rename_repo(arguments):
"""Rename a repository."""
oldpath = os.path.join(GIT_REPO_PATH, arguments.oldname)
@ -332,6 +375,20 @@ def subcommand_rename_repo(arguments):
os.rename(oldpath, newpath)
def subcommand_set_default_branch(arguments):
"""Set description of the repository."""
repo = arguments.name
branch = arguments.branch
if branch not in _get_branches(repo):
sys.exit('No such branch.')
subprocess.check_call([
'git', '-C', repo, 'symbolic-ref', 'HEAD',
"refs/heads/{}".format(branch)
], cwd=GIT_REPO_PATH)
def subcommand_set_repo_description(arguments):
"""Set description of the repository."""
_set_repo_description(arguments.name, arguments.description)
@ -355,10 +412,13 @@ def subcommand_repo_info(arguments):
print(
json.dumps(
dict(name=arguments.name[:-4],
description=_get_repo_description(arguments.name),
owner=_get_repo_owner(arguments.name),
access=_get_access_status(arguments.name))))
dict(
name=arguments.name[:-4],
description=_get_repo_description(arguments.name),
owner=_get_repo_owner(arguments.name),
access=_get_access_status(arguments.name),
default_branch=_get_default_branch(arguments.name),
)))
def subcommand_create_repo(arguments):

View File

@ -18,9 +18,7 @@ from plinth.modules.users.components import UsersAndGroups
from .forms import is_repo_url
from .manifest import ( # noqa, pylint: disable=unused-import
GIT_REPO_PATH,
backup,
clients)
GIT_REPO_PATH, backup, clients)
version = 1
@ -233,6 +231,18 @@ def _rename_repo(oldname, newname):
actions.superuser_run('gitweb', args)
def _set_default_branch(repo, branch):
"""Set default branch of the repository."""
args = [
'set-default-branch',
'--name',
repo,
'--branch',
branch,
]
actions.superuser_run('gitweb', args)
def _set_repo_description(repo, repo_description):
"""Set description of the repository."""
args = [
@ -277,6 +287,9 @@ def edit_repo(form_initial, form_cleaned):
else:
_set_repo_access(repo, 'public')
if form_cleaned['default_branch'] != form_initial['default_branch']:
_set_default_branch(repo, form_cleaned['default_branch'])
def delete_repo(repo):
"""Delete a repository."""

View File

@ -3,6 +3,7 @@
Django form for configuring Gitweb.
"""
import json
import re
from urllib.parse import urlparse
@ -11,9 +12,23 @@ from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth.modules import gitweb
def _get_branches(repo):
"""Get all the branches in the repository."""
branch_data = json.loads(
actions.run('gitweb', ['get-branches', '--name', repo]))
default_branch = branch_data['default_branch']
branches = branch_data['branches']
if default_branch not in branches:
branches.insert(0, default_branch)
return [(branch, branch) for branch in branches]
def get_name_from_url(url):
"""Get a repository name from URL"""
return urlparse(url).path.split('/')[-1]
@ -115,6 +130,16 @@ class EditRepoForm(CreateRepoForm):
'An alpha-numeric string that uniquely identifies a repository.'),
)
default_branch = forms.ChoiceField(
label=_('Default branch'),
help_text=_('Gitweb displays this as a default branch.'))
def __init__(self, *args, **kwargs):
"""Initialize the form with extra request argument."""
super().__init__(*args, **kwargs)
branches = _get_branches(self.initial['name'])
self.fields['default_branch'].choices = branches
def clean_name(self):
"""Check if the name is valid."""
name = self.cleaned_data['name']

View File

@ -27,12 +27,6 @@ Scenario: Create private repository
Then the repository should be listed as a private
And the repository should be listed on gitweb
Scenario: Delete repository
Given the gitweb application is enabled
And a repository
When I delete the repository
Then the repository should not be listed
@backups
Scenario: Backup and restore gitweb
Given the gitweb application is enabled
@ -70,6 +64,13 @@ Scenario: Edit repository metadata
And I set the metadata of the repository
Then the metadata of the repository should be as set
Scenario: Edit default branch of the repository
Given the gitweb application is enabled
And a repository with the branch branch1
When I set branch1 as a default branch
Then the gitweb site should show branch1 as a default repo branch
Scenario: Access public repository with git client
Given the gitweb application is enabled
And a public repository
@ -87,6 +88,12 @@ Scenario: Access private repository with git client
And the repository should be privately readable
And the repository should be privately writable
Scenario: Delete repository
Given the gitweb application is enabled
And a repository
When I delete the repository
Then the repository should not be listed
Scenario: Disable gitweb application
Given the gitweb application is enabled
When I disable the gitweb application

View File

@ -11,6 +11,15 @@ from unittest.mock import patch
import pytest
from django.forms import ValidationError
REPO_NAME = 'Test-repo'
REPO_DATA = {
'name': REPO_NAME,
'description': '',
'owner': '',
'access': 'private',
'default_branch': 'master',
}
def _action_file():
"""Return the path to the 'gitweb' actions file."""
@ -25,6 +34,7 @@ gitweb_actions = imp.load_source('gitweb', _action_file())
@pytest.fixture(name='call_action')
def fixture_call_action(tmpdir, capsys):
"""Run actions with custom repo root path."""
def _call_action(args, **kwargs):
gitweb_actions.GIT_REPO_PATH = str(tmpdir)
with patch('argparse._sys.argv', ['gitweb'] + args):
@ -35,47 +45,85 @@ def fixture_call_action(tmpdir, capsys):
return _call_action
def test_actions(call_action):
"""Test gitweb actions script."""
repo = 'Test-repo'
repo_renamed = 'Test-repo_2'
data = {
'name': repo,
'description': 'Test description',
'owner': 'Test owner',
'access': 'private'
@pytest.fixture(name='existing_repo')
def fixture_existing_repo(call_action):
"""A fixture to create a repository."""
try:
call_action(['delete-repo', '--name', REPO_NAME])
except FileNotFoundError:
pass
call_action([
'create-repo', '--name', REPO_NAME, '--description', '', '--owner', '',
'--is-private', '--keep-ownership'
])
def test_create_repo(call_action):
"""Test creating a repository."""
call_action([
'create-repo', '--name', REPO_NAME, '--description', '', '--owner', '',
'--is-private', '--keep-ownership'
])
assert json.loads(call_action(['repo-info', '--name',
REPO_NAME])) == REPO_DATA
def test_change_repo_medatada(call_action, existing_repo):
"""Test change a metadata of the repository."""
new_data = {
'name': REPO_NAME,
'description': 'description2',
'owner': 'owner2',
'access': 'public',
'default_branch': 'master',
}
# Create repository
call_action([
'create-repo', '--name', repo, '--description', data['description'],
'--owner', data['owner'], '--is-private', '--keep-ownership'
'set-repo-description', '--name', REPO_NAME, '--description',
new_data['description']
])
assert json.loads(call_action(['repo-info', '--name', repo])) == data
# Change metadata
data['description'] = 'Test description 2'
data['owner'] = 'Test owner 2'
data['access'] = 'public'
call_action([
'set-repo-description', '--name', repo, '--description',
data['description']
])
call_action(['set-repo-owner', '--name', repo, '--owner', data['owner']])
call_action(
['set-repo-access', '--name', repo, '--access', data['access']])
assert json.loads(call_action(['repo-info', '--name', repo])) == data
['set-repo-owner', '--name', REPO_NAME, '--owner', new_data['owner']])
call_action([
'set-repo-access', '--name', REPO_NAME, '--access', new_data['access']
])
# Rename repository
call_action(['rename-repo', '--oldname', repo, '--newname', repo_renamed])
with pytest.raises(RuntimeError, match='Repository not found'):
call_action(['repo-info', '--name', repo])
assert call_action(['repo-info', '--name', repo_renamed])
assert json.loads(call_action(['repo-info', '--name',
REPO_NAME])) == new_data
# Delete repository
call_action(['delete-repo', '--name', repo_renamed])
def test_rename_repository(call_action, existing_repo):
"""Test renaming a repository."""
new_name = 'Test-repo_2'
call_action(['rename-repo', '--oldname', REPO_NAME, '--newname', new_name])
with pytest.raises(RuntimeError, match='Repository not found'):
call_action(['repo-info', '--name', repo_renamed])
call_action(['repo-info', '--name', REPO_NAME])
assert json.loads(call_action(['repo-info', '--name', new_name])) == {
**REPO_DATA,
**{
'name': new_name
}
}
def test_get_branches(call_action, existing_repo):
"""Test getting all the branches of the repository."""
assert json.loads(call_action(['get-branches', '--name', REPO_NAME])) == {
"default_branch": "master",
"branches": []
}
def test_delete_repository(call_action, existing_repo):
"""Test deleting a repository."""
call_action(['delete-repo', '--name', REPO_NAME])
with pytest.raises(RuntimeError, match='Repository not found'):
call_action(['repo-info', '--name', REPO_NAME])
@pytest.mark.parametrize(

View File

@ -30,6 +30,13 @@ def gitweb_private_repo(session_browser):
_create_repo(session_browser, 'Test-repo', 'private', True)
@given(parsers.parse('a repository with the branch {branch:w}'))
def _create_repo_with_branch(session_browser, branch):
_delete_repo(session_browser, 'Test-repo', ignore_missing=True)
_create_repo(session_browser, 'Test-repo', 'public')
_create_branch('Test-repo', branch)
@given('both public and private repositories exist')
def gitweb_public_and_private_repo(session_browser):
_create_repo(session_browser, 'Test-repo', 'public', True)
@ -66,6 +73,11 @@ def gitweb_delete_repo(session_browser):
_delete_repo(session_browser, 'Test-repo')
@when(parsers.parse('I set {branch:w} as a default branch'))
def gitweb_set_default_branch(session_browser, branch):
_set_default_branch(session_browser, 'Test-repo', branch)
@when('I set the metadata of the repository')
def gitweb_edit_repo_metadata(session_browser, gitweb_repo_metadata):
_edit_repo_metadata(session_browser, 'Test-repo', gitweb_repo_metadata)
@ -76,6 +88,14 @@ def gitweb_using_git_client():
pass
@then(
parsers.parse(
'the gitweb site should show {branch:w} as a default repo branch'))
def gitweb_site_check_default_repo_branch(session_browser, branch):
assert _get_gitweb_site_default_repo_branch(session_browser,
'Test-repo') == branch
@then('the repository should be restored')
@then('the repository should be listed as a public')
def gitweb_repo_should_exists(session_browser):
@ -142,6 +162,39 @@ def gitweb_repo_privately_writable():
url_git_extension=True)
def _create_branch(repo, branch):
"""Create a branch on the remote repository."""
repo_url = _get_repo_url(repo, with_auth=True)
with _gitweb_temp_directory() as temp_directory:
repo_path = os.path.join(temp_directory, repo)
_create_local_repo(repo_path)
add_branch_commands = [['git', 'checkout', '-q', '-b', branch],
[
'git', '-c', 'user.name=Tester', '-c',
'user.email=tester', 'commit', '-q',
'--allow-empty', '-m', 'test_branch1'
],
['git', 'push', '-q', '-f', repo_url, branch]]
for command in add_branch_commands:
subprocess.check_call(command, cwd=repo_path)
def _create_local_repo(path):
"""Create a local repository."""
shutil.rmtree(path, ignore_errors=True)
os.mkdir(path)
create_repo_commands = [
'git init -q', 'git config http.sslVerify false',
'git -c "user.name=Tester" -c "user.email=tester" '
'commit -q --allow-empty -m "test"'
]
for command in create_repo_commands:
subprocess.check_call(command, shell=True, cwd=path)
def _create_repo(browser, repo, access=None, ok_if_exists=False):
"""Create repository."""
if not _repo_exists(browser, repo, access):
@ -187,6 +240,13 @@ def _edit_repo_metadata(browser, repo, metadata):
functional.submit(browser)
def _get_gitweb_site_default_repo_branch(browser, repo):
functional.nav_to_module(browser, 'gitweb')
browser.find_by_css('a[href="/gitweb/{0}.git"]'.format(repo)).first.click()
return browser.find_by_css('.head').first.text
def _get_repo_metadata(browser, repo):
"""Get repository metadata."""
functional.nav_to_module(browser, 'gitweb')
@ -268,19 +328,23 @@ def _repo_is_writable(repo, with_auth=False, url_git_extension=False):
if url_git_extension:
url = url + '.git'
with _gitweb_temp_directory() as cwd:
subprocess.run(['mkdir', 'test-project'], check=True, cwd=cwd)
cwd = os.path.join(cwd, 'test-project')
prepare_git_repo_commands = [
'git init -q', 'git config http.sslVerify false',
'git -c "user.name=Tester" -c "user.email=tester" '
'commit -q --allow-empty -m "test"'
]
for command in prepare_git_repo_commands:
subprocess.run(command, shell=True, check=True, cwd=cwd)
with _gitweb_temp_directory() as temp_directory:
repo_directory = os.path.join(temp_directory, 'test-project')
_create_local_repo(repo_directory)
git_push_command = ['git', 'push', '-qf', url, 'master']
return _gitweb_git_command_is_successful(git_push_command, cwd)
return _gitweb_git_command_is_successful(git_push_command,
repo_directory)
def _set_default_branch(browser, repo, branch):
"""Set default branch of the repository."""
functional.nav_to_module(browser, 'gitweb')
browser.find_link_by_href(
'/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click()
browser.find_by_id('id_gitweb-default_branch').select(branch)
functional.submit(browser)
def _set_repo_access(browser, repo, access):

View File

@ -24,14 +24,16 @@ EXISTING_REPOS = [
'description': '',
'owner': '',
'access': 'public',
'is_private': False
'is_private': False,
'default_branch': 'master',
},
{
'name': 'something2',
'description': '',
'owner': '',
'access': 'private',
'is_private': True
'is_private': True,
'default_branch': 'master',
},
]
@ -48,12 +50,19 @@ def fixture_gitweb_urls():
def action_run(*args, **kwargs):
"""Action return values."""
if args[1][0] == 'repo-info':
subcommand = args[1][0]
if subcommand == 'repo-info':
return json.dumps(EXISTING_REPOS[0])
if args[1][0] == 'check-repo-exists':
elif subcommand == 'check-repo-exists':
return True
elif subcommand == 'get-branches':
return json.dumps({
"default_branch": "master",
"branches": ["master", "branch1"]
})
return None
@ -208,7 +217,8 @@ def test_edit_repository_view(rf):
'gitweb-name': 'something_other.git',
'gitweb-description': 'test-description',
'gitweb-owner': 'test-owner',
'gitweb-is_private': True
'gitweb-is_private': True,
'gitweb-default_branch': 'branch1',
}
url = urls.reverse('gitweb:edit',
kwargs={'name': EXISTING_REPOS[0]['name']})
@ -277,7 +287,8 @@ def test_edit_repository_no_change_view(rf):
form_data = {
'gitweb-name': EXISTING_REPOS[0]['name'],
'gitweb-description': EXISTING_REPOS[0]['description'],
'gitweb-owner': EXISTING_REPOS[0]['owner']
'gitweb-owner': EXISTING_REPOS[0]['owner'],
'gitweb-default_branch': EXISTING_REPOS[0]['default_branch'],
}
request = rf.post(
urls.reverse('gitweb:edit',
@ -297,7 +308,8 @@ def test_edit_repository_failed_view(rf):
form_data = {
'gitweb-name': 'something_other',
'gitweb-description': 'test-description',
'gitweb-owner': 'test-owner'
'gitweb-owner': 'test-owner',
'gitweb-default_branch': 'master',
}
request = rf.post(
urls.reverse('gitweb:edit',