Skip to content

Commit

Permalink
Setting html_last_updated_fmt value to last commit
Browse files Browse the repository at this point in the history
Setting last_updated timestamp value to the last authored date of the
specific RST file instead of datetime.now().

Fixing broken docsV in TravisCI after using google analytics pypi
package.

Fixes #26
  • Loading branch information
Robpol86 committed Dec 10, 2016
1 parent 407427d commit d79edd5
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 11 deletions.
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ Changelog

This project adheres to `Semantic Versioning <http://semver.org/>`_.

Unreleased
----------

Added
* Time value of ``html_last_updated_fmt`` will be the last git commit (authored) date.

Fixed
* https://github.com/Robpol86/sphinxcontrib-versioning/issues/26

2.2.0 - 2016-09-15
------------------

Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
author = '@Robpol86'
copyright = '{}, {}'.format(time.strftime('%Y'), author)
html_last_updated_fmt = '%c'
master_doc = 'index'
project = __import__('setup').NAME
pygments_style = 'friendly'
Expand Down
15 changes: 14 additions & 1 deletion sphinxcontrib/versioning/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,14 @@ def chunk(iterator, max_size):
yield chunked


def run_command(local_root, command, env_var=True, pipeto=None, retry=0):
def run_command(local_root, command, env_var=True, pipeto=None, retry=0, environ=None):
"""Run a command and return the output.
:raise CalledProcessError: Command exits non-zero.
:param str local_root: Local path to git root directory.
:param iter command: Command to run.
:param dict environ: Environment variables to set/override in the command.
:param bool env_var: Define GIT_DIR environment variable (on non-Windows).
:param function pipeto: Pipe `command`'s stdout to this function (only parameter given).
:param int retry: Retry this many times on CalledProcessError after 0.1 seconds.
Expand All @@ -130,6 +131,8 @@ def run_command(local_root, command, env_var=True, pipeto=None, retry=0):

# Setup env.
env = os.environ.copy()
if environ:
env.update(environ)
if env_var and not IS_WINDOWS:
env['GIT_DIR'] = os.path.join(local_root, '.git')
else:
Expand Down Expand Up @@ -270,6 +273,8 @@ def fetch_commits(local_root, remotes):
def export(local_root, commit, target):
"""Export git commit to directory. "Extracts" all files at the commit to the target directory.
Set mtime of RST files to last commit date.
:raise CalledProcessError: Unhandled git command failure.
:param str local_root: Local path to git root directory.
Expand All @@ -278,6 +283,7 @@ def export(local_root, commit, target):
"""
log = logging.getLogger(__name__)
target = os.path.realpath(target)
mtimes = list()

# Define extract function.
def extract(stdout):
Expand All @@ -300,6 +306,8 @@ def extract(stdout):
queued_links.append(info)
else: # Handle files.
tar.extract(member=info, path=target)
if os.path.splitext(info.name)[1].lower() == '.rst':
mtimes.append(info.name)
for info in (i for i in queued_links if os.path.exists(os.path.join(target, i.linkname))):
tar.extract(member=info, path=target)
except tarfile.TarError as exc:
Expand All @@ -308,6 +316,11 @@ def extract(stdout):
# Run command.
run_command(local_root, ['git', 'archive', '--format=tar', commit], pipeto=extract)

# Set mtime.
for file_path in mtimes:
last_committed = int(run_command(local_root, ['git', 'log', '-n1', '--format=%at', commit, '--', file_path]))
os.utime(os.path.join(target, file_path), (last_committed, last_committed))


def clone(local_root, new_root, remote, branch, rel_dest, exclude):
"""Clone "local_root" origin into a new directory and check out a specific branch. Optionally run "git rm".
Expand Down
14 changes: 12 additions & 2 deletions sphinxcontrib/versioning/sphinx_.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
"""Interface with Sphinx."""

import datetime
import logging
import multiprocessing
import os
import sys

from sphinx import application, build_main
from sphinx import application, build_main, locale
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.config import Config as SphinxConfig
from sphinx.errors import SphinxError
from sphinx.jinja2glue import SphinxFileSystemLoader
from sphinx.util.i18n import format_date

from sphinxcontrib.versioning import __version__
from sphinxcontrib.versioning.lib import Config, HandledError, TempDir
Expand Down Expand Up @@ -83,7 +85,7 @@ def html_page_context(cls, app, pagename, templatename, context, doctree):
:param dict context: Jinja2 HTML context.
:param docutils.nodes.document doctree: Tree of docutils nodes.
"""
assert pagename or templatename or doctree # Unused, for linting.
assert templatename or doctree # Unused, for linting.
cls.VERSIONS.context = context
versions = cls.VERSIONS
this_remote = versions[cls.CURRENT_VERSION]
Expand Down Expand Up @@ -123,6 +125,14 @@ def html_page_context(cls, app, pagename, templatename, context, doctree):
if STATIC_DIR not in app.config.html_static_path:
app.config.html_static_path.append(STATIC_DIR)

# Reset last_updated with file's mtime (will be last git commit authored date).
if app.config.html_last_updated_fmt is not None:
file_path = app.env.doc2path(pagename)
if os.path.isfile(file_path):
lufmt = app.config.html_last_updated_fmt or getattr(locale, '_')('%b %d, %Y')
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
context['last_updated'] = format_date(lufmt, mtime, language=app.config.language, warn=app.warn)


def setup(app):
"""Called by Sphinx during phase 0 (initialization).
Expand Down
32 changes: 27 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""pytest fixtures for this directory."""

import datetime
import re
import time

import pytest

Expand All @@ -9,6 +11,24 @@

RE_BANNER = re.compile('>(?:<a href="([^"]+)">)?<b>Warning:</b> This document is for ([^<]+).(?:</a>)?</p>')
RE_URLS = re.compile('<li><a href="[^"]+">[^<]+</a></li>')
ROOT_TS = int(time.mktime((2016, 12, 5, 3, 17, 5, 0, 0, 0)))


def author_committer_dates(offset):
"""Return ISO time for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE environment variables.
Always starts on December 05 2016 03:17:05 AM local time. Committer date always 2 seconds after author date.
:param int offset: Minutes to offset both timestamps.
:return: GIT_AUTHOR_DATE and GIT_COMMITTER_DATE timestamps, can be merged into os.environ.
:rtype: dict
"""
dt = datetime.datetime.fromtimestamp(ROOT_TS) + datetime.timedelta(minutes=offset)
env = dict(GIT_AUTHOR_DATE=str(dt))
dt += datetime.timedelta(seconds=2)
env['GIT_COMMITTER_DATE'] = str(dt)
return env


def run(directory, command, *args, **kwargs):
Expand All @@ -32,6 +52,8 @@ def pytest_namespace():
:rtype: dict
"""
return dict(
author_committer_dates=author_committer_dates,
ROOT_TS=ROOT_TS,
run=run,
)

Expand Down Expand Up @@ -132,7 +154,7 @@ def fx_local_commit(local_empty):
"""
local_empty.join('README').write('Dummy readme file.')
run(local_empty, ['git', 'add', 'README'])
run(local_empty, ['git', 'commit', '-m', 'Initial commit.'])
run(local_empty, ['git', 'commit', '-m', 'Initial commit.'], environ=author_committer_dates(0))
return local_empty


Expand Down Expand Up @@ -191,12 +213,12 @@ def outdate_local(tmpdir, local_light, remote):
run(local_ahead, ['git', 'clone', remote, '.'])
run(local_ahead, ['git', 'checkout', '-b', 'un_pushed_branch'])
local_ahead.join('README').write('changed')
run(local_ahead, ['git', 'commit', '-am', 'Changed new branch'])
run(local_ahead, ['git', 'commit', '-am', 'Changed new branch'], environ=author_committer_dates(1))
run(local_ahead, ['git', 'tag', 'nb_tag'])
run(local_ahead, ['git', 'checkout', '--orphan', 'orphaned_branch'])
local_ahead.join('README').write('new')
run(local_ahead, ['git', 'add', 'README'])
run(local_ahead, ['git', 'commit', '-m', 'Added new README'])
run(local_ahead, ['git', 'commit', '-m', 'Added new README'], environ=author_committer_dates(2))
run(local_ahead, ['git', 'tag', '--annotate', '-m', 'Tag annotation.', 'ob_at'])
run(local_ahead, ['git', 'push', 'origin', 'nb_tag', 'orphaned_branch', 'ob_at'])
return local_ahead
Expand Down Expand Up @@ -248,7 +270,7 @@ def fx_local_docs(local):
'Sub page documentation 3.\n'
)
run(local, ['git', 'add', 'conf.py', 'contents.rst', 'one.rst', 'two.rst', 'three.rst'])
run(local, ['git', 'commit', '-m', 'Adding docs.'])
run(local, ['git', 'commit', '-m', 'Adding docs.'], environ=author_committer_dates(3))
run(local, ['git', 'push', 'origin', 'master'])
return local

Expand All @@ -263,7 +285,7 @@ def local_docs_ghp(local_docs):
run(local_docs, ['git', 'rm', '-rf', '.'])
local_docs.join('README').write('Orphaned branch for HTML docs.')
run(local_docs, ['git', 'add', 'README'])
run(local_docs, ['git', 'commit', '-m', 'Initial Commit'])
run(local_docs, ['git', 'commit', '-m', 'Initial Commit'], environ=author_committer_dates(4))
run(local_docs, ['git', 'push', 'origin', 'gh-pages'])
run(local_docs, ['git', 'checkout', 'master'])
return local_docs
50 changes: 50 additions & 0 deletions tests/test_git/test_export.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test function in module."""

import time
from datetime import datetime
from os.path import join
from subprocess import CalledProcessError

Expand Down Expand Up @@ -115,3 +117,51 @@ def test_symlink(tmpdir, local):
pytest.run(local, ['git', 'diff-index', '--quiet', 'HEAD', '--']) # Exit 0 if nothing changed.
files = sorted(f.relto(target) for f in target.listdir())
assert files == ['README', 'good_symlink']


def test_timezones(tmpdir, local):
"""Test mtime on RST files with different git commit timezones.
:param tmpdir: pytest fixture.
:param local: conftest fixture.
"""
files_dates = [
('local.rst', ''),
('UTC.rst', ' +0000'),
('PDT.rst', ' -0700'),
('PST.rst', ' -0800'),
]

# Commit files.
for name, offset in files_dates:
local.ensure(name)
pytest.run(local, ['git', 'add', name])
env = pytest.author_committer_dates(0)
env['GIT_AUTHOR_DATE'] += offset
env['GIT_COMMITTER_DATE'] += offset
pytest.run(local, ['git', 'commit', '-m', 'Added ' + name], environ=env)

# Run.
target = tmpdir.ensure_dir('target')
sha = pytest.run(local, ['git', 'rev-parse', 'HEAD']).strip()
export(str(local), sha, str(target))

# Validate.
actual = {i[0]: str(datetime.fromtimestamp(target.join(i[0]).mtime())) for i in files_dates}
if -time.timezone == -28800:
expected = {
'local.rst': '2016-12-05 03:17:05',
'UTC.rst': '2016-12-04 19:17:05',
'PDT.rst': '2016-12-05 02:17:05',
'PST.rst': '2016-12-05 03:17:05',
}
elif -time.timezone == 0:
expected = {
'local.rst': '2016-12-05 03:17:05',
'UTC.rst': '2016-12-05 03:17:05',
'PDT.rst': '2016-12-05 10:17:05',
'PST.rst': '2016-12-05 11:17:05',
}
else:
return pytest.skip('Need to add expected for {} timezone.'.format(-time.timezone))
assert actual == expected
4 changes: 1 addition & 3 deletions tests/test_git/test_filter_and_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

from sphinxcontrib.versioning.git import filter_and_date, GitError, list_remote

BEFORE = int(time.time())


def test_one_commit(local):
"""Test with one commit.
Expand All @@ -24,7 +22,7 @@ def test_one_commit(local):
# Test with existing conf_rel_path.
dates = filter_and_date(str(local), ['README'], [sha])
assert list(dates) == [sha]
assert dates[sha][0] >= BEFORE
assert dates[sha][0] >= pytest.ROOT_TS
assert dates[sha][0] < time.time()
assert dates[sha][1] == 'README'

Expand Down
48 changes: 48 additions & 0 deletions tests/test_routines/test_build_all.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test function in module."""

import re
from os.path import join

import pytest
Expand All @@ -9,6 +10,8 @@
from sphinxcontrib.versioning.routines import build_all, gather_git_info
from sphinxcontrib.versioning.versions import Versions

RE_LAST_UPDATED = re.compile(r'Last updated[^\n]+\n')


def test_single(tmpdir, local_docs, urls):
"""With single version.
Expand Down Expand Up @@ -273,6 +276,51 @@ def test_banner_tag(tmpdir, banner, config, local_docs, recent):
banner(dst.join(old, 'two.html'), '', 'an old version of Python')


def test_last_updated(tmpdir, local_docs):
"""Test last updated timestamp derived from git authored time.
:param tmpdir: pytest fixture.
:param local_docs: conftest fixture.
"""
local_docs.join('conf.py').write(
'html_last_updated_fmt = "%c"\n'
'html_theme="sphinx_rtd_theme"\n'
)
local_docs.join('two.rst').write('Changed\n', mode='a')
pytest.run(local_docs, ['git', 'commit', '-am', 'Changed two.'], environ=pytest.author_committer_dates(10))
pytest.run(local_docs, ['git', 'checkout', '-b', 'other', 'master'])
local_docs.join('three.rst').write('Changed\n', mode='a')
pytest.run(local_docs, ['git', 'commit', '-am', 'Changed three.'], environ=pytest.author_committer_dates(11))
pytest.run(local_docs, ['git', 'push', 'origin', 'master', 'other'])

versions = Versions(gather_git_info(str(local_docs), ['conf.py'], tuple(), tuple()))

# Export.
exported_root = tmpdir.ensure_dir('exported_root')
export(str(local_docs), versions['master']['sha'], str(exported_root.join(versions['master']['sha'])))
export(str(local_docs), versions['other']['sha'], str(exported_root.join(versions['other']['sha'])))

# Run.
destination = tmpdir.ensure_dir('destination')
build_all(str(exported_root), str(destination), versions)

# Verify master.
one = RE_LAST_UPDATED.findall(destination.join('master', 'one.html').read())
two = RE_LAST_UPDATED.findall(destination.join('master', 'two.html').read())
three = RE_LAST_UPDATED.findall(destination.join('master', 'three.html').read())
assert one == ['Last updated on Dec 5, 2016, 3:20:05 AM.\n']
assert two == ['Last updated on Dec 5, 2016, 3:27:05 AM.\n']
assert three == ['Last updated on Dec 5, 2016, 3:20:05 AM.\n']

# Verify other.
one = RE_LAST_UPDATED.findall(destination.join('other', 'one.html').read())
two = RE_LAST_UPDATED.findall(destination.join('other', 'two.html').read())
three = RE_LAST_UPDATED.findall(destination.join('other', 'three.html').read())
assert one == ['Last updated on Dec 5, 2016, 3:20:05 AM.\n']
assert two == ['Last updated on Dec 5, 2016, 3:27:05 AM.\n']
assert three == ['Last updated on Dec 5, 2016, 3:28:05 AM.\n']


@pytest.mark.parametrize('parallel', [False, True])
def test_error(tmpdir, config, local_docs, urls, parallel):
"""Test with a bad root ref. Also test skipping bad non-root refs.
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ passenv =
SSH_AUTH_SOCK
TRAVIS*
USER
usedevelop = False

[flake8]
exclude = .tox/*,build/*,docs/*,env/*,get-pip.py
Expand Down

0 comments on commit d79edd5

Please sign in to comment.