Merge branch 'master' into validate-payload-webhooks

master
Santos Gallegos 2018-12-03 12:58:00 -05:00
commit 4b6ceabfda
8 changed files with 144 additions and 189 deletions

View File

@ -2,7 +2,6 @@ language: python
python:
- 2.7
- 3.6
sudo: false
env:
- ES_VERSION=1.3.9 ES_DOWNLOAD_URL=https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-${ES_VERSION}.tar.gz
matrix:

View File

@ -18,8 +18,8 @@ log = logging.getLogger(__name__)
class UserProfileForm(forms.ModelForm):
first_name = CharField(label=_('First name'), required=False)
last_name = CharField(label=_('Last name'), required=False)
first_name = CharField(label=_('First name'), required=False, max_length=30)
last_name = CharField(label=_('Last name'), required=False, max_length=30)
class Meta(object):
model = UserProfile

View File

@ -52,21 +52,27 @@ Example layout
fabric -> rtd-builds/fabric/en/latest/ # single version
"""
from __future__ import absolute_import, unicode_literals
from builtins import object
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
import logging
import os
import shutil
import logging
from collections import OrderedDict
from builtins import object
from django.conf import settings
from readthedocs.builds.models import Version
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.core.utils import safe_makedirs, safe_unlink
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.doc_builder.environments import LocalEnvironment
from readthedocs.projects import constants
from readthedocs.projects.models import Domain
from readthedocs.projects.utils import run
log = logging.getLogger(__name__)
@ -83,6 +89,7 @@ class Symlink(object):
self.subproject_root = os.path.join(
self.project_root, 'projects'
)
self.environment = LocalEnvironment(project)
self.sanity_check()
def sanity_check(self):
@ -146,17 +153,27 @@ class Symlink(object):
else:
domains = Domain.objects.filter(project=self.project)
for dom in domains:
log_msg = 'Symlinking CNAME: {0} -> {1}'.format(dom.domain, self.project.slug)
log.info(constants.LOG_TEMPLATE.format(project=self.project.slug,
version='', msg=log_msg))
log_msg = 'Symlinking CNAME: {} -> {}'.format(
dom.domain, self.project.slug
)
log.info(
constants.LOG_TEMPLATE.format(
project=self.project.slug,
version='', msg=log_msg
)
)
# CNAME to doc root
symlink = os.path.join(self.CNAME_ROOT, dom.domain)
run(['ln', '-nsf', self.project_root, symlink])
self.environment.run('ln', '-nsf', self.project_root, symlink)
# Project symlink
project_cname_symlink = os.path.join(self.PROJECT_CNAME_ROOT, dom.domain)
run(['ln', '-nsf', self.project.doc_path, project_cname_symlink])
project_cname_symlink = os.path.join(
self.PROJECT_CNAME_ROOT, dom.domain
)
self.environment.run(
'ln', '-nsf', self.project.doc_path, project_cname_symlink
)
def remove_symlink_cname(self, domain):
"""Remove CNAME symlink."""
@ -201,10 +218,12 @@ class Symlink(object):
# TODO this should use os.symlink, not a call to shell. For now,
# this passes command as a list to be explicit about escaping
# characters like spaces.
status, _, stderr = run(['ln', '-nsf', docs_dir, symlink])
if status > 0:
log.error('Could not symlink path: status=%d error=%s',
status, stderr)
result = self.environment.run('ln', '-nsf', docs_dir, symlink)
if result.exit_code > 0:
log.error(
'Could not symlink path: status=%d error=%s',
result.exit_code, result.error
)
# Remove old symlinks
if os.path.exists(self.subproject_root):
@ -233,12 +252,16 @@ class Symlink(object):
for (language, slug) in list(translations.items()):
log_msg = 'Symlinking translation: {0}->{1}'.format(language, slug)
log.info(constants.LOG_TEMPLATE.format(project=self.project.slug,
version='', msg=log_msg))
log_msg = 'Symlinking translation: {}->{}'.format(language, slug)
log.info(
constants.LOG_TEMPLATE.format(
project=self.project.slug,
version='', msg=log_msg
)
)
symlink = os.path.join(self.project_root, language)
docs_dir = os.path.join(self.WEB_ROOT, slug, language)
run(['ln', '-nsf', docs_dir, symlink])
self.environment.run('ln', '-nsf', docs_dir, symlink)
# Remove old symlinks
for lang in os.listdir(self.project_root):
@ -268,9 +291,13 @@ class Symlink(object):
# Create symlink
if version is not None:
docs_dir = os.path.join(settings.DOCROOT, self.project.slug,
'rtd-builds', version.slug)
run(['ln', '-nsf', docs_dir, symlink])
docs_dir = os.path.join(
settings.DOCROOT,
self.project.slug,
'rtd-builds',
version.slug
)
self.environment.run('ln', '-nsf', docs_dir, symlink)
def symlink_versions(self):
"""
@ -280,7 +307,9 @@ class Symlink(object):
HOME/user_builds/<project>/rtd-builds/<version>
"""
versions = set()
version_dir = os.path.join(self.WEB_ROOT, self.project.slug, self.project.language)
version_dir = os.path.join(
self.WEB_ROOT, self.project.slug, self.project.language
)
# Include active public versions,
# as well as public versions that are built but not active, for archived versions
version_queryset = self.get_version_queryset()
@ -289,11 +318,21 @@ class Symlink(object):
safe_makedirs(version_dir)
for version in version_queryset:
log_msg = 'Symlinking Version: {}'.format(version)
log.info(constants.LOG_TEMPLATE.format(project=self.project.slug,
version='', msg=log_msg))
log.info(
constants.LOG_TEMPLATE.format(
project=self.project.slug,
version='',
msg=log_msg
)
)
symlink = os.path.join(version_dir, version.slug)
docs_dir = os.path.join(settings.DOCROOT, self.project.slug, 'rtd-builds', version.slug)
run(['ln', '-nsf', docs_dir, symlink])
docs_dir = os.path.join(
settings.DOCROOT,
self.project.slug,
'rtd-builds',
version.slug
)
self.environment.run('ln', '-nsf', docs_dir, symlink)
versions.add(version.slug)
# Remove old symlinks

View File

@ -2,16 +2,16 @@
"""Utility functions used by projects."""
from __future__ import (
absolute_import, division, print_function, unicode_literals)
absolute_import,
division,
print_function,
unicode_literals,
)
import fnmatch
import logging
import os
import subprocess
import traceback
import six
from builtins import object, open
from builtins import open
from django.conf import settings
log = logging.getLogger(__name__)
@ -32,82 +32,6 @@ def version_from_slug(slug, version):
return v
def find_file(filename):
"""
Recursively find matching file from the current working path.
:param file: Filename to match
:returns: A list of matching filenames.
"""
matches = []
for root, __, filenames in os.walk('.'):
for match in fnmatch.filter(filenames, filename):
matches.append(os.path.join(root, match))
return matches
def run(*commands):
"""
Run one or more commands.
Each argument in `commands` can be passed as a string or as a list. Passing
as a list is the preferred method, as space escaping is more explicit and it
avoids the need for executing anything in a shell.
If more than one command is given, then this is equivalent to
chaining them together with ``&&``; if all commands succeed, then
``(status, out, err)`` will represent the last successful command.
If one command failed, then ``(status, out, err)`` will represent
the failed command.
:returns: ``(status, out, err)``
"""
environment = os.environ.copy()
environment['READTHEDOCS'] = 'True'
if 'DJANGO_SETTINGS_MODULE' in environment:
del environment['DJANGO_SETTINGS_MODULE']
if 'PYTHONPATH' in environment:
del environment['PYTHONPATH']
# Remove PYTHONHOME env variable if set, otherwise pip install of requirements
# into virtualenv will install incorrectly
if 'PYTHONHOME' in environment:
del environment['PYTHONHOME']
cwd = os.getcwd()
if not commands:
raise ValueError('run() requires one or more command-line strings')
for command in commands:
# If command is a string, split it up by spaces to pass into Popen.
# Otherwise treat the command as an iterable.
if isinstance(command, six.string_types):
run_command = command.split()
else:
try:
run_command = list(command)
command = ' '.join(command)
except TypeError:
run_command = command
log.debug('Running command: cwd=%s command=%s', cwd, command)
try:
p = subprocess.Popen(
run_command,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=environment,
)
out, err = p.communicate()
ret = p.returncode
except OSError:
out = ''
err = traceback.format_exc()
ret = -1
log.exception('Command failed')
return (ret, out, err)
def safe_write(filename, contents):
"""
Normalize and write to filename.
@ -126,9 +50,3 @@ def safe_write(filename, contents):
with open(filename, 'w', encoding='utf-8', errors='ignore') as fh:
fh.write(contents)
fh.close()
class DictObj(object):
def __getattr__(self, attr):
return self.__dict__.get(attr)

View File

@ -49,30 +49,34 @@ class TestGitBackend(RTDTestCase):
self.dummy_conf.submodules.include = ALL
self.dummy_conf.submodules.exclude = []
def test_parse_branches(self):
data = """
develop
master
release/2.0.0
origin/2.0.X
origin/HEAD -> origin/master
origin/master
origin/release/2.0.0
origin/release/foo/bar
"""
expected_ids = [
('develop', 'develop'),
('master', 'master'),
('release/2.0.0', 'release/2.0.0'),
('origin/2.0.X', '2.0.X'),
('origin/master', 'master'),
('origin/release/2.0.0', 'release/2.0.0'),
('origin/release/foo/bar', 'release/foo/bar'),
def test_git_branches(self):
repo_path = self.project.repo
default_branches = [
# comes from ``make_test_git`` function
'submodule',
'relativesubmodule',
'invalidsubmodule',
]
given_ids = [(x.identifier, x.verbose_name) for x in
self.project.vcs_repo().parse_branches(data)]
self.assertEqual(expected_ids, given_ids)
branches = [
'develop',
'master',
'2.0.X',
'release/2.0.0',
'release/foo/bar',
'release-ünîø∂é',
]
for branch in branches:
create_git_branch(repo_path, branch)
repo = self.project.vcs_repo()
# We aren't cloning the repo,
# so we need to hack the repo path
repo.working_dir = repo_path
self.assertEqual(
set(branches + default_branches),
{branch.verbose_name for branch in repo.branches},
)
def test_git_update_and_checkout(self):
repo = self.project.vcs_repo()
@ -93,7 +97,7 @@ class TestGitBackend(RTDTestCase):
repo.working_dir = repo_path
self.assertEqual(
set(['v01', 'v02', 'release-ünîø∂é']),
set(vcs.verbose_name for vcs in repo.tags)
{vcs.verbose_name for vcs in repo.tags},
)
def test_check_for_submodules(self):

View File

@ -124,7 +124,9 @@ class TestCeleryBuilding(RTDTestCase):
self.assertTrue(result.successful())
@patch('readthedocs.projects.tasks.api_v2')
def test_check_duplicate_reserved_version_latest(self, api_v2):
@patch('readthedocs.projects.models.Project.checkout_path')
def test_check_duplicate_reserved_version_latest(self, checkout_path, api_v2):
checkout_path.return_value = self.project.repo
create_git_branch(self.repo, 'latest')
create_git_tag(self.repo, 'latest')
@ -144,7 +146,9 @@ class TestCeleryBuilding(RTDTestCase):
api_v2.project().sync_versions.post.assert_called()
@patch('readthedocs.projects.tasks.api_v2')
def test_check_duplicate_reserved_version_stable(self, api_v2):
@patch('readthedocs.projects.models.Project.checkout_path')
def test_check_duplicate_reserved_version_stable(self, checkout_path, api_v2):
checkout_path.return_value = self.project.repo
create_git_branch(self.repo, 'stable')
create_git_tag(self.repo, 'stable')

View File

@ -35,6 +35,27 @@ class ProfileViewsTest(TestCase):
self.assertEqual(self.user.last_name, 'Docs')
self.assertEqual(self.user.profile.homepage, 'readthedocs.org')
def test_edit_profile_with_invalid_values(self):
resp = self.client.get(
reverse('profiles_profile_edit'),
)
self.assertTrue(resp.status_code, 200)
resp = self.client.post(
reverse('profiles_profile_edit'),
data={
'first_name': 'a' * 31,
'last_name': 'b' * 31,
'homepage': 'c' * 101,
}
)
FORM_ERROR_FORMAT = 'Ensure this value has at most {} characters (it has {}).'
self.assertFormError(resp, form='form', field='first_name', errors=FORM_ERROR_FORMAT.format(30, 31))
self.assertFormError(resp, form='form', field='last_name', errors=FORM_ERROR_FORMAT.format(30, 31))
self.assertFormError(resp, form='form', field='homepage', errors=FORM_ERROR_FORMAT.format(100, 101))
def test_delete_account(self):
resp = self.client.get(
reverse('delete_account')

View File

@ -8,7 +8,6 @@ from __future__ import (
unicode_literals,
)
import csv
import logging
import os
import re
@ -17,7 +16,6 @@ import git
from builtins import str
from django.core.exceptions import ValidationError
from git.exc import BadName
from six import PY2, StringIO
from readthedocs.config import ALL
from readthedocs.projects.exceptions import RepositoryError
@ -174,51 +172,23 @@ class Backend(BaseVCS):
@property
def branches(self):
# Only show remote branches
retcode, stdout, _ = self.run(
'git',
'branch',
'-r',
record_as_success=True,
)
# error (or no branches found)
if retcode != 0:
return []
return self.parse_branches(stdout)
repo = git.Repo(self.working_dir)
versions = []
def parse_branches(self, data):
"""
Parse output of git branch -r.
# ``repo.branches`` returns local branches and
branches = repo.branches
# ``repo.remotes.origin.refs`` returns remote branches
if repo.remotes:
branches += repo.remotes.origin.refs
e.g.:
origin/2.0.X
origin/HEAD -> origin/master
origin/develop
origin/master
origin/release/2.0.0
origin/release/2.1.0
"""
clean_branches = []
# StringIO below is expecting Unicode data, so ensure that it gets it.
if not isinstance(data, str):
data = str(data)
delimiter = str(' ').encode('utf-8') if PY2 else str(' ')
raw_branches = csv.reader(StringIO(data), delimiter=delimiter)
for branch in raw_branches:
branch = [f for f in branch if f not in ('', '*')]
# Handle empty branches
if branch:
branch = branch[0]
if branch.startswith('origin/'):
verbose_name = branch.replace('origin/', '')
if verbose_name in ['HEAD']:
continue
clean_branches.append(
VCSVersion(self, branch, verbose_name))
else:
clean_branches.append(VCSVersion(self, branch, branch))
return clean_branches
for branch in branches:
verbose_name = branch.name
if verbose_name.startswith('origin/'):
verbose_name = verbose_name.replace('origin/', '')
if verbose_name == 'HEAD':
continue
versions.append(VCSVersion(self, str(branch), verbose_name))
return versions
@property
def commit(self):