From 6995a95f764efdddcffd4bd087126bdd83b26258 Mon Sep 17 00:00:00 2001 From: Stephen Smoogen Date: Mon, 25 Feb 2019 16:31:13 +0000 Subject: [PATCH] pagure-3.3.1 : add hotfix to allow for project requests https://pagure.io/pagure/pull-request/4295 --- .../frontend/files/hotfix-20190219-ui-fork.py | 2128 +++++++++++++++++ roles/pagure/frontend/tasks/main.yml | 11 + 2 files changed, 2139 insertions(+) create mode 100644 roles/pagure/frontend/files/hotfix-20190219-ui-fork.py diff --git a/roles/pagure/frontend/files/hotfix-20190219-ui-fork.py b/roles/pagure/frontend/files/hotfix-20190219-ui-fork.py new file mode 100644 index 0000000000..929ee3a2cd --- /dev/null +++ b/roles/pagure/frontend/files/hotfix-20190219-ui-fork.py @@ -0,0 +1,2128 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2014-2017 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +# pylint: disable=too-many-return-statements +# pylint: disable=too-many-branches +# pylint: disable=too-many-arguments +# pylint: disable=too-many-locals +# pylint: disable=too-many-statements +# pylint: disable=too-many-lines + + +from __future__ import unicode_literals, absolute_import + +import logging +import os +from math import ceil + +import flask +import pygit2 +from sqlalchemy.exc import SQLAlchemyError + +import pagure +import pagure.doc_utils +import pagure.exceptions +import pagure.lib.git +import pagure.lib.plugins +import pagure.lib.query +import pagure.lib.tasks +import pagure.forms +from pagure.config import config as pagure_config +from pagure.ui import UI_NS +from pagure.utils import ( + login_required, + __get_file_in_tree, + get_parent_repo_path, + is_true, +) + + +_log = logging.getLogger(__name__) + + +def _get_parent_request_repo_path(repo): + """ Return the path of the parent git repository corresponding to the + provided Repository object from the DB. + """ + if repo.parent: + return repo.parent.repopath("requests") + else: + return repo.repopath("requests") + + +@UI_NS.route("//pull-requests/") +@UI_NS.route("//pull-requests") +@UI_NS.route("///pull-requests/") +@UI_NS.route("///pull-requests") +@UI_NS.route("/fork///pull-requests/") +@UI_NS.route("/fork///pull-requests") +@UI_NS.route("/fork////pull-requests/") +@UI_NS.route("/fork////pull-requests") +def request_pulls(repo, username=None, namespace=None): + """ List all Pull-requests associated to a repo + """ + status = flask.request.args.get("status", "Open") + assignee = flask.request.args.get("assignee", None) + search_pattern = flask.request.args.get("search_pattern", None) + author = flask.request.args.get("author", None) + order = flask.request.args.get("order", "desc") + order_key = flask.request.args.get("order_key", "date_created") + + repo = flask.g.repo + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + total_open = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, status=True, count=True + ) + + total_merged = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, status="Merged", count=True + ) + + if status.lower() == "merged" or is_true(status, ["false", "0"]): + status_filter = "Merged" + requests = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status="Merged", + order=order, + order_key=order_key, + assignee=assignee, + author=author, + search_pattern=search_pattern, + offset=flask.g.offset, + limit=flask.g.limit, + ) + elif is_true(status, ["true", "1", "open"]): + status_filter = "Open" + requests = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status="Open", + order=order, + order_key=order_key, + assignee=assignee, + author=author, + search_pattern=search_pattern, + offset=flask.g.offset, + limit=flask.g.limit, + ) + elif status.lower() == "closed": + status_filter = "Closed" + requests = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status="Closed", + order=order, + order_key=order_key, + assignee=assignee, + author=author, + search_pattern=search_pattern, + offset=flask.g.offset, + limit=flask.g.limit, + ) + else: + status_filter = None + requests = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status=None, + order=order, + order_key=order_key, + assignee=assignee, + author=author, + search_pattern=search_pattern, + offset=flask.g.offset, + limit=flask.g.limit, + ) + + open_cnt = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status="Open", + assignee=assignee, + author=author, + search_pattern=search_pattern, + count=True, + ) + + merged_cnt = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status="Merged", + assignee=assignee, + author=author, + search_pattern=search_pattern, + count=True, + ) + + closed_cnt = pagure.lib.query.search_pull_requests( + flask.g.session, + project_id=repo.id, + status="Closed", + assignee=assignee, + author=author, + search_pattern=search_pattern, + count=True, + ) + + repo_obj = flask.g.repo_obj + if not repo_obj.is_empty and not repo_obj.head_is_unborn: + head = repo_obj.head.shorthand + else: + head = "master" + + total_page = 1 + if len(requests): + if status_filter == "Closed": + total_requests = closed_cnt + elif status_filter == "Merged": + total_requests = merged_cnt + elif status_filter == "Open": + total_requests = open_cnt + else: + total_requests = closed_cnt + merged_cnt + open_cnt + total_page = int(ceil(total_requests / float(flask.g.limit))) + + return flask.render_template( + "requests.html", + select="requests", + repo=repo, + username=username, + requests=requests, + open_cnt=open_cnt, + merged_cnt=merged_cnt, + closed_cnt=closed_cnt, + order=order, + order_key=order_key, + status=status, + status_filter=status_filter, + assignee=assignee, + author=author, + search_pattern=search_pattern, + head=head, + total_page=total_page, + total_open=total_open, + total_merged=total_merged, + ) + + +@UI_NS.route("//pull-request//") +@UI_NS.route("//pull-request/") +@UI_NS.route("///pull-request//") +@UI_NS.route("///pull-request/") +@UI_NS.route("/fork///pull-request//") +@UI_NS.route("/fork///pull-request/") +@UI_NS.route( + "/fork////pull-request//" +) +@UI_NS.route( + "/fork////pull-request/" +) +def request_pull(repo, requestid, username=None, namespace=None): + """ Create a pull request with the changes from the fork into the project. + """ + repo = flask.g.repo + + _log.info("Viewing pull Request #%s repo: %s", requestid, repo.fullname) + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if request.remote: + repopath = pagure.utils.get_remote_repo_path( + request.remote_git, request.branch_from + ) + parentpath = pagure.utils.get_repo_path(request.project) + else: + repo_from = request.project_from + parentpath = pagure.utils.get_repo_path(request.project) + repopath = parentpath + if repo_from: + repopath = pagure.utils.get_repo_path(repo_from) + + repo_obj = pygit2.Repository(repopath) + orig_repo = pygit2.Repository(parentpath) + + diff_commits = [] + diff = None + # Closed pull-request + if request.status != "Open": + commitid = request.commit_stop + try: + for commit in repo_obj.walk(commitid, pygit2.GIT_SORT_NONE): + diff_commits.append(commit) + if commit.oid.hex == request.commit_start: + break + except KeyError: + # This happens when repo.walk() cannot find commitid + pass + + if diff_commits: + # Ensure the first commit in the PR as a parent, otherwise + # point to it + start = diff_commits[-1].oid.hex + if diff_commits[-1].parents: + start = diff_commits[-1].parents[0].oid.hex + + # If the start and the end commits are the same, it means we are, + # dealing with one commit that has no parent, so just diff that + # one commit + if start == diff_commits[0].oid.hex: + diff = diff_commits[0].tree.diff_to_tree(swap=True) + else: + diff = repo_obj.diff( + repo_obj.revparse_single(start), + repo_obj.revparse_single(diff_commits[0].oid.hex), + ) + else: + try: + diff_commits, diff = pagure.lib.git.diff_pull_request( + flask.g.session, request, repo_obj, orig_repo + ) + except pagure.exceptions.PagureException as err: + flask.flash("%s" % err, "error") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not update this pull-request in the database", "error" + ) + + if diff: + diff.find_similar() + + form = pagure.forms.MergePRForm() + trigger_ci_pr_form = pagure.forms.TriggerCIPRForm() + + # we need to leave out all members of trigger_ci_conf that have + # "meta" set to False or meta["requires_project_hook_attr"] condition + # defined and it's not met + trigger_ci_conf = pagure_config["TRIGGER_CI"] + if not isinstance(trigger_ci_conf, dict): + trigger_ci_conf = {} + trigger_ci = {} + # make sure all the backrefs are set properly on repo + pagure.lib.plugins.get_enabled_plugins(repo) + for comment, meta in trigger_ci_conf.items(): + if not meta: + continue + cond = meta.get("requires_project_hook_attr", ()) + if cond and not pagure.utils.project_has_hook_attr_value(repo, *cond): + continue + trigger_ci[comment] = meta + + can_delete_branch = ( + pagure_config.get("ALLOW_DELETE_BRANCH", True) + and not request.remote_git + and pagure.utils.is_repo_committer( + request.project_from or request.project + ) + ) + return flask.render_template( + "repo_pull_request.html", + select="requests", + requestid=requestid, + repo=repo, + username=username, + repo_obj=repo_obj, + pull_request=request, + diff_commits=diff_commits, + diff=diff, + mergeform=form, + subscribers=pagure.lib.query.get_watch_list(flask.g.session, request), + tag_list=pagure.lib.query.get_tags_of_project(flask.g.session, repo), + can_delete_branch=can_delete_branch, + trigger_ci=trigger_ci, + trigger_ci_pr_form=trigger_ci_pr_form, + ) + + +@UI_NS.route("//pull-request/.patch") +@UI_NS.route("///pull-request/.patch") +@UI_NS.route("/fork///pull-request/.patch") +@UI_NS.route( + "/fork////pull-request/.patch" +) +def request_pull_patch(repo, requestid, username=None, namespace=None): + """ Returns the commits from the specified pull-request as patches. + """ + return request_pull_to_diff_or_patch( + repo, requestid, username, namespace, diff=False + ) + + +@UI_NS.route("//pull-request/.diff") +@UI_NS.route("///pull-request/.diff") +@UI_NS.route("/fork///pull-request/.diff") +@UI_NS.route( + "/fork////pull-request/.diff" +) +def request_pull_diff(repo, requestid, username=None, namespace=None): + """ Returns the commits from the specified pull-request as patches. + """ + return request_pull_to_diff_or_patch( + repo, requestid, username, namespace, diff=True + ) + + +def request_pull_to_diff_or_patch( + repo, requestid, username=None, namespace=None, diff=False +): + """ Returns the commits from the specified pull-request as patches. + + :arg repo: the `pagure.lib.model.Project` object of the current pagure + project browsed + :type repo: `pagure.lib.model.Project` + :arg requestid: the identifier of the pull-request to convert to patch + or diff + :type requestid: int + :kwarg username: the username of the user who forked then project when + the project viewed is a fork + :type username: str or None + :kwarg namespace: the namespace of the project if it has one + :type namespace: str or None + :kwarg diff: a boolean whether the data returned is a patch or a diff + :type diff: boolean + :return: the patch or diff representation of the specified pull-request + :rtype: str + + """ + repo = flask.g.repo + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if request.remote: + repopath = pagure.utils.get_remote_repo_path( + request.remote_git, request.branch_from + ) + parentpath = pagure.utils.get_repo_path(request.project) + else: + repo_from = request.project_from + parentpath = pagure.utils.get_repo_path(request.project) + repopath = parentpath + if repo_from: + repopath = pagure.utils.get_repo_path(repo_from) + + repo_obj = pygit2.Repository(repopath) + orig_repo = pygit2.Repository(parentpath) + + branch = repo_obj.lookup_branch(request.branch_from) + commitid = None + if branch: + commitid = branch.peel().hex + + diff_commits = [] + if request.status != "Open": + commitid = request.commit_stop + try: + for commit in repo_obj.walk(commitid, pygit2.GIT_SORT_NONE): + diff_commits.append(commit) + if commit.oid.hex == request.commit_start: + break + except KeyError: + # This happens when repo.walk() cannot find commitid + pass + else: + try: + diff_commits = pagure.lib.git.diff_pull_request( + flask.g.session, request, repo_obj, orig_repo, with_diff=False + ) + except pagure.exceptions.PagureException as err: + flask.flash("%s" % err, "error") + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + username=username, + repo=repo.name, + namespace=namespace, + ) + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not update this pull-request in the database", "error" + ) + + diff_commits.reverse() + patch = pagure.lib.git.commit_to_patch( + repo_obj, diff_commits, diff_view=diff + ) + + return flask.Response(patch, content_type="text/plain;charset=UTF-8") + + +@UI_NS.route( + "//pull-request//edit/", methods=("GET", "POST") +) +@UI_NS.route( + "//pull-request//edit", methods=("GET", "POST") +) +@UI_NS.route( + "///pull-request//edit/", + methods=("GET", "POST"), +) +@UI_NS.route( + "///pull-request//edit", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork///pull-request//edit/", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork///pull-request//edit", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork////pull-request//edit/", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork////pull-request//edit", + methods=("GET", "POST"), +) +@login_required +def request_pull_edit(repo, requestid, username=None, namespace=None): + """ Edit the title of a pull-request. + """ + + repo = flask.g.repo + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if request.status != "Open": + flask.abort(400, "Pull-request is already closed") + + if ( + not flask.g.repo_committer + and flask.g.fas_user.username != request.user.username + ): + flask.abort(403, "You are not allowed to edit this pull-request") + + form = pagure.forms.RequestPullForm() + if form.validate_on_submit(): + request.title = form.title.data.strip() + request.initial_comment = form.initial_comment.data.strip() + flask.g.session.add(request) + try: + flask.g.session.commit() + flask.flash("Pull request edited!") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not edit this pull-request in the database", "error" + ) + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + elif flask.request.method == "GET": + form.title.data = request.title + form.initial_comment.data = request.initial_comment + + return flask.render_template( + "pull_request_title.html", + select="requests", + request=request, + repo=repo, + username=username, + form=form, + ) + + +@UI_NS.route("//pull-request//comment", methods=["POST"]) +@UI_NS.route( + "//pull-request//comment//" + "/", + methods=("GET", "POST"), +) +@UI_NS.route( + "///pull-request//comment", + methods=["POST"], +) +@UI_NS.route( + "///pull-request//comment//" + "/", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork///pull-request//comment", + methods=["POST"], +) +@UI_NS.route( + "/fork///pull-request//comment/" + "//", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork////pull-request//" + "comment", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request//" + "comment///", + methods=("GET", "POST"), +) +@login_required +def pull_request_add_comment( + repo, + requestid, + commit=None, + filename=None, + row=None, + username=None, + namespace=None, +): + """ Add a comment to a commit in a pull-request. + """ + repo = flask.g.repo + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + is_js = flask.request.args.get("js", False) + tree_id = flask.request.args.get("tree_id") or None + + form = pagure.forms.AddPullRequestCommentForm() + form.commit.data = commit + form.filename.data = filename + form.requestid.data = requestid + form.row.data = row + form.tree_id.data = tree_id + + if form.validate_on_submit(): + comment = form.comment.data + + try: + trigger_ci = pagure_config["TRIGGER_CI"] + if isinstance(trigger_ci, dict): + trigger_ci = list(trigger_ci.keys()) + message = pagure.lib.query.add_pull_request_comment( + flask.g.session, + request=request, + commit=commit, + tree_id=tree_id, + filename=filename, + row=row, + comment=comment, + user=flask.g.fas_user.username, + trigger_ci=trigger_ci, + ) + flask.g.session.commit() + if not is_js: + flask.flash(message) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + if is_js: + return "error" + else: + flask.flash(str(err), "error") + + if is_js: + return "ok" + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + + if is_js and flask.request.method == "POST": + return "failed" + + return flask.render_template( + "pull_request_comment.html", + select="requests", + requestid=requestid, + repo=repo, + username=username, + commit=commit, + tree_id=tree_id, + filename=filename, + row=row, + form=form, + ) + + +@UI_NS.route( + "//pull-request//comment/drop", methods=["POST"] +) +@UI_NS.route( + "///pull-request//comment/drop", + methods=["POST"], +) +@UI_NS.route( + "/fork///pull-request//comment/drop", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request//" + "comment/drop", + methods=["POST"], +) +@login_required +def pull_request_drop_comment(repo, requestid, username=None, namespace=None): + """ Delete a comment of a pull-request. + """ + repo = flask.g.repo + + if not repo: + flask.abort(404, "Project not found") + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if flask.request.form.get("edit_comment"): + commentid = flask.request.form.get("edit_comment") + form = pagure.forms.EditCommentForm() + if form.validate_on_submit(): + return pull_request_edit_comment( + repo.name, requestid, commentid, username=username + ) + + form = pagure.forms.ConfirmationForm() + if form.validate_on_submit(): + + if flask.request.form.get("drop_comment"): + commentid = flask.request.form.get("drop_comment") + + comment = pagure.lib.query.get_request_comment( + flask.g.session, request.uid, commentid + ) + if comment is None or comment.pull_request.project != repo: + flask.abort(404, "Comment not found") + + if ( + flask.g.fas_user.username != comment.user.username + or comment.parent.status is False + ) and not flask.g.repo_committer: + flask.abort( + 403, + "You are not allowed to remove this comment from " + "this issue", + ) + + flask.g.session.delete(comment) + try: + flask.g.session.commit() + flask.flash("Comment removed") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.error(err) + flask.flash( + "Could not remove the comment: %s" % commentid, "error" + ) + + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + + +@UI_NS.route( + "//pull-request//comment//edit", + methods=("GET", "POST"), +) +@UI_NS.route( + "///pull-request//comment/" + "/edit", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork///pull-request//comment" + "//edit", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork////pull-request/" + "/comment//edit", + methods=("GET", "POST"), +) +@login_required +def pull_request_edit_comment( + repo, requestid, commentid, username=None, namespace=None +): + """Edit comment of a pull request + """ + is_js = flask.request.args.get("js", False) + + project = flask.g.repo + + if not project.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=project.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + comment = pagure.lib.query.get_request_comment( + flask.g.session, request.uid, commentid + ) + + if comment is None or comment.parent.project != project: + flask.abort(404, "Comment not found") + + if ( + flask.g.fas_user.username != comment.user.username + or comment.parent.status != "Open" + ) and not flask.g.repo_committer: + flask.abort(403, "You are not allowed to edit the comment") + + form = pagure.forms.EditCommentForm() + + if form.validate_on_submit(): + + updated_comment = form.update_comment.data + try: + message = pagure.lib.query.edit_comment( + flask.g.session, + parent=request, + comment=comment, + user=flask.g.fas_user.username, + updated_comment=updated_comment, + ) + flask.g.session.commit() + if not is_js: + flask.flash(message) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.error(err) + if is_js: + return "error" + else: + flask.flash( + "Could not edit the comment: %s" % commentid, "error" + ) + + if is_js: + return "ok" + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=project.name, + requestid=requestid, + ) + ) + + if is_js and flask.request.method == "POST": + return "failed" + + return flask.render_template( + "comment_update.html", + select="requests", + requestid=requestid, + repo=project, + username=username, + form=form, + comment=comment, + is_js=is_js, + ) + + +@UI_NS.route("//pull-request//reopen", methods=["POST"]) +@UI_NS.route( + "///pull-request//reopen", methods=["POST"] +) +@UI_NS.route( + "/fork///pull-request//reopen", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request//reopen", + methods=["POST"], +) +@login_required +def reopen_request_pull(repo, requestid, username=None, namespace=None): + """ Re-Open a pull request. + """ + form = pagure.forms.ConfirmationForm() + if form.validate_on_submit(): + + if not flask.g.repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=flask.g.repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if ( + not flask.g.repo_committer + and not flask.g.fas_user.username == request.user.username + ): + flask.abort( + 403, + "You are not allowed to reopen pull-request for this project", + ) + + try: + pagure.lib.query.reopen_pull_request( + flask.g.session, request, flask.g.fas_user.username + ) + except pagure.exceptions.PagureException as err: + flask.flash(str(err), "error") + + try: + flask.g.session.commit() + flask.flash("Pull request reopened!") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not update this pull-request in the database", "error" + ) + + else: + flask.flash("Invalid input submitted", "error") + + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo, + username=username, + namespace=namespace, + requestid=requestid, + ) + ) + + +@UI_NS.route( + "//pull-request//trigger-ci", methods=["POST"] +) +@UI_NS.route( + "///pull-request//trigger-ci", + methods=["POST"], +) +@UI_NS.route( + "/fork///pull-request//trigger-ci", + methods=["POST"], +) +@UI_NS.route( + ( + "/fork////pull-request/" + "/trigger-ci" + ), + methods=["POST"], +) +@login_required +def ci_trigger_request_pull(repo, requestid, username=None, namespace=None): + """ Trigger CI testing for a PR. + """ + form = pagure.forms.TriggerCIPRForm() + if not form.validate_on_submit(): + flask.flash("Invalid input submitted", "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo, + requestid=requestid, + username=username, + namespace=namespace, + ) + ) + + repo_obj = flask.g.repo + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo_obj.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + trigger_ci = pagure_config["TRIGGER_CI"] + if isinstance(trigger_ci, dict): + trigger_ci = list(trigger_ci.keys()) + pagure.lib.query.add_pull_request_comment( + flask.g.session, + request, + commit=None, + tree_id=None, + filename=None, + row=None, + comment=form.comment.data, + user=flask.g.fas_user.username, + notify=True, + notification=True, + trigger_ci=trigger_ci, + ) + + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo, + username=username, + namespace=namespace, + requestid=requestid, + ) + ) + + +@UI_NS.route("//pull-request//merge", methods=["POST"]) +@UI_NS.route( + "///pull-request//merge", methods=["POST"] +) +@UI_NS.route( + "/fork///pull-request//merge", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request//merge", + methods=["POST"], +) +@login_required +def merge_request_pull(repo, requestid, username=None, namespace=None): + """ Create a pull request with the changes from the fork into the project. + """ + + form = pagure.forms.MergePRForm() + if not form.validate_on_submit(): + flask.flash("Invalid input submitted", "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo, + requestid=requestid, + username=username, + namespace=namespace, + ) + ) + + repo = flask.g.repo + + _log.info( + "called merge_request_pull for repo: %s - requestid: %s", + repo.fullname, + requestid, + ) + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if not flask.g.repo_committer: + flask.abort( + 403, "You are not allowed to merge pull-request for this project" + ) + + if repo.settings.get("Only_assignee_can_merge_pull-request", False): + if not request.assignee: + flask.flash("This request must be assigned to be merged", "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + if request.assignee.username != flask.g.fas_user.username: + flask.flash("Only the assignee can merge this review", "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + + threshold = repo.settings.get("Minimum_score_to_merge_pull-request", -1) + if threshold > 0 and int(request.score) < int(threshold): + flask.flash( + "This request does not have the minimum review score necessary " + "to be merged", + "error", + ) + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + + if form.delete_branch.data: + if not pagure_config.get("ALLOW_DELETE_BRANCH", True): + flask.flash( + "This pagure instance does not allow branch deletion", "error" + ) + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + if not pagure.utils.is_repo_committer( + request.project_from or request.project + ): + flask.flash( + "You do not have permissions to delete the branch in the " + "source repo", + "error", + ) + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + if request.remote_git: + flask.flash("You can not delete branch in remote repo", "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + + _log.info("All checks in the controller passed") + + try: + if flask.request.form.get("comment"): + trigger_ci = pagure_config["TRIGGER_CI"] + if isinstance(trigger_ci, dict): + trigger_ci = list(trigger_ci.keys()) + message = pagure.lib.query.add_pull_request_comment( + flask.g.session, + request=request, + commit=None, + tree_id=None, + filename=None, + row=None, + comment=flask.request.form.get("comment"), + user=flask.g.fas_user.username, + trigger_ci=trigger_ci, + ) + flask.g.session.commit() + flask.flash(message) + + task = pagure.lib.tasks.merge_pull_request.delay( + repo.name, + namespace, + username, + requestid, + flask.g.fas_user.username, + delete_branch_after=form.delete_branch.data, + ) + return pagure.utils.wait_for_task( + task, + prev=flask.url_for( + "ui_ns.request_pull", + repo=repo.name, + namespace=namespace, + username=username, + requestid=requestid, + ), + ) + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash(str(err), "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo.name, + requestid=requestid, + username=username, + namespace=namespace, + ) + ) + except pygit2.GitError as err: + _log.info("GitError exception raised") + flask.flash("%s" % err, "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo.name, + requestid=requestid, + username=username, + namespace=namespace, + ) + ) + except pagure.exceptions.PagureException as err: + _log.info("PagureException exception raised") + flask.flash(str(err), "error") + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + repo=repo.name, + requestid=requestid, + username=username, + namespace=namespace, + ) + ) + + _log.info("All fine, returning") + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + repo=repo.name, + username=username, + namespace=namespace, + ) + ) + + +@UI_NS.route("//pull-request/close/", methods=["POST"]) +@UI_NS.route( + "///pull-request/close/", methods=["POST"] +) +@UI_NS.route( + "/fork///pull-request/close/", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request/close/", + methods=["POST"], +) +@login_required +def close_request_pull(repo, requestid, username=None, namespace=None): + """ Close a pull request without merging it. + """ + + form = pagure.forms.ConfirmationForm() + if form.validate_on_submit(): + + if not flask.g.repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=flask.g.repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if ( + not flask.g.repo_committer + and not flask.g.fas_user.username == request.user.username + ): + flask.abort( + 403, + "You are not allowed to close pull-request for this project", + ) + + pagure.lib.query.close_pull_request( + flask.g.session, request, flask.g.fas_user.username, merged=False + ) + try: + flask.g.session.commit() + flask.flash("Pull request canceled!") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not update this pull-request in the database", "error" + ) + + else: + flask.flash("Invalid input submitted", "error") + + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + repo=repo, + username=username, + namespace=namespace, + ) + ) + + +@UI_NS.route("//pull-request/refresh/", methods=["POST"]) +@UI_NS.route( + "///pull-request/refresh/", + methods=["POST"], +) +@UI_NS.route( + "/fork///pull-request/refresh/", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request/refresh/", + methods=["POST"], +) +@login_required +def refresh_request_pull(repo, requestid, username=None, namespace=None): + """ Refresh a remote pull request. + """ + + form = pagure.forms.ConfirmationForm() + if form.validate_on_submit(): + + if not flask.g.repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-requests found for this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=flask.g.repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if ( + not flask.g.repo_committer + and not flask.g.fas_user.username == request.user.username + ): + flask.abort( + 403, "You are not allowed to refresh this pull request" + ) + + task = pagure.lib.tasks.refresh_remote_pr.delay( + flask.g.repo.name, namespace, username, requestid + ) + return pagure.utils.wait_for_task( + task, + prev=flask.url_for( + "ui_ns.request_pull", + repo=flask.g.repo.name, + namespace=namespace, + username=username, + requestid=requestid, + ), + ) + else: + flask.flash("Invalid input submitted", "error") + + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=flask.g.repo.name, + requestid=requestid, + ) + ) + + +@UI_NS.route("//pull-request//update", methods=["POST"]) +@UI_NS.route( + "///pull-request//update", methods=["POST"] +) +@UI_NS.route( + "/fork///pull-request//update", + methods=["POST"], +) +@UI_NS.route( + "/fork////pull-request//update", + methods=["POST"], +) +@login_required +def update_pull_requests(repo, requestid, username=None, namespace=None): + """ Update the metadata of a pull-request. """ + repo = flask.g.repo + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-request allowed on this project") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, project_id=repo.id, requestid=requestid + ) + + if not request: + flask.abort(404, "Pull-request not found") + + if ( + not flask.g.repo_user + and flask.g.fas_user.username != request.user.username + ): + flask.abort(403, "You are not allowed to update this pull-request") + + form = pagure.forms.ConfirmationForm() + if form.validate_on_submit(): + tags = [ + tag.strip() + for tag in flask.request.form.get("tag", "").strip().split(",") + if tag.strip() + ] + + messages = set() + try: + # Adjust (add/remove) tags + msgs = pagure.lib.query.update_tags( + flask.g.session, + obj=request, + tags=tags, + username=flask.g.fas_user.username, + ) + messages = messages.union(set(msgs)) + + if flask.g.repo_user: + # Assign or update assignee of the ticket + msg = pagure.lib.query.add_pull_request_assignee( + flask.g.session, + request=request, + assignee=flask.request.form.get("user", "").strip() + or None, + user=flask.g.fas_user.username, + ) + if msg: + messages.add(msg) + + if messages: + # Add the comment for field updates: + not_needed = set(["Comment added", "Updated comment"]) + pagure.lib.query.add_metadata_update_notif( + session=flask.g.session, + obj=request, + messages=messages - not_needed, + user=flask.g.fas_user.username, + ) + messages.add("Metadata fields updated") + + flask.g.session.commit() + for message in messages: + flask.flash(message) + + except pagure.exceptions.PagureException as err: + flask.g.session.rollback() + flask.flash("%s" % err, "error") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash(str(err), "error") + + return flask.redirect( + flask.url_for( + "ui_ns.request_pull", + username=username, + namespace=namespace, + repo=repo.name, + requestid=requestid, + ) + ) + + +# Specific actions + + +@UI_NS.route("/do_fork/", methods=["POST"]) +@UI_NS.route("/do_fork//", methods=["POST"]) +@UI_NS.route("/do_fork/fork//", methods=["POST"]) +@UI_NS.route("/do_fork/fork///", methods=["POST"]) +@login_required +def fork_project(repo, username=None, namespace=None): + """ Fork the project specified into the user's namespace + """ + repo = flask.g.repo + + form = pagure.forms.ConfirmationForm() + if not form.validate_on_submit(): + flask.abort(400) + + if pagure.lib.query._get_project( + flask.g.session, + repo.name, + user=flask.g.fas_user.username, + namespace=namespace, + ): + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + repo=repo.name, + username=flask.g.fas_user.username, + namespace=namespace, + ) + ) + + try: + task = pagure.lib.query.fork_project( + session=flask.g.session, repo=repo, user=flask.g.fas_user.username + ) + + flask.g.session.commit() + return pagure.utils.wait_for_task( + task, + prev=flask.url_for( + "ui_ns.view_repo", + repo=repo.name, + username=username, + namespace=namespace, + _external=True, + ), + ) + except pagure.exceptions.PagureException as err: + flask.flash(str(err), "error") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + flask.flash(str(err), "error") + + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + repo=repo.name, + username=username, + namespace=namespace, + ) + ) + + +@UI_NS.route( + "//diff/../", + methods=("GET", "POST"), +) +@UI_NS.route( + "//diff/..", + methods=("GET", "POST"), +) +@UI_NS.route( + "///diff/../", + methods=("GET", "POST"), +) +@UI_NS.route( + "///diff/..", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork///diff/../", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork///diff/..", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork////diff/" + "../", + methods=("GET", "POST"), +) +@UI_NS.route( + "/fork////diff/" + "..", + methods=("GET", "POST"), +) +def new_request_pull( + repo, branch_to, branch_from, username=None, namespace=None +): + """ Create a pull request with the changes from the fork into the project. + """ + branch_to = flask.request.values.get("branch_to", branch_to) + project_to = flask.request.values.get("project_to") + + repo = flask.g.repo + + parent = repo + if repo.parent: + parent = repo.parent + + repo_obj = flask.g.repo_obj + + if not project_to: + parentpath = get_parent_repo_path(repo) + orig_repo = pygit2.Repository(parentpath) + else: + p_namespace = None + p_username = None + p_name = None + project_to = project_to.rstrip("/") + if project_to.startswith("fork/"): + tmp = project_to.split("fork/")[1] + p_username, left = tmp.split("/", 1) + else: + left = project_to + + if "/" in left: + p_namespace, p_name = left.split("/", 1) + else: + p_name = left + parent = pagure.lib.query.get_authorized_project( + flask.g.session, p_name, user=p_username, namespace=p_namespace + ) + if parent: + family = [ + p.url_path + for p in pagure.lib.query.get_project_family( + flask.g.session, repo + ) + ] + if parent.url_path not in family: + flask.abort( + 400, + "%s is not part of %s's family" + % (project_to, repo.url_path), + ) + orig_repo = pygit2.Repository(parent.repopath("main")) + else: + flask.abort(404, "No project found for %s" % project_to) + + if not parent.settings.get("pull_requests", True): + flask.abort(404, "No pull-request allowed on this project") + + if parent.settings.get( + "Enforce_signed-off_commits_in_pull-request", False + ): + flask.flash( + "This project enforces the Signed-off-by statement on all " + "commits" + ) + + try: + diff, diff_commits, orig_commit = pagure.lib.git.get_diff_info( + repo_obj, orig_repo, branch_from, branch_to + ) + except pagure.exceptions.PagureException as err: + flask.abort(400, str(err)) + + repo_committer = flask.g.repo_committer + + form = pagure.forms.RequestPullForm() + if form.validate_on_submit() and repo_committer: + try: + if parent.settings.get( + "Enforce_signed-off_commits_in_pull-request", False + ): + for commit in diff_commits: + if "signed-off-by" not in commit.message.lower(): + raise pagure.exceptions.PagureException( + "This repo enforces that all commits are " + "signed off by their author. " + ) + + if orig_commit: + orig_commit = orig_commit.oid.hex + + initial_comment = form.initial_comment.data.strip() or None + commit_start = commit_stop = None + if diff_commits: + commit_stop = diff_commits[0].oid.hex + commit_start = diff_commits[-1].oid.hex + request = pagure.lib.query.new_pull_request( + flask.g.session, + repo_to=parent, + branch_to=branch_to, + branch_from=branch_from, + repo_from=repo, + title=form.title.data, + initial_comment=initial_comment, + user=flask.g.fas_user.username, + commit_start=commit_start, + commit_stop=commit_stop, + ) + + try: + flask.g.session.commit() + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not register this pull-request in the database", + "error", + ) + + if not parent.is_fork: + url = flask.url_for( + "ui_ns.request_pull", + requestid=request.id, + username=None, + repo=parent.name, + namespace=namespace, + ) + else: + url = flask.url_for( + "ui_ns.request_pull", + requestid=request.id, + username=parent.user.user, + repo=parent.name, + namespace=namespace, + ) + + return flask.redirect(url) + except pagure.exceptions.PagureException as err: # pragma: no cover + # There could be a PagureException thrown if the flask.g.fas_user + # wasn't in the DB but then it shouldn't be recognized as a + # repo admin and thus, if we ever are here, we are in trouble. + flask.flash(str(err), "error") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + flask.flash(str(err), "error") + + if not flask.g.repo_committer: + form = None + + # if the pull request we are creating only has one commit, + # we automatically fill out the form fields for the PR with + # the commit title and bodytext + if len(diff_commits) == 1 and form: + form.title.data = diff_commits[0].message.strip().split("\n")[0] + form.initial_comment.data = diff_commits[0].message.partition("\n")[2] + + # Get the contributing templates from the requests git repo + contributing = None + requestrepopath = _get_parent_request_repo_path(repo) + if os.path.exists(requestrepopath): + requestrepo = pygit2.Repository(requestrepopath) + if not requestrepo.is_empty and not requestrepo.head_is_unborn: + commit = requestrepo[requestrepo.head.target] + contributing = __get_file_in_tree( + requestrepo, + commit.tree, + ["templates", "contributing.md"], + bail_on_tree=True, + ) + if contributing: + contributing, _ = pagure.doc_utils.convert_readme( + contributing.data, "md" + ) + + flask.g.branches = sorted(orig_repo.listall_branches()) + + if diff: + diff.find_similar() + + return flask.render_template( + "repo_new_pull_request.html", + select="requests", + repo=repo, + username=username, + orig_repo=orig_repo, + parent_branches=sorted(flask.g.repo_obj.listall_branches()), + diff_commits=diff_commits, + diff=diff, + form=form, + branch_to=branch_to, + branch_from=branch_from, + contributing=contributing, + parent=parent, + project_to=project_to, + ) + + +@UI_NS.route("//diff/remote/", methods=("GET", "POST")) +@UI_NS.route("//diff/remote", methods=("GET", "POST")) +@UI_NS.route("///diff/remote/", methods=("GET", "POST")) +@UI_NS.route("///diff/remote", methods=("GET", "POST")) +@UI_NS.route("/fork///diff/remote/", methods=("GET", "POST")) +@UI_NS.route("/fork///diff/remote", methods=("GET", "POST")) +@UI_NS.route( + "/fork////diff/remote/", methods=("GET", "POST") +) +@UI_NS.route( + "/fork////diff/remote", methods=("GET", "POST") +) +@login_required +def new_remote_request_pull(repo, username=None, namespace=None): + """ Create a pull request with the changes from a remote fork into the + project. + """ + confirm = flask.request.values.get("confirm", False) + + repo = flask.g.repo + + if pagure_config.get("DISABLE_REMOTE_PR", True): + flask.abort(404, "Remote pull-requests disabled on this server") + + if not repo.settings.get("pull_requests", True): + flask.abort(404, "No pull-request allowed on this project") + + if repo.settings.get("Enforce_signed-off_commits_in_pull-request", False): + flask.flash( + "This project enforces the Signed-off-by statement on all " + "commits" + ) + + orig_repo = flask.g.repo_obj + + form = pagure.forms.RemoteRequestPullForm() + if form.validate_on_submit(): + taskid = flask.request.values.get("taskid") + if taskid: + result = pagure.lib.tasks.get_result(taskid) + if not result.ready: + return pagure.utils.wait_for_task_post( + taskid, + form, + "ui_ns.new_remote_request_pull", + repo=repo.name, + username=username, + namespace=namespace, + ) + # Make sure to collect any exceptions resulting from the task + try: + result.get(timeout=0) + except Exception as err: + flask.abort(500, err) + + branch_from = form.branch_from.data.strip() + branch_to = form.branch_to.data.strip() + remote_git = form.git_repo.data.strip() + + repopath = pagure.utils.get_remote_repo_path(remote_git, branch_from) + if not repopath: + taskid = pagure.lib.tasks.pull_remote_repo.delay( + remote_git, branch_from + ) + return pagure.utils.wait_for_task_post( + taskid, + form, + "ui_ns.new_remote_request_pull", + repo=repo.name, + username=username, + namespace=namespace, + initial=True, + ) + + repo_obj = pygit2.Repository(repopath) + + try: + diff, diff_commits, orig_commit = pagure.lib.git.get_diff_info( + repo_obj, orig_repo, branch_from, branch_to + ) + except pagure.exceptions.PagureException as err: + flask.flash("%s" % err, "error") + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + username=username, + repo=repo.name, + namespace=namespace, + ) + ) + + if not confirm: + flask.g.branches = sorted(orig_repo.listall_branches()) + return flask.render_template( + "repo_new_pull_request.html", + select="requests", + repo=repo, + username=username, + orig_repo=orig_repo, + diff_commits=diff_commits, + diff=diff, + form=form, + branch_to=branch_to, + branch_from=branch_from, + remote_git=remote_git, + parent=repo, + ) + + try: + if repo.settings.get( + "Enforce_signed-off_commits_in_pull-request", False + ): + for commit in diff_commits: + if "signed-off-by" not in commit.message.lower(): + raise pagure.exceptions.PagureException( + "This repo enforces that all commits are " + "signed off by their author. " + ) + + if orig_commit: + orig_commit = orig_commit.oid.hex + + parent = repo + if repo.parent: + parent = repo.parent + + request = pagure.lib.query.new_pull_request( + flask.g.session, + repo_to=parent, + branch_to=branch_to, + branch_from=branch_from, + repo_from=None, + remote_git=remote_git, + title=form.title.data, + user=flask.g.fas_user.username, + ) + + if form.initial_comment.data.strip() != "": + pagure.lib.query.add_pull_request_comment( + flask.g.session, + request=request, + commit=None, + tree_id=None, + filename=None, + row=None, + comment=form.initial_comment.data.strip(), + user=flask.g.fas_user.username, + ) + + try: + flask.g.session.commit() + flask.flash("Request created") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.exception(err) + flask.flash( + "Could not register this pull-request in " "the database", + "error", + ) + + if not parent.is_fork: + url = flask.url_for( + "ui_ns.request_pull", + requestid=request.id, + username=None, + repo=parent.name, + namespace=namespace, + ) + else: + url = flask.url_for( + "ui_ns.request_pull", + requestid=request.id, + username=parent.user, + repo=parent.name, + namespace=namespace, + ) + + return flask.redirect(url) + except pagure.exceptions.PagureException as err: # pragma: no cover + # There could be a PagureException thrown if the + # flask.g.fas_user wasn't in the DB but then it shouldn't + # be recognized as a repo admin and thus, if we ever are + # here, we are in trouble. + flask.flash(str(err), "error") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + flask.flash(str(err), "error") + + flask.g.branches = sorted(orig_repo.listall_branches()) + if flask.request.method == "GET": + try: + branch_to = orig_repo.head.shorthand + except pygit2.GitError: + branch_to = "master" + else: + branch_to = form.branch_to.data.strip() + + return flask.render_template( + "remote_pull_request.html", + select="requests", + repo=repo, + username=username, + form=form, + branch_to=branch_to, + ) + + +@UI_NS.route( + "/fork_edit//edit//f/", + methods=["POST"], +) +@UI_NS.route( + "/fork_edit///edit//f/", + methods=["POST"], +) +@UI_NS.route( + "/fork_edit/fork///edit//" + "f/", + methods=["POST"], +) +@UI_NS.route( + "/fork_edit/fork////edit//" + "f/", + methods=["POST"], +) +@login_required +def fork_edit_file(repo, branchname, filename, username=None, namespace=None): + """ Fork the project specified and open the specific file to edit + """ + repo = flask.g.repo + + form = pagure.forms.ConfirmationForm() + if not form.validate_on_submit(): + flask.abort(400) + + if pagure.lib.query._get_project( + flask.g.session, + repo.name, + namespace=repo.namespace, + user=flask.g.fas_user.username, + ): + flask.flash("You had already forked this project") + return flask.redirect( + flask.url_for( + "ui_ns.edit_file", + username=flask.g.fas_user.username, + namespace=namespace, + repo=repo.name, + branchname=branchname, + filename=filename, + ) + ) + + try: + task = pagure.lib.query.fork_project( + session=flask.g.session, + repo=repo, + user=flask.g.fas_user.username, + editbranch=branchname, + editfile=filename, + ) + + flask.g.session.commit() + return pagure.utils.wait_for_task(task) + except pagure.exceptions.PagureException as err: + flask.flash(str(err), "error") + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + flask.flash(str(err), "error") + + return flask.redirect( + flask.url_for( + "ui_ns.view_repo", + repo=repo.name, + username=username, + namespace=namespace, + ) + ) + + +_REACTION_URL_SNIPPET = ( + "pull-request//comment//react" +) + + +@UI_NS.route("//%s/" % _REACTION_URL_SNIPPET, methods=["POST"]) +@UI_NS.route("//%s" % _REACTION_URL_SNIPPET, methods=["POST"]) +@UI_NS.route( + "///%s/" % _REACTION_URL_SNIPPET, methods=["POST"] +) +@UI_NS.route( + "///%s" % _REACTION_URL_SNIPPET, methods=["POST"] +) +@UI_NS.route( + "/fork///%s/" % _REACTION_URL_SNIPPET, methods=["POST"] +) +@UI_NS.route( + "/fork///%s" % _REACTION_URL_SNIPPET, methods=["POST"] +) +@UI_NS.route( + "/fork////%s/" % _REACTION_URL_SNIPPET, + methods=["POST"], +) +@UI_NS.route( + "/fork////%s" % _REACTION_URL_SNIPPET, + methods=["POST"], +) +@login_required +def pull_request_comment_add_reaction( + repo, requestid, commentid, username=None, namespace=None +): + repo = flask.g.repo + + form = pagure.forms.ConfirmationForm() + if not form.validate_on_submit(): + flask.abort(400, "CSRF token not valid") + + request = pagure.lib.query.search_pull_requests( + flask.g.session, requestid=requestid, project_id=repo.id + ) + + if not request: + flask.abort(404, "Comment not found") + + comment = pagure.lib.query.get_request_comment( + flask.g.session, request.uid, commentid + ) + + if "reaction" not in flask.request.form: + flask.abort(400, "Reaction not found") + + reactions = comment.reactions + r = flask.request.form["reaction"] + if not r: + flask.abort(400, "Empty reaction is not acceptable") + if flask.g.fas_user.username in reactions.get(r, []): + flask.abort(409, "Already posted this one") + + reactions.setdefault(r, []).append(flask.g.fas_user.username) + comment.reactions = reactions + flask.g.session.add(comment) + + try: + flask.g.session.commit() + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.error(err) + return "error" + + return "ok" diff --git a/roles/pagure/frontend/tasks/main.yml b/roles/pagure/frontend/tasks/main.yml index e51d277704..4cc619f33c 100644 --- a/roles/pagure/frontend/tasks/main.yml +++ b/roles/pagure/frontend/tasks/main.yml @@ -485,3 +485,14 @@ tags: - pagure - logrotate + +- name: hotfixes + copy: > + src=hotfix-20190219-ui-fork.py + dest=/usr/lib/python2.7/site-packages/pagure/ui/fork.py + owner=root group=root mode=0644 + tags: + - hotfix + - pagure + notify: + - restart pagure apache