# 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