releases/openstack_releases/gitutils.py
Doug Hellmann d287e47cfc fix stable branch membership detection
The rules for stable branch releases need to be relaxed a bit to account
for projects that are late to create their branches and only do so after
the default branch has been updated. This change makes the rules more
flexible so that it is possible to release for a stable branch on master
as long as that stable branch does not exist. The primary use for this
is to release and then create the stable branch in the same patch.

Change-Id: I2d8f9abcf8ffa34e29eb7021830fd635e6e0f85f
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2017-05-22 13:36:28 -04:00

295 lines
9.7 KiB
Python

# All Rights Reserved.
#
# 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 os.path
import subprocess
from openstack_releases import links
# Disable warnings about insecure connections.
from requests.packages import urllib3
urllib3.disable_warnings()
CGIT_SHA_TEMPLATE = 'http://git.openstack.org/cgit/%s/commit/?id=%s'
CGIT_TAG_TEMPLATE = 'http://git.openstack.org/cgit/%s/tag/?h=%s'
def find_modified_deliverable_files():
"Return a list of files modified by the most recent commit."
results = subprocess.check_output(
['git', 'diff', '--name-only', '--pretty=format:', 'HEAD^']
).decode('utf-8')
filenames = [
l.strip()
for l in results.splitlines()
if l.startswith('deliverables/')
]
return filenames
def commit_exists(repo, ref):
"""Return boolean specifying whether the reference exists in the repository.
Uses a cgit query instead of looking locally to avoid cloning a
repository or having Depends-On settings in a commit message allow
someone to fool the check.
"""
url = CGIT_SHA_TEMPLATE % (repo, ref)
return links.link_exists(url)
def tag_exists(repo, ref):
"""Return boolean specifying whether the reference exists in the repository.
Uses a cgit query instead of looking locally to avoid cloning a
repository or having Depends-On settings in a commit message allow
someone to fool the check.
"""
url = CGIT_TAG_TEMPLATE % (repo, ref)
return links.link_exists(url)
def clone_repo(workdir, repo, ref=None):
"Check out the code."
dest = os.path.join(workdir, repo)
if not os.path.exists(dest):
cmd = [
'zuul-cloner',
'--workspace', workdir,
]
cache_dir = os.environ.get('ZUUL_CACHE_DIR', '/opt/git')
if cache_dir and os.path.exists(cache_dir):
cmd.extend(['--cache-dir', cache_dir])
cmd.extend([
'git://git.openstack.org',
repo,
])
subprocess.check_call(cmd)
# Force an update, just in case the local version is still out of
# date.
print('Updating newly cloned repository in %s' % dest)
subprocess.check_call(
['git', 'fetch', '-v', '--tags'],
cwd=dest,
)
# If we were given some sort of reference, check that out.
if ref:
print('Updating %s to %s' % (repo, ref))
subprocess.check_call(
['git', 'checkout', ref],
cwd=dest,
)
def sha_for_tag(workdir, repo, version):
"""Return the SHA for a given tag
"""
# git log 2.3.11 -n 1 --pretty=format:%H
try:
actual_sha = subprocess.check_output(
['git', 'log', str(version), '-n', '1', '--pretty=format:%H'],
cwd=os.path.join(workdir, repo),
stderr=subprocess.STDOUT,
).decode('utf-8')
actual_sha = actual_sha.strip()
except subprocess.CalledProcessError as e:
print('ERROR getting SHA for tag %r: %s [%s]' %
(version, e, e.output.strip()))
actual_sha = ''
return actual_sha
def _filter_branches(output):
"Strip garbage from branch list output"
return [
n
for n in output.strip().split()
if '/' in n or n == 'master'
]
def check_branch_sha(workdir, repo, series, master, sha):
"""Check if the SHA is in the targeted branch.
The SHA must appear on a stable/$series branch (if it exists) or
master (if stable/$series does not exist). It is up to the
reviewer to verify that releases from master are in a sensible
location relative to other existing branches.
We do not compare $series against the existing branches ordering
because that would prevent us from retroactively creating a stable
branch for a project after a later stable branch is created (i.e.,
if stable/N exists we could not create stable/N-1).
"""
remote_match = 'remotes/origin/stable/%s' % series
try:
containing_branches = _filter_branches(
subprocess.check_output(
['git', 'branch', '-a', '--contains', sha],
cwd=os.path.join(workdir, repo),
).decode('utf-8')
)
# If the patch is on the named branch, everything is fine.
if remote_match in containing_branches:
return True
# If the expected branch does not exist yet, this may be a
# late release attempt to create that branch or just a project
# that hasn't branched, yet, and is releasing from master for
# that series. Allow the release, as long as it is on the
# master branch.
all_branches = _filter_branches(
subprocess.check_output(
['git', 'branch', '-a'],
cwd=os.path.join(workdir, repo),
).decode('utf-8')
)
if (remote_match not in all_branches) and ('master' in containing_branches):
return True
# At this point we know the release is not from the required
# branch and it is not from master, which means it is the
# wrong branch and should not be allowed.
return False
except subprocess.CalledProcessError as e:
print('ERROR checking SHA on branch: %s [%s]' % (e, e.output.strip()))
return False
def check_ancestry(workdir, repo, old_version, sha):
"Check if the SHA is in the ancestry of the previous version."
try:
ancestors = subprocess.check_output(
['git', 'log', '--oneline', '--ancestry-path',
'%s..%s' % (old_version, sha)],
cwd=os.path.join(workdir, repo),
).decode('utf-8').strip()
return bool(ancestors)
except subprocess.CalledProcessError as e:
print('ERROR checking ancestry: %s [%s]' % (e, e.output.strip()))
return False
def get_latest_tag(workdir, repo, sha=None):
cmd = ['git', 'describe', '--abbrev=0']
if sha is not None:
cmd.append(sha)
try:
return subprocess.check_output(
cmd,
cwd=os.path.join(workdir, repo),
stderr=subprocess.STDOUT,
).decode('utf-8').strip()
except subprocess.CalledProcessError as e:
print('WARNING failed to retrieve latest tag: %s [%s]' %
(e, e.output.strip()))
return None
def get_branches(workdir, repo):
try:
output = subprocess.check_output(
['git', 'branch', '-a'],
cwd=os.path.join(workdir, repo),
stderr=subprocess.STDOUT,
).decode('utf-8').strip()
# Example output:
# * (no branch)
# master
# stable/mitaka
# stable/newton
# stable/ocata
# remotes/origin/HEAD -> origin/master
# remotes/origin/master
# remotes/origin/stable/mitaka
# remotes/origin/stable/newton
# remotes/origin/stable/ocata
results = []
for line in output.splitlines():
branch = line.strip().lstrip('*').strip()
if branch.startswith('('):
continue
if '->' in branch:
continue
results.append(branch)
return results
except subprocess.CalledProcessError as e:
print('ERROR failed to retrieve list of branches: %s [%s]' %
(e, e.output.strip()))
return []
def branches_containing(workdir, repo, ref):
try:
output = subprocess.check_output(
['git', 'branch', '-r', '--contains', ref],
cwd=os.path.join(workdir, repo),
stderr=subprocess.STDOUT,
).decode('utf-8').strip()
# Example output:
# origin/stable/ocata
results = []
for line in output.splitlines():
results.append(line.strip())
return results
except subprocess.CalledProcessError as e:
print('ERROR failed to retrieve list of branches containing %s: %s [%s]' %
(ref, e, e.output.strip()))
return []
def get_branch_base(workdir, repo, branch):
"Return SHA at base of branch."
# 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)^^!
#
# Determine the first parent.
cmd = [
'git',
'rev-list',
'--first-parent',
'^origin/{}'.format(branch),
'master',
]
try:
parents = subprocess.check_output(
cmd,
cwd=os.path.join(workdir, repo),
stderr=subprocess.STDOUT,
).decode('utf-8').strip()
except subprocess.CalledProcessError as e:
print('WARNING failed to retrieve branch base: %s [%s]' %
(e, e.output.strip()))
return None
parent = parents.splitlines()[-1]
# Now get the ^^! commit
cmd = [
'git',
'rev-list',
'{}^^!'.format(parent),
]
try:
return subprocess.check_output(
cmd,
cwd=os.path.join(workdir, repo),
stderr=subprocess.STDOUT,
).decode('utf-8').strip()
except subprocess.CalledProcessError as e:
print('WARNING failed to retrieve branch base: %s [%s]' %
(e, e.output.strip()))
return None