Merge branch 'master' into validate-payload-webhooks
commit
4b6ceabfda
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue