#!/usr/bin/python2 """ git pre-commit hook for checking for problems with patches in RPM spec files It verifies that: - all patches are committed to git - all patches are applied in %prep - no unexpanded %patch macros exist in %prep If any of the above checks fail, the commit is aborted. """ # Copyright 2013 T.C. Hollingsworth # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. import os import re import rpm import subprocess import sys co = subprocess.check_output # lets check everything first and abort only at the end so packagers can fix # everything at once abort = False # stash any unstaged changes subprocess.call(['git', 'stash', '-q', '--keep-index']) # try and read the spec and abort if RPM can't parse it root = co(['git', 'rev-parse', '--show-toplevel']).strip() specfile = os.path.join(root, co(['fedpkg', 'gimmespec']).strip()) try: specobj = rpm.TransactionSet().parseSpec(specfile) except ValueError: sys.stderr.write('RPM is unable to parse the spec file, aborting commit!\n') sys.exit(1) spectxt = open(specfile).read() sources = { src[1]: src[0] for src in specobj.sources if src[2] == 1 } patches = { src[1]: src[0] for src in specobj.sources if src[2] == 2 } # make sure all patches are applied patchcalls = [ int(m) for m in re.findall(r'%\{?patch([0-9]+)\}?', spectxt, re.I) ] if re.search(r'(%\{?patches\}?|%autosetup)', spectxt) is None: for p in patches.iterkeys(): if p not in patchcalls: sys.stderr.write('Patch #{0} not applied in %prep\n'.format(p)) abort = True # make sure there are no unexpanded patch macros in %prep for p in patchcalls: if p not in patches: sys.stderr.write('Unexpanded patch #{0} macro in %prep\n'.format(p)) abort = True # make sure all sources and patches are committed committed_files = [ line.split('\t')[1] for line in co( ['git', 'ls-tree', '--full-tree', 'HEAD']).strip().split('\n') ] staged_files = co(['git', 'diff', '--cached', '--name-only']).strip().split('\n') source_files = [ line.split(' ')[1] for line in open( os.path.join(root, 'sources')).read().strip().split('\n') ] # could have dupes but we don't care all_files = committed_files + staged_files + source_files for num, url in sources.iteritems(): if os.path.basename(url) not in all_files: sys.stderr.write('Source #{0} not found in lookaside cache or git\n'.format(num)) abort = True for num, url in patches.iteritems(): if os.path.basename(url) not in all_files: sys.stderr.write('Patch #{0} not found in git or lookaside cache\n'.format(num)) abort = True # restore unstaged changes subprocess.call(['git', 'stash', 'pop', '-q']) # finally, abort the commit if that's what we want if abort: sys.exit(1)