/usr/lib/python3/dist-packages/reno/scanner.py is in python3-reno 1.3.0-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | # 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 subprocess
import sys
from reno import utils
_TAG_PAT = re.compile('tag: ([\d\.]+)')
LOG = logging.getLogger(__name__)
def _get_current_version(reporoot, branch=None):
"""Return the current version of the repository.
If the repo appears to contain a python project, use setup.py to
get the version so pbr (if used) can do its thing. Otherwise, use
git describe.
"""
cmd = ['git', 'describe', '--tags']
if branch is not None:
cmd.append(branch)
try:
result = utils.check_output(cmd, cwd=reporoot).strip()
if '-' in result:
# Descriptions that come after a commit look like
# 2.0.0-1-abcde, and we want to remove the SHA value from
# the end since we only care about the version number
# itself, but we need to recognize that the change is
# unreleased so keep the -1 part.
result, dash, ignore = result.rpartition('-')
except subprocess.CalledProcessError:
# This probably means there are no tags.
result = '0.0.0'
return result
def get_file_at_commit(reporoot, filename, sha):
"Return the contents of the file if it exists at the commit, or None."
try:
return utils.check_output(
['git', 'show', '%s:%s' % (sha, filename)],
cwd=reporoot,
)
except subprocess.CalledProcessError:
return None
def _file_exists_at_commit(reporoot, filename, sha):
"Return true if the file exists at the given commit."
return bool(get_file_at_commit(reporoot, filename, sha))
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
# The git log output from _get_tags_on_branch() looks like this sample
# from the openstack/nova repository for git 1.9.1:
#
# git log --simplify-by-decoration --pretty="%d"
# (HEAD, origin/master, origin/HEAD, gerrit/master, master)
# (apu/master)
# (tag: 13.0.0.0b1)
# (tag: 12.0.0.0rc3, tag: 12.0.0)
#
# And this for git 1.7.1 (RHEL):
#
# $ git log --simplify-by-decoration --pretty="%d"
# (HEAD, origin/master, origin/HEAD, master)
# (tag: 13.0.0.0b1)
# (tag: 12.0.0.0rc3, tag: 12.0.0)
# (tag: 12.0.0.0rc2)
# (tag: 2015.1.0rc3, tag: 2015.1.0)
# ...
# (tag: folsom-2)
# (tag: folsom-1)
# (essex-1)
# (diablo-2)
# (diablo-1)
# (2011.2)
#
# The difference in the tags with "tag:" and without appears to be
# caused by some being annotated and others not.
#
# So we might have multiple tags on a given commit, and we might have
# lines that have no tags or are completely blank, and we might have
# "tag:" or not. This pattern is used to find the tag entries on each
# line, ignoring tags that don't look like version numbers.
TAG_RE = re.compile('(?:[(]|tag: )([\d.ab]+)[,)]')
def _get_version_tags_on_branch(reporoot, branch):
"""Return tags from the branch, in date order.
Need to get the list of tags in the right order, because the topo
search breaks the date ordering. Use git to ask for the tags in
order, rather than trying to sort them, because many repositories
have "non-standard" tags or have renumbered projects (from
date-based to SemVer), for which sorting would require complex
logic.
"""
tags = []
tag_cmd = [
'git', 'log',
'--simplify-by-decoration',
'--pretty="%d"',
]
if branch:
tag_cmd.append(branch)
LOG.debug('running %s' % ' '.join(tag_cmd))
tag_results = utils.check_output(tag_cmd, cwd=reporoot)
LOG.debug(tag_results)
for line in tag_results.splitlines():
LOG.debug('line %r' % line)
for match in TAG_RE.findall(line):
tags.append(match)
return tags
def get_notes_by_version(reporoot, notesdir, branch=None):
"""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.
"""
LOG.debug('scanning %s/%s (branch=%s)' % (reporoot, notesdir, branch))
# 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 = _get_version_tags_on_branch(reporoot, branch)
LOG.debug('versions by date %r' % (versions_by_date,))
versions = []
earliest_seen = collections.OrderedDict()
# 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. 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.
current_version = _get_current_version(reporoot, branch)
LOG.debug('current repository version: %s' % current_version)
if current_version not in versions_by_date:
LOG.debug('adding %s to versions by date' % current_version)
versions_by_date.insert(0, current_version)
# Remember the most current filename for each id, to allow for
# renames.
last_name_by_id = {}
# FIXME(dhellmann): This might need to be more line-oriented for
# longer histories.
log_cmd = [
'git', 'log',
'--topo-order', # force traversal order rather than date order
'--pretty=%x00%H %d', # output contents in parsable format
'--name-only' # only include the names of the files in the patch
]
if branch is not None:
log_cmd.append(branch)
LOG.debug('running %s' % ' '.join(log_cmd))
history_results = utils.check_output(log_cmd, cwd=reporoot)
history = history_results.split('\x00')
current_version = current_version
for h in history:
h = h.strip()
if not h:
continue
# print(h)
hlines = h.splitlines()
# The first line of the block will include the SHA and may
# include tags, the other lines are filenames.
sha = hlines[0].split(' ')[0]
tags = _TAG_PAT.findall(hlines[0])
# Filter the files based on the notes directory we were
# given. We cannot do this in the git log command directly
# because it means we end up skipping some of the tags if the
# commits being tagged don't include any release note
# files. Even if this list ends up empty, we continue doing
# the other processing so that we record all of the known
# versions.
filenames = [
f
for f in hlines[2:]
if fnmatch.fnmatch(f, notesdir + '/*.yaml')
]
# If there are no tags in this block, assume the most recently
# seen version.
if not tags:
tags = [current_version]
else:
current_version = tags[0]
LOG.debug('%s has tags, updating current version to %s' %
(sha, current_version))
# Remember each version we have seen.
if current_version not in versions:
LOG.debug('%s is a new version' % current_version)
versions.append(current_version)
LOG.debug('%s contains files %s' % (sha, filenames))
# Remember the files seen, using their UUID suffix as a unique id.
for f in filenames:
# Updated as older tags are found, handling edits to release
# notes.
LOG.debug('setting earliest reference to %s to %s' %
(f, tags[0]))
uniqueid = _get_unique_id(f)
earliest_seen[uniqueid] = tags[0]
if uniqueid in last_name_by_id:
# We already have a filename for this id from a
# new commit, so use that one in case the name has
# changed.
LOG.debug('%s was seen before' % f)
continue
if _file_exists_at_commit(reporoot, f, sha):
# Remember this filename as the most recent version of
# the unique id we have seen, in case the name
# changed from an older commit.
last_name_by_id[uniqueid] = (f, sha)
LOG.debug('remembering %s as filename for %s' % (f, uniqueid))
# Invert earliest_seen to make a list of notes files for each
# version.
files_and_tags = collections.OrderedDict()
for v in 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 earliest_seen.items():
try:
base, sha = last_name_by_id[uniqueid]
files_and_tags[version].append((base, sha))
except KeyError:
# Unable to find the file again, skip it to avoid breaking
# the build.
msg = ('[reno] unable to find file associated '
'with unique id %r, skipping') % uniqueid
LOG.debug(msg)
print(msg, file=sys.stderr)
# Only return the parts of files_and_tags that actually have
# filenames associated with the versions.
trimmed = collections.OrderedDict()
for ov in versions_by_date:
if not files_and_tags.get(ov):
continue
# 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])
return trimmed
|