Skip to content

Commit 3c34456

Browse files
humitosagjohnson
authored andcommitted
Log git checkout and expose to users (#3520)
* Log `git checkout` and expose to users * Use `setup_env` and record for `git clone` * Use LocalEnvironment to record VCS commands in the build * Split LocalEnvironment into LocalBuildEnvironment and LocalEnvironment * LocalEnvironment * env fix * Run VCS command in the proper environment * Define env * Proper env variables * Pass proper env variables when running VCS commands * warn_only in status * Record command when inside a BuildEnvironment * Remove --quiet options * Do not record ``warn_only`` commands * Refactor * Add warn_only to other VCS * Remove `update_imported_docs` and use UpdateDocsTask * Hack the Environment logs to avoid fails because no version * Abstraction for UpdateDocsTask and SyncRepositoryTask These are two separated tasks that share some code by inheriting from SyncRepositoryMixin. The final goal (as ``core.views.hooks._build_url`` is *DEPRECATED*) is to finally remove the SyncRepositoryTask and merge that code into UpdateDocsTask. * Allow to not record specific command Either `record=False` or `warn_only=True` commands are not recorded. This attributes can be set at initialization time or when calling the `run()` method. * Proper use of `record`, `warn_only` and the new `force_success` * Use record_as_success instead of force_success We need the real exit_code for some command since there are decisions based on that code, but we want to record it as success. So, the fake exit_code is saved in the database but the real exit_code is used in the whole flow of the building process. * Replace force_success to record_as_success in calls * Lint * Fix docs styling * Remove unnecessary initializing variables
1 parent a79534b commit 3c34456

File tree

15 files changed

+403
-298
lines changed

15 files changed

+403
-298
lines changed

docs/builds.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ An example in code:
7575

7676
.. code-block:: python
7777
78-
update_imported_docs(version)
78+
update_docs_from_vcs(version)
7979
if exists('setup.py'):
8080
run('python setup.py install')
8181
if project.requirements_file:

readthedocs/core/management/commands/pull.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Command(BaseCommand):
1919
def handle(self, *args, **options):
2020
if args:
2121
for slug in args:
22-
tasks.update_imported_docs(
23-
utils.version_from_slug(slug, LATEST).pk
22+
version = utils.version_from_slug(slug, LATEST)
23+
tasks.SyncRepositoryTask().run(
24+
version.pk,
2425
)

readthedocs/core/views/hooks.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from readthedocs.builds.constants import LATEST
1313
from readthedocs.projects import constants
1414
from readthedocs.projects.models import Project, Feature
15-
from readthedocs.projects.tasks import update_imported_docs
15+
from readthedocs.projects.tasks import SyncRepositoryTask
1616

1717
import logging
1818

@@ -123,8 +123,12 @@ def _build_url(url, projects, branches):
123123
for project in projects:
124124
(built, not_building) = build_branches(project, branches)
125125
if not built:
126-
# Call update_imported_docs to update tag/branch info
127-
update_imported_docs.delay(project.versions.get(slug=LATEST).pk)
126+
# Call SyncRepositoryTask to update tag/branch info
127+
version = project.versions.get(slug=LATEST)
128+
sync_repository = SyncRepositoryTask()
129+
sync_repository.apply_async(
130+
args=(version.pk,),
131+
)
128132
msg = '(URL Build) Syncing versions for %s' % project.slug
129133
log.info(msg)
130134
all_built[project.slug] = built

readthedocs/doc_builder/environments.py

Lines changed: 135 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
13
"""Documentation Builder Environments."""
24

35
from __future__ import absolute_import
@@ -14,7 +16,7 @@
1416

1517
from readthedocs.core.utils import slugify
1618
from django.conf import settings
17-
from django.utils.translation import ugettext_lazy as _, ugettext_noop
19+
from django.utils.translation import ugettext_lazy as _
1820
from docker import Client
1921
from docker.utils import create_host_config
2022
from docker.errors import APIError as DockerAPIError, DockerException
@@ -40,7 +42,8 @@
4042
__all__ = (
4143
'api_v2',
4244
'BuildCommand', 'DockerBuildCommand',
43-
'BuildEnvironment', 'LocalEnvironment', 'DockerEnvironment',
45+
'LocalEnvironment',
46+
'LocalBuildEnvironment', 'DockerBuildEnvironment',
4447
)
4548

4649

@@ -70,7 +73,7 @@ class BuildCommand(BuildCommandResultMixin):
7073

7174
def __init__(self, command, cwd=None, shell=False, environment=None,
7275
combine_output=True, input_data=None, build_env=None,
73-
bin_path=None, description=None):
76+
bin_path=None, description=None, record_as_success=False):
7477
self.command = command
7578
self.shell = shell
7679
if cwd is None:
@@ -93,6 +96,7 @@ def __init__(self, command, cwd=None, shell=False, environment=None,
9396
self.description = ''
9497
if description is not None:
9598
self.description = description
99+
self.record_as_success = record_as_success
96100
self.exit_code = None
97101

98102
def __str__(self):
@@ -180,12 +184,21 @@ def get_command(self):
180184

181185
def save(self):
182186
"""Save this command and result via the API."""
187+
exit_code = self.exit_code
188+
189+
# Force record this command as success to avoid Build reporting errors
190+
# on commands that are just for checking purposes and do not interferes
191+
# in the Build
192+
if self.record_as_success:
193+
log.warning('Recording command exit_code as success')
194+
exit_code = 0
195+
183196
data = {
184197
'build': self.build_env.build.get('id'),
185198
'command': self.get_command(),
186199
'description': self.description,
187200
'output': self.output,
188-
'exit_code': self.exit_code,
201+
'exit_code': exit_code,
189202
'start_time': self.start_time,
190203
'end_time': self.end_time,
191204
}
@@ -268,7 +281,100 @@ def get_wrapped_command(self):
268281
for part in self.command]))))
269282

270283

271-
class BuildEnvironment(object):
284+
class BaseEnvironment(object):
285+
286+
"""
287+
Base environment class.
288+
289+
Used to run arbitrary commands outside a build.
290+
"""
291+
292+
def __init__(self, project, environment=None):
293+
# TODO: maybe we can remove this Project dependency also
294+
self.project = project
295+
self.environment = environment or {}
296+
self.commands = []
297+
298+
def record_command(self, command):
299+
pass
300+
301+
def _log_warning(self, msg):
302+
log.warning(LOG_TEMPLATE.format(
303+
project=self.project.slug,
304+
version='latest',
305+
msg=msg,
306+
))
307+
308+
def run(self, *cmd, **kwargs):
309+
"""Shortcut to run command from environment."""
310+
return self.run_command_class(cls=self.command_class, cmd=cmd, **kwargs)
311+
312+
def run_command_class(
313+
self, cls, cmd, record=None, warn_only=False,
314+
record_as_success=False, **kwargs):
315+
"""
316+
Run command from this environment.
317+
318+
:param cls: command class to instantiate a command
319+
:param cmd: command (as a list) to execute in this environment
320+
:param record: whether or not to record this particular command
321+
(``False`` implies ``warn_only=True``)
322+
:param warn_only: don't raise an exception on command failure
323+
:param record_as_success: force command ``exit_code`` to be saved as
324+
``0`` (``True`` implies ``warn_only=True`` and ``record=True``)
325+
"""
326+
if record is None:
327+
# ``self.record`` only exists when called from ``*BuildEnvironment``
328+
record = getattr(self, 'record', False)
329+
330+
if not record:
331+
warn_only = True
332+
333+
if record_as_success:
334+
record = True
335+
warn_only = True
336+
# ``record_as_success`` is needed to instantiate the BuildCommand
337+
kwargs.update({'record_as_success': record_as_success})
338+
339+
# Remove PATH from env, and set it to bin_path if it isn't passed in
340+
env_path = self.environment.pop('BIN_PATH', None)
341+
if 'bin_path' not in kwargs and env_path:
342+
kwargs['bin_path'] = env_path
343+
assert 'environment' not in kwargs, "environment can't be passed in via commands."
344+
kwargs['environment'] = self.environment
345+
346+
# ``build_env`` is passed as ``kwargs`` when it's called from a
347+
# ``*BuildEnvironment``
348+
build_cmd = cls(cmd, **kwargs)
349+
self.commands.append(build_cmd)
350+
build_cmd.run()
351+
352+
if record:
353+
# TODO: I don't like how it's handled this entry point here since
354+
# this class should know nothing about a BuildCommand (which are the
355+
# only ones that can be saved/recorded)
356+
self.record_command(build_cmd)
357+
358+
if build_cmd.failed:
359+
msg = u'Command {cmd} failed'.format(cmd=build_cmd.get_command())
360+
361+
if build_cmd.output:
362+
msg += u':\n{out}'.format(out=build_cmd.output)
363+
364+
if warn_only:
365+
self._log_warning(msg)
366+
else:
367+
raise BuildEnvironmentWarning(msg)
368+
return build_cmd
369+
370+
371+
class LocalEnvironment(BaseEnvironment):
372+
373+
# TODO: BuildCommand name doesn't make sense here, should be just Command
374+
command_class = BuildCommand
375+
376+
377+
class BuildEnvironment(BaseEnvironment):
272378

273379
"""
274380
Base build environment.
@@ -303,15 +409,13 @@ class BuildEnvironment(object):
303409

304410
def __init__(self, project=None, version=None, build=None, config=None,
305411
record=True, environment=None, update_on_success=True):
306-
self.project = project
412+
super(BuildEnvironment, self).__init__(project, environment)
307413
self.version = version
308414
self.build = build
309415
self.config = config
310416
self.record = record
311-
self.environment = environment or {}
312417
self.update_on_success = update_on_success
313418

314-
self.commands = []
315419
self.failure = None
316420
self.start_time = datetime.utcnow()
317421

@@ -348,48 +452,28 @@ def handle_exception(self, exc_type, exc_value, _):
348452
self.failure = exc_value
349453
return True
350454

351-
def run(self, *cmd, **kwargs):
352-
"""Shortcut to run command from environment."""
353-
return self.run_command_class(cls=self.command_class, cmd=cmd, **kwargs)
455+
def record_command(self, command):
456+
command.save()
354457

355-
def run_command_class(self, cls, cmd, **kwargs):
356-
"""
357-
Run command from this environment.
358-
359-
Use ``cls`` to instantiate a command
360-
361-
:param warn_only: Don't raise an exception on command failure
362-
"""
363-
warn_only = kwargs.pop('warn_only', False)
364-
# Remove PATH from env, and set it to bin_path if it isn't passed in
365-
env_path = self.environment.pop('BIN_PATH', None)
366-
if 'bin_path' not in kwargs and env_path:
367-
kwargs['bin_path'] = env_path
368-
assert 'environment' not in kwargs, "environment can't be passed in via commands."
369-
kwargs['environment'] = self.environment
370-
kwargs['build_env'] = self
371-
build_cmd = cls(cmd, **kwargs)
372-
self.commands.append(build_cmd)
373-
build_cmd.run()
374-
375-
# Save to database
376-
if self.record:
377-
build_cmd.save()
378-
379-
if build_cmd.failed:
380-
msg = u'Command {cmd} failed'.format(cmd=build_cmd.get_command())
458+
def _log_warning(self, msg):
459+
# :'(
460+
log.warning(LOG_TEMPLATE.format(
461+
project=self.project.slug,
462+
version=self.version.slug,
463+
msg=msg,
464+
))
381465

382-
if build_cmd.output:
383-
msg += u':\n{out}'.format(out=build_cmd.output)
466+
def run(self, *cmd, **kwargs):
467+
kwargs.update({
468+
'build_env': self,
469+
})
470+
return super(BuildEnvironment, self).run(*cmd, **kwargs)
384471

385-
if warn_only:
386-
log.warning(LOG_TEMPLATE
387-
.format(project=self.project.slug,
388-
version=self.version.slug,
389-
msg=msg))
390-
else:
391-
raise BuildEnvironmentWarning(msg)
392-
return build_cmd
472+
def run_command_class(self, *cmd, **kwargs): # pylint: disable=arguments-differ
473+
kwargs.update({
474+
'build_env': self,
475+
})
476+
return super(BuildEnvironment, self).run_command_class(*cmd, **kwargs)
393477

394478
@property
395479
def successful(self):
@@ -497,14 +581,14 @@ def update_build(self, state=None):
497581
log.exception("Unknown build exception")
498582

499583

500-
class LocalEnvironment(BuildEnvironment):
584+
class LocalBuildEnvironment(BuildEnvironment):
501585

502-
"""Local execution environment."""
586+
"""Local execution build environment."""
503587

504588
command_class = BuildCommand
505589

506590

507-
class DockerEnvironment(BuildEnvironment):
591+
class DockerBuildEnvironment(BuildEnvironment):
508592

509593
"""
510594
Docker build environment, uses docker to contain builds.
@@ -527,7 +611,7 @@ class DockerEnvironment(BuildEnvironment):
527611

528612
def __init__(self, *args, **kwargs):
529613
self.docker_socket = kwargs.pop('docker_socket', DOCKER_SOCKET)
530-
super(DockerEnvironment, self).__init__(*args, **kwargs)
614+
super(DockerBuildEnvironment, self).__init__(*args, **kwargs)
531615
self.client = None
532616
self.container = None
533617
self.container_name = slugify(

readthedocs/projects/apps.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class ProjectsConfig(AppConfig):
77
name = 'readthedocs.projects'
88

99
def ready(self):
10-
from readthedocs.projects.tasks import UpdateDocsTask
10+
from readthedocs.projects import tasks
1111
from readthedocs.worker import app
12-
app.tasks.register(UpdateDocsTask)
12+
app.tasks.register(tasks.SyncRepositoryTask)
13+
app.tasks.register(tasks.UpdateDocsTask)

readthedocs/projects/models.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
determine_stable_version, version_windows)
3434
from readthedocs.restapi.client import api
3535
from readthedocs.vcs_support.backends import backend_cls
36-
from readthedocs.vcs_support.base import VCSProject
3736
from readthedocs.vcs_support.utils import Lock, NonBlockingLock
3837

3938
log = logging.getLogger(__name__)
@@ -605,14 +604,24 @@ def has_htmlzip(self, version_slug=LATEST):
605604
def sponsored(self):
606605
return False
607606

608-
def vcs_repo(self, version=LATEST):
607+
def vcs_repo(self, version=LATEST, environment=None):
608+
"""
609+
Return a Backend object for this project able to handle VCS commands.
610+
611+
:param environment: environment to run the commands
612+
:type environment: doc_builder.environments.BuildEnvironment
613+
:param version: version slug for the backend (``LATEST`` by default)
614+
:type version: str
615+
"""
616+
# TODO: this seems to be the only method that receives a
617+
# ``version.slug`` instead of a ``Version`` instance (I prefer an
618+
# instance here)
619+
609620
backend = backend_cls.get(self.repo_type)
610621
if not backend:
611622
repo = None
612623
else:
613-
proj = VCSProject(
614-
self.name, self.default_branch, self.checkout_path(version), self.clean_repo)
615-
repo = backend(proj, version)
624+
repo = backend(self, version, environment)
616625
return repo
617626

618627
def repo_nonblockinglock(self, version, max_lock_age=5):

0 commit comments

Comments
 (0)