Exemplo n.º 1
0
def load_config(distribution):
    """Utility method to parse distutils/setuptools configuration.

    This is for use by other libraries to extract the command configuration.

    :param distribution: A :class:`distutils.dist.Distribution` object
    :returns: A tuple of a :class:`reno.config.Config` object, the output path
        of the human-readable release notes file, and the output file of the
        reno cache file
    """
    option_dict = distribution.get_option_dict(COMMAND_NAME)

    if option_dict.get('repo_root') is not None:
        repo_root = option_dict.get('repo_root')[1]
    else:
        repo_root = defaults.REPO_ROOT

    if option_dict.get('rel_notes_dir') is not None:
        rel_notes_dir = option_dict.get('rel_notes_dir')[1]
    else:
        rel_notes_dir = defaults.RELEASE_NOTES_SUBDIR

    if option_dict.get('output_file') is not None:
        output_file = option_dict.get('output_file')[1]
    else:
        output_file = defaults.RELEASE_NOTES_FILENAME

    conf = config.Config(repo_root, rel_notes_dir)
    cache_file = loader.get_cache_filename(conf)

    return (conf, output_file, cache_file)
Exemplo n.º 2
0
 def test_override(self):
     c = config.Config(self.tempdir.path)
     c.override(collapse_pre_releases=False, )
     actual = c.options
     expected = {o.name: o.default for o in config._OPTIONS}
     expected['collapse_pre_releases'] = False
     self.assertEqual(expected, actual)
Exemplo n.º 3
0
 def test_load_file_empty(self):
     config_path = self.tempdir.join('reno.yaml')
     with open(config_path, 'w') as fd:
         fd.write('# Add reno config here')
     self.addCleanup(os.unlink, config_path)
     c = config.Config(self.tempdir.path)
     self.assertEqual(True, c.collapse_pre_releases)
Exemplo n.º 4
0
 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
Exemplo n.º 5
0
 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'))
Exemplo n.º 6
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')
Exemplo n.º 7
0
 def test_override_multiple(self):
     c = config.Config(self.tempdir.path)
     c.override(notesdir='value1', )
     c.override(notesdir='value2', )
     actual = c.options
     expected = {o.name: o.default for o in config._OPTIONS}
     expected['notesdir'] = 'value2'
     self.assertEqual(expected, actual)
Exemplo n.º 8
0
 def test_defaults(self):
     c = config.Config(self.tempdir.path)
     actual = c.options
     expected = {
         o.name: o.default
         for o in config._OPTIONS
     }
     self.assertEqual(expected, actual)
Exemplo n.º 9
0
 def setUp(self):
     super(TestCache, self).setUp()
     self.useFixture(
         fixtures.MockPatch('reno.scanner.Scanner.get_file_at_commit',
                            new=self._get_note_body))
     self.useFixture(
         fixtures.MockPatch('reno.scanner.Scanner.get_version_dates',
                            new=self._get_dates))
     self.c = config.Config('.')
Exemplo n.º 10
0
    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,
            )
Exemplo n.º 11
0
    def run(self):
        conf = config.Config(self.repo_root, self.rel_notes_dir)

        # Generate the cache using the configuration options found
        # in the release notes directory and the default output
        # filename.
        cache_filename = cache.write_cache_db(
            conf=conf,
            versions_to_include=[],  # include all versions
            outfilename=None,  # generate the default name
        )
        log.info('wrote cache file to %s', cache_filename)

        ldr = loader.Loader(conf)
        text = formatter.format_report(
            ldr,
            conf,
            ldr.versions,
            title=self.distribution.metadata.name,
        )
        with open(self.output_file, 'w') as f:
            f.write(text)
        log.info('wrote release notes to %s', self.output_file)
Exemplo n.º 12
0
 def setUp(self):
     super(TestConfigProperties, self).setUp()
     # Temporary directory to store our config
     self.tempdir = self.useFixture(fixtures.TempDir())
     self.c = config.Config('releasenotes')
Exemplo n.º 13
0
    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
Exemplo n.º 14
0
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(
        '--from-template',
        help='Template to get the release note from.',
    )
    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)
Exemplo n.º 15
0
    def run(self):
        # type: () -> nodes.Node
        """
        Run to generate the output from .. ddtrace-release-notes:: directive


        1. Determine the max version cutoff we need to report for

           We determine this by traversing the git log until we
           find the first dev or release branch ref.

           If we are generating for 1.x branch we will use 2.0 as the cutoff.

           If we are generating for 0.60 branch we will use 0.61 as the cutoff.

           We do this to ensure if we are generating notes for older versions
           we do no include all up to date release notes. Think releasing 0.57.2
           when there is 0.58.0, 0.59.0, 1.0.0, etc we only want notes for < 0.58.

        2. Iterate through all release branches

           A release branch is one that matches the ``^[0-9]+.[0-9]+``` pattern

           Skip any that do not meet the max version cutoff.

        3. Determine the earliest version to report for each release branch

           If the release has only RC releases then use ``.0rc1`` as the earliest
           version. If there are non-RC releases then use ``.0`` version as the
           earliest.

           We do this because we want reno to only report notes that are for that
           given release branch but it will collapse RC releases if there is a
           non-RC tag on that branch. So there isn't a consistent "earliest version"
           we can use for in-progress/dev branches as well as released branches.


        4. Generate a reno config for reporting and generate the notes for each branch
        """
        # This is where we will aggregate the generated notes
        title = " ".join(self.content)
        result = statemachine.ViewList()

        # Determine the max version we want to report for
        max_version = self._get_report_max_version()
        LOG.info("capping max report version to %r", max_version)

        # For each release branch, starting with the newest
        for version, ref in self._release_branches:
            # If this version is equal to or greater than the max version we want to report for
            if max_version is not None and version >= max_version:
                LOG.info("skipping %s >= %s", version, max_version)
                continue

            # Older versions did not have reno release notes
            # DEV: Reno will fail if we try to run on a branch with no notes
            if (version.major, version.minor) < (0, 44):
                LOG.info("skipping older version %s", version)
                continue

            # Parse the branch name from the ref, we want origin/{major}.{minor}[-dev]
            _, _, branch = ref.partition("refs/remotes/")

            # Determine the earliest release tag for this version
            earliest_version = self._get_earliest_version(version)
            if not earliest_version:
                LOG.info("no release tags found for %s", version)
                continue

            # Setup reno config
            conf = config.Config(self._repo.path, "releasenotes")
            conf.override(
                branch=branch,
                collapse_pre_releases=True,
                stop_at_branch_base=True,
                earliest_version=earliest_version,
            )
            LOG.info(
                "scanning %s for %s release notes, stopping at %s",
                os.path.join(self._repo.path, "releasenotes/notes"),
                branch,
                earliest_version,
            )

            # Generate the formatted RST
            with loader.Loader(conf) as ldr:
                versions = ldr.versions
                LOG.info("got versions %s", versions)
                text = formatter.format_report(
                    ldr,
                    conf,
                    versions,
                    title=title,
                    branch=branch,
                )

            source_name = "<%s %s>" % (__name__, branch or "current branch")
            for line_num, line in enumerate(text.splitlines(), 1):
                LOG.debug("%4d: %s", line_num, line)
                result.append(line, source_name, line_num)

        # Generate the RST nodes to return for rendering
        node = nodes.section()
        node.document = self.state.document
        nested_parse_with_titles(self.state, result, node)
        return node.children
Exemplo n.º 16
0
 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)
Exemplo n.º 17
0
 def test_load_file_not_present(self):
     missing = 'reno.config.Config._report_missing_config_files'
     with mock.patch(missing) as error_handler:
         config.Config(self.tempdir.path)
         self.assertEqual(1, error_handler.call_count)
Exemplo n.º 18
0
    def run(self):
        title = ' '.join(self.content)
        branch = self.options.get('branch')
        relnotessubdir = self.options.get(
            'relnotessubdir',
            defaults.RELEASE_NOTES_SUBDIR,
        )
        reporoot = self._find_reporoot(
            self.options.get('reporoot', '.'),
            relnotessubdir,
        )
        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 'unreleased-version-title' in self.options:
            opt_overrides['unreleased_version_title'] = self.options.get(
                'unreleased-version-title')

        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)
        LOG.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
        LOG.info('got versions %s' % (versions, ))
        text = formatter.format_report(
            ldr,
            conf,
            versions,
            title=title,
            branch=branch,
        )
        source_name = '<%s %s>' % (__name__, branch or 'current branch')
        result = statemachine.ViewList()
        for line_num, line in enumerate(text.splitlines(), 1):
            LOG.debug('%4d: %s', line_num, line)
            result.append(line, source_name, line_num)

        node = nodes.section()
        node.document = self.state.document
        nested_parse_with_titles(self.state, result, node)
        return node.children
Exemplo n.º 19
0
 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)
Exemplo n.º 20
0
def generate_release_notes(
    repo,
    repo_path,
    start_revision,
    end_revision,
    show_dates,
    skip_requirement_merges,
    is_stable,
    series,
    email,
    email_from,
    email_reply_to,
    email_tags,
    include_pypi_link,
    changes_only,
    first_release,
    deliverable_file,
    description,
    publishing_dir_name,
):
    """Return the text of the release notes.

    :param repo: The name of the repo.
    :param repo_path: Path to the repo repository on disk.
    :param start_revision: First reference for finding change log.
    :param end_revision: Final reference for finding change log.
    :param show_dates: Boolean indicating whether or not to show dates
        in the output.
    :param skip_requirement_merges: Boolean indicating whether to
        skip merge commits for requirements changes.
    :param is_stable: Boolean indicating whether this is a stable
        series or not.
    :param series: String holding the name of the series.
    :param email: Boolean indicating whether the output format should
        be an email message.
    :param email_from: String containing the sender email address.
    :param email_reply_to: String containing the email reply-to address.
    :param email_tags: String containing the email header topic tags to add.
    :param include_pypi_link: Boolean indicating whether or not to
        include an automatically generated link to the PyPI package
        page.
    :param changes_only: Boolean indicating whether to limit output to
        the list of changes, without any extra data.
    :param first_release: Boolean indicating whether this is the first
        release of the project
    :param deliverable_file: The deliverable file path from the repo root.
    :param description: Description of the repo
    :param publishing_dir_name: The directory on publishings.openstack.org
        containing the package.
    """
    repo_name = repo.split('/')[-1]
    # Determine if this is a release candidate or not.
    is_release_candidate = 'rc' in end_revision

    # Do not mention the series in independent model since there is none
    if series == 'independent':
        series = ''

    if not email_from:
        raise RuntimeError('No email-from specified')

    # Get the commits that are in the desired range...
    git_range = "%s..%s" % (start_revision, end_revision)
    if show_dates:
        format = "--format=%h %ci %s"
    else:
        format = "--oneline"
    cmd = ["git", "log", "--no-color", format, "--no-merges", git_range]
    stdout = run_cmd(cmd, cwd=repo_path)
    changes = []
    for commit_line in stdout.splitlines():
        commit_line = commit_line.strip()
        if not commit_line or is_skippable_commit(skip_requirement_merges,
                                                  commit_line):
            continue
        else:
            changes.append(commit_line)

    # Filter out any requirement file changes...
    requirement_changes = []
    requirement_files = list(
        glob.glob(os.path.join(repo_path, '*requirements*.txt')))
    if requirement_files:
        cmd = ['git', 'diff', '-U0', '--no-color', git_range]
        cmd.extend(requirement_files)
        stdout = run_cmd(cmd, cwd=repo_path)
        requirement_changes = [
            line.strip() for line in stdout.splitlines() if line.strip()
        ]

    # Get statistics about the range given...
    cmd = ['git', 'diff', '--stat', '--no-color', git_range]
    stdout = run_cmd(cmd, cwd=repo_path)
    diff_stats = []
    for line in stdout.splitlines():
        line = line.strip()
        if not line or line.find("tests") != -1 or line.startswith("doc"):
            continue
        diff_stats.append(line)

    # Extract + valdiate needed sections...
    sections = parse_deliverable(series,
                                 repo_name,
                                 deliverable_file=deliverable_file)
    change_header = ["Changes in %s %s" % (repo, git_range)]
    change_header.append("-" * len(change_header[0]))

    # Look for reno notes for this version.
    if not changes_only:
        logging.getLogger('reno').setLevel(logging.WARNING)
        cfg = reno_config.Config(reporoot=repo_path, )
        branch = None
        if is_stable and series:
            branch = 'origin/stable/%s' % series
        cfg.override(branch=branch)
        ldr = loader.Loader(conf=cfg, ignore_cache=True)
        if end_revision in ldr.versions:
            rst_notes = formatter.format_report(
                loader=ldr,
                config=cfg,
                versions_to_include=[end_revision],
            )
            reno_notes = rst2txt.convert(rst_notes).decode('utf-8')
        else:
            LOG.warning(
                ('Did not find revision %r in list of versions '
                 'with release notes %r, skipping reno'),
                end_revision,
                ldr.versions,
            )
            reno_notes = ''
    else:
        reno_notes = ''

    # The recipient for announcements should always be the
    # [email protected] ML (except for
    # release-test)
    email_to = '*****@*****.**'
    if repo_name == 'openstack-release-test':
        email_to = '*****@*****.**'

    params = dict(sections)
    params.update({
        'project': repo,
        'description': description,
        'end_rev': end_revision,
        'range': git_range,
        'lib': repo_path,
        'skip_requirement_merges': skip_requirement_merges,
        'changes': changes,
        'requirement_changes': requirement_changes,
        'diff_stats': diff_stats,
        'change_header': "\n".join(change_header),
        'emotion': random.choice(EMOTIONS),
        'stable_series': is_stable,
        'series': series,
        'email': email,
        'email_from': email_from,
        'email_to': email_to,
        'email_reply_to': email_reply_to,
        'email_tags': email_tags,
        'reno_notes': reno_notes,
        'first_release': first_release,
        'publishing_dir_name': publishing_dir_name,
    })
    if include_pypi_link:
        params['pypi_url'] = PYPI_URL_TPL % repo_name
    else:
        params['pypi_url'] = None

    response = []
    if changes_only:
        response.append(expand_template(CHANGES_ONLY_TPL, params))
    else:
        if email:
            email_header = expand_template(EMAIL_HEADER_TPL.strip(), params)
            response.append(email_header.lstrip())
        if is_release_candidate:
            response.append(expand_template(RELEASE_CANDIDATE_TPL, params))
        else:
            header = expand_template(HEADER_RELEASE_TPL.strip(), params)
            response.append(parawrap.fill(header))
            response.append(expand_template(CHANGE_RELEASE_TPL, params))
    return '\n'.join(response)