diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index daed37f..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -source = reno -omit = reno/openstack/* - -[report] -ignore_errors = True diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7c5611f..0000000 --- a/.gitignore +++ /dev/null @@ -1,56 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -.eggs -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage* -.tox -nosetests.xml -.testrepository -.venv - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Complexity -output/*.html -output/*/index.html - -# Sphinx -doc/build - -# pbr generates these -AUTHORS -ChangeLog - -# Editors -*~ -.*.swp -.*sw? -/cover/ -/releasenotes/notes/reno.cache diff --git a/.gitreview b/.gitreview deleted file mode 100644 index 43ab608..0000000 --- a/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=openstack/reno.git diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 516ae6f..0000000 --- a/.mailmap +++ /dev/null @@ -1,3 +0,0 @@ -# Format is: -# -# diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 6d83b3c..0000000 --- a/.testr.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index e716304..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,14 +0,0 @@ -If you would like to contribute to the development of OpenStack, you must -follow the steps in this page: -https://docs.openstack.org/infra/manual/developers.html - -If you already have a good understanding of how the system works and your -OpenStack accounts are set up, you can skip to the development workflow -section of this documentation to learn how changes to OpenStack should be -submitted for review via the Gerrit tool: -https://docs.openstack.org/infra/manual/developers.html#development-workflow - -Pull requests submitted through GitHub will be ignored. - -Bugs should be filed on Launchpad, not GitHub: -https://bugs.launchpad.net/reno diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index ace48c9..0000000 --- a/HACKING.rst +++ /dev/null @@ -1,4 +0,0 @@ -reno Style Commandments -=============================================== - -Read the OpenStack Style Commandments https://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 68c771a..0000000 --- a/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c978a52..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include AUTHORS -include ChangeLog -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc diff --git a/README b/README new file mode 100644 index 0000000..8fcd2b2 --- /dev/null +++ b/README @@ -0,0 +1,14 @@ +This project is no longer maintained. + +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". + +For ongoing work on maintaining OpenStack packages in the Debian +distribution, please see the Debian OpenStack packaging team at +https://wiki.debian.org/OpenStack/. + +For any further questions, please email +openstack-dev@lists.openstack.org or join #openstack-dev on +Freenode. diff --git a/README.rst b/README.rst deleted file mode 100644 index fa6c266..0000000 --- a/README.rst +++ /dev/null @@ -1,58 +0,0 @@ -========================================= - reno: A New Way to Manage Release Notes -========================================= - -Reno is a release notes manager designed with high throughput in mind, -supporting fast distributed development teams without introducing -additional development processes. Our goal is to encourage detailed -and accurate release notes for every release. - -Reno uses git to store its data, along side the code being -described. This means release notes can be written when the code -changes are fresh, so no details are forgotten. It also means that -release notes can go through the same review process used for managing -code and other documentation changes. - -Reno stores each release note in a separate file to enable a large -number of developers to work on multiple patches simultaneously, all -targeting the same branch, without worrying about merge -conflicts. This cuts down on the need to rebase or otherwise manually -resolve conflicts, and keeps a development team moving quickly. - -Reno also supports multiple branches, allowing release notes to be -back-ported from master to maintenance branches together with the -code for bug fixes. - -Reno organizes notes into logical groups based on whether they -describe new features, bug fixes, known issues, or other topics of -interest to the user. Contributors categorize individual notes as they -are added, and reno combines them before publishing. - -Notes can be styled using reStructuredText directives, and reno's -Sphinx integration makes it easy to incorporate release notes into -automated documentation builds. - -Notes are automatically associated with the release version based on -the git tags applied to the repository, so it is not necessary to -track changes manually using a bug tracker or other tool, or to worry -that an important change will be missed when the release notes are -written by hand all at one time, just before a release. - -Modifications to notes are incorporated when the notes are shown in -their original location in the history. This feature makes it possible -to correct typos or otherwise fix a published release note after a -release is made, but have the new note content associated with the -original version number. Notes also can be deleted, eliminating them -from future documentation builds. - -Project Meta-data -================= - -.. .. image:: https://governance.openstack.org/badges/reno.svg - :target: https://governance.openstack.org/reference/tags/index.html - -* Free software: Apache license -* Documentation: https://docs.openstack.org/reno/latest/ -* Source: https://git.openstack.org/cgit/openstack/reno -* Bugs: https://bugs.launchpad.net/reno -* IRC: #openstack-release on freenode diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index 5c7b0bc..0000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import sys - -# oslosphinx uses reno and reno uses oslosphinx. Make oslosphinx for -# reno optional to break the build cycle -try: - import openstackdocstheme -except: - has_theme = False -else: - has_theme = True - - -sys.path.insert(0, os.path.abspath('../..')) -# -- General configuration ---------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', - # 'sphinx.ext.intersphinx', - 'reno.sphinxext', -] - -if has_theme: - extensions.append('openstackdocstheme') - html_theme = 'openstackdocs' - -# openstackdocstheme options -repository_name = 'openstack/reno' -bug_project = 'reno' -bug_tag = '' -html_last_updated_fmt = '%Y-%m-%d %H:%M' - -# autodoc generation is a bit aggressive and a nuisance when doing heavy -# text edit cycles. -# execute "export SPHINX_DEBUG=1" in your terminal to disable - -# The suffix of source filenames. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'reno' -copyright = u'2013, OpenStack Foundation' - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# Do not warn about non-local image URI -suppress_warnings = ['image.nonlocal_uri'] - -# -- Options for HTML output -------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -# html_theme_path = ["."] -# html_theme = '_theme' -# html_static_path = ['static'] - -# Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ('index', - '%s.tex' % project, - u'%s Documentation' % project, - u'OpenStack Foundation', 'manual'), -] - -# Example configuration for intersphinx: refer to the Python standard library. -# intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst deleted file mode 100644 index 3d4ceb4..0000000 --- a/doc/source/contributor/index.rst +++ /dev/null @@ -1,5 +0,0 @@ -============ -Contributing -============ - -.. include:: ../../../CONTRIBUTING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index e5bbf0f..0000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. include:: ../../README.rst - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - user/index - install/index - contributor/index - releasenotes/index diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst deleted file mode 100644 index 3ed7b92..0000000 --- a/doc/source/install/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -============ -Installation -============ - -At the command line:: - - $ pip install reno - -Sphinx Extension -================ - -To use the Sphinx extension built into reno, install the ``[sphinx]`` -extra dependencies:: - - $ pip install 'reno[sphinx]' diff --git a/doc/source/releasenotes/index.rst b/doc/source/releasenotes/index.rst deleted file mode 100644 index c226052..0000000 --- a/doc/source/releasenotes/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -=============== - Release Notes -=============== - -.. release-notes:: Unreleased - -.. release-notes:: Mainline - :branch: origin/master - -.. release-notes:: Newton Series - :branch: origin/stable/newton - :earliest-version: 1.9.0 diff --git a/doc/source/user/design.rst b/doc/source/user/design.rst deleted file mode 100644 index 2a1dd2a..0000000 --- a/doc/source/user/design.rst +++ /dev/null @@ -1,52 +0,0 @@ -===================================== - Design Constraints and Requirements -===================================== - -Managing release notes for a complex project over a long period of -time with many releases can be time consuming and error prone. Reno -helps automate the hard parts by devising a way to store the notes -inside the git repository where they can be tagged as part of the -release. - -We had several design inputs: - -* Release notes should be part of the git history, so as fixes in - master are back-ported to older branches the notes can go with the - code change. -* Release notes may need to change over time, as typos are found, - logical errors or confusing language needs to be fixed, or as more - information becomes available (CVE numbers, etc.). -* Release notes should be peer-reviewed, as with other documentation - and code changes. -* Notes are mutable in that a clone today vs a clone tomorrow might - have different release notes about the same change. -* Notes are immutable in that for a given git hash/tag the release - notes will be the same. Tagging a commit will change the version - description but that is all. -* We want to avoid merge issues when shepherding in a lot of - release-note-worthy changes, which we expect to happen on stable - branches always, and at release times on master branches. -* We want writing a release note to be straight-forward. -* We do not want release notes to be custom ordered within a release, - but we do want the ordering to be predictable and consistent. -* We must be able to entirely remove a release note. -* We must not make things progressively slow down to a crawl over - years of usage. -* Release note authors shouldn't need to know any special values for - naming their notes files (i.e., no change id or SHA value that has - special meaning). -* It would be nice if it was somewhat easy to identify the file - containing a release note on a particular topic. -* Release notes should be grouped by type in the output document. - - 1. New features - 2. Known issues - 3. Upgrade notes - 4. Security fixes - 5. Bugs fixes - 6. Other - -We want to eventually provide the ability to create a release notes -file for a given release and add it to the source distribution for the -project. As a first step, we are going to settle for publishing -release notes in the documentation for a project. diff --git a/doc/source/user/examples.rst b/doc/source/user/examples.rst deleted file mode 100644 index 0119177..0000000 --- a/doc/source/user/examples.rst +++ /dev/null @@ -1,17 +0,0 @@ -========== - Examples -========== - -Input file -========== - -.. literalinclude:: ../../../examples/notes/add-complex-example-6b5927c246456896.yaml - :caption: examples/notes/add-complex-example-6b5927c246456896.yaml - :language: yaml - -Rendered -======== - -.. release-notes:: - :relnotessubdir: examples - :earliest-version: 1.0.0 diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst deleted file mode 100644 index 3dc9ce2..0000000 --- a/doc/source/user/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -================= - reno User Guide -================= - -.. toctree:: - :maxdepth: 2 - - design - usage - sphinxext - examples diff --git a/doc/source/user/sphinxext.rst b/doc/source/user/sphinxext.rst deleted file mode 100644 index 18a6d6d..0000000 --- a/doc/source/user/sphinxext.rst +++ /dev/null @@ -1,88 +0,0 @@ -================== - Sphinx Extension -================== - -In addition to the command line tool, reno includes a Sphinx extension -for incorporating release notes for a project in its documentation -automatically. - -Enable the extension by adding ``'reno.sphinxext'`` to the -``extensions`` list in the Sphinx project ``conf.py`` file. - -.. rst:directive:: release-notes - - The ``release-notes`` directive accepts the same inputs as the - ``report`` subcommand, and inserts the report inline into the - current document where Sphinx then processes it to create HTML, - PDF, or other output formats. - - If the directive has a body, it is used to create a title entry - with ``=`` over and under lines (the typical heading style for the - top-level heading in a document). - - Options: - - *branch* - The name of the branch to scan. Defaults to the current branch. - - *reporoot* - The path to the repository root directory. Defaults to the - directory where ``sphinx-build`` is being run. - - *relnotessubdir* - The path under ``reporoot`` where the release notes are. Defaults - to ``releasenotes``. - - *notesdir* - The path under ``relnotessubdir`` where the release notes - are. Defaults to ``notes``. - - *version* - A comma separated list of versions to include in the notes. The - default is to include all versions found on ``branch``. - - *collapse-pre-releases* - A flag indicating that notes attached to pre-release versions - should be incorporated into the notes for the final release, - after the final release is tagged. - - *earliest-version* - A string containing the version number of the earliest version to - be included. For example, when scanning a branch, this is - typically set to the version used to create the branch to limit - the output to only versions on that branch. - - *ignore-notes* - A string containing a comma-delimited list of filenames or UIDs - for notes that should be ignored by the scanner. It is most - useful to set this when a note is edited on the wrong branch, - making it appear to be part of a release that it is not. - -Examples -======== - -The release notes for the "current" branch, with "Release Notes" as a -title. - -.. code-block:: rest - - .. release-notes:: Release Notes - -The release notes for the "stable/liberty" branch, with a separate -title. - -.. code-block:: rest - - ======================= - Liberty Release Notes - ======================= - - .. release-notes:: - :branch: stable/liberty - -The release notes for version "1.0.0". - -.. code-block:: rest - - .. release-notes:: 1.0.0 Release Notes - :version: 1.0.0 diff --git a/doc/source/user/usage.rst b/doc/source/user/usage.rst deleted file mode 100644 index 22180e4..0000000 --- a/doc/source/user/usage.rst +++ /dev/null @@ -1,325 +0,0 @@ -======== - Usage -======== - -Creating New Release Notes -========================== - -The ``reno`` command line tool is used to create a new release note -file in the correct format and with a unique name. The ``new`` -subcommand combines a random suffix with a "slug" value to create -the file with a unique name that is easy to identify again later. - -:: - - $ reno new slug-goes-here - Created new notes file in releasenotes/notes/slug-goes-here-95915aaedd3c48d8.yaml - -Within OpenStack projects, ``reno`` is often run via tox instead of -being installed globally. For example - -:: - - $ tox -e venv -- reno new slug-goes-here - venv develop-inst-nodeps: /mnt/projects/release-notes-generation/reno - venv runtests: commands[0] | reno new slug-goes-here - Created new notes file in releasenotes/notes/slug-goes-here-95915aaedd3c48d8.yaml - venv: commands succeeded - congratulations :) - $ git status - Untracked files: - (use "git add ..." to include in what will be committed) - - releasenotes/notes/slug-goes-here-95915aaedd3c48d8.yaml - -The ``--edit`` option opens the new note in a text editor. - -:: - - $ reno new slug-goes-here --edit - ... Opens the editor set in the EDITOR environment variable, editing the new file ... - Created new notes file in releasenotes/notes/slug-goes-here-95915aaedd3c48d8.yaml - - -By default, the new note is created under ``./releasenotes/notes``. -The ``--rel-notes-dir`` command-line flag changes the parent directory -(the ``notes`` subdirectory is always appended). It's also possible to -set a custom template to create notes (see `Configuring Reno`_ ). - -Editing a Release Note -====================== - -The note file is a YAML file with several sections. All of the text is -interpreted as having `reStructuredText`_ formatting. The permitted -sections are configurable (see below) but default to the following -list: - -prelude - General comments about the release. Prelude sections from all notes in a - release are combined, in note order, to produce a single prelude - introducing that release. This section is always included, regardless - of what sections are configured. - -features - A list of new major features in the release. - -issues - A list of known issues in the release. For example, if a new driver - is experimental or known to not work in some cases, it should be - mentioned here. - -upgrade - A list of upgrade notes in the release. For example, if a database - schema alteration is needed. - -deprecations - A list of features, APIs, configuration options to be deprecated in the - release. Deprecations should not be used for something that is removed in the - release, use upgrade section instead. Deprecation should allow time for users - to make necessary changes for the removal to happen in a future release. - -critical - A list of *fixed* critical bugs. - -security - A list of *fixed* security issues. - -fixes - A list of other *fixed* bugs. - -other - Other notes that are important but do not fall into any of the given - categories. - -Any sections that would be blank should be left out of the note file -entirely. - -.. code-block:: yaml - - --- - prelude: > - Replace this text with content to appear at the - top of the section for this release. - features: - - List new features here, or remove this section. - issues: - - List known issues here, or remove this section. - upgrade: - - List upgrade notes here, or remove this section. - deprecations: - - List deprecation notes here, or remove this section - critical: - - Add critical notes here, or remove this section. - security: - - Add security notes here, or remove this section. - fixes: - - Add normal bug fixes here, or remove this section. - other: - - Add other notes here, or remove this section. - -Note File Syntax ----------------- - -Release notes may include embedded `reStructuredText`_, including simple -inline markup like emphasis and pre-formatted text as well as complex -body structures such as nested lists and tables. To use these -formatting features, the note must be escaped from the YAML parser. - -The default template sets up the ``prelude`` section to use ``>`` so -that line breaks in the text are removed. This escaping mechanism is -not needed for the bullet items in the other sections of the template. - -To escape the text of any section and *retain* the newlines, prefix -the value with ``|``. For example: - -.. literalinclude:: ../../../examples/notes/add-complex-example-6b5927c246456896.yaml - :language: yaml - -See :doc:`examples` for the rendered version of the note. - -.. _reStructuredText: http://www.sphinx-doc.org/en/stable/rest.html - -Generating a Report -=================== - -Run ``reno report `` to generate a report -containing the release notes. The ``--branch`` argument can be used to -generate a report for a specific branch (the default is the branch -that is checked out). To limit the report to a subset of the available -versions on the branch, use the ``--version`` option (it can be -repeated). - -Notes are output in the order they are found when scanning the git -history of the branch using topological ordering. This is -deterministic, but not necessarily predictable or mutable. - -Checking Notes -============== - -Run ``reno lint `` to test the existing -release notes files against some rules for catching common -mistakes. The command exits with an error code if there are any -mistakes, so it can be used in a build pipeline to force some -correctness. - -Configuring Reno -================ - -Reno looks for an optional config file, either ``config.yaml`` in the release -notes directory or ``reno.yaml`` in the root directory. If the values in the -configuration file do not apply to the command being run, they are ignored. For -example, some reno commands take inputs controlling the branch, earliest -revision, and other common parameters that control which notes are included in -the output. Because they are commonly set options, a configuration file may be -the most convenient way to manage the values consistently. - -.. code-block:: yaml - - --- - branch: master - earliest_version: 12.0.0 - collapse_pre_releases: false - stop_at_branch_base: true - sections: - # The prelude section is implicitly included. - - [features, New Features] - - [issues, Known Issues] - - [upgrade, Upgrade Notes] - - [api, API Changes] - - [security, Security Issues] - - [fixes, Bug Fixes] - # Change prelude_section_name to 'release_summary' from default value - # 'prelude'. - prelude_section_name: release_summary - template: | - - ... - -Many of the settings in the configuration file can be overridden by -using command-line switches. For example: - -- ``--branch`` -- ``--earliest-version`` -- ``--collapse-pre-releases``/``--no-collapse-pre-releases`` -- ``--ignore-cache`` -- ``--stop-at-branch-base``/``--no-stop-at-branch-base`` - -The following options are configurable: - -`notesdir` - The notes subdirectory within the `relnotesdir` where the notes live. - - Defaults to ``notes``. - -`collapse_pre_releases` - Should pre-release versions be merged into the final release of the same - number (`1.0.0.0a1` notes appear under `1.0.0`). - - Defaults to ``True``. - -`stop_at_branch_base` - Should the scanner stop at the base of a branch (True) or go ahead and scan - the entire history (False)? - - Defaults to ``True``. - -`branch` - The git branch to scan. If a stable branch is specified but does not exist, - reno attempts to automatically convert that to an "end-of-life" tag. For - example, ``origin/stable/liberty`` would be converted to ``liberty-eol``. - - Defaults to the "current" branch checked out. - -`earliest_version` - The earliest version to be included. This is usually the lowest version - number, and is meant to be the oldest version. If unset, all versions will be - scanned. - - Defaults to ``None``. - -`template` - The template used by reno new to create a note. - -`release_tag_re` - The regex pattern used to match the repo tags representing a valid release - version. The pattern is compiled with the verbose and unicode flags enabled. - - Defaults to ``((?:[\d.ab]|rc)+)``. - -`pre_release_tag_re` - The regex pattern used to check if a valid release version tag is also a - valid pre-release version. The pattern is compiled with the verbose and - unicode flags enabled. The pattern must define a group called `pre_release` - that matches the pre-release part of the tag and any separator, e.g for - pre-release version `12.0.0.0rc1` the default RE pattern will identify - `.0rc1` as the value of the group 'pre_release'. - - Defaults to ``(?P\.\d+(?:[ab]|rc)+\d*)$``. - -`branch_name_re` - The pattern for names for branches that are relevant when scanning history to - determine where to stop, to find the "base" of a branch. Other branches are - ignored. - - Defaults to ``stable/.+``. - -`sections` - The identifiers and names of permitted sections in the release notes, in the - order in which the final report will be generated. A prelude section will - always be automatically inserted before the first element of this list. - -`prelude_section_name` - The name of the prelude section in the note template. Note that the - value for this must be a single word, but can have underscores. The - value is displayed in titlecase in the report after replacing - underscores with spaces. - - Defaults to ``prelude`` - -`ignore_null_merges` - OpenStack used to use null-merges to bring final release tags from - stable branches back into the master branch. This confuses the - regular traversal because it makes that stable branch appear to be - part of master and/or the later stable branch. This option allows us - to ignore those. - - When this option is set to True, any merge commits with no changes - and in which the second or later parent is tagged are considered - "null-merges" that bring the tag information into the current branch - but nothing else. - - Defaults to ``True``. - -`ignore_notes` - A list of filenames or UIDs for notes that should be ignored by the - reno scanner. It is most useful to set this when a note is edited on - the wrong branch, making it appear to be part of a release that it - is not. - - .. warning:: - - Setting the option in the main configuration file makes it apply - to all branches. To ignore a note in the HTML build, use the - ``ignore-notes`` parameter to the ``release-notes`` sphinx - directive. - -Debugging -========= - -The true location of formatting errors in release notes may be masked -because of the way release notes are included into sphinx documents. -To generate the release notes manually, so that they can be put into a -sphinx document directly for debugging, use the ``report`` command. - -.. code-block:: console - - $ reno report . - -Within OpenStack -================ - -The OpenStack project maintains separate instructions for configuring -the CI jobs and other project-specific settings used for reno. Refer -to the `Managing Release Notes -`__ -section of the Project Team Guide for details. diff --git a/examples/notes/add-complex-example-6b5927c246456896.yaml b/examples/notes/add-complex-example-6b5927c246456896.yaml deleted file mode 100644 index 24c31e1..0000000 --- a/examples/notes/add-complex-example-6b5927c246456896.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -prelude: | - This paragraph will - retain its newlines - when the value is passed to the - reStructuredText parser, which - will then merge them into - a single paragraph without - breaks. - - | These - | lines - | are prefixed - | with | so the reStructuredText - | parser will retain - | the line breaks. -features: - This note is a simple string, and does not retain its - formatting when it is rendered in HTML. rst markup here - may break the YAML parser, since the string is not escaped. -fixes: - - Use YAML lists to add multiple items to the same section. - - Another fix could be listed here. -other: - - | - This bullet item includes a paragraph and a nested list, - which works because the content of the YAML list item - is an escaped string block with reStructuredText formatting. - - * list item 1 - * list item 2 - - .. code-block:: text - - This example is also rendered - correctly on multiple lines - as a pre-formatted block. diff --git a/releasenotes/notes/add-config-file-e77084792c1dc695.yaml b/releasenotes/notes/add-config-file-e77084792c1dc695.yaml deleted file mode 100644 index 32ec204..0000000 --- a/releasenotes/notes/add-config-file-e77084792c1dc695.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -features: - - | - Reno now supports having a ``config.yaml`` file in your release notes - directory. It will search for file in the directory specified by - ``--rel-notes-dir`` and parse it. It will apply whatever options are - valid for that particular command. If an option is not relevant to a - particular sub-command, it will not attempt to apply them. diff --git a/releasenotes/notes/add-deprecations-section-6b0f118fe190585c.yaml b/releasenotes/notes/add-deprecations-section-6b0f118fe190585c.yaml deleted file mode 100644 index 45d4d9b..0000000 --- a/releasenotes/notes/add-deprecations-section-6b0f118fe190585c.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -features: - - Added a new section for deprecations that occur during a release diff --git a/releasenotes/notes/add-earliest-version-6f3d634770e855d0.yaml b/releasenotes/notes/add-earliest-version-6f3d634770e855d0.yaml deleted file mode 100644 index b5454d3..0000000 --- a/releasenotes/notes/add-earliest-version-6f3d634770e855d0.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -features: - - Add the ability to limit queries by stopping at an "earliest - version". This is intended to be used when scanning a branch, for - example, to stop at a point when the branch was created and not - include all of the history from the parent branch. \ No newline at end of file diff --git a/releasenotes/notes/add-linter-ce0a861ade64baf2.yaml b/releasenotes/notes/add-linter-ce0a861ade64baf2.yaml deleted file mode 100644 index b36983a..0000000 --- a/releasenotes/notes/add-linter-ce0a861ade64baf2.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -features: - - | - Add a ``lint`` command for checking the contents and names of the - release notes files against some basic validation rules. \ No newline at end of file diff --git a/releasenotes/notes/add-verbose-flag-88d72cb01812c616.yaml b/releasenotes/notes/add-verbose-flag-88d72cb01812c616.yaml deleted file mode 100644 index 25eed1e..0000000 --- a/releasenotes/notes/add-verbose-flag-88d72cb01812c616.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -features: - - Add the ``--verbose``, ``-v``, and ``-q`` options to the command line tool for producing different levels of debug output. diff --git a/releasenotes/notes/allow-short-branch-names-61a35be55f04cea4.yaml b/releasenotes/notes/allow-short-branch-names-61a35be55f04cea4.yaml deleted file mode 100644 index 280ce62..0000000 --- a/releasenotes/notes/allow-short-branch-names-61a35be55f04cea4.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -fixes: - - | - Fix a problem with branch references so that it is now possible to - use a local tracking branch name when the branch only exists on - the 'origin' remote. For example, this allows references to - 'stable/ocata' when there is no local branch with that name but - there is an 'origin/stable/ocata' branch. \ No newline at end of file diff --git a/releasenotes/notes/avoid-clashing-uids-e84ffe8132ce996d.yaml b/releasenotes/notes/avoid-clashing-uids-e84ffe8132ce996d.yaml deleted file mode 100644 index 4ea18c6..0000000 --- a/releasenotes/notes/avoid-clashing-uids-e84ffe8132ce996d.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -fixes: - - | - Fix a problem caused by failing to process multiple files with the - same UID portion of the filename. Ignore existing cases as long as - there is a corrective patch to remove them. Prevent new cases from - being introduced. See https://bugs.launchpad.net/reno/+bug/1688042 - for details. \ No newline at end of file diff --git a/releasenotes/notes/branches-eol-bcafc2a007a1eb9f.yaml b/releasenotes/notes/branches-eol-bcafc2a007a1eb9f.yaml deleted file mode 100644 index 0a852d9..0000000 --- a/releasenotes/notes/branches-eol-bcafc2a007a1eb9f.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -features: - - | - Explicitly allow reno to scan starting from a tag by specifying the - tag where a branch name would otherwise be used. - - | - Add logic to allow reno to detect a branch that has been marked as - end-of-life using the OpenStack community's process of tagging the - HEAD of a stable/foo branch foo-eol before deleting the - branch. This means that references to "stable/foo" are translated - to "foo-eol" when the branch does not exist, and that Sphinx - directives do not need to be manually updated. \ No newline at end of file diff --git a/releasenotes/notes/bug-1537451-f44591da125ba09d.yaml b/releasenotes/notes/bug-1537451-f44591da125ba09d.yaml deleted file mode 100644 index f033466..0000000 --- a/releasenotes/notes/bug-1537451-f44591da125ba09d.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -fixes: - - Resolves `a bug `__ - with properly detecting pre-release versions in the existing - history of a repository that resulted in some release notes not - appearing in the report output. diff --git a/releasenotes/notes/collapse-pre-releases-0b24e0bab46d7cf1.yaml b/releasenotes/notes/collapse-pre-releases-0b24e0bab46d7cf1.yaml deleted file mode 100644 index 9cd1675..0000000 --- a/releasenotes/notes/collapse-pre-releases-0b24e0bab46d7cf1.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -features: - - Add a flag to collapse pre-release notes into their final release, - if the final release tag is present. \ No newline at end of file diff --git a/releasenotes/notes/config-option-branch-name-re-8ecfe93195b8824e.yaml b/releasenotes/notes/config-option-branch-name-re-8ecfe93195b8824e.yaml deleted file mode 100644 index d776c0c..0000000 --- a/releasenotes/notes/config-option-branch-name-re-8ecfe93195b8824e.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -features: - - | - Add a configuration option ``branch_name_re`` to hold a regular expression - for choosing "interesting" branches when trying to automatically detect - how far back the scanner should look. The default is ``stable/.+``, which - works for the OpenStack practice of creating branches named after the - stable series of releases. -fixes: - - | - Fixes the logic for determining how far back in history to look when - scanning a given branch. Reno now looks for the base of the "previous" - branch, as determined by looking at branches matching ``branch_name_re`` - in lexical order. This may not work if branches are created using - version numbers as their names. diff --git a/releasenotes/notes/config-option-sections-9c68b070698e984a.yaml b/releasenotes/notes/config-option-sections-9c68b070698e984a.yaml deleted file mode 100644 index 73c77bc..0000000 --- a/releasenotes/notes/config-option-sections-9c68b070698e984a.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - | - Add a configuration option ``sections`` to hold the list of - permitted section identifiers and corresponding display names. - This also determines the order in which sections are collated. - diff --git a/releasenotes/notes/custom-tag-versions-d02028b6d35db967.yaml b/releasenotes/notes/custom-tag-versions-d02028b6d35db967.yaml deleted file mode 100644 index c1ce543..0000000 --- a/releasenotes/notes/custom-tag-versions-d02028b6d35db967.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -features: - - | - Add the ability to specify regular expressions to a define a - customised versioning scheme for release tags and pre-release tags. - - By default this change supports the current versioning scheme used by - OpenStack. - - To customise, update the config.yaml file with the appropriate values. - For example, for tags with versions like ``v1.0.0`` and pre-release - versions like ``v1.0.0rc1`` the following could be added to config.yaml:: - - release_tag_re: 'v\d\.\d\.\d(rc\d+)?' - pre_release_tag_re: '(?Prc\d+$)' diff --git a/releasenotes/notes/default-repository-root-cli-85d23034bef81619.yaml b/releasenotes/notes/default-repository-root-cli-85d23034bef81619.yaml deleted file mode 100644 index ebd74fc..0000000 --- a/releasenotes/notes/default-repository-root-cli-85d23034bef81619.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -features: - - Set the default value of the reporoot argument - for all command line programs to "." and make - it an optional parameter. \ No newline at end of file diff --git a/releasenotes/notes/dulwich-rewrite-3a5377162d97402b.yaml b/releasenotes/notes/dulwich-rewrite-3a5377162d97402b.yaml deleted file mode 100644 index d29bd26..0000000 --- a/releasenotes/notes/dulwich-rewrite-3a5377162d97402b.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -prelude: > - This release includes a significant rewrite of the internal logic of - reno to access git data through the dulwich library instead of the - git command line porcelain. diff --git a/releasenotes/notes/first-release-2857bfc9474c00b4.yaml b/releasenotes/notes/first-release-2857bfc9474c00b4.yaml deleted file mode 100644 index 916149e..0000000 --- a/releasenotes/notes/first-release-2857bfc9474c00b4.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -prelude: > - This is the first release. -features: - - Creating new notes files with unique names. - - Listing the files with notes related to each release. - - Producing a unified report of all of the notes for a release. diff --git a/releasenotes/notes/fix-branch-base-detection-95300805f26a0c15.yaml b/releasenotes/notes/fix-branch-base-detection-95300805f26a0c15.yaml deleted file mode 100644 index ffbc8a0..0000000 --- a/releasenotes/notes/fix-branch-base-detection-95300805f26a0c15.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -fixes: - - | - Fix a problem with the way reno automatically detects the initial - version in a branch that prevented it from including all of the - notes associated with a release, especially if the branch was - created at a pre-release version number. - `Bug #1652092 `__ diff --git a/releasenotes/notes/fix-delete-handling-55232c50b647aa57.yaml b/releasenotes/notes/fix-delete-handling-55232c50b647aa57.yaml deleted file mode 100644 index eaca402..0000000 --- a/releasenotes/notes/fix-delete-handling-55232c50b647aa57.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -fixes: - - | - Correct a problem with handling deleted release notes that - triggered a TypeError with a message like "Can't mix strings and - bytes in path components" \ No newline at end of file diff --git a/releasenotes/notes/fix-git-log-ordering-0e52f95f66c8db5b.yaml b/releasenotes/notes/fix-git-log-ordering-0e52f95f66c8db5b.yaml deleted file mode 100644 index d0aef66..0000000 --- a/releasenotes/notes/fix-git-log-ordering-0e52f95f66c8db5b.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -fixes: - - Fixes `bug 1522153 - `__ so that notes - added in commits that are merged after tags are associated with - the correct version. diff --git a/releasenotes/notes/fix-prelude-4e0bcb6f76571b4f.yaml b/releasenotes/notes/fix-prelude-4e0bcb6f76571b4f.yaml deleted file mode 100644 index 9a2bfc8..0000000 --- a/releasenotes/notes/fix-prelude-4e0bcb6f76571b4f.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -fixes: - - Fixed the section used in the report to include the prelude in the output. diff --git a/releasenotes/notes/fix-scanner-tag-detection-ef0a95c12a90f167.yaml b/releasenotes/notes/fix-scanner-tag-detection-ef0a95c12a90f167.yaml deleted file mode 100644 index e2820b0..0000000 --- a/releasenotes/notes/fix-scanner-tag-detection-ef0a95c12a90f167.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -fixes: - - Fix `bug 1517175 `__ to - ensure that all tagged versions are detected and that notes are associated - with the correct version numbers. diff --git a/releasenotes/notes/fix-sphinxext-scanner-0aa012ada66db773.yaml b/releasenotes/notes/fix-sphinxext-scanner-0aa012ada66db773.yaml deleted file mode 100644 index 097c097..0000000 --- a/releasenotes/notes/fix-sphinxext-scanner-0aa012ada66db773.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -fixes: - - | - Fixes a problem with the sphinx extension that caused it to - produce an error if it had a list of versions to include that were - not within the set that seemed to be on the branch because of the - branch-base detection logic. Now if a list of versions is - included, the scan always includes the full history. \ No newline at end of file diff --git a/releasenotes/notes/flexible-formatting-31c8de2599d3637d.yaml b/releasenotes/notes/flexible-formatting-31c8de2599d3637d.yaml deleted file mode 100644 index a761130..0000000 --- a/releasenotes/notes/flexible-formatting-31c8de2599d3637d.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -features: - Release notes entries may now be made up of single strings. This - simplifies formatting for smaller notes, and eliminates a class of - errors associated with escaping reStructuredText inside YAML lists. diff --git a/releasenotes/notes/ignore-notes-option-9d0bde540fbcdf22.yaml b/releasenotes/notes/ignore-notes-option-9d0bde540fbcdf22.yaml deleted file mode 100644 index 6fd41e4..0000000 --- a/releasenotes/notes/ignore-notes-option-9d0bde540fbcdf22.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -features: - - | - Add a new configuration option ``ignore_notes``. Setting the value - to a list of filenames or UIDs for notes causes the reno scanner - to ignore them. It is most useful to set this when a note is - edited on the wrong branch, making it appear to be part of a - release that it is not. diff --git a/releasenotes/notes/ignore-null-merges-56b7a8ed9b20859e.yaml b/releasenotes/notes/ignore-null-merges-56b7a8ed9b20859e.yaml deleted file mode 100644 index 03f3da6..0000000 --- a/releasenotes/notes/ignore-null-merges-56b7a8ed9b20859e.yaml +++ /dev/null @@ -1,18 +0,0 @@ ---- -features: - - | - By default, reno now ignores "null" merge commits that bring in - tags from other threads. The new configuration option - ``ignore_null_merges`` controls this behavior. Setting the flag to - False restores the previous behavior in which the null-merge - commits were traversed like any other merge commit. -upgrade: - - | - The new configuration option ``ignore_null_merges`` causes the - scanner to ignore merge commits with no changes when one of the - parents being merged in has a release tag on it. -fixes: - - | - This release fixes a problem with the scanner that may have caused - it to stop scanning a branch prematurely when the tag from another - branch had been merged into the history. diff --git a/releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml b/releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml deleted file mode 100644 index 5a02790..0000000 --- a/releasenotes/notes/include-working-copy-d0aed2e77bb095e6.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - | - Include the local working copy when scanning the history of the - current branch. Notes files must at least be staged to indicate - that they will eventually be part of the history, but subsequent - changes to the file do not need to also be staged to be seen. diff --git a/releasenotes/notes/log-levels-and-sphinx-161-6efe0d291718a657.yaml b/releasenotes/notes/log-levels-and-sphinx-161-6efe0d291718a657.yaml deleted file mode 100644 index f54a20e..0000000 --- a/releasenotes/notes/log-levels-and-sphinx-161-6efe0d291718a657.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -fixes: - - | - Sphinx 1.6.1 now interprets error and warning log messages as - reasons to abort the build when strict mode is enabled. This - release changes the log level for some calls that weren't really - errors to begin with to avoid having Sphinx abort the build - unnecessarily. \ No newline at end of file diff --git a/releasenotes/notes/no-show-source-option-ee02766b26fe53be.yaml b/releasenotes/notes/no-show-source-option-ee02766b26fe53be.yaml deleted file mode 100644 index 7074ad3..0000000 --- a/releasenotes/notes/no-show-source-option-ee02766b26fe53be.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -features: - - | - Add a ``--no-show-source`` option to the report command to skip - including the note reference file names and SHA information - in comments in the output. This restores the previous format of - the output for cases where it is meant to be read by people directly, - not just converted to HTML. diff --git a/releasenotes/notes/null-merge-infinite-loop-670367094ad83e19.yaml b/releasenotes/notes/null-merge-infinite-loop-670367094ad83e19.yaml deleted file mode 100644 index ed7ae8a..0000000 --- a/releasenotes/notes/null-merge-infinite-loop-670367094ad83e19.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -fixes: - - | - Remove an infinite loop in the traversal algorithm caused by some - null-merge skip situations. diff --git a/releasenotes/notes/optional-oslosphinx-55843a7f80a14e58.yaml b/releasenotes/notes/optional-oslosphinx-55843a7f80a14e58.yaml deleted file mode 100644 index ee0252f..0000000 --- a/releasenotes/notes/optional-oslosphinx-55843a7f80a14e58.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -other: - - The oslosphinx dependency for building documentation - is now optional. This breaks a build cycle between - oslosphinx and reno. diff --git a/releasenotes/notes/repodir-config-file-b6b8edc2975964fc.yaml b/releasenotes/notes/repodir-config-file-b6b8edc2975964fc.yaml deleted file mode 100644 index aedb19e..0000000 --- a/releasenotes/notes/repodir-config-file-b6b8edc2975964fc.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - | - reno will now scan for a ``reno.yaml`` file in the root repo directory if a - ``config.yaml`` file does not exist in the releasenotes directory. This - allows users to do away with the unnecessary ``notes`` subdirectory in the - releasenotes directory. diff --git a/releasenotes/notes/report-title-option-f0875bfdbc54dd7b.yaml b/releasenotes/notes/report-title-option-f0875bfdbc54dd7b.yaml deleted file mode 100644 index 0948c26..0000000 --- a/releasenotes/notes/report-title-option-f0875bfdbc54dd7b.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -features: - - | - Add a ``--title`` option to the report command. diff --git a/releasenotes/notes/reverse-slug-order-4c5f94e72d4f6fb9.yaml b/releasenotes/notes/reverse-slug-order-4c5f94e72d4f6fb9.yaml deleted file mode 100644 index 82ba0e0..0000000 --- a/releasenotes/notes/reverse-slug-order-4c5f94e72d4f6fb9.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -upgrade: - - | - Change the order of the slug and UUID value in the note filename so - the slug comes before the UUID to make tab completion easier to - use. - - Older files are still supported, and can be renamed to use - the new style. diff --git a/releasenotes/notes/show-less-unreleased-802781a1a3bf110e.yaml b/releasenotes/notes/show-less-unreleased-802781a1a3bf110e.yaml deleted file mode 100644 index 20673e0..0000000 --- a/releasenotes/notes/show-less-unreleased-802781a1a3bf110e.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -features: - - | - The scanner for the "current" branch (usually ``master``) now stops - when it encounters the base of an earlier branch matching the - ``branch_name_re`` config option. This results in less history - appearing on the unreleased pages, while still actually showing - the current series and any unreleased notes. diff --git a/releasenotes/notes/show-note-filename-in-report-a1118c917588b58d.yaml b/releasenotes/notes/show-note-filename-in-report-a1118c917588b58d.yaml deleted file mode 100644 index 286b754..0000000 --- a/releasenotes/notes/show-note-filename-in-report-a1118c917588b58d.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - | - The report output now includes debugging details with the filename - and sha for the version of the content used to indicate where the - content is from to assist with debugging formatting or content - issues. diff --git a/releasenotes/notes/sphinx-extension-4a092b4102370246.yaml b/releasenotes/notes/sphinx-extension-4a092b4102370246.yaml deleted file mode 100644 index 8e4c90e..0000000 --- a/releasenotes/notes/sphinx-extension-4a092b4102370246.yaml +++ /dev/null @@ -1,3 +0,0 @@ ---- -features: - - Add the sphinx extension for integration with documentation builds. diff --git a/releasenotes/notes/stop-scanning-branch-e5a8937c248acc99.yaml b/releasenotes/notes/stop-scanning-branch-e5a8937c248acc99.yaml deleted file mode 100644 index 3caccc5..0000000 --- a/releasenotes/notes/stop-scanning-branch-e5a8937c248acc99.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -features: - - Automatically stop scanning branches at the point where they - diverge from master. This avoids having release notes from older - versions, that appear on master before the branch, from showing up - in the versions from the branch. This logic is only applied to - branches created from master. diff --git a/releasenotes/notes/stop-scanning-branch-option-6a0156b183814d7f.yaml b/releasenotes/notes/stop-scanning-branch-option-6a0156b183814d7f.yaml deleted file mode 100644 index fbba24b..0000000 --- a/releasenotes/notes/stop-scanning-branch-option-6a0156b183814d7f.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -features: - - Add a new configuration option, stop_at_branch_base, to control - whether or not the scanner stops looking for changes at the point - where a branch diverges from master. The default is True, meaning - that the scanner does stop. A false value means that versions that - appear on master from a point earlier than when the branch was - created will be included when scanning the branch for release - notes. diff --git a/releasenotes/notes/support-custom-template-0534a2199cfec44c.yaml b/releasenotes/notes/support-custom-template-0534a2199cfec44c.yaml deleted file mode 100644 index cb3bed8..0000000 --- a/releasenotes/notes/support-custom-template-0534a2199cfec44c.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -features: - - | - Reno now supports to set through ``template`` attribute in - ``config.yaml`` a custom template which will be used by reno new - to create notes. diff --git a/releasenotes/notes/support-edit-ec5c01ad6144815a.yaml b/releasenotes/notes/support-edit-ec5c01ad6144815a.yaml deleted file mode 100644 index 2997e1e..0000000 --- a/releasenotes/notes/support-edit-ec5c01ad6144815a.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -features: - - | - Reno now enables with reno new ``--edit`` to create a note and edit it with - your editor (defined with EDITOR environment variable). diff --git a/releasenotes/notes/support-multi-line-notes-328853d8d596fd64.yaml b/releasenotes/notes/support-multi-line-notes-328853d8d596fd64.yaml deleted file mode 100644 index 7c21ffb..0000000 --- a/releasenotes/notes/support-multi-line-notes-328853d8d596fd64.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -features: - - | - Support note entries that span multiple lines using - preformatted syntax in YAML by prefixing the - list entry with ``|``. - - For example:: - - - | - This entry has two paragraphs. - - This is the second. diff --git a/reno/__init__.py b/reno/__init__.py deleted file mode 100644 index ce87e72..0000000 --- a/reno/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging - -import pbr.version - - -__version__ = pbr.version.VersionInfo( - 'reno').version_string() - -# Configure a null logger so that if reno is used as a library by an -# application that does not configure logging there are no warnings. -logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/reno/cache.py b/reno/cache.py deleted file mode 100644 index 20c2a8e..0000000 --- a/reno/cache.py +++ /dev/null @@ -1,107 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import sys - -import yaml - -from reno import loader -from reno import scanner - - -def build_cache_db(conf, versions_to_include): - s = scanner.Scanner(conf) - notes = s.get_notes_by_version() - - # Default to including all versions returned by the scanner. - if not versions_to_include: - versions_to_include = list(notes.keys()) - - # Build a cache data structure including the file contents as well - # as the basic data returned by the scanner. - file_contents = {} - for version in versions_to_include: - for filename, sha in notes[version]: - body = s.get_file_at_commit(filename, sha) - # We want to save the contents of the file, which is YAML, - # inside another YAML file. That looks terribly ugly with - # all of the escapes needed to format it properly as - # embedded YAML, so parse the input and convert it to a - # data structure that can be serialized cleanly. - y = yaml.safe_load(body) - file_contents[filename] = y - - cache = { - 'notes': [ - {'version': k, 'files': v} - for k, v in notes.items() - ], - 'file-contents': file_contents, - } - return cache - - -def write_cache_db(conf, versions_to_include, - outfilename=None): - """Create a cache database file for the release notes data. - - Build the cache database from scanning the project history and - write it to a file within the project. - - By default, the data is written to the same file the scanner will - try to read when it cannot look at the git history. If outfilename - is given and is '-' the data is written to stdout - instead. Otherwise, if outfilename is given, the data overwrites - the named file. - - Return the name of the file created, if any. - - """ - if outfilename == '-': - stream = sys.stdout - close_stream = False - elif outfilename: - stream = open(outfilename, 'w') - close_stream = True - else: - outfilename = loader.get_cache_filename(conf) - if not os.path.exists(os.path.dirname(outfilename)): - os.makedirs(os.path.dirname(outfilename)) - stream = open(outfilename, 'w') - close_stream = True - try: - cache = build_cache_db( - conf, - versions_to_include=versions_to_include, - ) - yaml.safe_dump( - cache, - stream, - allow_unicode=True, - explicit_start=True, - encoding='utf-8', - ) - finally: - if close_stream: - stream.close() - return outfilename - - -def cache_cmd(args, conf): - "Generates a release notes cache" - write_cache_db( - conf=conf, - versions_to_include=args.version, - outfilename=args.output, - ) - return diff --git a/reno/config.py b/reno/config.py deleted file mode 100644 index d44c72c..0000000 --- a/reno/config.py +++ /dev/null @@ -1,251 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging -import os.path - -import yaml - -from reno import defaults - -LOG = logging.getLogger(__name__) - - -class Config(object): - - _OPTS = { - # The notes subdirectory within the relnotesdir where the - # notes live. - 'notesdir': defaults.NOTES_SUBDIR, - - # Should pre-release versions be merged into the final release - # of the same number (1.0.0.0a1 notes appear under 1.0.0). - 'collapse_pre_releases': True, - - # Should the scanner stop at the base of a branch (True) or go - # ahead and scan the entire history (False)? - 'stop_at_branch_base': True, - - # The git branch to scan. Defaults to the "current" branch - # checked out. - 'branch': None, - - # The earliest version to be included. This is usually the - # lowest version number, and is meant to be the oldest - # version. - 'earliest_version': None, - - # The template used by reno new to create a note. - 'template': defaults.TEMPLATE.format(defaults.PRELUDE_SECTION_NAME), - - # The RE pattern used to match the repo tags representing a valid - # release version. The pattern is compiled with the verbose and unicode - # flags enabled. - 'release_tag_re': ''' - ((?:[\d.ab]|rc)+) # digits, a, b, and rc cover regular and - # pre-releases - ''', - - # The RE pattern used to check if a valid release version tag is also a - # valid pre-release version. The pattern is compiled with the verbose - # and unicode flags enabled. The pattern must define a group called - # 'pre_release' that matches the pre-release part of the tag and any - # separator, e.g for pre-release version '12.0.0.0rc1' the default RE - # pattern will identify '.0rc1' as the value of the group - # 'pre_release'. - 'pre_release_tag_re': ''' - (?P\.\d+(?:[ab]|rc)+\d*)$ - ''', - - # The pattern for names for branches that are relevant when - # scanning history to determine where to stop, to find the - # "base" of a branch. Other branches are ignored. - 'branch_name_re': 'stable/.+', - - # The identifiers and names of permitted sections in the - # release notes, in the order in which the final report will - # be generated. A prelude section will always be automatically - # inserted before the first element of this list. - 'sections': [ - ['features', 'New Features'], - ['issues', 'Known Issues'], - ['upgrade', 'Upgrade Notes'], - ['deprecations', 'Deprecation Notes'], - ['critical', 'Critical Issues'], - ['security', 'Security Issues'], - ['fixes', 'Bug Fixes'], - ['other', 'Other Notes'], - ], - - # The name of the prelude section in the note template. This - # allows users to rename the section to, for example, - # 'release_summary' or 'project_wide_general_announcements', - # which is displayed in titlecase in the report after - # replacing underscores with spaces. - 'prelude_section_name': defaults.PRELUDE_SECTION_NAME, - - # When this option is set to True, any merge commits with no - # changes and in which the second or later parent is tagged - # are considered "null-merges" that bring the tag information - # into the current branch but nothing else. - # - # OpenStack used to use null-merges to bring final release - # tags from stable branches back into the master branch. This - # confuses the regular traversal because it makes that stable - # branch appear to be part of master and/or the later stable - # branch. This option allows us to ignore those. - 'ignore_null_merges': True, - - # Note files to be ignored. It's useful to be able to ignore a - # file if it is edited on the wrong branch. Notes should be - # specified by their filename or UID. Setting the value in the - # configuration file makes it apply to all branches. - 'ignore_notes': [], - } - - @classmethod - def get_default(cls, opt): - "Return the default for an option." - try: - return cls._OPTS[opt] - except KeyError: - raise ValueError('unknown option name %r' % (opt,)) - - def __init__(self, reporoot, relnotesdir=None): - """Instantiate a Config object - - :param str reporoot: - The root directory of the repository. - :param str relnotesdir: - The directory containing release notes. Defaults to - 'releasenotes'. - """ - self.reporoot = reporoot - if relnotesdir is None: - relnotesdir = defaults.RELEASE_NOTES_SUBDIR - self.relnotesdir = relnotesdir - # Initialize attributes from the defaults. - self.override(**self._OPTS) - - self._contents = {} - self._load_file() - - def _load_file(self): - filenames = [ - os.path.join(self.reporoot, self.relnotesdir, 'config.yaml'), - os.path.join(self.reporoot, 'reno.yaml')] - - for filename in filenames: - if os.path.isfile(filename): - break - else: - LOG.info('no configuration file in: %s', ', '.join(filenames)) - return - - try: - with open(filename, 'r') as fd: - self._contents = yaml.safe_load(fd) - except IOError as err: - LOG.warning('did not load config file %s: %s', filename, err) - else: - self.override(**self._contents) - - def _rename_prelude_section(self, **kwargs): - key = 'prelude_section_name' - if key in kwargs and kwargs[key] != self._OPTS[key]: - new_prelude_name = kwargs[key] - - self.template = defaults.TEMPLATE.format(new_prelude_name) - - def override(self, **kwds): - """Set the values of the named configuration options. - - Take the values of the keyword arguments as the current value - of the same option, regardless of whether a value is already - present. - - """ - # Replace prelude section name if it has been changed. - self._rename_prelude_section(**kwds) - - for n, v in kwds.items(): - if n not in self._OPTS: - LOG.warning('ignoring unknown configuration value %r = %r', - n, v) - else: - setattr(self, n, v) - - def override_from_parsed_args(self, parsed_args): - """Set the values of the configuration options from parsed CLI args. - - This method assumes that the DEST values for the command line - arguments are named the same as the configuration options. - - """ - arg_values = { - o: getattr(parsed_args, o) - for o in self._OPTS.keys() - if hasattr(parsed_args, o) - } - self.override(**arg_values) - - @property - def reporoot(self): - return self._reporoot - - # Ensure that the 'reporoot' value always only ends in one '/'. - @reporoot.setter - def reporoot(self, value): - self._reporoot = value.rstrip('/') + '/' - - @property - def notespath(self): - """The path in the repo where notes are kept. - - .. important:: - - This does not take ``reporoot`` into account. You need to add this - manually if required. - """ - return os.path.join(self.relnotesdir, self.notesdir) - - @property - def options(self): - """Get all configuration options as a dict. - - Returns the actual configuration options after overrides. - """ - options = {o: getattr(self, o) for o in self._OPTS} - return options - -# def parse_config_into(parsed_arguments): - -# """Parse the user config onto the namespace arguments. - -# :param parsed_arguments: -# The result of calling :meth:`argparse.ArgumentParser.parse_args`. -# :type parsed_arguments: -# argparse.Namespace -# """ -# config_path = get_config_path(parsed_arguments.relnotesdir) -# config_values = read_config(config_path) - -# for key in config_values.keys(): -# try: -# getattr(parsed_arguments, key) -# except AttributeError: -# LOG.info('Option "%s" does not apply to this particular command.' -# '. Ignoring...', key) -# continue -# setattr(parsed_arguments, key, config_values[key]) - -# parsed_arguments._config = config_values diff --git a/reno/create.py b/reno/create.py deleted file mode 100644 index 0b5e0ea..0000000 --- a/reno/create.py +++ /dev/null @@ -1,65 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - -import os -import subprocess - -from reno import utils - - -def _pick_note_file_name(notesdir, slug): - "Pick a unique name in notesdir." - for i in range(50): - newid = utils.get_random_string() - notefilename = os.path.join(notesdir, '%s-%s.yaml' % (slug, newid)) - if not os.path.exists(notefilename): - return notefilename - else: - raise ValueError( - 'Unable to generate unique random filename ' - 'in %s after 50 tries' % notesdir, - ) - - -def _make_note_file(filename, template): - notesdir = os.path.dirname(filename) - if not os.path.exists(notesdir): - os.makedirs(notesdir) - with open(filename, 'w') as f: - f.write(template) - - -def _edit_file(filename): - if 'EDITOR' not in os.environ: - return False - subprocess.call([os.environ['EDITOR'], filename]) - return True - - -def create_cmd(args, conf): - "Create a new release note file from the template." - # NOTE(dhellmann): There is a short race window where we might try - # to pick a name that does not exist, then overwrite the file if - # it is created before we try to write it. This isn't a problem - # because this command is expected to be run by one developer in - # their local git tree, and so there should not be any concurrency - # concern. - slug = args.slug.replace(' ', '-') - filename = _pick_note_file_name(conf.notespath, slug) - _make_note_file(filename, conf.template) - if args.edit and not _edit_file(filename): - print('Was unable to edit the new note. EDITOR environment variable ' - 'is missing!') - print('Created new notes file in %s' % filename) - return diff --git a/reno/defaults.py b/reno/defaults.py deleted file mode 100644 index 927656d..0000000 --- a/reno/defaults.py +++ /dev/null @@ -1,83 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -RELEASE_NOTES_SUBDIR = 'releasenotes' -NOTES_SUBDIR = 'notes' -PRELUDE_SECTION_NAME = 'prelude' -# This is a format string, so it needs to be formatted wherever it is used. -TEMPLATE = """\ ---- -{0}: > - Replace this text with content to appear at the top of the section for this - release. All of the prelude content is merged together and then rendered - separately from the items listed in other parts of the file, so the text - needs to be worded so that both the prelude and the other items make sense - when read independently. This may mean repeating some details. Not every - release note requires a prelude. Usually only notes describing major - features or adding release theme details should have a prelude. -features: - - | - List new features here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -issues: - - | - List known issues here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -upgrade: - - | - List upgrade notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -deprecations: - - | - List deprecations notes here, or remove this section. All of the list - items in this section are combined when the release notes are rendered, so - the text needs to be worded so that it does not depend on any information - only available in another section, such as the prelude. This may mean - repeating some details. -critical: - - | - Add critical notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -security: - - | - Add security notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -fixes: - - | - Add normal bug fixes here, or remove this section. All of the list items - in this section are combined when the release notes are rendered, so the - text needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -other: - - | - Add other notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -""" diff --git a/reno/formatter.py b/reno/formatter.py deleted file mode 100644 index 4092a8d..0000000 --- a/reno/formatter.py +++ /dev/null @@ -1,83 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - - -def _indent_for_list(text, prefix=' '): - """Indent some text to make it work as a list entry. - - Indent all lines except the first with the prefix. - """ - lines = text.splitlines() - return '\n'.join([lines[0]] + [ - prefix + l - for l in lines[1:] - ]) + '\n' - - -def format_report(loader, config, versions_to_include, title=None, - show_source=True): - report = [] - if title: - report.append('=' * len(title)) - report.append(title) - report.append('=' * len(title)) - report.append('') - - # Read all of the notes files. - file_contents = {} - for version in versions_to_include: - for filename, sha in loader[version]: - body = loader.parse_note_file(filename, sha) - file_contents[filename] = body - - for version in versions_to_include: - report.append(version) - report.append('=' * len(version)) - report.append('') - - # Add the preludes. - notefiles = loader[version] - prelude_name = config.prelude_section_name - notefiles_with_prelude = [(n, sha) for n, sha in notefiles - if prelude_name in file_contents[n]] - if notefiles_with_prelude: - report.append(prelude_name.replace('_', ' ').title()) - report.append('-' * len(prelude_name)) - report.append('') - - for n, sha in notefiles_with_prelude: - if show_source: - report.append('.. %s @ %s\n' % (n, sha)) - report.append(file_contents[n][prelude_name]) - report.append('') - - # Add other sections. - for section_name, section_title in config.sections: - notes = [ - (n, fn, sha) - for fn, sha in notefiles - if file_contents[fn].get(section_name) - for n in file_contents[fn].get(section_name, []) - ] - if notes: - report.append(section_title) - report.append('-' * len(section_title)) - report.append('') - for n, fn, sha in notes: - if show_source: - report.append('.. %s @ %s\n' % (fn, sha)) - report.append('- %s' % _indent_for_list(n)) - report.append('') - - return '\n'.join(report) diff --git a/reno/linter.py b/reno/linter.py deleted file mode 100644 index 9cbda99..0000000 --- a/reno/linter.py +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - -import glob -import logging -import os.path - -from reno import loader -from reno import scanner - -LOG = logging.getLogger(__name__) - - -def lint_cmd(args, conf): - "Check some common mistakes" - LOG.debug('starting lint') - notesdir = os.path.join(conf.reporoot, conf.notespath) - notes = glob.glob(os.path.join(notesdir, '*.yaml')) - - error = 0 - load = loader.Loader(conf, ignore_cache=True) - allowed_section_names = [conf.prelude_section_name] + \ - [s[0] for s in conf.sections] - - uids = {} - for f in notes: - LOG.debug('examining %s', f) - uid = scanner._get_unique_id(f) - uids.setdefault(uid, []).append(f) - - content = load.parse_note_file(f, None) - for section_name in content.keys(): - if section_name not in allowed_section_names: - LOG.warning('unrecognized section name %s in %s', - section_name, f) - error = 1 - - for uid, names in sorted(uids.items()): - if len(names) > 1: - LOG.warning('UID collision: %s', names) - error = 1 - - return error diff --git a/reno/lister.py b/reno/lister.py deleted file mode 100644 index aa1ec4d..0000000 --- a/reno/lister.py +++ /dev/null @@ -1,38 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - -import logging - -from reno import loader - -LOG = logging.getLogger(__name__) - - -def list_cmd(args, conf): - "List notes files based on query arguments" - LOG.debug('starting list') - reporoot = conf.reporoot - ldr = loader.Loader(conf) - if args.version: - versions = args.version - else: - versions = ldr.versions - for version in versions: - notefiles = ldr[version] - print(version) - for n, sha in notefiles: - if n.startswith(reporoot): - n = n[len(reporoot):] - print('\t%s (%s)' % (n, sha)) - return diff --git a/reno/loader.py b/reno/loader.py deleted file mode 100644 index 6f8dcb0..0000000 --- a/reno/loader.py +++ /dev/null @@ -1,140 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging -import os.path - -import six -import yaml - -from reno import scanner - -LOG = logging.getLogger(__name__) - - -def get_cache_filename(conf): - return os.path.normpath(os.path.join( - conf.reporoot, conf.notespath, 'reno.cache')) - - -class Loader(object): - "Load the release notes for a given repository." - - def __init__(self, conf, - ignore_cache=False): - """Initialize a Loader. - - The versions are presented in reverse chronological order. - - Notes files are associated with the earliest version for which - they were available, regardless of whether they changed later. - - :param conf: Parsed configuration from file - :type conf: reno.config.Config - :param ignore_cache: Do not load a cache file if it is present. - :type ignore_cache: bool - """ - self._config = conf - self._ignore_cache = ignore_cache - - self._reporoot = conf.reporoot - self._notespath = conf.notespath - self._branch = conf.branch - self._collapse_pre_releases = conf.collapse_pre_releases - self._earliest_version = conf.earliest_version - - self._cache = None - self._scanner = None - self._scanner_output = None - self._cache_filename = get_cache_filename(conf) - - self._load_data() - - def _load_data(self): - cache_file_exists = os.path.exists(self._cache_filename) - - if self._ignore_cache and cache_file_exists: - LOG.debug('ignoring cache file %s', self._cache_filename) - - if (not self._ignore_cache) and cache_file_exists: - with open(self._cache_filename, 'r') as f: - self._cache = yaml.safe_load(f.read()) - # Save the cached scanner output to the same attribute - # it would be in if we had loaded it "live". This - # simplifies some of the logic in the other methods. - self._scanner_output = { - n['version']: n['files'] - for n in self._cache['notes'] - } - else: - self._scanner = scanner.Scanner(self._config) - self._scanner_output = self._scanner.get_notes_by_version() - - @property - def versions(self): - "A list of all of the versions found." - return list(self._scanner_output.keys()) - - def __getitem__(self, version): - "Return data about the files that should go into a given version." - return self._scanner_output[version] - - def parse_note_file(self, filename, sha): - """Return the data structure encoded in the note file. - - Emit warnings for content that does not look valid in some - way, but return it anyway for backwards-compatibility. - - """ - if self._cache: - content = self._cache['file-contents'][filename] - else: - body = self._scanner.get_file_at_commit(filename, sha) - content = yaml.safe_load(body) - - cleaned_content = {} - - for section_name, section_content in content.items(): - if section_name == self._config.prelude_section_name: - if not isinstance(section_content, six.string_types): - LOG.warning( - ('The %s section of %s ' - 'does not parse as a single string. ' - 'Is the YAML input escaped properly?') % - (self._config.prelude_section_name, filename), - ) - else: - if isinstance(section_content, six.string_types): - # A single string is OK, but wrap it with a list - # so the rest of the code can treat the data model - # consistently. - section_content = [section_content] - elif not isinstance(section_content, list): - LOG.warning( - ('The %s section of %s ' - 'does not parse as a string or list of strings. ' - 'Is the YAML input escaped properly?') % ( - section_name, filename), - ) - else: - for item in section_content: - if not isinstance(item, six.string_types): - LOG.warning( - ('The item %r in the %s section of %s ' - 'parses as a %s instead of a string. ' - 'Is the YAML input escaped properly?' - ) % (item, section_name, - filename, type(item)), - ) - cleaned_content[section_name] = section_content - - return cleaned_content diff --git a/reno/main.py b/reno/main.py deleted file mode 100644 index 3237661..0000000 --- a/reno/main.py +++ /dev/null @@ -1,198 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import argparse -import logging -import sys - -from reno import cache -from reno import config -from reno import create -from reno import defaults -from reno import linter -from reno import lister -from reno import report - -_query_args = [ - (('--version',), - dict(default=[], - action='append', - help='the version(s) to include, defaults to all')), - (('--branch',), - dict(default=config.Config.get_default('branch'), - help='the branch to scan, defaults to the current')), - (('--collapse-pre-releases',), - dict(action='store_true', - default=config.Config.get_default('collapse_pre_releases'), - help='combine pre-releases with their final release')), - (('--no-collapse-pre-releases',), - dict(action='store_false', - dest='collapse_pre_releases', - help='show pre-releases separately')), - (('--earliest-version',), - dict(default=None, - help='stop when this version is reached in the history')), - (('--ignore-cache',), - dict(default=False, - action='store_true', - help='if there is a cache file present, do not use it')), - (('--stop-at-branch-base',), - dict(action='store_true', - default=True, - dest='stop_at_branch_base', - help='stop scanning when the branch meets master')), - (('--no-stop-at-branch-base',), - dict(action='store_false', - dest='stop_at_branch_base', - help='do not stop scanning when the branch meets master')), -] - - -def _build_query_arg_group(parser): - group = parser.add_argument_group('query') - for args, kwds in _query_args: - group.add_argument(*args, **kwds) - - -def main(argv=sys.argv[1:]): - parser = argparse.ArgumentParser() - parser.add_argument( - '-v', '--verbose', - dest='verbosity', - default=logging.INFO, - help='produce more output', - action='store_const', - const=logging.DEBUG, - ) - parser.add_argument( - '-q', '--quiet', - dest='verbosity', - action='store_const', - const=logging.WARN, - help='produce less output', - ) - parser.add_argument( - '--rel-notes-dir', '-d', - dest='relnotesdir', - default=defaults.RELEASE_NOTES_SUBDIR, - help='location of release notes YAML files', - ) - subparsers = parser.add_subparsers( - title='commands', - ) - - do_new = subparsers.add_parser( - 'new', - help='create a new note', - ) - do_new.add_argument( - '--edit', - action='store_true', - help='Edit note after its creation (require EDITOR env variable)', - ) - do_new.add_argument( - 'slug', - help='descriptive title of note (keep it short)', - ) - do_new.add_argument( - 'reporoot', - default='.', - nargs='?', - help='root of the git repository', - ) - do_new.set_defaults(func=create.create_cmd) - - do_list = subparsers.add_parser( - 'list', - help='list notes files based on query arguments', - ) - _build_query_arg_group(do_list) - do_list.add_argument( - 'reporoot', - default='.', - nargs='?', - help='root of the git repository', - ) - do_list.set_defaults(func=lister.list_cmd) - - do_report = subparsers.add_parser( - 'report', - help='generate release notes report', - ) - do_report.add_argument( - 'reporoot', - default='.', - nargs='?', - help='root of the git repository', - ) - do_report.add_argument( - '--output', '-o', - default=None, - help='output filename, defaults to stdout', - ) - do_report.add_argument( - '--no-show-source', - dest='show_source', - default=True, - action='store_false', - help='do not show the source for notes', - ) - do_report.add_argument( - '--title', - default='Release Notes', - help='set the main title of the generated report', - ) - _build_query_arg_group(do_report) - do_report.set_defaults(func=report.report_cmd) - - do_cache = subparsers.add_parser( - 'cache', - help='generate release notes cache', - ) - do_cache.add_argument( - 'reporoot', - default='.', - nargs='?', - help='root of the git repository', - ) - do_cache.add_argument( - '--output', '-o', - default=None, - help=('output filename, ' - 'defaults to the cache file within the notesdir, ' - 'use "-" for stdout'), - ) - _build_query_arg_group(do_cache) - do_cache.set_defaults(func=cache.cache_cmd) - - do_linter = subparsers.add_parser( - 'lint', - help='check some common mistakes', - ) - do_linter.add_argument( - 'reporoot', - default='.', - nargs='?', - help='root of the git repository', - ) - do_linter.set_defaults(func=linter.lint_cmd) - - args = parser.parse_args(argv) - conf = config.Config(args.reporoot, args.relnotesdir) - conf.override_from_parsed_args(args) - - logging.basicConfig( - level=args.verbosity, - format='%(message)s', - ) - - return args.func(args, conf) diff --git a/reno/report.py b/reno/report.py deleted file mode 100644 index aab6be0..0000000 --- a/reno/report.py +++ /dev/null @@ -1,38 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - -from reno import formatter -from reno import loader - - -def report_cmd(args, conf): - "Generates a release notes report" - ldr = loader.Loader(conf) - if args.version: - versions = args.version - else: - versions = ldr.versions - text = formatter.format_report( - ldr, - conf, - versions, - title=args.title, - show_source=args.show_source, - ) - if args.output: - with open(args.output, 'w') as f: - f.write(text) - else: - print(text) - return diff --git a/reno/scanner.py b/reno/scanner.py deleted file mode 100644 index 69942d2..0000000 --- a/reno/scanner.py +++ /dev/null @@ -1,1196 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - -import collections -import fnmatch -import logging -import os.path -import re -import sys - -from dulwich import diff_tree -from dulwich import index as d_index -from dulwich import objects -from dulwich import porcelain -from dulwich import repo - -LOG = logging.getLogger(__name__) - - -def _parse_version(v): - parts = v.split('.') + ['0', '0', '0'] - result = [] - for p in parts[:3]: - try: - result.append(int(p)) - except ValueError: - result.append(p) - return result - - -def _get_unique_id(filename): - base = os.path.basename(filename) - root, ext = os.path.splitext(base) - uniqueid = root[-16:] - if '-' in uniqueid: - # This is an older file with the UUID at the beginning - # of the name. - uniqueid = root[:16] - return uniqueid - - -def _note_file(name): - """Return bool indicating if the filename looks like a note file. - - This is used to filter the files in changes based on the notes - directory we were given. We cannot do this in the walker directly - because it means we end up skipping some of the tags if the - commits being tagged don't include any release note files. - - """ - if not name: - return False - if fnmatch.fnmatch(name, '*.yaml'): - return True - else: - LOG.info('found and ignored extra file %s', name) - return False - - -def _changes_in_subdir(repo, walk_entry, subdir): - """Iterator producing changes of interest to reno. - - The default changes() method of a WalkEntry computes all of the - changes in the entire repo at that point. We only care about - changes in a subdirectory, so this reimplements - WalkeEntry.changes() with that filter in place. - - The alternative, passing paths to the TreeWalker, does not work - because we need all of the commits in sequence so we can tell when - the tag changes. We have to look at every commit to see if it - either has a tag, a note file, or both. - - NOTE(dhellmann): The TreeChange entries returned as a result of - the manipulation done by this function have the subdir prefix - stripped. - - """ - commit = walk_entry.commit - store = repo.object_store - - parents = walk_entry._get_parents(commit) - - if not parents: - changes_func = diff_tree.tree_changes - parent_subtree = None - elif len(parents) == 1: - changes_func = diff_tree.tree_changes - parent_tree = repo[repo[parents[0]].tree] - parent_subtree = repo._get_subtree(parent_tree, subdir) - if parent_subtree: - parent_subtree = parent_subtree.sha().hexdigest().encode('ascii') - else: - changes_func = diff_tree.tree_changes_for_merge - parent_subtree = [ - repo._get_subtree(repo[repo[p].tree], subdir) - for p in parents - ] - parent_subtree = [ - p.sha().hexdigest().encode('ascii') - for p in parent_subtree - if p - ] - subdir_tree = repo._get_subtree(repo[commit.tree], subdir) - if subdir_tree: - commit_subtree = subdir_tree.sha().hexdigest().encode('ascii') - else: - commit_subtree = None - if parent_subtree == commit_subtree: - return [] - return changes_func(store, parent_subtree, commit_subtree) - - -class _ChangeAggregator(object): - """Collapse a series of changes based on uniqueness for file uids. - - The list of TreeChange instances describe changes between the old - and new repository trees. The change has a type, and new and old - paths and shas. - - Simple add, delete, and change operations are handled directly. - - There is a rename type, but detection of renamed files is - incomplete so we handle that ourselves based on the UID value - built into the filenames (under the assumption that if someone - changes that part of the filename they want it treated as a - different file for some reason). If we see both an add and a - delete for a given UID treat that as a rename. - - The SHA values returned are for the commit, rather than the blob - values in the TreeChange objects. - - The path values in the change entries are encoded, so we return - the decoded values to make consuming them easier. - - """ - - _rename_op = set([diff_tree.CHANGE_ADD, diff_tree.CHANGE_DELETE]) - _modify_op = set([diff_tree.CHANGE_MODIFY]) - _delete_op = set([diff_tree.CHANGE_DELETE]) - _add_op = set([diff_tree.CHANGE_ADD]) - - def __init__(self): - # Track UIDs that had a duplication issue but have been - # deleted so we know not to throw an error for them. - self._deleted_bad_uids = set() - - def aggregate_changes(self, walk_entry, changes): - sha = walk_entry.commit.id - by_uid = collections.defaultdict(list) - for ec in changes: - if not isinstance(ec, list): - ec = [ec] - else: - ec = ec - for c in ec: - LOG.debug('change %r', c) - if c.type == diff_tree.CHANGE_ADD: - path = c.new.path.decode('utf-8') if c.new.path else None - if _note_file(path): - uid = _get_unique_id(path) - by_uid[uid].append((c.type, path, sha)) - else: - LOG.debug('ignoring') - elif c.type == diff_tree.CHANGE_DELETE: - path = c.old.path.decode('utf-8') if c.old.path else None - if _note_file(path): - uid = _get_unique_id(path) - by_uid[uid].append((c.type, path, sha)) - else: - LOG.debug('ignoring') - elif c.type == diff_tree.CHANGE_MODIFY: - path = c.new.path.decode('utf-8') if c.new.path else None - if _note_file(path): - uid = _get_unique_id(path) - by_uid[uid].append((c.type, path, sha)) - else: - LOG.debug('ignoring') - else: - raise ValueError('unhandled change type: {!r}'.format(c)) - - results = [] - for uid, changes in sorted(by_uid.items()): - if len(changes) == 1: - results.append((uid,) + changes[0]) - else: - types = set(c[0] for c in changes) - if types == self._rename_op: - # A rename, combine the data from the add and - # delete entries. - added = [ - c for c in changes if c[0] == diff_tree.CHANGE_ADD - ][0] - deled = [ - c for c in changes if c[0] == diff_tree.CHANGE_DELETE - ][0] - results.append( - (uid, diff_tree.CHANGE_RENAME, deled[1]) + added[1:] - ) - elif types == self._modify_op: - # Merge commit with modifications to the same files in - # different commits. - for c in changes: - results.append((uid, diff_tree.CHANGE_MODIFY, - c[1], sha)) - elif types == self._delete_op: - # There were multiple files in one commit using the - # same UID but different slugs. Treat them as - # different files and allow them to be deleted. - results.extend( - (uid, diff_tree.CHANGE_DELETE, c[1], sha) - for c in changes - ) - self._deleted_bad_uids.add(uid) - elif types == self._add_op: - # There were multiple files in one commit using the - # same UID but different slugs. Warn the user about - # this case and then ignore the files. We allow delete - # (see above) to ensure they can be cleaned up. - msg = ('%s: found several files in one commit (%s)' - ' with the same UID: %s' % - (uid, sha, [c[1] for c in changes])) - if uid not in self._deleted_bad_uids: - raise ValueError(msg) - else: - LOG.info(msg) - else: - raise ValueError('Unrecognized changes: {!r}'.format( - changes)) - return results - - -class _ChangeTracker(object): - - def __init__(self): - # Track the versions we have seen and the earliest version for - # which we have seen a given note's unique id. - self.versions = [] - self.earliest_seen = collections.OrderedDict() - # Remember the most current filename for each id, to allow for - # renames. - self.last_name_by_id = {} - # Remember uniqueids that have had files deleted. - self.uniqueids_deleted = set() - - def _common(self, uniqueid, sha, version): - if version not in self.versions: - self.versions.append(version) - # Update the "earliest" version where a UID appears - # every time we see it, because we are scanning the - # history in reverse order so "early" items come - # later. - if uniqueid in self.earliest_seen: - LOG.debug('%s: resetting earliest reference from %s to %s for %s', - uniqueid, self.earliest_seen[uniqueid], version, sha) - else: - LOG.debug('%s: setting earliest reference to %s for %s', - uniqueid, version, sha) - self.earliest_seen[uniqueid] = version - - def add(self, filename, sha, version): - uniqueid = _get_unique_id(filename) - self._common(uniqueid, sha, version) - LOG.info('%s: adding %s from %s', - uniqueid, filename, version) - - # If we have recorded that a UID was deleted, that - # means that was the last change made to the file and - # we can ignore it. - if uniqueid in self.uniqueids_deleted: - LOG.debug( - '%s: has already been deleted, ignoring this change', - uniqueid, - ) - return - - # A note is being added in this commit. If we have - # not seen it before, it was added here and never - # changed. - if uniqueid not in self.last_name_by_id: - self.last_name_by_id[uniqueid] = (filename, sha) - LOG.info( - '%s: new %s in commit %s', - uniqueid, filename, sha, - ) - else: - LOG.debug( - '%s: add for file we have already seen', - uniqueid, - ) - - def rename(self, filename, sha, version): - uniqueid = _get_unique_id(filename) - self._common(uniqueid, sha, version) - - # If we have recorded that a UID was deleted, that - # means that was the last change made to the file and - # we can ignore it. - if uniqueid in self.uniqueids_deleted: - LOG.debug( - '%s: has already been deleted, ignoring this change', - uniqueid, - ) - return - - # The file is being renamed. We may have seen it - # before, if there were subsequent modifications, - # so only store the name information if it is not - # there already. - if uniqueid not in self.last_name_by_id: - self.last_name_by_id[uniqueid] = (filename, sha) - LOG.info( - '%s: update to %s in commit %s', - uniqueid, filename, sha, - ) - else: - LOG.debug( - '%s: renamed file already known with the new name', - uniqueid, - ) - - def modify(self, filename, sha, version): - uniqueid = _get_unique_id(filename) - self._common(uniqueid, sha, version) - - # If we have recorded that a UID was deleted, that - # means that was the last change made to the file and - # we can ignore it. - if uniqueid in self.uniqueids_deleted: - LOG.debug( - '%s: has already been deleted, ignoring this change', - uniqueid, - ) - return - - # An existing file is being modified. We may have - # seen it before, if there were subsequent - # modifications, so only store the name - # information if it is not there already. - if uniqueid not in self.last_name_by_id: - self.last_name_by_id[uniqueid] = (filename, sha) - LOG.info( - '%s: update to %s in commit %s', - uniqueid, filename, sha, - ) - else: - LOG.debug( - '%s: modified file already known', - uniqueid, - ) - - def delete(self, filename, sha, version): - uniqueid = _get_unique_id(filename) - self._common(uniqueid, sha, version) - # This file is being deleted without a rename. If - # we have already seen the UID before, that means - # that after the file was deleted another file - # with the same UID was added back. In that case - # we do not want to treat it as deleted. - # - # Never store deleted files in last_name_by_id so - # we can safely use all of those entries to build - # the history data. - if uniqueid not in self.last_name_by_id: - self.uniqueids_deleted.add(uniqueid) - LOG.info( - '%s: note deleted in %s', - uniqueid, sha, - ) - else: - LOG.debug( - '%s: delete for file re-added after the delete', - uniqueid, - ) - - -class RenoRepo(repo.Repo): - - # Populated by _load_tags(). - _all_tags = None - _shas_to_tags = None - - def _get_commit_from_tag(self, tag, tag_sha): - """Return the commit referenced by the tag and when it was tagged.""" - tag_obj = self[tag_sha] - - if isinstance(tag_obj, objects.Tag): - # A signed tag has its own SHA, but the tag refers to - # the commit and that's the SHA we'll see when we scan - # commits on a branch. - git_obj = tag_obj - while True: - # Tags can point to other tags, in such cases follow the chain - # of tags until there are no more. - child_obj = self[git_obj.object[1]] - if isinstance(child_obj, objects.Tag): - git_obj = child_obj - else: - break - - tagged_sha = git_obj.object[1] - date = tag_obj.tag_time - elif isinstance(tag_obj, objects.Commit): - # Unsigned tags refer directly to commits. This seems - # to especially happen when the tag definition moves - # to the packed-refs list instead of being represented - # by its own file. - tagged_sha = tag_obj.id - date = tag_obj.commit_time - else: - raise ValueError( - ('Unrecognized tag object {!r} with ' - 'tag {} and SHA {!r}: {}').format( - tag_obj, tag, tag_sha, type(tag_obj)) - ) - return tagged_sha, date - - def _load_tags(self): - self._all_tags = { - k.partition(b'/tags/')[-1].decode('utf-8'): v - for k, v in self.get_refs().items() - if k.startswith(b'refs/tags/') - } - self._shas_to_tags = {} - for tag, tag_sha in self._all_tags.items(): - tagged_sha, date = self._get_commit_from_tag(tag, tag_sha) - self._shas_to_tags.setdefault(tagged_sha, []).append((tag, date)) - - def get_tags_on_commit(self, sha): - "Return the tag(s) on a commit, in application order." - if self._all_tags is None: - self._load_tags() - tags_and_dates = self._shas_to_tags.get(sha, []) - tags_and_dates.sort(key=lambda x: x[1]) - return [t[0] for t in tags_and_dates] - - def _get_subtree(self, tree, path): - "Given a tree SHA and a path, return the SHA of the subtree." - try: - mode, tree_sha = tree.lookup_path(self.get_object, - path.encode('utf-8')) - except KeyError: - # Some part of the path wasn't found, so the subtree is - # not present. Return the sentinel value. - return None - else: - tree = self[tree_sha] - return tree - - def get_file_at_commit(self, filename, sha): - """Return the contents of the file. - - If sha is None, return the working copy of the file. If the - file cannot be read from the working dir, return None. - - If the sha is not None and the file exists at the commit, - return the data from the stored blob. If the file does not - exist at the commit, return None. - - """ - if sha is None: - # Get the copy from the working directory. - try: - with open(os.path.join(self.path, filename), 'r') as f: - return f.read() - except IOError: - return None - # Get the tree associated with the commit identified by the - # input SHA, then look through the items in the tree to find - # the one with the path matching the filename. Take the - # associated SHA from the tree and get the file contents from - # the repository. - if hasattr(sha, 'encode'): - sha = sha.encode('ascii') - commit = self[sha] - tree = self[commit.tree] - try: - mode, blob_sha = tree.lookup_path(self.get_object, - filename.encode('utf-8')) - except KeyError: - # Some part of the filename wasn't found, so the file is - # not present. Return the sentinel value. - return None - else: - blob = self[blob_sha] - return blob.data - - -class Scanner(object): - - def __init__(self, conf): - self.conf = conf - self.reporoot = self.conf.reporoot - self._repo = RenoRepo(self.reporoot) - self.release_tag_re = re.compile( - self.conf.release_tag_re, - flags=re.VERBOSE | re.UNICODE, - ) - self.pre_release_tag_re = re.compile( - self.conf.pre_release_tag_re, - flags=re.VERBOSE | re.UNICODE, - ) - self.branch_name_re = re.compile( - self.conf.branch_name_re, - flags=re.VERBOSE | re.UNICODE, - ) - self._ignore_uids = set( - _get_unique_id(fn) - for fn in self.conf.ignore_notes - ) - - def _get_ref(self, name): - if name: - candidates = [ - 'refs/heads/' + name, - 'refs/remotes/' + name, - 'refs/tags/' + name, - # If a stable branch was removed, look for its EOL tag. - 'refs/tags/' + (name.rpartition('/')[-1] + '-eol'), - # If someone is using the "short" name for a branch - # without a local tracking branch, look to see if the - # name exists on the 'origin' remote. - 'refs/remotes/origin/' + name, - ] - for ref in candidates: - key = ref.encode('utf-8') - if key in self._repo.refs: - sha = self._repo.refs[key] - o = self._repo[sha] - if isinstance(o, objects.Tag): - # Branches point directly to commits, but - # signed tags point to the signature and we - # need to dereference it to get to the commit. - sha = o.object[1] - return sha - # If we end up here we didn't find any of the candidates. - raise ValueError('Unknown reference {!r}'.format(name)) - return self._repo.refs[b'HEAD'] - - def _get_walker_for_branch(self, branch): - branch_head = self._get_ref(branch) - return self._repo.get_walker(branch_head) - - def _get_valid_tags_on_commit(self, sha): - return [tag for tag in self._repo.get_tags_on_commit(sha) - if self.release_tag_re.match(tag)] - - def _get_tags_on_branch(self, branch): - "Return a list of tag names on the given branch." - results = [] - for c in self._get_walker_for_branch(branch): - # shas_to_tags has encoded versions of the shas - # but the commit object gives us a decoded version - sha = c.commit.sha().hexdigest().encode('ascii') - tags = self._get_valid_tags_on_commit(sha) - results.extend(tags) - return results - - def _get_current_version(self, branch=None): - "Return the current version of the repository, like git describe." - # This is similar to _get_tags_on_branch() except that it - # counts up to where the tag appears and it returns when it - # finds the first tagged commit (there is no need to scan the - # rest of the branch). - commit = self._repo[self._get_ref(branch)] - count = 0 - while commit: - # shas_to_tags has encoded versions of the shas - # but the commit object gives us a decoded version - sha = commit.sha().hexdigest().encode('ascii') - tags = self._get_valid_tags_on_commit(sha) - if tags: - if count: - val = '{}-{}'.format(tags[-1], count) - else: - val = tags[-1] - return val - if commit.parents: - # Only traverse the first parent of each node. - commit = self._repo[commit.parents[0]] - count += 1 - else: - commit = None - return '0.0.0' - - def _strip_pre_release(self, tag): - """Return tag with pre-release identifier removed if present.""" - pre_release_match = self.pre_release_tag_re.search(tag) - if pre_release_match: - try: - start = pre_release_match.start('pre_release') - end = pre_release_match.end('pre_release') - except IndexError: - raise ValueError( - ("The pre-release tag regular expression, {!r}, is missing" - " a group named 'pre_release'.").format( - self.pre_release_tag_re.pattern - ) - ) - else: - stripped_tag = tag[:start] + tag[end:] - else: - stripped_tag = tag - - return stripped_tag - - def _get_branch_base(self, branch): - "Return the tag at base of the branch." - # Based on - # http://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git - # git rev-list $(git rev-list --first-parent \ - # ^origin/stable/newton master | tail -n1)^^! - # - # Build the set of all commits that appear on the master - # branch, then scan the commits that appear on the specified - # branch until we find something that is on both. - master_commits = set( - c.commit.sha().hexdigest() - for c in self._get_walker_for_branch('master') - ) - for c in self._get_walker_for_branch(branch): - if c.commit.sha().hexdigest() in master_commits: - # We got to this commit via the branch, but it is also - # on master, so this is the base. - tags = self._get_valid_tags_on_commit( - c.commit.sha().hexdigest().encode('ascii')) - if tags: - return tags[-1] - else: - # Naughty, naughty, branching without tagging. - LOG.info( - ('There is no tag on commit %s at the base of %s. ' - 'Branch scan short-cutting is disabled.'), - c.commit.sha().hexdigest(), branch) - return None - return None - - def _topo_traversal(self, branch): - """Generator that yields the branch entries in topological order. - - The topo ordering in dulwich does not match the git command line - output, so we have our own that follows the branch being merged - into the mainline before following the mainline. This ensures that - tags on the mainline appear in the right place relative to the - merge points, regardless of the commit date on the entry. - - # * d1239b6 (HEAD -> master) Merge branch 'new-branch' - # |\ - # | * 9478612 (new-branch) one commit on branch - # * | 303e21d second commit on master - # * | 0ba5186 first commit on master - # |/ - # * a7f573d original commit on master - - """ - head = self._get_ref(branch) - - # Map SHA values to Entry objects, because we will be traversing - # commits not entries. - all = {} - - children = {} - - # Populate all and children structures by traversing the - # entire graph once. It doesn't matter what order we do this - # the first time, since we're just recording the relationships - # of the nodes. - for e in self._repo.get_walker(head): - all[e.commit.id] = e - for p in e.commit.parents: - children.setdefault(p, set()).add(e.commit.id) - - # Track what we have already emitted. - emitted = set() - - # Use a deque as a stack with the nodes left to process. This - # lets us avoid recursion, since we have no idea how deep some - # branches might be. - todo = collections.deque() - todo.appendleft(head) - - ignore_null_merges = self.conf.ignore_null_merges - if ignore_null_merges: - LOG.debug('ignoring null-merge commits') - - while todo: - sha = todo.popleft() - entry = all[sha] - null_merge = False - - # OpenStack used to use null-merges to bring final release - # tags from stable branches back into the master - # branch. This confuses the regular traversal because it - # makes that stable branch appear to be part of master - # and/or the later stable branch. When we hit one of those - # tags, skip it and take the first parent. - if ignore_null_merges and len(entry.commit.parents) > 1: - # Look for tags on the 2nd and later parents. The - # first parent is part of the branch we were - # originally trying to traverse, and any tags on it - # need to be kept. - for p in entry.commit.parents[1:]: - t = self._get_valid_tags_on_commit(p) - # If we have a tag being merged in, we need to - # include a check to verify that this is actually - # a null-merge (there are no changes). - if t and not entry.changes(): - LOG.debug( - 'treating %s as a null-merge because ' - 'parent %s has tag(s) %s', - sha, p, t, - ) - null_merge = True - break - if null_merge: - # Make it look like the parent entries that we're - # going to skip have been emitted so the - # bookkeeping for children works properly and we - # can continue past the merge. - emitted.update(set(entry.commit.parents[1:])) - # Make it look like the current entry was emitted - # so the bookkeeping for children works properly - # and we can continue past the merge. - emitted.add(sha) - # Now set up the first parent so it is processed - # later, as long as we haven't already processed - # it. - first_parent = entry.commit.parents[0] - if (first_parent not in todo and - first_parent not in emitted): - todo.appendleft(first_parent) - continue - - # If a node has multiple children, it is the start point - # for a branch that was merged back into the rest of the - # tree. We will have already processed the merge commit - # and are traversing either the branch that was merged in - # or the base into which it was merged. We want to stop - # traversing the branch that was merged in at the point - # where the branch was created, because we are trying to - # linearize the history. At that point, we go back to the - # merge node and take the other parent node, which should - # lead us back to the origin of the branch through the - # mainline. - unprocessed_children = [ - c - for c in children.get(sha, set()) - if c not in emitted - ] - - if not unprocessed_children: - # All children have been processed. Remember that we have - # processed this node and then emit the entry. - emitted.add(sha) - yield entry - - # Now put the parents on the stack from left to right - # so they are processed right to left. If the node is - # already on the stack, leave it to be processed in - # the original order where it was added. - # - # NOTE(dhellmann): It's not clear if this is the right - # solution, or if we should re-stack and then ignore - # duplicate emissions at the top of this - # loop. Checking if the item is already on the todo - # stack isn't very expensive, since we don't expect it - # to grow very large, but it's not clear the output - # will be produced in the right order. - for p in entry.commit.parents: - if p not in todo and p not in emitted: - todo.appendleft(p) - - else: - # Has unprocessed children. Do not emit, and do not - # restack, since when we get to the other child they will - # stack it. - pass - - def get_file_at_commit(self, filename, sha): - "Return the contents of the file if it exists at the commit, or None." - return self._repo.get_file_at_commit(filename, sha) - - def _file_exists_at_commit(self, filename, sha): - "Return true if the file exists at the given commit." - return bool(self.get_file_at_commit(filename, sha)) - - def _get_series_branches(self): - "Get branches matching the branch_name_re config option." - refs = self._repo.get_refs() - LOG.debug('refs %s', list(refs.keys())) - branch_names = set() - for r in refs.keys(): - name = None - r = r.decode('utf-8') - if r.startswith('refs/remotes/origin/'): - name = r[20:] - elif r.startswith('refs/heads/'): - name = r[11:] - if name and self.branch_name_re.search(name): - branch_names.add(name) - return list(sorted(branch_names)) - - def _get_earlier_branch(self, branch): - "Return the name of the branch created before the given branch." - # FIXME(dhellmann): Assumes branches come in order based on - # name. That may not be true for projects that branch based on - # version numbers instead of names. - if branch.startswith('origin/'): - branch = branch[7:] - LOG.debug('looking for the branch before %s', branch) - branch_names = self._get_series_branches() - if branch not in branch_names: - LOG.debug('Could not find branch %r among %s', - branch, branch_names) - return None - LOG.debug('found branches %s', branch_names) - current = branch_names.index(branch) - if current == 0: - # This is the first branch. - LOG.debug('%s appears to be the first branch', branch) - return None - previous = branch_names[current - 1] - LOG.debug('found earlier branch %s', previous) - return previous - - def _find_scan_stop_point(self, earliest_version, versions_by_date, - collapse_pre_releases, branch): - """Return the version to use to stop the scan. - - Use the list of versions_by_date to get the tag with a - different version created *before* the branch to ensure that - we include notes that go with that version that *is* in the - branch. - - :param earliest_version: Version string of the earliest - version to be included in the output. - :param versions_by_date: List of version strings in reverse - chronological order. - :param collapse_pre_releases: Boolean indicating whether we are - collapsing pre-releases or not. If false, the next tag - is used, regardless of its version. - :param branch: The name of the branch we are scanning. - - """ - if not earliest_version: - return None - earliest_parts = _parse_version(earliest_version) - try: - idx = versions_by_date.index(earliest_version) + 1 - except ValueError: - # The version we were given is not present, use a full - # scan. - return None - # We need to look for the previous branch's root. - if branch and branch != 'master': - previous_branch = self._get_earlier_branch(branch) - if not previous_branch: - # This was the first branch, so scan the whole - # history. - return None - previous_base = self._get_branch_base(previous_branch) - return previous_base - is_pre_release = bool(self.pre_release_tag_re.search(earliest_version)) - if is_pre_release and not collapse_pre_releases: - # We just take the next tag. - return versions_by_date[idx] - # We need to look for a different version. - for candidate in versions_by_date[idx:]: - parts = _parse_version(candidate) - if parts != earliest_parts: - # The candidate is a different version, use it. - return candidate - return None - - def get_notes_by_version(self): - """Return an OrderedDict mapping versions to lists of notes files. - - The versions are presented in reverse chronological order. - - Notes files are associated with the earliest version for which - they were available, regardless of whether they changed later. - - :param reporoot: Path to the root of the git repository. - :type reporoot: str - """ - - reporoot = self.reporoot - notesdir = self.conf.notespath - branch = self.conf.branch - earliest_version = self.conf.earliest_version - collapse_pre_releases = self.conf.collapse_pre_releases - stop_at_branch_base = self.conf.stop_at_branch_base - - LOG.info('scanning %s/%s (branch=%s earliest_version=%s)', - reporoot.rstrip('/'), notesdir.lstrip('/'), - branch or '*current*', earliest_version) - - # Determine the current version, which might be an unreleased or - # dev version if there are unreleased commits at the head of the - # branch in question. - current_version = self._get_current_version(branch) - LOG.debug('current repository version: %s' % current_version) - - # Determine all of the tags known on the branch, in their date - # order. We scan the commit history in topological order to ensure - # we have the commits in the right version, so we might encounter - # the tags in a different order during that phase. - versions_by_date = self._get_tags_on_branch(branch) - LOG.debug('versions by date %r' % (versions_by_date,)) - if earliest_version and earliest_version not in versions_by_date: - raise ValueError( - 'earliest-version set to unknown revision {!r}'.format( - earliest_version)) - - # If the user has told us where to stop, use that as the - # default. - scan_stop_tag = self._find_scan_stop_point( - earliest_version, versions_by_date, - collapse_pre_releases, branch) - - # If the user has not told us where to stop, try to work it - # out for ourselves. - if not branch and not earliest_version and stop_at_branch_base: - # On the current branch, stop at the point where the most - # recent branch was created, if we can find one. - LOG.debug('working on current branch without earliest_version') - branches = self._get_series_branches() - if branches: - for earlier_branch in reversed(branches): - LOG.debug('checking if current branch is later than %s', - earlier_branch) - scan_stop_tag = self._get_branch_base(earlier_branch) - if scan_stop_tag in versions_by_date: - LOG.info( - 'looking at %s at base of %s to ' - 'stop scanning the current branch', - scan_stop_tag, earlier_branch - ) - break - else: - LOG.info('unable to find the previous branch base') - scan_stop_tag = None - if scan_stop_tag: - # If there is a tag on this branch after the point - # where the earlier branch was created, then use that - # tag as the earliest version to show in the current - # "series". If there is no such tag, then go all the - # way to the base of that earlier branch. - try: - idx = versions_by_date.index(scan_stop_tag) + 1 - earliest_version = versions_by_date[idx] - except ValueError: - # The scan_stop_tag is not in versions_by_date. - earliest_version = None - except IndexError: - # The idx is not in versions_by_date. - earliest_version = scan_stop_tag - elif branch and stop_at_branch_base and not earliest_version: - # If branch is set and is not "master", - # then we want to stop at the version before the tag at the - # base of the branch, which involves a bit of searching. - LOG.debug('determining earliest_version from branch') - branch_base = self._get_branch_base(branch) - scan_stop_tag = self._find_scan_stop_point( - branch_base, versions_by_date, - collapse_pre_releases, branch) - if not scan_stop_tag: - earliest_version = branch_base - else: - idx = versions_by_date.index(scan_stop_tag) - earliest_version = versions_by_date[idx - 1] - if earliest_version and collapse_pre_releases: - if self.pre_release_tag_re.search(earliest_version): - # The earliest version won't actually be the pre-release - # that might have been tagged when the branch was created, - # but the final version. Strip the pre-release portion of - # the version number. - earliest_version = self._strip_pre_release( - earliest_version - ) - if earliest_version: - LOG.info('earliest version to include is %s', earliest_version) - else: - LOG.info('including entire branch history') - if scan_stop_tag: - LOG.info('stopping scan at %s', scan_stop_tag) - - # Since the version may not already be known, make sure it is - # in the list of versions by date. And since it is the most - # recent version, go ahead and insert it at the front of the - # list. - if current_version not in versions_by_date: - versions_by_date.insert(0, current_version) - versions_by_date.insert(0, '*working-copy*') - - # Track the versions we have seen and the earliest version for - # which we have seen a given note's unique id. - tracker = _ChangeTracker() - - # Process the local index, if we are scanning the current - # branch. - if not branch: - prefix = notesdir.rstrip('/') + '/' - index = self._repo.open_index() - - # Pretend anything known to the repo and changed but not - # staged is part of the fake version '*working-copy*'. - LOG.debug('scanning unstaged changes') - for fname in d_index.get_unstaged_changes(index, self.reporoot): - fname = fname.decode('utf-8') - LOG.debug('found unstaged file %s', fname) - if fname.startswith(prefix) and _note_file(fname): - fullpath = os.path.join(self.reporoot, fname) - if os.path.exists(fullpath): - LOG.debug('found file %s', fullpath) - tracker.add(fname, None, '*working-copy*') - else: - LOG.debug('deleted file %s', fullpath) - tracker.delete(fname, None, '*working-copy*') - - # Pretend anything in the index is part of the fake - # version "*working-copy*". - LOG.debug('scanning staged schanges') - changes = porcelain.get_tree_changes(self._repo) - for fname in changes['add']: - fname = fname.decode('utf-8') - if fname.startswith(prefix) and _note_file(fname): - tracker.add(fname, None, '*working-copy*') - for fname in changes['modify']: - fname = fname.decode('utf-8') - if fname.startswith(prefix) and _note_file(fname): - tracker.modify(fname, None, '*working-copy*') - for fname in changes['delete']: - fname = fname.decode('utf-8') - if fname.startswith(prefix) and _note_file(fname): - tracker.delete(fname, None, '*working-copy*') - - aggregator = _ChangeAggregator() - - # Process the git commit history. - for counter, entry in enumerate(self._topo_traversal(branch), 1): - - sha = entry.commit.id - tags_on_commit = self._get_valid_tags_on_commit(sha) - - LOG.debug('%06d %s %s', counter, sha, tags_on_commit) - - # If there are no tags in this block, assume the most recently - # seen version. - tags = tags_on_commit - if not tags: - tags = [current_version] - else: - current_version = tags_on_commit[-1] - LOG.info('%06d %s updating current version to %s', - counter, sha, current_version) - - # Look for changes to notes files in this commit. The - # change has only the basename of the path file, so we - # need to prefix that with the notesdir before giving it - # to the tracker. - changes = _changes_in_subdir(self._repo, entry, notesdir) - for change in aggregator.aggregate_changes(entry, changes): - uniqueid = change[0] - - if uniqueid in self._ignore_uids: - LOG.info('ignoring %s based on configuration setting', - uniqueid) - continue - - c_type = change[1] - - if c_type == diff_tree.CHANGE_ADD: - path, blob_sha = change[-2:] - fullpath = os.path.join(notesdir, path) - tracker.add(fullpath, sha, current_version) - - elif c_type == diff_tree.CHANGE_DELETE: - path, blob_sha = change[-2:] - fullpath = os.path.join(notesdir, path) - tracker.delete(fullpath, sha, current_version) - - elif c_type == diff_tree.CHANGE_RENAME: - path, blob_sha = change[-2:] - fullpath = os.path.join(notesdir, path) - tracker.rename(fullpath, sha, current_version) - - elif c_type == diff_tree.CHANGE_MODIFY: - path, blob_sha = change[-2:] - fullpath = os.path.join(notesdir, path) - tracker.modify(fullpath, sha, current_version) - - else: - raise ValueError( - 'unknown change instructions {!r}'.format(change) - ) - - if scan_stop_tag and scan_stop_tag in tags: - LOG.info( - ('reached end of branch after %d commits at %s ' - 'with tags %s'), - counter, sha, tags) - break - - # Invert earliest_seen to make a list of notes files for each - # version. - files_and_tags = collections.OrderedDict() - for v in tracker.versions: - files_and_tags[v] = [] - # Produce a list of the actual files present in the repository. If - # a note is removed, this step should let us ignore it. - for uniqueid, version in tracker.earliest_seen.items(): - try: - base, sha = tracker.last_name_by_id[uniqueid] - LOG.debug('%s: sorting %s into version %s', - uniqueid, base, version) - files_and_tags[version].append((base, sha)) - except KeyError: - # Unable to find the file again, skip it to avoid breaking - # the build. - msg = ('unable to find release notes file associated ' - 'with unique id %r, skipping') % uniqueid - LOG.debug(msg) - print(msg, file=sys.stderr) - - # Combine pre-releases into the final release, if we are told to - # and the final release exists. - if collapse_pre_releases: - LOG.debug('collapsing pre-release versions into final releases') - collapsing = files_and_tags - files_and_tags = collections.OrderedDict() - for ov in versions_by_date: - if ov not in collapsing: - # We don't need to collapse this one because there are - # no notes attached to it. - continue - pre_release_match = self.pre_release_tag_re.search(ov) - LOG.debug('checking %r', ov) - if pre_release_match: - # Remove the trailing pre-release part of the version - # from the string. - canonical_ver = self._strip_pre_release(ov) - if canonical_ver not in versions_by_date: - # This canonical version was never tagged, so we - # do not want to collapse the pre-releases. Reset - # to the original version. - canonical_ver = ov - else: - LOG.debug('combining into %r', canonical_ver) - else: - canonical_ver = ov - if canonical_ver not in files_and_tags: - files_and_tags[canonical_ver] = [] - files_and_tags[canonical_ver].extend(collapsing[ov]) - - LOG.debug('files_and_tags %s', - {k: len(v) for k, v in files_and_tags.items()}) - # Only return the parts of files_and_tags that actually have - # filenames associated with the versions. - LOG.debug('trimming') - trimmed = collections.OrderedDict() - for ov in versions_by_date: - if not files_and_tags.get(ov): - continue - LOG.debug('keeping %s', ov) - # Sort the notes associated with the version so they are in a - # deterministic order, to avoid having the same data result in - # different output depending on random factors. Earlier - # versions of the scanner assumed the notes were recorded in - # chronological order based on the commit date, but with the - # change to use topological sorting that is no longer - # necessarily true. We want the notes to always show up in the - # same order, but it doesn't really matter what order that is, - # so just sort based on the unique id. - trimmed[ov] = sorted(files_and_tags[ov]) - # If we have been told to stop at a version, we can do that - # now. - if earliest_version and ov == earliest_version: - LOG.debug('stopping trimming at %s', earliest_version) - break - - LOG.debug( - 'found %d versions and %d files', - len(trimmed.keys()), sum(len(ov) for ov in trimmed.values()), - ) - return trimmed diff --git a/reno/sphinxext.py b/reno/sphinxext.py deleted file mode 100644 index 45e57ef..0000000 --- a/reno/sphinxext.py +++ /dev/null @@ -1,123 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging -import os.path - -from docutils import nodes -from docutils.parsers import rst -from docutils.parsers.rst import directives -from docutils import statemachine -from sphinx.util.nodes import nested_parse_with_titles - -from dulwich import repo -from reno import config -from reno import defaults -from reno import formatter -from reno import loader - - -class ReleaseNotesDirective(rst.Directive): - - has_content = True - - option_spec = { - 'branch': directives.unchanged, - 'reporoot': directives.unchanged, - 'relnotessubdir': directives.unchanged, - 'notesdir': directives.unchanged, - 'version': directives.unchanged, - 'collapse-pre-releases': directives.flag, - 'earliest-version': directives.unchanged, - 'stop-at-branch-base': directives.flag, - 'ignore-notes': directives.unchanged, - } - - def run(self): - env = self.state.document.settings.env - app = env.app - - def info(msg): - app.info('[reno] %s' % (msg,)) - - title = ' '.join(self.content) - branch = self.options.get('branch') - reporoot_opt = self.options.get('reporoot', '.') - reporoot = os.path.abspath(reporoot_opt) - # When building on RTD.org the root directory may not be - # the current directory, so look for it. - reporoot = repo.Repo.discover(reporoot).path - relnotessubdir = self.options.get('relnotessubdir', - defaults.RELEASE_NOTES_SUBDIR) - ignore_notes = [ - name.strip() - for name in self.options.get('ignore-notes', '').split(',') - ] - conf = config.Config(reporoot, relnotessubdir) - opt_overrides = {} - if 'notesdir' in self.options: - opt_overrides['notesdir'] = self.options.get('notesdir') - version_opt = self.options.get('version') - # FIXME(dhellmann): Force these flags True for now and figure - # out how Sphinx passes a "false" flag later. - # 'collapse-pre-releases' in self.options - opt_overrides['collapse_pre_releases'] = True - # Only stop at the branch base if we have not been told - # explicitly which versions to include. - opt_overrides['stop_at_branch_base'] = (version_opt is None) - if 'earliest-version' in self.options: - opt_overrides['earliest_version'] = self.options.get( - 'earliest-version') - if branch: - opt_overrides['branch'] = branch - if ignore_notes: - opt_overrides['ignore_notes'] = ignore_notes - conf.override(**opt_overrides) - - notesdir = os.path.join(relnotessubdir, conf.notesdir) - info('scanning %s for %s release notes' % - (os.path.join(conf.reporoot, notesdir), - branch or 'current branch')) - - ldr = loader.Loader(conf) - if version_opt is not None: - versions = [ - v.strip() - for v in version_opt.split(',') - ] - else: - versions = ldr.versions - info('got versions %s' % (versions,)) - text = formatter.format_report( - ldr, - conf, - versions, - title=title, - ) - source_name = '<%s %s>' % (__name__, branch or 'current branch') - result = statemachine.ViewList() - for line in text.splitlines(): - result.append(line, source_name) - - node = nodes.section() - node.document = self.state.document - nested_parse_with_titles(self.state, result, node) - return node.children - - -def setup(app): - app.add_directive('release-notes', ReleaseNotesDirective) - - logging.basicConfig( - level=logging.INFO, - format='[%(name)s] %(message)s', - ) diff --git a/reno/tests/__init__.py b/reno/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/reno/tests/base.py b/reno/tests/base.py deleted file mode 100644 index c1f9829..0000000 --- a/reno/tests/base.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import fixtures -import testtools - - -class TestCase(testtools.TestCase): - - """Test case base class for all unit tests.""" - - def setUp(self): - super(TestCase, self).setUp() - self._stdout_fixture = fixtures.StringStream('stdout') - self.stdout = self.useFixture(self._stdout_fixture).stream - self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout)) - self._stderr_fixture = fixtures.StringStream('stderr') - self.stderr = self.useFixture(self._stderr_fixture).stream - self.useFixture(fixtures.MonkeyPatch('sys.stderr', self.stderr)) diff --git a/reno/tests/test_cache.py b/reno/tests/test_cache.py deleted file mode 100644 index 5051bc4..0000000 --- a/reno/tests/test_cache.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import fixtures -import textwrap - -import mock - -from reno import cache -from reno import config -from reno.tests import base - - -class TestCache(base.TestCase): - - scanner_output = { - '0.0.0': [('note1', 'shaA')], - '1.0.0': [('note2', 'shaB'), ('note3', 'shaC')], - } - - note_bodies = { - 'note1': textwrap.dedent(""" - prelude: > - This is the prelude. - """), - 'note2': textwrap.dedent(""" - issues: - - This is the first issue. - - This is the second issue. - """), - 'note3': textwrap.dedent(""" - features: - - We added a feature! - """) - } - - def _get_note_body(self, filename, sha): - return self.note_bodies.get(filename, '') - - def setUp(self): - super(TestCache, self).setUp() - self.useFixture( - fixtures.MockPatch('reno.scanner.Scanner.get_file_at_commit', - new=self._get_note_body) - ) - self.c = config.Config('.') - - def test_build_cache_db(self): - with mock.patch('reno.scanner.Scanner.get_notes_by_version') as gnbv: - gnbv.return_value = self.scanner_output - db = cache.build_cache_db( - self.c, - versions_to_include=[], - ) - expected = { - 'notes': [ - {'version': k, 'files': v} - for k, v in self.scanner_output.items() - ], - 'file-contents': { - 'note1': { - 'prelude': 'This is the prelude.\n', - }, - 'note2': { - 'issues': [ - 'This is the first issue.', - 'This is the second issue.', - ], - }, - 'note3': { - 'features': ['We added a feature!'], - }, - }, - } - self.assertEqual(expected, db) diff --git a/reno/tests/test_config.py b/reno/tests/test_config.py deleted file mode 100644 index 82a4446..0000000 --- a/reno/tests/test_config.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import argparse -import os - -import fixtures - -from reno import config -from reno import defaults -from reno import main -from reno.tests import base - -import mock - - -class TestConfig(base.TestCase): - EXAMPLE_CONFIG = """ -collapse_pre_releases: false -""" - - def setUp(self): - super(TestConfig, self).setUp() - # Temporary directory to store our config - self.tempdir = self.useFixture(fixtures.TempDir()) - - def test_defaults(self): - c = config.Config(self.tempdir.path) - actual = c.options - self.assertEqual(config.Config._OPTS, actual) - - def test_override(self): - c = config.Config(self.tempdir.path) - c.override( - collapse_pre_releases=False, - ) - actual = c.options - expected = {} - expected.update(config.Config._OPTS) - expected['collapse_pre_releases'] = False - self.assertEqual(expected, actual) - - def test_override_multiple(self): - c = config.Config(self.tempdir.path) - c.override( - notesdir='value1', - ) - c.override( - notesdir='value2', - ) - actual = c.options - expected = {} - expected.update(config.Config._OPTS) - expected['notesdir'] = 'value2' - self.assertEqual(expected, actual) - - def test_load_file_not_present(self): - with mock.patch.object(config.LOG, 'info') as logger: - config.Config(self.tempdir.path) - self.assertEqual(1, logger.call_count) - - def _test_load_file(self, config_path): - with open(config_path, 'w') as fd: - fd.write(self.EXAMPLE_CONFIG) - self.addCleanup(os.unlink, config_path) - c = config.Config(self.tempdir.path) - self.assertEqual(False, c.collapse_pre_releases) - - def test_load_file_in_releasenotesdir(self): - rn_path = self.tempdir.join('releasenotes') - os.mkdir(rn_path) - config_path = self.tempdir.join('releasenotes/config.yaml') - self._test_load_file(config_path) - - def test_load_file_in_repodir(self): - config_path = self.tempdir.join('reno.yaml') - self._test_load_file(config_path) - - def test_get_default(self): - d = config.Config.get_default('notesdir') - self.assertEqual('notes', d) - - def test_get_default_unknown(self): - self.assertRaises( - ValueError, - config.Config.get_default, - 'unknownopt', - ) - - def _run_override_from_parsed_args(self, argv): - parser = argparse.ArgumentParser() - main._build_query_arg_group(parser) - args = parser.parse_args(argv) - c = config.Config(self.tempdir.path) - c.override_from_parsed_args(args) - return c - - def test_override_from_parsed_args_empty(self): - c = self._run_override_from_parsed_args([]) - actual = { - o: getattr(c, o) - for o in config.Config._OPTS.keys() - } - self.assertEqual(config.Config._OPTS, actual) - - def test_override_from_parsed_args(self): - c = self._run_override_from_parsed_args([ - '--no-collapse-pre-releases', - ]) - actual = c.options - expected = {} - expected.update(config.Config._OPTS) - expected['collapse_pre_releases'] = False - self.assertEqual(expected, actual) - - def test_override_from_parsed_args_ignore_non_options(self): - parser = argparse.ArgumentParser() - main._build_query_arg_group(parser) - parser.add_argument('not_a_config_option') - args = parser.parse_args(['value']) - c = config.Config(self.tempdir.path) - c.override_from_parsed_args(args) - self.assertFalse(hasattr(c, 'not_a_config_option')) - - -class TestConfigProperties(base.TestCase): - - def setUp(self): - super(TestConfigProperties, self).setUp() - # Temporary directory to store our config - self.tempdir = self.useFixture(fixtures.TempDir()) - self.c = config.Config('releasenotes') - - def test_reporoot(self): - self.c.reporoot = 'blah//' - self.assertEqual('blah/', self.c.reporoot) - self.c.reporoot = 'blah' - self.assertEqual('blah/', self.c.reporoot) - - def test_notespath(self): - self.assertEqual('releasenotes/notes', self.c.notespath) - self.c.override(notesdir='thenotes') - self.assertEqual('releasenotes/thenotes', self.c.notespath) - - def test_template(self): - template = defaults.TEMPLATE.format(defaults.PRELUDE_SECTION_NAME) - self.assertEqual(template, self.c.template) - self.c.override(template='i-am-a-template') - self.assertEqual('i-am-a-template', self.c.template) - - def test_prelude_override(self): - template = defaults.TEMPLATE.format(defaults.PRELUDE_SECTION_NAME) - self.assertEqual(template, self.c.template) - self.c.override(prelude_section_name='fake_prelude_name') - expected_template = defaults.TEMPLATE.format('fake_prelude_name') - self.assertEqual(expected_template, self.c.template) - - def test_prelude_and_template_override(self): - template = defaults.TEMPLATE.format(defaults.PRELUDE_SECTION_NAME) - self.assertEqual(template, self.c.template) - self.c.override(prelude_section_name='fake_prelude_name', - template='i-am-a-template') - self.assertEqual('fake_prelude_name', self.c.prelude_section_name) - self.assertEqual('i-am-a-template', self.c.template) diff --git a/reno/tests/test_create.py b/reno/tests/test_create.py deleted file mode 100644 index 2ad374a..0000000 --- a/reno/tests/test_create.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import fixtures -import mock - -from reno import create -from reno.tests import base - - -class TestPickFileName(base.TestCase): - - @mock.patch('os.path.exists') - def test_not_random_enough(self, exists): - exists.return_value = True - self.assertRaises( - ValueError, - create._pick_note_file_name, - 'somepath', - 'someslug', - ) - - @mock.patch('os.path.exists') - def test_random_enough(self, exists): - exists.return_value = False - result = create._pick_note_file_name('somepath', 'someslug') - self.assertIn('somepath', result) - self.assertIn('someslug', result) - - -class TestCreate(base.TestCase): - - def setUp(self): - super(TestCreate, self).setUp() - self.tmpdir = self.useFixture(fixtures.TempDir()).path - - def test_create_from_template(self): - filename = create._pick_note_file_name(self.tmpdir, 'theslug') - create._make_note_file(filename, 'i-am-a-template') - with open(filename, 'r') as f: - body = f.read() - self.assertEqual('i-am-a-template', body) - - def test_edit(self): - self.useFixture(fixtures.EnvironmentVariable('EDITOR', 'myeditor')) - with mock.patch('subprocess.call') as call_mock: - self.assertTrue(create._edit_file('somepath')) - call_mock.assert_called_once_with(['myeditor', 'somepath']) - - def test_edit_without_editor_env_var(self): - self.useFixture(fixtures.EnvironmentVariable('EDITOR')) - with mock.patch('subprocess.call') as call_mock: - self.assertFalse(create._edit_file('somepath')) - call_mock.assert_not_called() diff --git a/reno/tests/test_formatter.py b/reno/tests/test_formatter.py deleted file mode 100644 index 23ed3e1..0000000 --- a/reno/tests/test_formatter.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock - -from reno import config -from reno import formatter -from reno import loader -from reno.tests import base - - -class TestFormatterBase(base.TestCase): - - scanner_output = { - '0.0.0': [('note1', 'shaA')], - '1.0.0': [('note2', 'shaB'), ('note3', 'shaC')], - } - - versions = ['0.0.0', '1.0.0'] - - def _get_note_body(self, reporoot, filename, sha): - return self.note_bodies.get(filename, '') - - def setUp(self): - super(TestFormatterBase, self).setUp() - - def _load(ldr): - ldr._scanner_output = self.scanner_output - ldr._cache = { - 'file-contents': self.note_bodies - } - - self.c = config.Config('reporoot') - - with mock.patch('reno.loader.Loader._load_data', _load): - self.ldr = loader.Loader( - self.c, - ignore_cache=False, - ) - - -class TestFormatter(TestFormatterBase): - - note_bodies = { - 'note1': { - 'prelude': 'This is the prelude.', - }, - 'note2': { - 'issues': [ - 'This is the first issue.', - 'This is the second issue.', - ], - }, - 'note3': { - 'features': [ - 'We added a feature!', - ], - 'upgrade': None, - }, - } - - def test_with_title(self): - result = formatter.format_report( - loader=self.ldr, - config=self.c, - versions_to_include=self.versions, - title='This is the title', - ) - self.assertIn('This is the title', result) - - def test_versions(self): - result = formatter.format_report( - loader=self.ldr, - config=self.c, - versions_to_include=self.versions, - title='This is the title', - ) - self.assertIn('0.0.0\n=====', result) - self.assertIn('1.0.0\n=====', result) - - def test_without_title(self): - result = formatter.format_report( - loader=self.ldr, - config=self.c, - versions_to_include=self.versions, - title=None, - ) - self.assertNotIn('This is the title', result) - - def test_default_section_order(self): - result = formatter.format_report( - loader=self.ldr, - config=self.c, - versions_to_include=self.versions, - title=None, - ) - prelude_pos = result.index('This is the prelude.') - issues_pos = result.index('This is the first issue.') - features_pos = result.index('We added a feature!') - expected = [prelude_pos, features_pos, issues_pos] - actual = list(sorted([prelude_pos, features_pos, issues_pos])) - self.assertEqual(expected, actual) - - -class TestFormatterCustomSections(TestFormatterBase): - note_bodies = { - 'note1': { - 'prelude': 'This is the prelude.', - }, - 'note2': { - 'features': [ - 'This is the first feature.', - ], - 'api': [ - 'This is the API change for the first feature.', - ], - }, - 'note3': { - 'api': [ - 'This is the API change for the second feature.', - ], - 'features': [ - 'This is the second feature.', - ], - }, - } - - def setUp(self): - super(TestFormatterCustomSections, self).setUp() - self.c.override(sections=[ - ['api', 'API Changes'], - ['features', 'New Features'], - ]) - - def test_custom_section_order(self): - result = formatter.format_report( - loader=self.ldr, - config=self.c, - versions_to_include=self.versions, - title=None, - ) - prelude_pos = result.index('This is the prelude.') - api_pos = result.index('API Changes') - features_pos = result.index('New Features') - expected = [prelude_pos, api_pos, features_pos] - actual = list(sorted([prelude_pos, features_pos, api_pos])) - self.assertEqual(expected, actual) diff --git a/reno/tests/test_loader.py b/reno/tests/test_loader.py deleted file mode 100644 index 33b6d28..0000000 --- a/reno/tests/test_loader.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging -import textwrap - -import fixtures -import mock -import six -import yaml - -from reno import config -from reno import loader -from reno.tests import base - - -class TestValidate(base.TestCase): - - scanner_output = { - '0.0.0': [('note', 'shaA')], - } - - versions = ['0.0.0'] - - def setUp(self): - super(TestValidate, self).setUp() - self.logger = self.useFixture( - fixtures.FakeLogger( - format='%(message)s', - level=logging.WARNING, - ) - ) - self.c = config.Config('reporoot') - - def _make_loader(self, note_bodies): - def _load(ldr): - ldr._scanner_output = self.scanner_output - ldr._cache = { - 'file-contents': {'note1': note_bodies}, - } - - with mock.patch('reno.loader.Loader._load_data', _load): - return loader.Loader( - self.c, - ignore_cache=False, - ) - - def test_prelude_list(self): - note_bodies = yaml.safe_load(textwrap.dedent(''' - prelude: - - This is the first comment. - - This is a second. - ''')) - self.assertIsInstance(note_bodies['prelude'], list) - ldr = self._make_loader(note_bodies) - ldr.parse_note_file('note1', None) - self.assertIn('prelude', self.logger.output) - - def test_non_prelude_single_string_converted_to_list(self): - note_bodies = yaml.safe_load(textwrap.dedent(''' - issues: | - This is a single string. - ''')) - print(type(note_bodies['issues'])) - self.assertIsInstance(note_bodies['issues'], six.string_types) - ldr = self._make_loader(note_bodies) - parse_results = ldr.parse_note_file('note1', None) - self.assertIsInstance(parse_results['issues'], list) - - def test_note_with_colon_as_dict(self): - note_bodies = yaml.safe_load(textwrap.dedent(''' - issues: - - This is the first issue. - - dict: This is parsed as a dictionary. - ''')) - self.assertIsInstance(note_bodies['issues'][-1], dict) - ldr = self._make_loader(note_bodies) - ldr.parse_note_file('note1', None) - self.assertIn('dict', self.logger.output) diff --git a/reno/tests/test_scanner.py b/reno/tests/test_scanner.py deleted file mode 100644 index f8d52fa..0000000 --- a/reno/tests/test_scanner.py +++ /dev/null @@ -1,2209 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import unicode_literals - -import itertools -import logging -import os.path -import re -import subprocess -import time -import unittest - -from dulwich import diff_tree -from dulwich import objects -import fixtures -import mock -from testtools.content import text_content - -from reno import config -from reno import create -from reno import scanner -from reno.tests import base -from reno import utils - - -_SETUP_TEMPLATE = """ -import setuptools -try: - import multiprocessing # noqa -except ImportError: - pass - -setuptools.setup( - setup_requires=['pbr'], - pbr=True) -""" - -_CFG_TEMPLATE = """ -[metadata] -name = testpkg -summary = Test Package - -[files] -packages = - testpkg -""" - - -class GPGKeyFixture(fixtures.Fixture): - """Creates a GPG key for testing. - - It's recommended that this be used in concert with a unique home - directory. - """ - - def setUp(self): - super(GPGKeyFixture, self).setUp() - tempdir = self.useFixture(fixtures.TempDir()) - gnupg_version_re = re.compile('^gpg\s.*\s([\d+])\.([\d+])\.([\d+])') - gnupg_version = utils.check_output(['gpg', '--version'], - cwd=tempdir.path) - for line in gnupg_version.split('\n'): - gnupg_version = gnupg_version_re.match(line) - if gnupg_version: - gnupg_version = (int(gnupg_version.group(1)), - int(gnupg_version.group(2)), - int(gnupg_version.group(3))) - break - else: - if gnupg_version is None: - gnupg_version = (0, 0, 0) - config_file = tempdir.path + '/key-config' - f = open(config_file, 'wt') - try: - if gnupg_version[0] == 2 and gnupg_version[1] >= 1: - f.write(""" - %no-protection - %transient-key - """) - f.write(""" - %no-ask-passphrase - Key-Type: RSA - Name-Real: Example Key - Name-Comment: N/A - Name-Email: example@example.com - Expire-Date: 2d - Preferences: (setpref) - %commit - """) - finally: - f.close() - # Note that --quick-random (--debug-quick-random in GnuPG 2.x) - # does not have a corresponding preferences file setting and - # must be passed explicitly on the command line instead - if gnupg_version[0] == 1: - gnupg_random = '--quick-random' - elif gnupg_version[0] >= 2: - gnupg_random = '--debug-quick-random' - else: - gnupg_random = '' - cmd = ['gpg', '--gen-key', '--batch'] - if gnupg_random: - cmd.append(gnupg_random) - cmd.append(config_file) - subprocess.check_call( - cmd, - cwd=tempdir.path, - # Direct stderr to its own pipe, from which we don't read, - # to quiet the commands. - stderr=subprocess.PIPE, - ) - - -class GitRepoFixture(fixtures.Fixture): - - logger = logging.getLogger('git') - - def __init__(self, reporoot): - self.reporoot = reporoot - super(GitRepoFixture, self).__init__() - - def setUp(self): - super(GitRepoFixture, self).setUp() - self.useFixture(GPGKeyFixture()) - os.makedirs(self.reporoot) - self.git('init', '.') - self.git('config', '--local', 'user.email', 'example@example.com') - self.git('config', '--local', 'user.name', 'reno developer') - self.git('config', '--local', 'user.signingkey', - 'example@example.com') - - def git(self, *args): - self.logger.debug('$ git %s', ' '.join(args)) - output = utils.check_output( - ['git'] + list(args), - cwd=self.reporoot, - ) - self.logger.debug(output) - return output - - def commit(self, message='commit message'): - self.git('add', '.') - self.git('commit', '-m', message) - self.git('show', '--pretty=format:%H') - time.sleep(0.1) # force a delay between commits - - def add_file(self, name): - with open(os.path.join(self.reporoot, name), 'w') as f: - f.write('adding %s\n' % name) - self.commit('add %s' % name) - - -class Base(base.TestCase): - - logger = logging.getLogger('test') - - def _add_notes_file(self, slug='slug', commit=True, legacy=False, - contents='i-am-also-a-template'): - n = self.get_note_num() - if legacy: - basename = '%016x-%s.yaml' % (n, slug) - else: - basename = '%s-%016x.yaml' % (slug, n) - filename = os.path.join(self.reporoot, 'releasenotes', 'notes', - basename) - create._make_note_file(filename, contents) - self.repo.commit('add %s' % basename) - return os.path.join('releasenotes', 'notes', basename) - - def _make_python_package(self): - setup_name = os.path.join(self.reporoot, 'setup.py') - with open(setup_name, 'w') as f: - f.write(_SETUP_TEMPLATE) - cfg_name = os.path.join(self.reporoot, 'setup.cfg') - with open(cfg_name, 'w') as f: - f.write(_CFG_TEMPLATE) - pkgdir = os.path.join(self.reporoot, 'testpkg') - os.makedirs(pkgdir) - init = os.path.join(pkgdir, '__init__.py') - with open(init, 'w') as f: - f.write("Test package") - self.repo.commit('add test package') - - def setUp(self): - super(Base, self).setUp() - self.fake_logger = self.useFixture( - fixtures.FakeLogger( - format='%(levelname)8s %(name)s %(message)s', - level=logging.DEBUG, - nuke_handlers=True, - ) - ) - # Older git does not have config --local, so create a temporary home - # directory to permit using git config --global without stepping on - # developer configuration. - self.useFixture(fixtures.TempHomeDir()) - self.useFixture(fixtures.NestedTempfile()) - self.temp_dir = self.useFixture(fixtures.TempDir()).path - self.reporoot = os.path.join(self.temp_dir, 'reporoot') - self.repo = self.useFixture(GitRepoFixture(self.reporoot)) - self.c = config.Config(self.reporoot) - self._counter = itertools.count(1) - self.get_note_num = lambda: next(self._counter) - - -class BasicTest(Base): - - def test_non_python_no_tags(self): - filename = self._add_notes_file() - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'0.0.0': [filename]}, - results, - ) - - def test_python_no_tags(self): - self._make_python_package() - filename = self._add_notes_file() - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'0.0.0': [filename]}, - results, - ) - - def test_note_before_tag(self): - filename = self._add_notes_file() - self.repo.add_file('not-a-release-note.txt') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [filename]}, - results, - ) - - def test_note_commit_tagged(self): - filename = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [filename]}, - results, - ) - - def test_note_commit_after_tag(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - filename = self._add_notes_file() - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0-1': [filename]}, - results, - ) - - def test_other_commit_after_tag(self): - filename = self._add_notes_file() - self.repo.add_file('ignore-1.txt') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.repo.add_file('ignore-2.txt') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [filename]}, - results, - ) - - def test_multiple_notes_after_tag(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - f2 = self._add_notes_file() - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0-2': [f1, f2]}, - results, - ) - - def test_multiple_notes_within_tag(self): - self._make_python_package() - f1 = self._add_notes_file(commit=False) - f2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [f1, f2]}, - results, - ) - - def test_multiple_tags(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - f2 = self._add_notes_file() - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f1], - '2.0.0-1': [f2], - }, - results, - ) - - def test_rename_file(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - f2 = f1.replace('slug1', 'slug2') - self.repo.git('mv', f1, f2) - self.repo.commit('rename note file') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f2], - }, - results, - ) - - def test_rename_file_sort_earlier(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - f2 = f1.replace('slug1', 'slug0') - self.repo.git('mv', f1, f2) - self.repo.commit('rename note file') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f2], - }, - results, - ) - - def test_edit_file(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - with open(os.path.join(self.reporoot, f1), 'w') as f: - f.write('---\npreamble: new contents for file') - self.repo.commit('edit note file') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f1], - }, - results, - ) - - def test_legacy_file(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file('slug1', legacy=True) - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - f2 = f1.replace('slug1', 'slug2') - self.repo.git('mv', f1, f2) - self.repo.commit('rename note file') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f2], - }, - results, - ) - - def test_rename_legacy_file_to_new(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file('slug1', legacy=True) - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - # Rename the file with the new convention of placing the UUID - # after the slug instead of before. - f2 = f1.replace('0000000000000001-slug1', - 'slug1-0000000000000001') - self.repo.git('mv', f1, f2) - self.repo.commit('rename note file') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f2], - }, - results, - ) - - def test_limit_by_earliest_version(self): - self._make_python_package() - self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'middle tag', '2.0.0') - f3 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'last tag', '3.0.0') - self.c.override( - earliest_version='2.0.0', - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f2], - '3.0.0': [f3], - }, - results, - ) - - def test_delete_file(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file('slug1') - f2 = self._add_notes_file('slug2') - self.repo.git('rm', f1) - self.repo.commit('remove note file') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f2], - }, - results, - ) - - def test_rename_then_delete_file(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file('slug1') - f2 = f1.replace('slug1', 'slug2') - self.repo.git('mv', f1, f2) - self.repo.git('status') - self.repo.commit('rename note file') - self.repo.git('rm', f2) - self.repo.commit('remove note file') - f3 = self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - log_results = self.repo.git('log', '--topo-order', - '--pretty=%H %d', - '--name-only') - self.addDetail('git log', text_content(log_results)) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'2.0.0': [f3], - }, - results, - ) - - def test_staged_file(self): - # Prove that we can get a file we have staged. - # Start with a standard commit and tag - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - # Now stage a release note - n = self.get_note_num() - basename = 'staged-note-%016x.yaml' % n - filename = os.path.join(self.reporoot, 'releasenotes', 'notes', - basename) - create._make_note_file(filename, 'staged note') - self.repo.git('add', filename) - status_results = self.repo.git('status') - self.addDetail('git status', text_content(status_results)) - # Now run the scanner - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - self.assertEqual( - {'*working-copy*': [ - (os.path.join('releasenotes', 'notes', basename), - None)], - }, - raw_results, - ) - - @unittest.skip('dulwich does not know how to identify new files') - def test_added_tagged_not_staged(self): - # Prove that we can get a file we have created but not staged. - # Start with a standard commit and tag - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - # Now create a note without staging it - n = self.get_note_num() - basename = 'staged-note-%016x.yaml' % n - filename = os.path.join(self.reporoot, 'releasenotes', 'notes', - basename) - create._make_note_file(filename, 'staged note') - status_results = self.repo.git('status') - self.addDetail('git status', text_content(status_results)) - # Now run the scanner - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - # Take the staged version of the file, but associate it with - # tagged version 1.0.0 because the file was added before that - # version. - self.assertEqual( - {'1.0.0': [(os.path.join('releasenotes', 'notes', basename), - None)], - }, - raw_results, - ) - - def test_modified_tagged_not_staged(self): - # Prove that we can get a file we have changed but not staged. - # Start with a standard commit and tag - self._make_python_package() - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - # Now modify the note - fullpath = os.path.join(self.repo.reporoot, f1) - with open(fullpath, 'w') as f: - f.write('modified first note') - status_results = self.repo.git('status') - self.addDetail('git status', text_content(status_results)) - # Now run the scanner - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - # Take the staged version of the file, but associate it with - # tagged version 1.0.0 because the file was added before that - # version. - self.assertEqual( - {'1.0.0': [(f1, None)], - }, - raw_results, - ) - - def test_stop_on_master_with_other_branch(self): - self._make_python_package() - self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'middle tag', '2.0.0') - f3 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'last tag', '3.0.0') - self.repo.git('branch', 'stable/a') - f4 = self._add_notes_file() - self.c.override( - earliest_version=None, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'3.0.0-1': [f4], - '3.0.0': [f3], - }, - results, - ) - - def test_stop_on_master_without_limits_or_branches(self): - self._make_python_package() - f1 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'middle tag', '2.0.0') - f3 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'last tag', '3.0.0') - f4 = self._add_notes_file() - self.c.override( - earliest_version=None, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'3.0.0-1': [f4], - '3.0.0': [f3], - '2.0.0': [f2], - '1.0.0': [f1], - }, - results, - ) - - -class IgnoreTest(Base): - - def test_by_fullname(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - f2 = self._add_notes_file() - self.c.override( - ignore_notes=[f1], - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0-2': [f2]}, - results, - ) - - def test_by_basename(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - f2 = self._add_notes_file() - self.c.override( - ignore_notes=[os.path.basename(f1)], - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0-2': [f2]}, - results, - ) - - def test_by_uid(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - f2 = self._add_notes_file() - self.c.override( - ignore_notes=[scanner._get_unique_id(f1)], - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0-2': [f2]}, - results, - ) - - def test_by_multiples(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - f1 = self._add_notes_file() - f2 = self._add_notes_file() - self.c.override( - ignore_notes=[ - scanner._get_unique_id(f1), - scanner._get_unique_id(f2), - ], - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {}, - results, - ) - - -class FileContentsTest(Base): - - def test_basic_file(self): - # Prove that we can get a file we have committed. - f1 = self._add_notes_file(contents='well-known-contents') - r = scanner.RenoRepo(self.reporoot) - contents = r.get_file_at_commit(f1, 'HEAD') - self.assertEqual( - b'well-known-contents', - contents, - ) - - def test_no_such_file(self): - # Returns None when the file does not exist at all. - # (we have to commit something, otherwise there is no HEAD) - self._add_notes_file(contents='well-known-contents') - r = scanner.RenoRepo(self.reporoot) - contents = r.get_file_at_commit('no-such-dir/no-such-file', 'HEAD') - self.assertEqual( - None, - contents, - ) - - def test_edit_file_and_commit(self): - # Prove that we can edit a file and see the changes. - f1 = self._add_notes_file(contents='initial-contents') - with open(os.path.join(self.reporoot, f1), 'w') as f: - f.write('new contents for file') - self.repo.commit('edit note file') - r = scanner.RenoRepo(self.reporoot) - contents = r.get_file_at_commit(f1, 'HEAD') - self.assertEqual( - b'new contents for file', - contents, - ) - - def test_earlier_version_of_edited_file(self): - # Prove that we are not always just returning the most current - # version of a file. - f1 = self._add_notes_file(contents='initial-contents') - with open(os.path.join(self.reporoot, f1), 'w') as f: - f.write('new contents for file') - self.repo.commit('edit note file') - self.scanner = scanner.Scanner(self.c) - r = scanner.RenoRepo(self.reporoot) - head = r.head() - parent = r.get_parents(head)[0] - parent = parent.decode('ascii') - contents = r.get_file_at_commit(f1, parent) - self.assertEqual( - b'initial-contents', - contents, - ) - - def test_edit_file_without_commit(self): - # Prove we are not picking up the contents from the local - # filesystem outside of the git history. - f1 = self._add_notes_file(contents='initial-contents') - with open(os.path.join(self.reporoot, f1), 'w') as f: - f.write('new contents for file') - r = scanner.RenoRepo(self.reporoot) - contents = r.get_file_at_commit(f1, 'HEAD') - self.assertEqual( - b'initial-contents', - contents, - ) - - def test_staged_file(self): - # Prove we are not picking up the contents from the local - # filesystem outside of the git history. - f1 = self._add_notes_file(contents='initial-contents') - with open(os.path.join(self.reporoot, f1), 'w') as f: - f.write('new contents for file') - r = scanner.RenoRepo(self.reporoot) - contents = r.get_file_at_commit(f1, None) - self.assertEqual( - 'new contents for file', - contents, - ) - - -class PreReleaseTest(Base): - - def test_alpha(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0.0a1') - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0.0a2') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0.0a2': [f1], - }, - results, - ) - - def test_beta(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0.0b1') - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0.0b2') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0.0b2': [f1], - }, - results, - ) - - def test_release_candidate(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0.0rc1') - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0.0rc2') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0.0rc2': [f1], - }, - results, - ) - - def test_collapse(self): - files = [] - self._make_python_package() - files.append(self._add_notes_file('slug1')) - self.repo.git('tag', '-s', '-m', 'alpha tag', '1.0.0.0a1') - files.append(self._add_notes_file('slug2')) - self.repo.git('tag', '-s', '-m', 'beta tag', '1.0.0.0b1') - files.append(self._add_notes_file('slug3')) - self.repo.git('tag', '-s', '-m', 'release candidate tag', '1.0.0.0rc1') - files.append(self._add_notes_file('slug4')) - self.repo.git('tag', '-s', '-m', 'full release tag', '1.0.0') - self.c.override( - collapse_pre_releases=True, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': files, - }, - results, - ) - - def test_collapse_without_full_release(self): - self._make_python_package() - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'alpha tag', '1.0.0.0a1') - f2 = self._add_notes_file('slug2') - self.repo.git('tag', '-s', '-m', 'beta tag', '1.0.0.0b1') - f3 = self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'release candidate tag', '1.0.0.0rc1') - self.c.override( - collapse_pre_releases=True, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0.0a1': [f1], - '1.0.0.0b1': [f2], - '1.0.0.0rc1': [f3], - }, - results, - ) - - def test_collapse_without_notes(self): - self._make_python_package() - self.repo.git('tag', '-s', '-m', 'earlier tag', '0.1.0') - f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'alpha tag', '1.0.0.0a1') - f2 = self._add_notes_file('slug2') - self.repo.git('tag', '-s', '-m', 'beta tag', '1.0.0.0b1') - f3 = self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'release candidate tag', '1.0.0.0rc1') - self.c.override( - collapse_pre_releases=True, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0.0a1': [f1], - '1.0.0.0b1': [f2], - '1.0.0.0rc1': [f3], - }, - results, - ) - - -class MergeCommitTest(Base): - - def test_1(self): - # Create changes on master and in the branch - # in order so the history is "normal" - n1 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.repo.git('checkout', '-b', 'test_merge_commit') - n2 = self._add_notes_file() - self.repo.git('checkout', 'master') - self.repo.add_file('ignore-1.txt') - # Merge the branch into master. - self.repo.git('merge', '--no-ff', 'test_merge_commit') - time.sleep(0.1) # force a delay between commits - self.repo.add_file('ignore-2.txt') - self.repo.git('tag', '-s', '-m', 'second tag', '2.0.0') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [n1], - '2.0.0': [n2]}, - results, - ) - self.assertEqual( - ['2.0.0', '1.0.0'], - list(raw_results.keys()), - ) - - def test_2(self): - # Create changes on the branch before the tag into which it is - # actually merged. - self.repo.add_file('ignore-0.txt') - self.repo.git('checkout', '-b', 'test_merge_commit') - n1 = self._add_notes_file() - self.repo.git('checkout', 'master') - n2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.repo.add_file('ignore-1.txt') - # Merge the branch into master. - self.repo.git('merge', '--no-ff', 'test_merge_commit') - time.sleep(0.1) # force a delay between commits - self.repo.git('show') - self.repo.add_file('ignore-2.txt') - self.repo.git('tag', '-s', '-m', 'second tag', '2.0.0') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [n2], - '2.0.0': [n1]}, - results, - ) - self.assertEqual( - ['2.0.0', '1.0.0'], - list(raw_results.keys()), - ) - - def test_3(self): - # Create changes on the branch before the tag into which it is - # actually merged, with another tag in between the time of the - # commit and the time of the merge. This should reflect the - # order of events described in bug #1522153. - self.repo.add_file('ignore-0.txt') - self.repo.git('checkout', '-b', 'test_merge_commit') - n1 = self._add_notes_file() - self.repo.git('checkout', 'master') - n2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.repo.add_file('ignore-1.txt') - self.repo.git('tag', '-s', '-m', 'second tag', '1.1.0') - self.repo.git('merge', '--no-ff', 'test_merge_commit') - time.sleep(0.1) # force a delay between commits - self.repo.add_file('ignore-2.txt') - self.repo.git('tag', '-s', '-m', 'third tag', '2.0.0') - self.repo.add_file('ignore-3.txt') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - # Since the 1.1.0 tag has no notes files, it does not appear - # in the output. It's only there to trigger the bug as it was - # originally reported. - self.assertEqual( - {'1.0.0': [n2], - '2.0.0': [n1]}, - results, - ) - self.assertEqual( - ['2.0.0', '1.0.0'], - list(raw_results.keys()), - ) - - def test_4(self): - # Create changes on the branch before the tag into which it is - # actually merged, with another tag in between the time of the - # commit and the time of the merge. This should reflect the - # order of events described in bug #1522153. - self.repo.add_file('ignore-0.txt') - self.repo.git('checkout', '-b', 'test_merge_commit') - n1 = self._add_notes_file() - self.repo.git('checkout', 'master') - n2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.repo.add_file('ignore-1.txt') - n3 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'second tag', '1.1.0') - self.repo.git('merge', '--no-ff', 'test_merge_commit') - time.sleep(0.1) # force a delay between commits - self.repo.add_file('ignore-2.txt') - self.repo.git('tag', '-s', '-m', 'third tag', '2.0.0') - self.repo.add_file('ignore-3.txt') - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [n2], - '1.1.0': [n3], - '2.0.0': [n1]}, - results, - ) - self.assertEqual( - ['2.0.0', '1.1.0', '1.0.0'], - list(raw_results.keys()), - ) - - -class NullMergeTest(Base): - - def setUp(self): - super(NullMergeTest, self).setUp() - self.repo.add_file('ignore-0.txt') - self.n1 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - - # Create a branch, add a note, and tag it. - self.repo.git('checkout', '-b', 'test_ignore_null_merge') - self.n2 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'second tag', '2.0.0') - - # Move back to master and advance it. - self.repo.git('checkout', 'master') - self.repo.add_file('ignore-1.txt') - self.n3 = self._add_notes_file() - - # Merge only the tag from the first branch back into master. - self.repo.git( - 'merge', '--no-ff', '--strategy', 'ours', '2.0.0', - ) - - # Add another note file. - self.n4 = self._add_notes_file() - self.repo.git('tag', '-s', '-m', 'third tag', '3.0.0') - - self.repo.git('log', '--decorate', '--oneline', '--graph', '--all') - # The results should look like: - # - # * afea344 (HEAD -> master, tag: 3.0.0) add slug-0000000000000004.yaml - # * 7bb295c Merge tag '2.0.0' - # |\ - # | * 260c80b (tag: 2.0.0, test_ignore_null_merge) add slug-0000000000000002.yaml # noqa - # * | 5981ae3 add slug-0000000000000003.yaml - # * | 00f9376 add ignore-1.txt - # |/ - # * d24faf9 (tag: 1.0.0) add slug-0000000000000001.yaml - # * 6c221cd add ignore-0.txt - - def test_ignore(self): - # The scanner should skip over the null-merge and include the - # notes that come before the version being merged in, up to - # the base of the previous branch. - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [self.n1], - '3.0.0': [self.n3, self.n4]}, - results, - ) - - def test_follow(self): - # The scanner should not skip over the null-merge. The output - # should include the 2.0.0 tag that was merged in, as well as - # the earlier 1.0.0 version. - self.c.override( - ignore_null_merges=False, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - {'1.0.0': [self.n1], - '2.0.0': [self.n2, self.n3], - '3.0.0': [self.n4]}, - results, - ) - - -class UniqueIdTest(Base): - - def test_legacy(self): - uid = scanner._get_unique_id( - 'releasenotes/notes/0000000000000001-slug1.yaml' - ) - self.assertEqual('0000000000000001', uid) - - def test_modern(self): - uid = scanner._get_unique_id( - 'releasenotes/notes/slug1-0000000000000001.yaml' - ) - self.assertEqual('0000000000000001', uid) - - -class BranchBaseTest(Base): - - def setUp(self): - super(BranchBaseTest, self).setUp() - self._make_python_package() - self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self._add_notes_file('slug2') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'first tag', '3.0.0') - self.repo.git('checkout', '2.0.0') - self.repo.git('branch', 'not-master') - self.repo.git('checkout', 'master') - self.scanner = scanner.Scanner(self.c) - - def test_current_branch_no_extra_commits(self): - # checkout the branch and then ask for its base - self.repo.git('checkout', 'not-master') - self.assertEqual( - '2.0.0', - self.scanner._get_branch_base('not-master'), - ) - - def test_current_branch_extra_commit(self): - # checkout the branch and then ask for its base - self.repo.git('checkout', 'not-master') - self._add_notes_file('slug4') - self.assertEqual( - '2.0.0', - self.scanner._get_branch_base('not-master'), - ) - - def test_alternate_branch_no_extra_commits(self): - # checkout master and then ask for the alternate branch base - self.repo.git('checkout', 'master') - self.assertEqual( - '2.0.0', - self.scanner._get_branch_base('not-master'), - ) - - def test_alternate_branch_extra_commit(self): - # checkout master and then ask for the alternate branch base - self.repo.git('checkout', 'not-master') - self._add_notes_file('slug4') - self.repo.git('checkout', 'master') - self.assertEqual( - '2.0.0', - self.scanner._get_branch_base('not-master'), - ) - - def test_no_tag_at_base(self): - # remove the tag at the branch point - self.repo.git('tag', '-d', '2.0.0') - self._add_notes_file('slug4') - self.repo.git('checkout', 'master') - self.assertIsNone( - self.scanner._get_branch_base('not-master') - ) - - -class BranchTest(Base): - - def setUp(self): - super(BranchTest, self).setUp() - self._make_python_package() - self.f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.f2 = self._add_notes_file('slug2') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'first tag', '3.0.0') - - def test_files_current_branch(self): - self.repo.git('checkout', '2.0.0') - self.repo.git('checkout', '-b', 'stable/2') - f21 = self._add_notes_file('slug21') - log_text = self.repo.git('log', '--decorate') - self.addDetail('git log', text_content(log_text)) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '2.0.0-1': [f21], - '2.0.0': [self.f2], - }, - results, - ) - - def test_files_stable_from_master(self): - self.repo.git('checkout', '2.0.0') - self.repo.git('checkout', '-b', 'stable/2') - f21 = self._add_notes_file('slug21') - self.repo.git('checkout', 'master') - log_text = self.repo.git('log', '--pretty=%x00%H %d', '--name-only', - 'stable/2') - self.addDetail('git log', text_content(log_text)) - self.c.override( - branch='stable/2', - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '2.0.0': [self.f2], - '2.0.0-1': [f21], - }, - results, - ) - - def test_files_stable_from_master_no_stop_base(self): - self.repo.git('checkout', '2.0.0') - self.repo.git('checkout', '-b', 'stable/2') - f21 = self._add_notes_file('slug21') - self.repo.git('checkout', 'master') - log_text = self.repo.git('log', '--pretty=%x00%H %d', '--name-only', - 'stable/2') - self.addDetail('git log', text_content(log_text)) - self.c.override( - branch='stable/2', - ) - self.c.override( - stop_at_branch_base=False, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '1.0.0': [self.f1], - '2.0.0': [self.f2], - '2.0.0-1': [f21], - }, - results, - ) - - def test_pre_release_branch_no_collapse(self): - f4 = self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'pre-release', '4.0.0.0rc1') - # Add a commit on master after the tag - self._add_notes_file('slug5') - # Move back to the tag and create the branch - self.repo.git('checkout', '4.0.0.0rc1') - self.repo.git('checkout', '-b', 'stable/4') - # Create a commit on the branch - f41 = self._add_notes_file('slug41') - log_text = self.repo.git( - 'log', '--pretty=%x00%H %d', '--name-only', '--graph', - '--all', '--decorate', - ) - self.addDetail('git log', text_content(log_text)) - rev_list = self.repo.git('rev-list', '--first-parent', - '^stable/4', 'master') - self.addDetail('rev-list', text_content(rev_list)) - self.c.override( - branch='stable/4', - collapse_pre_releases=False, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '4.0.0.0rc1': [f4], - '4.0.0.0rc1-1': [f41], - }, - results, - ) - - def test_pre_release_branch_collapse(self): - f4 = self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'pre-release', '4.0.0.0rc1') - # Add a commit on master after the tag - self._add_notes_file('slug5') - # Move back to the tag and create the branch - self.repo.git('checkout', '4.0.0.0rc1') - self.repo.git('checkout', '-b', 'stable/4') - # Create a commit on the branch - f41 = self._add_notes_file('slug41') - self.repo.git('tag', '-s', '-m', 'release', '4.0.0') - log_text = self.repo.git( - 'log', '--pretty=%x00%H %d', '--name-only', '--graph', - '--all', '--decorate', - ) - self.addDetail('git log', text_content(log_text)) - rev_list = self.repo.git('rev-list', '--first-parent', - '^stable/4', 'master') - self.addDetail('rev-list', text_content(rev_list)) - self.c.override( - branch='stable/4', - collapse_pre_releases=True, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '4.0.0': [f4, f41], - }, - results, - ) - - def test_pre_release_note_before_branch(self): - f4 = self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'beta', '4.0.0.0b1') - self.repo.add_file('not-a-release-note.txt') - self.repo.git('tag', '-s', '-m', 'pre-release', '4.0.0.0rc1') - # Add a commit on master after the tag - self._add_notes_file('slug5') - # Move back to the tag and create the branch - self.repo.git('checkout', '4.0.0.0rc1') - self.repo.git('checkout', '-b', 'stable/4') - # Create a commit on the branch - f41 = self._add_notes_file('slug41') - self.repo.git('tag', '-s', '-m', 'release', '4.0.0') - log_text = self.repo.git( - 'log', '--pretty=%x00%H %d', '--name-only', '--graph', - '--all', '--decorate', - ) - self.addDetail('git log', text_content(log_text)) - rev_list = self.repo.git('rev-list', '--first-parent', - '^stable/4', 'master') - self.addDetail('rev-list', text_content(rev_list)) - self.c.override( - branch='stable/4', - collapse_pre_releases=True, - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '4.0.0': [f4, f41], - }, - results, - ) - - def test_full_release_branch(self): - f4 = self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'release', '4.0.0') - # Add a commit on master after the tag - self._add_notes_file('slug5') - # Move back to the tag and create the branch - self.repo.git('checkout', '4.0.0') - self.repo.git('checkout', '-b', 'stable/4') - # Create a commit on the branch - f41 = self._add_notes_file('slug41') - log_text = self.repo.git( - 'log', '--pretty=%x00%H %d', '--name-only', '--graph', - '--all', '--decorate', - ) - self.addDetail('git log', text_content(log_text)) - rev_list = self.repo.git('rev-list', '--first-parent', - '^stable/4', 'master') - self.addDetail('rev-list', text_content(rev_list)) - self.c.override( - branch='stable/4', - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '4.0.0': [f4], - '4.0.0-1': [f41], - }, - results, - ) - - def test_branch_tip_of_master(self): - # We have branched from master, but not added any commits to - # master. - f4 = self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'release', '4.0.0') - self.repo.git('checkout', '-b', 'stable/4') - # Create a commit on the branch - f41 = self._add_notes_file('slug41') - f42 = self._add_notes_file('slug42') - log_text = self.repo.git( - 'log', '--pretty=%x00%H %d', '--name-only', '--graph', - '--all', '--decorate', - ) - self.addDetail('git log', text_content(log_text)) - rev_list = self.repo.git('rev-list', '--first-parent', - '^stable/4', 'master') - self.addDetail('rev-list', text_content(rev_list)) - self.c.override( - branch='stable/4', - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '4.0.0': [f4], - '4.0.0-2': [f41, f42], - }, - results, - ) - - def test_branch_no_more_commits(self): - # We have branched from master, but not added any commits to - # our branch or to master. - f4 = self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'release', '4.0.0') - self.repo.git('checkout', '-b', 'stable/4') - # Create a commit on the branch - log_text = self.repo.git( - 'log', '--pretty=%x00%H %d', '--name-only', '--graph', - '--all', '--decorate', - ) - self.addDetail('git log', text_content(log_text)) - rev_list = self.repo.git('rev-list', '--first-parent', - '^stable/4', 'master') - self.addDetail('rev-list', text_content(rev_list)) - self.c.override( - branch='stable/4', - ) - self.scanner = scanner.Scanner(self.c) - raw_results = self.scanner.get_notes_by_version() - results = { - k: [f for (f, n) in v] - for (k, v) in raw_results.items() - } - self.assertEqual( - { - '4.0.0': [f4], - }, - results, - ) - - def test_remote_branches(self): - self.repo.git('checkout', '2.0.0') - self.repo.git('checkout', '-b', 'stable/2') - self.repo.git('checkout', 'master') - scanner1 = scanner.Scanner(self.c) - head1 = scanner1._get_ref('stable/2') - self.assertIsNotNone(head1) - print('head1', head1) - # Create a second repository by cloning the first. - print(utils.check_output( - ['git', 'clone', self.reporoot, 'reporoot2'], - cwd=self.temp_dir, - )) - reporoot2 = os.path.join(self.temp_dir, 'reporoot2') - print(utils.check_output( - ['git', 'remote', 'update'], - cwd=reporoot2, - )) - print(utils.check_output( - ['git', 'remote', '-v'], - cwd=reporoot2, - )) - print(utils.check_output( - ['find', '.git/refs'], - cwd=reporoot2, - )) - print(utils.check_output( - ['git', 'branch', '-a'], - cwd=reporoot2, - )) - c2 = config.Config(reporoot2) - scanner2 = scanner.Scanner(c2) - head2 = scanner2._get_ref('origin/stable/2') - self.assertIsNotNone(head2) - self.assertEqual(head1, head2) - - def test_remote_branch_without_prefix(self): - self.repo.git('checkout', '2.0.0') - self.repo.git('checkout', '-b', 'stable/2') - self.repo.git('checkout', 'master') - scanner1 = scanner.Scanner(self.c) - head1 = scanner1._get_ref('stable/2') - self.assertIsNotNone(head1) - print('head1', head1) - # Create a second repository by cloning the first. - print(utils.check_output( - ['git', 'clone', self.reporoot, 'reporoot2'], - cwd=self.temp_dir, - )) - reporoot2 = os.path.join(self.temp_dir, 'reporoot2') - print(utils.check_output( - ['git', 'remote', 'update'], - cwd=reporoot2, - )) - print(utils.check_output( - ['git', 'remote', '-v'], - cwd=reporoot2, - )) - print(utils.check_output( - ['find', '.git/refs'], - cwd=reporoot2, - )) - print(utils.check_output( - ['git', 'branch', '-a'], - cwd=reporoot2, - )) - c2 = config.Config(reporoot2) - scanner2 = scanner.Scanner(c2) - head2 = scanner2._get_ref('stable/2') - self.assertIsNotNone(head2) - self.assertEqual(head1, head2) - - -class ScanStopPointPrereleaseVersionsTest(Base): - - def setUp(self): - super(ScanStopPointPrereleaseVersionsTest, self).setUp() - self.scanner = scanner.Scanner(self.c) - self._make_python_package() - self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first series', '1.0.0.0rc1') - self.repo.git('checkout', '-b', 'stable/a') - self._add_notes_file('slug2') - self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'second tag', '1.0.0') - self.repo.git('checkout', 'master') - self._add_notes_file('slug4') - self._add_notes_file('slug5') - self.repo.git('tag', '-s', '-m', 'second series', '2.0.0.0b3') - self._add_notes_file('slug6') - self._add_notes_file('slug7') - self.repo.git('tag', '-s', '-m', 'second tag', '2.0.0.0rc1') - self.repo.git('checkout', '-b', 'stable/b') - self._add_notes_file('slug8') - self._add_notes_file('slug9') - self.repo.git('tag', '-s', '-m', 'third tag', '2.0.0') - self.repo.git('checkout', 'master') - - def test_beta_collapse(self): - self.assertEqual( - '1.0.0.0rc1', - self.scanner._find_scan_stop_point( - '2.0.0.0b3', ['2.0.0.0b3', '1.0.0.0rc1'], - True, 'master'), - ) - - def test_rc_collapse_master(self): - self.assertEqual( - '1.0.0.0rc1', - self.scanner._find_scan_stop_point( - '2.0.0.0rc1', ['2.0.0.0rc1', '2.0.0.0b3', '1.0.0.0rc1'], - True, 'master'), - ) - - def test_rc_collapse_branch(self): - self.assertEqual( - '1.0.0.0rc1', - self.scanner._find_scan_stop_point( - '2.0.0.0rc1', ['2.0.0.0rc1', '2.0.0.0b3', '1.0.0.0rc1'], - True, 'stable/b'), - ) - - def test_rc_no_collapse(self): - self.assertEqual( - '2.0.0.0b3', - self.scanner._find_scan_stop_point( - '2.0.0.0rc1', ['2.0.0.0rc1', '2.0.0.0b3', '1.0.0.0rc1'], - False, 'master'), - ) - - def test_stable_branch_with_collapse(self): - self.assertEqual( - '1.0.0.0rc1', - self.scanner._find_scan_stop_point( - '2.0.0', ['2.0.0', '2.0.0.0rc1', '2.0.0.0b3', '1.0.0.0rc1'], - True, 'stable/b'), - ) - - # def test_nova_newton(self): - # self.assertEqual( - # '13.0.0.0rc3', - # self.scanner._find_scan_stop_point( - # '14.0.0', - # [u'14.0.3', u'14.0.2', u'14.0.1', u'14.0.0.0rc2', - # u'14.0.0', u'14.0.0.0rc1', u'14.0.0.0b3', u'14.0.0.0b2', - # u'14.0.0.0b1', u'13.0.0.0rc3', u'13.0.0', u'13.0.0.0rc2', - # u'13.0.0.0rc1', u'13.0.0.0b3', u'13.0.0.0b2', u'13.0.0.0b1', - # u'12.0.0.0rc3', u'12.0.0', u'12.0.0.0rc2', u'12.0.0.0rc1', - # u'12.0.0.0b3', u'12.0.0.0b2', u'12.0.0.0b1', u'12.0.0a0', - # u'2015.1.0rc3', u'2015.1.0', u'2015.1.0rc2', u'2015.1.0rc1', - # u'2015.1.0b3', u'2015.1.0b2', u'2015.1.0b1', u'2014.2.rc2', - # u'2014.2', u'2014.2.rc1', u'2014.2.b3', u'2014.2.b2', - # u'2014.2.b1', u'2014.1.rc1', u'2014.1.b3', u'2014.1.b2', - # u'2014.1.b1', u'2013.2.rc1', u'2013.2.b3', u'2013.1.rc1', - # u'folsom-2', u'folsom-1', u'essex-1', u'diablo-2', - # u'diablo-1', u'2011.2', u'2011.2rc1', u'2011.2gamma1', - # u'2011.1rc1', u'0.9.0'], - # True), - # ) - - -class ScanStopPointRegularVersionsTest(Base): - - def setUp(self): - super(ScanStopPointRegularVersionsTest, self).setUp() - self.scanner = scanner.Scanner(self.c) - self._make_python_package() - self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first series', '1.0.0') - self.repo.git('checkout', '-b', 'stable/a') - self._add_notes_file('slug2') - self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'second tag', '1.0.1') - self.repo.git('checkout', 'master') - self._add_notes_file('slug4') - self._add_notes_file('slug5') - self.repo.git('tag', '-s', '-m', 'second series', '2.0.0') - self._add_notes_file('slug6') - self._add_notes_file('slug7') - self.repo.git('tag', '-s', '-m', 'second tag', '2.0.1') - self.repo.git('checkout', '-b', 'stable/b') - self._add_notes_file('slug8') - self._add_notes_file('slug9') - self.repo.git('tag', '-s', '-m', 'third tag', '2.0.2') - self.repo.git('checkout', 'master') - - def test_invalid_earliest_version(self): - self.assertIsNone( - self.scanner._find_scan_stop_point( - 'not.a.numeric.version', [], True, 'stable/b'), - ) - - def test_none(self): - self.assertIsNone( - self.scanner._find_scan_stop_point( - None, [], True, 'stable/b'), - ) - - def test_unknown_version(self): - self.assertIsNone( - self.scanner._find_scan_stop_point( - '2.0.2', [], True, 'stable/b'), - ) - - def test_only_version(self): - self.assertIsNone( - self.scanner._find_scan_stop_point( - '2.0.2', ['1.0.0'], True, 'stable/b'), - ) - - def test_find_prior_branch(self): - self.assertEqual( - '1.0.0', - self.scanner._find_scan_stop_point( - '2.0.2', ['2.0.2', '2.0.1', '2.0.0', '1.0.0'], - True, 'stable/b'), - ) - - -class GetRefTest(Base): - - def setUp(self): - super(GetRefTest, self).setUp() - self._make_python_package() - self.f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.repo.git('branch', 'stable/foo') - self.repo.git('tag', 'bar-eol') - - def test_signed_tag(self): - self.scanner = scanner.Scanner(self.c) - ref = self.scanner._get_ref('1.0.0') - expected = self.scanner._repo.head() - self.assertEqual(expected, ref) - - def test_unsigned_tag(self): - self.scanner = scanner.Scanner(self.c) - ref = self.scanner._get_ref('bar-eol') - expected = self.scanner._repo.head() - self.assertEqual(expected, ref) - - def test_eol_tag_from_branch(self): - self.scanner = scanner.Scanner(self.c) - ref = self.scanner._get_ref('stable/bar') - expected = self.scanner._repo.head() - self.assertEqual(expected, ref) - - def test_head(self): - self.scanner = scanner.Scanner(self.c) - ref = self.scanner._get_ref(None) - expected = self.scanner._repo.head() - self.assertEqual(expected, ref) - - -class TagsTest(Base): - - def setUp(self): - super(TagsTest, self).setUp() - self._make_python_package() - self.f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.f2 = self._add_notes_file('slug2') - self.repo.git('tag', '-s', '-m', 'first tag', '2.0.0') - self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'first tag', '3.0.0') - - def test_master(self): - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_tags_on_branch(None) - self.assertEqual( - ['3.0.0', '2.0.0', '1.0.0'], - results, - ) - - def test_get_ref(self): - self.scanner = scanner.Scanner(self.c) - ref = self.scanner._get_ref('3.0.0') - expected = self.scanner._repo.head() - self.assertEqual(expected, ref) - - def test_not_master(self): - self.repo.git('checkout', '2.0.0') - self.repo.git('checkout', '-b', 'not-master') - self._add_notes_file('slug4') - self.repo.git('tag', '-s', '-m', 'not on master', '2.0.1') - self.repo.git('checkout', 'master') - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_tags_on_branch('not-master') - self.assertEqual( - ['2.0.1', '2.0.0', '1.0.0'], - results, - ) - - def test_unsigned(self): - self._add_notes_file('slug4') - self.repo.git('tag', '-m', 'first tag', '4.0.0') - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_tags_on_branch(None) - self.assertEqual( - ['4.0.0', '3.0.0', '2.0.0', '1.0.0'], - results, - ) - - def test_tagged_tag_annotated(self): - time.sleep(1) - self.repo.git('tag', '-s', '-m', 'fourth tag', '4.0.0', '3.0.0') - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_tags_on_branch(None) - self.assertEqual( - ['3.0.0', '4.0.0', '2.0.0', '1.0.0'], - results, - ) - - def test_tagged_tag_lightweight(self): - time.sleep(1) - self.repo.git('tag', '-m', 'fourth tag', '4.0.0', '3.0.0') - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_tags_on_branch(None) - self.assertEqual( - ['3.0.0', '4.0.0', '2.0.0', '1.0.0'], - results, - ) - - -class VersionTest(Base): - - def setUp(self): - super(VersionTest, self).setUp() - self._make_python_package() - self.f1 = self._add_notes_file('slug1') - self.repo.git('tag', '-s', '-m', 'first tag', '1.0.0') - self.f2 = self._add_notes_file('slug2') - self.repo.git('tag', '-s', '-m', 'second tag', '2.0.0') - self._add_notes_file('slug3') - self.repo.git('tag', '-s', '-m', 'third tag', '3.0.0') - - def test_tagged_head(self): - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_current_version(None) - self.assertEqual( - '3.0.0', - results, - ) - - def test_head_after_tag(self): - self._add_notes_file('slug4') - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_current_version(None) - self.assertEqual( - '3.0.0-1', - results, - ) - - def test_multiple_tags(self): - # The timestamp resolution appears to be 1 second, so sleep to - # ensure distinct timestamps for the 2 tags. In practice it is - # unlikely that anything could apply 2 signed tags within a - # single second (certainly not a person). - time.sleep(1) - self.repo.git('tag', '-s', '-m', 'fourth tag', '4.0.0') - self.scanner = scanner.Scanner(self.c) - results = self.scanner._get_current_version(None) - self.assertEqual( - '4.0.0', - results, - ) - - -class AggregateChangesTest(Base): - - def setUp(self): - super(AggregateChangesTest, self).setUp() - self.aggregator = scanner._ChangeAggregator() - - def test_ignore(self): - entry = mock.Mock() - n = self.get_note_num() - name = 'prefix/add-%016x' % n # no .yaml extension - entry.commit.id = 'commit-id' - changes = [ - diff_tree.TreeChange( - type=diff_tree.CHANGE_ADD, - old=objects.TreeEntry(path=None, mode=None, sha=None), - new=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='not-a-hash', - ) - ) - ] - results = self.aggregator.aggregate_changes(entry, changes) - self.assertEqual( - [], - results, - ) - - def test_add(self): - entry = mock.Mock() - n = self.get_note_num() - name = 'prefix/add-%016x.yaml' % n - entry.commit.id = 'commit-id' - changes = [ - diff_tree.TreeChange( - type=diff_tree.CHANGE_ADD, - old=objects.TreeEntry(path=None, mode=None, sha=None), - new=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='not-a-hash', - ) - ) - ] - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual( - [('%016x' % n, 'add', name, 'commit-id')], - results, - ) - - def test_add_multiple_after_delete(self): - # Adding multiple files in one commit using the same UID but - # different slug after we have seen a delete for the same UID - # causes the files to be ignored. - entry = mock.Mock() - n = self.get_note_num() - uid = '%016x' % n - changes = [] - for i in range(2): - name = 'prefix/add%d-%s.yaml' % (i, uid) - entry.commit.id = 'commit-id' - changes.append( - diff_tree.TreeChange( - type=diff_tree.CHANGE_ADD, - old=objects.TreeEntry(path=None, mode=None, sha=None), - new=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='not-a-hash', - ) - ) - ) - # Set up the aggregator as though it had already seen a delete - # operation. Since the scan happens in reverse chronological - # order, the delete would have happened after the add, and we - # can ignore the files because the error has been corrected in - # a later patch. - self.aggregator._deleted_bad_uids.add(uid) - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual([], results) - - def test_add_multiple_without_delete(self): - # Adding multiple files in one commit using the same UID but - # different slug without a delete operation causes an - # exception. - entry = mock.Mock() - n = self.get_note_num() - uid = '%016x' % n - changes = [] - for i in range(2): - name = 'prefix/add%d-%s.yaml' % (i, uid) - entry.commit.id = 'commit-id' - changes.append( - diff_tree.TreeChange( - type=diff_tree.CHANGE_ADD, - old=objects.TreeEntry(path=None, mode=None, sha=None), - new=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='not-a-hash', - ) - ) - ) - - # aggregate_changes() is a generator, so we have to wrap it in - # list() to process the data, so we need a little temporary - # function to do that and pass to assertRaises(). - def get_results(): - return list(self.aggregator.aggregate_changes(entry, changes)) - - self.assertRaises( - ValueError, - get_results, - ) - - def test_delete(self): - entry = mock.Mock() - n = self.get_note_num() - name = 'prefix/delete-%016x.yaml' % n - entry.commit.id = 'commit-id' - changes = [ - diff_tree.TreeChange( - type=diff_tree.CHANGE_DELETE, - old=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='not-a-hash', - ), - new=objects.TreeEntry(path=None, mode=None, sha=None) - ) - ] - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual( - [('%016x' % n, 'delete', name, entry.commit.id)], - results, - ) - - def test_delete_multiple(self): - # Delete multiple files in one commit using the same UID but - # different slug. - entry = mock.Mock() - n = self.get_note_num() - changes = [] - expected = [] - for i in range(2): - name = 'prefix/delete%d-%016x.yaml' % (i, n) - entry.commit.id = 'commit-id' - changes.append( - diff_tree.TreeChange( - type=diff_tree.CHANGE_DELETE, - old=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='not-a-hash', - ), - new=objects.TreeEntry(path=None, mode=None, sha=None), - ) - ) - expected.append(('%016x' % n, 'delete', name, 'commit-id')) - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual(expected, results) - - def test_change(self): - entry = mock.Mock() - n = self.get_note_num() - name = 'prefix/change-%016x.yaml' % n - entry.commit.id = 'commit-id' - changes = [ - diff_tree.TreeChange( - type=diff_tree.CHANGE_MODIFY, - old=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='old-sha', - ), - new=objects.TreeEntry( - path=name.encode('utf-8'), - mode='0222', - sha='new-sha', - ), - ) - ] - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual( - [('%016x' % n, 'modify', name, 'commit-id')], - results, - ) - - def test_add_then_delete(self): - entry = mock.Mock() - n = self.get_note_num() - new_name = 'prefix/new-%016x.yaml' % n - old_name = 'prefix/old-%016x.yaml' % n - entry.commit.id = 'commit-id' - changes = [ - diff_tree.TreeChange( - type=diff_tree.CHANGE_ADD, - old=objects.TreeEntry(path=None, mode=None, sha=None), - new=objects.TreeEntry( - path=new_name.encode('utf-8'), - mode='0222', - sha='new-hash', - ) - ), - diff_tree.TreeChange( - type=diff_tree.CHANGE_DELETE, - old=objects.TreeEntry( - path=old_name.encode('utf-8'), - mode='0222', - sha='old-hash', - ), - new=objects.TreeEntry(path=None, mode=None, sha=None) - ) - ] - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual( - [('%016x' % n, 'rename', old_name, new_name, 'commit-id')], - results, - ) - - def test_delete_then_add(self): - entry = mock.Mock() - n = self.get_note_num() - new_name = 'prefix/new-%016x.yaml' % n - old_name = 'prefix/old-%016x.yaml' % n - entry.commit.id = 'commit-id' - changes = [ - diff_tree.TreeChange( - type=diff_tree.CHANGE_DELETE, - old=objects.TreeEntry( - path=old_name.encode('utf-8'), - mode='0222', - sha='old-hash', - ), - new=objects.TreeEntry(path=None, mode=None, sha=None) - ), - diff_tree.TreeChange( - type=diff_tree.CHANGE_ADD, - old=objects.TreeEntry(path=None, mode=None, sha=None), - new=objects.TreeEntry( - path=new_name.encode('utf-8'), - mode='0222', - sha='new-hash', - ) - ), - ] - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual( - [('%016x' % n, 'rename', old_name, new_name, 'commit-id')], - results, - ) - - def test_tree_changes(self): - # Under some conditions when dulwich sees merge commits, - # changes() returns a list with nested lists. See commit - # cc11da6dcfb1dbaa015e9804b6a23f7872380c1b in this repo for an - # example. - entry = mock.Mock() - n = self.get_note_num() - # The files modified by the commit are actually - # reno/scanner.py, but the fake names are used in this test to - # comply with the rest of the configuration for the scanner. - old_name = 'prefix/old-%016x.yaml' % n - entry.commit.id = 'commit-id' - changes = [[ - diff_tree.TreeChange( - type='modify', - old=diff_tree.TreeEntry( - path=old_name.encode('utf-8'), - mode=33188, - sha=b'8247dfdd116fd0e3cc4ba32328e4a3eafd227de6', - ), - new=diff_tree.TreeEntry( - path=old_name.encode('utf-8'), - mode=33188, - sha=b'611f3663f54afb1f018a6a8680b6488da50ac340', - ), - ), - diff_tree.TreeChange( - type='modify', - old=diff_tree.TreeEntry( - path=old_name.encode('utf-8'), - mode=33188, - sha=b'ecb7788066eefa9dc8f110b56360efe7b1140b84', - ), - new=diff_tree.TreeEntry( - path=old_name.encode('utf-8'), - mode=33188, - sha=b'611f3663f54afb1f018a6a8680b6488da50ac340', - ), - ), - ]] - results = list(self.aggregator.aggregate_changes(entry, changes)) - self.assertEqual( - [('%016x' % n, 'modify', old_name, 'commit-id'), - ('%016x' % n, 'modify', old_name, 'commit-id')], - results, - ) diff --git a/reno/tests/test_utils.py b/reno/tests/test_utils.py deleted file mode 100644 index cb76cc7..0000000 --- a/reno/tests/test_utils.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock -import six - -from reno.tests import base -from reno import utils - - -class TestGetRandomString(base.TestCase): - - @mock.patch('random.randrange') - @mock.patch('os.urandom') - def test_no_urandom(self, urandom, randrange): - urandom.side_effect = Exception('cannot use this') - randrange.return_value = ord('a') - actual = utils.get_random_string() - expected = '61' * 8 # hex for ord('a') - self.assertIsInstance(actual, six.text_type) - self.assertEqual(expected, actual) - - @mock.patch('random.randrange') - @mock.patch('os.urandom') - def test_with_urandom(self, urandom, randrange): - urandom.return_value = b'\x62' * 8 - randrange.return_value = ord('a') - actual = utils.get_random_string() - expected = '62' * 8 # hex for ord('b') - self.assertIsInstance(actual, six.text_type) - self.assertEqual(expected, actual) diff --git a/reno/utils.py b/reno/utils.py deleted file mode 100644 index 88cd098..0000000 --- a/reno/utils.py +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import binascii -import logging -import os -import os.path -import random -import subprocess - -LOG = logging.getLogger(__name__) - - -def get_random_string(nbytes=8): - """Return a fixed-length random string - - :rtype: six.text_type - """ - try: - # NOTE(dhellmann): Not all systems support urandom(). - # hexlify returns six.binary_type, decode to convert to six.text_type. - val = binascii.hexlify(os.urandom(nbytes)).decode('utf-8') - except Exception as e: - print('ERROR, perhaps urandom is not supported: %s' % e) - val = u''.join(u'%02x' % random.randrange(256) - for i in range(nbytes)) - return val - - -def check_output(*args, **kwds): - """Unicode-aware wrapper for subprocess.check_output""" - process = subprocess.Popen(stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - *args, **kwds) - output, errors = process.communicate() - retcode = process.poll() - if errors: - LOG.debug('ran: %s', ' '.join(*args)) - LOG.debug('returned: %s', retcode) - LOG.debug('error output: %s', errors.rstrip()) - LOG.debug('regular output: %s', output.rstrip()) - if retcode: - LOG.debug('raising error') - raise subprocess.CalledProcessError(retcode, args, output=output) - return output.decode('utf-8') diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2f2a16e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -pbr>=1.4 -PyYAML>=3.1.0 -six>=1.9.0 -dulwich>=0.15.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4f304ec..0000000 --- a/setup.cfg +++ /dev/null @@ -1,50 +0,0 @@ -[metadata] -name = reno -summary = RElease NOtes manager -description-file = - README.rst -author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = https://docs.openstack.org/reno/latest/ -classifier = - Environment :: OpenStack - Intended Audience :: Information Technology - Intended Audience :: System Administrators - License :: OSI Approved :: Apache Software License - Operating System :: POSIX :: Linux - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - -[files] -packages = - reno - -[entry_points] -console_scripts = - reno = reno.main:main - -[extras] -sphinx = - sphinx>=1.5.1,!=1.6.1 # BSD - docutils>=0.11 # OSI-Approved Open Source, Public Domain - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 -warning-is-error = 1 - -[upload_sphinx] -upload-dir = doc/build/html - -[compile_catalog] -directory = reno/locale -domain = reno - -[update_catalog] -domain = reno -output_dir = reno/locale -input_file = reno/locale/reno.pot diff --git a/setup.py b/setup.py deleted file mode 100644 index 056c16c..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT -import setuptools - -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass - -setuptools.setup( - setup_requires=['pbr'], - pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 203542f..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 - -mock>=1.2 - -coverage>=3.6 -python-subunit>=0.0.18 -openstackdocstheme>=1.11.0 # Apache-2.0 -testrepository>=0.0.18 -testscenarios>=0.4 -testtools>=1.4.0 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 88a0101..0000000 --- a/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -[tox] -minversion = 1.6 -envlist = py35,py27,pep8 -skipsdist = True - -[testenv] -usedevelop = True -install_command = pip install -U {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} -deps = - -r{toxinidir}/test-requirements.txt - .[sphinx] -commands = - python setup.py test --slowest --coverage --coverage-package-name=reno --testr-args='{posargs}' - coverage report --show-missing - -[testenv:pep8] -commands = - flake8 - reno -q lint - -[testenv:venv] -commands = {posargs} - -[testenv:cover] -commands = python setup.py test --coverage --testr-args='{posargs}' - -[testenv:docs] -commands = python setup.py build_sphinx - -[testenv:debug] -commands = oslo_debug_helper {posargs} - -[flake8] -# E123, E125 skipped as they are invalid PEP-8. - -show-source = True -ignore = E123,E125 -builtins = _ -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build