| |
@@ -24,9 +24,11 @@
|
| |
import re
|
| |
import shlex
|
| |
import shutil
|
| |
+ import stat
|
| |
import subprocess
|
| |
import sys
|
| |
import tempfile
|
| |
+ import textwrap
|
| |
import time
|
| |
from itertools import groupby
|
| |
from multiprocessing.dummy import Pool as ThreadPool
|
| |
@@ -926,7 +928,7 @@
|
| |
|
| |
if specs:
|
| |
# Prefer the spec matching the directory name
|
| |
- self._spec = os.path.basename(self.layout.specdir) + '.spec'
|
| |
+ self._spec = os.path.basename(self.layout.root_dir) + '.spec'
|
| |
if specs != [self._spec]:
|
| |
if self._spec not in specs:
|
| |
self._spec = specs[0]
|
| |
@@ -1626,6 +1628,7 @@
|
| |
|
| |
if not bare_dir:
|
| |
self._add_git_excludes(os.path.join(path, git_dir))
|
| |
+ self._add_git_pre_push_hook(os.path.join(path, git_dir))
|
| |
|
| |
return
|
| |
|
| |
@@ -1711,6 +1714,7 @@
|
| |
|
| |
# Add excludes
|
| |
self._add_git_excludes(branch_path)
|
| |
+ self._add_git_pre_push_hook(branch_path)
|
| |
except (git.GitCommandError, OSError) as e:
|
| |
raise rpkgError('Could not locally clone %s from %s: %s'
|
| |
% (branch, repo_path, e))
|
| |
@@ -1768,11 +1772,51 @@
|
| |
for item in self.git_excludes:
|
| |
git_excludes.add(item)
|
| |
if not os.path.exists(git_excludes_path):
|
| |
- # prepare ".git/info" directory if is missing
|
| |
+ # prepare ".git/info" directory if it is missing
|
| |
os.makedirs(os.path.dirname(git_excludes_path))
|
| |
git_excludes.write()
|
| |
self.log.debug('Git-excludes patterns were added into %s' % git_excludes_path)
|
| |
|
| |
+ def _add_git_pre_push_hook(self, conf_dir):
|
| |
+ """
|
| |
+ Create pre-push hook script and write it in the location:
|
| |
+ <repository_directory>/.git/hooks/pre-push
|
| |
+ This hook script is run right before the 'git push' command.
|
| |
+ The script contains one command - 'pre-push-check' that checks
|
| |
+ for possible user mistakes.
|
| |
+ """
|
| |
+ tool_name = os.path.basename(sys.argv[0]) # rhpkg|fedpkg|...
|
| |
+ hook_content = textwrap.dedent("""
|
| |
+ #!/bin/bash
|
| |
+
|
| |
+ _remote="$1"
|
| |
+ _url="$2"
|
| |
+
|
| |
+ exit_code=0
|
| |
+ while read -r _local_ref local_sha _remote_ref _remote_sha
|
| |
+ do
|
| |
+ command -v {0} >/dev/null 2>&1 || {{ echo >&2 "Warning: '{0}' is missing, \\
|
| |
+ pre-push check is omitted. See .git/hooks/pre-push"; exit 0; }}
|
| |
+ {0} pre-push-check "$local_sha"
|
| |
+ ret_code=$?
|
| |
+ if [ $ret_code -ne 0 ] && [ $exit_code -eq 0 ]; then
|
| |
+ exit_code=$ret_code
|
| |
+ fi
|
| |
+ done
|
| |
+
|
| |
+ exit $exit_code
|
| |
+ """).strip().format(tool_name)
|
| |
+ git_pre_push_hook_path = os.path.join(conf_dir, '.git/hooks/pre-push')
|
| |
+ if not os.path.exists(os.path.dirname(git_pre_push_hook_path)):
|
| |
+ # prepare ".git/hooks" directory if it is missing
|
| |
+ os.makedirs(os.path.dirname(git_pre_push_hook_path))
|
| |
+ with open(git_pre_push_hook_path, 'w') as hook_file:
|
| |
+ hook_file.writelines(hook_content)
|
| |
+ # set script's permissions as executable
|
| |
+ file_stat = os.stat(git_pre_push_hook_path)
|
| |
+ os.chmod(git_pre_push_hook_path, file_stat.st_mode | stat.S_IEXEC)
|
| |
+ self.log.debug('Pre-push hook script was added into %s' % git_pre_push_hook_path)
|
| |
+
|
| |
def commit(self, message=None, file=None, files=[], signoff=False):
|
| |
"""Commit changes to a repository (optionally found at path)
|
| |
|
| |
@@ -2133,7 +2177,7 @@
|
| |
|
| |
return patches_not_tracked
|
| |
|
| |
- def push(self, force=False, extra_config=None):
|
| |
+ def push(self, force=False, no_verify=False, extra_config=None):
|
| |
"""Push changes to the remote repository"""
|
| |
self.check_repo(is_dirty=False, all_pushed=False)
|
| |
|
| |
@@ -2157,6 +2201,8 @@
|
| |
cmd.append('push')
|
| |
if force:
|
| |
cmd += ['-f']
|
| |
+ if no_verify:
|
| |
+ cmd += ['--no-verify']
|
| |
if self.quiet:
|
| |
cmd.append('-q')
|
| |
self._run_command(cmd, cwd=self.path)
|
| |
@@ -4364,3 +4410,105 @@
|
| |
% (self.repo_name))
|
| |
|
| |
return self._repo_name, version, release
|
| |
+
|
| |
+ def pre_push_check(self, ref):
|
| |
+ show_hint = ('Hint: this check (pre-push hook script) can be bypassed by adding '
|
| |
+ 'the argument \'--no-verify\' argument to the push command.')
|
| |
+ try:
|
| |
+ commit = self.repo.commit(ref)
|
| |
+ except Exception:
|
| |
+ self.log.error('Wrong reference to a commit: \'{0}\''.format(ref))
|
| |
+ sys.exit(1)
|
| |
+
|
| |
+ try:
|
| |
+ # Assume, that specfile names are same in the active branch
|
| |
+ # and in the pushed branch (git checkout f37 && git push origin rawhide)
|
| |
+ # in this case 'f37' is active branch and 'rawhide' is pushed branch.
|
| |
+ specfile_path_absolute = os.path.join(self.layout.specdir, self.spec)
|
| |
+ # convert to relative path
|
| |
+ specfile_path = os.path.relpath(specfile_path_absolute, start=self.path)
|
| |
+ spec_content = self.repo.git.cat_file("-p", "{0}:{1}".format(ref, specfile_path))
|
| |
+ except Exception:
|
| |
+ # It might be the case of an empty commit
|
| |
+ self.log.warning('Specfile doesn\'t exist. Push operation continues.')
|
| |
+ return
|
| |
+
|
| |
+ # load specfile content from pushed branch and save it into a temporary file
|
| |
+ with tempfile.NamedTemporaryFile(mode="w+") as temporary_spec:
|
| |
+ temporary_spec.write(spec_content)
|
| |
+ temporary_spec.flush()
|
| |
+ # get all source files from the specfile (including patches)
|
| |
+ cmd = ('spectool', '-l', temporary_spec.name)
|
| |
+ ret, stdout, _ = self._run_command(cmd, return_text=True, return_stdout=True)
|
| |
+ if ret != 0:
|
| |
+ self.log.error('Command \'{0}\' failed. Push operation '
|
| |
+ 'was cancelled.'.format(' '.join(cmd)))
|
| |
+ self.log.warning(show_hint)
|
| |
+ sys.exit(2)
|
| |
+
|
| |
+ source_files = []
|
| |
+ # extract source files from the spectool's output
|
| |
+ for line in stdout.split('\n'):
|
| |
+ file_location = re.sub(r'(?:Source|Patch)\d+\s*:\s*(\w+)', r'\1', line, re.IGNORECASE)
|
| |
+ if file_location:
|
| |
+ # find out the format of the source file path. From URL use just the file name.
|
| |
+ # We want to keep hierarchy of the files if possible
|
| |
+ res = urllib.parse.urlparse(file_location)
|
| |
+ if res.scheme and res.netloc:
|
| |
+ source_files.append(os.path.basename(res.path))
|
| |
+ else:
|
| |
+ source_files.append(file_location)
|
| |
+
|
| |
+ if not len(source_files):
|
| |
+ self.log.warning('No source files found in the specfile \'{0}\'. '
|
| |
+ 'Push operation continues.'.format(specfile_path))
|
| |
+
|
| |
+ try:
|
| |
+ sources_file_path_absolute = self.sources_filename
|
| |
+ # convert to relative path
|
| |
+ sources_file_path = os.path.relpath(sources_file_path_absolute, start=self.path)
|
| |
+ sources_file_content = self.repo.git.cat_file(
|
| |
+ '-p', '{0}:{1}'.format(ref, sources_file_path))
|
| |
+ except Exception:
|
| |
+ self.log.warning('\'sources\' file doesn\'t exist. Push operation continues.')
|
| |
+ # NOTE: check doesn't fail when 'sources' file doesn't exist. Just skips the rest.
|
| |
+ # it might be the case of the push without 'sources' = retiring the repository
|
| |
+ return
|
| |
+
|
| |
+ # load 'sources' file content from pushed branch and save it into a temporary file
|
| |
+ with tempfile.NamedTemporaryFile(mode="w+") as temporary_sources_file:
|
| |
+ temporary_sources_file.write(sources_file_content)
|
| |
+ temporary_sources_file.flush()
|
| |
+ # parse 'sources' files content
|
| |
+ sourcesf = SourcesFile(temporary_sources_file.name, self.source_entry_type)
|
| |
+ sourcesf_entries = set(item.file for item in sourcesf.entries)
|
| |
+
|
| |
+ # list of all files (their relative paths) in the commit
|
| |
+ repo_entries = set(item.path for item in commit.tree.traverse() if item.type != "tree")
|
| |
+
|
| |
+ # check whether every source file is either listed in the 'sources' file or tracked in git
|
| |
+ for source_file in source_files:
|
| |
+ listed = source_file in sourcesf_entries
|
| |
+ tracked = source_file in repo_entries
|
| |
+ if not (listed or tracked):
|
| |
+ self.log.error('Source file \'{0}\' was neither listed in the \'sources\' file '
|
| |
+ 'nor tracked in git. '
|
| |
+ 'Push operation was cancelled'.format(source_file))
|
| |
+ self.log.warning(show_hint)
|
| |
+ sys.exit(3)
|
| |
+
|
| |
+ # verify all file entries in 'sources' were uploaded to the lookaside cache
|
| |
+ for entry in sourcesf.entries:
|
| |
+ filename = entry.file
|
| |
+ hash = entry.hash
|
| |
+ file_exists_in_lookaside = self.lookasidecache.remote_file_exists(
|
| |
+ self.ns_repo_name if self.lookaside_namespaced else self.repo_name,
|
| |
+ filename,
|
| |
+ hash)
|
| |
+ if not file_exists_in_lookaside:
|
| |
+ self.log.error('Source file (or tarball) \'{}\' wasn\'t uploaded to the lookaside '
|
| |
+ 'cache. Push operation was cancelled.'.format(filename))
|
| |
+ self.log.warning(show_hint)
|
| |
+ sys.exit(4)
|
| |
+
|
| |
+ return 0 # The push operation continues
|
| |
This check should prevent unwanted pushing of incorrect
configuration. When a 'git push' command is executed, the git hook
'pre-push' script is activated. Checks available:
1. Is tarball/source file added to the 'sources' file?
2. Are files from 'sources' file uploaded into the lookaside cache?
JIRA: RHELCMP-10415
Fixes: https://pagure.io/fedpkg/issue/491
Relates: https://pagure.io/releng/issue/9955
Signed-off-by: Ondrej Nosek onosek@redhat.com