123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- # -*- coding: utf-8 -*-
- """
- (c) 2014-2017 - Copyright Red Hat Inc
- Authors:
- Pierre-Yves Chibon <pingou@pingoured.fr>
- """
- from __future__ import absolute_import, unicode_literals
- import os
- import subprocess
- import sys
- import traceback
- import six
- import wtforms
- import pagure.lib.git
- import pagure.lib.query
- from pagure.config import config as pagure_config
- from pagure.exceptions import FileNotFoundException
- from pagure.lib.git_auth import get_git_auth_helper
- from pagure.lib.plugins import get_enabled_plugins
- class RequiredIf(wtforms.validators.DataRequired):
- """Wtforms validator setting a field as required if another field
- has a value.
- """
- def __init__(self, fields, *args, **kwargs):
- if isinstance(fields, six.string_types):
- fields = [fields]
- self.fields = fields
- super(RequiredIf, self).__init__(*args, **kwargs)
- def __call__(self, form, field):
- for fieldname in self.fields:
- nfield = form._fields.get(fieldname)
- if nfield is None:
- raise Exception('no field named "%s" in form' % fieldname)
- if bool(nfield.data):
- if (
- not field.data
- or isinstance(field.data, six.string_types)
- and not field.data.strip()
- ):
- if self.message is None:
- message = field.gettext("This field is required.")
- else:
- message = self.message
- field.errors[:] = []
- raise wtforms.validators.StopValidation(message)
- class BaseRunner(object):
- dbobj = None
- @classmethod
- def runhook(
- cls, session, username, hooktype, project, repotype, repodir, changes
- ):
- """Run a specific hook on a project.
- By default, this calls out to the pre_receive, update or post_receive
- functions as appropriate.
- Args:
- session (Session): Database session
- username (string): The user performing a push
- project (model.Project): The project this call is made for
- repotype (string): Value of lib.query.get_repotypes() indicating
- for which repo the current call is
- repodir (string): Directory where a clone of the specified repo is
- located. Do note that this might or might not be a writable
- clone.
- changes (dict): A dict with keys being the ref to update, values
- being a tuple of (from, to).
- For example: {'refs/heads/master': (hash_from, hash_to), ...}
- """
- if hooktype == "pre-receive":
- cls.pre_receive(
- session=session,
- username=username,
- project=project,
- repotype=repotype,
- repodir=repodir,
- changes=changes,
- )
- elif hooktype == "update":
- cls.update(
- session=session,
- username=username,
- project=project,
- repotype=repotype,
- repodir=repodir,
- changes=changes,
- )
- elif hooktype == "post-receive":
- cls.post_receive(
- session=session,
- username=username,
- project=project,
- repotype=repotype,
- repodir=repodir,
- changes=changes,
- )
- else:
- raise ValueError('Invalid hook type "%s"' % hooktype)
- @staticmethod
- def pre_receive(session, username, project, repotype, repodir, changes):
- """Run the pre-receive tasks of a hook.
- For args, see BaseRunner.runhook.
- """
- pass
- @staticmethod
- def update(session, username, project, repotype, repodir, changes):
- """Run the update tasks of a hook.
- For args, see BaseRunner.runhook.
- Note that the "changes" list has exactly one element.
- """
- pass
- @staticmethod
- def post_receive(session, username, project, repotype, repodir, changes):
- """Run the post-receive tasks of a hook.
- For args, see BaseRunner.runhook.
- """
- pass
- class BaseHook(object):
- """Base class for pagure's hooks."""
- name = None
- form = None
- description = None
- backref = None
- db_object = None
- # hook_type is not used in hooks that use a Runner class, as those can
- # implement run actions on whatever is useful to them.
- hook_type = "post-receive"
- runner = None
- @classmethod
- def set_up(cls, project):
- """Install the generic post-receive hook that allow us to call
- multiple post-receive hooks as set per plugin.
- """
- if project.is_on_repospanner:
- # If the project is on repoSpanner, there's nothing to set up,
- # as the hook script will be arranged by repo creation.
- return
- hook_files = os.path.join(
- os.path.dirname(os.path.realpath(__file__)), "files"
- )
- for repotype in pagure.lib.query.get_repotypes():
- repopath = project.repopath(repotype)
- if repopath is None:
- continue
- # Make sure the hooks folder exists
- hookfolder = os.path.join(repopath, "hooks")
- if not os.path.exists(hookfolder):
- os.makedirs(hookfolder)
- for hooktype in ("pre-receive", "update", "post-receive"):
- # Install the main hook file
- target = os.path.join(hookfolder, hooktype)
- if not os.path.exists(target):
- if os.path.islink(target):
- os.unlink(target)
- os.symlink(os.path.join(hook_files, "hookrunner"), target)
- @classmethod
- def base_install(cls, repopaths, dbobj, hook_name, filein):
- """Method called to install the hook for a project.
- :arg project: a ``pagure.model.Project`` object to which the hook
- should be installed
- :arg dbobj: the DB object the hook uses to store the settings
- information.
- """
- if cls.runner:
- # In the case of a new-style hook (with a Runner), there is no
- # need to copy any files into place
- return
- for repopath in repopaths:
- if not os.path.exists(repopath):
- raise FileNotFoundException("Repo %s not found" % repopath)
- hook_files = os.path.join(
- os.path.dirname(os.path.realpath(__file__)), "files"
- )
- # Make sure the hooks folder exists
- hookfolder = os.path.join(repopath, "hooks")
- if not os.path.exists(hookfolder):
- os.makedirs(hookfolder)
- # Install the hook itself
- hook_file = os.path.join(
- repopath, "hooks", cls.hook_type + "." + hook_name
- )
- if not os.path.exists(hook_file):
- os.symlink(os.path.join(hook_files, filein), hook_file)
- @classmethod
- def base_remove(cls, repopaths, hook_name):
- """Method called to remove the hook of a project.
- :arg project: a ``pagure.model.Project`` object to which the hook
- should be installed
- """
- for repopath in repopaths:
- if not os.path.exists(repopath):
- raise FileNotFoundException("Repo %s not found" % repopath)
- hook_path = os.path.join(
- repopath, "hooks", cls.hook_type + "." + hook_name
- )
- if os.path.exists(hook_path):
- os.unlink(hook_path)
- @classmethod
- def install(cls, *args):
- """In sub-classess, this can be used for installation of the hook.
- However, this is not required anymore for hooks with a Runner.
- This class is here as backwards compatibility.
- All args are ignored.
- """
- if not cls.runner:
- raise ValueError("BaseHook.install called for runner-less hook")
- @classmethod
- def remove(cls, *args):
- """In sub-classess, this can be used for removal of the hook.
- However, this is not required anymore for hooks with a Runner.
- This class is here as backwards compatibility.
- All args are ignored.
- """
- if not cls.runner:
- raise ValueError("BaseHook.remove called for runner-less hook")
- @classmethod
- def is_enabled_for(cls, project):
- """Determine if this hook should be run for given project.
- On some Pagure instances, some hooks should be run on all projects
- that fulfill certain criteria. It is therefore not necessary to keep
- database objects for them.
- If a hook's backref is set to None, this method is run to determine
- whether the hook should be run or not. These hooks also won't show
- up on settings page, since they can't be turned off.
- :arg project: The project to inspect
- :type project: pagure.lib.model.Project
- :return: True if this hook should be run on the given project,
- False otherwise
- """
- return False
- def run_project_hooks(
- session,
- username,
- project,
- hooktype,
- repotype,
- repodir,
- changes,
- is_internal,
- pull_request,
- ):
- """Function to run the hooks on a project
- This will first call all the plugins with a Runner on the project,
- and afterwards, for a non-repoSpanner repo, run all hooks/<hooktype>.*
- scripts in the repo.
- Args:
- session: Database session
- username (string): The user performing a push
- project (model.Project): The project this call is made for
- repotype (string): Value of lib.query.get_repotypes() indicating
- for which repo the currnet call is
- repodir (string): Directory where a clone of the specified repo is
- located. Do note that this might or might not be a writable
- clone.
- hooktype (string): The type of hook to run: pre-receive, update
- or post-receive
- changes (dict): A dict with keys being the ref to update, values being
- a tuple of (from, to).
- is_internal (bool): Whether this push originated from Pagure internally
- pull_request (model.PullRequest or None): The pull request whose merge
- is initiating this hook run.
- """
- debug = pagure_config.get("HOOK_DEBUG", False)
- # First we run dynamic ACLs
- authbackend = get_git_auth_helper()
- if is_internal and username == "pagure":
- if debug:
- print("This is an internal push, dynamic ACL is pre-approved")
- elif not authbackend.is_dynamic:
- if debug:
- print("Auth backend %s is static-only" % authbackend)
- elif hooktype == "post-receive":
- if debug:
- print("Skipping auth backend during post-receive")
- else:
- if debug:
- print(
- "Checking push request against auth backend %s" % authbackend
- )
- todeny = []
- for refname in changes:
- change = changes[refname]
- authresult = authbackend.check_acl(
- session,
- project,
- username,
- refname,
- is_update=hooktype == "update",
- revfrom=change[0],
- revto=change[1],
- is_internal=is_internal,
- pull_request=pull_request,
- repotype=repotype,
- repodir=repodir,
- )
- if debug:
- print(
- "Auth result for ref %s: %s"
- % (refname, "Accepted" if authresult else "Denied")
- )
- if not authresult:
- print(
- "Denied push for ref '%s' for user '%s'"
- % (refname, username)
- )
- todeny.append(refname)
- for toremove in todeny:
- del changes[toremove]
- if not changes:
- print("All changes have been rejected")
- sys.exit(1)
- # Now we run the hooks for plugins
- haderrors = False
- for plugin, _ in get_enabled_plugins(project):
- if not plugin.runner:
- if debug:
- print(
- "Hook plugin %s should be ported to Runner" % plugin.name
- )
- else:
- if debug:
- print("Running plugin %s" % plugin.name)
- try:
- plugin.runner.runhook(
- session=session,
- username=username,
- hooktype=hooktype,
- project=project,
- repotype=repotype,
- repodir=repodir,
- changes=changes,
- )
- except Exception as e:
- if hooktype != "pre-receive" or debug:
- traceback.print_exc()
- else:
- print(str(e))
- haderrors = True
- if project.is_on_repospanner:
- # We are done. We are not doing any legacy hooks for repoSpanner
- return
- hookdir = os.path.join(repodir, "hooks")
- if not os.path.exists(hookdir):
- return
- stdin = ""
- args = []
- if hooktype == "update":
- refname = six.next(six.iterkeys(changes))
- (revfrom, revto) = changes[refname]
- args = [refname, revfrom, revto]
- else:
- stdin = (
- "\n".join(
- [
- "%s %s %s" % (changes[refname] + (refname,))
- for refname in changes
- ]
- )
- + "\n"
- )
- stdin = stdin.encode("utf-8")
- if debug:
- print(
- "Running legacy hooks (if any) with args: %s, stdin: %s"
- % (args, stdin)
- )
- for hook in os.listdir(hookdir):
- # This is for legacy hooks, which create symlinks in the form of
- # "post-receive.$pluginname"
- if hook.startswith(hooktype + "."):
- hookfile = os.path.join(hookdir, hook)
- # By-pass all the old hooks that pagure may have created before
- # moving to the runner architecture
- if hook in pagure.lib.query.ORIGINAL_PAGURE_HOOK:
- continue
- if hook.endswith(".sample"):
- # Ignore the samples that Git inserts
- continue
- # Execute
- print(
- "Running legacy hook %s. "
- "Please ask your admin to port this to the new plugin "
- "format, as the current system will cease functioning "
- "in a future Pagure release" % hook
- )
- # Using subprocess.Popen rather than check_call so that stdin
- # can be passed without having to use a temporary file.
- proc = subprocess.Popen(
- [hookfile] + args, cwd=repodir, stdin=subprocess.PIPE
- )
- proc.communicate(stdin)
- ecode = proc.wait()
- if ecode != 0:
- print("Hook %s errored out" % hook)
- haderrors = True
- if haderrors:
- session.close()
- raise SystemExit(1)
- def extract_changes(from_stdin):
- """Extracts a changes dict from either stdin or argv
- Args:
- from_stdin (bool): Whether to use stdin. If false, uses argv
- """
- changes = {}
- if from_stdin:
- for line in sys.stdin:
- (oldrev, newrev, refname) = str(line).strip().split(str(" "), 2)
- if six.PY2:
- refname = refname.decode("utf-8")
- changes[refname] = (oldrev, newrev)
- else:
- (refname, oldrev, newrev) = sys.argv[1:]
- if six.PY2:
- refname = refname.decode("utf-8")
- changes[refname] = (oldrev, newrev)
- return changes
- def run_hook_file(hooktype):
- """Runs a specific hook by grabbing the changes and running functions.
- Args:
- hooktype (string): The name of the hook to run: pre-receive, update
- or post-receive
- """
- if pagure_config.get("NOGITHOOKS") or False:
- return
- if hooktype not in ("pre-receive", "update", "post-receive"):
- raise ValueError("Hook type %s not valid" % hooktype)
- changes = extract_changes(from_stdin=hooktype != "update")
- session = pagure.lib.model_base.create_session(pagure_config["DB_URL"])
- if not session:
- raise Exception("Unable to initialize db session")
- pushuser = os.environ.get("GL_USER")
- is_internal = os.environ.get("internal", False) == "yes"
- pull_request = None
- if "pull_request_uid" in os.environ:
- pull_request = pagure.lib.query.get_request_by_uid(
- session, os.environ["pull_request_uid"]
- )
- if pagure_config.get("HOOK_DEBUG", False):
- print("Changes: %s" % changes)
- gitdir = os.path.abspath(os.environ["GIT_DIR"])
- (
- repotype,
- username,
- namespace,
- repo,
- ) = pagure.lib.git.get_repo_info_from_path(gitdir)
- project = pagure.lib.query._get_project(
- session, repo, user=username, namespace=namespace
- )
- if not project:
- raise Exception(
- "Not able to find the project corresponding to: %s - %s - "
- "%s - %s" % (repotype, username, namespace, repo)
- )
- if pagure_config.get("HOOK_DEBUG", False):
- print("Running %s hooks for %s" % (hooktype, project.fullname))
- run_project_hooks(
- session,
- pushuser,
- project,
- hooktype,
- repotype,
- gitdir,
- changes,
- is_internal,
- pull_request,
- )
- session.close()
|