releases-0.7.0/000755 000765 000024 00000000000 12402217101 014255 5ustar00jforcierstaff000000 000000 releases-0.7.0/dev-requirements.txt000644 000765 000024 00000000325 12320311310 020311 0ustar00jforcierstaff000000 000000 # Task runner invoke>=0.6.0 invocations>=0.4.1 # Tests (N.B. integration suite also uses Invoke as above) spec>=0.11.1 mock==1.0.1 # Just for tests...heh six>=1.4.1 # Docs -e . sphinx>=1.1 sphinx_rtd_theme>=0.1.5 releases-0.7.0/docs/000755 000765 000024 00000000000 12402217101 015205 5ustar00jforcierstaff000000 000000 releases-0.7.0/LICENSE000644 000765 000024 00000002442 12260704106 015274 0ustar00jforcierstaff000000 000000 Copyright (c) 2014, Jeff Forcier All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. releases-0.7.0/MANIFEST.in000644 000765 000024 00000000265 12402211651 016022 0ustar00jforcierstaff000000 000000 include LICENSE include dev-requirements.txt include tasks.py recursive-include docs * recursive-exclude docs/_build * recursive-include tests * recursive-exclude tests *.pyc *.pyo releases-0.7.0/PKG-INFO000644 000765 000024 00000001675 12402217101 015363 0ustar00jforcierstaff000000 000000 Metadata-Version: 1.0 Name: releases Version: 0.7.0 Summary: A Sphinx extension for changelog manipulation Home-page: https://github.com/bitprophet/releases Author: Jeff Forcier Author-email: jeff@bitprophet.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules releases-0.7.0/README.rst000644 000765 000024 00000002670 12241471342 015763 0ustar00jforcierstaff000000 000000 .. image:: https://secure.travis-ci.org/bitprophet/releases.png?branch=master :target: https://travis-ci.org/bitprophet/releases What is Releases? ================= Releases is a `Sphinx `_ extension designed to help you keep a source control friendly, merge friendly changelog file & turn it into useful, human readable HTML output. Specifically: * The source format (kept in your Sphinx tree as ``changelog.rst``) is a stream-like timeline that plays well with source control & only requires one entry per change (even for changes that exist in multiple release lines). * The output (when you have the extension installed and run your Sphinx build command) is a traditional looking changelog page with a section for every release; multi-release issues are copied automatically into each release. * By default, feature and support issues are only displayed under feature releases, and bugs are only displayed under bugfix releases. This can be overridden on a per-issue basis. Some background on why this tool was created can be found in `this blog post `_. For more documentation, including detailed installation and usage information, please see http://releases.readthedocs.org. .. note:: You can install the `development version `_ via ``pip install releases==dev``. releases-0.7.0/releases/000755 000765 000024 00000000000 12402217101 016060 5ustar00jforcierstaff000000 000000 releases-0.7.0/releases.egg-info/000755 000765 000024 00000000000 12402217101 017552 5ustar00jforcierstaff000000 000000 releases-0.7.0/setup.cfg000644 000765 000024 00000000073 12402217101 016076 0ustar00jforcierstaff000000 000000 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 releases-0.7.0/setup.py000644 000765 000024 00000002265 12243514510 016003 0ustar00jforcierstaff000000 000000 #!/usr/bin/env python from setuptools import setup # Version info -- read without importing _locals = {} with open('releases/_version.py') as fp: exec(fp.read(), None, _locals) version = _locals['__version__'] setup( name='releases', version=version, description='A Sphinx extension for changelog manipulation', author='Jeff Forcier', author_email='jeff@bitprophet.org', url='https://github.com/bitprophet/releases', packages=['releases'], classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Unix', 'Operating System :: POSIX', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Topic :: Software Development', 'Topic :: Software Development :: Build Tools', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) releases-0.7.0/tasks.py000644 000765 000024 00000000544 12343415221 015766 0ustar00jforcierstaff000000 000000 from invocations import docs from invocations.testing import test from invocations.packaging import release from invoke import Collection from invoke import run from invoke import task @task() def integration_tests(): """Runs integration tests.""" run('inv test -o --tests=integration') ns = Collection(test, integration_tests, release, docs) releases-0.7.0/tests/000755 000765 000024 00000000000 12402217101 015417 5ustar00jforcierstaff000000 000000 releases-0.7.0/tests/changelog.py000644 000765 000024 00000041652 12343641356 017751 0ustar00jforcierstaff000000 000000 from tempfile import mkdtemp from shutil import rmtree import six from spec import Spec, skip, eq_, raises from mock import Mock from docutils.nodes import ( reference, bullet_list, list_item, title, raw, paragraph, Text, section, ) from docutils.utils import new_document from sphinx.application import Sphinx from releases import ( Issue, issues_role, Release, release_role, construct_releases, construct_nodes, generate_changelog, ) from releases import setup as releases_setup # avoid unittest crap def _app(**kwargs): # Create a real Sphinx app, with stupid temp dirs because it assumes. # Helps catch things like "testing a config option but forgot # app.add_config_value()" src, doctree = mkdtemp(), mkdtemp() try: # STFU Sphinx :( Sphinx._log = lambda self, message, wfile, nonl=False: None app = Sphinx( srcdir=src, confdir=None, outdir=None, doctreedir=doctree, buildername='html', ) finally: [rmtree(x) for x in (src, doctree)] releases_setup(app) # Mock out the config within. More horrible assumptions by Sphinx :( config = { 'releases_release_uri': 'foo_%s', 'releases_issue_uri': 'bar_%s', 'releases_debug': False, } # Allow tinkering with document filename if 'docname' in kwargs: app.env.temp_data['docname'] = kwargs.pop('docname') # Allow config overrides for name in kwargs: config['releases_{0}'.format(name)] = kwargs[name] # Stitch together as the sphinx app init() usually does w/ real conf files app.config._raw_config = config app.config.init_values() return app def _inliner(app=None): app = app or _app() return Mock(document=Mock(settings=Mock(env=Mock(app=app)))) # Obtain issue() object w/o wrapping all parse steps def _issue(type_, number, **kwargs): text = str(number) if kwargs.get('backported', False): text += " backported" if kwargs.get('major', False): text += " major" if kwargs.get('line', None): text += " (%s+)" % kwargs['line'] app = kwargs.get('app', None) return issues_role( name=type_, rawtext='', text=text, lineno=None, inliner=_inliner(app=app), )[0][0] def _entry(i): """ Easy wrapper for issue/release objects. Default is to give eg an issue/release object that gets wrapped in a LI->P. May give your own (non-issue/release) object to skip auto wrapping. (Useful since _entry() is often called a few levels deep.) """ if not isinstance(i, (Issue, Release)): return i return list_item('', paragraph('', '', i)) def _release(number, **kwargs): app = kwargs.get('app', None) nodes = release_role( name=None, rawtext='', text='%s <2013-11-20>' % number, lineno=None, inliner=_inliner(app=app), )[0] return list_item('', paragraph('', '', *nodes)) def _release_list(*entries): entries = list(entries) # lol tuples # Translate simple objs into changelog-friendly ones for index, item in enumerate(entries): if isinstance(item, six.string_types): entries[index] = _release(item) else: entries[index] = _entry(item) # Insert initial/empty 1st release to start timeline entries.append(_release('1.0.0')) return entries def _changelog2dict(changelog): d = {} for r in changelog: d[r['obj'].number] = r['entries'] return d def _releases(*entries, **kwargs): app = kwargs.get('app', None) or _app() return construct_releases(_release_list(*entries), app) def _setup_issues(self): self.f = _issue('feature', '12') self.s = _issue('support', '5') self.b = _issue('bug', '15') self.mb = _issue('bug', '200', major=True) self.bf = _issue('feature', '27', backported=True) self.bs = _issue('support', '29', backported=True) class releases(Spec): """ Organization of issues into releases (parsing) """ def setup(self): _setup_issues(self) def _expect_entries(self, all_entries, in_, not_in): # Grab 2nd release as 1st is the empty 'beginning of time' one entries = _releases(*all_entries)[1]['entries'] eq_(len(entries), len(in_)) for x in in_: assert x in entries for x in not_in: assert x not in entries def feature_releases_include_features_and_support_not_bugs(self): self._expect_entries( ['1.1.0', self.f, self.b, self.s], [self.f, self.s], [self.b] ) def feature_releases_include_major_bugs(self): self._expect_entries( ['1.1.0', self.f, self.b, self.mb], [self.f, self.mb], [self.b] ) def bugfix_releases_include_bugs(self): self._expect_entries( ['1.0.2', self.f, self.b, self.mb], [self.b], [self.mb, self.f], ) def bugfix_releases_include_backported_features(self): self._expect_entries( ['1.0.2', self.bf, self.b, self.s], [self.b, self.bf], [self.s] ) def bugfix_releases_include_backported_support(self): self._expect_entries( ['1.0.2', self.f, self.b, self.s, self.bs], [self.b, self.bs], [self.s, self.f] ) def unmarked_bullet_list_items_treated_as_bugs(self): fake = list_item('', paragraph('', '', raw('', 'whatever'))) releases = _releases('1.0.2', self.f, fake) entries = releases[1]['entries'] eq_(len(entries), 1) assert self.f not in entries assert isinstance(entries[0], Issue) eq_(entries[0].number, None) def unreleased_items_go_in_unreleased_releases(self): releases = _releases(self.f, self.b) # Should have two unreleased lists, one feature w/ feature, one bugfix # w/ bugfix. bugfix, feature = releases[1:] eq_(len(feature['entries']), 1) eq_(len(bugfix['entries']), 1) assert self.f in feature['entries'] assert self.b in bugfix['entries'] eq_(feature['obj'].number, 'unreleased_feature') eq_(bugfix['obj'].number, 'unreleased_bugfix') def issues_consumed_by_releases_are_not_in_unreleased(self): releases = _releases('1.0.2', self.f, self.b, self.s, self.bs) release = releases[1]['entries'] unreleased = releases[-1]['entries'] assert self.b in release assert self.b not in unreleased def oddly_ordered_bugfix_releases_and_unreleased_list(self): # Release set up w/ non-contiguous feature+bugfix releases; catches # funky problems with 'unreleased' buckets b2 = _issue('bug', '2') f3 = _issue('feature', '3') changelog = _releases( '1.1.1', '1.0.2', self.f, b2, '1.1.0', f3, self.b ) assert f3 in changelog[1]['entries'] assert b2 in changelog[2]['entries'] assert b2 in changelog[3]['entries'] def release_line_bugfix_specifier(self): b50 = _issue('bug', '50') b42 = _issue('bug', '42', line='1.1') f25 = _issue('feature', '25') b35 = _issue('bug', '35') b34 = _issue('bug', '34') f22 = _issue('feature', '22') b20 = _issue('bug', '20') c = _changelog2dict(_releases( '1.2.1', '1.1.2', '1.0.3', b50, b42, '1.2.0', '1.1.1', '1.0.2', f25, b35, b34, '1.1.0', '1.0.1', f22, b20 )) for rel, issues in ( ('1.0.1', [b20]), ('1.1.0', [f22]), ('1.0.2', [b34, b35]), ('1.1.1', [b34, b35]), ('1.2.0', [f25]), ('1.0.3', [b50]), # the crux - is not b50 + b42 ('1.1.2', [b50, b42]), ('1.2.1', [b50, b42]), ): eq_(set(c[rel]), set(issues)) def releases_can_specify_issues_explicitly(self): # Build regular list-o-entries b2 = _issue('bug', '2') b3 = _issue('bug', '3') changelog = _release_list( '1.0.1', '1.1.1', b3, b2, self.b, '1.1.0', self.f ) # Modify 1.0.1 release to be speshul changelog[0][0].append(Text("2, 3")) rendered = construct_releases(changelog, _app()) # 1.0.1 includes just 2 and 3, not bug 1 one_0_1 = rendered[3]['entries'] one_1_1 = rendered[2]['entries'] assert self.b not in one_0_1 assert b2 in one_0_1 assert b3 in one_0_1 # 1.1.1 includes all 3 (i.e. the explicitness of 1.0.1 didn't affect # the 1.1 line bucket.) assert self.b in one_1_1 assert b2 in one_1_1 assert b3 in one_1_1 def explicit_release_list_split_works_with_unicode(self): b = _issue('bug', '17') changelog = _release_list('1.0.1', b) changelog[0][0].append(Text(six.text_type('17'))) # When using naive method calls, this explodes construct_releases(changelog, _app()) def explicit_feature_release_features_are_removed_from_unreleased(self): f1 = _issue('feature', '1') f2 = _issue('feature', '2') changelog = _release_list('1.1.0', f1, f2) # Ensure that 1.1.0 specifies feature 2 changelog[0][0].append(Text("2")) rendered = _changelog2dict(construct_releases(changelog, _app())) # 1.1.0 should have feature 2 only assert f2 in rendered['1.1.0'] assert f1 not in rendered['1.1.0'] # unreleased feature list should still get/see feature 1 assert f1 in rendered['unreleased_feature'] # now-released feature 2 should not be in unreleased_feature assert f2 not in rendered['unreleased_feature'] def explicit_bugfix_releases_get_removed_from_unreleased(self): b1 = _issue('bug', '1') b2 = _issue('bug', '2') changelog = _release_list('1.0.1', b1, b2) # Ensure that 1.0.1 specifies bug 2 changelog[0][0].append(Text('2')) rendered = construct_releases(changelog, _app()) # 1.0.1 should have bug 2 only assert b2 in rendered[1]['entries'] assert b1 not in rendered[1]['entries'] # unreleased bug list should still get/see bug 1 assert b1 in rendered[2]['entries'] @raises(ValueError) def explicit_releases_error_on_unfound_issues(self): # Just a release - result will have 1.0.0, 1.0.1, and unreleased changelog = _release_list('1.0.1') # No issues listed -> this clearly doesn't exist in any buckets changelog[1][0].append(Text("25")) # This should asplode construct_releases(changelog, _app()) def duplicate_issue_numbers_adds_two_issue_items(self): test_changelog = _releases('1.0.1', self.b, self.b) test_changelog = _changelog2dict(test_changelog) eq_(len(test_changelog['1.0.1']), 2) def duplicate_zeroes_dont_error(self): cl = _releases('1.0.1', _issue('bug', '0'), _issue('bug', '0')) cl = _changelog2dict(cl) assert len(cl['1.0.1']) == 2 def _obj2name(obj): cls = obj if isinstance(obj, type) else obj.__class__ return cls.__name__.split('.')[-1] def _expect_type(node, cls): type_ = _obj2name(node) name = _obj2name(cls) msg = "Expected %r to be a %s, but it's a %s" % (node, name, type_) assert isinstance(node, cls), msg class nodes(Spec): """ Expansion/extension of docutils nodes (rendering) """ def setup(self): _setup_issues(self) def _generate(self, *entries, **kwargs): app = kwargs.get('app', None) nodes = construct_nodes(_releases(*entries, app=app)) # By default, yield the contents of the bullet list. return nodes if kwargs.get('raw', False) else nodes[0][1][0] def _test_link(self, kwargs, type_, expected): app = _app(**kwargs) nodes = construct_nodes(construct_releases([ _release('1.0.2', app=app), _entry(_issue('bug', 15, app=app)), _release('1.0.0'), ], app=app)) if type_ == 'release': header = nodes[0][0][0].astext() assert expected in header elif type_ == 'issue': link = nodes[0][1][0][0][2] eq_(link['refuri'], expected) else: raise Exception("Gave unknown type_ kwarg to _test_link()!") def issues_with_numbers_appear_as_number_links(self): self._test_link({}, 'issue', 'bar_15') def releases_appear_as_header_links(self): self._test_link({}, 'release', 'foo_1.0.2') def links_will_use_github_option_if_defined(self): kwargs = { 'release_uri': None, 'issue_uri': None, 'github_path': 'foo/bar', } for type_, expected in ( ('issue', 'https://github.com/foo/bar/issues/15'), ('release', 'https://github.com/foo/bar/tree/1.0.2'), ): self._test_link(kwargs, type_, expected) def issue_links_prefer_explicit_setting_over_github_setting(self): kwargs = { 'release_uri': None, 'issue_uri': 'explicit_issue_%s', 'github_path': 'foo/bar', } self._test_link(kwargs, 'issue', 'explicit_issue_15') def release_links_prefer_explicit_setting_over_github_setting(self): kwargs = { 'release_uri': 'explicit_release_%s', 'issue_uri': None, 'github_path': 'foo/bar', } self._test_link(kwargs, 'release', 'explicit_release_1.0.2') def _assert_prefix(self, entries, expectation): assert expectation in self._generate(*entries)[0][0][0] def bugs_marked_as_bugs(self): self._assert_prefix(['1.0.2', self.b], 'Bug') def features_marked_as_features(self): self._assert_prefix(['1.1.0', self.f], 'Feature') def support_marked_as_support(self): self._assert_prefix(['1.1.0', self.s], 'Support') def dashed_issues_appear_as_unlinked_issues(self): node = self._generate('1.0.2', _issue('bug', '-')) assert not isinstance(node[0][2], reference) def zeroed_issues_appear_as_unlinked_issues(self): node = self._generate('1.0.2', _issue('bug', '0')) assert not isinstance(node[0][2], reference) def un_prefixed_list_items_appear_as_unlinked_bugs(self): fake = list_item('', paragraph('', '', raw('', 'whatever'))) node = self._generate('1.0.2', fake) assert 'Bug' in str(node[0][0]) assert 'whatever' in str(node[0][3]) def issues_remain_wrapped_in_unordered_list_nodes(self): node = self._generate('1.0.2', self.b, raw=True)[0][1] _expect_type(node, bullet_list) _expect_type(node[0], list_item) def release_headers_have_local_style_tweaks(self): node = self._generate('1.0.2', self.b, raw=True)[0][0] _expect_type(node, raw) # Header w/ bottom margin assert '

: ". May give a 'ticket number' of ' backported' to indicate a backported feature or support ticket. This extra info will be stripped out prior to parsing. May also give 'major' in the same vein, implying the bug was a major bug released in a feature release. May give a 'ticket number' of ``-`` or ``0`` to generate no hyperlink. """ # Old-style 'just the issue link' behavior issue_no, _, ported = utils.unescape(text).partition(' ') # Lol @ access back to Sphinx config = inliner.document.settings.env.app.config if issue_no not in ('-', '0'): if config.releases_issue_uri: # TODO: deal with % vs .format() ref = config.releases_issue_uri % issue_no elif config.releases_github_path: ref = "https://github.com/{0}/issues/{1}".format( config.releases_github_path, issue_no) link = nodes.reference(rawtext, '#' + issue_no, refuri=ref, **options) else: link = None issue_no = None # So it doesn't gum up dupe detection later # Additional 'new-style changelog' stuff if name in ISSUE_TYPES: nodelist = issue_nodelist(name, link) line = None # Sanity check if ported not in ('backported', 'major', ''): match = release_line_re.match(ported) if not match: raise ValueError("Gave unknown issue metadata '%s' for issue no. %s" % (ported, issue_no)) else: line = match.groups()[0] # Create temporary node w/ data & final nodes to publish node = Issue( number=issue_no, type_=name, nodelist=nodelist, backported=(ported == 'backported'), major=(ported == 'major'), line=line, ) return [node], [] # Return old style info for 'issue' for older changelog entries else: return [link], [] def release_nodes(text, slug, date, config): # Doesn't seem possible to do this "cleanly" (i.e. just say "make me a # title and give it these HTML attributes during render time) so...fuckit. # We were already doing fully raw elements elsewhere anyway. And who cares # about a PDF of a changelog? :x if config.releases_release_uri: # TODO: % vs .format() uri = config.releases_release_uri % slug elif config.releases_github_path: uri = "https://github.com/{0}/tree/{1}".format( config.releases_github_path, slug) link = '{1}'.format(uri, text) datespan = '' if date: datespan = ' {0}'.format(date) header = '

{0}{1}

'.format(link, datespan) return nodes.section('', nodes.raw(rawtext='', text=header, format='html'), ids=[text] ) year_arg_re = re.compile(r'^(.+?)\s*(?$', re.DOTALL) def release_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """ Invoked as :release:`N.N.N `. Turns into useful release header + link to GH tree for the tag. """ # Make sure year has been specified match = year_arg_re.match(text) if not match: msg = inliner.reporter.error("Must specify release date!") return [inliner.problematic(rawtext, rawtext, msg)], [msg] number, date = match.group(1), match.group(2) # Lol @ access back to Sphinx config = inliner.document.settings.env.app.config nodelist = [release_nodes(number, number, date, config)] # Return intermediate node node = Release(number=number, date=date, nodelist=nodelist) return [node], [] def get_line(obj): # 1.2.7 -> 1.2 return '.'.join(obj.number.split('.')[:-1]) def append_unreleased_entries(app, lines, releases): """ Entries not yet released get special 'release' entries (that lack an actual release object). """ log = partial(_log, config=app.config) for which in ('bugfix', 'feature'): nodelist = [release_nodes( "Next %s release" % which, 'master', None, app.config )] line = 'unreleased_%s' % which log("Creating '%s' faux-release with %r" % (line, lines[line])) releases.append({ 'obj': Release(number=line, date=None, nodelist=nodelist), 'entries': lines[line] }) def construct_entry_with_release(focus, issues, lines, log, releases, rest): """ Releases 'eat' the entries in their line's list and get added to the final data structure. They also inform new release-line 'buffers'. Release lines, once the release obj is removed, should be empty or a comma-separated list of issue numbers. """ line = get_line(focus) log("release for line %r" % line) # Check for explicitly listed issues first explicit = None if rest[0].children: explicit = [x.strip() for x in rest[0][0].split(',')] # Do those by themselves since they override all other logic if explicit: log("Explicit issues requested: %r" % (explicit,)) # First scan global issue dict, dying if not found missing = [i for i in explicit if i not in issues] if missing: raise ValueError( "Couldn't find issue(s) #{0} in the changelog!".format( ', '.join(missing))) # Obtain objects from global list entries = [] for i in explicit: for flattened_issue_item in itertools.chain(issues[i]): entries.append(flattened_issue_item) # Create release log("entries in this release: %r" % (entries,)) releases.append({ 'obj': focus, 'entries': entries, }) # Introspect entries to determine which buckets they should get # removed from for obj in entries: if obj.type == 'bug': # Major bugfix: remove from unreleased_feature if obj.major: log("Removing #%s from unreleased" % obj.number) lines['unreleased_feature'].remove(obj) # Regular bugfix: remove from bucket for this release's # line + unreleased_bugfix else: if obj in lines['unreleased_bugfix']: log("Removing #%s from unreleased" % obj.number) lines['unreleased_bugfix'].remove(obj) if obj in lines[line]: log("Removing #%s from %s" % (obj.number, line)) lines[line].remove(obj) # Regular feature/support: remove from unreleased_feature # Backported feature/support: remove from bucket for this # release's line (if applicable) + unreleased_feature else: log("Removing #%s from unreleased" % obj.number) lines['unreleased_feature'].remove(obj) if obj in lines.get(line, []): lines[line].remove(obj) # Implicit behavior otherwise else: # New release line/branch detected. Create it & dump unreleased # features. if line not in lines: log("not seen prior, making feature release") lines[line] = [] entries = [ x for x in lines['unreleased_feature'] if x.type in ('feature', 'support') or x.major ] releases.append({ 'obj': focus, 'entries': entries }) lines['unreleased_feature'] = [] # Existing line -> empty out its bucket into new release. # Skip 'major' bugs as those "belong" to the next release (and will # also be in 'unreleased_feature' - so safe to nuke the entire # line) else: log("pre-existing, making bugfix release") entries = [x for x in lines[line] if not x.major] log("entries in this release: %r" % (entries,)) releases.append({ 'obj': focus, 'entries': entries, }) lines[line] = [] # Clean out the items we just released from # 'unreleased_bugfix'. (Can't nuke it because there might # be some unreleased bugs for other release lines.) for x in entries: if x in lines['unreleased_bugfix']: lines['unreleased_bugfix'].remove(x) def construct_entry_without_release(focus, issues, lines, log, rest): # Handle rare-but-valid non-issue-attached line items, which are # always bugs. (They are their own description.) if not isinstance(focus, Issue): # First, sanity check for potential mistakes resulting in an issue node # being buried within something else. buried = focus.traverse(Issue) if buried: msg = """ Found issue node ({0!r}) buried inside another node: {1} Please double-check your ReST syntax! There is probably text in the above output that will show you which part of your changelog to look at. For example, indentation problems can accidentally generate nested definition lists. """ raise ValueError(msg.format(buried[0], str(buried[0].parent))) # OK, it looks legit - make it a bug. log("Found line item w/ no real issue object, creating bug") focus = Issue( type_='bug', nodelist=issue_nodelist('bug'), description=nodes.list_item('', nodes.paragraph('', '', focus)), ) else: focus.attributes['description'] = rest # Add to global list or die trying issues[focus.number] = issues.get(focus.number, []) + [focus] if focus.type == 'bug': # Major bugs go into unreleased_feature if focus.major: lines['unreleased_feature'].append(focus) log("Adding to unreleased_feature") # Regular bugs go into per-line buckets ('major' bugs do # not) as well as unreleased_bugfix. Adjust for bugs with a # 'line' (minimum line no.) attribute. else: bug_lines = [x for x in lines if x != 'unreleased_feature'] if focus.line: bug_lines = [x for x in bug_lines if (x != 'unreleased_bugfix' and LooseVersion(x) >= LooseVersion(focus.line))] bug_lines = bug_lines + ['unreleased_bugfix'] for line in bug_lines: log("Adding to %r" % line) lines[line].append(focus) else: # Backported feature/support items go into all lines, including # both 'unreleased' lists if focus.backported: for line in lines: log("Adding to release line %r" % line) lines[line].append(focus) # Non-backported feature/support items go into feature releases # only. else: log("Adding to unreleased_feature") lines['unreleased_feature'].append(focus) def construct_releases(entries, app): log = partial(_log, config=app.config) # Walk from back to front, consuming entries & copying them into # per-release buckets as releases are encountered. Store releases in order. releases = [] lines = {'unreleased_bugfix': [], 'unreleased_feature': []} # Also keep a master hash of issues by number to detect duplicates & assist # in explicitly defined release lists. issues = {} for obj in reversed(entries): # Issue object is always found in obj (LI) index 0 (first, often only # P) and is the 1st item within that (index 0 again). # Preserve all other contents of 'obj'. focus = obj[0].pop(0) rest = obj log(repr(focus)) # Releases 'eat' the entries in their line's list and get added to the # final data structure. They also inform new release-line 'buffers'. # Release lines, once the release obj is removed, should be empty or a # comma-separated list of issue numbers. if isinstance(focus, Release): construct_entry_with_release(focus, issues, lines, log, releases, rest) # Entries get copied into release line buckets as follows: # * Features and support go into 'unreleased_feature' for use in new # feature releases. # * Bugfixes go into all release lines (so they can be printed in >1 # bugfix release as appropriate) as well as 'unreleased_bugfix' (so # they can be displayed prior to release'). Caveats include bugs marked # 'major' (they go into unreleased_feature instead) or with 'N.N+' # (meaning they only go into release line buckets for that release and # up.) # * Support/feature entries marked as 'backported' go into all # release lines as well, on the assumption that they were released to # all active branches. # * The 'rest' variable (which here is the bug description, vitally # important!) is preserved by stuffing it into the focus (issue) # object - it will get unpacked by construct_nodes() later. else: construct_entry_without_release(focus, issues, lines, log, rest) append_unreleased_entries(app, lines, releases) return releases def construct_nodes(releases): result = [] # Reverse the list again so the final display is newest on top for d in reversed(releases): if not d['entries']: continue obj = d['obj'] entries = [] for entry in d['entries']: # Use nodes.Node.deepcopy to deepcopy the description # node. If this is not done, multiple references to the same # object (e.g. a reference object in the description of #649, which # is then copied into 2 different release lists) will end up in the # doctree, which makes subsequent parse steps very angry (index() # errors). desc = entry['description'].deepcopy() # Additionally, expand any other issue roles found in the # description - sometimes we refer to related issues inline. (They # can't be left as issue() objects at render time since that's # undefined.) # Use [:] slicing to avoid mutation during the loops. for index, node in enumerate(desc[:]): for subindex, subnode in enumerate(node[:]): if isinstance(subnode, Issue): desc[index][subindex:subindex+1] = subnode['nodelist'] # Rework this entry to insert the now-rendered issue nodes in front # of the 1st paragraph of the 'description' nodes (which should be # the preserved LI + nested paragraph-or-more from original # markup.) # FIXME: why is there no "prepend a list" method? for node in reversed(entry['nodelist']): desc[0].insert(0, node) entries.append(desc) # Entry list list_ = nodes.bullet_list('', *entries) # Insert list into release nodelist (as it's a section) obj['nodelist'][0].append(list_) # Release header header = nodes.paragraph('', '', *obj['nodelist']) result.extend(header) return result def generate_changelog(app, doctree): if app.env.docname != app.config.releases_document_name: return # Second item inside main document is the 'modern' changelog bullet-list # object, whose children are the nodes we care about. source = doctree[0] changelog = source.children.pop(1) # Walk + parse into release mapping releases = construct_releases(changelog.children, app) # Construct new set of nodes to replace the old, and we're done source[1:1] = construct_nodes(releases) def setup(app): # Issue base URI setting: releases_issue_uri # E.g. 'https://github.com/fabric/fabric/issues/' app.add_config_value(name='releases_issue_uri', default=None, rebuild='html') # Release-tag base URI setting: releases_release_uri # E.g. 'https://github.com/fabric/fabric/tree/' app.add_config_value(name='releases_release_uri', default=None, rebuild='html') # Convenience Github version of above app.add_config_value(name='releases_github_path', default=None, rebuild='html') # Which document to use as the changelog app.add_config_value(name='releases_document_name', default='changelog', rebuild='html') # Debug output app.add_config_value(name='releases_debug', default=False, rebuild='html') # Register intermediate roles for x in list(ISSUE_TYPES) + ['issue']: app.add_role(x, issues_role) app.add_role('release', release_role) # Hook in our changelog transmutation at appropriate step app.connect('doctree-read', generate_changelog) releases-0.7.0/releases/_version.py000644 000765 000024 00000000120 12402215400 020247 0ustar00jforcierstaff000000 000000 __version_info__ = (0, 7, 0) __version__ = '.'.join(map(str, __version_info__)) releases-0.7.0/releases/models.py000644 000765 000024 00000002202 12343415221 017720 0ustar00jforcierstaff000000 000000 from docutils import nodes # Issue type list (keys) + color values ISSUE_TYPES = { 'bug': 'A04040', 'feature': '40A056', 'support': '4070A0', } class Issue(nodes.Element): @property def type(self): return self['type_'] @property def backported(self): return self.get('backported', False) @property def major(self): return self.get('major', False) @property def number(self): return self.get('number', None) @property def line(self): return self.get('line', None) def __repr__(self): flag = '' if self.backported: flag = 'backported' elif self.major: flag = 'major' elif self.line: flag = self.line + '+' if flag: flag = ' ({0})'.format(flag) return '<{issue.type} #{issue.number}{flag}>'.format(issue=self, flag=flag) class Release(nodes.Element): @property def number(self): return self['number'] def __repr__(self): return ''.format(self.number) releases-0.7.0/docs/changelog.rst000644 000765 000024 00000010347 12402215367 017707 0ustar00jforcierstaff000000 000000 ========= Changelog ========= * :release:`0.7.0 <2014-09-04>` * :bug:`30 major` Add LICENSE (plus a handful of other administrative files) to a ``MANIFEST.in`` so sdists pick it up. Thanks to Zygmunt Krynicki for catch & original patch (:issue:`33`). * :feature:`21` Allow duplicate issue numbers; not allowing them was technically an implementation detail. Thanks to Dorian Puła for the patch. * :release:`0.6.1 <2014-04-06>` * :bug:`-` Fix a silly issue with the new feature from :issue:`22` where it accidentally referred to the Sphinx document *title* instead of the document *filename*. * :release:`0.6.0 <2014-04-03>` * :feature:`22` Make the document name used as the changelog - previously hardcoded as ``changelog`` (``.rst``) - configurable. Thanks to James Mills for the feature request. * :feature:`26` Allow specifying Github path shorthand config option instead of explicit release/issue URL strings. * :release:`0.5.3 <2014-03-15>` * :bug:`25` Empty/no-issue line items broke at some point; fixed. * :bug:`24` Broke inline issue parsing; fixed now. * :release:`0.5.2 <2014-03-13>` * :bug:`23` Rework implementation to deal with issue descriptions that span more than one paragraph - subsequent paragraphs/blocks were not being displayed prior. * :release:`0.5.1 <2014-02-11>` * :bug:`-` Fix silly bug in :issue:`20` that cropped up on Python 3.x. * :release:`0.5.0 <2014-02-11>` * :feature:`20` Allow specifying minimum release line in bugfixes that don't apply to all active lines (e.g. because they pertain to a recently added feature.) * :release:`0.4.0 <2013-12-24>` * :feature:`17` Allow releases to explicitly define which issues they include. Useful for overriding default assumptions (e.g. a special bugfix release from an otherwise dormant line.) * :release:`0.3.1 <2013-12-18>` * :bug:`16` Fix some edge cases regarding release ordering & unreleased issue display. Includes splitting unreleased display info into two 'Next release' pseudo-release entries. * :support:`15` Add :doc:`/concepts` to flesh out some assumptions not adequately explained in :doc:`/usage`. * :release:`0.3.0 <2013-11-21>` * :feature:`11` Fix up styling so changelogs don't look suboptimal under `the new Read The Docs theme `_. Still looks OK under their old theme too! * :support:`0` Move to actual Sphinx docs so we can use ourselves. * :support:`0` Created a basic test suite to protect against regressions. * :bug:`9 major` Clean up additional 'unreleased' display/organization behavior, including making sure ALL unreleased issues show up as 'unreleased'. Thanks to Donald Stufft for the report. * :feature:`1` (also :issue:`3`, :issue:`10`) Allow using ``-`` or ``0`` as a dummy issue 'number', which will result in no issue number/link being displayed. Thanks to Markus Zapke-Gründemann and Hynek Schlawack for patches & discussion. * This feature lets you categorize changes that aren't directly related to issues in your tracker. It's an improvement over, and replacement for, the previous "vanilla bullet list items are treated as bugs" behavior. * Said behavior (non-role-prefixed bullet list items turning into regular bugs) is being retained as there's not a lot to gain from deactivating it. * :release:`0.2.4 <2013.10.04>` * :support:`0 backported` Handful of typos, doc tweaks & addition of a .gitignore file. Thanks to Markus Zapke-Gründemann. * :bug:`0` Fix duplicate display of "bare" (not prefixed with an issue role) changelog entries. Thanks again to Markus. * :support:`0 backported` Edited the README/docs to be clearer about how Releases works/operates. * :support:`0 backported` Explicitly documented how non-role-prefixed line items are preserved. * :bug:`0` Updated non-role-prefixed line items so they get prefixed with a '[Bug]' signifier (since they are otherwise treated as bugfix items.) * :release:`0.2.3 <2013.09.16>` * :bug:`0` Fix a handful of bugs in release assignment logic. * :release:`0.2.2 <2013.09.15>` * :bug:`0` Ensured Python 3 compatibility. * :release:`0.2.1 <2013.09.15>` * :bug:`0` Fixed a stupid bug causing invalid issue hyperlinks. * :release:`0.2.0 <2013.09.15>` * :feature:`0` Basic functionality. releases-0.7.0/docs/concepts.rst000644 000765 000024 00000016225 12276521173 017603 0ustar00jforcierstaff000000 000000 ======== Concepts ======== Basic conceptual info about how Releases organizes and thinks about issues and releases. For details on formatting/etc (e.g. so you can interpret the examples below), see :doc:`/usage`. Issue and release types ======================= * Issues are always one of three types: **features**, **bug fixes** or **support items**. * **Features** are (typically larger) changes adding new behavior. * **Bug fixes** are (typically minor) changes addressing incorrect behavior, crashes, etc. * **Support items** vary in size but are usually non-code-related changes, such as documentation or packaging updates. * Releases also happen to come in three flavors: * **Major releases** are backwards incompatible releases, often with large/sweeping changes to a codebase. * They increment the first version number only, e.g. ``1.0.0``. * **Feature releases** (sometimes called **minor** or **secondary**) are backwards compatible with the previous major release, and focus on adding new functionality (code, or support, or both.) They sometimes include major/complex bug fixes which are too risky to include in a bugfix release. * The second version number is incremented for these, e.g. ``1.1.0``. * **Bugfix releases** (sometimes called **tertiary**) focus on fixing incorrect behavior while minimizing the risk of creating more bugs. Rarely, they will include small new features deemed important enough to backport from their 'native' feature release. * These releases increment the third/final version number, e.g. ``1.1.1``. Release organization ==================== We parse changelog timelines so the resulting per-release issue lists honor the above descriptions. Here are the core rules, with examples. See :doc:`/usage` for details on formatting/etc. * **By default, bugfixes go into bugfix releases, features and support items go into feature releases.** * Input:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :support:`4` Updated our test runner * :bug:`3` Another bugfix * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.0.1``: bug #1, bug #3 * ``1.1.0``: feature #2, support #4 * **Bugfixes are assumed to backport to all stable release lines by default, and are displayed as such.** However, this can be overridden on a per-release and/or per-bug basis - see later bullet points. * Input:: * :release:`1.1.1 ` * :release:`1.0.2 ` * :bug:`3` Fixed another bug, onoes * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.0.1``: bug #1 * ``1.1.0``: feature #2 * ``1.0.2``: bug #3 * ``1.1.1``: bug #3 * **Bugfixes marked 'major' go into feature releases instead.** * Input:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :bug:`3 major` Big bugfix with lots of changes * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.0.1``: bug #1 * ``1.1.0``: feature #2, bug #3 * **Features or support items marked 'backported' appear in both bugfix and feature releases.** * Input:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :bug:`4` Fixed another bug * :feature:`3` Regular feature * :feature:`2 backported` Small new feature worth backporting * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.0.1``: bug #1, feature #2, bug #4 * ``1.1.0``: feature #2, feature #3 * **Releases implicitly include all issues from their own, and prior, release lines.** (Again, unless the release explicitly states otherwise - see below.) * For example, in the below changelog (remembering that changelogs are written in descending order from newest to oldest entry) the code released as ``1.1.0`` includes the changes from bugs #1 and #3, in addition to its explicitly stated contents of feature #2:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :bug:`3` Another bugfix * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Again, to be explicit, the rendered changelog displays this breakdown: * ``1.0.1``: bug #1, bug #3 * ``1.1.0``: feature #2 But it's *implied* that ``1.1.0`` includes the contents of ``1.0.1`` because it released afterwards/simultaneously and is a higher release line. * **Releases may be told explicitly which issues to include** (using a comma-separated list.) This is useful for the rare bugfix that gets backported beyond the actively supported release lines. For example, below shows a project whose lifecycle is "release 1.0; release 1.1 and drop active support for 1.0; put out a special 1.0.x release." Without the explicit issue list for 1.0.1, Releases would roll up all bugfixes, including the two that didn't actually apply to the 1.0 line. * Input:: * :release:`1.0.1 ` 1, 5 * :release:`1.1.1 ` * :bug:`5` Bugfix that applied back to 1.0. * :bug:`4` Bugfix that didn't apply to 1.0, only 1.1 * :bug:`3` Bugfix that didn't apply to 1.0, only 1.1 * :release:`1.1.0 ` * :feature:`2` Implemented new feature * :bug:`1` Fixed a 1.0.0 bug * :release:`1.0.0 ` * Result: * ``1.1.0``: feature #2 * ``1.1.1``: bugs #3, #4 and #5 * ``1.0.1``: bugs #1 and #5 only * **Bugfix issues may be told explicitly which release line they 'start' in.** This is useful for bugs that don't go back all the way to the oldest actively supported line - it keeps them from showing up in "too-old" releases. The below example includes a project actively supporting 1.5, 1.6 and 1.7 release lines, with a couple of bugfixes that only applied to 1.6+. * Input:: * :release:`1.7.1 ` * :release:`1.6.2 ` * :release:`1.5.3 ` * :bug:`50` Bug applying to all lines * :bug:`42 (1.6+)` A bug only applying to the new feature in 1.6 * :release:`1.7.0 ` * :release:`1.6.1 ` * :release:`1.5.2 ` * :feature:`25` Another new feature * :bug:`35` Bug that applies to all lines * :bug:`34` Bug that applies to all lines * :release:`1.6.0 ` * :release:`1.5.1 ` * :feature:`22` Some new feature * :bug:`20` Bugfix * :release:`1.5.0 ` * Result: * ``1.5.1``: bug #20 * ``1.6.0``: feature #22 * ``1.5.2``: bugs #34, #35 * ``1.6.1``: bugs #34, #35 * ``1.7.0``: feature #25 * ``1.5.3``: bug #50 only * ``1.6.2``: bugs #50 and #42 * ``1.7.1``: bugs #50 and #42 releases-0.7.0/docs/conf.py000644 000765 000024 00000001155 12343675014 016525 0ustar00jforcierstaff000000 000000 from datetime import datetime import os import sys import sphinx_rtd_theme extensions = [] templates_path = ['_templates'] source_suffix = '.rst' master_doc = 'index' project = u'Releases' year = datetime.now().year copyright = u'%d Jeff Forcier' % year # Ensure project directory is on PYTHONPATH for version, autodoc access sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), '..'))) exclude_patterns = ['_build'] # RTD theme html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Dogfood extensions.append('releases') releases_github_path = 'bitprophet/releases' releases-0.7.0/docs/index.rst000644 000765 000024 00000000253 12256403236 017063 0ustar00jforcierstaff000000 000000 ======== Releases ======== .. include:: ../README.rst Table of Contents ================= .. toctree:: :maxdepth: 2 concepts usage changelog todo releases-0.7.0/docs/todo.rst000644 000765 000024 00000000763 12254372120 016722 0ustar00jforcierstaff000000 000000 ==== TODO ==== * Possibly add more keywords to allow control over additional edge cases. * Add shortcut format option for the release/issue URI settings - GitHub users can just give their GitHub acct/repo and we will fill in the rest. * Maybe say pre-1.0 releases consider all bugs 'major' (so one can e.g. put out an 0.4.0 which is all bugfixes). Iffy because what if you *wanted* regular feature-vs-bugfix releases pre-1.0? (which is common.) * Make sure regular ``:issue:`` is documented. releases-0.7.0/docs/usage.rst000644 000765 000024 00000013242 12317350433 017060 0ustar00jforcierstaff000000 000000 ===== Usage ===== To use Releases, mimic the format seen in `its own changelog `_ or in `Fabric's changelog `_. Specifically: * Install ``releases`` and update your Sphinx ``conf.py`` to include it in the ``extensions`` list setting: ``extensions = ['releases']``. * Also set the ``releases_release_uri`` and ``releases_issue_uri`` top level options - they determine the targets of the issue & release links in the HTML output. Both should have an unevaluated ``%s`` where the release/issue number would go. * Alternately, if your project is hosted on Github, set the ``releases_github_path`` setting instead, to e.g. ``account/project``. Releases will then use an appropriate Github URL for both releases and issues. * If ``releases_release_uri`` or ``releases_issue_uri`` are *also* configured, they will be preferred over ``releases_github_path``. (If only one is configured, the other link type will continue using ``releases_github_path``.) * See `Fabric's docs/conf.py `_ for an example. * You may optionally set ``releases_debug = True`` to see debug output while building your docs. * Create a Sphinx document named ``changelog.rst`` with a top-level header followed by a bulleted list. * If you wish to use a different document name, use another config option (as per previous bullet point), ``releases_document_name``. E.g. ``releases_document_name = "CHANGES"`` would cause Releases to mutate a file called ``CHANGES.rst`` instead of ``changelog.rst``. * List items are to be ordered chronologically with the newest ones on top. * As you fix issues, put them on the top of the list. * As you cut releases, put those on the top of the list and they will include the issues below them. * Issues with no releases above them will end up in a specially marked "Unreleased" section of the rendered changelog. * Bullet list items should use the ``support``, ``feature`` or ``bug`` roles to mark issues, or ``release`` to mark a release. These special roles must be the first element in each list item. * Line-items that do not start with any issue role will be considered bugs (both in terms of inclusion in releases, and formatting) and, naturally, will not be given a hyperlink. * Issue roles are of the form ``:type:`number[ keyword]```. Specifically: * ``number`` is used to generate the link to the actual issue in your issue tracker (going by the ``releases_issue_uri`` option). It's used for both the link target & (part of) the link text. * If ``number`` is given as ``-`` or ``0`` (as opposed to a "real" issue number), no issue link will be generated. You can use this for items without a related issue. * Keywords are optional and may be one of: * ``backported``: Given on *support* or *feature* issues to denote backporting to bugfix releases; such issues will show up in both release types. E.g. placing ``:support:`123 backported``` in your changelog below releases '1.1.1' and '1.2.0' will cause it to appear in both of those releases' lists. * ``major``: Given on *bug* issues to denote inclusion in feature, instead of bugfix, releases. E.g. placing ``:bug:`22 major``` below releases '1.1.1' and '1.2.0' will cause it to appear in '1.2.0' **only**. * ``(N.N+)`` where ``N.N`` is a valid release line, e.g. ``1.1`` or ``2.10``: Given on *bug* issues to denote minimum release line. E.g. when actively backporting most bugs to release lines 1.2, 1.3 and 1.4, you might specify ``:bug:`55 (1.3+)``` to note that bug 55 only applies to releases in 1.3 and above - not 1.2. * Regular Sphinx content may be given after issue roles and will be preserved as-is when rendering. For example, in ``:bug:`123` Fixed a bug, thanks `@somebody`!``, the rendered changelog will preserve/render "Fixed a bug, thanks ``@somebody``!" after the issue link. * Release roles are of the form ``:release:`number ```. * You may place a comma-separated (whitespace optional) list of issue numbers after the release role, and this will limit the issues included in that release to that explicit list. * Otherwise, releases include all relevant issues as outlined above and in :doc:`/concepts`. Then build your docs; in the rendered output, ``changelog.html`` should show issues grouped by release, as per the above rules. Examples: `Releases' own rendered changelog `_, `Fabric's rendered changelog `_. Optional styling additions ========================== If you have any nontrivial changelog entries (e.g. whose description spans multiple paragraphs or includes their own bulleted lists, etc) you may run into `docutils' rather enthusiastic bulleted list massaging `_ which can then make your releases look different from one another. To help combat this, it may be useful to add the following rule to the Sphinx theme you're using:: div#changelog > div.section > ul > li > p:only-child { margin-bottom: 0; } .. note:: Some themes, like `Alabaster `_, may already include this style rule.